iOS 如何使用自定义 DNS 并保持正确的 TLS SNI?
数据小陈
结论
URLSession 没有公开接口可以为单个 session 指定 DNS resolver,也不能单独设置 TLS 的 SNI。将 URL 改为 https://<IP>,再设置 HTTP Host,无法解决 TLS 握手时的主机名问题:Host 是 TLS 建立后发送的 HTTP 头,SNI 则在握手过程中发送。SecPolicyCreateSSL(true, originalHost as CFString) 用于验证证书主机名,不会把连接的 SNI 改成域名。
如果应用必须将连接定向到 DoH 返回的 IP,同时保留域名作为 SNI 和证书校验目标,可以考虑使用 Network.framework 自行建立连接并实现 HTTP 传输。这不是给 URLSession 增加一项配置。应用或网络库还需要处理请求发送、响应解析、重定向和连接复用等工作。
使用 Network.framework 设置 TLS 主机名
连接目标可以设为解析得到的 IP,TLS server name 则设为原始域名。例如:
import Network
func makeConnection(ip: String, domain: String) -> NWConnection {
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_tls_server_name(
tlsOptions.securityProtocolOptions,
domain
)
let parameters = NWParameters(
tls: tlsOptions,
tcp: NWProtocolTCP.Options()
)
return NWConnection(
host: NWEndpoint.Host(ip),
port: 443,
using: parameters
)
}
连接建立后,还需要通过 TLS 连接发送 HTTP 请求,并将 Host 设为域名,例如:
GET /path HTTP/1.1
Host: example.com
Connection: close
设置 TLS server name 后,握手会使用 example.com,连接仍指向解析出的 IP。应保留系统的 TLS 证书验证;不能因为连接 IP 与证书域名不同,就关闭验证或无条件接受 trust。上面的代码只展示了连接参数,不是完整的 HTTP 客户端实现。实际应用还需处理请求与响应、超时、取消、重定向和连接生命周期。
更合适的处理顺序
- 先检查 DNS 答案。 DoH 查询可能返回 CNAME、多个 A 记录或 IPv6 的 AAAA 记录。不要默认
Answer?.first?.data一定是可直接连接的 IPv4 地址;还要处理无答案、解析失败和记录过期等情况。 - 优先修复域名解析配置。 如果问题来自域名记录、权威 DNS 或 CDN 配置,修复服务端 DNS 通常比在应用中固定 IP 更稳妥。如果有可用的备用域名,也可以将其配置为应用的访问地址,并确保该域名与证书匹配。
- 确实需要为单个应用自定义解析时,使用自定义传输层。 可以使用
Network.framework,或经过验证且支持指定解析结果和 TLS server name 的网络库。确认它能保留证书校验,并支持所需的 HTTP 功能。设置Host头不能让URLSession实现同样的效果。 - 如果要修改设备的系统 DNS,评估系统级方案。 Network Extension 的 DNS 配置属于系统网络配置,可能需要用户授权、相应 entitlement,并满足用途审核要求。它不是普通应用为某个
URLSession单独指定 resolver 的 API。是否适合具体应用,需要根据目标 iOS 版本和应用用途核对 Apple 当前要求。
注意事项
- 原示例中的 DoH 请求使用
type=A,只查询 IPv4 地址。如果目标网络依赖 IPv6,还需要考虑 AAAA 记录和连接选择。 - 手动将域名解析为 IP 会增加 DNS 缓存、TTL、故障切换和 CDN 调度的维护责任。不要长期固定 DoH 返回的地址。
- 不要通过跳过 TLS 验证来规避
-1200,否则无法可靠验证服务器身份。 - 如果无法承担自建 HTTP 传输的维护成本,优先在服务端或 DNS/CDN 层解决解析问题。如果问题只出现在特定 ISP,应先确认该域名的权威解析,以及该网络实际返回的记录。
备注:内容仅供参考。
