移動網際網路的網路狀況是十分複雜的,三大運營商、3G、4G、Wi-Fi、地點等任何一個狀態的改變都會導致網路狀況的變化,並且運營商、代理商們還可能在其中搞一些小破壞,比如經常會有使用者反饋說某個頁面訪問不了或者返回結果不正確等問題,這種狀況一般都是發生了域名劫持,通用的解決方案就是使用 IP 直連,跨過運營商 LocalDNS 伺服器解析過程,從而達到降低延遲、避免劫持的效果。
Why IP
為什麼大家會選擇直接使用 IP 來進行連線呢?它具有多方面的優勢:
-
防劫持,可以繞過運營商 LocalDNS 解析過程,避免域名劫持,提高網路訪問成功率
-
降低延遲,DNS 解析是一個相對耗時的工作,跳過這個過程可以降低一定的延遲
-
精準排程,運營商解析返回的節點不一定是最優的,自己獲取 IP 可以基於自己的策略來獲取最精準的、最優的節點
對於獲取 IP,我瞭解到的是兩種方案,一種是直接接入騰訊或者阿里的 HTTPDNS 服務,在發起請求的時候是通過 HTTPDNS 獲取 IP,然後直接使用 IP 來進行業務訪問;一種是內建 Server IP,可以在啟動等階段由服務端下發域名和 IP 的對應列表,客戶端來進行快取,發起網路請求的時候直接根據快取 IP 來進行業務訪問。
HTTPS & DNS 劫持
起初我是存在一些疑問的,為什麼 HTTPS 還會存在 DNS 劫持問題呢?HTTPS 從身份認證、內容加密、防止篡改三個方面來保證網路安全,但是它的時機相對靠後,DNS 解析是網路請求的第一個步驟,完整的步驟是 DNS 解析 -> TCP 連線 -> TLS 握手 -> Request -> Response,所以 HTTPS 對 DNS 劫持也無能為力,但是可以通過客戶端身份認證來避免被塞廣告等狀況的發生,被劫持後直接訪問失敗。
身份認證
HTTPS 網路下我們會對服務端的身份進行認證,就拿 AFNetworking 來說,它提供了三種身份認證的可選方案:
-
AFSSLPinningModeNone(Default)
-
AFSSLPinningModePublicKey
-
AFSSLPinningModeCertificate
對於 AFSSLPinningModeNone,客戶端自己不會對服務端證書進行自主校驗,而是直接預設信任,將證書交給系統來進行校驗,判斷返回的證書是否是官方機構頒發的,如果是則信任,這個應該也是普通開發者採用最多的方案。這種認證方案如果被劫持後返回的證書也是官方機構頒發的,那麼客戶端就會正常進行網路訪問,但是返回的資料就不是想要的資料,比如被塞廣告等現象的出現。
對於 AFSSLPinningModePublicKey、AFSSLPinningModeCertificate 兩種方案,客戶端本地通常會儲存一份服務端證書, AFSSLPinningModePublicKey 代表客戶端會將伺服器端返回的證書與本地儲存的證書中的 PublicKey 部分進行自主校驗,AFSSLPinningModeCertificate 代表客戶端會將伺服器端返回的證書和本地儲存的證書中的所有內容,包括 PublicKey 和證書部分全部進行自主校驗。如果校驗成功,才繼續進行系統驗證等後續行為。
HTTP IP Connect
雖然 Apple 很早就開始推 ATS,但是由於國內的網路情況特別複雜,所以 Apple 還是支援 HTTP 網路請求的,並且 HTTP 協議下的網路請求的比例也很大。實現 HTTP 協議下 IP 連線其實是很簡單的,我們只需要通過 NSURLProtocol 來攔截網路請求,然後將符號條件的網路請求 URL 中的域名修改為 IP 就可以啦。
但是也會存在一些小問題,域名置換為 IP 之後,服務端無法根據 URL 來判斷你要訪問哪個域名,所以我們需要手動的將 host 欄位塞到 header 中去,方便伺服器的正確識別。
HTTPS IP Connect
HTTPS 比 HTTP 多了一個 TLS 握手的過程,這個過程中涉及到一個證書校驗的問題,服務端會在第二次握手的時候返回一個證書,來使得客戶端可以校驗它的身份,避免出現假冒的狀況。在這次校驗過程中,會校驗證書的 domain 域是否包含本次 Request 的 host,並且校驗返回的證書是否是官方機構頒發的可信證書。由於 IP 連線會將 URL 中的域名置換為 IP,所以就會導致返回的證書和我們的 Request 校驗失敗的問題。
為了解決證書的校驗的問題,我們需要在證書校驗前,再進行一次域名的替換,這次需要把 URL 中 IP 置換為域名,這樣證書校驗的問題便可迎刃而解。
SNI IP Connect
通常情況下我們的證書是和域名繫結的,有時候會存在一個 IP 對應多個域名的情況,這種情況如果客戶端 IP 直連的時候,沒有告訴服務端他要請求的是哪個域名的證書,服務端就沒辦法返回正確的證書,從而導致客戶端證書校驗失敗。
SNI 的全稱 Server Name Indication,為了解決一個伺服器使用多個域名和證書的 SSL/TLS 擴充套件。在連線到伺服器建立 SSL 連線時,客戶端可以在第一次握手的 Client Hello 中的 SNI 擴充套件欄位中填入要訪問站點的域名(Hostname),這樣伺服器就可以根據域名返回一個合適的證書。
綜上所述,我們在進行 IP 直連的時候,面對單 IP 多域名的情況需要客戶端手動配置 SNI 欄位,但是上層的網路庫 NSURLSession、NSURLConnection 都沒有提供配置的介面,我們需要使用更加底層的 libcurl 庫和 CFNetwork 來完成 SNI 欄位的配置。以 CFNetwork 為例,可以使用如下程式碼進行 SNI 的配置。
// HTTPS 請求處理 SNI 場景
NSString *host = [self.swizzleRequest.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];
複製程式碼
寫在最後
其實 IP 直連的文章早已數不勝數,本篇也只是一個學習實踐的筆記而已,更多相關資料和程式碼我會在末尾給出。NSURLProtocol 和 IP 直連有很多要注意的點,比如 NSURLProtocol 攔截 post 請求獲取 httpbody 為空、Cookie 的處理、WebView 的處理等。有些知識本人也沒有實踐,不過可以在參考文章中找到相應的處理方案。