上一篇P2P通訊標準協議(一)介紹了在NAT上進行埠繫結的通用規則,應用程式可以根據這個協議來設計網路以外的通訊。
但是,STUN/RFC5389協議裡能處理的也只有市面上大多數的Cone NAT
(關於NAT型別可以參照P2P通訊原理與實現),
對於Symmetric NAT
,傳統的P2P打洞方法是不適用的。因此為了保證通訊能夠建立,我們可以在沒辦法的情況下用保證成功的中繼方法(Relaying),
雖然使用中繼會對伺服器負擔加重,而且也算不上P2P,但是至少保證了最壞情況下通道的通暢,從而不至於受NAT型別的限制。TURN/RFC5766就是為此目的而進行的擴充。
TURN簡介
TURN的全稱為Traversal Using Relays around NAT,是STUN/RFC5389的一個擴充,主要新增了Relay功能。如果終端在NAT之後,
那麼在特定的情景下,有可能使得終端無法和其對等端(peer)進行直接的通訊,這時就需要公網的伺服器作為一箇中繼,
對來往的資料進行轉發。這個轉發的協議就被定義為TURN。TURN和其他中繼協議的不同之處在於,它允許客戶端使用同一個中繼地址(relay address)
與多個不同的peer進行通訊。
使用TURN協議的客戶端必須能夠通過中繼地址和對等端進行通訊,並且能夠得知每個peer的的IP地址和埠(確切地說,應該是peer的伺服器反射地址)。
而這些行為如何完成,是不在TURN協議範圍之內的。其中一個可用的方式是客戶端通過email來告知對等端資訊,
另一種方式是客戶端使用一些指定的協議,如“introduction” 或 “rendezvous”,詳見RFC5128
如果TURN使用於ICE協議中,relay地址會作為一個候選,由ICE在多個候選中進行評估,選取最合適的通訊地址。一般來說中繼的優先順序都是最低的。
TURN協議被設計為ICE協議(Interactive Connectivity Establishment)的一部分,而且也強烈建議使用者在他們的程式裡使用ICE,但是也可以獨立於ICE的執行。
值得一提的是,TURN協議本身是STUN的一個擴充,因此絕大部分TURN報文都是STUN型別的,作為STUN的一個擴充,TURN增加了新的方法(method)和屬性(attribute)。
因此閱讀本章時最好先了解一下STUN協議。
操作概述
在典型的情況下,TURN客戶端連線到內網中,並且通過一個或者多個NAT到達公網,TURN伺服器架設在公網中,不同的客戶端以TURN伺服器為中繼和其他peer進行通訊,如下圖所示:
Peer A
Server-Reflexive +---------+
Transport Address | |
192.0.2.150:32102 | |
| /| |
TURN | / ^| Peer A |
Client’s Server | / || |
Host Transport Transport | // || |
Address Address | // |+---------+
10.1.1.2:49721 192.0.2.15:3478 |+-+ // Peer A
| | ||N| / Host Transport
| +-+ | ||A|/ Address
| | | | v|T| 192.168.100.2:49582
| | | | /+-+
+---------+| | | |+---------+ / +---------+
| || |N| || | // | |
| TURN |v | | v| TURN |/ | |
| Client |----|A|----------| Server |------------------| Peer B |
| | | |^ | |^ ^| |
| | |T|| | || || |
+---------+ | || +---------+| |+---------+
| || | |
| || | |
+-+| | |
| | |
| | |
Client’s | Peer B
Server-Reflexive Relayed Transport
Transport Address Transport Address Address
192.0.2.1:7000 192.0.2.15:50000 192.0.2.210:49191
在上圖中,左邊的TURN Client是位於NAT後面的一個客戶端(內網地址是10.1.1.2:49721),連線公網的TURN伺服器(預設埠3478)後,
伺服器會得到一個Client的反射地址
(Reflexive Transport Address, 即NAT分配的公網IP和埠)192.0.2.1:7000,
此時Client會通過TURN命令建立或管理ALLOCATION
,allocation是伺服器上的一個資料結構,包含了中繼地址的資訊。
伺服器隨後會給Client分配一箇中繼地址,即圖中的192.0.2.15:50000,另外兩個對等端若要通過TURN協議和Client進行通訊,
可以直接往中繼地址收發資料即可,TURN伺服器會把發往指定中繼地址的資料轉發到對應的Client,這裡是其反射地址。
Server上的每一個allocation都唯一對應一個client,並且只有一箇中繼地址,因此當資料包到達某個中繼地址時,伺服器總是知道應該將其轉發到什麼地方。
但值得一提的是,一個Client可能在同一時間在一個Server上會有多個allocation,這和上述規則是並不矛盾的。
傳輸
在協議中,TURN伺服器與peer之間的連線都是基於UDP的,但是伺服器和客戶端之間可以通過其他各種連線來傳輸STUN報文,
比如TCP/UDP/TLS-over-TCP. 客戶端之間通過中繼傳輸資料時候,如果用了TCP,也會在服務端轉換為UDP,因此建議客戶端使用
UDP來進行傳輸. 至於為什麼要支援TCP,那是因為一部分防火牆會完全阻擋UDP資料,而對於三次握手的TCP資料則不做隔離.
分配(Allocations)
要在伺服器端獲得一箇中繼分配,客戶端須使用分配事務. 客戶端傳送分配請求(Allocate request)到伺服器,然後伺服器
返回分配成功響應,幷包含了分配的地址.客戶端可以在屬性欄位描述其想要的分配型別(比如生命週期).由於中繼資料實現了
安全傳輸,伺服器會要求對客戶端進行驗證,主要使用STUN的long-term credential mechanism
.
一旦中繼傳輸地址分配好,客戶端必須要將其保活.通常的方法是傳送重新整理請求(Refresh request)到服務端.這在TURN
中是一個標準的方法.重新整理頻率取決於分配的生命期,預設為10分鐘.客戶端也可以在重新整理請求裡指定一個更長的生命期,
而伺服器會返回一個實際上分配的時間. 當客戶端想中指通訊時,可以傳送一個生命期為0的重新整理請求.
伺服器和客戶端都儲存有一個成為五元組(5-TUPLE)的資訊,比如對於客戶端來說,五元組包括客戶端本地地址/埠,伺服器地址/埠,
和傳輸協議;伺服器也是類似,只不過將客戶端的地址變為其反射地址,因為那才是伺服器所見到的. 伺服器和客戶端在分配
請求中都帶有5-TUPLE資訊,並且也在接下來的資訊傳輸中使用,因此彼此都知道哪一次分配對應哪一次傳輸.
TURN TURN Peer Peer
client server A B
|-- Allocate request --------------->| | |
| | | |
|<--------------- Allocate failure --| | |
| (401 Unauthorized) | | |
| | | |
|-- Allocate request --------------->| | |
| | | |
|<---------- Allocate success resp --| | |
| (192.0.2.15:50000) | | |
// // // //
| | | |
|-- Refresh request ---------------->| | |
| | | |
|<----------- Refresh success resp --| | |
| | | |
如上圖所示,客戶端首先傳送Allocate請求,但是沒帶驗證資訊,因此STUN伺服器會返回error response,客戶端收到錯誤後加上
所需的驗證資訊再次請求,才能進行成功的分配.
傳送機制(Send Mechanism)
client和peer之間有兩種方法通過TURN server交換應用資訊,第一種是使用Send
和Data
方法(method),第二種是使用
通道(channels),兩種方法都通過某種方式告知伺服器哪個peer應該接收資料,以及伺服器告知client資料來自哪個peer.
Send Mechanism使用了Send和Data指令(Indication).其中Send指令用來把資料從client傳送到server,而Data指令用來把資料從
server傳送到client.當使用Send指令時,客戶端傳送一個Send Indication到服務端,其中包含:
- XOR-PEER-ADDRESS屬性,指定對等端的(伺服器反射)地址.
- DATA屬性,包含要傳給對等端的資訊.
當伺服器收到Send Indication之後,會將DATA部分的資料解析出來,並將其以UDP的格式轉發到對應的端點去,並且在封裝
資料包的時候把client的中繼地址作為源地址.從而從對等端傳送到中繼地址的資料也會被伺服器轉發到client上.
值得一提的是,Send/Data Indication是不支援驗證的,因為長效驗證機制不支援對indication的驗證,因此為了防止攻擊,
TURN要求client在給對等端傳送indication之前先安裝一個到對等端的許可(permission),如下圖所示,client到Peer B
沒有安裝許可,導致其indication資料包將被伺服器丟棄,對於peer B也是同樣:
TURN TURN Peer Peer
client server A B
| | | |
|-- CreatePermission req (Peer A) -->| | |
|<-- CreatePermission success resp --| | |
| | | |
|--- Send ind (Peer A)-------------->| | |
| |=== data ===>| |
| | | |
| |<== data ====| |
|<-------------- Data ind (Peer A) --| | |
| | | |
| | | |
|--- Send ind (Peer B)-------------->| | |
| | dropped | |
| | | |
| |<== data ==================|
| dropped | | |
| | | |
TURN支援兩種方式來建立許可,一種是傳送CreatePermission request
通道機制(Channels)
對於一些應用程式,比如VOIP(Voice over IP),在Send/Data Indication中多加的36位元組格式資訊會加重客戶端和服務端
之間的頻寬壓力.為改善這種情況,TURN提供了第二種方法來讓client和peer互動資料.該方法使用另一種資料包格式,
即ChannelData message
,通道資料包文. ChannelData message不使用STUN頭部,而使用一個4位元組的頭部,包含了
一個稱之為通道號的值(channel number).每一個使用中的通道號都與一個特定的peer繫結,即作為對等端地址的一個記號.
要將一個通道與對等端繫結,客戶端首先傳送一個通道繫結請求(ChannelBind Request)到伺服器,並且指定一個未繫結的通道號以及對等端的地址資訊.
繫結後client和server都能通過ChannelData message來傳送和轉發資料.通道繫結預設持續10分鐘,並且可以通過重新傳送
ChannelBind Request來重新整理持續時間.和Allocation不同的是,並沒有直接刪除繫結的方法,只能等待其超時自動失效.
TURN TURN Peer Peer
client server A B
| | | |
|-- ChannelBind req ---------------->| | |
| (Peer A to 0x4001) | | |
| | | |
|<---------- ChannelBind succ resp --| | |
| | | |
|-- [0x4001] data ------------------>| | |
| |=== data ===>| |
| | | |
| |<== data ====| |
|<------------------ [0x4001] data --| | |
| | | |
|--- Send ind (Peer A)-------------->| | |
| |=== data ===>| |
| | | |
| |<== data ====| |
|<------------------ [0x4001] data --| | |
| | | |
上圖中0x4001為通道號,即ChannelData message的頭部中頭2位元組,值得一提的是通道號的選取有如下要求:
- 0x0000-0x3FFF : 這一段的值不能用來作為通道號
- 0x4000-0x7FFF : 這一段是可以作為通道號的值,一共有16383種不同值在目前來看是足夠用的
- 0x8000-0xFFFF : 這一段是保留值,留給以後使用
還是那句老話,關於協議具體的細節可以去翻閱RFC5766的草稿,其中每個屬性以及其格式都介紹得很詳細.
例項
在上一章也提到過,因為RFC是標準協議,因此實現上往往有良好的相容性和擴充性.現存的開源P2P應用程式,
如果按照標準來設計,可以很容易與之對接.其中比較著名的就是PJSIP,PJSIP是一個開源的多媒體
通訊庫,實現了許多標準協議,如SIP, SDP, RTP, STUN, TURN 和 ICE. 當然我們也能自己實現.比如GitHub
上的TurnServer就是其中一個對TURN服務端的實現.下面在區域網環境下對TURN資料包進行
簡要分析.首先有如下機器情況:
- TurnServer執行在192.168.1.110,使用預設埠3478,採用使用者名稱和密碼驗證,其中使用者名稱為pannzh,密碼123456
- TurnClient執行在192.168.1.106,為了方便,令peer也在192.168.1.106執行,埠為59593
這裡使用wireshark來抓包分析,關於wireshark的簡介可以參照我之前的文章細說中間人攻擊(一),
首先TurnClient傳送Allocation請求:
可以看到第一次requst被伺服器拒絕,因為後者要求nonce驗證資訊,伺服器的返回中包含了nonce資訊,
除此之外還包含了ERROR-CODE,SOFTWARE,FINGERPRINT屬性.
在下一次request請求中,客戶端加上了收到的nonce,以及USERNAME和REALM等屬性,再次傳送到TurnServer:
伺服器接收到了正確的allocation請求,於是返回succcess response,可以看到在返回中帶有預設的lifetime為1800秒,
XOR-MAPPED-ADDRESS以及XOR-RELAY-ADDRESS等屬性:
前文也說過,若要和peer進行通訊,必須先建立一個許可,因此Client向伺服器傳送CreatePermission請求,其中攜帶了peer的資訊:
伺服器如果通過驗證,就會返回success response,隨後Client可以通過上文說到的兩種方法與Peer進行通訊,比如下面的Send indication方法:
通過對TurnServer傳送indication告知資料的接收方以及資料內容讓TurnServer進行轉發,從而間接地向對等端傳送DATA.
而從對等端來看,就是收到一個從client的relay地址192.168.1.110:65315到目的地址192.168.1.106:59593(即peer地址)的UDP資料包.
後記
本來打算這篇介紹完TURN和ICE的,不過後來發現內容實在有點多,即便是隻粗略介紹.因此只能把ICE協議的介紹留在下一篇來說了.
TURN協議因為是STUN的擴充,當然也沿襲了STUN的工具性質,只為穿越NAT提供方法,而不作為P2P通訊的完整解決方案.一個比較適合
研究的TurnServer原始碼我也放到了這裡,,而客戶端的實現則根據每個人的具體需求而不同,因此不再贅述.