p2p小结(四):TCP打洞

NAT只允许其内部的主机主动连接到外部主机,外部发起的连接请求会被NAT拒绝掉。这也就是说,若想跟一个处于NAT内部的主机建立连接,就算知道了NAT为该主机分配的共有IP和端口,也只能让这个NAT内的主机主动去连你才行。然而,若你本身也处于一个NAT之后的话,问题就变得更加复杂。

NAT的这种特性为p2p带来的困扰就在于:“一个peer无法自由地与另一个peer建立连接”,这是与p2p的思想相违背的。必须要实现一种机制来应对这个问题,让peer之间的互联变得没有障碍,仿佛NAT不存在一样,这就是所谓的“NAT穿越”。

(注:上面提到的“连接”是一个泛指,UDP和TCP通讯都统称为“连接”,TCP的连接成功,就是TCP定义的连接成功,UDP的连接成功,指的是发出UDP包后收到了对方的返回)。

上篇已对NAT穿越技术做了概述,其中与p2p相关的是Hole punching,即:“打洞”。这里总结基于TCP的打洞过程。

努力的大方向就是要让NAT中产生session,一个session就像一个“隧道”或者一个“洞”,在NAT上“凿”出这个“洞”,让NAT以外的数据包可以进来。

要完成“打洞”这个操作,仅凭借peer本身显然是无法完成的,这些peer都身处p2p网络之中,根本不知道其他peer的任何信息,不可能做进一步的动作。需要一个有全局视野的服务程序来进行协调。

假设一个场景:peer A、B和服务S共同组成了一个p2p网络。A、B启动后,就向S进行注册(上报自己的私有地址,端口号等信息),S会分配给该peer一个ID,这个ID就是peer在该p2p网络中的唯一标识。A、B上线(成功注册)后,A主动向S提出对B的连接申请,S接到请求后,协助A、B完成连接。 此图以及后面出现的图中的实心圆点均代表端口,云形图是对网络拓扑的抽象,在不需要关注NAT的时候,也会用一个云形图直接将其略过。

根据实际的NAT类型及网络拓扑情况,需要讨论多种情形。NAT的类型可以在软件层面进行探测,而网络拓扑不得而知,不过,经实践发现,要完成“打洞”任务并不需要知道精确的拓扑信息。

为方便讨论问题,我们假设A、B所在的NAT的类型以及网络拓扑情况都是已知的,至于如何知道,下篇进行说明。

A、B处于同一私有网络中

这种情况容易处理,可以忽略NAT的存在,不存在“打洞”的问题。A直接向B的(私网IP:私网端口)发起连接即可:

A、B不在同一私有网络中,此时要对多种情况进行讨论

  1. B暴露在公网,没有NAT设备

    此时,不用考虑A所在的NAT(图中假设A的NAT为Cone类型)的情况,A直接向B的(公网IP:公网端口)发起连接即可。当由A发出的SYN报文段走出NAT时,一个从A到B的“洞”就打通了:

  2. B处于Full Cone类型的NAT

    Full Cone型的NAT,其特点就是不考察外进数据包的来源(源IP,源端口号)。可简单理解为:打出一个“洞”谁都可以进。这么说来就很容易处理,因为B已与服务器S建立了一个TCP连接,即:B所在的NAT中已经开好了一个“洞”,虽然这个“洞”是从B朝向S的,但因其为Full Cone型,不是源自S的数据包也可顺利进入。A直接向B的(公网IP:公网端口)发起连接即可:

  3. B处于有限制的Cone NAT后

    此时B所在NAT上的“洞”就不再允许任意数据进入了,它会根据源IP地址(带地址限制的Cone NAT)或源IP地址和源端口号(带端口限制的Cone NAT)对到来的数据包进行过滤。

    • <1> A直接放在公网,没有NAT

      此种情况,A不动,B去连A即可:

    • <2> A所在NAT的类型为Full Cone

      此时,也是A不动,B去连A:

    • <3> A所在NAT的类型也为有限制的Cone NAT

      此时,A,B要互相向朝着对方的方向打洞,只有这样做,从A发向B的TCP报文段才可通过NAT-B最终发给B,同时B发向A的ACK报文段也可通过NAT-A最终发给A。这是A主动向B发报文的情况,B主动向A发报文也是一样的。

      A、B均向对方发送SYN报文段,此时TCP的行为依赖于操作系统的实现。不过,总有一端connect成功,另一端accept成功。一次就连接成功是理想情况,通常状况是NAT的洞还没打开,对方的SYN就来了,结果被丢弃。因此,要多次尝试connect,设定一个超时时间或尝试次数来决定是否结束尝试。

    • <4> A所在的NAT的类型为Symmetric NAT

      Symmetric NAT的特点是关注从内部出去的包的目的IP,一旦目的IP变了,它就会产生新的session,对于从外部过来的包,它会针对源IP进行过滤。

      这种情况下的操作与上面的情况类似,A、B互相朝着对方打洞。A朝B方向没什么特别,B朝A方向则有所不同。由于Symmetric的特性,A向B发出SYN报文段的时候,会产生一个全新的session,它不会使用A连接S时产生的那个session,因为目标IP变了,不是S的IP地址。

      产生一个新的session也没什么大不了的,重点是NAT-A所分配的那个新的公有端口号谁也不知道是多少。用程序去做的话,还想不到可以取到这个端口号的办法。

      不知道A的新公网端口号,B如何进行connect,不能connect何谈“打洞”?不过多数NAT分配端口号的策略是依次增一,这个新分配的端口号一定是在A连S成功之后。假设A连S时分配的公有端口号为n,则可以n+1为起点依次增一逐个尝试。如果试不出来,就以n-1为起点,依次减一逐个尝试。

  4. B处于Symmetric类型的NAT之后

    • <1> A直接放在公网,没有NAT

      A不动,B去连A即可:

    • <2> A所在NAT的类型为Full Cone

      A不动,B直接去连A:

    • <3> A所在NAT的类型为有限制的Cone NAT

      A,B要互相向朝着对方的方向打洞。特别的地方上面已提到过,Symmetric会新创建一个session,新分配的那个公有端口号不得而知。A向B方向打洞时,要猜测B的公有端口号,测试方法上面已给出。

    • <4> A所在的NAT的类型也为Symmetric NAT

      A,B要互相向朝着对方的方向打洞。由于双方的NAT都是Symmetric类型,双方都要猜测对方的公有端口号:

以上的讨论,都是单一的NAT设备,如果在当前NAT之外还存在一个更高级别的NAT设备该怎么办?如果更高级别的NAT之外还有NAT呢?

除图中画出的NAT之外,云形图所代表的网络拓扑中也可能存在NAT设备。

对S来说,无论有多少层NAT存在,其所能感知到的只有最外层的那个,其余均是透明的存在。各层次之间依照NAT的规则来处理进出的数据包,这个不需要我们的程序来干预,也没法干预。


Prev-p2p小结(三):NAT穿越
Next-p2p小结(五):探测NAT类型