5. NAT穿透
5.1 转发
最可靠但又是最低效的点对点通信方法,莫过于将p2p网络通信看作一个C/S结构,通过服务器来转发信息.如下图,两个客户端A和B,均与服务器S初始化了一个TCP或UDP连接,服务器S具有公网固定IP地址,两个客户端分布在不同的私网中,这样,他们各自的NAT代理服务器将不允许他们进行直连.
Server S
|
|
+----------------------+----------------------+
| |
NAT A NAT B
| |
| |
Client A Client B
取而代之的方式是,两个客户端可以把服务器S当作信使来转发消息.比如,为了将消息发送到B,A先发送一条信息给服务器S,服务器S再利用初始化时已经建立的连接,将信息转发给B.
这个方法的优势是:它适合于任何NAT包括Symmetric NAT.但是它的劣势也很明显:它将全面依赖并消耗服务器的资源和网络带宽.名为 TURN 的协议定义了一个利用转发技术进行可靠通信的模型.
5.2 反向连接
这里介绍第二种技术,但是它只能在通信的两端只有一端处于NAT之后的情况下.举例来说,假设客户端A处于NAT之后,而客户端B有一个公网IP地址,如下图所示.
Server S
18.181.0.31:1235
|
|
+----------------------+----------------------+
| |
NAT A |
155.99.25.11:62000 |
| |
| |
Client A Client B
10.0.0.1:1234 138.76.29.7:1234
现在我们假设客户端B将会与客户端A初始化一个端对端连接会话.B将首先试图连接A的一个地址---客户端A认为是它自己的地址10.0.0.1:1234或者是从服务器S观察到的地址155.99.25.11:62000.然而不论是连接哪一个,都不可能成功.第一种情况:试图直接连到10.0.0.1肯定会失败,因为10.0.0.1根本就不是一个可以在公网上路由的IP地址;第二种情况,从B传来的请求将能够到达端口NAT A的端口62000,但NAT A却会拒绝这个连接请求,因为只有外出的连接才允许进入. 在所有的尝试都失败之后,客户端B就只能通过服务器S来请求A做一个"反向"连接到客户端B,客户端A将打开一个与客户端B通讯的连接(在B的公网IP地址和端口号上).NAT
A允许这个连接通过,因为这个连接起源于NAT A的内部,并且同时客户端B能够受这个连接因为B并不位于NAT之后.
这个方法的优势是:它也适合于任何NAT包括Symmetric NAT.它的主要限制在于,只能有一端位于NAT之后.
5.3 UDP打洞
第三种技术,也是这篇文章主要要介绍的,就是非常有名的"UDP打洞技术".这里将考虑两种典型场景,来介绍连接的双方应用程序如何按照计划的进行通信的,第一种场景,我们假设两个客户端都处于不同的NAT之后;第二种场景,我们假设两个客户端处于同一个NAT之后,但是它们彼此都不知道(他们在同一个NAT中).
5.3.1 处于不同NAT之后的客户端通信
我们假设 Client A和Client B都拥有自己的私有IP地址,并且都处在不同的NAT之后,端对端的程序运行于 CLIENT A,CLIENT B,S之间,并且它们都开放了UDP端口1234. CLIENT A和CLIENT B首先分别与S建立通信会话,这时NAT A把它自己的UDP端口62000分配给CLIENT A与S的会话,NAT B也把自己的UDP端口31000分配给CLIENT B与S的会话.如下图所示:
Server S
18.181.0.31:1234
|
|
+----------------------+----------------------+
| |
NAT A NAT B
155.99.25.11:62000 138.76.29.7:31000
| |
| |
Client A Client B
10.0.0.1:1234 10.1.1.3:1234
假如这个时候 CLIENT A 想与 CLIENT B建立一条UDP通信直连,如果 CLIENT A只是简单的发送一个UDP信息到CLIENT B的公网地址
138.76.29.7:31000的话,NAT B会不加考虑的将这个信息丢弃(除非NAT B是一个 full cone NAT),因为这个UDP信息中所包含的地址信息,与CLIENT B和服务器S建立连接时存储在NAT B中的服务器S的地址信息不符.同样的,CLIENT B如果做同样的事情,发送的UDP信息也会被NAT A丢弃.
假如 CLIENT A 开始发送一个UDP信息到CLIENT B的公网地址上,与此同时,他又通过S中转发送了一个邀请信息给CLIENT B,请求CLIENT B也给CLIENT A发送一个UDP信息到 CLIENT A的公网地址上.这时CLIENT A向CLIENT B的公网IP(138.76.29.7:31000)发送的信息导致 NAT A 打开一个处于CLIENT A的私有地址和CLIENT B的公网地址之间的新的通信会话,与此同时NAT B也打开了一个处于CLIENT B的私有地址和CLIENT A的公网地址(155.99.25.11:62000)之间的新的通信会话.一旦这个新的UDP会话各自向对方打开了,CLIENT
A和CLIENT B之间就可以直接通信,而无需S来牵线搭桥了.这就是所谓的打洞技术.
一旦这种处于NAT之后的端对端的直连建立之后,连接的双方可以轮流担任对方的"媒人",把对方介绍给其他的客户端,这样就极大的降低了服务器S的工作量.
5.3.2 处于相同NAT之后的客户端通信
我们假设 Client A和Client B都拥有自己的私有IP地址,并且都处在相同的NAT之后,端对端的程序运行于 CLIENT A,CLIENT B,S之间,
CLIENT A和CLIENT B分别与S建立通信会话,经过NAT转换后,A的公网端口被映射为62000,B的公网端口映射为62001.如下图所示:
Server S
18.181.0.31:1234
|
|
NAT
A-S 155.99.25.11:62000
B-S 155.99.25.11:62001
|
+----------------------+----------------------+
| |
Client A Client B
10.0.0.1:1234 10.1.1.3:1234
根据前面介绍的"打洞"技术,CLIENT A将发送一个UDP信息到CLIENT B的公网地址上,数据包源端为(10.0.0.1:124),目的端为(155.99.25.11:62001).该数据包能否被B收到,取决于当前的NAT是否支持"发夹"转换(hairpin转换,也就是同一台设备不同端口之间的UDP数据包能否到达).
首先,支持"发夹"转换的NAT设备还远没有支持"打洞"技术的NAT设备多,其次,即使NAT设备支持"发夹"转换,在这种情况下也应该通过网内端到端实现,而不是将数据包无谓 地经过NAT设备,这是一种对资源的浪费.
5.3.3 一般"打洞"过程
综合上面介绍的客户端处于不同NAT之后和处于同一NAT之后,我们说下一般的"打洞"过程.
1. 打洞技术假定客户端A和B可以与公网内的已知的集中服务器建立UDP连接(可以互发UDP数据包).当一个客户端在S上登陆的时候,服务器记录下该客户端的两个endpoints(IP地址,UDP端口),一个是该客户端确信自己是通过该ip和端口与服务器S进行通信的,另一个是服务器S记录下的由服务器"观察"到的该客户端实际与自己通信所使用的ip和端口.我们可以把前一个endpoint看作是客户端的内网ip和端口,把后一个endpoint看作是客户端的内网ip和端口经过NAT转换后的公网ip和端口.服务器可以从客户端的登陆消息的消息体中得到该客户端的内网endpoint相关信息,可以通过对登陆消息的IP或UDP头得到该客户端的公网endpoint.
2.假设Client A想向B发起连接,于是A向服务器S发送消息,请求S帮助建立与B的UDP连接.这时,S将B的公网和内网的endpoint发给A.可知,A与B通过与S的一次通信就可以知道对方的公网和内网的endpoint.
3.Client A通过B的内网endpoint发送UDP数据包.针对5.3.2节问题的解决方案.如果B和A在同一NAT后,则很快收到响应.如果B和A不在同一NAT后,则超时.
4.Client A通过B的外网endpoint发送UDP数据包.
回到5.3.1节介绍的具体方法.CLIENT A发出UDP包(10.0.0.1:1234,138.76.29.7:31000),经NAT A转换为(155.99.25.11:62000,138.76.29.7:31000),经NAT B转换为(155.99.25.11:62000,10.1.1.3:1234).如果在此数据包到达NAT B前,B发送过UDP包到A的公网endpoint,则NAT B允许此包到达B机.在5.3.2节下,A发出UDP包(10.0.0.1:1234,155.99.25.11:62001),NAT
先转换为(155.99.25.11:62000,155.99.25.11:62001),再转换为(155.99.25.11:62000,10.1.1.3:1234).
5.步骤3,4发送的数据包是为了"打洞",打洞成功后,就进入真正的P2P传输了.
还有一种情况,B和A不在同一NAT后,C和A在同一NAT后,且B和C的内网endpoint一致,这个时候A从S拿到的目的端应该有2个.所以针对3,4步骤取先有回应的目的端不可取,应该先做步骤3,有回应直接到步骤5,没有回应到步骤4.
5.3.4 客户端分别处于多层NAT之后
在有些网络拓扑中就存在多层NAT设备,让我们来看看下图这种情况:
假如 NAT X 是由 Internet服务供应商(ISP)配置的一个大型NAT,它使用少量的公网IP地址来为一些客户群提供服务,NAT A和NAT B则是
为ISP的两个客户群所配置的小一点的独立NAT网关,它们为各自客户群的私人家庭网络提供IP地址.只有Server S和NAT X拥有公网固定IP地址,而NAT A 和 NAT B所拥有的"公网"IP地址对于ISP的寻址域来说则实际上"私有"的.
Server S
18.181.0.31:1234
|
|
NAT X
A-S 155.99.25.11:62000
B-S 155.99.25.11:62001
|
|
+----------------------+----------------------+
| |
NAT A NAT B
192.168.1.1:30000 192.168.1.2:31000
| |
| |
Client A Client B
10.0.0.1:1234 10.1.1.3:1234
现在让我们假设Client A和Client B想要建立一条端对端 的UDP直连.Client A和 Client B只知道Server S记录的他们真正的公网地址
155.99.25.11:62000和155.99.25.11:62001,而且他们只能通过这个公网地址建立连接,即NAT X必须得支持"loopback translation"(也称hairpin转换)才行.
Client A和 Client B也知道对方内网地址10.0.0.1:1234和10.1.1.3:1234,毫无疑问,通过内网地址是建立不了连接的.Client A和 Client B并不知道对方NAT B和NAT A的地址192.168.1.2:31000和192.168.1.1:30000,即便假设我们通过某种途径得知了这些地址,还是不能够保证这样就能进行通话了,因为这些地址是由ISP的私有寻址域分配的,可能会与私有域所分配的其他无关客户端地址相冲突.
5.3.5 UDP在空闲状态下的超时问题
由于UDP转换协议提供的"洞"不是绝对可靠的,多数NAT设备内部都有一个UDP转换的空闲状态计时器,如果在一段时间内没有UDP数据通信,NAT设备会关掉由"打洞"操作打出来的"洞",做为应用程序来讲如果想要做到与设备无关,就最好在穿越NAT的以后设定一个穿越的有效期.很遗憾目前没有标准有效期,这个有效期与NAT设备内部的配置有关,最短的只有20秒左右.在这个有效期内,即使没有p2p数据包需要传输,应用程序为了维持该"洞"可以正常工作,也必须向对方发送"打洞"维持包.这个维持包是需要双方应用都发送的,只有一方发送不会维持另一方的session正常工作.除了频繁发送"打洞"维持包以外,还有一个方法就是在当前的"洞"有效期过期之前,p2p客户端双方重新"打洞",丢弃原有的"洞",这也不失为一个有效的方法.
5.4 . UPD端口号预言
在使用"UDP打洞技术"时有一点必须要注意:它只能在双方的NAT都是cone NAT时才能正常工作.这些NAT在使用时保持着端口的
绑定----[私有IP,私有UDP端口]对和[公网IP,公网UDP端口]对的一一对应.
如果像 symmetricNAT那样给每个新的会话都分配一个新的公网端口,那么UDP应用程序想要与其他外部客户端进行通话,就无法重复使用已经建立好的通信转换.
让我们来考虑这样一种情况,有两个客户端A和B,他们都藏在不同的Symmetric NAT后面,他们都开放了一个UDP连接给具有固定IP的Server S.如下图:
Server S
18.181.0.31:1234
|
|
+----------------------+----------------------+
| |
Symmetric NAT A Symmetric NAT B
A-S 155.99.25.11:62000 B-S 138.76.29.7:31000
| |
| |
Client A Client B
10.0.0.1:1234 10.1.1.3:1234
NAT A 分配了它自己的UDP端口62000,用来保持客户端A与服务器S的通信会话,NAT B 也分配了31000端口,用来保持客户端B与
服务器S的通信会话.通过与服务器S的对话,客户端A和客户端B都相互知道了对方所映射的真实IP和端口.
客户端A发送一条UDP消息到 138.76.29.7:31001(请注意到端口号的增加),同时客户端B发送一条UDP消息到 155.99.25.11:62001.
如果NAT A 和NAT B继续分配端口给新的会话,并且从A-S和B-S的会话时间消耗得并不多的话,那么一条处于客户端A和客户端B之间的双向会话通道就建立了.
客户端A发出的消息送达B导致了NAT A打开了一个新的会话,并且我们希望 NAT A将会指派62001端口给这个新的会话,因为62001是
继62000后,NAT会自动指派给 从服务器S到客户端A之间的新会话的端口号;类似的,客户端B发出的消息送达A导致了NAT B打开了
一个新的会话,并且我们希望 NAT B将会指派31001这个端口给新的会话;如果两个客户端都正确的猜测到了对方新会话被指派的端口号,
那么这个客户端A----客户端B的双向连接就被打通了.其结果如下图所示:
Server S
18.181.0.31:1234
|
|
+----------------------+----------------------+
| |
NAT A NAT B
A-S 155.99.25.11:62000 B-S 138.76.29.7:31000
A-B 155.99.25.11:62001 B-A 138.76.29.7:31001
| |
| |
Client A Client B
10.0.0.1:1234 10.1.1.3:1234
明显的,有许多因素会导致这个方法失败:如果这个预言的新端口(62001和31001) 恰好已经被一个不相关的会话所使用,那么NAT就会跳过这个端口号,这个连接就会宣告失败;如果两个NAT有时或者总是不按照顺序来生成新的端口号,那么这个方法也是行不通的.如果隐藏在NAT A后的一个不同的客户端X(或者在NAT B后)打开了一个新的"外出"UDP 连接,并且无论这个连接的目的如何,只要这个动作发生在客户端A建立了与服务器S的连接之后,客户端A与客户端B建立连接之前,那么这个无关的客户端X 就会趁人不备地"偷"到这个我们渴望分配的端口.所以,这个方法变得如此脆弱而且不堪一击,只要任何一个NAT方包含以上碰到的问题,这个方法都不会奏效.
最后,如果P2P的一方处在两级或者两级以上的NAT下面,并且这些NAT接近这个客户端是symmetric的话,端口号预言是无效的.
因此,并不推荐使用这个方法来写新的P2P应用程序.
5.5 同时开放TCP连接
这里有一种方法能够在某种情况下建立一个穿透NAT的端对端TCP直连.我们知道,绝大多数的TCP会话的建立,都是通过一端先发送一个SYN包开始,另一方则回发一个SYN-ACK包的过程.然而,这里确实存在另外一种情况,就是P2P的双方各自同时地发出一个SYN包到对方的公网地址上,然后各自都单独地返回一个ACK响应来建立一个TCP会话.这个过程被称之为"Simultaneous open"(同时开放连接).
如果一个NAT接收到一个来自私有网络外面的TCP SYN包,这个包想发起一个"引入"的TCP连接,一般来说,NAT会拒绝这个连接请求并扔掉这个SYN 包,或者回送一个TCP RST(connection reset,重建连接)包给请求方.但是,有一种情况,当这个接收到的SYN包中的源IP地址和端口,目标IP地址和端口都与NAT登记的一个已经激活的TCP会话中的地址信息相符时,NAT将会放行这个SYN 包,让它进入NAT内部.特别要指出,如果NAT恰好看到一个刚刚发送出去的一个SYN包也和上面接收到的SYN包中的地址信息相符合的话,那么NAT将会认为这个TCP连接已经被激活,并将允许这个方向的SYN包进入NAT内部.
如果Client A和Client B能够彼此正确的预知对方的NAT将会给下一个TCP连接分配的公网TCP端口,并且两个客户端能够同时地发起一个"外出"的TCP连接,并在对方的SYN包到达之前,自己刚发送出去的SYN包都能顺利的穿过自己的NAT的话,一条端对端的TCP连接就成功地建立了.
不幸的是,这个诡计比5.4节所讲的UDP端口预言更容易被粉碎,并且对时间的敏感性的依赖更多.
这个属于TCP穿透,它同样基于NAT对TCP穿透的支持,基本原理和UDP穿透一致.具体实现后面会继续介绍.