本文主要介绍 iOS 端 App 集成 HTTPDNS 时实现“IP 直连”的解决方案。关于 iOS 上如何集成 HTTPDNS,请先查看 iOS SDK 开发手册 。
1. 前言
在移动端网络环境中,DNS 劫持、LocalDNS 缓存污染等问题时常导致域名无法正确解析、网络请求失败。针对这些场景,阿里云 HTTPDNS 提供了可靠的域名递归解析服务,帮助移动 App 绕过本地 DNS 的潜在风险,提升网络请求的成功率和稳定性。
但是,在 iOS 平台上使用 HTTPDNS,需要将请求原本的域名替换为实际解析出来的 IP 再直接发起请求,这就可能引发额外的问题,尤其是在 HTTPS 和 SNI 等复杂场景下。因此,在集成 HTTPDNS 之前,需要对可能出现的问题和可行的解决方案有一个全面的认识,以便在业务中安全且正确地使用 HTTPDNS。
本文将介绍在 iOS 上使用 HTTPDNS 会遇到的主要问题,并给出在不同场景下的集成方案和各自的利弊,希望帮助开发者快速完成 HTTPDNS 的接入。
2. 在 iOS 上使用 HTTPDNS 会遇到哪些问题
在移动端应用中,如果我们将原始
URL
中的域名(如
example.com
)替换为
HTTPDNS
解析得到的
IP,往往会遇到以下几个方面的问题。之所以会产生这些问题,与
HTTPS
协议在不同层次(TLS/SSL
层和
HTTP
层)对
Host
字段的使用方式密切相关:
-
TLS/SSL 层: 在 HTTPS 场景下,客户端会先进行 TLS/SSL 握手,其中会使用到 URL 中的 Host 来完成以下任务:
-
证书校验 :验证服务器证书的域名(Common Name 或 Subject Alternative Name)是否与请求的 Host 一致。
-
SNI(Server Name Indication) :在建立 TLS 连接时,客户端会将所请求的域名信息(即 URL 中的 Host)发送给服务器,以便服务器返回对应的证书。
-
-
HTTP 层: 在完成 TLS 握手后,客户端会在 HTTP 请求的 Header 中包含
Host
字段,用于告诉服务器此请求对应的具体站点或资源。如果将 URL 中的域名替换为 IP,又未手动设置 HTTP Header 里的Host
,就可能让服务器无法识别要访问的实际域名,从而导致请求失败或返回异常内容。
根据上述 Host 在 HTTPS 协议栈各层的作用描述,可以看到,若只把 URL 中的域名直接替换为 HTTPDNS 解析出来的 IP,则会引起以下技术问题:
-
域名与证书不匹配 对于 HTTPS 请求,如果 URL 里直接使用 HTTPDNS 解析出来的 IP 作为请求的 Host,TLS 层面无法匹配到正确的证书域名(Common Name 或 SAN 扩展域名),会导致 SSL 握手失败。
-
SNI(Server Name Indication)问题 在 SNI 场景中,同一服务器 IP 可能对应多个域名的证书。如果客户端在 SSL 握手阶段没有传递正确的域名信息(只发送了 IP),服务端就无法返回匹配该域名的证书,导致 SSL 握手失败。由于 iOS 的高层网络 API(如
NSURLSession
)并未暴露直接配置 SNI 的接口,SNI 问题常难以通过简单方式解决。 -
Host 头与业务寻址 如果仅把 URL 中的域名替换为 IP,却忘记在 HTTP 请求头中显式设置
Host
为原始域名,那么在 HTTP 层服务器端可能无法识别具体的站点或资源。例如 CDN 场景下,服务器需要依赖 Host 字段来分发正确的内容,一旦 Host 为 IP,将导致服务异常。 -
底层网络库的选择 iOS 自带的高层 API(
NSURLSession
等)在定制 SNI 或手动证书校验方面扩展性较弱。如果开发者想自行处理 SNI 或修改 TLS 握手逻辑,就需要使用更底层的接口(如CFNetwork
、libcurl
等),但这会带来更高的开发和维护成本。
综上所述, 直接将 URL 中的域名替换为 HTTPDNS 解析出来的 IP,在 HTTPS 场景下会影响 TLS 层的证书校验与 SNI 传递,并在 HTTP 层可能导致 Host 头信息异常 。因此在 iOS 端整合 HTTPDNS,需要有针对性地解决这些问题,才能确保网络请求的可靠性与安全性。
3. 普通 HTTP 场景、HTTPS+非 SNI 场景接入方案
在针对
普通
HTTP
或
HTTPS + 非
SNI
这两种场景下,我们通常可以继续使用系统自带的
NSURLSession
以及常规的网络请求逻辑,只需做一些相对简单的处理即可。需要注意的是,
普通
HTTP
场景并不涉及
TLS
握手和证书校验;而
HTTPS + 非
SNI
场景需要考虑到证书校验,但可以通过在
NSURLSession
中
Hook
验证流程的方式来应对。
3.1 普通 HTTP 场景标题
对于 普通 HTTP 请求,网络链路中不存在 TLS/SSL 握手,也无需证书校验,因此集成 HTTPDNS 的核心操作仅在 HTTP 层进行:
-
替换请求 URL 中的 Host 为 HTTPDNS 解析出的 IP
-
例如原始请求 URL 为
http://example.com/api
,HTTPDNS 解析后得到 IP1.2.3.4
,则把 URL 修改为http://1.2.3.4/api
。
-
-
在 HTTP Header 中显式设置
Host
为原始域名-
若使用
NSMutableURLRequest
,可在请求头中添加request.allHTTPHeaderFields[@"Host"] = @"example.com";
确保服务器在应用层能够识别到正确的域名。
-
优点 :实现简单;只需在现有 HTTP 请求中替换 Host+设置 Header,开发量较低。
缺点 :仅适用于 HTTP 明文协议,无法解决 HTTPS 场景下的证书校验和 SNI 相关问题。
3.2 HTTPS + 非 SNI 场景标题
对于不使用
SNI
机制或仅包含少量固定域名的
HTTPS
站点(证书中只涵盖这些域名),可以在
NSURLSession
层通过以下方式来完成
HTTPDNS
接入与证书校验:
-
替换请求 URL 中的 Host 为 HTTPDNS 解析出的 IP
-
例如原始请求 URL 为
https://example.com/api
,HTTPDNS 解析后得到 IP1.2.3.4
,则把 URL 修改为https://1.2.3.4/api
。
-
-
在 HTTP Header 中显式设置
Host
为原始域名-
同理,可以在
NSMutableURLRequest
里设置request.allHTTPHeaderFields[@"Host"] = @"example.com";
。
-
-
Hook 证书校验过程
-
由于这是 HTTPS 请求,需要在 TLS 握手阶段进行证书校验。此时,若直接拿 IP 作为 Host 检查,就会出现 域名与证书不匹配 的问题。
-
可以在
NSURLSessionDelegate
的回调方法URLSession:didReceiveChallenge:completionHandler:
中,将系统获取到的serverTrust
进行验证时,改用原始域名(example.com
)替换掉 IP,从而通过证书校验。 -
代码示例:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { NSString *originalHost = [self getOriginalHostFromRequest:task.originalRequest]; SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; if ([self evaluateServerTrust:serverTrust forDomain:originalHost]) { // 证书校验通过 NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; completionHandler(NSURLSessionAuthChallengeUseCredential, credential); } else { // 证书校验失败,使用默认处理 completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } else { completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); - (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain { // 创建证书校验策略 NSMutableArray *policies = [NSMutableArray array]; if (domain) { [policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) domain)]; } else { [policies addObject:(__bridge_transfer id) SecPolicyCreateBasicX509()]; // 绑定校验策略到服务端的证书上 SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef) policies); // 评估当前serverTrust是否可信任,官方建议在result = kSecTrustResultUnspecified 或 kSecTrustResultProceed的情况下serverTrust可以被验证通过,https://developer.apple.com/library/ios/technotes/tn2232/_index.html // 关于SecTrustResultType的详细信息请参考SecTrust.h SecTrustResultType result; SecTrustEvaluate(serverTrust, &result); return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed); }
-
优点 :
-
不需要引入额外的第三方库,直接使用系统原生
NSURLSession
和证书校验逻辑. -
实现成本相对可控,适合不涉及 SNI 或只需少量域名证书的场景。
缺点 :
-
无法处理 SNI 场景。若在同一 IP 上部署多个域名(如 CDN 场景),则仍然会因服务器返回错误的证书而握手失败。
4. HTTPS+SNI 场景接入方案
针对
SNI(单
IP
多
HTTPS
域名)的场景,简单的
NSURLSession
方案中无法在
SSL
握手阶段发送正确的域名信息,导致握手失败。为了解决此问题,需要在更底层的
Socket
层级修改或指定
SNI
字段。常见的做法有以下三种:
4.1 自定义 NSURLProtocol 实现
iOS
允许开发者通过继承
NSURLProtocol
来拦截系统发起的网络请求,并在底层自行实现
HTTP/HTTPS
请求逻辑。可以基于
CFNetwork
或者
NSInputStream/NSOutputStream
等接口,手动完成所有网络操作:
-
拦截请求
-
在
canInitWithRequest:
方法中判断是否要拦截当前请求。 -
在
startLoading
方法里,将原始请求 URL 中的域名替换为 IP,并保留原始域名用于后续的证书校验和 SNI 设置。
-
-
设置 SNI
-
通过
CFStream
相关 API 或者SecureTransport
接口,指定kCFStreamSSLPeerName
为原始域名,这样在 SSL 握手阶段,底层会带上正确的域名信息。
-
-
证书校验
-
手动执行证书验证流程,确保证书中包含的域名与原始域名匹配。
-
优点 :不依赖第三方库,完全基于系统底层 API,灵活度高。
缺点 :实现成本较高,需要开发者手动处理重定向、Cookie、缓存、编码、流量统计等;不支持连接复用,性能一般;且维护风险大,升级系统或网络环境时需要额外适配。
如果需要参考示例,阿里云提供了
httpdns_ios_demo
中
HttpDnsNSURLProtocolImpl.m
的示例实现,可根据业务需求进行修改或复用。
4.2 自行使用 libcurl 实现网络请求
libcurl
是
C
语言实现的跨平台网络库,支持手动设置
SNI
字段,从而在
SSL
握手阶段传递正确的域名信息,以完成多域名共享同一
IP
的证书校验场景。大致流程如下:
-
解析域名,得到对应 IP
-
例如使用 HTTPDNS 提供的 API
resolveHostSyncNonBlocking:
等方法,获取到目标域名的 IP 地址。
-
-
设置 SNI 和 IP 映射
-
通过
CURLOPT_RESOLVE
或其他 API,把“域名:端口:解析到的 IP”映射写入到 curl 内部 DNS 缓存。 -
依然使用原始域名作为
CURLOPT_URL
的访问目标,保证 TLS 握手时会带上正确的域名信息。
-
-
证书校验
-
libcurl
默认开启证书校验,也可以根据需求使用相应的回调对证书进行更细粒度的检查。
-
下面是一个核心示例代码片段(伪代码),演示如何在 iOS 中通过 libcurl 使用 HTTPDNS 解析的结果并完成请求:
CURL *curl_handle = curl_easy_init();
if (curl_handle) {
// 例如从HTTPDNS得到IP = 1.2.3.4,目标域名 = example.com,端口 = 443
struct curl_slist *dnsResolve = NULL;
dnsResolve = curl_slist_append(dnsResolve, "example.com:443:1.2.3.4");
// 设置域名-IP映射
curl_easy_setopt(curl_handle, CURLOPT_RESOLVE, dnsResolve);
// 依然使用原始域名作为URL
curl_easy_setopt(curl_handle, CURLOPT_URL, "https://example.com");
// 开启SSL验证
curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 2L);
// 发起请求
CURLcode res = curl_easy_perform(curl_handle);
// 检查结果
if (res != CURLE_OK) {
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
// 清理
curl_easy_cleanup(curl_handle);
curl_slist_free_all(dnsResolve);
}
优点 :
-
成熟、稳定,支持丰富的协议,能适应复杂的网络环境;对 SNI 场景有内置支持。
缺点 :
-
需要将
libcurl
编译进 iOS 项目,使用纯 C 接口,对 Objective-C/Swift 项目的开发者有一定学习成本。 -
同时也要处理自定义的请求流程或自行封装 HTTP 逻辑(如 Cookie、重定向、缓存等)。
4.3 使用 EMASCurl
为降低在
iOS
上直接使用
libcurl
的接入门槛,阿里云
EMAS
团队提供了
EMASCurl
库,它对
libcurl
进行封装,并支持与
HTTPDNS
的直接对接。
-
安装与拦截
-
提供了两种主要使用方式: 1)拦截指定
NSURLSessionConfiguration
创建的NSURLSession
; 2)拦截系统全局[NSURLSession sharedSession]
。 -
API 接口请查询 github 上的 README 文件。
-
-
与 HTTPDNS 配合
-
实现
EMASCurlProtocolDNSResolver
协议即可将 HTTPDNS 解析结果交给 EMASCurl。 -
在
resolveDomain:
方法中调用[HttpDnsService resolveHostSyncNonBlocking:]
获取 IP 地址,然后返回给 EMASCurl 来完成后续的 SNI 设置和请求发送。
-
-
证书校验
-
EMASCurl 依赖
libcurl
的证书校验机制,开发者也可通过相应接口扩展或自定义证书校验,以适配业务需求。
-
优点:
-
封装了
libcurl
的底层能力,API 更符合 iOS 开发者的使用习惯。 -
通过 DNS Hook 机制与 HTTPDNS 轻松对接。
-
同时解决了 SNI 场景的域名传递和证书校验问题,降低集成成本。
缺点 :
-
依赖第三方库(EMASCurl+libcurl),需要注意兼容性、版本升级等。
-
对于非常复杂的 HTTP 特性或自定义需求,仍需要阅读和理解 EMASCurl 内部封装实现,以确保业务可用性。
-
在接入 EMASCurl 时,需要测试常见的 HTTP/HTTPS 特性(如重定向、Cookie、并发请求等)是否符合业务需求。
-
若业务对安全或网络性能有严格要求,需要评估 EMASCurl 在当前 iOS 系统版本下的表现。
-
确保在不同网络环境(Wi-Fi/蜂窝网络/代理等)下都能正常完成请求和握手。
5. 总结
根据业务中是否需要支持 SNI、多域名及证书校验等需求,可结合以下方案进行选择。下表对各方案进行对比:
方案 |
适用场景 |
优点 |
缺点 |
仅设置 Host 和 Header |
普通 HTTP 场景 |
- 集成成本最低 |
-仅适用于 HTTP 明文协议 |
NSURLSession + Hook 证书校验 (仍需设置 Host 和 Header) |
HTTPS+非 SNI 场景 |
- 集成成本低 - 沿用系统 API,无需额外库 |
- 不支持 SNI |
自定义 NSURLProtocol |
全部场景 需要更灵活的底层控制 |
- 完全基于系统底层 API - 自由度高 |
- 开发、维护成本高 - 无连接复用,性能一般 - 需要自行处理重定向、Cookie、缓存、编码等特殊场景 |
libcurl |
全部场景 跨平台或自定义 HTTP 流程 |
- 成熟、稳定 - 可设置 SNI 字段、丰富协议支持 - 证书校验可灵活扩展 |
- C 接口对 ObjC/Swift 开发者有一定学习成本 - 需要自行封装 Cookie、重定向、缓存等 |
EMASCurl |
全部场景 期望 iOS 端简单集成 |
- 对 libcurl 封装较好 - 与 HTTPDNS 对接简单 - 实现 SNI 与证书校验 |
- 依赖第三方库,需要注意兼容与升级 - 特殊需求需阅读源码进行二次开发 |
开发者应结合自身业务的 多域名需求 、 网络安全要求 、 兼容性 、 维护成本 以及 对第三方库的接受度 进行综合评估,选择合适的接入方案,并在上线前充分测试网络请求的可用性和安全性。