P2P通訊標準協議(二)之TURN

有價值炮灰發表於2015-12-15

上一篇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交換應用資訊,第一種是使用SendData方法(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請求:

allocation

可以看到第一次requst被伺服器拒絕,因為後者要求nonce驗證資訊,伺服器的返回中包含了nonce資訊,
除此之外還包含了ERROR-CODE,SOFTWARE,FINGERPRINT屬性.

error-nonce

在下一次request請求中,客戶端加上了收到的nonce,以及USERNAME和REALM等屬性,再次傳送到TurnServer:

allocation2

伺服器接收到了正確的allocation請求,於是返回succcess response,可以看到在返回中帶有預設的lifetime為1800秒,
XOR-MAPPED-ADDRESS以及XOR-RELAY-ADDRESS等屬性:

allocation-success

前文也說過,若要和peer進行通訊,必須先建立一個許可,因此Client向伺服器傳送CreatePermission請求,其中攜帶了peer的資訊:

createpermission

伺服器如果通過驗證,就會返回success response,隨後Client可以通過上文說到的兩種方法與Peer進行通訊,比如下面的Send indication方法:

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原始碼我也放到了這裡,,而客戶端的實現則根據每個人的具體需求而不同,因此不再贅述.

相關文章