NAT 穿透技术的了解

38 篇文章 3 订阅

NAT穿透的就是让位于不同内部地址的机器之间通信。

 

目前我知道的技术就是:

1、relaying   就是双方都和一个具有公有ip的server,S连接,然后通过S来进行转发,这是最可靠,但效率最低的方式。

2、UDP hole punching  就是UDP打洞技术。

3、TCP hole punching   就是TCP打洞技术。

 

以下内容转自:http://blog.csdn.net/wuqiubin/archive/2008/10/11/3059151.aspx

1 转发方式
 
最可靠但同时也是效率最低的p2p穿越NAT进行通信的方法是采用类似C/S方式的转发。假定两个节点
A和B每个节点都有向外的TCP或UDP连接,联入公共的已知服务器S,S的公网IP地址是18.181.0.31,
端口号是1234(如图2所示),每个客户端位于不同的私有内网中,并且它们的NAT设备妨碍了客户
端之间直接的p2p连接。做为对直连方案的替代方案,两个客户端可以利用公共的服务器S进行
消息的转发。例如,A为了将消息送给B,A只需将消息发给S,然后由S转发给B,这一过程将使用A与
B事先与S建立好的连接。
转发方式通常只能在双方客户端都连接到服务器的时候有效。这种方式的缺点在于,它假定服务
器的处理能力和网络带宽以及通信延迟都是理想的情况下,不会受到客户端个数的影响。但是,由于
没有其它的方法能够像转发方式那样,可以穿越现存的所有NAT设备,因此在构建高可靠性的p2p系统
的时候,通过服务器转发的方式依旧是一个非常有用的保证系统可靠性的方法。TURN协议定义了如何
实现安全的转发方式。

假定A要发起对B的直接连接,“打洞”过程如下所示:
(endpoint指ip地址和端口的配对)
(1)A最初不知道如何向B发起连接,于是A向服务器S发送消息,请求S帮助建立与B的UDP连接。

(2)S将含有B的公网和内网的endpoint发给A,同时,S将含有A的公网和内网的endpoint的用于请求
连接的消息也发给B。一旦这些消息顺利到达,A与B就都知道了对方的公网和内网的endpoint。

(3)当A收到由S发来的包含B的公网和内网endpoint的消息,A开始向这些B的endpoint发送UDP数据包,
并且A会自动锁定第一个给出响应的B的endpoint。同理,当B收到由S发来的A的公网和内网endpoint
以后,也会开始向A的公网和内网的endpoint发送UDP数据包,并且自动锁定第一个得到A的回应的
endpoint。由于A与B的互相向对方发送UDP数据包的操作是异步的,所以A和B发送数据包的时间先后
并没有严格的时序要求。
 
下面我们就来看一下这三个角色之间是如何进行UDP“打洞”的。在这里我们分为三种具体情景来讨论:
第一种也是最“简单”的一种情景,两个客户端都位于同一个NAT设备后面,位于同一个内网中;
第二种也是最普遍的一种情景,两个客户端分别位于不同的NAT设备后面,分属不同的内网;
第三种是客户端位于两层NAT设备之后,通常最上层的NAT是由ISP网络提供商,第二层的NAT是
家用的NAT路由器之类的设备。
 
通常情况下由应用程序自身确定的网络物理层连接方式是很困难的,有时甚至是不可能的,即使是上述
的若干种情景下可以穿越NAT,也只是代表在一定时期内有效,而不是永久有效的。诸如STUN之类的网络
协议或许可以提供必要的NAT信息,但在遇到多层NAT设备的时候,通常这些信息也不是完全完整和有效的。
尽管如此,只要NAT设备的响应是“合理”的,在通常情况下“打洞”技术还是能够在应用程序对网络状况
一无所知的前提下自动适用于多数场合。(“合理”的NAT响应将在第五章中详细讨论)
 
3.3 p2p客户端位于同一个NAT设备后面
 
首先假设两个客户端位于同一个NAT设备后面,并且位于相同的内网(相同的私有IP地址域)如图4所示。
A与S建立了UDP连接,经过NAT转换后,A的公网端口被映射为62000。B同样与S建立了UDP连接,公网端口
映射为62005。
 
 
 
 
 
(图4)
 
假设A想通过服务器S做为介绍人,发起对B的连接。A向S发出消息请求与B进行连接。S将B的公网endpoint
(即公网ip和 port)以及内网endpoint(即内网ip和port)发给A,同时把A的公网、内网的endpoints发给B。由A和B发往对方公网 endpoint的UDP数据包能否被对方收到,这取决于当前的NAT是否支持“发夹”转换(hairpin转换,也就是同一台设备,不同端口之间的 UDP数据包能否到达,详见3.5节)。但是A与B往
对方内网endpoint发送的UDP数据包是一定可以到达的,无论如何,内网数据包不需要路由,并且速度
更快。A与B有很大的可能性采用内网的endpoint进行常规的p2p通信。
 
假定NAT设备支持“发夹”转换,应用程序也忽略由内网endpoint的连接,那么A、B会采用公网endpoint
做为p2p通信的连接,这势必会造成数据包无谓地经过NAT设备,这是一种对资源的浪费。我们会在
第六节讨论这种情况,毕竟支持“发夹”转换的NAT设备还远没有对“打洞”技术支持的NAT设备多。
就目前的网络情况而言,应用程序在“打洞”的时候,最好还是把公网endpoint和内网endpoint都
实验一下。
 
3.4 p2p客户端位于不同的NAT设备后面
 
假定A与B在不同的NAT设备后面,分属不同的内网,如图5所示。A与B都经由各自的NAT设备与
服务器S建立了UDP连接,A与B的本地端口号均为4321,服务器S的公网端口号为1234。在“向外”
session中,A的公网IP被映射为155.99.25.11,公网端口为62000,B的公网IP被映射为
138.76.29.7,公网端口为31000。
如下所示:
客户端A-->
本地IP:10.0.0.1,本地端口:4321,公网IP:155.99.25.11,公网端口:62000
客户端B-->
本地IP:10.1.1.3,本地端口:4321,公网IP:138.76.29.7,公网端口:31000
 
 
 
(图 5)
 
在A向服务器S发送的登陆消息体中,会包含A的内网endpoint信息,即10.0.0.1:4321;
服务器S会记录下A的内网endpoint,同时会把自己观察到的A的公网endpoint记录下来,
即155.99.25.11:62000。同理,服务器S会记录下B的内网endpoint,10.1.1.3:4321和
由S观察到的B的公网endpoint,138.76.29.7:31000。无论A与B二者任何一方向S发送
p2p连接请求,服务器都会将其记录下来的上述的公网、内网endpoint发送给A、B。
 
由于A、B分属不同的内网,它们彼此的内网endpoint无法在公网中路由,所以发往各自
内网endpoint的UDP数据包会发送到错误的主机或者根本不存在的主机。因此应用程序
对于收到的消息必须经过授权和过滤,只有通过授权的的消息才能是从对方的endpoint
发出来的,例如,可以在消息中加入对方的程序名称、加密算法,或者至少是一个双方都
从服务器S上的预先得到的随机数字。
 
现在假定A的第一个消息将发往B的公网endpoint,如图5所示。该消息途经A的NAT设备,
并在该设备上生成了一个“向外”的session。新的session源endpoint是10.0.0.1:4321
该endpoint和A与服务器S的建立连接的时候NAT生成的源endpoint一样,但它的目的
endpoint不同。如果A的NAT设备给出的响应是“友好”的,那么A的NAT设备将保留A的
内网endpoint,并且所有来自A的源endpoint(10.0.0.1:4321)的数据包都沿用
A与S事先建立起来的session,公网endpoint均为(155.99.25.11:62000)。
A向B的公网endpoint发送消息的过程就是“打洞”的过程,从A的内网的角度来看应为从
(10.0.0.1:4321)发往(138.76.29.7:31000),从A的在其NAT设备上建立的session
来看,是从(155.99.25.11:62000)发到(138.76.29.7:31000)。
 
如果A发给B的公网endpoint的消息包在B向A发送消息包之前到达B的NAT设备,B的NAT
会认为A发过来的消息是未经授权的公网消息,会丢弃掉该数据包。B发往A的消息包
根上述的过程一样,会在B的NAT上建立一个(10.1.1.3:4321,155.99.25.11:62000)
的session(通常也会沿用B与S连接时建立的session,只是该session现在不光可以
接受由S发给B的消息,还可以接受从A的NAT设备-155.99.25.11:6200发来的消息)

一旦A与B都向对方的NAT在公网上的endpoint发送了数据包,就打开了A与B之间的“洞”,
A与B向对方的公网endpoint发送数据,等效为向对方的客户端直接发送UDP数据包了。
一旦应用程序确认已经可以通过往对方的公网endpoint发送数据包的方式让数据包到达
NAT后面的目的应用程序,程序会自动停止继续发送用于“打洞”的数据包,转而开始
真正的p2p数据传输。
3.5 p2p客户端位于多层NAT设备后面
 
有的网络拓扑结构包含了多个NAT设备,如果没有掌握该拓扑结构的详细信息,两个客户端
之间是无法建立“最优化”的p2p路由的。现在我们来讨论最后一种情况,如图6所示。假定
NAT C是由ISP(Internet Service Provider)提供的工业级的NAT设备,NAT C提供将多个
下属的用户NAT或用户节点映射到有限的几个公网IP的服务,NAT A和NAT B做为NAT C的内网
节点将把用户的家庭网络或内部网络接入NAT C的内网,然后用户的内部网络就可以经由NAT C
访问公网了。从这种拓扑结构上来看,只有服务器S与NAT C是真正拥有公网可路由IP地址的
设备,而NAT A和NAT B所使用的“公网”IP地址,实际上是由ISP服务提供商设定的(相对于NAT C
而言)内网地址(本位的后续部分我把这个由ISP提供的内网地址相对于NAT A和NAT B称之为
“伪”公网地址),同理隶属于NAT A与NAT B的客户端,相对与NAT A,NAT B而言,它们处于
NAT A,NAT B的内网,以此类推,客户端可以放到到多层NAT设备后面。客户端A和客户端B
发起对服务器S的连接的时候,就会依次在NAT A和NAT B上建立向外的session,而NAT A、
NAT B要联入公网的时候,会在NAT C上再建立向外的session。
 
 
(图 6)
现在假定客户端A和B希望通过UDP“打洞”完成两个客户端的p2p直连。最优化的路由策略是
客户端A向客户端B的“伪公网”IP上发送数据包,即ISP服务提供商指定的内网IP,NAT B的
“伪”公网endpoint,10.0.1.2:55000。由于从服务器S的角度只能观察到真正的公网地址,
也就是NAT A,NAT B在NAT C建立的session的真正的公网地址155.99.25.11:62000以及
155.99.25.11:62005,所以非常不幸,客户端A与客户端B是无法通过服务器S知道这些
“伪”公网的地址的。而且即使客户端A和B通过某种手段可以得到NAT A和NAT B的“伪”公网
地址,我们仍然不建议采用上述的“最优化”的打洞方式,这是因为这些地址是由ISP服务
提供商提供的或许会存在与客户端本身所在的内网地址重复的可能性。(例如:NAT A的
内网的IP地址域恰好与NAT A在NAT C的“伪”公网IP地址域重复,这样就会导致打洞数据包
无法发出的问题)
 
因此客户端别无选择,只能使用由公网服务器S观察到的A,B的公网endpoint进行“打洞”
操作,用于“打洞”的数据包将由NAT C进行转发,这里NAT C是否支持“发夹”转换或“环路”
转换非常重要,否则数据包将无法由NAT C转发给NAT A和NAT B,进而无法到达客户端A和B。
当客户端A向客户端B的公网endpoint(155.99.25.11:62005)发送UDP数据包的时候,NAT A
首先把数据包的源endpoint由A的内网endpoint(10.0.0.1:4321)转换为“伪”公网endpoint
(10.0.1.1:45000),现在数据包到了NAT C,NAT C应该可以识别出来该数据包是要发往
自身转换过的公网endpoint,如果NAT C可以给出“合理”响应的话,NAT C将把该数据包的
源endpoint改为155.99.25.11:62000,目的endpoint改为10.0.1.2:55000,即NAT B的
“伪”公网endpoint,NAT B最后会将收到的数据包发往客户端B。同样,由B发往A的数据包
也会经过类似的过程。也有很多NAT设备不支持类似这样的“发夹”转换,但是已经有越来
越多的NAT设备生产厂商开始加入对该转换的支持。
 
3.6 UDP在空闲状态下的超时问题
 
由于UDP转换协议提供的“洞”不是绝对可靠的,多数NAT设备内部都有一个UDP转换的空闲状态
计时器,如果在一段时间内没有UDP数据通信,NAT设备会关掉由“打洞”操作打出来的“洞”,
做为应用程序来讲如果想要做到与设备无关,就最好在穿越NAT的以后设定一个穿越的有效期。
很遗憾目前没有标准有效期,这个有效期与NAT设备内部的配置有关,最短的只有20秒左右。
在这个有效期内,即使没有p2p数据包需要传输,应用程序为了维持该“洞”可以正常工作,也
必须向对方发送“打洞”维持包。这个维持包是需要双方应用都发送的,只有一方发送不会维持
另一方的session正常工作。除了频繁发送“打洞”维持包以外,还有一个方法就是在当前的“洞”
有效期过期之前,p2p客户端双方重新“打洞”,丢弃原有的“洞”,这也不失为一个有效的方法。
4 关于TCP打洞技术

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

4.1 套接字和TCP端口的重用

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

为了让TCP“打洞”能够顺利工作,我们需要使用一个本地的TCP端口来监听来自外部的TCP连接,同时
建立多个向外的TCP连接。幸运的是,所有的主流操作系统都能够支持特殊的TCP套接字参数,通常
叫做“SO_REUSEADDR”,该参数允许应用程序将多个套接字绑定到本地的一个endpoint(只要所有要
绑定的套接字都设置了SO_REUSEADDR参数即可)。BSD系统引入了SO_REUSEPORT参数,该参数用于区分
端口重用还是地址重用,在这样的系统里面,上述所有的参数必须都设置才行。

4.2 打开p2p的TCP流

假定客户端A希望建立与B的TCP连接。我们像通常一样假定A和B已经与公网上的已知服务器S建立了TCP
连接。服务器记录下来每个联入的客户端的公网和内网的endpoints,如同为UDP服务的时候一样。
从协议层来看,TCP“打洞”与UDP“打洞”是几乎完全相同的过程。

1、客户端A使用其与服务器S的连接向服务器发送请求,要求服务器S协助其连接客户端B。
2、S将B的公网和内网的TCP endpoint返回给A,同时,S将A的公网和内网的endpoint发送给B。
3、客户端A和B使用连接S的端口异步地发起向对方的公网、内网endpoint的TCP连接,同时监听
各自的本地TCP端口是否有外部的连接联入。
4、A和B开始等待向外的连接是否成功,检查是否有新连接联入。如果向外的连接由于某种网络
错误而失败,如:“连接被重置”或者“节点无法访问”,客户端只需要延迟一小段时间(例如
延迟一秒钟),然后重新发起连接即可,延迟的时间和重复连接的次数可以由应用程序编写者
来确定。
5、TCP连接建立起来以后,客户端之间应该开始鉴权操作,确保目前联入的连接就是所希望的
连接。如果鉴权失败,客户端将关闭连接,并且继续等待新的连接联入。客户端通常采用
“先入为主”的策略,只接受第一个通过鉴权操作的客户端,然后将进入p2p通信过程不再继续
等待是否有新的连接联入。


(图 7)

与UDP不同的是,使用UDP协议的每个客户端只需要一个套接字即可完成与服务器S通信,
并同时与多个p2p客户端通信的任务,而TCP客户端必须处理多个套接字绑定到同一个本地
TCP端口的问题,如图7所示。

现在来看更加实际的一种情景,A与B分别位于不同的NAT设备后面,如图5所示,并且假定图中
的端口号是TCP协议的端口号,而不是UDP的端口号。图中向外的连接代表A和B向对方的内网
endpoint发起的连接,这些连接或许会失败或者无法连接到对方。如同使用UDP协议进行“打洞”
操作遇到的问题一样,TCP的“打洞”操作也会遇到内网的IP与“伪”公网IP重复造成连接失败或者
错误连接之类的问题。

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

4.3 从应用程序的角度来看TCP“打洞”

从应用程序的角度来看,在进行TCP“打洞”的时候都发生了什么呢?假定A首先向B发出SYN包,
该包发往B的公网endpoint,并且被B的NAT设备丢弃,但是B发往A的公网endpoint的SYN包则
通过A的NAT到达了A,然后,会发生以下的两种结果中的一种,具体是哪一种取决于操作系统
对TCP协议的实现:

(1)A的TCP实现会发现收到的SYN包就是其发起连接并希望联入的B的SYN包,通俗一点来说
就是“说曹操,曹操到”的意思,本来A要去找B,结果B自己找上门来了。A的TCP协议栈因此
会把B做为A向B发起连接connect的一部分,并认为连接已经成功。程序A调用的异步connect()
函数将成功返回,A的listen()等待从外部联入的函数将没有任何反映。此时,B联入A的操作
在A程序的内部被理解为A联入B连接成功,并且A开始使用这个连接与B开始p2p通信。

由于收到的SYN包中不包含A需要的ACK数据,因此,A的TCP将用SYN-ACK包回应B的公网endpoint,
并且将使用先前A发向B的SYN包一样的序列号。一旦B的TCP收到由A发来的SYN-ACK包,则把自己
的ACK包发给A,然后两端建立起TCP连接。简单的说,第一种,就是即使A发往B的SYN包被B的NAT
丢弃了,但是由于B发往A的包到达了A。结果是,A认为自己连接成功了,B也认为自己连接成功
了,不管是谁成功了,总之连接是已经建立起来了。

(2)另外一种结果是,A的TCP实现没有像(1)中所讲的那么“智能”,它没有发现现在联入的B
就是自己希望联入的。就好比在机场接人,明明遇到了自己想要接的人却不认识,误认为是其它
的人,安排别人给接走了,后来才知道是自己错过了机会,但是无论如何,人已经接到了任务
已经完成了。然后,A通过常规的listen()函数和accept()函数得到与B的连接,而由A发起的向
B的公网endpoint的连接会以失败告终。尽管A向B的连接失败,A仍然得到了B发起的向A的连接,
等效于A与B之间已经联通,不管中间过程如何,A与B已经连接起来了,结果是A和B的基于TCP协议
的p2p连接已经建立起来了。

第一种结果适用于基于BSD的操作系统对于TCP的实现,而第二种结果更加普遍一些,多数linux和
windows系统都会按照第二种结果来处理。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值