P2P通訊標準協議(一)之STUN

前一段時間在P2P通訊原理與實現中介紹了P2P打洞的基本原理和方法,咱們能夠根據其原理爲本身的網絡程序設計一套通訊規則,
固然若是這套程序只有本身在使用是沒什麼問題的。但是在現實生活中,咱們的程序每每還須要和第三方的協議(如SDP,SIP)進行對接,所以使用標準化
的通用規則來進行P2P連接創建是頗有必要的。本文就來介紹一下當前主要應用於P2P通訊的幾個標準協議,主要有STUN/RFC3489STUN/RFC5389
TURN/RFC5766以及ICE/RFC5245html

STUN簡介

在前言裏咱們看到,RFC3489和RFC5389的名稱都是STUN,但其全稱是不一樣的。在RFC3489裏,
STUN的全稱是Simple Traversal of User Datagram Protocol (UDP) Through Network Address Translators (NATs)
即穿越NAT的簡單UDP傳輸,是一個輕量級的協議,容許應用程序發現本身和公網之間的中間件類型,
同時也能容許應用程序發現本身被NAT分配的公網IP。這個協議在2003年3月被提出,其介紹頁面裏
說到已經被STUN/RFC5389所替代,後者纔是咱們要詳細介紹的。git

RFC5389中,STUN的全稱爲Session Traversal Utilities for NAT,即NAT環境下的會話傳輸工具,
是一種處理NAT傳輸的協議,但主要做爲一個工具來服務於其餘協議。和STUN/RFC3489
相似,能夠被終端用來發現其公網IP和端口,同時能夠檢測端點間的鏈接性,也能夠做爲一種保活(keep-alive)協議來維持NAT的綁定。
和RFC3489最大的不一樣點在於,STUN自己再也不是一個完整的NAT傳輸解決方案,而是在NAT傳輸環境中做爲一個輔助的解決方法,
同時也增長了TCP的支持。RFC5389廢棄了RFC3489,所以後者一般稱爲classic STUN,但依舊是後向兼容的。
而完整的NAT傳輸解決方案則使用STUN的工具性質,ICE就是一個基於offer/answer方法的完整NAT傳輸方案,如SIPgithub

STUN是一個C/S架構的協議,支持兩種傳輸類型。一種是請求/響應(request/respond)類型,由客戶端給服務器發送請求,
並等待服務器返回響應;另外一種是指示類型(indication transaction),由服務器或者客戶端發送指示,另外一方不產生響應。
兩種類型的傳輸都包含一個96位的隨機數做爲事務ID(transaction ID),對於請求/響應類型,事務ID容許客戶端將響應和產生響應的請求鏈接起來;
對於指示類型,事務ID一般做爲debugging aid使用。服務器

全部的STUN報文信息都含有一個固定頭部,包含了方法,類和事務ID。方法表示是具體哪種傳輸類型(兩種傳輸類型又分了不少具體類型),
STUN中只定義了一個方法,即binding(綁定),其餘的方法能夠由使用者自行拓展;Binding方法能夠用於請求/響應類型和指示類型,
用於前者時能夠用來肯定一個NAT給客戶端分配的具體綁定,用於後者時能夠保持綁定的激活狀態。類表示報文類型是請求/成功響應/錯誤響應/指示。
在固定頭部以後是零個或者多個屬性(attribute),長度也是不固定的。cookie

STUN報文結構

STUN報文和大多數網絡類型的格式同樣,是以大端編碼(big-endian)的,即最高有效位在左邊。全部的STUN報文都以20字節的頭部開始,後面跟着若干個屬性。下面來詳細說說。網絡

STUN報文頭部

STUN頭部包含了STUN消息類型,magic cookie,事務ID和消息長度,以下:架構

0                   1                   2                   3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |0 0|     STUN Message Type     |         Message Length        |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                         Magic Cookie                          |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                                                               |
  |                     Transaction ID (96 bits)                  |
  |                                                               |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

最高的2位必須置零,這能夠在當STUN和其餘協議複用的時候,用來區分STUN包和其餘數據包。
STUN Message Type字段定義了消息的類型(請求/成功響應/失敗響應/指示)和消息的主方法。
雖然咱們有4個消息類別,但在STUN中只有兩種類型的事務,即請求/響應類型和指示類型。
響應類型分爲成功和出錯兩種,用來幫助快速處理STUN信息。Message Type字段又能夠進一步分解爲以下結構:svn

0                 1
 2  3  4 5 6 7 8 9 0 1 2 3 4 5
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+
|M |M |M|M|M|C|M|M|M|C|M|M|M|M|
|11|10|9|8|7|1|6|5|4|0|3|2|1|0|
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+

其中顯示的位爲從最高有效位M11到最低有效位M0,M11到M0表示方法的12位編碼。C1和C0兩位表示類的編碼。好比對於binding方法來講,
0b00表示request,0b01表示indication,0b10表示success response,0b11表示error response,每個method都有可能對應不一樣的傳輸類別。
拓展定義新方法的時候注意要指定該方法容許哪些類型的消息。工具

Magic Cookie字段包含固定值0x2112A442,這是爲了前向兼容RFC3489,由於在classic STUN中,這一區域是事務ID的一部分。
另外選擇固定數值也是爲了服務器判斷客戶端是否能識別特定的屬性。還有一個做用就是在協議多路複用時候也能夠將其做爲判斷標誌之一。編碼

Transaction ID字段是個96位的標識符,用來區分不一樣的STUN傳輸事務。對於request/response傳輸,事務ID由客戶端選擇,
服務器收到後以一樣的事務ID返回response;對於indication則由發送方自行選擇。事務ID的主要功能是把request和response聯繫起來,
同時也在防止攻擊方面有必定做用。服務端也把事務ID看成一個Key來識別不一樣的STUN客戶端,所以必須格式化且隨機在0~2^(96-1)之間。
重發一樣的request請求時能夠重用相同的事務ID,可是客戶端進行新的傳輸時,必須選擇一個新的事務ID。

Message Length字段存儲了信息的長度,以字節爲單位,不包括20字節的STUN頭部。因爲全部的STUN屬性都是都是4字節對齊(填充)的,
所以這個字段最後兩位應該恆等於零,這也是辨別STUN包的一個方法之一。

STUN屬性

在STUN報文頭部以後,一般跟着0個或者多個屬性,每一個屬性必須是TLV編碼的(Type-Length-Value)。其中Type字段和Length字段都是16位,
Value字段爲爲32位表示,以下:

0                   1                   2                   3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |         Type                  |            Length             |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                         Value (variable)                ....
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Length字段必須包含Value部分須要補齊的長度,以字節爲單位。因爲STUN屬性以32bit邊界對齊,所以屬性內容不足4字節的都會以padding bit進行補齊。
padding bit會被忽略,但能夠是任何值。

Type字段爲屬性的類型。任何屬性類型都有可能在一個STUN報文中出現超過一次。除非特殊指定,不然其出現的順序是有意義的:
即只有第一次出現的屬性會被接收端解析,而其他的將被忽略。爲了之後版本的拓展和改進,屬性區域被分爲兩個部分。
Type值在0x0000-0x7FFF之間的屬性被指定爲強制理解,意思是STUN終端必需要理解此屬性,不然將返回錯誤信息;而0x8000-0xFFFF
之間的屬性爲選擇性理解,即若是STUN終端不識別此屬性則將其忽略。目前STUN的屬性類型由IANA維護。

這裏簡要介紹幾個常見屬性的Value結構:

  • MAPPED-ADDRESS

MAPPED-ADDRESS同時也是classic STUN的一個屬性,之因此還存在也是爲了前向兼容。其包含了NAT客戶端的反射地址,
Family爲IP類型,即IPV4(0x01)或IPV6(0x02),Port爲端口,Address爲32位或128位的IP地址。注意高8位必須所有置零,
並且接收端必需要將其忽略掉。

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0 0 0 0 0 0 0|    Family     |           Port                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                 Address (32 bits or 128 bits)                 |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • XOR-MAPPED-ADDRESS

XOR-MAPPED-ADDRESS和MAPPED-ADDRESS基本相同,不一樣點是反射地址部分通過了一次異或(XOR)處理。對於X-Port字段,
是將NAT的映射端口以小端形式與magic cookie的高16位進行異或,再將結果轉換成大端形式而獲得的,X-Address也是相似。
之因此要通過這麼一次轉換,是由於在實踐中發現不少NAT會修改payload中自身公網IP的32位數據,從而致使NAT打洞失敗。

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |x x x x x x x x|    Family     |         X-Port                |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                X-Address (Variable)
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • ERROR-CODE

ERROR-CODE屬性用於error response報文中。其包含了300-699表示的錯誤代碼,以及一個UTF-8格式的文字出錯信息(Reason Phrase)。

0                   1                   2                   3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |           Reserved, should be 0         |Class|     Number    |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |      Reason Phrase (variable)                                ..
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

另外,錯誤代碼在語義上還與SIP和HTTP協議保持一致。好比:

300:嘗試代替(Try Alternate),客戶端應該使用該請求聯繫一個代替的服務器。這個錯誤響應僅在請求包括一個

USERNAME屬性和一個有效的MESSAGE-INTEGRITY屬性時發送;不然它不會被髮送,而是發送錯誤代碼爲400的錯誤響應;
400:錯誤請求(Bad Request),請求變形了,客戶端在修改先前的嘗試前不該該重試該請求。
401:未受權(Unauthorized),請求未包括正確的資格來繼續。客戶端應該採用一個合適的資格來重試該請求。
420:未知屬性(Unknown Attribute),服務器收到一個STUN包包含一個強制理解的屬性可是它不會理解。
服務器必須將不認識的屬性放在錯誤響應的UNKNOWN-ATTRIBUTE屬性中。
438:過時Nonce(Stale Nonce),客戶端使用的Nonce再也不有效,應該使用響應中提供的Nonce來重試。
500:服務器錯誤(Server Error),服務器遇到臨時錯誤,客戶端應該再次嘗試。

此外還有不少屬性,如USERNAME,NONCE,REALM,SOFTWARE等,具體能夠翻閱RFC3489

STUN 通訊過程

1. 產生一個Request或Indication

當產生一個Request或者Indication報文時,終端必須根據上文提到的規則來生成頭部,class字段必須是Request或者Indication,
而method字段爲Binding或者其餘用戶拓展的方法。屬性部分選擇該方法所須要的對應屬性,好比在一些
情景下咱們會須要authenticaton屬性或FINGERPRINT屬性,注意在發送Request報文時候,須要加上SOFTWARE屬性(內含軟件版本描述)。

2. 發送Requst或Indication

目前,STUN報文能夠經過UDP,TCP以及TLS-over-TCP的方法發送,其餘方法在之後也會添加進來。STUN的使用者必須指定其使用的傳輸協議,
以及終端肯定接收端IP地址和端口的方式,好比經過基於DNS的方法來肯定服務器的IP和端口。

2.1 經過UDP發送

當使用UDP協議運行STUN時,STUN的報文可能會因爲網絡問題而丟失。可靠的STUN請求/響應傳輸是經過客戶端重發request請求來實現的,
所以,在UDP運行時,Indication報文是不可靠的。STUN客戶端經過RTO(Retransmission TimeOut)
來決定是否重傳Requst,而且在每次重傳後將RTO翻倍。具體重傳時間的選取能夠參考相關文章,如RFC2988。
重傳直到接收到Response才中止,或者重傳次數到達指定次數Rc,Rc應該是可配置的,且默認值爲7。

2.2 經過TCP或者TCP-over-TLS發送

對於這種情景,客戶端打開對服務器的鏈接。在某些狀況下,此TCP連接只傳輸STUN報文,而在其餘拓展中,
在一個TCP連接裏可能STUN報文和其餘協議的報文會進行多路複用(Multiplexed)。數據傳輸的可靠性由TCP協議自己來保證。
值得一提的是,在一次TCP鏈接中,STUN客戶端可能發起多個傳輸,有可能在前一個Request的Response還沒收到時就再次發送了一個新的Request,
所以客戶端應該保持TCP連接打開,認全部STUN事務都已完成。

3. 接收STUN消息

當STUN終端接收到一個STUN報文時,首先檢查報文的規則是否合法,即前兩位是否爲0,magic cookie是否爲0x2112A442,報文長度是否正確以及對應的方法是否支持。
若是消息類別爲Success/Error Response,終端會檢測其事務ID是否與當前正在處理的事務ID相同。若是使用了FINGERPRINT拓展的話還會檢查FINGERPRINT屬性是否正確。
完成身份認證檢查以後,STUN終端會接着檢查其他未知屬性。

3.1 處理Request

若是請求包含一個或者多個強制理解的未知屬性,接收端會返回error response,錯誤代碼420(ERROR-CODE屬性),
並且包含一個UNKNOWN-ATTRIBUTES屬性來告知發送方哪些強制理解的屬性是未知的。服務端接着檢查方法和其餘指定要求,若是全部檢查都成功,
則會產生一個Success Response給客戶端。

  • 3.1.1 生成Success Response或Error Response

    • 若是服務器經過某種驗證方法(authentication mechanism)經過了請求方的驗證,那麼在響應報文裏最好也加上對應的驗證屬性。
    • 服務器端也應該加上指定方法所須要的屬性信息,另外協議建議服務器返回時也加上SOFTWARE屬性。
    • 對於Binding方法,除非特別指明,通常不要求進行額外的檢查。當生成Success Response時,服務器在響應里加上XOR-MAPPED-ADDRESS屬性。
      對於UDP,這是其源IP和端口信息,對於TCP或TLS-over-TCP,這就是服務器端所看見的這次TCP鏈接的源IP和端口。
  • 3.1.2 發送Success Response或Error Response

    • 發送響應時候若是是用UDP協議,則發往其源IP和端口,若是是TCP則直接用相同的TCP連接回發便可。

3.2 處理Indication

若是Indication報文包含未知的強制理解屬性,則此報文會被接收端忽略並丟棄。若是對Indication報文的檢查都沒有錯誤,則服務端會進行相應的處理,
可是不會返回Response。對於Binding方法,通常不須要額外的檢查或處理。收到信息的服務端僅須要刷新對應NAT的端口綁定。

因爲Indication報文在用UDP協議傳輸時不會進行重傳,所以發送方也不須要處理重傳的狀況。

3.3 處理Success Response

若是Success Response包含了未知的強制理解屬性,則響應會被忽略而且認爲這次傳輸失敗。客戶端對報文進行檢查經過以後,就能夠開始處理這次報文。

以Binding方法爲例,客戶端會檢查報文中是否包含XOR-MAPPED-ADDRESS屬性,而後是地址類型,若是是不支持的地址類型,則這個屬性會被忽略掉。

3.4 處理Error Response

若是Error Response包含了未知的強制理解屬性,或者沒有包含ERROR-CODE屬性,則響應會被忽略而且認爲這次傳輸失敗。
隨後客戶端會對驗證方法進行處理,這有可能會產生新的傳輸。

  • 到目前爲止,對錯誤響應的處理主要基於ERROR-CODE屬性的值,並遵循以下規則:

    • 若是error code在300到399之間,客戶端被建議認爲這次傳輸失敗,除非用了ALTERNATE-SERVER拓展;
    • 若是error code在400到499之間,客戶端認爲這次傳輸失敗;
    • 若是error code在500到599之間,客戶端可能會須要重傳請求,而且必須限制重傳的次數。

任何其餘的error code值都會致使客戶端認爲這次傳輸失敗。

後記

上面只是介紹了STUN/RFC5389協議的基礎部分,協議自己還包含了許多mechanism,如身份驗證(Authentication),DNS Discovery,FINGERPRINT Mechanisms,
ALTERNATE-SERVER Mechanism等,身份驗證又分爲長期驗證和短時間驗證,從而保證了傳輸的靈活性並減小服務器的負擔。具體能夠詳細閱讀白皮書。
我原本打算一篇文章把P2P通訊的全部協議都介紹完不過如今看來彷佛篇幅過長了,因此關於TURN和ICE就放在下一篇介紹好了。
另外因爲SourceForge的StunServer的源代碼已經長期不更新,所以我從svn的倉庫中整理了一下放到了GitHub上面,
須要的能夠自行去取來參考一下STUN交互的實現,固然了雖然實現的是TurnServer,但除了Relay部分基本上都是和STUN相似的。
另外在P2P原理與實現所作的一個P2P聊天應用時, 順便也作了個基於RFC3489的STUN客戶端, 基於Python3,
用於檢測用戶的NAT類型, 能夠參見p2p-over-middleboxes.

相關文章
相關標籤/搜索