譯者序
本文翻譯自 2020 年的一篇英文博客:How NAT traversal works。 ? ? 設想這樣一個問題:在北京和上海各有一臺局域網的機器(例如一臺是家里的臺式機,一 臺是連接到店鋪WiFi 的筆記本),二者都是私網 IP 地址,但可以訪問公網,?如何讓這兩臺機器通信呢? ? 既然二者都能訪問公網,那最簡單的方式就是在公網上架設一個中繼服務器:兩臺機器分別連接到中繼服務,后者完成雙向轉發。這種方式有很大的性能開銷,而且中繼服務器很容易成為瓶頸。 ?
有沒有辦法不用中繼,讓兩臺機器直接通信呢? ? *Tailscale是一家軟件公司,開發了一個開源軟件定義的mesh VPN和基于Web的管理服務。同時,該公司用于構建安全網絡的零配置 VPN也名Tailscale。 *由于譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閱原文。以下是譯文。
引言
背景:IPv4 地址短缺,引入 NAT
全球 IPv4 地址早已不夠用,因此人們發明了 NAT(網絡地址轉換)來緩解這個問題。 ? 簡單來說,大部分機器都使用私有 IP 地址,如果它們需要訪問公網服務,那么:
出向流量:需要經過一臺 NAT 設備,它會對流量進行 SNAT,將私有 srcIP+Port 轉 換成 NAT 設備的公網 IP+Port(這樣應答包才能回來),然后再將包發出去;
應答流量(入向):到達 NAT 設備后進行相反的轉換,然后再轉發給客戶端。
整個過程對雙方透明。 ? 以上是本文所討論問題的基本背景。 ?
需求:兩臺經過 NAT 的機器建立點對點連接
在以上所描述的 NAT 背景下,我們從最簡單的問題開始:如何在兩臺經過 NAT 的機器之間建立點對點連接(直連)。如下圖所示: ?
直接用機器的 IP 互連顯然是不行的,因為它們都是私有 IP(例如 192.168.1.x)。在 Tailscale 中,我們會建立一個 WireGuard 隧道?來解決這個問題 —— 但這并不是太重要,因為我們將過去幾代人努力都整合到了一個工具集,?這些技術廣泛適用于各種場景。例如, ?
WebRTC 使用這些技術在瀏覽器之間完成 peer-to-peer 語音、視頻和數據傳輸;
VoIP 電話和一些視頻游戲也使用類似機制,雖然不是所有情況下都很成功。
方案:NAT 穿透
>兩個必備前提:UDP + 能直接控制 socket ? 如果想設計自己的協議來實現 NAT 穿透,那必須滿足以下兩個條件: ?
協議應該基于 UDP
理論上用 TCP 也能實現,但它會給本已相當復雜的問題再增加一層復雜性, 甚至還需要定制化內核,取決于你想實現到什么程度。本文接下來都將關注在 UDP 上。 ? 如果考慮 TCP 是想在 NAT 穿透時獲得面向流的連接( stream-oriented connection),可以考慮用 QUIC 來替代,它構建在 UDP 之上,因此我們能將關注點放在 UDP NAT 穿透,而仍然能獲得一個很好的流協議(stream protocol)。 ?
對收發包的 socket 有直接控制權
例如,從經驗上來說,無法基于某個現有的網絡庫實現 NAT 穿透,因為我們必須在使用的“主要”協議之外,發送和接收額外的數據包。 ? 某些協議(例如 WebRTC)將 NAT 穿透與其他部分緊密集成。但如果你在構建自己的協議,?建議將 NAT 穿透作為一個獨立實體,與主協議并行運行,二者僅 僅是共享 socket 的關系,如下圖所示,這將帶來很大幫助:
>保底方式:中繼 ? 在某些場景中,直接訪問 socket 這一條件可能很難滿足。 ? 退而求其次的一個方式是設置一個 local proxy(本地代理),主協議與這個 proxy 通信 ,后者來完成 NAT 穿透,將包中繼(relay)給對端。這種方式增加了一個額外的間接層 ,但好處是: ?
仍然能獲得 NAT 穿透,
不需要對已有的應用程序做任何改動。
挑戰:有狀態防火墻和 NAT 設備
有了以上鋪墊,下面就從最基本的原則開始,一步步看如何實現一個企業級的 NAT 穿透方案。 ? 我們的目標是:在兩個設備之間通過 UDP 實現雙向通信, 有了這個基礎,上層的其他協議(WireGuard, QUIC, WebRTC 等)就能做一些更酷的事情。 ? 但即便這個看似最基本的功能,在實現上也要解決兩個障礙: ?
有狀態防火墻
NAT 設備
穿透防火墻
有狀態防火墻是以上兩個問題中相對比較容易解決的。實際上,大部分 NAT 設備都自帶了一個有狀態防火墻, 因此要解決第二個問題,必須先解決有第一個問題。 ? 有狀態防火墻具體有很多種類型,有些你可能見過: ?
Windows Defender firewall
Ubuntu’s ufw (using iptables/nftables)
BSD/macOS?pf
AWS Security Groups(安全組)
有狀態防火墻
>默認行為(策略) ? 以上防火墻的配置都是很靈活的,但大部分配置默認都是如下行為: ?
允許所有出向連接(allows all “outbound” connections)
禁止所有入向連接(blocks all “inbound” connections)
可能有少量例外規則,例如 allowing inbound SSH。 ? >如何區分入向和出向包 ? 連接(connection)和方向(direction)都是協議設計者頭腦中的概念,到了物理傳輸層,每個連接都是雙向的;允許所有的包雙向傳輸。那防火墻是如何區分哪些是入向包、哪些是出向包的呢?這就要回到“有狀態”(stateful)這三個字了:有狀態防火墻會記錄它看到的每個包,當收到下一個包時,會利用這些信息(狀態)來判斷應該做什么。 ? 對UDP 來說,規則很簡單:如果防火墻之前看到過一個出向包(outbound),就會允許相應的入向包(inbound)通過,以下圖為例: ?
? 筆記本電腦中自帶了一個防火墻,當該防火墻看到從這臺機器出去的?2.2.2.2:1234 -> 5.5.5.5:5678?包時,就會記錄一下:5.5.5.5:5678 -> 2.2.2.2:1234?入向包應該放行。?這里的邏輯是:我們信任的世界(即筆記本)想主動與?5.5.5.5:5678?通信,因此應該放行(allow)其回包路徑。 ? 某些非常寬松的防火墻只要看到有從 2.2.2.2:1234 出去的包,就 會允許所有從外部進入 2.2.2.2:1234 的流量。這種防火墻對我們的 NAT 穿透來說非常友好,但已經越來越少見了。 ?
防火墻朝向(face-off)與穿透方案
>防火墻朝向相同 ? 場景特點:服務端 IP 可直接訪問 ? 在 NAT 穿透場景中,以上默認規則對 UDP 流量的影響不大 —— 只要路徑上所有防火墻的“朝向”是一樣的。一般來說,從內網訪問公網上的某個服務器都屬于這種情況。 ? 我們唯一的要求是:連接必須是由防火墻后面的機器發起的。這是因為在它主動和別人通信之前,沒人能主動和它通信,如下圖所示: ? 穿透方案:客戶端直連服務端,或 hub-and-spoke 拓撲
? 但上圖是假設了通信雙方中,其中一端(服務端)是能直接訪問到的。在 VPN 場景中,這就形成了所謂的 hub-and-spoke 拓撲:中心的 hub 沒有任何防火墻策略,誰都能訪問到;防火墻后面的 spokes 連接到 hub。如下圖所示:
>防火墻朝向不同
場景特點:服務端 IP 不可直接訪問 ? 但如果兩個“客戶端”想直連,以上方式就不行了,此時兩邊的防火墻相向而立,如下圖所示:
根據前面的討論,這種情況意味著:兩邊要同時發起連接請求,但也意味著兩邊都無法發起有效請求,因為對方先發起請求才能在它的防火墻上打開一條縫讓我們進去!如何破解這個問題呢?一種方式是讓用戶重新配置一邊或兩邊的防火墻,打開一個端口, 允許對方的流量進來。 ?
這顯然對用戶不友好,在像 Tailscale 這樣的 mesh 網絡中的擴展性也不好,在 mesh 網絡中,我們假設對端會以一定的粒度在公網上移動;
此外,在很多情況下用戶也沒有防火墻的控制權限:例如在咖啡館或機場中,連接的路由器是不受你控制的(否則你可能就有麻煩了)。
因此,我們需要尋找一種不用重新配置防火墻的方式。 ? 穿透方案:兩邊同時主動建連,在本地防火墻為對方打開一個洞 ? 解決的思路還是先重新審視前面提到的有狀態防火墻規則: ?
對于 UDP,其規則(邏輯)是:包必須先出去才能進來(packets must flow out before packets can flow back in)。
注意,這里除了要滿足包的 IP 和端口要匹配這一條件之外,并沒有要求包必須是相關的(related)。換句話說,只要某些包帶著正確的源和目的地址出去了,任何看起來像是響應的包都會被防火墻放進來?—— 即使對端根本沒收到你發出去的包。
因此,要穿透這些有狀態防火墻,我們只需要共享一些信息:讓兩端提前知道對方使用的 ip:port: ?
手動靜態配置是一種方式,但顯然擴展性不好;
我們開發了一個 coordination server, 以靈活、安全的方式來同步 ip:port 信息。
有了對方的 ip:port 信息之后,兩端開始給對方發送 UDP 包。在這個過程中,我們預 料到某些包將會被丟棄。因此,雙方必須要接受某些包會丟失的事實, 因此如果是重要信息,你必須自己準備好重傳。對 UDP 來說丟包是可接受的,但這里尤其需要接受。 ? 來看一下具體建連(穿透)過程: ? 1)如圖所示,筆記本出去的第一包,2.2.2.2:1234 -> 7.7.7.7:5678,穿過 Windows Defender 防火墻進入到公網。 ?
? 對方的防火墻會將這個包攔截掉,因為它沒有?7.7.7.7:5678 -> 2.2.2.2:1234?的流量記錄。但另一方面,Windows Defender 此時已經記錄了出向連接,因此會允許?7.7.7.7:5678 -> 2.2.2.2:1234?的應答包進來。 ? 2)接著,第一個?7.7.7.7:5678 -> 2.2.2.2:1234?穿過它自己的防火墻到達公網。
? 到達客戶端側時,Windows Defender 認為這是剛才出向包的應答包,因此就放行它進入了!?此外,右側的防火墻此時也記錄了:2.2.2.2:1234 -> 7.7.7.7:5678?的包應該放行。 ? 3)筆記本收到服務器發來的包之后,發送一個包作為應答。這個包穿過 Windows Defender 防火墻 和服務端防火墻(因為這是對服務端發送的包的應答包),達到服務端。
成功!這樣我們就建立了一個穿透兩個相向防火墻的雙向通信連接。而初看之下,這項任務似乎是不可能完成的。 ?
關于穿透防火墻的一些思考
穿透防火墻并非永遠這么輕松,有時會受一些第三方系統的間接影響,需要仔細處理。那穿透防火墻需要注意什么呢?重要的一點是:通信雙方必須幾乎同時發起通信, 這樣才能在路徑上的防火墻打開一條縫,而且兩端還都是活著的。 ? >雙向主動建連:旁路信道 ? 如何實現“同時”呢?一種方式是兩端不斷重試,但顯然這種方式很浪費資源。假如雙方都 知道何時開始建連就好了。 ? 這聽上去是雞生蛋、蛋生雞的問題了:雙方想要通信,必須先提前通個信。 ? 但實際上,我們可以通過旁路信道(side channel)來達到這個目的 ,并且這個旁路信道并不需要很 fancy:它可以有幾秒鐘的延遲、只需要傳送幾 KB 的 信息,因此即使是一個配置非常低的虛擬機,也能為幾千臺機器提供這樣的旁路通信服務。 ?
在遙遠的過去,我曾用 XMPP 聊天消息作為旁路,效果非常不錯。
另一個例子是 WebRTC,它需要你提供一個自己的“信令信道”(signalling channel, 這個詞也暗示了 WebRTC 的 IP telephony ancestry),并將其配置到 WebRTC API。
在 Tailscale,我們的協調服務器(coordination server)和 DERP (Detour Encrypted Routing Protocol) 服務器集群是我們的旁路信道。
>非活躍連接被防火墻清理 ? 有狀態防火墻內存通常比較有限,因此會定期清理不活躍的連接(UDP 常見的是 30s), 因此要保持連接 alive 的話需要定期通信,否則就會被防火墻關閉,為避免這個問題, 我們 ?
要么定期向對方發包來 keepalive;
要么有某種帶外方式來按需重建連接。
>問題都解決了?不,挑戰剛剛開始 ? 對于防火墻穿透來說, 我們并不需要關心路徑上有幾堵墻?—— 只要它們是有狀態防火墻且允許出 向連接,這種同時發包(simultaneous transmission)機制就能穿透任意多層防火墻。這一點對我們來說非常友好,因為只需要實現一個邏輯,然后能適用于任何地方了。 ? …對嗎? ? 其實,不完全對。這個機制有效的前提是:我們能提前知道對方的 ip:port。而這就涉及到了我們今天的主題:NAT,它會使前面我們剛獲得的一點滿足感頓時消失。 ? 下面,進入本文正題。 ?
NAT 的本質
NAT 設備與有狀態防火墻
可以認為 NAT 設備是一個增強版的有狀態防火墻,雖然它的增強功能 對于本文場景來說并不受歡迎:除了前面提到的有狀態攔截/放行功能之外,它們還會在數據包經過時修改這些包。 ?
NAT 穿透與 SNAT/DNAT
具體來說,NAT 設備能完成某種類型的網絡地址轉換,例如,替換源或目的 IP 地址或端口。 ?
討論連接問題和 NAT 穿透問題時,我們只會受 source NAT —— SNAT 的影響;
DNAT 不會影響 NAT 穿透。
SNAT 的意義:解決 IPv4 地址短缺問題
SNAT 最常見的使用場景是將很多設備連接到公網,而只使用少數幾個公網 IP。例如對于消費級路由器,會將所有設備的(私有) IP 地址映射為單個連接到公網的 IP 地址。 ? 這種方式存在的意義是:我們有遠多于可用公網 IP 數量的設備需要連接到公網,(至少 對 IPv4 來說如此,IPv6 的情況后面會討論)。NAT 使多個設備能共享同一 IP 地址,因 此即使面臨 IPv4 地址短缺的問題,我們仍然能不斷擴張互聯網的規模。 ?
SNAT 過程:以家用路由器為例
假設你的筆記本連接到家里的 WiFi,下面看一下它連接到公網某個服務器時的情形: ? 1)筆記本發送 UDP packet?192.168.0.20:1234 -> 7.7.7.7:5678。
這一步就好像筆記本有一個公網 IP 一樣,但源地址 192.168.0.20?是私有地址, 只能出現在私有網絡,公網不認,收到這樣的包時它不知道如何應答。 ? 2)家用路由器出場,執行 SNAT。 ? 包經過路由器時,路由器發現這是一個它沒有見過的新會話(session)。它知道 192.168.0.20?是私有 IP,公網無法給這樣的地址回包,但它有辦法解決: ?
在它自己的公網 IP 上挑一個可用的 UDP 端口,例如?2.2.2.2:4242,
然后創建一個 NAT mapping:192.168.0.20:1234 <--> 2.2.2.2:4242,
然后將包發到公網,此時源地址變成了 2.2.2.2:4242 而不是原來的 192.168.0.20:1234。因此服務端看到的是轉換之后地址,
接下來,每個能匹配到這條映射規則的包,都會被路由器改寫 IP 和 端口。
? 3)反向路徑是類似的,路由器會執行相反的地址轉換,將 2.2.2.2:4242 變回 192.168.0.20:1234。對于筆記本來說,它根本感知不知道這正反兩次變換過程。 ? 這里是拿家用路由器作為例子,但辦公網的原理是一樣的。不同之處在 于,辦公網的 NAT 可能有多臺設備組成(高可用、容量等目的),而且它們有不止一個公 網 IP 地址可用,因此在選擇可用的公網 ip:port 來做映射時,選擇空間更大,能支持 更多客戶端。 ?
SNAT 給穿透帶來的挑戰
現在我們遇到了與前面有狀態防火墻類似的情況,但這次是 NAT 設備:通信雙方 不知道對方的 ip:port 是什么,因此無法主動建連,如下圖所示:
但這次比有狀態防火墻更糟糕,嚴格來說,在雙方發包之前,根本無法確定(自己及對方的)ip:port 信息,因為?只有出向包經過路由器之后才會產生 NAT mapping(即,可以被對方連接的 ip:port 信息)。 ? 因此我們又回到了與防火墻遇到的問題,并且情況更糟糕:雙方都需要主動和對 方建連,但又不知道對方的公網地址是多少,只有當對方先說話之后,我們才能拿到它的地址信息。 ? 如何破解以上死鎖呢?這就輪到 STUN 登場了。 ? 穿透 “NAT+防火墻”:STUN (Session Traversal Utilities for NAT) 協議 ? STUN 既是一些對 NAT 設備行為的詳細研究,也是一種協助 NAT 穿透的協議。本文主要關注 STUN 協議。 ?
STUN 原理
STUN 基于一個簡單的觀察:從一個會被 NAT 的客戶端訪問公網服務器時, 服務器看到的是 NAT 設備的公網 ip:port 地址,而非該?客戶端的局域網 ip:port 地址。 ? 也就是說,服務器能告訴客戶端它看到的客戶端的 ip:port 是什么。因此,只要將這個信息以某種方式告訴通信對端(peer),后者就知道該和哪個地址建連了!這樣就又簡化為前面的防火墻穿透問題了。 ? 本質上這就是 STUN 協議的工作原理,如下圖所示: ?
筆記本向 STUN 服務器發送一個請求:“從你的角度看,我的地址什么?”
STUN 服務器返回一個響應:“我看到你的 UDP 包是從這個地址來的:ip:port”。
The STUN protocol has a bunch more stuff in it — there’s a way of obfuscating the?ip:port?in the response to stop really broken NATs from mangling the packet’s payload, and a whole authentication mechanism that only really gets used by TURN and ICE, sibling protocols to STUN that we’ll talk about in a bit. We can ignore all of that stuff for address discovery. ?
為什么 NAT 穿透邏輯和主協議要共享同一個 socket
理解了 STUN 原理,也就能理解為什么我們在文章開頭說,如果?要實現自己的 NAT 穿透邏輯和主協議,就必須讓二者共享同一個 socket: ?
每個 socket 在 NAT 設備上都對應一個映射關系(私網地址 -> 公網地址);
STUN 服務器只是輔助穿透的基礎設施;
與 STUN 服務器通信之后,在 NAT 及防火墻設備上打開了一個連接,允許入向包進來(回憶前面內容,?只要目的地址對,UDP 包就能進來,不管這些包是不是從 STUN 服務器來的);
因此,接下來只要將這個地址告訴我們的通信對端(peer),讓它往這個地址發包,就能實現穿透了。
STUN 的問題:不能穿透所有 NAT 設備(例如企業級 NAT 網關)
有了 STUN,我們的穿透目的似乎已經實現了:每臺機器都通過 STUN 來獲取自己的私網 socket 對應的公網 ip:port,然后把這個信息告訴對端,然后兩端 同時發起穿透防火墻的嘗試,后面的過程就和上一節介紹的防火墻穿透一樣了,對嗎? ? 答案是:看情況。某些情況下確實如此,但有些情況下卻不行。通常來說, ?
對于大部分家用路由器場景,這種方式是沒問題的;
但對于一些企業級 NAT 網關來說,這種方式無法奏效。
NAT 設備的說明書上越強調它的安全性,STUN 方式失敗的可能性就越高。(但注意,從實際意義上來說, NAT 設備在任何方面都并不會增強網絡的安全性,但這不是本文重點,因此不展開。) ?
重新審視 STUN 的前提
再次審視前面關于 STUN 的假設:當 STUN 服務器告訴客戶端在公網看來它的地址是?2.2.2.2:4242?時,那所有目的地址是?2.2.2.2:4242?的包就都能穿透防火墻到達該客戶端。 ? 這也正是問題所在:這一點并不總是成立。 ?
某些 NAT 設備的行為與我們假設的一致,它們的有狀態防火墻組件只要看到有客戶端自己 發起的出向包,就會允許相應的入向包進入;因此只要利用 STUN 功能,再加上兩端同時 發起防火墻穿透,就能把連接打通;
in theory, there are also NAT devices that are super relaxed, and don’t ship with stateful firewall stuff at all. In those, you don’t even need simultaneous transmission, the STUN request gives you an internet?ip:port?that anyone can connect to with no further ceremony. If such devices do still exist, they’re increasingly rare. ?
另外一些 NAT 設備就要困難很多了,它會針對每個目的地址來生成一條相應的映射關系。在這樣的設備上,如果我們用相同的 socket 來分別發送數據包到 5.5.5.5:1234 and 7.7.7.7:2345,我們就會得到 2.2.2.2 上的兩個不同的端口,每個目的地址對應一個。如果反向包的端口用的不對,包就無法通過防火墻。如下圖所示:
? 中場補課:NAT 正式術語
? 知道 NAT 設備的行為并不是完全一樣之后,我們來引入一些正式術語。 ?
早期術語
如果之前接觸過 NAT 穿透,可能會聽說過下面這些名詞: ?
“Full Cone”
“Restricted Cone”
“Port-Restricted Cone”
“Symmetric” NATs
這些都是 NAT 穿透領域的早期術語。 ? 但其實這些術語相當讓人困惑。我每次都要 查一下 Restricted Cone NAT 是什么意思。從實際經驗來看,我并不是唯一對此感到困惑的人。例如,如今互聯網上將 “easy” NAT 歸類為 Full Cone,而實際上它們更應該歸類為 Port-Restricted Cone。 ?
近期研究與新術語
最近的一些研究和 RFC 已經提出了一些更準確的術語。 ?
首先,它們明確了如下事實:NAT 設備的行為差異表現在多個維度, 而并非只有早期研究中所說的 “cone” 這一個維度,因此基于 “cone” 來劃分類別并不是很有幫助。
其次,新研究和新術語能更準確地描述 NAT 在做什么。
前面提到的所謂?"easy" 和 "hard" NAT,只在一個維度有不同:NAT 映射是否考慮到目的地址信息。RFC 4787 中, ?
將?easy NAT 及其變種稱為 “Endpoint-Independent Mapping” (EIM,終點無關的映射)
但是,從“命名很難”這一程序員界的偉大傳統來說,EIM 這個詞其實 也并不是 100% 準確,因為這種 NAT 仍然依賴 endpoint,只不過依賴的是源 endpoint:每個 source ip:port 對應一個映射 —— 否則你的包就會和別人的包混在一起,導致混亂。 ? 嚴格來說,EIM 應該稱為 “Destination Endpoint Independent Mapping” (DEIM?), 但這個名字太拗口了,而且按照慣例,Endpoint 永遠指的是 Destination Endpoint。 ?
將 hard NAT 以及變種稱為 “Endpoint-Dependent Mapping”(EDM,終點相關的映射) 。
EDM 中還有一個子類型,依據是只根據 dst_ip 做映射,還是根據 dst_ip + dst_port 做映射。對于 NAT 穿透來說,這種區分對來說是一樣的:它們都會導致 STUN 方式不可用。 ?
老的 cone 類型劃分
你可能會有疑問:根據是否依賴 endpoint 這一條件,只能組合出兩種可能,那為什么傳 統分類中會有四種 cone 類型呢?答案是 cone 包含了兩個正交維度的 NAT 行為: ?
NAT 映射行為:前面已經介紹過了;
有狀態防火墻行為:與前者類似,也是分為與 endpoint 相關還是無關兩種類型。
因此最終組合如下: NAT Cone Types
? 分解到這種程度之后就可以看出,cone 類型對 NAT 穿透場景來說并沒有什么意義。我們關心的只有一點:是否是 Symmetric —— 換句話說,一個 NAT 設備是 EIM 還是 EDM 類型的。 ?
針對 NAT 穿透場景:簡化 NAT 分類
以上討論可知,雖然理解防火墻的具體行為很重要,但對于編寫 NAT 穿透代碼來說,這一點并不重要。我們的兩端同時發包方式(simultaneous transmission trick)能?有效穿透以上三種類型的防火墻。在真實場景中, 我們主要在處理的是 IP-and-port endpoint-dependent 防火墻。 ? 因此,對于實際 NAT 穿透實現,我們可以將以上分類簡化成:
更多 NAT 規范(RFC)
想了解更多新的 NAT 術語,可參考 ?
RFC?4787?(NAT Behavioral Requirements for UDP)
RFC?5382?(for TCP)
RFC?5508?(for ICMP)
如果自己實現 NAT,那應該遵循這些 RFC 的規范,這樣才能使你的 NAT 行為符合業界慣例,與其他廠商的設備或軟件良好兼容。 ?
穿透 NAT+防火墻:STUN 不可用時,fallback 到中繼模式
問題回顧與保底方式(中繼)
補完基礎知識(尤其是定義了什么是 hard NAT)之后,回到我們的 NAT 穿透主題。 ?
第 1~4 節已經解決了 STUN 和防火墻穿透的問題;
但 hard NAT 對我們來說是個大問題,只要路徑上出現一個這種設備,前面的方案就行不通了。
準備放棄了嗎?這才進入 NAT 真正有挑戰的部分:如果已經試過了前面介紹的所有方式 仍然不能穿透,我們該怎么辦呢? ?
實際上,確實有很多 NAT 實現在這種情況下都會選擇放棄,向用戶報一個“無法連接”之類的錯誤。
但對我們來說,這么快就放棄顯然是不可接受的 —— 解決不了連通性問題,Tailscale 就沒有存在的意義。
我們的保底解決方式是:創建一個中繼連接(relay)實現雙方的無障礙地通信。但是,中繼方式性能不是很差嗎?這要看具體情況: ?
如果能直連,那顯然沒必要用中繼方式;
但如果無法直連,而中繼路徑又非常接近雙方直連的真實路徑,并且帶寬足夠大,那中 繼方式并不會明顯降低通信質量。延遲肯定會增加一點,帶寬會占用一些,但?相比完全連接不上,還是更能讓用戶接受的。
不過要注意:我們只有在無法直連時才會選擇中繼方式。實際場景中, ?
對于大部分網絡,我們都能通過前面介紹的方式實現直連;
剩下的長尾用中繼方式來解決,并不算一個很糟的方式。
此外,某些網絡會阻止 NAT 穿透,其影響比這種 hard NAT 大多了。例如,我們觀察到 UC Berkeley guest WiFi 禁止除 DNS 流量之外的所有 outbound UDP 流量。不管用什么 NAT 黑科技,都無法繞過這個攔截。因此我們終歸還是需要一些可靠的 fallback 機制。 ?
中繼協議:TURN、DERP
有多種中繼實現方式。 ? 1)TURN (Traversal Using Relays around NAT):經典方式,核心理念是 ?
用戶(人)先去公網上的 TURN 服務器認證,成功后后者會告訴你:“我已經為你分配了 ip:port,接下來將為你中繼流量”;
然后將這個 ip:port 地址告訴對方,讓它去連接這個地址,接下去就是非常簡單的客戶端/服務器通信模型了。
Tailscale 并不使用 TURN。這種協議用起來并不是很好,而且與 STUN 不同, 它沒有真正的交互性,因為互聯網上并沒有公開的 TURN 服務器。 ? 2)DERP (Detoured Encrypted Routing Protocol) ? 這是我們創建的一個協議,DERP。 ?
它是一個通用目的包中繼協議,運行在 HTTP 之上,而大部分網絡都是允許 HTTP 通信的。
它根據目的公鑰(destination’s public key)來中繼加密的流量(encrypted payloads)。
前面也簡單提到過,DERP 既是我們在 NAT 穿透失敗時的保底通信方式(此時的角色 與 TURN 類似),也是在其他一些場景下幫助我們完成 NAT 穿透的旁路信道。換句話說,它既是我們的保底方式,也是有更好的穿透鏈路時,幫助我們進行連接升 級(upgrade to a peer-to-peer connection)的基礎設施。 ?
小結
有了“中繼”這種保底方式之后,我們穿透的成功率大大增加了。如果此時不再閱讀本文接下來的內容,而是把上面介紹的穿透方式都實現了,我預計: ?
90% 的情況下,你都能實現直連穿透;
剩下的 10% 里,用中繼方式能穿透一些(some);
這已經算是一個“足夠好”的穿透實現了。 ?
穿透 NAT+防火墻:企業級改進
如果你并不滿足于“足夠好”,那我們可以做的事情還有很多! ? 本節將介紹一些五花八門的 tricks,在某些特殊場景下會幫到我們。單獨使用這項技術都無法解決 NAT 穿透問題,但將它們巧妙地組合起來,我們能更加接近 100% 的穿透成功率。 ?
穿透 hard NAT:暴力端口掃描
回憶 hard NAT 中遇到的問題,如下圖所示,關鍵問題是:easy NAT 不知道該往 hard NAT 方的哪個 ip:port 發包。
但必須要往正確的 ip:port 發包,才能穿透防火墻,實現雙向互通。怎么辦呢? ?
首先,我們能知道 hard NAT 的一些 ip:port,因為我們有 STUN 服務器。
這里先假設我們獲得的這些 IP 地址都是正確的(這一點并不總是成立,但這里先這么假 設。而實際上,大部分情況下這一點都是成立的,如果對此有興趣,可以參考 REQ-2 in RFC 4787)。
IP 地址確定了,剩下的就是端口了??偣灿?65535 中可能,我們能遍歷這個端口范圍嗎?
如果發包速度是 100 packets/s,那最壞情況下,需要 10 分鐘來找到正確的端口。還是那句話,這雖然不是最優的,但總比連不上好。 ? 這很像是端口掃描(事實上,確實是),實際中可能會觸發對方的網絡入侵檢測軟件。 ?
基于生日悖論改進暴力掃描:hard side 多開端口 + easy side 隨機探測
利用 birthday paradox 算法, 我們能對端口掃描進行改進。 ?
上一節的基本前提是:hard side 只打開一個端口,然后 easy side 暴力掃描 65535 個端口來尋找這個端口;
這里的改進是:在 hard size 開多個端口,例如 256 個(即同時打開 256 個 socket,目的地址都是 easy side 的 ip:port), 然后 easy side 隨機探測這邊的端口。
這里省去算法的數學模型,如果你對實現干興趣,可以看看我寫的 python calculator。計算過程是“經典”生日悖論的一個小變種。下面是隨著 easy side random probe 次數(假設 hard size 256 個端口)的變化,兩邊打開的端口有重合(即通信成功)的概率:
? 根據以上結果,如果還是假設 100 ports/s 這樣相當溫和的探測速率,那 2 秒鐘就有約 50% 的成功概率。即使非常不走運,我們仍然能在 20s 時幾乎 100% 穿透成功,而此時只探測了總端口空間的 4%。 ? 非常好!雖然這種 hard NAT 給我們帶來了嚴重的穿透延遲,但最終結果仍然是成功的。那么,如果是兩個 hard NAT,我們還能處理嗎? ?
雙 hard NAT 場景
這種情況下仍然可以用前面的?多端口+隨機探測?方式,但成功概率要低很多了: ?
每次通過一臺 hard NAT 去探測對方的端口(目的端口)時,我們自己同時也生成了一個隨機源端口;
這意味著我們的搜索空間變成了二維?{src port, dst port}?對,而不再是之前的一維 dst port 空間。
這里我們也不就具體計算展開,只告訴結果:仍然假設目的端打開 256 個端口,從源端發起 2048 次(20 秒), 成功的概率是:0.01%。 ? 如果你之前學過生日悖論,就并不會對這個結果感到驚訝。理論上來說, ?
要達到 99.9% 的成功率,我們需要兩邊各進行170,000 次探測 —— 如果還是以 100 packets/sec 的速度,就需要 28 分鐘。
要達到 50% 的成功率,“只”需要 54,000 packets,也就是 9 分鐘。
如果不使用生日悖論方式,而且暴力窮舉,需要 1.2 年時間!
對于某些應用來說,28 分鐘可能仍然是一個可接受的時間。用半個小時暴力穿透 NAT 之后, 這個連接就可以一直用著 —— 除非 NAT 設備重啟,那樣就需要再次花半個小時穿透建個新連接。但對于 交互式應用來說,這樣顯然是不可接受的。 ? 更糟糕的是,如果去看常見的辦公網路由器,你會震驚于它的 active session low limit 有多么低。例如,一臺 Juniper SRX 300?最多支持 64,000 active sessions。也就是說, ?
如果我們想創建一個成功的穿透連接,就會把它的整張 session 表打爆?(因為我們要暴力探測 65535 個端口,每次探測都是一條新連接記錄)!這顯然要求這臺路由器能從容優雅地處理過載的情況。
這只是創建一條連接帶來的影響!如果 20 臺機器同時對這臺路由器發起穿透呢?絕對的災難!
至此,我們通過這種方式穿透了比之前更難一些的網絡拓撲。這是一個很大的成就,因為?家用路由器一般都是 easy NAT,hard NAT 一般都是辦公網路由器或云 NAT 網關。 ?
home-to-office(家->辦公室)
home-to-cloud (家->云)
這意味著這種方式能幫我們解決上述的場景,以及一部分以下場景。 ?
office-to-cloud (辦公室->云)
cloud-to-cloud (云->辦公室)
控制端口映射(port mapping)過程:UPnP/NAT-PMP/PCP 協議
如果我們能讓 NAT 設備的行為簡單點,不要把事情搞這么復雜,那建 立連接(穿透)就會簡單很多。真有這樣的好事嗎?還真有,有專門的一種協議叫?端口映射協議(port mapping protocols)。通過這種協議禁用掉前面 遇到的那些亂七八糟的東西之后,我們將得到一個非常簡單的“請求-響應”。 ? 下面是三個具體的端口映射協議: ?
UPnP IGD?(Universal Plug’n’Play Internet Gateway Device)
最老的端口控制協議, 誕生于 1990s 晚期,因此使用了很多上世紀 90 年代的技術 (XML、SOAP、multicast HTTP over UDP —— 對,HTTP over UDP ),而且很難準確和安全地實現這個協議。但以前很多路由器都內置了 UPnP 協議, 現在仍然很多。請求和響應: ? “你好,請將我的?lan-ip:port?轉發到公網(WAN)”, “好的,我已經為你分配了一個公網映射 wan-ip:port ”。 ?
NAT-PMP
UPnP IGD 出來幾年之后,Apple 推出了一個功能類似的協議,名為 NAT-PMP (NAT Port Mapping Protocol)。 ? 但與 UPnP 不同,這個協議只做端口轉發,不管是在客戶端還是服務端,實現起來都非常簡單。 ?
PCP
稍后一點,又出現了 NAT-PMP v2 版,并起了個新名字PCP (Port Control Protocol)。 ? 因此要更好地實現穿透,可以 ?
先判斷本地的默認網關上是否啟用了 UPnP IGD, NAT-PMP and PCP;
如果探測發現其中任何一種協議有響應,我們就申請一個公網端口映射;
可以將這理解為一個加強版 STUN:我們不僅能發現自己的公網 ip:port,而且能指示我們的 NAT 設備對我們的通信對端友好一些 —— 但并不是為這個端口修改或添加防火墻規則。
但我們不能假設這個協議一定可用: ?
本地 NAT 設備可能不支持這個協議;
設備支持但默認禁用了,或者沒人知道還有這么個功能,因此從來沒開過;
安全策略要求關閉這個特性。
這一點非常常見,因為 UPnP 協議曾曝出一些高危漏洞(后面都修復了,因此如果是較新的設備,可以安全地使用 UPnP —— 如果實現沒問題)。不幸的是,某些設備的配置中,UPnP, NAT-PMP,PCP 是放在一個開關里的(可能 統稱為 “UPnP” 功能),一開全開,一關全關。因此如果有人擔心 UPnP 的安全性,他連另 外兩個也用不了。 ? 最后,終歸來說,只要這種協議可用,就能有效地減少一次 NAT,大大方便建連過程。但接下來看一些不常見的場景。 ?
多 NAT 協商(Negotiating numerous NATs)
目前為止,我們看到的客戶端和服務端都各只有一個 NAT 設備。如果有多個 NAT 設備會 怎么樣?例如下面這種拓撲: ?
這個例子比較簡單,不會給穿透帶來太大問題。包從客戶端 A 經過多次 NAT 到達公網的過程,與前面分析的穿過多層有狀態防火墻是一樣的: ?
額外的這層(NAT 設備)對客戶端和服務端來說都不可見,我們的穿 透技術也不關心中間到底經過了多少層設備。
真正有影響的其實只是最后一層設備,因為對端需要在這一層設備上 找到入口讓包進來。
具體來說,真正有影響的是端口轉發協議。 ?
客戶端使用這種協議分配端口時,為我們分配端口的是最靠近客戶端的這層 NAT 設備;
而我們期望的是讓最離客戶端最遠的那層 NAT 來分配,否則我們得到的就是一個網絡中間層分配的 ip:port,對端是用不了的;
不幸的是,這幾種協議都不能遞歸地告訴我們下一層 NAT 設備是多少 —— 雖然可以用 traceroute 之類的工具來探測網絡路徑,再加上 猜路上的設備是不是 NAT 設備(嘗試發送 NAT 請求) —— 但這個就看運氣了。
這就是為什么互聯網上充斥著大量的文章說 double-NAT 有多糟糕,以 及警告用戶為保持后向兼容不要使用 double-NAT。但實際上,double-NAT 對于絕大部分 互聯網應用來說都是不可見的(透明的),因為大部分應用并不需要主動地做這種 NAT 穿 透。 ? 但我也絕不是在建議你在自己的網絡中設置 double-NAT。 ?
破壞了端口映射協議之后,某些視頻游戲的多人(multiplayer)模式就會無法使用;
也可能會使你的 IPv6 網絡無法派上用場,后者是不用 NAT 就能雙向直連的一個好方案。
但如果 double-NAT 并不是你能控制的,那除了不能用到這種端口映射協議之外,其他大部分東西都是不受影響的。 ? double-NAT 的故事到這里就結束了嗎?—— 并沒有,而且更大型的 double-NAT 場景將展現在我們面前。 ?
運營商級 NAT 帶來的問題
即使用 NAT 來解決 IPv4 地址不夠的問題,地址仍然是不夠用的,ISP(互聯網服務提供商) 顯然 無法為每個家庭都分配一個公網 IP 地址。那怎么解決這個問題呢?ISP 的做法是不夠了就再嵌套一層 NAT: ?
家用路由器將你的客戶端 SNAT 到一個 “intermediate” IP 然后發送到運營商網絡;
ISP’s network 中的 NAT 設備再將這些 intermediate IPs 映射到少量的公網 IP。
后面這種 NAT 就稱為“運營商級 NAT”(carrier-grade NAT,或稱電信級 NAT),縮寫 CGNAT。如下圖所示:
CGNAT 對 NAT 穿透來說是一個大麻煩。 ?
在此之前,辦公網用戶要快速實現 NAT 穿透,只需在他們的路由器上手動設置端口映射就行了。
但有了 CGNAT 之后就不管用了,因為你無法控制運營商的 CGNAT!
好消息是:這其實是 double-NAT 的一個小變種,因此前面介紹的解決方式大部分還仍然是適用的。某些東西可能會無法按預期工作,但只要肯給 ISP 交錢,這些也都能解決。除了 port mapping protocols,其他我們已經介紹的所有東西在 CGNAT 里都是適用的。 ? 新挑戰:同一 CGNAT 側直連,STUN 不可用 ? 但我們確實遇到了一個新挑戰:如何直連兩個在同一 CGNAT 但不同家用路由器中的對端呢?如下圖所示:
在這種情況下,STUN 就無法正常工作了:STUN 看到的是客戶端在公網(CGNAT 后面)看到的地址, 而我們想獲得的是在 “middle network” 中的 ip:port,這才是對端真正需要的地址, ?
解決方案:如果端口映射協議能用:一端做端口映射 ? 怎么辦呢?
如果你想到了端口映射協議,那恭喜,答對了!如果 peer 中任何一個 NAT 支持端口映射協議, 對我們就能實現穿透,因為它分配的 ip:port 正是對端所需要的信息。 ? 這里諷刺的是:double-NAT(指 CGNAT)破壞了端口映射協議,但在這里又救了我們!當然,我們假設這些協議一定可用,因為 CGNAT ISP 傾向于在它們的家用路由器側關閉 這些功能,已避免軟件得到“錯誤的”結果,產生混淆。 ?
解決方案:如果端口映射協議不能用:NAT hairpin 模式
如果不走運,NAT 上沒有端口映射功能怎么辦? ? 讓我們回到基于 STUN 的技術,看會發生什么。兩端在 CGNAT 的同一側,假設 STUN 告訴我們 A 的地址是 2.2.2.2:1234,B 的地址是?2.2.2.2:5678。 ? 那么接下來的問題是:如果 A 向?2.2.2.2:5678?發包會怎么樣?期望的 CGNAT 行為是: ? 執行 A 的 NAT 映射規則,即對?2.2.2.2:1234 -> 2.2.2.2:5678?進行 SNAT。 ? 注意到目的地址?2.2.2.2:5678?匹配到的是 B 的入向 NAT 映射,因此接著對這個包執行 DNAT,將目的 IP 改成 B 的私有地址。 ? 通過 CGNAT 的 internal 接口(而不是 public 接口,對應公網)將包發給 B。 ? 這種 NAT 行為有個專門的術語,叫?hairpinning(直譯為發卡,意思 是像發卡一樣,沿著一邊上去,然后從另一邊繞回來),
大家應該猜到的一個事實是:不是所以 NAT 都支持 hairpin 模式。實際上,大量 well-behaved NAT 設備都不支持 hairpin 模式, ?
因為它們都有?“只有 src_ip 是私有地址且 dst_ip 是公網地址的包才會經過我”?之類的假設。
因此對于這種目的地址不是公網、需要讓路由器把包再轉回內網的包,它們會直接丟棄。
這些邏輯甚至是直接實現在路由芯片中的,因此除非升級硬件,否則單靠軟件編程無法改變這種行為。
Hairpin 是所有 NAT 設備的特性(支持或不支持),并不是 CGNAT 獨有的。 ?
在大部分情況下,這個特性對我們的 NAT 穿透目的來說都是無所謂的,因為我們期望中?兩個 LAN NAT 設備會直接通信,不會再向上繞到它們的默認網關 CGNAT 來解決這個問題。
Hairpin 特性可有可無這件事有點遺憾,這可能也是為什么 hairpin 功能經常 broken 的原因。 ?
一旦必須涉及到 CGNAT,那 hairpinning 對連接性來說就至關重要了。
Hairpinning 使內網連接的行為與公網連接的行為完成一致,因此我們無需關心目的 地址類型,也不用知曉自己是否在一臺 CGNAT 后面。 ? 如果 hairpinning 和 port mapping protocols 都不可用,那只能降級到中繼模式了。 ?
全 IPv6 網絡:理想之地,但并非問題全無
行文至此,一些讀者可能已經對著屏幕咆哮:不要再用 IPv4 了!?花這么多時間精力解決這些沒意義的東西,還不如直接換成 IPv6! ?
的確,之所以有這些亂七八糟的東西,就是因為 IPv4 地址不夠了,我們一直在用越來越復雜的 NAT 來給 IPv4 續命。
如果 IP 地址夠用,無需 NAT 就能讓世界上的每個設備都有一個自己的公網 IP 地址,這些問題不就解決了嗎?
簡單來說,是的,這也正是 IPv6 能做的事情。但是,也只說對了一半:在理想的全 IPv6 世界中,所有這些東西會變得更加簡單,但我們面臨的問題并不會完全消失?—— 因為有狀態防火墻仍然還是存在的。 ?
辦公室中的電腦可能有一個公網 IPv6 地址,但你們公司肯定會架設一個防火墻,只允許 你的電腦主動訪問公網,而不允許反向主動建連。
其他設備上的防火墻也仍然存在,應用類似的規則。
因此,我們仍然會用到 ?
本文最開始介紹的防火墻穿透技術,以及;
幫助我們獲取自己的公網 ip:port 信息的旁路信道;
仍然需要在某些場景下 fallback 到中繼模式,例如 fallback 到最通用的 HTTP 中繼 協議,以繞過某些網絡禁止 outbound UDP 的問題。
但我們現在可以拋棄 STUN、生日悖論、端口映射協議、hairpin 等等東西了。這是一個好消息! ?
全球 IPv4/IPv6 部署現狀
另一個更加嚴峻的現實問題是:當前并不是一個全 IPv6 世界。目前世界上 ?
大部分還是 IPv4;
大約 33% 是 IPv6,而且分布極度不均勻,因此某些 通信對所在的可能是 100% IPv6,也可能是 0%,或二者之間。
不幸的是,這意味著,IPv6 **還**無法作為我們的解決方案。就目前來說,它只是我們的工具箱中的一個備選。對于某些 peer 來說,它簡直是完美工 具,但對其他 peer 來說,它是用不了的。如果目標是“任何情況下都能穿透(連接) 成功”,那我們就仍然需要 IPv4+NAT 那些東西。 ?
新場景:NAT64/DNS64
IPv4/IPv6 共存也引出了一個新的場景:NAT64 設備。
前面介紹的都是 NAT44 設備:它們將一個 IPv4 地址轉換成另一 IPv4 地址。NAT64 從名字可以看出,是將一個內側 IPv6 地址轉換成一個外側 IPv4 地址。利用 DNS64 設備,我們能將 IPv4 DNS 應答給 IPv6 網絡,這樣對終端來說,它看到的就是一個 全 IPv6 網絡,而仍然能訪問 IPv4 公網。 ? Incidentally, you can extend this naming scheme indefinitely. There have been some experiments with NAT46; you could deploy NAT66 if you enjoy chaos; and some RFCs use NAT444 for carrier-grade NAT. ? 如果需要處理 DNS 問題,那這種方式工作良好。例如,如果連接到 google.com,將這個域名解析成 IP 地址的過程會涉及到 DNS64 設備,它又會進一步 involve NAT64 設備,但后一步對用戶來說是無感知的。 ? 但對于 NAT 和防火墻穿透來說,我們會關心每個具體的 IP 地址和端口。 ? 解決方案:CLAT (Customer-side transLATor) ? 如果設備支持 CLAT (Customer-side translator — from Customer XLAT),那我們就很幸運: ?
CLAT 假裝操作系統有直接 IPv4 連接,而背后使用的是 NAT64,以對應用程序無感知。在有 CLAT 的設備上,我們無需做任何特殊的事情。
CLAT 在移動設備上非常常見,但在桌面電腦、筆記本和服務器上非常少見, 因此在后者上,必須自己做 CLAT 做的事情:檢測 NAT64+DNS64 的存在,然后正確地使用它們。
解決方案:CLAT 不存在時,手動穿透 NAT64 設備
首先檢測是否存在 NAT64+DNS64。
方法很簡單:向 ipv4only.arpa. 發送一個 DNS 請求。這個域名會解析 到一個已知的、固定的 IPv4 地址,而且是純 IPv4 地址。如果得到的 是一個 IPv6 地址,就可以判斷有 DNS64 服務器做了轉換,而它必然會用到 NAT64。這樣 就能判斷出 NAT64 的前綴是多少。 ?
此后,要向 IPv4 地址發包時,發送格式為{NAT64 prefix + IPv4 address}?的 IPv6 包。類似地,收到來源格式為?{NAT64 prefix + IPv4 address}?的包時,就是 IPv4 流量。
接下來,通過 NAT64 網絡與 STUN 通信來獲取自己在 NAT64 上的公網 ip:port,接 下來就回到經典的 NAT 穿透問題了 —— 除了需要多做一點點事情。
幸運的是,如今的大部分 v6-only 網絡都是移動運營商網絡,而幾乎所有手機都支持 CLAT。運營 v6-only 網絡的 ISPs 會在他們給你的路由器上部署 CLAT,因此最后你其實不需要做什么事情。但如果想實現 100% 穿透,就需要解決這種邊邊角角的問題,即必須顯式支持從 v6-only 網絡連接 v4-only 對端。 ?
將所有解決方式集成到 ICE 協議
針對具體場景,該選擇哪種穿透方式? ? 至此,我們的 NAT 穿透之旅終于快結束了。我們已經覆蓋了有狀態防火墻、簡單和高級 NAT、IPv4 和 IPv6。只要將以上解決方式都實現了,NAT 穿透的目的就達到了! ? 但是: ?
對于給定的 peer,如何判斷改用哪種方式呢?
如何判斷這是一個簡單有狀態防火墻的場景,還是該用到生日悖論算法,還是需要手動處理 NAT64 呢?
還是通信雙方在一個 WiFi 網絡下,連防火墻都沒有,因此不需要任何操作呢?
早期 NAT 穿透比較簡單,能讓我們精確判斷出 peer 之間的路徑特點,然后針對性地采用相應的解決方式。但后面,網絡工程師和 NAT 設備開發工程師引入了一些新理念,給路徑判斷造成很大困難。因此 我們需要簡化客戶端側的思考(判斷邏輯)。 ? 這就要提到 Interactive Connectivity Establishment (ICE,交換式連接建立) 協議了。與 STUN/TURN 類似,ICE 來自電信領域,因此其 RFC 充滿了 SIP、SDP、信令會話、撥號等等電話術語。但如果忽略這些領域術語,我們會看到它描述了一個極其優雅的判斷最佳連接路徑的算法。 ? 真的?這個算法是:每種方法都試一遍,然后選擇最佳的那個方法。就是這個算法,驚喜嗎? ? 來更深入地看一下這個算法。 ? ICE (Interactive Connectivity Establishment) 算法 ? 這里的討論不會嚴格遵循 ICE spec,因此如果是在自己實現一個可互操作的 ICE 客戶端,應該通讀RFC 8445, 根據它的描述來實現。這里忽略所有電信術語,只關注核心的算法邏輯, 并提供幾個在 ICE 規范允許范圍的靈活建議。 ? 1)為實現和某個 peer 的通信,首先需要確定我們自己用的(客戶端側)這個 socket 的地址, 這是一個列表,至少應該包括: ?
我們自己的 IPv6?ip:ports
我們自己的 IPv4 LAN?ip:ports(局域網地址)
通過 STUN 服務器獲取到的我們自己的 IPv4 WAN?ip:ports(公網地址,可能會經過 NAT64 轉換)
通過端口映射協議獲取到的我們自己的 IPv4 WAN?ip:port(NAT 設備的端口映射協議分配的公網地址)
運營商提供給我們的 endpoints(例如,靜態配置的端口轉發)
2)通過旁路信道與 peer 互換這個列表。兩邊都拿到對方的列表后,就開始互相探測對方提供的地址。?列表中地址沒有優先級,也就是說,如果對方給的了 15 個地址,那我們應該把這 15 個地址都探測一遍。 ? 這些探測包有兩個目的: ?
打開防火墻,穿透 NAT,也就是本文一直在介紹的內容;
健康檢測。我們在不斷交換(最好是已認證的)“ping/pong” 包,來檢測某個特定的路徑是不是端到端通的。
3)最后,一小會兒之后,從可用的備選地址中(根據某些條件)選擇“最佳”的那個,任務完成! ? 這個算法的優美之處在于:只要選擇最佳線路(地址)的算法是正確的,那就總能獲得最佳路徑。 ?
ICE 會預先對這些備選地址進行排序(通常:LAN > WAN > WAN+NAT),但用戶也可以自己指定這個排序行為。
從 v0.100.0 開始,Tailscale 從原來的 hardcode 優先級切換成了根據 round-trip latency 的方式,它大部分情況下排序的結果和 LAN > WAN > WAN+NAT 是一致的。但相比于靜態排序,我們是動態計算每條路徑應該屬于哪個類別。
ICE spec 將協議組織為兩個階段: ?
探測階段
通信階段
但不一定要嚴格遵循這兩個步驟的順序。在 Tailscale, ?
我們發現更優的路徑之后就會自動切換過去;
所有的連接都是先選擇 DERP 模式(中繼模式)。這意味著連接立即就能建立(優先級最低但 100% 能成功的模式),用戶不用任何等待;
然后并行進行路徑發現。通常幾秒鐘之后,我們就能發現一條更優路徑,然后將現有連接透明升級(upgrade)過去。
但有一點需要關心:非對稱路徑。ICE 花了一些精力來保證通信雙方選擇的是相同的網絡 路徑,這樣才能保證這條路徑上有雙向流量,能保持防火墻和 NAT 設備的連接一直處于 open 狀態。自己實現的話,其實并不需要花同樣大的精力來實現這個保證,但需要確保你所有使用的所有路徑上,都有雙向流量。這個目標就很簡單了,只需要定期在所有已使用的路徑上發 ping/pong 就行了。 ? 健壯性與降級 ? 要實現健壯性,還需要檢測當前已選擇的路徑是否已經失敗了(例如,NAT 設備維護清掉了所有狀態), 如果失敗了就要降級(downgrade)到其他路徑。這里有兩種方式: ?
持續探測所有路徑,維護一個降級時會用的備用地址列表;
直接降級到保底的中繼模式,然后再通過路徑探測升級到更好的路徑。
考慮到發生降級的概率是非常小的,因此這種方式可能是更經濟的。
安全
最后需要提到安全。 ? 本文的所有內容都假設:我們使用的上層協議已經有了自己的安全機制( 例如 QUIC 協議有 TLS 證書,WireGuard 協議有自己的公鑰)。如果還沒有安全機制,那顯然是要立即補上的。一旦動態切換路徑,基于 IP 的安全機制就是無用的了?(IP 協議最開始就沒怎么考慮安全性),至少要有端到端的認證。
嚴格來說,如果上層協議有安全機制,那即使收到是欺騙性的 ping/pong 流量,問題都不大, 最壞的情況也就是攻擊者誘導兩端通過他們的系統來中繼流量。而有了端到端安全機制,這并不是一個大問題(取決于你的威脅模型)。
但出于謹慎考慮,最好還是對路徑發現的包也做認證和加密。具體如何做可以咨詢你們的應用安全工程師。
結束語
我們終于完成了 NAT 穿透的目標! ? 如果實現了以上提到的所有技術,你將得到一個業內領先的 NAT 穿透軟件,能在絕大多數場景下實現端到端直連。如果直連不了,還可以降級到保底的中繼模式(對于長尾來說只能靠中繼了)。 ? 但這些工作相當復雜!其中一些問題研究起來很有意思,但很難做到完全正確,尤其是那些 非常邊邊角角的場景,真正出現的概率極小,但解決它們所需花費的經歷又極大。不過,這種工作只需要做一次,一旦解決了,你就具備了某種超級能力:探索令人激動的、相對還比較嶄新的端到端應用(peer-to-peer applications)世界。 ?
跨公網 端到端直連
去中心化軟件領域中的許多有趣想法,簡化之后其實都變成了?跨過公網(互聯網)實現端到端直連?這一問題,開始時可能覺得很簡單,但真正做才發現比想象中難多了。現在知道如何解決這個問題了,動手開做吧! ?
結束語之 TL; DR
實現健壯的 NAT 穿透需要下列基礎: ?
一種基于 UDP 的協議;
能在程序內直接訪問 socket;
有一個與 peer 通信的旁路信道;
若干 STUN 服務器;
一個保底用的中繼網絡(可選,但強烈推薦)。
然后需要: ?
遍歷所有的 ip:port;
查詢 STUN 服務器來獲取自己的公網 ip:port 信息,以及判斷自己這一側的 NAT 的“難度”(difficulty);
使用 port mapping 協議來獲取更多的公網 ip:ports;
檢查 NAT64,通過它獲取自己的公網 ip:port;
將自己的所有公網 ip:ports 信息通過旁路信道與 peer 交換,以及某些加密秘鑰來保證通信安全;
通過保底的中繼方式與對方開始通信(可選,這樣連接能快速建立)
如果有必要/想這么做,探測對方的提供的所有 ip:port,以及執行生日攻擊(birthday attacks)來穿透 harder NAT;
發現更優路徑之后,透明升級到該路徑;
如果當前路徑斷了,降級到其他可用的路徑;
確保所有東西都是加密的,并且有端到端認證。
編輯:黃飛
評論
查看更多