DNS
解析本質(zhì)上是localDNS
的解析想鹰,說白了,你給它一個域名购披,它返回給你一個IPlist
DNS(Domain Name System)
即域名解析系統(tǒng)杖挣,這個東西說對于開發(fā)者來說,應(yīng)該是沒有不知道的刚陡。說簡單點(diǎn)惩妇,這個系統(tǒng)的作用就是將域名解析成IP地址。我們的每一次網(wǎng)絡(luò)請求筐乳,如果是使用域名歌殃,那么就是進(jìn)行域名解析。
一個優(yōu)秀的域名服務(wù)應(yīng)該能夠滿足兩點(diǎn)要求:
-
能夠正確的返回IP地址
蝙云, -
就是能夠根據(jù)網(wǎng)絡(luò)情況返回所請求的域名最近的服務(wù)器IP
氓皱。
一: DNS解析路線
LocalDNS
一個DNS查詢,會先從本地緩存查找勃刨,如果沒有或者已經(jīng)過期波材,就從DNS服務(wù)器查詢,如果客戶端沒有主動設(shè)置DNS服務(wù)器身隐,一般是從服務(wù)商DNS服務(wù)器上查找廷区。這就出現(xiàn)了不可控。因?yàn)槿绻褂昧薎PS的LocalDNS域名服務(wù)器贾铝,那么基本都會或多或少地?zé)o法避免在有中國特色的互聯(lián)網(wǎng)環(huán)境中遭遇到各種域名被緩存隙轻、用戶跨網(wǎng)訪問緩慢等問題。
我們先來看看普通域名服務(wù)會有什么問題:
-
1. 域名劫持:
一些小服務(wù)商以及小地方的服務(wù)商非常喜歡干這個事情垢揩。根據(jù)騰訊給出的數(shù)據(jù)玖绿,DNS劫持率7%,惡意劫持率2%叁巨。網(wǎng)速給的劫持率是10-15%斑匪。- 把你的域名解析到競爭對手那里,然后哭死都不知道俘种,為什么流量下降了秤标。
- 在你的代碼當(dāng)中绝淡,插入廣告或者追蹤代碼。這就是為什么在淘寶或者百度搜索一下東西苍姜,很快就有人聯(lián)系你牢酵。
- 下載APK文件的時候,替換你的文件衙猪,下載一個其他應(yīng)用或者山寨應(yīng)用馍乙。
- 打開一個頁面,先跳轉(zhuǎn)到廣告聯(lián)盟垫释,然后跳轉(zhuǎn)到這個頁面丝格。無緣無故多花廣告錢,以及對運(yùn)營的誤導(dǎo)棵譬。
2.智能DNS策略失效
智能DNS
显蝌,就是為了調(diào)度用戶訪問策略,但是這些因素會導(dǎo)致智能DNS策略失效订咸。
- 小運(yùn)營商曼尊,沒有DNS服務(wù)器,直接調(diào)用別的服務(wù)商脏嚷,導(dǎo)致服務(wù)商識別錯誤骆撇,直接跨網(wǎng)傳輸,速度大大下降父叙。
- 服務(wù)商多長NAT恋腕,實(shí)際IP莺掠,獲得不了,結(jié)果沒有就近訪問宇植。
- 一些運(yùn)營商將IP設(shè)置到開卡地布疼,即使漫游到其他地方城豁,結(jié)果也是沒有就近訪問逗抑。
目前國內(nèi)大多數(shù)企業(yè)對于域名解析這塊問題沒有進(jìn)行特殊處理镊绪,這導(dǎo)致了上述說的那些問題,其中域名劫持的問題相當(dāng)普遍带欢。那么有沒有一種方法能夠避免上述的情況呢?有烤惊,當(dāng)然有乔煞。那就是使用HTTPDNS
。
HttpDNS
其實(shí)也是對DNS
解析的另一種實(shí)現(xiàn)方式柒室,只是將域名解析的協(xié)議由DNS協(xié)議換成了Http協(xié)議渡贾,并不復(fù)雜。使用HTTP
協(xié)議向D+
服務(wù)器的80
端口進(jìn)行請求雄右,代替?zhèn)鹘y(tǒng)的DNS協(xié)議向DNS服務(wù)器的53端口進(jìn)行請求空骚,繞開了運(yùn)營商的Local DNS
纺讲,從而避免了使用運(yùn)營商Local DNS
造成的劫持和跨網(wǎng)問題。
接入HttpDNS
也是很簡單的囤屹,使用普通DNS時熬甚,客戶端發(fā)送網(wǎng)絡(luò)請求時,就直接發(fā)送出去了肋坚,有底層網(wǎng)絡(luò)框架進(jìn)行域名解析乡括。當(dāng)接入HttpDNS
時,就需要自己發(fā)送域名解析的HTTP
請求智厌,當(dāng)客戶端拿到域名對應(yīng)的IP
之后诲泌,就向直接往此IP發(fā)送業(yè)務(wù)協(xié)議請求。
這樣铣鹏,就再也不用再考慮傳統(tǒng)DNS解析會帶來的那些問題了敷扫,因?yàn)槭鞘褂肏TTP協(xié)議,所以不用擔(dān)心域名劫持問題了诚卸;而且葵第,如果選擇好的DNS服務(wù)器提供商,還保證將用戶引導(dǎo)的訪問速度最快的IDC節(jié)點(diǎn)上惨险。
HTTPDNS
是使用http
請求替換域名解析的過程羹幸,但一般這個http請求
都是基于https
的,且是IP直連
的辫愉,這樣我們就保證了栅受,這個解析域名的http
請求不會被劫持并且內(nèi)容安全.
二:HTTPDNS服務(wù)
我們一般在客戶端上做HTTPDNS
服務(wù)的解決方案的時候,策略可簡單可復(fù)雜恭朗,但大體要圍繞以下幾個問題:
- 1.數(shù)據(jù)要預(yù)先獲取屏镊,
- 2.運(yùn)營商變化的時候要更新數(shù)據(jù),
- 3.為了提升獲取ip的成功率要有過期數(shù)據(jù)的預(yù)取策略痰腮,
- 4.當(dāng)由于種種原因而芥,獲取不到
HTTPDNS
的IP
時,要降級為獲取localDNS
的IP
iOS
網(wǎng)絡(luò)庫沒有Android
的網(wǎng)絡(luò)庫訂制的那么深度膀值,其實(shí)就是AFNetworking
和okhttp
的差距棍丐,所以針對HTTPDNS
的網(wǎng)路庫適配(才是今天的最佳實(shí)踐)iOS
會更加原始一些,它處理的問題大致有以下幾類:
域名替換IP
沧踏,防止劫持的關(guān)鍵就是不采用域名請求歌逢,取而代之的是IP直連
**
https
的處理,由于替換了IP
翘狱,https
可信任域名檢驗(yàn)機(jī)制獲取不到域名**HTTP Proxy
的處理秘案,當(dāng)iOS網(wǎng)絡(luò)切換成HTTP
代理后,由于替換了IP會導(dǎo)致連接失敗,一般的處理方式是關(guān)閉HTTPDNS
服務(wù)Cookie
的處理阱高,由于替換了IP赚导,導(dǎo)致Cookie
的domain
獲取不到,從而使Cookie
失效IPV6
的處理赤惊,蘋果在2016年強(qiáng)制推行的IPV6
吼旧,由于我們切換成IP直連的方式,所以會在iOS8.4
以下的版本荐捻,IPV6-only
的網(wǎng)絡(luò)環(huán)境下連接失敗黍少,蘋果官方的建議采用getAddressInfo
方法解決這個問題,但這個方法在iOS9.2后才支持將一個IPV4的IP轉(zhuǎn)成一個IPV6的IP处面,所以我們一般會在這種情況下降級UIWebView/WKWebView
的處理厂置,不管是哪種WebView
,我們都可以采用蘋果的黑科技NSURLProtocol
進(jìn)行網(wǎng)絡(luò)層面的攔截魂角,從而接管WebView的網(wǎng)絡(luò)能力昵济,從而支持WebView
的DNS
反劫持
iOS端
的網(wǎng)絡(luò)層是基于AFNetworking
進(jìn)行封裝實(shí)現(xiàn)的,iOS
端的網(wǎng)絡(luò)框架NSURLSession
沒有提供DNS解析相關(guān)的接口供使用者進(jìn)行自定義修改DNS
解析結(jié)果野揪,因此在iOS端接入HTTPDNS
有幾個通用的問題需要處理访忿,如請求的URL的域名替換為IP地址、請求頭中設(shè)置原始HOST斯稳、SSL證書校驗(yàn)處理海铆、Cookie問題處理、重定向挣惰、SNI場景下的問題處理卧斟,以及對應(yīng)的SNI場景下的數(shù)據(jù)編解碼和鏈接復(fù)用等問題,上述這些問題都需要有一個統(tǒng)一的解決方案憎茂。
因此珍语,我們在騰訊云HTTPDNS
的SDK作為提供HTTPDNS
的基礎(chǔ)能力之上,單獨(dú)封裝了iOS端HTTPDNS
的接入層SDK竖幔,主要用來實(shí)現(xiàn)一些定制的策略和解決上述問題板乙,同時也方便后續(xù)更換SDK或者接入自部署的HTTPDNS
方案,讓上層各業(yè)務(wù)方能夠無感知底層HTTPDNS
服務(wù)的存在拳氢,減少業(yè)務(wù)入侵性募逞。
iOS端接入層SDK架構(gòu)圖如下圖所示:
接口層
接口層提供的部分接口:
// 開啟HTTPDNS服務(wù)
- (void)startHTTPDNS;
// 白名單列表,如果設(shè)置了白名單馋评,則只有在白名單內(nèi)域名走h(yuǎn)ttpdns服務(wù)
@property (nonatomic, copy) NSArray *whiteDomainList;
// 黑名單列表凡辱,如果設(shè)置了黑名單,黑名單內(nèi)域名都不走h(yuǎn)ttpdns栗恩,黑名單的優(yōu)先級最高
@property (nonatomic, copy) NSArray *blackDomainList;
// 是否允許緩存ip,允許緩存的情況下,在通過第三方服務(wù)無法獲取ip的情況下磕秤,允許使用上次解析成功的ip進(jìn)行請求乳乌,默認(rèn)YES
@property (nonatomic, assign) BOOL enableCachedIP;
策略層
策略層主要提供不同的策略組合和配置,能夠使得SDK能夠穩(wěn)定的對外提供HTTPDNS
服務(wù)市咆,下面簡單介紹一下每個策略的內(nèi)容:
容災(zāi)策略:SDK
內(nèi)部優(yōu)先使用HTTPDNS
服務(wù)汉操,當(dāng)HTTPDNS
服務(wù)不可用時,即無法獲得有效ip時蒙兰,服務(wù)自動降級為運(yùn)營商的LocalDNS
服務(wù)磷瘤,確保不受HTTPDNS
服務(wù)不可用時導(dǎo)致系統(tǒng)故障無法發(fā)出網(wǎng)絡(luò)請求。注:目前階段沒有接入內(nèi)置ip策略搜变,后續(xù)會考慮
采缚。
黑白名單策略:APP內(nèi)的網(wǎng)絡(luò)請求域名眾多,目前并不是所有的網(wǎng)絡(luò)請求都走HTTPDNS
服務(wù)挠他,設(shè)置了白名單或者黑名單后扳抽,會根據(jù)黑白名單中的域名去執(zhí)行HTTPDNS
,如果設(shè)置了白名單殖侵,則只有白名單內(nèi)的域名走HTTPDNS
服務(wù)贸呢;如果設(shè)置了黑名單,黑名單內(nèi)的域名不走HTTPDNS
服務(wù)拢军,黑名單的優(yōu)先級高于白名單楞陷。
緩存策略:緩存策略
除了基礎(chǔ)服務(wù)層中騰訊云HTTPDNS
SDK提供的基于TTL
的緩存策略外,我們自己封裝的接入層SDK中還存在一份內(nèi)存緩存和本地化持久緩存茉唉,持久化緩存主要用來解決啟動APP時無法獲取HTTPDNS
中的IP的問題固蛾,內(nèi)存緩存主要為查詢策略提供服務(wù)。當(dāng)某個基于HTTPDNS
的IP地址導(dǎo)致請求失敗后赌渣,會清除當(dāng)前域名和IP的緩存數(shù)據(jù)魏铅。同時外部可控制是否使用緩存。
查詢策略:查詢策略
主要是為了解決坚芜,短時間內(nèi)同一個域名多次調(diào)用基礎(chǔ)服務(wù)層的域名查詢服務(wù)览芳,當(dāng)狀態(tài)是正在查詢中時,后來者不再調(diào)用查詢服務(wù)鸿竖,直接從緩存策略中的內(nèi)存緩存中讀取可用的IP沧竟,如果緩存內(nèi)也無可用的IP,則直接降級為運(yùn)營商的LocalDNS
查詢缚忧。查詢策略可在確保服務(wù)可用的同時悟泵,有效減少和HTTPDNS
服務(wù)器交互的次數(shù)。
注入層
注入層在iOS端是依賴
NSURLProtocol
進(jìn)行攔截網(wǎng)絡(luò)請求闪水,在這里不再具體介紹NSURLProtocol
的用法糕非。基于NSURLProtocol攔截網(wǎng)絡(luò)請求,我們分別實(shí)現(xiàn)了兩套方案朽肥,在不需要處理SNI場景的情況下禁筏,基于NSURLSession
實(shí)現(xiàn);在需要處理SNI
(Server Name Indication
衡招,單IP多HTTPS證書)場景的情況下篱昔,基于CFNetwork
實(shí)現(xiàn)。下面我們看一下兩種方案:
- (1)SNI場景下基于CFNetwork的實(shí)現(xiàn)方案:
SNI(Server Name Indication)
是為了解決一個服務(wù)器使用多個域名和證書的SSL/TLS擴(kuò)展始腾。它的工作原理如下:
在連接到服務(wù)器建立SSL鏈接之前先發(fā)送要訪問站點(diǎn)的域名(Hostname)州刽。
服務(wù)器根據(jù)這個域名返回一個合適的證書。
上述過程中浪箭,當(dāng)客戶端使用HTTPDNS
解析域名時穗椅,請求URL中的host
會被替換成HTTPDNS
解析出來的IP,導(dǎo)致服務(wù)器獲取到的域名為解析后的IP
山林,無法找到匹配的證書房待,只能返回默認(rèn)的證書或者不返回,所以會出現(xiàn)SSL/TLS
握手不成功的錯誤驼抹。
由于iOS上層網(wǎng)絡(luò)庫NSURLSession
沒有提供接口進(jìn)行SNI字段的配置桑孩,因此可以考慮使用NSURLProtocol
攔截網(wǎng)絡(luò)請求,然后使用CFHTTPMessageRef
創(chuàng)建NSInputStream
實(shí)例進(jìn)行Socket
通信框冀,并設(shè)置其kCFStreamSSLPeerName
的值流椒。
注:上述文字來自于騰訊HTTPDNS官方文檔。
基于CFHTTPMessageRef
和NSInputStream
設(shè)置SNI
關(guān)鍵代碼如下:
// 設(shè)置SNI host信息
NSString *host = [self.sniRequest.allHTTPHeaderFields objectForKey:@"Host"];
if (!host) {
host = self.originalRequest.URL.host;
}
[self.inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
NSDictionary *sslProperties = @{ (__bridge id) kCFStreamSSLPeerName : host };
[self.inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
基于CFNetwork
的實(shí)現(xiàn)方案明也,除了設(shè)置SNI
信息外宣虾,還需要考慮的數(shù)據(jù)編解碼的問題,在我們看到的眾多的開源代碼和文章中很少有人提及這一點(diǎn)温数,因此我們在處理響應(yīng)數(shù)據(jù)時需要添加類似如下代碼進(jìn)行響應(yīng)數(shù)據(jù)的解碼操作:
//檢查`Content-Encoding`绣硝,返回數(shù)據(jù)是否需要進(jìn)行解碼操作;
//此處僅做了gzip解碼的處理撑刺,業(yè)務(wù)場景若確定有其他編碼格式鹉胖,需自行完成擴(kuò)展。
NSString *contentEncoding = [self.response.headerFields objectForKey:@"Content-Encoding"];
if (contentEncoding && [contentEncoding isEqualToString:@"gzip"]) {
[self.delegate task:self didReceiveData:[self ungzipData:self.resultData]];
} else {
[self.delegate task:self didReceiveData:self.resultData];
}
此外還有非常重要的一點(diǎn)够傍,基于CFNetwork的實(shí)現(xiàn)方案甫菠,需要考慮連接復(fù)用的問題,不能每次請求都重新創(chuàng)建冕屯,重新連接的成本非常高寂诱。這也是我們在看開源代碼和文章從來不會提及的部分,如果此處不處理安聘,性能消耗非常嚴(yán)重痰洒。
尤其我們目前大部分請求都已經(jīng)是HTTP2.0(也就是H2)了瓢棒,性能對比會更加明顯。但由于蘋果的CFNetwork
框架是不支持HTTP2.0
的丘喻,也就是我們很難基于CFNetwork
實(shí)現(xiàn)到HTTP2.0
的相關(guān)特性音羞。我們目前是實(shí)現(xiàn)了HTTP1.1
協(xié)議中連接復(fù)用這一部分功能,不需要每次請求都重新建立連接仓犬。
基本原理為相同host、port舍肠、scheme
的請求搀继,在請求發(fā)起時如果有可用的沒過期的連接可以復(fù)用,就不需要重新建立連接翠语,直接復(fù)用連接即可叽躯,如果連接在本地過期,或者服務(wù)端通過響應(yīng)頭主動關(guān)閉連接肌括,則連接不復(fù)用点骑,進(jìn)行連接關(guān)閉。判斷服務(wù)端是否連接復(fù)用谍夭,可通過響應(yīng)頭的Connection
為keep-alive
還是close
進(jìn)行判斷黑滴。
- (2)非SNI場景下基于CFNetwork的實(shí)現(xiàn)方案:
基于NSURLSession
的實(shí)現(xiàn)比較簡單,在通過NSURLProtocol
進(jìn)行攔截請求后紧索,只需要將Request
中的域名替換成IP袁辈,在請求頭中設(shè)置原始Host
字段和Cookie
字段,重新構(gòu)建dataTask
任務(wù)珠漂,發(fā)起請求即可晚缩,簡單的示例代碼如下:
//處理url和host dnsResultURL為替換ip后的URL
NSMutableURLRequest *ipRequest = [originRequest mutableCopy];
ipRequest.URL = [NSURL URLWithString:dnsResultURL];
[ipRequest setValue:url.host forHTTPHeaderField:@"Host"];
//處理cookie,由于url變了媳危,系統(tǒng)并不會攜帶原域名下的cookie
NSString *cookieString = [[MFSNICookieManager sharedManager] requestCookieHeaderForURL:url];
[ipRequest setValue:cookieString forHTTPHeaderField:@"Cookie"];
self.ipRequest = ipRequest;
self.clientThread = [NSThread currentThread];
self.ipTask = [[[self class] sharedDemux] dataTaskWithRequest:ipRequest delegate:self modes:self.modes];
if(self.ipTask){
[self.ipTask resume];
}
在HTTPS
的證書校驗(yàn)流程中荞彼,由于我們修改了請求URL中的Host
為IP地址,因此證書驗(yàn)證流程無法通過待笑,因此需要修改證書的驗(yàn)證流程鸣皂,在證書驗(yàn)證時,將IP替換為原來的域名滋觉,再進(jìn)行證書驗(yàn)證签夭。示例代碼如下:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
if (!challenge) {
return;
}
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential = nil;
//獲取原始域名host,用原始請求即可獲取
NSString *host = [[self.originRequest allHTTPHeaderFields] objectForKey:@"Host"];
if (!host) {
host = self.originRequest.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直接使用默認(rèn)的驗(yàn)證方案
completionHandler(disposition, credential);
}
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain {
//創(chuàng)建證書策略
NSMutableArray *policies = [NSMutableArray array];
if (domain) {
[policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) domain)];
} else {
[policies addObject:(__bridge_transfer id) SecPolicyCreateBasicX509()];
}
//綁定校驗(yàn)策略到服務(wù)端的證書上
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef) policies);
/*
* 評估當(dāng)前serverTrust是否可信任椎侠,
* 官方建議在result = kSecTrustResultUnspecified 或 kSecTrustResultProceed
* 的情況下serverTrust可以被驗(yàn)證通過第租,https://developer.apple.com/library/ios/technotes/tn2232/_index.html
* 關(guān)于SecTrustResultType的詳細(xì)信息請參考SecTrust.h
*/
SecTrustResultType result;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
SecTrustEvaluate(serverTrust, &result);
#pragma clang diagnostic pop
return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
基礎(chǔ)服務(wù)層
基礎(chǔ)服務(wù)層目前階段主要依賴騰訊云HTTPDNS
SDK提供基礎(chǔ)查詢服務(wù),主要提供基于TTL
的緩存存儲和過期處理邏輯我纪,同時這一層還提供SDK
的內(nèi)部緩存存儲以及日志和基礎(chǔ)校驗(yàn)等功能;
? ? ? ? ? ?如果你對
性能
有這很高的要求慎宾,同時又需要處理SNI
場景的問題丐吓,我建議不要直接主動使用HTTPDNS
,而是在運(yùn)營商LocalDNS
獲取的IP請求失敗的情況下趟据,可以在底層直接使用基于CFNetwork
的網(wǎng)絡(luò)請求進(jìn)行重試券犁,這樣就能在請求DNS劫持
和性能
中間得到一個平衡,既能保證在運(yùn)營商的LocalDNS
解析出現(xiàn)問題時能夠走HTTPDNS
汹碱,保證成功率和可用性粘衬;同時又能夠在運(yùn)營商的LocalDNS
可用時,使用基于NSURLSession
的請求咳促,享受系統(tǒng)實(shí)現(xiàn)的HTTP2.0
特性帶來的性能提升稚新。如果,不需要處理SNI
的問題跪腹,就老老實(shí)實(shí)使用基于NSURLSession
的實(shí)現(xiàn)方案