本文主要介紹HTTPS(含SNI)業務場景下在iOS端使用HTTPDNS時實現”IP直連“的解決方案。
當前最佳實踐文檔介紹在實際網絡請求場景中,如何使用HTTPDNS解析出的IP。關于HTTPDNS本身的解析服務,請先查看iOS SDK 開發手冊。
背景說明
1. HTTPS場景的特點
發送HTTPS請求首先要進行SSL/TLS握手,握手過程大致如下:
客戶端發起握手請求,攜帶隨機數、支持算法列表等參數。
服務端收到請求,選擇合適的算法,下發公鑰證書和隨機數。
客戶端對服務端證書進行校驗,并發送隨機數信息,該信息使用公鑰加密。
服務端通過私鑰獲取隨機數信息。
雙方根據以上交互的信息生成session ticket,用作該連接后續數據傳輸的加密密鑰。
上述過程中,和HTTPDNS有關的是第三步,客戶端需要驗證服務端下發的證書,驗證過程有以下兩個要點:
客戶端用本地保存的根證書解開證書鏈,確認服務端下發的證書是由可信任的機構頒發的。
客戶端需要檢查證書的domain域和擴展域,看是否包含本次請求的host。
如果上述兩點都校驗通過,就證明當前的服務端是可信任的,否則就是不可信任,應當中斷當前連接。
當客戶端使用HTTPDNS解析域名時,請求URL中的host會被替換成HTTPDNS解析出來的IP,所以在證書驗證的第2步,會出現domain不匹配的情況,導致SSL/TLS握手不成功。
2. 什么是SNI
SNI(Server Name Indication)是為了解決一個服務器使用多個域名和證書的SSL/TLS擴展。它的工作原理如下:
在連接到服務器建立SSL鏈接之前先發送要訪問站點的域名(Hostname)。
服務器根據這個域名返回一個合適的證書。
目前,大多數操作系統和瀏覽器都已經很好地支持SNI擴展,OpenSSL 0.9.8也已經內置這一功能。
上述過程中,當客戶端使用HTTPDNS解析域名時,請求URL中的host會被替換成HTTPDNS解析出來的IP,導致服務器獲取到的域名為解析后的IP,無法找到匹配的證書,只能返回默認的證書或者不返回,所以會出現SSL/TLS握手不成功的錯誤。
比如當你需要通過HTTPS訪問CDN資源時,CDN的站點往往服務了很多的域名,所以需要通過SNI指定具體的域名證書進行通信。
非SNI場景解決方案
針對“domain不匹配”問題,可以采用如下方案解決:hook證書校驗過程第2步,將IP直接替換成原來的域名,再執行證書驗證。
基于該方案發起網絡請求,若報出SSL校驗錯誤
,比如iOS系統報錯kCFStreamErrorDomainSSL, -9813; The certificate for this server is invalid
,請檢查應用場景是否為SNI(單IP多HTTPS域名)。
此示例針對NSURLSession接口。
/*
* NSURLSession
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler {
if (!challenge) {
return;
}
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential = nil;
/*
* 獲取原始域名信息。
*/
NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
if (!host) {
host = self.request.URL.host;
}
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
// 對于其他的challenges直接使用默認的驗證方案
completionHandler(disposition,credential);
}
SNI場景解決方案
SNI(單IP多HTTPS證書)場景下,iOS上層網絡庫NSURLSession
沒有提供接口進行SNI字段
的配置,因此需要Socket層級的底層網絡庫例如CFNetwork
,來實現IP直連網絡請求
適配方案。而基于CFNetwork的解決方案需要開發者考慮數據的收發、重定向、解碼、緩存等問題(CFNetwork是非常底層的網絡實現),希望開發者合理評估該場景的使用風險。
針對SNI場景,通過Socket層級的底層網絡庫實現網絡請求,有兩種主流方案:
自定義NSURLProtocol實現,基于CFNetwork完整實現HTTP請求邏輯,在其中hook證書校驗步驟。
使用基于原生支持設置SNI字段的更底層的庫,比如libcurl。
1. 自定義NSURLProtocol方案
整個實現比較復雜,我們在demo中提供了示例實現,可直接復用。參考 httpdns_ios_demo 中的 HttpDnsNSURLProtocolImpl.m 文件。
2. 其他底層網絡庫方案
以libcurl為例,libcurl / cURL至少7.18.1(2008年3月30日)在SNI支持下編譯一個 SSL/TLS 工具包,curl
中有一個--resolve
方法可以實現使用指定IP訪問HTTPS網站。
在iOS實現中,代碼如下:
// {HTTPS域名}:443:{IP地址}
NSString *curlHost = ...;
_hosts_list = curl_slist_append(_hosts_list, curlHost.UTF8String);
curl_easy_setopt(_curl, CURLOPT_RESOLVE, _hosts_list);
其中curlHost
形如:{HTTPS域名}:443:{IP地址}
_hosts_list
是結構體類型hosts_list
,可以設置多個IP與Host之間的映射關系。curl_easy_setopt
方法中傳入CURLOPT_RESOLVE
將該映射設置到 HTTPS 請求中。這樣就可以達到設置SNI的目的。