内容概述:在p2p通信領域中,由NAT(Network Address Translation,網絡位址轉換)引起的問題已經衆所周知了,它會導緻在NAT内部的p2p用戶端在無論以何種有效的公網ip都無法通路的問題。雖然目前已經發展出多種穿越NAT的技術,但相關的技術文檔卻很少,用來證明這些技術的穩定性和優點的實際資料更少。本文的目的在于描述和分析在實際中運用得最廣泛、最可靠同時也是最簡單的一種NAT穿越技術,該技術通常被稱為“打洞”技術。目前,“打洞”技術已經在UDP通信領域中得到了廣泛的了解和應用,在此,也将讨論如何利用它實作可靠的p2p的TCP流通信。在收集了大量的“打洞”技術可以穿越的NAT裝置和網絡的資料以後,我們發現82%的已測 NAT裝置支援UDP形式的“打洞”穿越,64%的已測NAT裝置支援TCP流形式的“打洞”穿越。由于重量級p2p應用程式(如,VOIP、BT、線上遊戲等)的使用者需求量持續上升,并且該事實也已經引起了NAT裝置生産廠商的廣泛關注,是以,我們認為未來會有越來越多的NAT裝置提供對“打洞”穿越技術的支援。
1、介紹
使用者量高速增長以及大量安全問題的巨大壓力迫使Internet技術不斷向前發展,但是這些新興的技術很大程度地增加了應用程式開發的成本和複雜性。Internet最初的位址體系是每個節點有一個唯一不變的全局位址,可以通過該位址直接與任何其它的節點進行通信,而現如今,該位址體系已經被新的實際上廣泛使用的位址體系所替換,新的位址體系是由全局位址域和通過NAT接入全局位址域的大量私有位址域組成。在新的位址體系中(如圖1所示),隻有在 “main”全局位址域中的節點可以在網絡中很容易地與任何其它的擁有全局位址的節點通信,因為該節點擁有全局的、唯一的、可路由的位址。在私有網絡中的節點可以與在同一個私有網絡中的其它節點進行通信,并且在通常情況下可以向全局位址中的某個“著名”的節點發起TCP連接配接或發送UDP資料包。NAT裝置在此扮演的角色就是為從内網向公網發起的連接配接的節點配置設定臨時的轉發session,将來自内網的資料包的位址和端口轉換為公網的位址和端口,将來自公網的資料包的位址和端口轉換為内網的端口和位址,同時NAT将屏蔽所有未經授權的來自公網的資料包。

新的Internet位址體系非常适合于“用戶端/伺服器”這樣的通信模式,一個典型的C/S通信模式是:用戶端在内網(私有位址域),伺服器在公網(全局位址域),通過NAT将内網和公網連接配接起來。這種位址體系使得在不同内網(私有位址域)中的兩個節點很難直接通信,而這恰恰是p2p應用 (如,電話會議或線上遊戲)中最基本的要求。很顯然,我們需要一種方法即使在NAT裝置存在的前提下,仍然能夠無障礙地實作p2p通信。 在不同内網的兩個節點之間建立p2p連接配接的最有效的方法就是“打洞”。該技術在基于UDP的應用程式中得到了廣泛的應用,同樣的,該技術也可以用于基于TCP的應用程式。有趣的是,與“打洞”字面上的意思剛好相反,該技術不會影響到内網的安全。事實上,“打洞”技術使得p2p軟體的絕大部分功能都在NAT裝置預設的安全政策的控制之下,這些都由NAT裝置建立的session來管理。本文闡述了适用于UDP和TCP的“打洞”技術,并較長的描述了重要“打洞”過程中,應用程式和NAT裝置之間的行為。 不幸的是,由于NAT裝置的響應和行為不是标準的,是以沒有任何技術可以穿越現有的所有NAT裝置。本文提供了一些在現有NAT裝置上進行“打洞”的實驗結果。我們收集的資料來自于網際網路上使用了“NAT Check”工具并在大量不同生産廠商的NAT裝置上進行“打洞”實驗的使用者。由于資料是來自于一個叫做“self-selecting”的使用者社群,或許不會完全代表在Internet上真正部署和使用的NAT裝置,但是結果無論如何還是很令人興奮的。 在做基本的“打洞”操作評估的時候,我們應該指出在現有的NAT裝置“打洞”的複雜度上,不同的複雜度會有不同的結果。但目前我們把讨論的重點集中于開發最簡單的,可以應用于任何網絡拓撲結構的、穩定的、有正确NAT響應的NAT裝置上的“打洞”技術。我們有意避免使用一些“聰明的小把戲”通過欺騙某些NAT裝置來達到短期内穿越較多的NAT裝置,但從長期來看會引起網絡未知錯誤的技術。 盡管引入IPv6會極大地增加網際網路的位址空間,進而減少對于NAT裝置的需求量,但短期内IPv6确實增加了對NAT裝置的需求量,因為 NAT裝置本身提供了一種友善的方法進行IPv4與IPv6位址域轉換。另外私有網絡上建立匿名和加密通路節點也有利于組織機構的安全性以及不受外界幹擾,這些都意味着NAT還将存在相當長的一段時間。同樣,防火牆技術也不會由于有了足夠的ip位址而消失,IPv6的防火牆仍然會預設丢掉所有未經授權的資料包,仍然可以讓在IPv6環境下工作的應用程式“打洞”。 本文接下來的部分按照如下的方式組織:第二章介紹基本的NAT穿越概念和術語;第三章介紹UDP“打洞”過程;第四章介紹TCP“打洞”過程;第五章介紹支援“打洞”的NAT裝置必須具有那些特性;第六章介紹我們在目前流行的NAT裝置上的“打洞”實驗結果;第七章讨論相關的網絡問題;第八章全文總結以及結束語。 2、基本概念本節介紹了本文使用到的基本的NAT術語,着重描述了适用于UDP和TCP兩種協定的通用的NAT穿越技術。 2.1、NAT術語 本文絕大部分術語和分類來自于RFC 2663定義,另外一些來自于較新的RFC 3489中的定義。 了解session是很重要的。一個TCP或UDP的session endpoint是由一個IP位址,端口号組成,每個session是由兩個session endpoint構成。從内網節點的角度來看,一個session由4部分組成分别為:本地IP,本地端口,遠端IP,遠端端口。session的方向通常代表了資料包的初始流動的方向;對于TCP來說就是SYN包的流向,對于UDP來說就是第一個使用者資料包的流向。 NAT有很多種,但最普遍的一種類型叫做“傳統”NAT,或者“向外”NAT。他們在内網和公網之間提供了一個“不對稱”橋的映射。“向外”NAT在預設情況下隻允許向外的session穿越NAT: 從外向内的的資料包都會被丢棄掉,除非NAT裝置事先已經定義了這些從外向内的資料包是已存在的内網session的一部分。 “外向”NAT會造成p2p協定的混亂,因為當p2p的雙方決定向在不同NAT後面的對方開始通信的時候,無論哪一方試圖初始化一個session,另一方的NAT都會拒絕這個請求。NAT穿越的核心思想就是讓p2p的雙方的NAT看上去都是“向外”的NAT。 “向外”NAT有兩種類型:(1)“基礎”NAT,該NAT隻轉換IP位址,不轉換端口号。(2)NAPT(Network Address/Port Translation)NAPT轉換整個session endpoints。由于NAPT允許内網的多個節點通過共享的方式使用同一個的公共的IP位址,是以,支援NAPT的NAT裝置才會越來越多。盡管本文通篇讨論的内容都是基于支援NAPT的NAT裝置的,但這些規律和技術同樣适用于“基礎”NAT。 2.2 轉發方式 最可靠但同時也是效率最低的p2p穿越NAT進行通信的方法是采用類似C/S方式的轉發。假定兩個節點A和B每個節點都有向外的TCP或UDP 連接配接,聯入公共的已知伺服器S,S的公網IP位址是18.181.0.31,端口号是1234(如圖2所示),每個用戶端位于不同的私有内網中,并且它們的NAT裝置妨礙了用戶端之間直接的p2p連接配接。做為對直連方案的替代方案,兩個用戶端可以利用公共的伺服器S進行消息的轉發。例如,A為了将消息送給 B,A隻需将消息發給S,然後由S轉發給B,這一過程将使用A與B事先與S建立好的連接配接。
轉發方式通常隻能在雙方用戶端都連接配接到伺服器的時候有效。這種方式的缺點在于,它假定伺服器的處理能力和網絡帶寬以及通信延遲都是理想的情況下,不會受到用戶端個數的影響。但是,由于沒有其它的方法能夠像轉發方式那樣,可以穿越現存的所有NAT裝置,是以在建構高可靠性的p2p系統的時候,通過伺服器轉發的方式依舊是一個非常有用的保證系統可靠性的方法。TURN協定定義了如何實作安全的轉發方式。 2.3 反向連接配接方式 一些p2p的應用程式采用了直接但是有所限制的技術來實作NAT穿越,該技術叫做“反向連接配接”,這是用于當兩個節點聯入伺服器S的時候,隻有一個一個節點在NAT裝置的後面(如圖3所示)。如果A希望建立與B的連接配接,那麼A可以直接聯入B,因為B是在公網中存在的,沒有經過NAT轉換,而且A的 NAT裝置也允許A直接由内網發起向外網的連接配接。如果B希望建立與A的連接配接,很不幸,A的NAT裝置會阻止該操作,此時,B可以借助于轉發伺服器S,向A 發送“反向連接配接”請求,由A“主動”連接配接B,進而達到A與B的p2p通信的目的。
盡管該技術的局限性非常明顯,但是使用已知的伺服器做為中介輔助p2p用戶端雙方進行p2p連接配接的思想已經成為了更加通用的“打洞”技術的基本思想。 3 UDP打洞方式 即使兩個p2p用戶端都位于NAT裝置後面,UDP打洞方式也能夠通過已知的伺服器實作p2p用戶端直連。該技術在RFC 3027的第5.1節中曾有所提及,在網絡上可以找到對其較模糊的描述,在最近的IP協定實驗中得到應用,在多種線上遊戲協定中得到了應用。 3.1 集中伺服器 打洞技術假定用戶端A和B可以與公網内的已知的集中伺服器建立UDP連接配接(可以互發UDP資料包)。當一個用戶端在S上登陸的時候,伺服器記錄下該用戶端的兩個endpoints(IP位址,UDP端口),一個是該用戶端确信自己是通過該ip和端口與伺服器S進行通信的,另一個是伺服器S記錄下的由伺服器“觀察”到的該用戶端實際與自己通信所使用的ip和端口。我們可以把前一個endpoint看作是用戶端的内網ip和端口,把後一個 endpoint看作是用戶端的内網ip和端口經過NAT轉換後的公網ip和端口。伺服器可以從用戶端的登陸消息的消息體中得到該用戶端的内網 endpoint相關資訊,可以通過對登陸消息的IP或UDP頭得到該用戶端的公網endpoint。如果該用戶端不是位于NAT裝置後面,那麼采用上述方法得到的兩個endpoint的值應該完全相同。 也有一些“弱智”的NAT裝置會掃描UDP資料包的包體,尋找4位元組的位域,看上去很像IP位址的位域,并且把它們改為與IP頭一樣的位址。為了避免這種行為的NAT裝置對UDP資料包包體的修改,應用程式可以采用直接對IP位址的值進行加密的方式騙過NAT裝置的檢查。 3.2 建立p2p的session 假定A要發起對B的直接連接配接,“打洞”過程如下所示:(endpoint指ip位址和端口的配對) (1)A最初不知道如何向B發起連接配接,于是A向伺服器S發送消息,請求S幫助建立與B的UDP連接配接。 (2)S将含有B的公網和内網的endpoint發給A,同時,S将含有A的公網和内網的endpoint的用于請求連接配接的消息也發給B。一旦這些消息順利到達,A與B就都知道了對方的公網和内網的endpoint。 (3)當A收到由S發來的包含B的公網和内網endpoint的消息,A開始向這些B的endpoint發送UDP資料包,并且A會自動鎖定第一個給出響應的B的endpoint。同理,當B收到由S發來的A的公網和内網endpoint以後,也會開始向A的公網和内網的endpoint發送 UDP資料包,并且自動鎖定第一個得到A的回應的endpoint。由于A與B的互相向對方發送UDP資料包的操作是異步的,是以A和B發送資料包的時間先後并沒有嚴格的時序要求。 下面我們就來看一下這三個角色之間是如何進行UDP“打洞”的。在這裡我們分為三種具體情景來讨論:第一種也是最“簡單”的一種情景,兩個用戶端都位于同一個NAT裝置後面,位于同一個内網中;第二種也是最普遍的一種情景,兩個用戶端分别位于不同的NAT裝置後面,分屬不同的内網;第三種是用戶端位于兩層NAT裝置之後,通常最上層的NAT是由ISP網絡提供商,第二層的NAT是家用的NAT路由器之類的裝置。 通常情況下由應用程式自身确定的網絡實體層連接配接方式是很困難的,有時甚至是不可能的,即使是上述的若幹種情景下可以穿越NAT,也隻是代表在一定時期内有效,而不是永久有效的。諸如STUN之類的網絡協定或許可以提供必要的NAT資訊,但在遇到多層NAT裝置的時候,通常這些資訊也不是完全完整和有效的。盡管如此,隻要NAT裝置的響應是“合理”的,在通常情況下“打洞”技術還是能夠在應用程式對網絡狀況一無所知的前提下自動适用于多數場合。(“合理”的NAT響應将在第五章中詳細讨論) 3.3 p2p用戶端位于同一個NAT裝置後面 首先假設兩個用戶端位于同一個NAT裝置後面,并且位于相同的内網(相同的私有IP位址域)如圖4所示。A與S建立了UDP連接配接,經過NAT轉換後,A的公網端口被映射為62000。B同樣與S建立了UDP連接配接,公網端口映射為62005。
(圖4) 假設A想通過伺服器S做為介紹人,發起對B的連接配接。A向S發出消息請求與B進行連接配接。S将B的公網endpoint(即公網ip和port)以及内網endpoint(即内網ip和port)發給A,同時把A的公網、内網的endpoints發給B。由A和B發往對方公網endpoint的 UDP資料包能否被對方收到,這取決于目前的NAT是否支援“發夾”轉換(hairpin轉換,也就是同一台裝置,不同端口之間的UDP資料包能否到達,詳見3.5節)。但是A與B往對方内網endpoint發送的UDP資料包是一定可以到達的,無論如何,内網資料包不需要路由,并且速度更快。A與B有很大的可能性采用内網的endpoint進行正常的p2p通信。 假定NAT裝置支援“發夾”轉換,應用程式也忽略由内網endpoint的連接配接,那麼A、B會采用公網endpoint做為p2p通信的連接配接,這勢必會造成資料包無謂地經過NAT裝置,這是一種對資源的浪費。我們會在第六節讨論這種情況,畢竟支援“發夾”轉換的NAT裝置還遠沒有對“打洞”技術支援的NAT裝置多。就目前的網絡情況而言,應用程式在“打洞”的時候,最好還是把公網endpoint和内網endpoint都實驗一下。 3.4 p2p用戶端位于不同的NAT裝置後面 假定A與B在不同的NAT裝置後面,分屬不同的内網,如圖5所示。A與B都經由各自的NAT裝置與伺服器S建立了UDP連接配接,A與B的本地端口号均為4321,伺服器S的公網端口号為1234。在“向外”session中,A的公網IP被映射為155.99.25.11,公網端口為62000, B的公網IP被映射為138.76.29.7,公網端口為31000。 如下所示:用戶端A-->本地IP:10.0.0.1,本地端口:4321,公網IP:155.99.25.11,公網端口:62000用戶端B-->本地IP:10.1.1.3,本地端口:4321,公網IP:138.76.29.7,公網端口:31000
(圖 5) 在A向伺服器S發送的登陸消息體中,會包含A的内網endpoint資訊,即10.0.0.1:4321;伺服器S會記錄下A的内網 endpoint,同時會把自己觀察到的A的公網endpoint記錄下來,即155.99.25.11:62000。同理,伺服器S會記錄下B的内網 endpoint,10.1.1.3:4321和由S觀察到的B的公網endpoint,138.76.29.7:31000。無論A與B二者任何一方向 S發送p2p連接配接請求,伺服器都會将其記錄下來的上述的公網、内網endpoint發送給A、B。 由于A、B分屬不同的内網,它們彼此的内網endpoint無法在公網中路由,是以發往各自内網endpoint的UDP資料包會發送到錯誤的主機或者根本不存在的主機。是以應用程式對于收到的消息必須經過授權和過濾,隻有通過授權的的消息才能是從對方的endpoint發出來的,例如,可以在消息中加入對方的程式名稱、加密算法,或者至少是一個雙方都從伺服器S上的預先得到的随機數字。 現在假定A的第一個消息将發往B的公網endpoint,如圖5所示。該消息途經A的NAT裝置,并在該裝置上生成了一個“向外”的 session。新的session源endpoint是10.0.0.1:4321該endpoint和A與伺服器S的建立連接配接的時候NAT生成的源 endpoint一樣,但它的目的endpoint不同。如果A的NAT裝置給出的響應是“友好”的,那麼A的NAT裝置将保留A的内網 endpoint,并且所有來自A的源endpoint(10.0.0.1:4321)的資料包都沿用A與S事先建立起來的session,公網 endpoint均為(155.99.25.11:62000)。A向B的公網endpoint發送消息的過程就是“打洞”的過程,從A的内網的角度來看應為從(10.0.0.1:4321)發往(138.76.29.7:31000),從A的在其NAT裝置上建立的session來看,是從(155.99.25.11:62000)發到(138.76.29.7:31000)。 如果A發給B的公網endpoint的消息包在B向A發送消息包之前到達B的NAT裝置,B的NAT會認為A發過來的消息是未經授權的公網消息,會丢棄掉該資料包。B發往A的消息包根上述的過程一樣,會在B的NAT上建立一個(10.1.1.3:4321,155.99.25.11: 62000)的session(通常也會沿用B與S連接配接時建立的session,隻是該session現在不光可以接受由S發給B的消息,還可以接受從A 的NAT裝置-155.99.25.11:6200發來的消息) 一旦A與B都向對方的NAT在公網上的endpoint發送了資料包,就打開了A與B之間的“洞”,A與B向對方的公網endpoint發送資料,等效為向對方的用戶端直接發送UDP資料包了。一旦應用程式确認已經可以通過往對方的公網endpoint發送資料包的方式讓資料包到達NAT後面的目的應用程式,程式會自動停止繼續發送用于“打洞”的資料包,轉而開始真正的p2p資料傳輸。 ps: 現今解決UDP穿透NAT的技術已經比較成熟,例如STUN.
通過STUN,已經解決了UDP方式的穿透問題。使用STUN,clientA發送一個UDP包clientB,盡管這個包被clientB的NAT阻止,但卻使clientA的NAT建立了一個本地狀态,該狀态允許clientB的回應包不被clientA的NAT 阻止而到達clientA。然後,clientB發送一個UDP 包給clientA,clientA的NAT 認為這個包是第一個包的網絡流的一部分,是以防火牆讓它通過;同樣,clientB的NAT 把第二個包(clientB發給clientA的包)當作一個連接配接發起,是以建立本地狀态并路由clientA的回應包。很多流行的IM應用程式就是使用的這種方式。 STUN(Simple Traversal of User Datagram Protocol through Network Address Translators (NATs),NAT的UDP簡單穿越)是一種網絡協定,它允許位于NAT(或多重NAT)後的客戶 端找出自己的公網位址,查出自己位于哪種類型的NAT之後以及NAT為某一個本地端口所綁定的Internet端端口。這些資訊被用來在兩個同時處于 NAT 路由器之後的主機之間建立UDP通信。該協定由RFC 3489定義。 3.5 p2p用戶端位于多層NAT裝置後面 有的網絡拓撲結構包含了多個NAT裝置,如果沒有掌握該拓撲結構的詳細資訊,兩個用戶端之間是無法建立“最優化”的p2p路由的。現在我們來讨論最後一種情況,如圖6所示。假定NAT C是由ISP(Internet Service Provider)提供的工業級的NAT裝置,NAT C提供将多個下屬的使用者NAT或使用者節點映射到有限的幾個公網IP的服務,NAT A和NAT B做為NAT C的内網節點将把使用者的家庭網絡或内部網絡接入NAT C的内網,然後使用者的内部網絡就可以經由NAT C通路公網了。從這種拓撲結構上來看,隻有伺服器S與NAT C是真正擁有公網可路由IP位址的裝置,而NAT A和NAT B所使用的“公網”IP位址,實際上是由ISP服務提供商設定的(相對于NAT C而言)内網位址(本位的後續部分我把這個由ISP提供的内網位址相對于NAT A和NAT B稱之為“僞”公網位址),同理隸屬于NAT A與NAT B的用戶端,相對與NAT A,NAT B而言,它們處于NAT A,NAT B的内網,以此類推,用戶端可以放到到多層NAT裝置後面。用戶端A和用戶端B發起對伺服器S的連接配接的時候,就會依次在NAT A和NAT B上建立向外的session,而NAT A、NAT B要聯入公網的時候,會在NAT C上再建立向外的session。
(圖 6) 現在假定用戶端A和B希望通過UDP“打洞”完成兩個用戶端的p2p直連。最優化的路由政策是用戶端A向用戶端B的“僞公網”IP上發送資料包,即ISP服務提供商指定的内網IP,NAT B的“僞”公網endpoint,10.0.1.2:55000。由于從伺服器S的角度隻能觀察到真正的公網位址,也就是NAT A,NAT B在NAT C建立的session的真正的公網位址155.99.25.11:62000以及155.99.25.11:62005,是以非常不幸,用戶端A與用戶端B是無法通過伺服器S知道這些“僞”公網的位址的。而且即使用戶端A和B通過某種手段可以得到NAT A和NAT B的“僞”公網位址,我們仍然不建議采用上述的“最優化”的打洞方式,這是因為這些位址是由ISP服務提供商提供的或許會存在與用戶端本身所在的内網位址重複的可能性。(例如:NAT A的内網的IP位址域恰好與NAT A在NAT C的“僞”公網IP位址域重複,這樣就會導緻打洞資料包無法發出的問題) 是以用戶端别無選擇,隻能使用由公網伺服器S觀察到的A,B的公網endpoint進行“打洞”操作,用于“打洞”的資料包将由NAT C進行轉發,這裡NAT C是否支援“發夾”轉換或“環路”轉換非常重要,否則資料包将無法由NAT C轉發給NAT A和NAT B,進而無法到達用戶端A和B。當用戶端A向用戶端B的公網endpoint(155.99.25.11:62005)發送UDP資料包的時候,NAT A首先把資料包的源endpoint由A的内網endpoint(10.0.0.1:4321)轉換為“僞”公網endpoint(10.0.1.1: 45000),現在資料包到了NAT C,NAT C應該可以識别出來該資料包是要發往自身轉換過的公網endpoint,如果NAT C可以給出“合理”響應的話,NAT C将把該資料包的源endpoint改為155.99.25.11:62000,目的endpoint改為10.0.1.2:55000,即NAT B的“僞”公網endpoint,NAT B最後會将收到的資料包發往用戶端B。同樣,由B發往A的資料包也會經過類似的過程。也有很多NAT裝置不支援類似這樣的“發夾”轉換,但是已經有越來越多的NAT裝置生産廠商開始加入對該轉換的支援。 3.6 UDP在空閑狀态下的逾時問題 由于UDP轉換協定提供的“洞”不是絕對可靠的,多數NAT裝置内部都有一個UDP轉換的空閑狀态計時器,如果在一段時間内沒有UDP資料通信,NAT裝置會關掉由“打洞”操作打出來的“洞”,做為應用程式來講如果想要做到與裝置無關,就最好在穿越NAT的以後設定一個穿越的有效期。很遺憾目前沒有标準有效期,這個有效期與NAT裝置内部的配置有關,最短的隻有20秒左右。在這個有效期内,即使沒有p2p資料包需要傳輸,應用程式為了維持該 “洞”可以正常工作,也必須向對方發送“打洞”維持包。這個維持包是需要雙方應用都發送的,隻有一方發送不會維持另一方的session正常工作。除了頻繁發送“打洞”維持包以外,還有一個方法就是在目前的“洞”有效期過期之前,p2p用戶端雙方重新“打洞”,丢棄原有的“洞”,這也不失為一個有效的方法。
4 關于TCP打洞技術建立穿越NAT裝置的p2p的TCP連接配接隻比UDP複雜一點點,TCP協定的“打洞”從協定層來看是與UDP的“打洞”過程非常相似的。盡管如此,基于TCP協定的打洞至今為止還沒有被很好的了解,這也造成了對其提供支援的NAT裝置不是很多。在NAT裝置支援的前提下,基于TCP的 “打洞”技術實際上與基于UDP的“打洞”技術一樣快捷、可靠。實際上,隻要NAT裝置支援的話,基于TCP的p2p技術的健壯性将比基于UDP的技術的更強一些,因為TCP協定的狀态機給出了一種标準的方法來精确的擷取某個TCP session的生命期,而UDP協定則無法做到這一點。4.1 套接字和TCP端口的重用實作基于TCP協定的p2p“打洞”過程中,最主要的問題不是來自于TCP協定,而是來自于來自于應用程式的API接口。這是由于标準的伯克利(Berkeley)套接字的API是圍繞着建構用戶端/伺服器程式而設計的,API允許TCP流套接字通過調用connect()函數來建立向外的連接配接,或者通過listen()和accept函數接受來自外部的連接配接,但是,API不提供類似UDP那樣的,同一個端口既可以向外連接配接,又能夠接受來自外部的連接配接。而且更糟的是,TCP的套接字通常僅允許建立1對1的響應,即應用程式在将一個套接字綁定到本地的一個端口以後,任何試圖将第二個套接字綁定到該端口的操作都會失敗。為了讓TCP“打洞”能夠順利工作,我們需要使用一個本地的TCP端口來監聽來自外部的TCP連接配接,同時建立多個向外的TCP連接配接。幸運的是,所有的主流作業系統都能夠支援特殊的TCP套接字參數,通常叫做“SO_REUSEADDR”,該參數允許應用程式将多個套接字綁定到本地的一個endpoint(隻要所有要綁定的套接字都設定了SO_REUSEADDR參數即可)。BSD系統引入了SO_REUSEPORT參數,該參數用于區分端口重用還是位址重用,在這樣的系統裡面,上述所有的參數必須都設定才行。4.2 打開p2p的TCP流假定用戶端A希望建立與B的TCP連接配接。我們像通常一樣假定A和B已經與公網上的已知伺服器S建立了TCP連接配接。伺服器記錄下來每個聯入的用戶端的公網和内網的endpoints,如同為UDP服務的時候一樣。從協定層來看,TCP“打洞”與UDP“打洞”是幾乎完全相同的過程。1、用戶端A使用其與伺服器S的連接配接向伺服器發送請求,要求伺服器S協助其連接配接用戶端B。2、S将B的公網和内網的TCP endpoint傳回給A,同時,S将A的公網和内網的endpoint發送給B。3、用戶端A和B使用連接配接S的端口異步地發起向對方的公網、内網 endpoint的TCP連接配接,同時監聽各自的本地TCP端口是否有外部的連接配接聯入。4、A和B開始等待向外的連接配接是否成功,檢查是否有新連接配接聯入。如果向外的連接配接由于某種網絡錯誤而失敗,如:“連接配接被重置”或者“節點無法通路”,用戶端隻需要延遲一小段時間(例如延遲一秒鐘),然後重新發起連接配接即可,延遲的時間和重複連接配接的次數可以由應用程式編寫者來确定。5、TCP連接配接建立起來以後,用戶端之間應該開始鑒權操作,確定目前聯入的連接配接就是所希望的連接配接。如果鑒權失敗,用戶端将關閉連接配接,并且繼續等待新的連接配接聯入。用戶端通常采用“先入為主”的政策,隻接受第一個通過鑒權操作的用戶端,然後将進入p2p通信過程不再繼續等待是否有新的連接配接聯入。
(圖 7)
與UDP 不同的是,使用UDP協定的每個用戶端隻需要一個套接字即可完成與伺服器S通信,并同時與多個p2p用戶端通信的任務,而TCP用戶端必須處理多個套接字綁定到同一個本地TCP端口的問題,如圖7所示。現在來看更加實際的一種情景,A與B分别位于不同的NAT裝置後面,如圖5所示,并且假定圖中的端口号是 TCP協定的端口号,而不是UDP的端口号。圖中向外的連接配接代表A和B向對方的内網endpoint發起的連接配接,這些連接配接或許會失敗或者無法連接配接到對方。如同使用UDP協定進行“打洞”操作遇到的問題一樣,TCP的“打洞”操作也會遇到内網的IP與“僞”公網IP重複造成連接配接失敗或者錯誤連接配接之類的問題。用戶端向彼此公網endpoint發起連接配接的操作,會使得各自的NAT裝置打開新的“洞”允許A與B的TCP資料通過。如果NAT裝置支援TCP“打洞” 操作的話,一個在用戶端之間的基于TCP協定的流通道就會自動建立起來。如果A向B發送的第一個SYN包發到了B的NAT裝置,而B在此前沒有向A發送 SYN包,B的NAT裝置會丢棄這個包,這會引起A的“連接配接失敗”或“無法連接配接”問題。而此時,由于A已經向B發送過SYN包,B發往A的SYN包将被看作是由A發往B的包的回應的一部分,是以B發往A的SYN包會順利地通過A的NAT裝置,到達A,進而建立起A與B的p2p連接配接。4.3 從應用程式的角度來看TCP“打洞”從應用程式的角度來看,在進行TCP“打洞”的時候都發生了什麼呢?假定A首先向B發出SYN包,該包發往B的公網 endpoint,并且被B的NAT裝置丢棄,但是B發往A的公網endpoint的SYN包則通過A的NAT到達了A,然後,會發生以下的兩種結果中的一種,具體是哪一種取決于作業系統對TCP協定的實作:(1)A的TCP實作會發現收到的SYN包就是其發起連接配接并希望聯入的B的SYN包,通俗一點來說就是“說曹操,曹操到”的意思,本來A要去找B,結果B自己找上門來了。A的TCP協定棧是以會把B做為A向B發起連接配接connect的一部分,并認為連接配接已經成功。程式A調用的異步connect()函數将成功傳回,A的listen()等待從外部聯入的函數将沒有任何反映。此時,B聯入A的操作在A程式的内部被了解為A聯入B連接配接成功,并且A開始使用這個連接配接與B開始p2p通信。由于收到的SYN包中不包含A需要的ACK資料,是以,A的TCP将用 SYN-ACK包回應B的公網endpoint,并且将使用先前A發向B的SYN包一樣的序列号。一旦B的TCP收到由A發來的SYN-ACK包,則把自己的ACK包發給A,然後兩端建立起TCP連接配接。簡單的說,第一種,就是即使A發往B的SYN包被B的NAT丢棄了,但是由于B發往A的包到達了A。結果是,A認為自己連接配接成功了,B也認為自己連接配接成功了,不管是誰成功了,總之連接配接是已經建立起來了。(2)另外一種結果是,A的TCP實作沒有像(1)中所講的那麼“智能”,它沒有發現現在聯入的B就是自己希望聯入的。就好比在機場接人,明明遇到了自己想要接的人卻不認識,誤認為是其它的人,安排别人給接走了,後來才知道是自己錯過了機會,但是無論如何,人已經接到了任務已經完成了。然後,A通過正常的listen()函數和accept()函數得到與B的連接配接,而由A發起的向B的公網endpoint的連接配接會以失敗告終。盡管A向B的連接配接失敗,A仍然得到了B發起的向A的連接配接,等效于A與B之間已經聯通,不管中間過程如何,A與B已經連接配接起來了,結果是A和B的基于TCP協定的p2p連接配接已經建立起來了。第一種結果适用于基于BSD的作業系統對于TCP的實作,而第二種結果更加普遍一些,多數linux和windows系統都會按照第二種結果來處理。