P2P如何实现内网穿透(打洞)

目录

概述

P2P简介

P2P通信技术

中继(Relaying)

逆向连接(Connection reversal)

UDP打洞

端点处于不同NAT

端点处于相同的NAT

固定端口绑定

空闲状态下的超时问题

TCP打洞

套接字和TCP端口的重用

打开P2P的TCP流

TCP同时打开

参考资料


概述

P2P简介

我们知道,内网设备是不能直接访问公网的,如果需要内网设备可以访问公网,需要借助NAT(Net Address Transmit)。如果告诉你,可以不通过公网,可以实现两个不同网络的内网设备间的直接通信,你信不信?通过最基本的认知分析,上述说法好像有问题:不通过公网,怎么可能实现两个内网设备的交互呢?如果有这种方法,那么肯定会在某个方面用到公网。

P2P就是这样的技术,可以使得不同局域网的内网设备可以直连。P2P(peer to peer)点对点技术又称对等互联网络技术,是一种网络新技术,依赖网络中参与者的计算能力和带宽,而不是把依赖都聚集在较少的几台服务器上。P2P网络通常用于通过Ad Hoc连接来连接节点。这类网络可以用于多种用途,各种档案分享软件已经得到了广泛的使用(譬如迅雷、电驴)。P2P技术也被使用在类似VoIP等实时媒体业务的数据通信中。

P2P有个很重要的能力,内网穿透能力,具有这个能力后,不同私网的设备可以直接进行通信。

P2P通信技术

根据客户端的不同,客户端之间进行P2P传输的方法也略有不同,P2P主要有中继、逆向连接、打洞(hole punching)等技术。

P2P打洞技术依赖于通常防火墙和锥形NAT(cone NAT)允许正当的P2P应用程序在中间件中打洞且与对方建立直接连接的特性。

根据传输方式的不同,P2P打洞技术分为TCP打洞及UDP打洞技术。

关于NAT的相关知识见文章《NAT,私网访问公网的利器》

 

中继(Relaying)

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

P2P如何实现内网穿透(打洞)_第1张图片 图 1 中继

        

当服务端连接的客户端比较少,且网络流量不大时,效果还不错。但是如果有很多客户端连接并且网络流量很多,服务端的压力就会很大。

 

逆向连接(Connection reversal)

此种方法在两个端点中有一个不存在中间件(如NAT)的时候有效。例如,Client A在NAT之后而Client B拥有全局IP地址,如图 2所示:

P2P如何实现内网穿透(打洞)_第2张图片 图 2 逆向连接

 

Client A内网地址为10.0.0.1,使用TCP,端口为1234。A和Server S建立了一个连接,Server的IP地址为18.181.0.31,监听1235端口。NAT A给Client A分配了TCP端口62000,地址为NAT的公网IP地址155.99.25.11,作为Client A对外当前会话的临时IP和端口。因此Server S认为Client A就是155.99.25.11:62000。而Client B由于有公网地址,所以对Server S来说Client B就是138.76.29.7:1234。

当Client B想要主动发起对Client A的P2P连接时,需要指定目的地址及端口为155.99.25.11:62000。由于NAT工作的原理问题,NAT A会拒绝将收到的对Client A的请求转发给Client A。拒绝该请求主要有如下原因:

1. NAT A没有映射过62000端口,NAT A不知道该请求是给谁的

2. NAT A映射过62000端口,但是需要首先从Client A发起请求,然后才能转发应答

在直接连接Client A失败之后,Client B可以通过Server S向Client A中继一个连接请求,从而从Client A方向“逆向“地建立起Client A- Client B之间的点对点连接(因为Client A连接到了Server S)。

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

 

UDP打洞

对于UDP打洞,考虑如下的两张场景:

1. 两个Client 处在两个不同的NAT

2. 两个Client 在同一个NAT

端点处于不同NAT

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

P2P如何实现内网穿透(打洞)_第3张图片 图 3端点在不同的NAT之下

假设Client A打算与Client B直接建立一个UDP通信会话。如果Client A直接给Client B的公网地址138.76.29.7:31000发送UDP数据,NAT B很可能会无视进入的数据(除非是Full Cone NAT),Client B往Client A直接发信息也类似。

为了解决上述问题,在Client A开始给Client B的公网地址发送UDP数据的同时,Client A给Server S发送一个中继请求,要求Client B开始给Client A的公网地址发送UDP信息。Client A往Client B的输出信息会导致NAT A打开一个Client A的内网地址与Client B的外网地址之间的通讯会话,Client B往Client A亦然。当两个方向都打开会话之后,Client A和Client B就能直接通讯,而无须再通过Server S了。

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

端点处于相同的NAT

如果Client A和Client B正好在同一个NAT(而且可能他们自己并不知道),Client A和Server S建立了一个UDP会话,NAT为此分配了公网端口62000,Client B同样和Server S建立会话,分配到了端口62001,如图 4所示:

P2P如何实现内网穿透(打洞)_第4张图片 图 4端点在相同的NAT之下

 

假设Client A和Client B使用了上节介绍的UDP打洞技术来建立P2P通路,那么交互流程就是这样了:

1. Client A和Client B得到由Server S观测到的对方的公网IP和端口号,然后给对方的地址发送信息

2. 当Client A发送一个UDP数据包给Client B的公网地址,数据包最初有源IP地址和端口地址10.0.0.1:1234和目的地址155.99.25.11:62001

3. NAT收到包后,将其转换为源155.99.25.11:62000(Client A的公网地址)和目的10.1.1.3:1234,NAT发现目的公网地址和其本身的公网地址相同,此时有两种做法:

4. 如果NAT支持回环传输(NAT允许同一个内网的主机间相互通信),那么直接将数据发给Client B

5. 如果NAT不支持回环传输,有可能将该数据丢弃

因为从内部到达NAT的数据会被“回送”到内网中而不是转发到外网。即便NAT支持回环传输,这种转换和转发在此情况下也是没必要的,且有可能会增加A与B的对话延时和加重NAT的负担。

 

解决上述问题也很简单:

1. 当Client A和Client B最初通过Server S交换地址信息时,它们包含自身的IP地址和端口号(从自己看),同时也包含从服务器看的自己的地址和端口号

2. Client 向对方的公网地址及内网地址同时发生数据,并使用第一个成功通信的地址作为对方地址。如果两个Client 在同一个NAT后,发送到对方内网地址的数据最有可能先到达,从而可以建立一条不经过NAT的通信链路

3. 如果两个Client 在不同的NAT之后,发送给对方内网地址的数据包根本就到达不了对方,但仍然可以通过公网地址来建立通路。

值得一提的是,虽然这些数据包通过某种方式验证,但是在不同NAT的情况下完全有可能会导致Client A往Client B发送的信息发送到其他Client A内网网段中无关的结点上去的。

固定端口绑定

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

空闲状态下的超时问题

由于UDP转换协议提供的“洞”不是绝对可靠的,多数NAT设备内 部都有一个UDP转换的空闲状态计时器,如果在一段时间内没有 UDP数据通信,NAT设备会关掉由“打洞”操作打出来的“洞”作为应用程序来讲如果想要做到与设备无关,就最好在穿越NAT后 设定一个穿越的有效期。这个有效期与NAT设备内部的配置有关,目前没有标准有效期,最短的只有20秒左右。在这个有效期内, 即使没有P2P数据报文需要传输,应用程序为了维持该“洞”可以 正常工作,也必须向对方发送“打洞”维持报文。这个维持报文 是需要双方应用都发送的,只有一方发送不会维持另一方的映射 关系正常工作。除了频繁发送“打洞”维持报文以外,还有一个 方法就是在当前的“洞”有效期过期之前,P2P客户端双方重新 “打洞”,丢弃原有的“洞”,这也不失为一个有效的方法。

 

TCP打洞

建立穿越NAT设备的P2P的TCP连接只比UDP复杂一点点,TCP协议的 “打洞”从协议层来看是与UDP的“打洞”过程非常相似的。尽管如此,基于TCP协议的打洞至今为止还没有被很好的理解,这也造成了的对其提供支持的NAT设备不是很多。在NAT设备支持的前提下,基于TCP的“打洞”技术实际上与基于UDP的“打洞”技术一样快捷、可靠。实际上,只要NAT设备支持的话,基于TCP的P2P技术的健壮性将比基于UDP技术的更强一些,因为TCP协议的状态机给出了一种标准的方法来精确的获取某个TCP session的生命期,而UDP协议则无法做到这一点。

套接字和TCP端口的重用

实现基于TCP协议的P2P打洞过程中,最主要的问题不是来自于TCP协议,而是来自于应用程序的API接口。这是由于标准的伯克利(Berkeley)套接字的API是围绕着构建Client /服务器程序而设计的,API允许TCP套接字通过调用connect()函数来建立向外的连接,或者通过listen()和accept函数接受来自外部的连接,但是,API不提供类似UDP那样的,同一个端口既可以向外连接,又能够接受来自外部的连接。而且更糟的是,TCP的套接字通常仅允许建立1对1的响应,即应用程序在将一个套接字绑定到本地的一个端口以后,任何试图将第二个套接字绑定到该端口的操作都会失败。

为了让TCP打洞能够顺利工作,我们需要使用一个本地的TCP端口来监听来自外部的TCP连接,同时建立多个向外的TCP连接(通过SO_REUSEADDR、SO_REUSEADDR)。

打开P2P的TCP流

假定Client A希望建立与Client B的TCP连接(Client A和Client B已经与公网上的服务器建立了TCP连接),步骤为:

1. Client A使用其与服务器的连接向服务器发送请求,要求服务器协助其连接Client B;

2. 服务器将Client B的公网和内网的TCP地址的二元组信息返回给A,同时,服务器将Client A的公网和内网的地址二元组也发送给Client B;

3. Client A和Client B使用连接服务器的端口异步地发起向对方的公网、内网地址二元组的TCP连接,同时监听各自的本地TCP端口是否有外部的连接联入;

4. Client A和Client B开始等待向外的连接是否成功,检查是否有新连接联入。如果向外的连接由于某种网络错误而失败,如:“连接被重置”或者“节点无法访问”,Client 只需要延迟一段时间(例如延迟一秒钟),然后重新发起连接即可,延迟的时间和重复连接的次数可以由应用程序编写者来确定;

5. TCP连接建立起来以后,Client 之间应该开始鉴权操作,确保目前联入的连接就是所希望的连接。如果鉴权失败,Client 将关闭连接,并且继续等待新的连接联入。Client 通常采用“先入为主”的策略,只接受第一个通过鉴权操作的Client ,然后将进入P2P通信过程不再继续等待是否有新的连接联入。

P2P如何实现内网穿透(打洞)_第5张图片 图 5 TCP打洞流程图

与UDP不同的是,因为使用UDP协议的每个Client 只需要一个套接字即可完成与服务器的通信,而TCP Client 必须处理多个套接字绑定到同一个本地TCP端口的问题。现在来看实际中常见的一种情景,A与B分别位于不同的NAT设备后面,如图 3所示,并且假定图中的端口号是TCP协议的端口号,而不是UDP的端口号。图中向外的连接代表Client A和Client B向对方的内网地址二元组发起的连接,这些连接或许会失败或者无法连接到对方。如同使用UDP协议进行“打洞”操作遇到的问题一样,TCP的“打洞”操作也会遇到内网的IP与“伪”公网IP重复造成连接失败或者错误连接之类的问题。

Client 向彼此公网地址二元组发起连接的操作,会使得各自的NAT设备打开新的“洞”允许Client A与Client B的TCP数据通过。如果NAT设备支持TCP“打洞”操作的话,一个在Client 之间的基于TCP协议的流通道就会自动建立起来。如果Client A向Client B发送的第一个SYN包发到了Client B的NAT设备,而Client B在此前没有向Client A发送SYN包,Client B的NAT设备会丢弃这个包,这会引起A的“连接失败”或“无法连接”问题。而此时,由于Client A已经向Client B发送过SYN包,Client B发往Client A的SYN包将被看作是由Client A发往Client B的包的回应的一部分,所以Client B发往Client A的SYN包会顺利地通过Client A的NAT设备,到达Client A,从而建立起Client A与Client B的P2P连接。

TCP同时打开

 有一种特殊的TCP P2P打洞场景:假定各终端的TCP连接启动时间比较巧合,使得他们各自发送的 SYN报文,在到达对方的NAT设备之前,对方的SYN报文都已经 穿越NAT设备,并在NAT设备上生成TCP映射关系。在这种“幸 运”的情况下,NAT设备不会拒绝SYN报文,双方的SYN报文都 能通过终端间的NAT设备,到达对方。此时,终端会发现TCP连 接同时打开,每个终端的TCP返回SYN-ACK报文,报文的SYN 部分必须与之前发送的SYN报文一样,而ACK部分告知对方到达的SYN信息。

为什么这种方式可以建立TCP连接呢?这是因为TCP三次握手中有一种特殊的场景:通信双方同时在相同端口打开连接的场景,如图 6所示:

P2P如何实现内网穿透(打洞)_第6张图片 图 6 TCP同时打开连接

 

参考资料

http://www.h3c.com/cn/MiniSite/Technology_Circle/Net_Reptile/ 第五期(NAT专题)

p2p通信原理及实现

P2P的原理和常见的实现方式(为libjingle开路)

扫描二维码,关注“小眼睛的梦呓”公众号,在手机端查看文章 扫描二维码,关注“清远的梦呓”公众号,在手机端查看文章

 

你可能感兴趣的:(网络通信)