本文主要介紹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的認證校正機制,開發人員也可通過相應介面擴充或自訂認證校正,以適配業務需求。
HTTP/3支援
EMASCurl 提供了基於支援 QUIC 的
libcurl封裝版本 EMASCurl/HTTP3,開發人員可按需接入以使用 HTTP/3 能力,無需額外適配。
優點:
封裝了
libcurl的底層能力,API更符合iOS開發人員的使用習慣。通過DNS Hook機制與HTTPDNS輕鬆對接。
同時解決了SNI情境的網域名稱傳遞和認證校正問題,降低整合成本。
支援 HTTP/3,開箱即用,無需自行編譯適配。
缺點:
依賴第三方庫(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與認證校正 - 支援HTTP/3 | - 依賴第三方庫,需要注意相容與升級 - 特殊需求需閱讀源碼進行二次開發 |
開發人員應結合自身業務的多網域名稱需求、網路安全要求、相容性、維護成本以及對第三方庫的接受度進行綜合評估,選擇合適的接入方案,並在上線前充分測試網路請求的可用性和安全性。