基于TCP/UDP的P2P网络通信协议研究与实现

此章节是理论知识,下个章节会奉献源码

摘    要

 

对等式网络(peer-to-peer,简称P2P),又称点对点技术,是一种实现网络中不同主机直接通信的技术。在物联网的应用中,大量的设备需要能进行点对点的通信。但如今的互联网中存在着一些中间件,如NAT和防火墙,导致两个不在同一内网的客户端无法直接通信。本文讨论如何跨越NAT实现网络中的主机直接通信的问题,研究与实现基于TCP/UDP的P2P网络通信协议。

关键词:P2P点对点网络通信NAT

 

目录

第1章 前言 1

第2章 常见的几种中间件 2

2.1防火墙(Firewall) 2

2.2网络地址转换器(NAT) 2

2.3基本NAT 3

2.4网络地址-端口转换器(NAPT) 3

2.5锥形NAT(Cone NAT) 3

2.6对称NAT(Cone NAT) 3

第3章 常见的几种P2P技术实现方式 4

3.1中继(Relaying) 4

3.1.1 技术原理 4

3.2 UDP打洞(UDP hole punching) 4

3.2.1 技术原理 4

3.2.2 端点在不同的NAT之下 5

3.2.3 端点在相同的NAT之下 6

3.3 不同的NAT对UDP打洞的影响 7

3.3.1 全锥形NAT 7

3.3.2 受限锥形NAT 7

3.3.3 端口受限锥形NAT 7

3.3.4 对称NAT 8

3.3.5 四种NAT比较 8

第4章 P2P技术实现 9

4.1 STUN技术 9

4.1.1 STUN技术原理 9

4.1.2 STUN报文结构 10

4.1.3 STUN通信过程 11

4.2 TURN技术 14

4.2.1 TURN技术原理 14

4.2.2 TURN传输过程 14

4.3 ICE技术 18

4.3.1 ICE技术原理 18

4.3.2 ICE的工作流程 18

第5章 结论 22

参考文献 23

致    谢 24

附录: 25

 

 

第1章 前言

P2P是(Peer to Peer)的缩写,在计算机网络通信中P2P是相对于服务器/客户端模式而言的,通常的服务器/客户端模式下有一台强大的服务器接受大量的客户端的连接。当客户端之间需要通信的时候需要经由服务器转发。这种模式当客户端规模扩展到一定程度时对于服务器的CPU处理能力、带宽都是很大的考验。P2P技术可以让客户端之间直接通信,实现所谓的端到端(P2P)直接通信,此时中心服务器的负荷明显降低。随着物联网的兴起,大量设备的接入,网络通信的需求越来越大,传统的服务器/客户端模式已经很难满足需求。P2P技术为万物互联的概念提供了关键的技术基础。


2 常见的几种中间件

本章将介绍P2P通信网络中的各种中间件,以及它们在整个网络通信过程中起的作用。

2.1防火墙(Firewall

防火墙技术是通过有机结合各类用于安全管理与筛选的软件和硬件设备,帮助计算机网络于其内、外网之间构建一道相对隔绝的防护屏障,用以保护用户资料和信息安全性的一种技术。

防火墙主要限制内网和公网的通信,通常会丢弃未经许可的数据包。防火墙会检测试图进入内网数据包的IP地址和端口号信息。

2.2网络地址转换器(NAT

NAT最初定义在RFC1631,用在接入广域网中,通过修改IP报文的地址信息,实现将内部网络的私有IP地址到外部网络的公有IP地址的转换。私有IP地址是指内部网络或主机的IP地址,公有IP地址是指在因特网上全球唯一的IP地址。

如图2-1显示:

基于TCP/UDP的P2P网络通信协议研究与实现

 

图2-1 NAT数据传输过程

  1. 计算机A要连接Internet时,数据包会先发送给NAT主机,此时数据包的Header的sourceIP(源IP)为 192.168.1.100
  2. NAT主机接收到这个数据包的时候,会将计算机A对外发送的数据包的sourceIP(192.168.1.100)修改成ppp0这个接口所具有的公共IP。因为是公共IP这个数据包就可以连上Internet了,同时NAT会记下这个数据包是由哪个(192.168.1.100)计算机发送过来的。
  3. 由Internet传送回来的数据包,也会经过NAT,这个时候NAT主机会查询原本记录的路由信息,并将IP由ppp0上面的公共IP改回原来的192.168.1.100
  4. 最后由NAT将该数据包传送给原先的计算机A(192.168.1.100)

.

2.3基本NAT

基本NAT会将内网主机的IP地址映射为一个公网IP,不改变其TCP/UDP端口号。基本NAT通常只有NAT有公网IP池的时候才会起作用。

2.4网络地址-端口转换器(NAPT)

目前为止最常见的是NAPT,会检测并修改出入数据包的IP和端口号,从而允许内网多个主机共享同一个公网IP。

2.5锥形NAT(Cone NAT)

在建立一对(公网IP,公网端口号 和 内网IP、内网端口号)二元组的绑定后,Cone NAT会重用这组绑定用于接下里该应用程序的所有会话(同一个内网IP和端口号),只要有一个会话还是**状态的。

2.6对称NAT(Cone NAT)

对称NAT正好相反,不在所有公网-内网对的会话中维持一个固定的端口绑定。其为每个新的会话开辟一个新的端口。

 

 

 

 

 

3 常见的几种P2P技术实现方式

根据客户端的不同,本章将介绍现有的P2P通信的几种技术实现方式。

3.1中继(Relaying

3.1.1 技术原理

中继技术是目前最靠谱也是最简单的一种P2P通信实现技术。原理是通过一个有公网IP的服务器,对两个内网的客户端数据进行中继转发。如图3-1所示:

 

 

图3-1 中继架构图基于TCP/UDP的P2P网络通信协议研究与实现

客户端A和客户端B不直接通信,而是先通过服务器S建立链接。客户端A把数据发送给服务器S,再由服务器S将数据转发给客户端B。

很明显,当链接的客户端变多了之后,会显著增加服务器的负担,完全没有体现出P2P的优势。

 

3.2 UDP打洞UDP hole punching

3.2.1 技术原理

UDP打洞是被广泛采用的P2P通信技术,也称之为“P2P打洞”。UDP打洞技术依赖于通常防火墙和cone NAT允许正当的P2P应用程序在中间件中打洞且与对方建立直接链接的特性。

以下主要考虑两种常见的场景,以及应用程序如何设计去完美地处理这些情况。第一种场景代表了大多数情况,即两个需要直接链接的客户端处在两个不同的NAT之后;第二种场景是两个客户端在同一个NAT之后,但客户端自己并不需要知道。

3.2.2 端点在不同的NAT之下

假设客户端A和客户端B的地址都是内网地址,且在不同的NAT下。A、B上运行的P2P应用程序和服务器S都使用了UDP端口8888,A和B分别初始化了与Server的UDP通信,地址映射如图3-2所示:

基于TCP/UDP的P2P网络通信协议研究与实现

图3-2 不同的NAT

  1. NAT A将 客户端A 的IP地址、端口号映射为 公网IP(157.100.34.32 : 7800)。NAT B将客户端B的IP地址、端口号映射为 公网IP(159.70.50.4:8900)。
  2. 假设此时客户端 A打算与客户端B建立一个UDP的通信会话。此时客户端A直接往客户端B的公网地址159.70.50.4:8900送UDP数据,NAT B将很可能会无视进入的数据(除非是Full Cone NAT),因为源地址和端口与S不匹配,而最初只与S建立过会话。B往A直接发信息也类似。
  3. 假设A开始给B的公网地址发送UDP数据的同时,给服务器S发送一个中继请求,要求B开始给A的公网地址发送UDP信息。A往B的输出信息会导致NAT A打开一个A的内网地址与与B的外网地址之间的新通讯会话,B往A亦然。一旦新的UDP会话在两个方向都打开之后,客户端A和客户端B就能直接通讯,而无须再通过引导服务器S了。
  4. UDP打洞技术有许多有用的性质。一旦一个的P2P链接建立,链接的双方都能反过来作为“引导服务器”来帮助其他中间件后的客户端进行打洞,极大减少了服务器的负载。应用程序不需要知道中间件具体是什么(如果有的话),因为以上的过程在没有中间件或者有多个中间件的情况下也一样能建立通信链路。

 

3.2.3 端点在相同的NAT之下

现在考虑这样一种情景,两个客户端A和B正好在同一个NAT之后(而且可能他们自己并不知道),因此在同一个内网网段之内。客户端A和服务器S建立了一个UDP会话,NAT为此分配了公网端口62000,B同样和S建立会话,分配到了端口62001,如下图:

基于TCP/UDP的P2P网络通信协议研究与实现

图3-2 相同的NAT

  假设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 不同的NAT对UDP打洞的影响

UDP打洞技术目前依赖于NAT类型。本章将讨论不同的NAT下,UDP打洞的可行性与技术难点。

目前NAT的主要分类是 锥形NAT、对称NAT。而锥形NAT又可分为三类:全锥形NAT、受限锥形NAT、端口受限锥形NAT。

3.3.1 全锥形NAT

在一个新会话建立了公网/内网端口绑定之后,全锥形NAT接下来会接受对应公网端口的所有数据,无论是来自哪个(公网)终端。全锥NAT有时候也被称为“混杂”NAT(promiscuous NAT)。

故而,全锥形NAT是最容易实现UDP打洞技术的。一旦建立公网/内网端口绑定后,任何客户端都可以直接和内网的主机通信。

3.3.2 受限锥形NAT

在受限锥形NAT只会转发符合某个条件的输入数据包。条件为:外部(源)IP地址匹配内网主机之前发送一个或多个数据包的结点的IP地址。受限NAT通过限制输入数据包为一组“已知的”外部IP地址,有效地精简了防火墙的规则。

处于受限锥形NAT下的客户端A想和客户端B进行P2P通信,一开始,A给B的公网地址发送UDP数据的同时,给服务器S发送一个中继请求,要求B开始给A的公网地址发送UDP信息。A往B的输出信息会导致NAT A打开一个A的内网地址与与B的外网地址之间的新通讯会话,B往A亦然。一旦新的UDP会话在两个方向都打开之后,客户端A和客户端B就能直接通讯。

3.3.3 端口受限锥形NAT

端口受限锥形NAT类似于受限锥形,只当外部数据包的IP地址和端口号都匹配内网主机发送过的地址和端口号时才进行转发。端口受限锥形NAT为内部结点提供了和对称NAT相同等级的保护,以隔离未关联的数据。

端口受限锥形NAT实现UDP打洞的原理和受限锥形NAT一样,也是需要想服务器发送一个中继请求。

3.3.4 对称NAT

假设客户端A、B都处于对称NAT下。一开始,A给B的公网地址发送UDP数据的同时,给服务器S发送一个中继请求,要求B开始给A的公网地址发送UDP信息。由于对称NAT为每个新的会话开辟一个新的端口。此时NAT为B开启了一个新的端口,而服务器、A都不知道这个新的端口是多少,故而无法实现UDP打洞。

3.3.5 四种NAT比较

假设客户端A(192.168.0.3, 100)和server(1.1.1.1, 1111)在路由器上建立好映射关系后,如果这个时候路由器(8.8.8.8)在800端口上收到从另外一台server(2.2.2.2, 2222)发来的数据,此时有四种情况:

1、无条件转发给(192.168.0.3, 100), 这就是全锥型(Full Cone)NAT。

2、如果(192.168.0.3, 100)之前给(2.2.2.2)发送过数据,则转发, 这就是受限锥型(Restricted Cone)。

3、如果(192.168.0.3, 100)之前给(2.2.2.2, 2222)发送过数据,则转发,这就是端口受限锥型(Port Restricted Cone)。

4、丢弃报文,拒绝转发, 这就是对称型NAT。

从上面也描述也可以看出,安全性系数:对称型 > 端口受限锥型 > 受限锥型 > 全锥型。

不同NAT的穿透性:

NAT A类型

NAT B类型

能否udp打洞

全锥型

全锥型

全锥型

受限锥型

全锥型

端口受限锥型

全锥型

对称型

受限锥型

受限锥型

受限锥型

端口受限锥型

受限锥型

对称型

端口受限锥型

端口受限锥型

端口受限锥型

对称型

✘, 无法打通

对称型

对称型

✘, 无法打通

表3-1 不同的NAT传统性

 

4 P2P技术实现

第3章节介绍了P2P打洞的基本原理和方法,本章节介绍一下当前主要应用于P2P通信的几个标准协议,主要有STUN/RFC3489STUN/RFC5389TURN/RFC5766以及ICE/RFC5245

4.1 STUN技术

4.1.1 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所替代。

RFC5389中,STUN的全称为Session Traversal Utilities for NAT,即NAT环境下的会话传输工具,是一种处理NAT传输的协议,但主要作为一个工具来服务于其他协议。和STUN/RFC3489类似,可以被终端用来发现其公网IP和端口,同时可以检测端点间的连接性,也可以作为一种保活(keep-alive)协议来维持NAT的绑定。
和RFC3489最大的不同点在于,STUN本身不再是一个完整的NAT传输解决方案,而是在NAT传输环境中作为一个辅助的解决方法,同时也增加了TCP的支持。RFC5389废弃了RFC3489,因此后者通常称为classic STUN,但依旧是后向兼容的。

STUN是一个C/S架构的协议,支持两种传输类型。一种是请求/响(request/respond)类型,由客户端给服务器发送请求,并等待服务器返回响应;另一种是指示类型(indication transaction),由服务器或者客户端发送指示,另一方不产生响应。
两种类型的传输都包含一个96位的随机数作为事务ID(transaction ID),对于请求/响应类型,事务ID允许客户端将响应和产生响应的请求连接起来;对于指示类型,事务ID通常作为debugging aid使用。

所有的STUN报文信息都含有一个固定头部,包含了方法,类和事务ID。方法表示是具体哪一种传输类型(两种传输类型又分了很多具体类型),STUN中只定义了一个方法,即binding(绑定),其他的方法可以由使用者自行拓展;Binding方法可以用于请求/响应类型和指示类型,用于前者时可以用来确定一个NAT给客户端分配的具体绑定,用于后者时可以保持绑定的**状态。类表示报文类型是请求/成功响应/错误响应/指示。在固定头部之后是零个或者多个属性(attribute),长度也是不固定的。

4.1.2 STUN报文结构

STUN报文和大多数网络类型的格式一样,是以大端编码(big-endian)的,即最高有效位在左边。所有的STUN报文都以20字节的头部开始,后面跟着若干个属性。

STUN头部包含了STUN消息类型,magic cookie,事务ID和消息长度,如下:

基于TCP/UDP的P2P网络通信协议研究与实现

图4-1 STUN报文头部

最高的2位必须置零,这可以在当STUN和其他协议复用的时候,用来区分STUN包和其他数据包。
STUN Message Type字段定义了消息的类型(请求/成功响应/失败响应/指示)和消息的主方法。虽然我们有4个消息类别,但在STUN中只有两种类型的事务,即请求/响应类型和指示类型。
响应类型分为成功和出错两种,用来帮助快速处理STUN信息。Message Type字段又可以进一步分解为如下结构:

基于TCP/UDP的P2P网络通信协议研究与实现

图4-2 Message Type字段

其中显示的位为从最高有效位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包的一个方法之一。

4.1.3 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值都会导致客户端认为此次传输失败。

 

 

 

4.2 TURN技术

4.2.1 TURN技术原理

TURN的全称为Traversal Using Relays around NAT,是STUN/RFC5389的一个拓展,主要添加了Relay功能。如果终端在NAT之后,那么在特定的情景下,有可能使得终端无法和其对等端(peer)进行直接的通信,这时就需要公网的服务器作为一个中继,
对来往的数据进行转发。这个转发的协议就被定义为TURN。TURN和其他中继协议的不同之处在于,它允许客户端使用同一个中继地址(relay address)与多个不同的peer进行通信。

使用TURN协议的客户端必须能够通过中继地址和对等端进行通讯,并且能够得知每个peer的的IP地址和端口(确切地说,应该是peer的服务器反射地址)。
而这些行为如何完成,是不在TURN协议范围之内的。

如果TURN使用于ICE协议中,relay地址会作为一个候选,由ICE在多个候选中进行评估,选取最合适的通讯地址。一般来说中继的优先级都是最低的。
TURN协议被设计为ICE协议(Interactive Connectivity Establishment)的一部分,而且也强烈建议用户在他们的程序里使用ICE,但是也可以独立于ICE的运行。
值得一提的是,TURN协议本身是STUN的一个拓展,因此绝大部分TURN报文都是STUN类型的,作为STUN的一个拓展,TURN增加了新的方法(method)和属性(attribute)。

4.2.2 TURN传输过程

在协议中,TURN服务器与peer之间的连接都是基于UDP的,但是服务器和客户端之间可以通过其他各种连接来传输STUN报文,比如TCP/UDP/TLS-over-TCP. 客户端之间通过中继传输数据时候,如果用了TCP,也会在服务端转换为UDP,因此建议客户端使用
UDP来进行传输. 至于为什么要支持TCP,那是因为一部分防火墙会完全阻挡UDP数据,而对于三次握手的TCP数据则不做隔离。

1、分配(Allocations)

要在服务器端获得一个中继分配,客户端须使用分配事务. 客户端发送分配请求(Allocate request)到服务器,然后服务器返回分配成功响应,并包含了分配的地址.客户端可以在属性字段描述其想要的分配类型(比如生命周期).由于中继数据实现了安全传输,服务器会要求对客户端进行验证,主要使用STUN的long-term credential mechanism。

一旦中继传输地址分配好,客户端必须要将其保活.通常的方法是发送刷新请求(Refresh request)到服务端.这在TURN中是一个标准的方法.刷新频率取决于分配的生命期,默认为10分钟.客户端也可以在刷新请求里指定一个更长的生命期,
而服务器会返回一个实际上分配的时间. 当客户端想中指通信时,可以发送一个生命期为0的刷新请求.

服务器和客户端都保存有一个成为五元组(5-TUPLE)的信息,比如对于客户端来说,五元组包括客户端本地地址/端口,服务器地址/端口,和传输协议;服务器也是类似,只不过将客户端的地址变为其反射地址,因为那才是服务器所见到的. 服务器和客户端在分配请求中都带有5-TUPLE信息,并且也在接下来的信息传输中使用,因此彼此都知道哪一次分配对应哪一次传输。

基于TCP/UDP的P2P网络通信协议研究与实现

图4-3 传输过程

如上图所示,客户端首先发送Allocate请求,但是没带验证信息,因此STUN服务器会返回error response,客户端收到错误后加上
所需的验证信息再次请求,才能进行成功的分配.

2、发送机制(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支持两种方式来创建许可,一种是发送CreatePermission request

3、信道机制(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不同的是,并没有直接删除绑定的方法,只能等待其超时自动失效。

基于TCP/UDP的P2P网络通信协议研究与实现

图4-4 信道号

上图中0x4001为信道号,即ChannelData message的头部中头2字节,值得一提的是信道号的选取有如下要求:

0x0000-0x3FFF : 这一段的值不能用来作为信道号。

0x4000-0x7FFF : 这一段是可以作为信道号的值,一共有16383种不同值在目前来看是足够用的。

0x8000-0xFFFF : 这一段是保留值,留给以后使用。

 

 

 

 

 

 

 

 

 

 

 

4.3 ICE技术

4.3.1 ICE技术原理

ICE的全称为Interactive Connectivity Establishment,即交互式连接建立。ICE是一个用于在offer/answer模式下的NAT传输协议,主要用于UDP下多媒体会话的建立,其使用了STUN协议以及TURN协议,同时也能被其他实现了offer/answer模型的的其他程序所使用,比如SIP(Session Initiation Protocol).

使用offer/answer模型(RFC3264)的协议通常很难在NAT之间穿透,因为其目的一般是建立多媒体数据流,而且在报文中还携带了数据的源IP和端口信息,这在通过NAT时是有问题的.RFC3264还尝试在客户端之间建立直接的通路,因此中间就缺少
了应用层的封装.这样设计是为了减少媒体数据延迟,减少丢包率以及减少程序部署的负担.然而这一切都很难通过NAT而完成。

有很多解决方案可以使得这些协议运行于NAT环境之中,包括应用层网关(ALGs),Classic STUN以及Realm Specific IP+SDP协同工作等方法。不幸的是,这些技术都是在某些网络拓扑下工作很好,而在另一些环境下表现又很差,因此我们需要一个单一的,可*定制的解决方案,以便能在所有环境中都能较好工作。

4.3.2 ICE的工作流程

一个典型的ICE工作环境如下,

基于TCP/UDP的P2P网络通信协议研究与实现

图4-5 ICE工作环境

有两个端点L和R,都运行在各自的NAT之后(他们自己也许并不知道),NAT的类型和性质也是未知的。L和R通过交换SDP信息在彼此之间建立多媒体会话,通常交换通过一个SIP服务器完成。      

ICE的基本思路是,每个终端都有一系列传输地址(包括传输协议,IP地址和端口)的候选,可以用来和其他端点进行通信.。其中可能包括:

直接和网络接口联系的传输地址(host address)

经过NAT转换的传输地址,即反射地址(server reflective address)

TURN服务器分配的中继地址(relay address)

虽然潜在要求任意一个L的候选地址都能用来和R的候选地址进行通信.但是实际中发现有许多组合是无法工作的.举例来说,如果L和R都在NAT之后而且不处于同一内网,他们的直接地址就无法进行通信.ICE的目的就是为了发现哪一对候选地址的组合可以工作,并且通过系统的方法对所有组合进行测试(用一种精心挑选的顺序).

为了执行ICE,客户端必须要识别出其所有的地址候选,ICE中定义了三种候选类型,有些是从物理地址或者逻辑网络接口继承而来,其他则是从STUN或者TURN服务器发现的.很自然,一个可用的地址为和本地网络接口直接联系的地址,通常是内网地址,称为HOST CANDIDATE,如果客户端有多个网络接口,比如既连接了WiFi又插着网线,那么就可能有多个内网地址候选.

其次,客户端通过STUN或者TURN来获得更多的候选传输地址,即SERVER REFLEXIVE CANDIDATES和RELAYED CANDIDATES,如果TURN服务器是标准化的,那么两种地址都可以通过TURN服务器获得.当L获得所有的自己的候选地址之后,会将其按优先级排序,然后通过signaling通道发送到R.候选地址被存储在SDP offer报文的属性部分.当R接收到offer之后,就会进行同样的获选地址收集过程,并返回给L。

这一步骤之后,两个对等端都拥有了若干自己和对方的候选地址,并将其配对,组成CANDIDATE PAIRS.为了查看哪对组合可以工作,每个终端都进行一系列的检查.每个检查都是一次STUN request/response传输,将request从候选地址对的本地地址发送到远端地址. 连接性检查的基本原则很简单:

以一定的优先级将候选地址对进行排序.

以该优先级顺序发送checks请求

从其他终端接收到checks的确认信息

两端连接性测试,结果是一个4次握手过程:

基于TCP/UDP的P2P网络通信协议研究与实现

图4-6 ICE 四次握手过程

值的一提的是,STUN request的发送和接收地址都是接下来进多媒体传输(如RTP和RTCP)的地址和端口,所以,客户端实际上是将STUN协议与RTP/RTCP协议在数据包中进行复用(而不是在端口上复用)。

由于STUN Binding request用来进行连接性测试,因此STUN Binding response中会包含终端的实际地址,如果这个地址和之前学习的所有地址都不匹配,发送方就会生成一个新的candidate,称为PEER REFLEXIVE CANDIDATE,和其他candidate一样,也要通过ICE的检查测试.

连接性检查(Connectivity Checks)

所有的ICE实现都要求与STUN(RFC5389)兼容,并且废弃Classic STUN(RFC3489).ICE的完整实现既生成checks(作为STUN client),
也接收checks(作为STUN server),而lite实现则只负责接收checks.这里只介绍完整实现情况下的检查过程.

1. 为中继候选地址生成许可(Permissions).

2. 从本地候选往远端候选发送Binding Request.

在Binding请求中通常需要包含一些特殊的属性,以在ICE进行连接性检查的时候提供必要信息。

PRIORITY 和 USE-CANDIDATE:

终端必须在其request中包含PRIORITY属性,指明其优先级,优先级由公式计算而得。如果有需要也可以给出特别指定的候选(即USE-CANDIDATE属性)。

ICE-CONTROLLED和ICE-CONTROLLING:

在每次会话中,每个终端都有一个身份,有两种身份,即受控方(controlled role)和主控方(controlling role)。主控方负责选择最终用来通讯的候选地址对,受控方被告知哪个候选地址对用来进行哪次媒体流传输,并且不生成更新过的offer来提示此次告知.发起ICE处理进程(即生成offer)的一方必须是主控方,而另一方则是受控方。如果终端是受控方,那么在request中就必须加上ICE-CONTROLLED属性,同样,如果终端是主控方,就需要ICE-CONTROLLING属性.

生成Credential

作为连接性检查的Binding Request必须使用STUN的短期身份验证.验证的用户名被格式化为一系列username段的联结,包含了发送请求的所有对等端的用户名,以冒号隔开;密码就是对等端的密码.

3. 处理Response

当收到Binding Response时,终端会将其与Binding Request相联系,通常通过事务ID.随后将会将此事务ID与候选地址对进行绑定.

失败响应

如果STUN传输返回487(Role Conflict)错误响应,终端首先会检查其是否包含了ICE-CONTROLLED或ICE-CONTROLLING属性.如果有ICE-CONTROLLED,终端必须切换为controlling role;如果请求包含ICE-CONTROLLING属性,则必须切换为controlled role.切换好之后,终端必须使产生487错误的候选地址对进入检查队列中,并将此地址对的状态设置为Waiting.

成功响应,一次连接检查在满足下列所有情况时候就被认为成功:

STUN传输产生一个Success Response

response的源IP和端口等于Binding Request的目的IP和端口

response的目的IP和端口等于Binding Request的源IP和端口

终端收到成功响应之后,先检查其mapped address是否与本地记录的地址对有匹配,如果没有则生成一个新的候选地址。即对等端的反射地址.如果有匹配,则终端会构造一个可用候选地址对(valid pair).通常很可能地址对不存在于任何检查列表中,检索检查列表中没有被服务器反射的本地地址,这些地址把它们的本地候选转换成服务器反射地址的基地址,并把冗余的地址去除掉。