NAT - P2P通信技术

P2P通信技术

根据客户端的不同,客户端之间进行P2P传输的方法也略有不同,这里介绍了现有的穿越中间件进行P2P通信的几种技术.

中继(Relaying)

这是最可靠但也是最低效的一种P2P通信实现. 其原理是通过一个有公网IP的服务器中间人对两个内网客户端的通信数据进行中继和转发. 如下图所示:

                       Server S
                          |
                          |
   +----------------------+----------------------+
   |                                             |
 NAT A                                         NAT B
   |                                             |
   |                                             |
Client A                                      Client B

客户端A和客户端B不直接通信,而是先都与服务端S建立链接,然后再通过S和对方建立的通路来中继传递的数据. 这钟方法的缺陷很明显, 当链接的客户端变多之后,会显著增加服务器的负担,完全没体现出P2P的优势. 但这种方法的好处是能保证成功,因此在实践中也常作为一种备选方案.

逆向链接(Connection reversal)

第二种方法在当两个端点中有一个不存在中间件的时候有效. 例如,客户端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 

客户端A内网地址为10.0.0.1,且应用程序正在使用TCP端口1234. A和服务器S建立了一个链接,服务器的IP地址为18.181.0.31,监听1235端口. NAT A给客户端A分配了TCP端口62000,地址为NAT的公网IP地址155.99.25.11, 作为客户端A对外当前会话的临时IP和端口. 因此S认为客户端A就是155.99.25.11:62000. 而B由于有公网地址,所以对S来说B就是138.76.29.7:1234.

当客户端B想要发起一个对客户端A的P2P链接时,要么链接A的外网地址155.99.25.11:62000,要么链接A的内网地址10.0.0.1:1234,然而两种方式链接都会失败. 链接10.0.0.1:1234失败自不用说,为什么链接155.99.25.11:62000也会失败呢?来自B的TCP SYN握手请求到达NAT A的时候会被拒绝,因为对NAT A来说只有外出的链接才是允许的. 在直接链接A失败之后,B可以通过S向A中继一个链接请求,从而从A方向“逆向“地建立起A-B之间的点对点链接.

很多当前的P2P系统都实现了这种技术,但其局限性也是很明显的,只有当其中一方有公网IP时链接才能建立. 越来越多的情况下, 通信的双方都在NAT之后,因此就要用到我们下面介绍的第三种技术了.

UDP打洞(UDP hole punching)

第三种P2P通信技术,被广泛采用的,名为“P2P打洞“. P2P打洞技术依赖于通常防火墙和Cone NAT允许正当的P2P应用程序在中间件中打洞且与对方建立直接链接的特性. 下面主要考虑两种常见的场景,以及应用程序如何设计去完美地处理这些情况. 第一种场景代表了大多数情况,即两个需要直接链接的客户端处在两个不同的NAT之后; 第二种场景是两个客户端在同一个NAT之后,但客户端自己可能并不知道(比如同一ISP下面的不同子网).

端点在不同的NAT之后

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

                            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

现在假设客户端A打算与客户端B直接建立一个UDP通信会话. 如果A直接给B的公网地址138.76.29.7:31000发送UDP数据,NAT B将很可能会无视进入的 数据(除非是Full Cone NAT),因为源地址和端口与S不匹配,而最初只与S建立过会话. B往A直接发信息也类似.

假设A开始给B的公网地址发送UDP数据的同时,给服务器S发送一个中继请求,要求B开始给A的公网地址发送UDP信息. A往B的输出信息会导致NAT A打开 一个A的内网地址与与B的外网地址之间的新通讯会话,B往A亦然. 一旦新的UDP会话在两个方向都打开之后,客户端A和客户端B就能直接通讯, 而无须再通过引导服务器S了.

UDP打洞技术有许多有用的性质. 一旦一个的P2P链接建立,链接的双方都能反过来作为“引导服务器”来帮助其他中间件后的客户端进行打洞, 极大减少了服务器的负载. 应用程序不需要知道中间件具体是什么(如果有的话),因为以上的过程在没有中间件或者有多个中间件的情况下 也一样能建立通信链路.

端点在相同的NAT之后

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

假设A和B使用了上节介绍的UDP打洞技术来建立P2P通路,那么会发生什么呢?首先A和B会得到由S观测到的对方的公网IP和端口号,然后给对方的地址发送信息. 两个客户端只有在NAT允许内网主机对内网其他主机发起UDP会话的时候才能正常通信,我们把这种情况称之为"回环传输“(loopback transmission),因为从内部 到达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内网网段中无关的结点上去的.

端点在多级NAT之后

在一些拓朴结构中,可能会存在多级NAT设备,在这种情况下,如果没有关于拓朴的具体信息, 两个Peer要建立“最优”的P2P链接是不可能的,下面来说为什么. 以下图为例:

                            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

假设NAT X是一个网络提供商ISP部署的工业级NAT,其下子网共用一个公网地址155.99.25.11,NAT A和NAT B分别是其下不同用户的网关部署的NAT. 只有服务器S 和NAT X有全局的路由地址. Client A在NAT A的子网中,同时Client B在NAT B的子网中,每经过一级NAT都要进行一次网络地址转换.

现在假设A和B打算建立直接P2P链接,用一般的方法(通过Server S来打洞)自然是没问题的,那能不能优化呢?一种想当然的优化办法是A直接把信息发送给NAT B的 内网地址192.168.1.2:31000,且B通过NAT B把信息发送给A的路由地址192.168.1.1:30000,不幸的是,A和B都没有办法得知这两个目的地址,因为S只看见了客户端 ‵全局‵地址155.99.25.11. 退一步说,即便A和B通过某种方法得知了那些地址,我们也无法保证他们是可用的. 因为ISP分配的子网地址可能和NAT A B分配的子网地址 域相冲突. 因此客户端没有其他选择,只能使用S来进行打洞并进行回环传输.

固定端口绑定

UDP打洞技术有一个主要的条件:只有当两个NAT都是Cone NAT(或者非NAT的防火墙)时才能工作. 因为其维持了一个给定的(内网IP,内网UDP)二元组 和(公网IP, 公网UDP)二元组固定的端口绑定,只要该UDP端口还在使用中,就不会变化. 如果像对称NAT一样,给每个新会话分配一个新的公网端口,就 会导致UDP应用程序无法使用跟外部端点已经打通了的通信链路. 由于Cone NAT是当今最广泛使用的,尽管有一小部分的对称NAT是不支持打洞的,UDP打洞 技术也还是被广泛采纳应用.

具体实现

一般的网络编程,都是客户端比服务端要难,因为要处理与服务器的通信同时还要处理来自用户的事件;对于P2P客户端来说更是如此,因为P2P客户端不止作 为客户端,同时也作为对等连接的服务器端. 这里的大体思路是,输入命令传输给服务器之后,接收来自服务器的反馈,并执行相应代码. 例如A想要与B建立 通信链路,先给服务器发送punch命令以及给B发送数据,服务器接到命令后给B发送punch_requst信息以及A的端点信息,B收到之后向A发送数据打通通路,然 后A与B就可以进行P2P通信了. 经测试,打通通路后即便把服务器关闭,A与B也能正常通信.

TCP打洞(TCP Hole Punching)

关于TCP打洞,有一点需要提的是,因为TCP是基于连接的,所以任何未经连接而发送的数据都会被丢弃,这导致在recv的时候是无法直接从peer端读取数据. 其实这对UDP也一样,如果对UDP的socket进行了connect,其也会忽略连接之外的数据,详见connect(2).

所以,如果我们要进行TCP打洞,通常需要重用本地的endpoint来发起新的TCP连接,这样才能将已经打开的NAT利用起来. 具体来说,则是要设置socket的 SO_REUSEADDRSO_REUSEPORT属性,根据系统不同,其实现也不尽一致. 一般来说,TCP打洞的步骤如下:

  • A 发送 SYN 到 B(出口地址,下同),从而创建NAT A的一组映射
  • B 发送 SYN 到 A, 创建NAT B的一组映射
  • 根据时序不同,两个SYN中有一个会被对方的NAT丢弃,另一个成功通过NAT
  • 通过NAT的SYN报文被其中一方收到,即返回SYNACK, 完成握手
  • 至此,TCP的打洞成功,获得一个不依赖于服务器的链接

由于TCP连接是由操作系统控制的, 而不是由应用控制的, 而且TCP包的序列号是随机生成, 所以TCP打洞的成功率就相对较低. 因此如果NAT对接收到的包进行TCP序列号检测时若没有现有的连接可以对应, 该TCP包很可能会被NAT丢弃掉.

posted @ 2021-01-26 09:35  非法关键字  阅读(218)  评论(0编辑  收藏  举报