Flutter 中 SNI 的处理
问题根源
Dart 的 HttpClient 底层用的是 dart:io 中的 SecureSocket,在 TLS 握手时会自动把 URL 中的 host 作为 SNI 发出去。你用 IP 替换了域名后,SNI 就变成了一个 IP 地址,服务端收到一个 IP 作为 SNI,大概率返回默认证书或直接断开连接。
Dart 层能做什么
SecureSocket.connect 有一个 host 参数,如果传的是 String 类型,Dart 会把它同时用作 SNI。但问题是 Dio 底层封装了 HttpClient,你没有直接操控 SecureSocket 的机会。
Dart 2.17+ 的 HttpClient 支持通过 ConnectionTask 自定义连接过程,理论上可以在这里插入自定义的 SecureSocket.connect 调用,手动指定 SNI 为原始域名、连接目标为 IP。但这个 API 比较底层,使用起来很脆弱,而且 Dio 的 HttpClientAdapter 并没有完全暴露这个能力。
实际业界怎么做
主流方案是走原生层处理,原因很简单:iOS 的 NSURLSession 和 Android 的 OkHttp 对 SNI 的控制能力远比 Dart 强。
iOS 端:通过 URLProtocol 拦截请求,或者直接用 CFNetwork 层的 API,可以在创建 TLS 连接时单独指定 SNI。阿里云的 HTTPDNS SDK 就是这么做的,它 hook 了网络层,在 TLS 握手前把 SNI 设回原始域名。
Android 端:OkHttp 提供了 Dns 接口,可以自定义域名解析逻辑。直接实现这个接口返回 HTTPDNS 解析的 IP,OkHttp 内部会自动处理好 SNI——因为它知道原始域名是什么,只是把 DNS 解析这一步交给了你。这是最优雅的方案。
落地路径
- 如果服务端只有一个站点(单域名单 IP):SNI 不重要,服务端只有一张证书,不需要靠 SNI 选证书。只需要处理证书校验就行,Dart 层的
badCertificateCallback就够了。 - 如果服务端多域名共享 IP:必须正确设置 SNI。这时候建议放弃在 Dart 层做 HTTPDNS,改为在 iOS/Android 原生层各自实现,通过 MethodChannel 或直接用原生的 HTTPDNS SDK,让原生网络库自己处理 SNI。
- 另一个思路:不替换 URL 中的域名,而是在原生层 hook DNS 解析过程。这样 Dart 层的代码完全不用改,TLS 握手时 SNI 自然是正确的域名,只是 DNS 解析走了 HTTPDNS 通道。iOS 上可以通过
CFNetwork的 DNS hook,Android 上就是 OkHttp 的Dns接口。
一句话总结
Dart 层对 SNI 的控制力不够,真正需要 HTTPDNS 的生产环境,在原生层 hook DNS 解析是最干净的方案——上层代码零改动,SNI、证书、Cookie 全部自然正确。