一、引言
1.1 背景:IPv4地址短缺,引入NAT
全球IPv4地址早已不夠用,因此人們發明了NAT(網絡地址轉換)來緩解這個問題。
簡單來說,大部分機器都使用私有IP地址,如果它們需要訪問公網服務,那么,
- 出向流量:需要經過一臺NAT設備,它會對流量進行SNAT,將私有srcIP+Port轉換成NAT設備的公網IP+Port(這樣應答包才能回來),然后再將包發出去;
- 應答流量(入向):到達NAT設備后進行相反的轉換,然后再轉發給客戶端。
整個過程對雙方透明。
更多關于NAT的內容,可參考(譯)NAT-網絡地址轉換(2016)。譯注。
以上就是本文所討論的問題的基本背景。
1.2 需求:兩臺經過NAT的機器建立點對點連接
在以上所描述的NAT背景下,我們從最簡單的問題開始:如何在兩臺經過NAT的機器之間建立點對點連接(直連)。如下圖所示:
直接用機器的IP互連顯然是不行的,因為它們都是私有IP(例如192.168.1.x)。在Tailscale中,我們會建立一個WireGuard®隧道來解決這個問題——但這并不是太重要,因為我們將過去幾代人努力都整合到了一個工具集,這些技術廣泛適用于各種場景。例如,
- WebRTC使用這些技術在瀏覽器之間完成peer-to-peer語音、視頻和數據傳輸,
- VoIP電話和一些視頻游戲也使用類似機制,雖然不是所有情況下都很成功。
接下來,本文將在一般意義上討論這些技術,并在合適的地方拿Tailscale和其他一些東西作為例子。
1.3 方案:NAT穿透
1.3.1 兩個必備前提:UDP+能直接控制socket
如果想設計自己的協議來實現NAT穿透,那必須滿足以下兩個條件:
- 協議應該基于UDP。
理論上用TCP也能實現,但它會給本已相當復雜的問題再增加一層復雜性,甚至還需要定制化內核——取決于你想實現到什么程度。本文接下來都將關注在UDP上。
如果考慮TCP是想在NAT穿透時獲得面向流的連接(stream-orientedconnection),可以考慮用QUIC來替代,它構建在UDP之上,因此我們能將關注點放在UDPNAT穿透,而仍然能獲得一個很好的流協議(streamprotocol)。
- 對收發包的socket有直接控制權。
例如,從經驗上來說,無法基于某個現有的網絡庫實現NAT穿透,因為我們必須在使用的“主要”協議之外,發送和接收額外的數據包。
某些協議(例如WebRTC)將NAT穿透與其他部分緊密集成。但如果你在構建自己的協議,建議將NAT穿透作為一個獨立實體,與主協議并行運行,二者僅僅是共享socket的關系,如下圖所示,這將帶來很大幫助:
1.3.2 保底方式:中繼
在某些場景中,直接訪問socket這一條件可能很難滿足。
退而求其次的一個方式是設置一個localproxy(本地代理),主協議與這個proxy通信,后者來完成NAT穿透,將包中繼(relay)給對端。這種方式增加了一個額外的間接成本,但好處是:
- 仍然能獲得NAT穿透,
- 不需要對已有的應用程序做任何改動。
1.4 挑戰:有狀態防火墻和NAT設備
有了以上鋪墊,下面就從最基本的原則開始,一步步看如何實現一個企業級的NAT穿透方案。
我們的目標是:在兩個設備之間通過UDP實現雙向通信,有了這個基礎,上層的其他協議(WireGuard,QUIC,WebRTC等)就能做一些更酷的事情。
但即便這個看似最基本的功能,在實現上也要解決兩個障礙:
- 有狀態防火墻
- NAT設備
二、穿透防火墻
有狀態防火墻是以上兩個問題中相對比較容易解決的。實際上,大部分NAT設備都自帶了一個有狀態防火墻,因此要解決第二個問題,必須先解決有第一個問題。
有狀態防火墻具體有很多種類型,有些你可能見過:
- WindowsDefenderfirewall
- Ubuntu’sufw(usingiptables/nftables)
- BSD/macOSpf
- AWSSecurityGroups(安全組)
2.1 有狀態防火墻
2.1.1 默認行為(策略)
以上防火墻的配置都是很靈活的,但大部分配置默認都是如下行為:
- 允許所有出向連接(allowsall“outbound”connections)
- 禁止所有入向連接(blocksall“inbound”connections)
可能有少量例外規則,例如allowinginboundSSH。
2.1.2 如何區分入向和出向包
連接(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穿透來說非常友好,但已經越來越少見了。
2.2 防火墻朝向(face-off)與穿透方案
2.2.1 防火墻朝向相同
場景特點:服務端IP可直接訪問
在NAT穿透場景中,以上默認規則對UDP流量的影響不大——只要路徑上所有防火墻的“朝向”是一樣的。一般來說,從內網訪問公網上的某個服務器都屬于這種情況。
我們唯一的要求是:連接必須是由防火墻后面的機器發起的。這是因為在它主動和別人通信之前,沒人能主動和它通信,如下圖所示:
穿透方案:客戶端直連服務端,或hub-and-spoke拓撲
但上圖是假設了通信雙方中,其中一端(服務端)是能直接訪問到的。在VPN場景中,這就形成了所謂的hub-and-spoke拓撲:中心的hub沒有任何防火墻策略,誰都能訪問到;防火墻后面的spokes連接到hub。如下圖所示:
2.2.2 防火墻朝向不同的方向
場景特點:服務端IP不可直接訪問
但如果兩個“客戶端”相連,以上方式就不行了,此時兩邊的防火墻相向而立,如下圖所示:
根據前面的討論,這種情況意味著:兩邊要同時發起連接請求,但也意味著兩邊都無法發起有效請求,因為對方先發起請求才能在它的防火墻上打開一條縫讓我們進去!如何破解這個問題呢?一種方式是讓用戶重新配置一邊或兩邊的防火墻,打開一個端口,允許對方的流量進來。
- 這顯然對用戶不友好,在像Tailscale這樣的mesh網絡中的擴展性也不好,在mesh網絡中,我們假設對端會以一定的粒度在公網上移動。
- 此外,在很多情況下用戶也沒有防火墻的控制權限:例如在咖啡館或機場中,連接的路由器是不受你控制的(否則你可能就有麻煩了)。
因此,我們需要尋找一種不用重新配置防火墻的方式。
穿透方案:兩邊同時主動建連,在本地防火墻為對方打開一個洞
解決的思路還是先重新審視前面提到的有狀態防火墻規則:
- 對于UDP,其規則(邏輯)是:包必須先出去才能進來(packetsmustflowoutbeforepacketscanflowbackin)。
- 注意,這里除了要滿足包的IP和端口要匹配這一條件之外,并沒有要求包必須是相關的(related)。換句話說,只要某些包帶著正確的來源和目的地址出去了,任何看起來像是響應的包都會被防火墻放進來——即使對端根本沒收到你發出去的包。
因此,要穿透這些有狀態防火墻,我們只需要共享一些信息:讓兩端提前知道對方使用的ip:port:
- 手動靜態配置是一種方式,但顯然擴展性不好;
- 我們開發了一個coordinationserver,以靈活、安全的方式來同步ip:port信息。
有了對方的ip:port信息之后,兩端開始給對方發送UDP包。在這個過程中,我們預料到某些寶物將會被丟棄。因此,雙方必須要接受某些包會丟失的事實,因此如果是重要信息,你必須自己準備好重傳。對UDP來說丟包是可接受的,但這里尤其需要接受。
來看一下具體建連(穿透)過程:
- 如圖所示,筆記本出去的第一包,2.2.2.2:1234->7.7.7.7:5678,穿過WindowsDefender防火墻進入到公網。
對方的防火墻會將這個包攔截掉,因為它沒有7.7.7.7:5678->2.2.2.2:1234的流量記錄。但另一方面,WindowsDefender此時已經記錄了出向連接,因此會允許7.7.7.7:5678->2.2.2.2:1234的應答包進來。
- 接著,第一個7.7.7.7:5678->2.2.2.2:1234穿過它自己的防火墻到達公網。
到達客戶端側時,WindowsDefender認為這是剛才出向包的應答包,因此就放行它進入了!此外,右側的防火墻此時也記錄了:2.2.2.2:1234->7.7.7.7:5678的包應該放行。
- 筆記本收到服務器發來的包之后,發送一個包作為應答。這個包穿過WindowsDefender防火墻和服務端防火墻(因為這是對服務端發送的包的應答包),達到服務端。
成功!這樣我們就建立了一個穿透兩個相向防火墻的雙向通信連接。而初看之下,這項任務似乎是不可能完成的。
2.3 關于穿透防火墻的一些思考
穿透防火墻并非永遠這么輕松,有時會受一些第三方系統的間接影響,需要仔細處理。那穿透防火墻需要注意什么呢?重要的一點是:通信雙方必須幾乎同時發起通信,這樣才能在路徑上的防火墻打開一條縫,而且兩端還都是活著的。
2.3.1 雙向主動建連:旁路信道
如何實現“同時”呢?一種方式是兩端不斷重試,但顯然這種方式很浪費資源。假如雙方都知道何時開始建連就好了。
- 這聽上去是雞生蛋蛋生雞的問題了:雙方想要通信,必須先提前通個信。
- 但實際上,我們可以通過旁路信道(sidechannel)來達到這個目的,并且這個旁路信道并不需要很fancy:它可以有幾秒鐘的延遲、只需要傳送幾KB的信息,因此即使是一個配置非常低的虛擬機,也能為幾千臺機器提供這樣的旁路通信服務。在遙遠的過去,我曾用XMPP聊天消息作為旁路,效果非常不錯。另一個例子是WebRTC,它需要你提供一個自己的“信令信道”(signallingchannel,這個詞也暗示了WebRTC的IPtelephonyancestry),并將其配置到WebRTCAPI。在Tailscale,我們的協調服務器(coordinationserver)和DERP(DetourEncryptedRoutingProtocol)服務器集群是我們的旁路信道。
2.3.2 非活躍連接被防火墻清理
有狀態防火墻內存通常比較有限,因此會定期清理不活躍的連接(UDP常見的是30s),因此要保持連接alive的話需要定期通信,否則就會被防火墻關閉,為避免這個問題,我們,
- 要么定期向對方發包來keepalive,
- 要么有某種帶外方式來按需重建連接。
2.3.3 問題都解決了?不,挑戰剛剛開始
對于防火墻穿透來說,我們并不需要關心路徑上有幾堵墻——只要它們是有狀態防火墻且允許出向連接,這種同時發包(simultaneoustransmission)機制就能穿透任意多層防火墻。這一點對我們來說非常友好,因為只需要實現一個邏輯,然后能適用于任何地方了。
…對嗎?
其實,不完全對。這個機制有效的前提是:我們能提前知道對方的ip:port。而這就涉及到了我們今天的主題:NAT,它會使前面我們剛獲得的一點滿足感頓時消失。
下面,進入本文正題。
三、NAT的本質
3.1 NAT設備與有狀態防火墻
可以認為NAT設備是一個增強版的有狀態防火墻,雖然它的增強功能對于本文場景來說并不受歡迎:除了前面提到的有狀態攔截/放行功能之外,它們還會在數據包經過時修改這些包。
3.2 NAT穿透與SNAT/DNAT
具體來說,NAT設備能完成某種類型的網絡地址轉換,例如,替換源或目的IP地址或端口。
- 討論連接問題和NAT穿透問題時,我們只會受sourceNAT——SNAT的影響。
- DNAT不會影響NAT穿透。
3.3 SNAT的意義:解決IPv4地址短缺問題
SNAT最常見的使用場景是將很多設備連接到公網,而只使用少數幾個公網IP。例如對于消費級路由器,會將所有設備的(私有)IP地址映射為單個連接到公網的IP地址。
這種方式存在的意義是:我們有遠多于可用公網IP數量的設備需要連接到公網,(至少對IPv4來說如此,IPv6的情況后面會討論)。NAT使多個設備能共享同一IP地址,因此即使面臨IPv4地址短缺的問題,我們仍然能不斷擴張互聯網的規模。
3.4 SNAT過程:以家用路由器為例
假設你的筆記本連接到家里的WiFi,下面看一下它連接到公網某個服務器時的情形:
- 筆記本發送UDPpacket192.168.0.20:1234->7.7.7.7:5678。
這一步就好像筆記本有一個公網IP一樣,但源地址192.168.0.20是私有地址,只能出現在私有網絡,公網不認,收到這樣的包時它不知道如何應答。
- 家用路由器出場,執行SNAT。
包經過路由器時,路由器發現這是一個它沒有見過的新會話(session)。它知道192.168.0.20是私有IP,公網無法給這樣的地址回包,但它有辦法解決:
- 在它自己的公網IP上挑一個可用的UDP端口,例如2.2.2.2:4242,然后創建一個NATmapping:192.168.0.20:1234<-->2.2.2.2:4242,然后將包發到公網,此時源地址變成了2.2.2.2:4242而不是原來的192.168.0.20:1234。因此服務端看到的是轉換之后地址,接下來,每個能匹配到這條映射規則的包,都會被路由器改寫IP和端口。
- 反向路徑是類似的,路由器會執行相反的地址轉換,將2.2.2.2:4242變回192.168.0.20:1234。對于筆記本來說,它根本感知不知道這正反兩次變換過程。
這里是拿家用路由器作為例子,但辦公網的原理是一樣的。不同之處在于,辦公網的NAT可能有多臺設備組成(高可用、容量等目的),而且它們有不止一個公網IP地址可用,因此在選擇可用的公網ip:port來做映射時,選擇空間更大,能支持更多客戶端。
3.5 SNAT給穿透帶來的挑戰
現在我們遇到了與前面有狀態防火墻類似的情況,但這次是NAT設備:通信雙方不知道對方的ip:port是什么,因此無法主動建連,如下圖所示:
但這次比有狀態防火墻更糟糕,嚴格來說,在雙方發包之前,根本無法確定(自己及對方的)ip:port信息,因為只有出向包經過路由器之后才會產生NATmapping(即,可以被對方連接的ip:port信息)。
因此我們又回到了與防火墻遇到的問題,并且情況更糟糕:雙方都需要主動和對方建連,但又不知道對方的公網地址是多少,只有當對方先說話之后,我們才能拿到它的地址信息。
如何破解以上死鎖呢?這就輪到STUN登場了。
四、穿透“NAT+防火墻”:STUN(SessionTraversalUtilitiesforNAT)協議
STUN既是一些對NAT設備行為的詳細研究,也是一種協助NAT穿透的協議。本文主要關注STUN協議。
4.1 STUN原理
STUN基于一個簡單的觀察:從一個會被NAT的客戶端訪問公網服務器時,服務器看到的是NAT設備的公網ip:port地址,而非該客戶端的局域網ip:port地址。
也就是說,服務器能告訴客戶端它看到的客戶端的ip:port是什么。因此,只要將這個信息以某種方式告訴通信對端(peer),后者就知道該和哪個地址建連了!這樣就又簡化為前面的防火墻穿透問題了。
本質上這就是STUN協議的工作原理,如下圖所示:
- 筆記本向STUN服務器發送一個請求:“從你的角度看,我的地址什么?”
- STUN服務器返回一個響應:“我看到你的UDP包是從這個地址來的:ip:port”。
4.2 為什么NAT穿透邏輯和主協議要共享同一個socket
理解了STUN原理,也就能理解為什么我們在文章開頭說,如果要實現自己的NAT穿透邏輯和主協議,就必須讓二者共享同一個socket:
- 每個socket在NAT設備上都對應一個映射關系(私網地址->公網地址),
- STUN服務器只是輔助穿透的基礎設施,
- 與STUN服務器通信之后,在NAT及防火墻設備上打開了一個連接,允許入向包進來(回憶前面內容,只要目的地址對,UDP包就能進來,不管這些包是不是從STUN服務器來的),
- 因此,接下來只要將這個地址告訴我們的通信對端(peer),讓它往這個地址發包,就能實現穿透了。
4.3 STUN的問題:不能穿透所有NAT設備(例如企業級NAT網關)
有了STUN,我們的穿透目的似乎已經實現了:每臺機器都通過STUN來獲取自己的私網socket對應的公網ip:port,然后把這個信息告訴對端,然后兩端同時發起穿透防火墻的嘗試,后面的過程就和上一節介紹的防火墻穿透一樣了,對嗎?
答案是:看情況。某些情況下確實如此,但有些情況下卻不行。通常來說,
- 對于大部分家用路由器場景,這種方式是沒問題的;
- 但對于一些企業級NAT網關來說,這種方式無法奏效。
NAT設備的說明書上越強調它的安全性,STUN方式失敗的可能性就越高。(但注意,從實際意義上來說,NAT設備在任何方面都并不會增強網絡的安全性,但這不是本文重點,因此不展開。)
4.4 重新審視STUN的前提
再次審視前面關于STUN的假設:當STUN服務器告訴客戶端在公網看來它的地址是2.2.2.2:4242時,那所有目的地址是2.2.2.2:4242的包就都能穿透防火墻到達該客戶端。
這也正是問題所在:這一點并不總是成立。
- 某些NAT設備的行為與我們假設的一致,它們的有狀態防火墻組件只要看到有客戶端自己發起的出向包,就會允許相應的入向包進入;因此只要利用STUN功能,再加上兩端同時發起防火墻穿透,就能把連接打通;
- 另外一些NAT設備就要困難很多了,它會針對每個目的地址來生成一條相應的映射關系。在這樣的設備上,如果我們用相同的socket來分別發送數據包到5.5.5.5:1234and7.7.7.7:2345,我們就會得到2.2.2.2上的兩個不同的端口,每個目的地址對應一個。如果反向包的端口用的不對,包就無法通過防火墻。如下圖所示:
五、中場補課:NAT正式術語
知道NAT設備的行為并不是完全一樣之后,我們來引入一些正式術語。
5.1 早期術語
如果之前接觸過NAT穿透,可能會聽說過下面這些名詞:
- “FullCone”
- “RestrictedCone”
- “Port-RestrictedCone”
- “Symmetric”NATs
這些都是NAT穿透領域的早期術語。
但其實這些術語相當讓人困惑。我每次都要查一下RestrictedConeNAT是什么意思。從實際經驗來看,我并不是唯一對此感到困惑的人。例如,如今互聯網上將“easy”NAT歸類為FullCone,而實際上它們更應該歸類為Port-RestrictedCone。
5.2 近期研究與新術語
最近的一些研究和RFC已經提出了一些更準確的術語。
- 首先,它們明確了如下事實:NAT設備的行為差異表現在多個維度,而并非只有早期研究中所說的“cone”這一個維度,因此基于“cone”來劃分類別并不是很有幫助。
- 其次,新研究和新術語能更準確地描述NAT在做什么。
前面提到的所謂"easy"和"hard"NAT,只在一個維度有不同:NAT映射是否考慮到目的地址信息。RFC4787中,
- 將easyNAT及其變種稱為“Endpoint-IndependentMapping”(EIM,終點無關的映射)
但是,從“命名很難”這一程序員界的偉大傳統來說,EIM這個詞其實也并不是100%準確,因為這種NAT仍然依賴endpoint,只不過依賴的是源endpoint:每個sourceip:port對應一個映射——否則你的包就會和別人的包混在一起,導致混亂。
嚴格來說,EIM應該稱為“DestinationEndpointIndependentMapping”(DEIM?),但這個名字太拗口了,而且按照慣例,Endpoint永遠指的是DestinationEndpoint。
- 將hardNAT以及變種稱為“Endpoint-DependentMapping”(EDM,終點相關的映射)。
EDM中還有一個子類型,依據是只根據dst_ip做映射,還是根據dst_ip+dst_port做映射。對于NAT穿透來說,這種區分對來說是一樣的:它們都會導致STUN方式不可用。
5.3 老的cone類型劃分
你可能會有疑問:根據是否依賴endpoint這一條件,只能組合出兩種可能,那為什么傳統分類中會有四種cone類型呢?答案是cone包含了兩個正交維度的NAT行為:
- NAT映射行為:前面已經介紹過了,
- 有狀態防火墻行為:與前者類似,也是分為與endpoint相關還是無關兩種類型。
因此最終組合如下:
NATConeTypes
Endpoint無關NATmapping
Endpoint相關NATmapping(alltypes)
Endpoint無關防火墻
FullConeNAT
N/A*
Endpoint相關防火墻(dst.IPonly)
RestrictedConeNAT
N/A*
Endpoint相關防火墻(dst.IP+port)
Port-RestrictedConeNAT
SymmetricNAT
分解到這種程度之后就可以看出,cone類型對NAT穿透場景來說并沒有什么意義。我們關心的只有一點:是否是Symmetric——換句話說,一個NAT設備是EIM還是EDM類型的。
5.4 針對NAT穿透場景:簡化NAT分類
以上討論可知,雖然理解防火墻的具體行為很重要,但對于編寫NAT穿透代碼來說,這一點并不重要。我們的兩端同時發包方式(simultaneoustransmissiontrick)能有效穿透以上三種類型的防火墻。在真實場景中,我們主要在處理的是IP-and-portendpoint-dependent防火墻。
因此,對于實際NAT穿透實現,我們可以將以上分類簡化成:
Endpoint-IndependentNATmapping
Endpoint-DependentNATmapping(dst.IPonly)
Firewallisyes
EasyNAT
HardNAT
5.5 更多NAT規范(RFC)
想了解更多新的NAT術語,可參考
- RFC4787(NATBehavioralRequirementsforUDP)
- RFC5382(forTCP)
- RFC5508(forICMP)
如果自己實現NAT,那應該(should)遵循這些RFC的規范,這樣才能使你的NAT行為符合業界慣例,與其他廠商的設備或軟件良好兼容。
六、穿透NAT+防火墻:STUN不可用時,fallback到中繼模式
6.1 問題回顧與保底方式(中繼)
補完基礎知識(尤其是定義了什么是hardNAT)之后,回到我們的NAT穿透主題。
- 第1~4節已經解決了STUN和防火墻穿透的問題,
- 但hardNAT對我們來說是個大問題,只要路徑上出現一個這種設備,前面的方案就行不通了。
準備放棄了嗎?這才進入NAT真正有挑戰的部分:如果已經試過了前面介紹的所有方式仍然不能穿透,我們該怎么辦呢?
- 實際上,確實有很多NAT實現在這種情況下都會選擇放棄,向用戶報一個“無法連接”之類的錯誤。
- 但對我們來說,這么快就放棄顯然是不可接受的——解決不了連通性問題,Tailscale就沒有存在的意義。
我們的保底解決方式是:創建一個中繼連接(relay)實現雙方的無障礙地通信。但是,中繼方式性能不是很差嗎?這要看具體情況:
- 如果能直連,那顯然沒必要用中繼方式;
- 但如果無法直連,而中繼路徑又非常接近雙方直連的真實路徑,并且帶寬足夠大,那中繼方式并不會明顯降低通信質量。延遲肯定會增加一點,帶寬會占用一些,但相比完全連接不上,還是更能讓用戶接受的。
不過要注意:我們只有在無法直連時才會選擇中繼方式。實際場景中,
- 對于大部分網絡,我們都能通過前面介紹的方式實現直連,
- 剩下的長尾用中繼方式來解決,并不算一個很糟的方式。
此外,某些網絡會阻止NAT穿透,其影響比這種hardNAT大多了。例如,我們觀察到UCBerkeleyguestWiFi禁止除DNS流量之外的所有outboundUDP流量。不管用什么NAT黑科技,都無法繞過這個攔截。因此我們終歸還是需要一些可靠的fallback機制。
6.2 中繼協議:TURN、DERP
有多種中繼實現方式。
- TURN(TraversalUsingRelaysaroundNAT):經典方式,核心理念是
- 用戶(人)先去公網上的TURN服務器認證,成功后后者會告訴你:“我已經為你分配了ip:port,接下來將為你中繼流量”,然后將這個ip:port地址告訴對方,讓它去連接這個地址,接下去就是非常簡單的客戶端/服務器通信模型了。
Tailscale并不使用TURN。這種協議用起來并不是很好,而且與STUN不同,它沒有真正的交互性,因為互聯網上并沒有公開的TURN服務器。
- DERP(DetouredEncryptedRoutingProtocol)
這是我們創建的一個協議,DERP,
- 它是一個通用目的包中繼協議,運行在HTTP之上,而大部分網絡都是允許HTTP通信的。它根據目的公鑰(destination’spublickey)來中繼加密的流量(encryptedpayloads)。
前面也簡單提到過,DERP既是我們在NAT穿透失敗時的保底通信方式(此時的角色與TURN類似),也是在其他一些場景下幫助我們完成NAT穿透的旁路信道。換句話說,它既是我們的保底方式,也是有更好的穿透鏈路時,幫助我們進行連接升級(upgradetoapeer-to-peerconnection)的基礎設施。
6.3 小結
有了“中繼”這種保底方式之后,我們穿透的成功率大大增加了。如果此時不再閱讀本文接下來的內容,而是把上面介紹的穿透方式都實現了,我預計:
- 90%的情況下,你都能實現直連穿透;
- 剩下的10%里,用中繼方式能穿透一些(some);
這已經算是一個“足夠好”的穿透實現了。
七、穿透NAT+防火墻:企業級改進
如果你并不滿足于“足夠好”,那我們可以做的事情還有很多!
本節將介紹一些五花八門的tricks,在某些特殊場景下會幫到我們。單獨使用這項技術都無法解決NAT穿透問題,但將它們巧妙地組合起來,我們能更加接近100%的穿透成功率。
7.1 穿透hardNAT:暴力端口掃描
回憶hardNAT中遇到的問題,如下圖所示,關鍵問題是:easyNAT不知道該往hardNAT方的哪個ip:port發包。
但必須要往正確的ip:port發包,才能穿透防火墻,實現雙向互通。怎么辦呢?
- 首先,我們能知道hardNAT的一些ip:port,因為我們有STUN服務器。
這里先假設我們獲得的這些IP地址都是正確的(這一點并不總是成立,但這里先這么假設。而實際上,大部分情況下這一點都是成立的,如果對此有興趣,可以參考REQ-2inRFC4787)。
- IP地址確定了,剩下的就是端口了。總共有65535中可能,我們能遍歷這個端口范圍嗎?
如果發包速度是100packets/s,那最壞情況下,需要10分鐘來找到正確的端口。還是那句話,這雖然不是最優的,但總比連不上好。
這很像是端口掃描(事實上,確實是),實際中可能會觸發對方的網絡入侵檢測軟件。
7.2 基于生日悖論改進暴力掃描:hardside多開端口+easyside隨機探測
利用birthdayparadox算法,我們能對端口掃描進行改進。
- 上一節的基本前提是:hardside只打開一個端口,然后easyside暴力掃描65535個端口來尋找這個端口;
- 這里的改進是:在hardsize開多個端口,例如256個(即同時打開256個socket,目的地址都是easyside的ip:port),然后easyside隨機探測這邊的端口。
這里省去算法的數學模型,如果你對實現干興趣,可以看看我寫的pythoncalculator。計算過程是“經典”生日悖論的一個小變種。下面是隨著easysiderandomprobe次數(假設hardsize256個端口)的變化,兩邊打開的端口有重合(即通信成功)的概率:
隨機探測次數
成功概率
174
50%
256
64%
1024
98%
2048
99.9%
根據以上結果,如果還是假設100ports/s這樣相當溫和的探測速率,那2秒鐘就有約50%的成功概率。即使非常不走運,我們仍然能在20s時幾乎100%穿透成功,而此時只探測了總端口空間的4%。
非常好!雖然這種hardNAT給我們帶來了嚴重的穿透延遲,但最終結果仍然是成功的。那么,如果是兩個hardNAT,我們還能處理嗎?
7.3 雙hardNAT場景
這種情況下仍然可以用前面的多端口+隨機探測方式,但成功概率要低很多了:
- 每次通過一臺hardNAT去探測對方的端口(目的端口)時,我們自己同時也生成了一個隨機源端口,
- 這意味著我們的搜索空間變成了二維{srcport,dstport}對,而不再是之前的一維dstport空間。
這里我們也不就具體計算展開,只告訴結果:仍然假設目的端打開256個端口,從源端發起2048次(20秒),成功的概率是:0.01%。
如果你之前學過生日悖論,就并不會對這個結果感到驚訝。理論上來說,
- 要達到99.9%的成功率,我們需要兩邊各進行170,000次探測——如果還是以100packets/sec的速度,就需要28分鐘。
- 要達到50%的成功率,“只”需要54,000packets,也就是9分鐘。
- 如果不使用生日悖論方式,而且暴力窮舉,需要1.2年時間!
對于某些應用來說,28分鐘可能仍然是一個可接受的時間。用半個小時暴力穿透NAT之后,這個連接就可以一直用著——除非NAT設備重啟,那樣就需要再次花半個小時穿透建個新連接。但對于交互式應用來說,這樣顯然是不可接受的。
更糟糕的是,如果去看常見的辦公網路由器,你會震驚于它的activesessionlowlimit有多么低。例如,一臺JuniperSRX300最多支持64,000activesessions。也就是說,
- 如果我們想創建一個成功的穿透連接,就會把它的整張session表打爆(因為我們要暴力探測65535個端口,每次探測都是一條新連接記錄)!這顯然要求這臺路由器能從容優雅地處理過載的情況。
- 這只是創建一條連接帶來的影響!如果20臺機器同時對這臺路由器發起穿透呢?絕對的災難!
至此,我們通過這種方式穿透了比之前更難一些的網絡拓撲。這是一個很大的成就,因為家用路由器一般都是easyNAT,hardNAT一般都是辦公網路由器或云NAT網關。這意味著這種方式能幫我們解決
- home-to-office(家->辦公室)
- home-to-cloud(家->云)
的場景,以及一部分
- office-to-cloud(辦公室->云)
- cloud-to-cloud(云->辦公室)
場景。
7.4 控制端口映射(portmapping)過程:UPnP/NAT-PMP/PCP協議
如果我們能讓NAT設備的行為簡單點,不要把事情搞這么復雜,那建立連接(穿透)就會簡單很多。真有這樣的好事嗎?還真有,有專門的一種協議叫端口映射協議(portmappingprotocols)。通過這種協議禁用掉前面遇到的那些亂七八糟的東西之后,我們將得到一個非常簡單的“請求-響應”。
下面是三個具體的端口映射協議:
- UPnPIGD(UniversalPlug’n’PlayInternetGatewayDevice)
最老的端口控制協議,誕生于1990s晚期,因此使用了很多上世紀90年代的技術(XML、SOAP、multicastHTTPoverUDP——對,HTTPoverUDP),而且很難準確和安全地實現這個協議。但以前很多路由器都內置了UPnP協議,現在仍然很多。
請求和響應:
- “你好,請將我的lan-ip:port轉發到公網(WAN)”,“好的,我已經為你分配了一個公網映射wan-ip:port”。
- NAT-PMP
UPnPIGD出來幾年之后,Apple推出了一個功能類似的協議,名為NAT-PMP(NATPortMappingProtocol)。
但與UPnP不同,這個協議只做端口轉發,不管是在客戶端還是服務端,實現起來都非常簡單。
- PCP
稍后一點,又出現了NAT-PMPv2版,并起了個新名字PCP(PortControlProtocol)。
因此要更好地實現穿透,可以
- 先判斷本地的默認網關上是否啟用了UPnPIGD,NAT-PMPandPCP,
- 如果探測發現其中任何一種協議有響應,我們就申請一個公網端口映射,
可以將這理解為一個加強版STUN:我們不僅能發現自己的公網ip:port,而且能指示我們的NAT設備對我們的通信對端友好一些——但并不是為這個端口修改或添加防火墻規則。
- 接下來,任何到達我們NAT設備的、地址是我們申請的端口的包,都會被設備轉發到我們。
但我們不能假設這個協議一定可用:
- 本地NAT設備可能不支持這個協議;
- 設備支持但默認禁用了,或者沒人知道還有這么個功能,因此從來沒開過;
- 安全策略要求關閉這個特性。
這一點非常常見,因為UPnP協議曾曝出一些高危漏洞(后面都修復了,因此如果是較新的設備,可以安全地使用UPnP——如果實現沒問題)。不幸的是,某些設備的配置中,UPnP,NAT-PMP,PCP是放在一個開關里的(可能統稱為“UPnP”功能),一開全開,一關全關。因此如果有人擔心UPnP的安全性,他連另外兩個也用不了。
最后,終歸來說,只要這種協議可用,就能有效地減少一次NAT,大大方便建連過程。但接下來看一些不常見的場景。
7.5 多NAT協商(NegotiatingnumerousNATs)
目前為止,我們看到的客戶端和服務端都各只有一個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場景將展現在我們面前。
7.6 運營商級NAT帶來的問題
即使用NAT來解決IPv4地址不夠的問題,地址仍然是不夠用的,ISP(互聯網服務提供商)顯然無法為每個家庭都分配一個公網IP地址。那怎么解決這個問題呢?ISP的做法是不夠了就再嵌套一層NAT:
- 家用路由器將你的客戶端SNAT到一個“intermediate”IP然后發送到運營商網絡,
- ISP’snetwork中的NAT設備再將這些intermediateIPs映射到少量的公網IP。
后面這種NAT就稱為“運營商級NAT”(carrier-gradeNAT,或稱電信級NAT),縮寫CGNAT。如下圖所示:
CGNAT對NAT穿透來說是一個大麻煩。
- 在此之前,辦公網用戶要快速實現NAT穿透,只需在他們的路由器上手動設置端口映射就行了。
- 但有了CGNAT之后就不管用了,因為你無法控制運營商的CGNAT!
好消息是:這其實是double-NAT的一個小變種,因此前面介紹的解決方式大部分還仍然是適用的。某些東西可能會無法按預期工作,但只要肯給ISP交錢,這些也都能解決。除了portmappingprotocols,其他我們已經介紹的所有東西在CGNAT里都是適用的。
新挑戰:同一CGNAT側直連,STUN不可用
但我們確實遇到了一個新挑戰:如何直連兩個在同一CGNAT但不同家用路由器中的對端呢?如下圖所示:
在這種情況下,STUN就無法正常工作了:STUN看到的是客戶端在公網(CGNAT后面)看到的地址,而我們想獲得的是在“middlenetwork”中的ip:port,這才是對端真正需要的地址,
解決方案:如果端口映射協議能用:一端做端口映射
怎么辦呢?
如果你想到了端口映射協議,那恭喜,答對了!如果peer中任何一個NAT支持端口映射協議,對我們就能實現穿透,因為它分配的ip:port正是對端所需要的信息。
這里諷刺的是:double-NAT(指CGNAT)破壞了端口映射協議,但在這里又救了我們!當然,我們假設這些協議一定可用,因為CGNATISP傾向于在它們的家用路由器側關閉這些功能,已避免軟件得到“錯誤的”結果,產生混淆。
解決方案:如果端口映射協議不能用:NAThairpin模式
如果不走運,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-behavedNAT設備都不支持hairpin模式,
- 因為它們都有“只有src_ip是私有地址且dst_ip是公網地址的包才會經過我”之類的假設。
- 因此對于這種目的地址不是公網、需要讓路由器把包再轉回內網的包,它們會直接丟棄。
- 這些邏輯甚至是直接實現在路由芯片中的,因此除非升級硬件,否則單靠軟件編程無法改變這種行為。
Hairpin是所有NAT設備的特性(支持或不支持),并不是CGNAT獨有的。
- 在大部分情況下,這個特性對我們的NAT穿透目的來說都是無所謂的,因為我們期望中兩個LANNAT設備會直接通信,不會再向上繞到它們的默認網關CGNAT來解決這個問題。
Hairpin特性可有可無這件事有點遺憾,這可能也是為什么hairpin功能經常broken的原因。
- 一旦必須涉及到CGNAT,那hairpinning對連接性來說就至關重要了。
Hairpinning使內網連接的行為與公網連接的行為完成一致,因此我們無需關心目的地址類型,也不用知曉自己是否在一臺CGNAT后面。
如果hairpinning和portmappingprotocols都不可用,那只能降級到中繼模式了。
7.7 全IPv6網絡:理想之地,但并非問題全無
行文至此,一些讀者可能已經對著屏幕咆哮:不要再用IPv4了!花這么多時間精力解決這些沒意義的東西,還不如直接換成IPv6!
- 的確,之所以有這些亂七八糟的東西,就是因為IPv4地址不夠了,我們一直在用越來越復雜的NAT來給IPv4續命。
- 如果IP地址夠用,無需NAT就能讓世界上的每個設備都有一個自己的公網IP地址,這些問題不就解決了嗎?
簡單來說,是的,這也正是IPv6能做的事情。但是,也只說對了一半:在理想的全IPv6世界中,所有這些東西會變得更加簡單,但我們面臨的問題并不會完全消失——因為有狀態防火墻仍然還是存在的。
- 辦公室中的電腦可能有一個公網IPv6地址,但你們公司肯定會架設一個防火墻,只允許你的電腦主動訪問公網,而不允許反向主動建連。
- 其他設備上的防火墻也仍然存在,應用類似的規則。
因此,我們仍然會用到
- 本文最開始介紹的防火墻穿透技術,以及
- 幫助我們獲取自己的公網ip:port信息的旁路信道
- 仍然需要在某些場景下fallback到中繼模式,例如fallback到最通用的HTTP中繼協議,以繞過某些網絡禁止outboundUDP的問題。
但我們現在可以拋棄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設備,我們能將IPv4DNS應答給IPv6網絡,這樣對終端來說,它看到的就是一個全IPv6網絡,而仍然能訪問IPv4公網。
如果需要處理DNS問題,那這種方式工作良好。例如,如果連接到google.com,將這個域名解析成IP地址的過程會涉及到DNS64設備,它又會進一步involveNAT64設備,但后一步對用戶來說是無感知的。
但對于NAT和防火墻穿透來說,我們會關心每個具體的IP地址和端口。
解決方案:CLAT(Customer-sidetransLATor)
如果設備支持CLAT(Customer-sidetranslator—fromCustomerXLAT),那我們就很幸運:
- CLAT假裝操作系統有直接IPv4連接,而背后使用的是NAT64,以對應用程序無感知。在有CLAT的設備上,我們無需做任何特殊的事情。
- CLAT在移動設備上非常常見,但在桌面電腦、筆記本和服務器上非常少見,因此在后者上,必須自己做CLAT做的事情:檢測NAT64+DNS64的存在,然后正確地使用它們。
解決方案:CLAT不存在時,手動穿透NAT64設備
- 首先檢測是否存在NAT64+DNS64。
方法很簡單:向ipv4only.arpa.發送一個DNS請求。這個域名會解析到一個已知的、固定的IPv4地址,而且是純IPv4地址。如果得到的是一個IPv6地址,就可以判斷有DNS64服務器做了轉換,而它必然會用到NAT64。這樣就能判斷出NAT64的前綴是多少。
- 此后,要向IPv4地址發包時,發送格式為{NAT64prefix+IPv4address}的IPv6包。類似地,收到來源格式為{NAT64prefix+IPv4address}的包時,就是IPv4流量。
- 接下來,通過NAT64網絡與STUN通信來獲取自己在NAT64上的公網ip:port,接下來就回到經典的NAT穿透問題了——除了需要多做一點點事情。
幸運的是,如今的大部分v6-only網絡都是移動運營商網絡,而幾乎所有手機都支持CLAT。運營v6-only網絡的ISPs會在他們給你的路由器上部署CLAT,因此最后你其實不需要做什么事情。但如果想實現100%穿透,就需要解決這種邊邊角角的問題,即必須顯式支持從v6-only網絡連接v4-only對端。
7.8 將所有解決方式集成到ICE協議
針對具體場景,該選擇哪種穿透方式?
至此,我們的NAT穿透之旅終于快結束了。我們已經覆蓋了有狀態防火墻、簡單和高級NAT、IPv4和IPv6。只要將以上解決方式都實現了,NAT穿透的目的就達到了!
但是,
- 對于給定的peer,如何判斷改用哪種方式呢?
- 如何判斷這是一個簡單有狀態防火墻的場景,還是該用到生日悖論算法,還是需要手動處理NAT64呢?
- 還是通信雙方在一個WiFi網絡下,連防火墻都沒有,因此不需要任何操作呢?
早期NAT穿透比較簡單,能讓我們精確判斷出peer之間的路徑特點,然后針對性地采用相應的解決方式。但后面,網絡工程師和NAT設備開發工程師引入了一些新理念,給路徑判斷造成很大困難。因此我們需要簡化客戶端側的思考(判斷邏輯)。
這就要提到InteractiveConnectivityEstablishment(ICE,交換式連接建立)協議了。與STUN/TURN類似,ICE來自電信領域,因此其RFC充滿了SIP、SDP、信令會話、撥號等等電話術語。但如果忽略這些領域術語,我們會看到它描述了一個極其優雅的判斷最佳連接路徑的算法。
真的?這個算法是:每種方法都試一遍,然后選擇最佳的那個方法。就是這個算法,驚喜嗎?
來更深入地看一下這個算法。
ICE(InteractiveConnectivityEstablishment)算法
這里的討論不會嚴格遵循ICEspec,因此如果是在自己實現一個可互操作的ICE客戶端,應該通讀RFC8445,根據它的描述來實現。這里忽略所有電信術語,只關注核心的算法邏輯,并提供幾個在ICE規范允許范圍的靈活建議。
- 為實現和某個peer的通信,首先需要確定我們自己用的(客戶端側)這個socket的地址,這是一個列表,至少應該包括:
- 我們自己的IPv6ip:ports我們自己的IPv4LANip:ports(局域網地址)通過STUN服務器獲取到的我們自己的IPv4WANip:ports(公網地址,可能會經過NAT64轉換)通過端口映射協議獲取到的我們自己的IPv4WANip:port(NAT設備的端口映射協議分配的公網地址)運營商提供給我們的endpoints(例如,靜態配置的端口轉發)
- 通過旁路信道與peer互換這個列表。兩邊都拿到對方的列表后,就開始互相探測對方提供的地址。列表中地址沒有優先級,也就是說,如果對方給的了15個地址,那我們應該把這15個地址都探測一遍。
這些探測包有兩個目的:
- 打開防火墻,穿透NAT,也就是本文一直在介紹的內容;健康檢測。我們在不斷交換(最好是已認證的)“ping/pong”包,來檢測某個特定的路徑是不是端到端通的。
- 最后,一小會兒之后,從可用的備選地址中(根據某些條件)選擇“最佳”的那個,任務完成!
這個算法的優美之處在于:只要選擇最佳線路(地址)的算法是正確的,那就總能獲得最佳路徑。
- ICE會預先對這些備選地址進行排序(通常:LAN>WAN>WAN+NAT),但用戶也可以自己指定這個排序行為。
- 從v0.100.0開始,Tailscale從原來的hardcode優先級切換成了根據round-triplatency的方式,它大部分情況下排序的結果和LAN>WAN>WAN+NAT是一致的。但相比于靜態排序,我們是動態計算每條路徑應該屬于哪個類別。
ICEspec將協議組織為兩個階段:
- 探測階段
- 通信階段
但不一定要嚴格遵循這兩個步驟的順序。在Tailscale,
- 我們發現更優的路徑之后就會自動切換過去,
- 所有的連接都是先選擇DERP模式(中繼模式)。這意味著連接立即就能建立(優先級最低但100%能成功的模式),用戶不用任何等待,
- 然后并行進行路徑發現。通常幾秒鐘之后,我們就能發現一條更優路徑,然后將現有連接透明升級(upgrade)過去。
但有一點需要關心:非對稱路徑。ICE花了一些精力來保證通信雙方選擇的是相同的網絡路徑,這樣才能保證這條路徑上有雙向流量,能保持防火墻和NAT設備的連接一直處于open狀態。自己實現的話,其實并不需要花同樣大的精力來實現這個保證,但需要確保你所有使用的所有路徑上,都有雙向流量。這個目標就很簡單了,只需要定期在所有已使用的路徑上發ping/pong就行了。
健壯性與降級
要實現健壯性,還需要檢測當前已選擇的路徑是否已經失敗了(例如,NAT設備維護清掉了所有狀態),如果失敗了就要降級(downgrade)到其他路徑。這里有兩種方式:
- 持續探測所有路徑,維護一個降級時會用的備用地址列表;
- 直接降級到保底的中繼模式,然后再通過路徑探測升級到更好的路徑。
考慮到發生降級的概率是非常小的,因此這種方式可能是更經濟的。
7.9 安全
最后需要提到安全。
本文的所有內容都假設:我們使用的上層協議已經有了自己的安全機制(例如QUIC協議有TLS證書,WireGuard協議有自己的公鑰)。如果還沒有安全機制,那顯然是要立即補上的。一旦動態切換路徑,基于IP的安全機制就是無用的了(IP協議最開始就沒怎么考慮安全性),至少要有端到端的認證。
- 嚴格來說,如果上層協議有安全機制,那即使收到是欺騙性的ping/pong流量,問題都不大,最壞的情況也就是攻擊者誘導兩端通過他們的系統來中繼流量。而有了端到端安全機制,這并不是一個大問題(取決于你的威脅模型)。
- 但出于謹慎考慮,最好還是對路徑發現的包也做認證和加密。具體如何做可以咨詢你們的應用安全工程師。
八、結束語
我們終于完成了NAT穿透的目標!
如果實現了以上提到的所有技術,你將得到一個業內領先的NAT穿透軟件,能在絕大多數場景下實現端到端直連。如果直連不了,還可以降級到保底的中繼模式(對于長尾來說只能靠中繼了)。
但這些工作相當復雜!其中一些問題研究起來很有意思,但很難做到完全正確,尤其是那些非常邊邊角角的場景,真正出現的概率極小,但解決它們所需花費的經歷又極大。不過,這種工作只需要做一次,一旦解決了,你就具備了某種超級能力:探索令人激動的、相對還比較嶄新的端到端應用(peer-to-peerapplications)世界。
8.1 跨公網端到端直連
去中心化軟件領域中的許多有趣想法,簡化之后其實都變成了跨過公網(互聯網)實現端到端直連這一問題,開始時可能覺得很簡單,但真正做才發現比想象中難多了。現在知道如何解決這個問題了,動手開做吧!
8.2 結束語之TL;DR
實現健壯的NAT穿透需要下列基礎:
- 一種基于UDP的協議;
- 能在程序內直接訪問socket;
- 有一個與peer通信的旁路信道;
- 若干STUN服務器;
- 一個保底用的中繼網絡(可選,但強烈推薦)
然后需要:
- 遍歷所有的ip:port;
- 查詢STUN服務器來獲取自己的公網ip:port信息,以及判斷自己這一側的NAT的“難度”(difficulty);
- 使用portmapping協議來獲取更多的公網ip:ports;
- 檢查NAT64,通過它獲取自己的公網ip:port;
- 將自己的所有公網ip:ports信息通過旁路信道與peer交換,以及某些加密秘鑰來保證通信安全;
- 通過保底的中繼方式與對方開始通信(可選,這樣連接能快速建立)
- 如果有必要/想這么做,探測對方的提供的所有ip:port,以及執行生日攻擊(birthdayattacks)來穿透harderNAT;
- 發現更優路徑之后,透明升級到該路徑;
- 如果當前路徑斷了,降級到其他可用的路徑;
- 確保所有東西都是加密的,并且有端到端認證。