天天看點

P2P通信原理與實作(C++)

當今網際網路到處存在着一些中間件(MIddleBoxes),如NAT和防火牆,導緻兩個(不在同一内網)中的用戶端無法直接通信。這些問題即便是到了IPV6時代也會存在,因為即使不需要NAT,但還有其他中間件如防火牆阻擋了連結的建立。中間件大多都是在C/S架構上設計的,其中相對隐匿的客戶機主動向周知的服務端(擁有靜态IP位址和DNS名稱)發起連結請求。大多數中間件實作了一種非對稱的通訊模型,即内網中的主機可以初始化對外的連結,而外網的主機卻不能初始化對内網的連結,除非經過中間件管理者特殊配置。本文主要讨論了在中間件為常見的NAPT的情況下通過UDP打洞進行P2P連結的過程。

1.簡介

  當今網際網路到處存在着一些中間件(MIddleBoxes),如NAT和防火牆,導緻兩個(不在同一内網)中的用戶端無法直接通信。這些問題即便是到了IPV6時代也會存在,因為即使不需要NAT,但還有其他中間件如防火牆阻擋了連結的建立。

  當今部署的中間件大多都是在C/S架構上設計的,其中相對隐匿的客戶機主動向周知的服務端(擁有靜态IP位址和DNS名稱)發起連結請求。大多數中間件實作了一種非對稱的通訊模型,即内網中的主機可以初始化對外的連結,而外網的主機卻不能初始化對内網的連結,除非經過中間件管理者特殊配置。在中間件為常見的NAPT的情況下(也是本文主要讨論的),内網中的用戶端沒有單獨的公網IP位址,而是通過NAPT轉換,和其他同一内網使用者共享一個公網IP。這種内網主機隐藏在中間件後的不可通路性對于一些用戶端

軟體如浏覽器來說并不是一個問題,因為其隻需要初始化對外的連結,從某方面來看反而還對隐私保護有好處。

  然而在P2P應用中,内網主機(用戶端)需要對另外的終端(Peer)直接建立連結,但是發起者和響應者可能在不同的中間件後面,兩者都沒有公網IP位址。而外部對NAT公網IP和端口主動的連結或資料都會因内網未請求被丢棄掉。本文讨論的就是如何跨越NAT實作内網主機直接通訊的問題。

2.術語

防火牆(Firewall):

  防火牆主要限制内網和公網的通訊,通常丢棄未經許可的資料包。防火牆會檢測(但是不修改)試圖進入内網資料包的IP位址和TCP/UDP端口資訊。

網絡位址轉換器(NAT):

  NAT不止檢查進入資料包的頭部,而且對其進行修改,進而實作同一内網中不同主機共用更少的公網IP(通常是一個)。

基本NAT(Basic NAT):

  基本NAT會将内網主機的IP位址映射為一個公網IP,不改變其TCP/UDP端口号。基本NAT通常隻有在當NAT有公網IP池的時候才有用。

網絡位址-端口轉換器(NAPT):

  到目前為止最常見的即為NAPT,其檢測并修改出入資料包的IP位址和端口号,進而允許多個内網主機同時共享一個公網IP位址。

錐形NAT(Cone NAT):

  在建立了一對(公網IP,公網端口)和(内網IP,内網端口)二進制組的綁定之後,Cone NAT會重用這組綁定用于接下來該應用程式的所有會話(同一内網IP和端口),隻要還有一個會話還是激活的。

  例如,假設用戶端A建立了兩個連續的對外會話,從相同的内部端點(10.0.0.1:1234)到兩個不同的外部服務端S1和S2。Cone NAT隻為兩個會話映射了一個公網端點(155.99.25.11:62000),確定用戶端端口的“身份”在位址轉換的時候保持不變。由于基本NAT和防火牆都不改變資料包的端口号,是以這些類型的中間件也可以看作是退化的Cone NAT。

P2P通信原理與實作(C++)

對稱NAT(Symmetric NAT)

  對稱NAT正好相反,不在所有公網-内網對的會話中維持一個固定的端口綁定。其為每個新的會話開辟一個新的端口。如下圖所示:

P2P通信原理與實作(C++)

 其中Cone NAT根據NAT如何接收已經建立的(公網IP,公網端口)對的輸入資料還可以細分為以下三類:

1) 全錐形NAT(Full Cone NAT)

  在一個新會話建立了公網/内網端口綁定之後,全錐形NAT接下來會接受對應公網端口的所有資料,無論是來自哪個(公網)終端。全錐NAT有時候也被稱為“混雜”NAT(promiscuous NAT)。

2) 受限錐形NAT(Restricted Cone NAT)

  受限錐形NAT隻會轉發符合某個條件的輸入資料包。條件為:外部(源)IP位址比對内網主機之前發送一個或多個資料包的結點的IP位址。受限NAT通過限制輸入資料包為一組“已知的”外部IP位址,有效地精簡了防火牆的規則。

3) 端口受限錐形NAT(Port-Restricted Cone NAT)

  端口受限錐形NAT也類似,隻當外部資料包的IP位址和端口号都比對内網主機發送過的位址和端口号時才進行轉發。端口受限錐形NAT為内部結點提供了和對稱NAT相同等級的保護,以隔離未關聯的資料。

3. P2P通信

  根據用戶端的不同,用戶端之間進行P2P傳輸的方法也略有不同,這裡介紹了現有的穿越中間件進行P2P通信的幾種技術。

3.1 中繼(Relaying)

  這是最可靠但也是最低效的一種P2P通信實作。其原理是通過一個有公網IP的伺服器中間人對兩個内網用戶端的通信資料進行中繼和轉發。如下圖所示:

P2P通信原理與實作(C++)

  用戶端A和用戶端B不直接通信,而是先都與服務端S建立連結,然後再通過S和對方建立的通路來中繼傳遞的資料。這鐘方法的缺陷很明顯,當連結的用戶端變多之後,會顯著增加伺服器的負擔,完全沒展現出P2P的優勢。

3.2 逆向連結(Connection reversal)

  第二種方法在當兩個端點中有一個不存在中間件的時候有效。例如,用戶端A在NAT之後而用戶端B擁有全局IP位址,如下圖:

P2P通信原理與實作(C++)

  用戶端A内網位址為10.0.0.1,且應用程式正在使用TCP端口1234。A和伺服器S建立了一個連結,伺服器的IP位址為18.181.0.31,監聽1235端口。NAT A給用戶端A配置設定了TCP端口62000,位址為NAT的公網IP位址155.99.25.11,作為用戶端A對外目前會話的臨時IP和端口。是以S認為用戶端A就是155.99.25.11:62000。而B由于有公網位址,是以對S來說B就是138.76.29.7:1234。

  當用戶端B想要發起一個對用戶端A的P2P連結時,要麼連結A的外網位址155.99.25.11:62000,要麼連結A的内網位址10.0.0.1:1234,然而兩種方式連結都會失敗。連結10.0.0.1:1234失敗自不用說,為什麼連結155.99.25.11:62000也會失敗呢?來自B的TCP SYN握手請求到達NAT A的時候會被拒絕,因為對NAT A來說隻有外出的連結才是允許的。

  在直接連結A失敗之後,B可以通過S向A中繼一個連結請求,進而從A方向“逆向“地建立起A-B之間的點對點連結。

  很多目前的P2P系統都實作了這種技術,但其局限性也是很明顯的,隻有當其中一方有公網IP時連結才能建立。越來越多的情況下,通信的雙方都在NAT之後,是以就要用到我們下面介紹的第三種技術了。

3.3 UDP打洞(UDP hole punching)

  第三種P2P通信技術,被廣泛采用的,名為“P2P打洞“。P2P打洞技術依賴于通常防火牆和cone NAT允許正當的P2P應用程式在中間件中打洞且與對方建立直接連結的特性。以下主要考慮兩種常見的場景,以及應用程式如何設計去完美地處理這些情況。第一種場景代表了大多數情況,即兩個需要直接連結的用戶端處在兩個不同的NAT之後;第二種場景是兩個用戶端在同一個NAT之後,但用戶端自己并不需要知道。

3.3.1. 端點在不同的NAT之下

  假設用戶端A和用戶端B的位址都是内網位址,且在不同的NAT後面。A、B上運作的P2P應用程式和伺服器S都使用了UDP端口1234,A和B分别初始化了與Server的UDP通信,位址映射如圖所示:

P2P通信原理與實作(C++)

  現在假設用戶端A打算與用戶端B直接建立一個UDP通信會話。如果A直接給B的公網位址138.76.29.7:31000發送UDP資料,NAT B将很可能會無視進入的資料(除非是Full Cone NAT),因為源位址和端口與S不比對,而最初隻與S建立過會話。B往A直接發資訊也類似。

  假設A開始給B的公網位址發送UDP資料的同時,給伺服器S發送一個中繼請求,要求B開始給A的公網位址發送UDP資訊。A往B的輸出資訊會導緻NAT A打開一個A的内網位址與與B的外網位址之間的新通訊會話,B往A亦然。一旦新的UDP會話在兩個方向都打開之後,用戶端A和用戶端B就能直接通訊,而無須再通過引導伺服器S了。

  UDP打洞技術有許多有用的性質。一旦一個的P2P連結建立,連結的雙方都能反過來作為“引導伺服器”來幫助其他中間件後的用戶端進行打洞,極大減少了伺服器的負載。應用程式不需要知道中間件具體是什麼(如果有的話),因為以上的過程在沒有中間件或者有多個中間件的情況下也一樣能建立通信鍊路。

3.3.2. 端點在相同的NAT之下

  現在考慮這樣一種情景,兩個用戶端A和B正好在同一個NAT之後(而且可能他們自己并不知道),是以在同一個内網網段之内。用戶端A和伺服器S建立了一個UDP會話,NAT為此配置設定了公網端口62000,B同樣和S建立會話,配置設定到了端口62001,如下圖:

P2P通信原理與實作(C++)

  假設A和B使用了上節介紹的UDP打洞技術來建立P2P通路,那麼會發生什麼呢?首先A和B會得到由S觀測到的對方的公網IP和端口号,然後給對方的位址發送資訊。兩個用戶端隻有在NAT允許内網主機對内網其他主機發起UDP會話的時候才能正常通信,我們把這種情況稱之為"回環傳輸“(lookback translation),因為從内部到達NAT的資料會被“回送”到内網中而不是轉發到外網。例如,當A發送一個UDP資料包給B的公網位址時,資料包最初有源IP位址和端口位址10.0.0.1:1234和目的位址155.99.25.11:62001,NAT收到包後,将其轉換為源155.99.25.11:62000(A的公網位址)和目的10.1.1.3:1234,然後再轉發給B。即便NAT支援回環傳輸,這種轉換和轉發在此情況下也是沒必要的,且有可能會增加A與B的對話延時和加重NAT的負擔。

  對于這個問題,解決方案是很直覺的。當A和B最初通過S交換位址資訊時,他們應該包含自身的IP位址和端口号(從自己看),同時也包含從伺服器看的自己的位址和端口号。然後用戶端同時開始從對方已知的兩個的位址中同時開始互相發送資料,并使用第一個成功通信的位址作為對方位址。如果兩個用戶端在同一個NAT後,發送到對方内網位址的資料最有可能先到達,進而可以建立一條不經過NAT的通信鍊路;如果兩個用戶端在不同的NAT之後,發送給對方内網位址的資料包根本就到達不了對方,但仍然可以通過公網位址來建立通路。值得一提的是,雖然這些資料包通過某種方式驗證,但是在不同NAT的情況下完全有可能會導緻A往B發送的資訊發送到其他A内網網段中無關的結點上去的。

3.3.3. 固定端口綁定

  UDP打洞技術有一個主要的條件:隻有當兩個NAT都是Cone NAT(或者非NAT的防火牆)時才能工作。因為其維持了一個給定的(内網IP,内網UDP)二進制組和(公網IP, 公網UDP)二進制組固定的端口綁定,隻要該UDP端口還在使用中,就不會變化。如果像對稱NAT一樣,給每個新會話配置設定一個新的公網端口,就會導緻UDP應用程式無法使用跟外部端點已經打通了的通信鍊路。由于Cone NAT是當今最廣泛使用的,盡管有一小部分的對稱NAT是不支援打洞的,UDP打洞技術也還是被廣泛采納應用。

4. 具體實作

  如果了解了上面所說的内容,那麼代碼實作起來倒很簡單了 。這裡采用C++的異步IO庫來實作引導伺服器和P2P用戶端的簡單功能,目的是打通兩個用戶端的通信鍊路,使兩個不同區域網路之間的用戶端可以實作直接通信。

4.1 引導服務端設計

  引導伺服器運作在一個有公網位址的裝置上,并且接收指定端口的來自客戶的指令(這裡是用端口号2333)。

用戶端其實可以而且也最好應該與伺服器建立TCP連結,但我這裡為了圖友善,也隻采用了UDP的通信方式。服務端監聽2333端口的指令,然後執行相應的操作,目前包含的指令有:

login, 用戶端登入,使得其記錄在伺服器traker中,讓其他peer可以對其發對外連結接請求。

logout,用戶端登出,使其對peer隐藏。因為伺服器不會追蹤用戶端的登入狀态。

list,用戶端檢視目前的登入使用者。

punch <client>, 對指定使用者(序号)進行打洞。

help, 檢視有哪些可用的指令。

4.2 P2P用戶端設計

  一般的網絡程式設計,都是用戶端比服務端要難,因為要處理與伺服器的通信同時還要處理來自使用者的事件;對于P2P用戶端來說更是如此,因為P2P用戶端不止作為用戶端,同時也作為對等連接配接的伺服器端。

這裡的大體思路是,輸入指令傳輸給伺服器之後,接收來自伺服器的回報,并執行相應代碼。例如A想要與B建立通信鍊路,先給伺服器發送punch指令以及給B發送資料,伺服器接到指令後給B發送punch_requst資訊以及A的端點資訊,B收到之後向A發送資料打通通路,然後A與B就可以進行P2P通信了。經測試,打通通路後即便把伺服器關閉,A與B也能正常通信。

一個UDP打洞的例子見 https://github.com/pannzh/P2P-Over-MiddleBoxes-Demo

# 2018-04-06 更新

關于TCP打洞,有一點需要提的是,因為TCP是基于連接配接的,是以任何未經連接配接而發送的資料都會被丢棄,這導緻在recv的時候是無法直接從peer端讀取資料。

其實這對UDP也一樣,如果對UDP的socket進行了connect,其也會忽略連接配接之外的資料,詳見`connect(2)`。

是以,如果我們要進行TCP打洞,通常需要重用本地的endpoint來發起新的TCP連接配接,這樣才能将已經打開的NAT利用起來。具體來說,則是要設定socket的

`SO_REUSEADDR`或`SO_REUSEPORT`屬性,根據系統不同,其實作也不盡一緻。一般來說,TCP打洞的步驟如下:

- A 發送 SYN 到 B (出口位址,下同),進而建立NAT A的一組映射

- B 發送 SYN 到 A, 建立NAT B的一組映射

- 根據時序不同,兩個SYN中有一個會被對方的NAT丢棄,另一個成功通過NAT

- 通過NAT的SYN封包被其中一方收到,即傳回SYNACK, 完成握手

- 至此,TCP的打洞成功,獲得一個不依賴于伺服器的連結

PS: 後續文章更新在:

https://evilpan.com/2015/10/31/p2p-over-middle-box/

=== 文檔資訊 ===

  • 版權聲明: 署名-非商業性使用(創意共享4.0許可證)
  • 個人站: evilpan.com
  • 部落格園: cnblogs.com/pannengzhi/
  • 公衆号: 有價值炮灰

歡迎交流,轉載請保留出處

=== EOF ===

繼續閱讀