目前几种常见穿NAT的方法分析

目前几种常见穿NAT的方法分析


本文转自http://blog.csdn.net/wcl0715/archive/2006/04/25/676078.aspx

 

       NAT的出现在一定程度上解决了发展中国家网络地址资源不足的情况,然而,这种解决方法也带来了一些问题,尤其是对网络要求十分苛刻的流媒体传输方面,这些问题变得尤为突出(什么是NAT请参考BOLG的另外一篇文章),在一个带有NAT结构的网络环境中,不能实现P2P(peer to peer也就是点对点)的数据传输是VOIP的一个硬伤,而如何穿过这些NAT,实现数据的点对点传输也成了VOIP开发人员不得不面对的一个头疼问题.

       目前,解决NAT问题的方法大体有四种(按照我的个人观点,不同意的请拍砖):修改设备、辅助探测、设备支持和流中转。下面我来分别说明。

       (1)修改设备。这种方法也就是通过修改防火墙、router等方法,对在有NAT情况下,实现P2P的数据传输方式。优点很明显,不用对UA做任何修改,缺点也同样突出:不可能指望所有的防火墙或者router都支持。这种方法不牵扯到技术方面的东西,更多的工作交了防火墙或者router开发人员,我不想多说。

       (2)辅助探测。和第一种方法相比较,这种方法不需要对防火墙或者router做任何修改,但UAC需要做一些配合,才能完成NAT的穿越。STUN是这种穿越方式的典型代表。STUN是RFC为了解决NAT而做出的一个标准(你可以在RFC下载到相关文挡,在我的开发资源里面有连接),符合这个标准的UAC和服务器,依靠他们之间的一些信息交换,可以确定出UAC在公网的IP地址和端口,从而达到穿越NAT实现P2P的目的。STUN不需要修改网络的任何部分,也可以在多层NAT下,实现P2P(注:并不是真正意义上的P2P),是一种很可靠的NAT解决方法,因此,STUN在NAT的初期得到了迅速的发展,你可以找到很多支持STUN的UAC和server资源。STUN的缺点也是显而易见的:首先,他需要UAC支持。其次用这种方法必须要得到服务器的支持并且要求UAC不停的向一个公网IP发送数据包以维持端口(keep alive)。而这几个都不是STUN的硬伤,真正置STUN于死地的是一种叫对称NAT的NAT类型,对于这种NAT类型,STUN 无能为力。在NAT的早期,这种对称NAT是很少的,然而,随着对网络安全要求的提高,目前生产的NAT几乎全是对称NAT,这也就等同于宣布STUN的死亡。

      (3)设备支持。这种方法和第一种有些类似,但是区别也是很大的,我用这种方法的典型实例:UPNP来说明。UPNP是微软和Intel力推的一种标准(显然这两个家伙大家都不喜欢,但这并不意味着它们的东西不好),你可以认为这种标准是另一种USB标准,只要你符合这种标准,你生产的USB设备可以被世界上任何一天USB设备使用(我不想讨论主从的问题)。而UPNP是网络设备的USB标准,只要你生产的设备符合UPNP标准,那么它就变成了可以连接到网络上的,通过网络控制的USB设备,你用就可以,而不需要关心如何用,怎么用。

      第一次看到UPNP的协议文挡的时候,我就对自己说:这是个好东西。尤其是当我看到,目前绝大部分(近期生产)网络设备都支持UPNP的时候,我就更加坚信了我的观点。

     事实上,我们穿越NAT所需要用的只是UPNP协议族中的一个:WANIPCONNECT SERVICE,也就是互连网接入服务(请允许我这么翻译,虽然这么有点不恰当,但是更加通俗)。你可以理解为USB设备有N多,而我们用的是U盘一样。通过这个协议,你可以控制互连网接入设备(通常是router),来做一些特殊操作,打开一个IP地址影射来实现P2P。这个过程很简单,只是发几个符合要求的数据包给UPNP服务设备,但功能却很强大,的确很迷人。(你可以在开发资源里面找到UPNP的相关连接)

     UPNP的优点如上所说,不需要在累述,但缺点也是有的:1、他需要UAC支持,完成对UPNP设备的控制,令人高兴的是,这不是很难。2、这种方法不支持多层NAT嵌套。3、总会碰到不支持UPNP或者号称支持,实际上却不支持的UPNP设备。

     (4)流中转。流中转是依靠一个公网服务器对两个UAC的RTP流进行中转的方式解决NAT问题(注意:只是解决,而不是实现P2P),在这种方式下,UAC不需要做任何修改(或者很少修改),并且可以穿越所有的NAT。典型的是outbound。也就是具有公网IP的一台RTP流转发服务器。如果outbound server和sip server 没有合成到一个程序中,那么你必须首先把信令发送给outbound server,其它的你不需要再关心。如果是已经合成了一个,那你可以象在公网中一样使用你的UAC,不必再考虑NAT的问题(舒服吧)。

      流转发(outbound)方式简单,可靠。它使你不再需要考虑NAT的任何东西,可以穿越所有NAT,并且UAC不需要做任何修改,目前网络上有很多SERVER都是工作在这种方式下。但是它的代价也是昂贵的:它不能实现P2P。这在两个UAC距离服务器很远的情况下,会有高的网络延时和高的丢包率,而这种环境下的语音或者视频效果,可能是你不能忍受的。

      由于我水平有限,我所知道的NAT解决方案就这四种。如果你问我:xxx方式(比如ICE)怎么没有的时候,请您先考虑下这种方法是不是上面方法中的一种或几种的组合(比如汽车是一种交通工具,船也是一种,而我并不认为你把汽车和船弄到了一起就是种新的交通工具),当您知道有别的好的NAT解决方案的时候,麻烦您通知我下,在此谢过。




===============================================================================================

穿NAT的方法的核心思想及实现

本文转自 http://blog.csdn.net/wcl0715/archive/2006/04/25/676075.aspx,像原作者致意

 

          本来题目是几种,不过写到这里的时候,其中的一些东西已经被其它文章所含概了,所以这里只谈NAT的穿透的核心思想:punch技术,我称之为对穿.

   (1)先来说对穿的工作原理

   在这里我用两个都是在port restrict cone NAT后面的UAC做例子,假设一个是 A,一个是D。两个NAT 分别是B和C。

   先来说对穿的过程

   STEP1:UAC A通过A的porta向STUN服务器发送数据包,得到本socket的NAT的外网IP和端口portnat,注意,此时,这个portnat已经和stun的IP和port绑定在了一起,这个断口将拒绝一切非STUN的IP和对应端口的数据包。

   STEP2:现在,假设另一IP(为E),通过某种方法(如sip server)得到了此IP和portnat,在t1时刻,开始不停通过porte向此IP和portnat发送数据包。很不幸的,根据port restrict cone NAT的原理,这些数据包将被NAT全部拒绝,因为此时,这个portnat已经被绑定给了STUN的IP和port,因此他拒绝一切非stun的IP和port发来的数据包,而E发来这些数据包不符合这个port的通过要求,因此这些数据被NAT全部过滤掉,也更不可能到达A。

   STEP3:在t2时刻A在次通过A的porta向E的IP和端口porte发送了若干个数据包。这是关键,然后如何呢?让我们来分析一下:

   对于NAT来说,A的发送port没有改变,因此根据CONE NAT的原理,它仍然用在step1中打开的端口portnat向E发送数据包。对应的,NAT的此port的帮定也就发生了变化,它允许来自E的porte端口的数据包通过NAT,到达A。这也就意味着,t2时刻以后的来自E的数据包A都可以接收到。

   (2)SIP中RTP流的对穿过程

   应该说,如果你理解了上面穿透过程,以下的东西是很好理解的。

   首先,A通过SIP信令得到D在NAT C的IP和RTP接收端口portd,并且开始向此IP和port发送RTP流。同样的D也随后想A的NAT B的IP和port发送RTP流。

   理想的情况,在A或D发送的RTP流还没有到达对方之前,A,D都已经开始了发送RTP流,这样,A和D发送出来的数据就都不会被对方的NAT拒绝。遗憾的是这是很难做到的,通常都是一方到达,而另一放的数据还没有开始发送,而在现实中,这种情况占了绝大部分。

   假设D发送过来的RTP流已经到达了A所在NAT B的RTP接收端口,而此时,A还没有通过此端口向D发送数据,那么这些包将被无情的拒绝。而一旦A通过这个端口发送了数据,那么以后D发送来的数据,能够全部通过NAT,到达A,这也就完成了RTP的穿越。同样的情况也发生在NAT C,A发送的数据也可以通过C到达D。

   (3)对称NAT下,对穿失败的原因所在

   实际上,对称NAT失去了可以对穿的基础:port不变。下面说下失败过程。

   当A通过本地端口PA向STUN服务器发送数据包,得到本端口在NAT上的影射端口PB,并把此端口作为RTP接收流的port。通过SIP信令,这个port被告之给了D,D随后向这个PORT发送数据包,此时,A通过本地端口PA向D发送数据包。如果是cone NAT,NAT仍然会用A向STUN发送数据的端口发送数据,可是,遗憾的是,对于对称NAT,NAT会重新打开一个新的port,用来发送给D的这些数据。也就是说,A期望这些数据通过最开始得到的影射端口PB来向D发送这些数据包,而NAT却没有这么做。所以,D发来的数据永远能不能满足进入规则,这些包将被永远丢弃。

   (4)额外的一些思考

   前面已经说过,在实际的应用中,一方的数据包都会在另外一放开始发送数据出去之前到达对方,这也就意味着这些数据包将被丢失。所以,在音频RTP传输中,你有可能只听到声音“喂”的后一半,虽然有些不舒服,但毕竟影响不大,还是可以被人接受的。

   然而在视频传输中,为了减少通过网络传输的流量,很多编码采取发一个I贞,然后发N个P贞,再发一个I贞的办法减小数据量。而发送给对方第一个被发送的往往是I贞,而一旦I贞的丢失意味着丢失的一方必须等下一个I贞的到来,这个时间可能很长。因此丢失一方在很长的一段时间内没有视频显示,这对用户来说是很不友好的。甚至是不能容忍的。

   一个解决办法是,在视频开始的时候,传送多个I贞。这样,即使头一个没有到达,后面的I贞也能解决问题。然而,这样的解决办法也带来了一些问题。因为通常I贞都比较大,多发送I贞就意味着增加了网络流量。在环境恶劣的情况下,对方可能一个I贞都没有收完整。

   另一个方法是在开始接通之前,发送冗余数据,直到接收到对方来的数据之后才开始发真正的数据包,但这样又给代码的编写提出了要求,也增加了系统的不稳定因素。




===============================================================================================

NAT、防火墙的原理区别和分类


本文转自 http://blog.csdn.net/wcl0715/archive/2006/04/25/676070.aspx

 

       1、NAT

       NAT是Net Address Translation 的缩写,从名字也可以看出,它是负责网络地址转换的一个协议。通俗的说,它负责把私网内的的IP和端口转换成公网的IP和端口,也即使我们通常所说的IP地址影射。例如:公司内一般有一个私网,假设为10.1.1.1网段。公司通过一个公网服务器(机器A)接入Internet,此服务器内网IP为10.1.1.1,外网为220.220.1.6。私网内的机器B 10.1.1.102打开自己的88端口想连接公网的一台WEB服务器C,假设IP为:204.56.43.8,端口为80。过程如下:

        step1: B (10.1.1.102:88->204.56.43.8:80)将数据包发送给网关A (10.1.1.1)

        step2: A   将(10.1.1.102:88)数据包的地址更换为服务器外网IP,并分配一个的端口(如何分配端口方法是不同的,以后会详细的说),假设为200,数据包也就变成了(220.220.1.6:200->204.56.43.8:80),服务器A将数据发送出去

        step3: C 将响应数据发送给网络服务器 A (204.56.43.8:80->220.220.1.6:200)

        step4:  A将C返回的响应数据报的IP地址的目的IP和端口做如下修改:(204.56.43.8:80-> 10.1.1.102:88)然后把数据发送出去

        step5:  B接收到C的响应数据。

       这个过程实际上也是NAT的工作过程,在B角度,B完全感受不到自己是在内网,它似乎有在公网一样的权利,这也就是NAT的优点所在:用同一个IP实现多个用户的Internet接入。

       2、防火墙

       刚读大学,第一次装防火墙软家的时候觉得这东西真***神秘,咋做的呢,呵呵。

       防火墙是通过管理网络端口的方法来拒绝和允许网络连接,这些拒绝或允许的原则由firewall的设计者来指定,并没有统一的标准,但防火墙大多都遵循以下几个原则:

       (1)允许内部向外部发送数据包

       (2)拒绝一切外部向内部的主动连接

       (3)允许本地发起的,符合firewall规则的外部数据包穿过firewall

       在第三条中,firewall的规则通常有以下两种:1、返回的数据包的源IP必须是内部发起的目的IP。2、返回的数据包的源IP必须是内部发起的目的IP并且返回的数据包的源端口必须是内部发起的目的端口。

       3、防火墙和NAT

       很多人把防火墙的概念混为一谈,其实NAT就是NAT,它负责IP地址影射。防火墙就是防火墙,它负责数据包的过滤。但为什么会有N多人分不清楚呢?原因很简单,是因为NAT的功能有了变化。为什么要变换呢?是因为NAT碰到了问题。为什么NAT会碰到问题呢?是因为#·#—……#¥。

       还是通过上面NAT的来说。假设C向B发送数据的过程中,C的另外一个端口100,也想向B发送数据包,那么当这个包到达A的时候A如何处理呢?过还是不过呢?如果过了,那么从另外一个IP到达A的数据包是否也允许过呢?显然在网络安全日益受到威胁的今天,让这些包通过是危险的。所以NAT决定不让这些包通过,也就是说NAT有了包过滤功能。于是:

      firewall:NAT,包过滤是我的事情,你多管什么闲事?(有没有核武器是我的事,你管得着吗?)

      NAT:让这些包通过不安全,所以我必须过滤这些数据包(伊朗有核武器,是个威胁,我必须干掉它)。

     firewall:那你是NAT啊你还是防火墙?(那你的主权,人权和和平自由呢?)

     NAT:(咬牙状)我是有部分防火墙功能的NAT,你咋地?(我想干啥干啥,你管得找吗?)

     firewall:.......(什么东西啊,整个一个杂种,还美呢)

      (其实,从概念上将,并不能这么说,但是便于理解,也没有什么深究的必要,就这么着吧)

     4、NAT分类

     NAT根据原理可以分成两类:锥型NAT和对称NAT

      (1)锥型NAT。还用上面的例子来说,在锥型NAT中,B的同一个端口去连世界上任何一台人类计算机的任何端口(日本除外),它在服务器A中得到的外部影射端口都是同一个。也就是说,在服务器A,它只有一个出口,假设为5060,而目的地却有很多,如果服务器的端口是一个平面,而所有计算机的端口在另外一个平面上的一个圆环之内,并且这两个面是平行的,那么将这些连写用线连起来,就构成了一个圆锥。这也就是cone的由来:锥。

      还记得刚才我们说的带防火墙功能的NAT,把防火墙功能加到cone类型的NAT中,就产生了三种不同的NAT:

       1。不对包进行过滤,任何包都可以通过服务器A的5060端口到达B full cone

       2。只要IP符合规则就可以到达B restrict cone

       3。IP和端口都要符合规才能到达B port restrict cone

       (说明:关于restrict cone,port restrict cone以前有位专家级别的人物说我弄反了,我也懒得去查,因为我有自己的理解,我觉得如果我理解错了,那么这个命名就有问题。)

      (2)对称NAT

       在对称NAT中,当B的同一个端口访问外部的IPC的不同端口的时候,A机器都会打开一个不同的外部端口来连接这C的不同端口。从C机器的端口来说,每一个端口在机器A中都有一个端口与之对应,这也就是对称NAT名称的由来:对称。

       对称NAT是很霸道的NAT,当数据包到达A的时候,必须IP和PORT都符合规则,数据包才允许通过。不仔细考虑,你可能觉得没什么,和port restrict cone一样啊,没什么特别,可是,你再和NAT的端口对称联系起来,这个东西就很恐怖了。因为它意味着:一旦一个端口在A打开了,那么这个连接也就确立了B:PORT->A:port->C:port的连接,无论何重情况下,这个连接都不可能被第三者使用。也就是说,一旦A上产生了一个端口,那么第一个知道这个端口的人一定会立刻拥有这个端口的终生使用权,而别人都没办法知道或者使用(除非A或者C告诉别人,但是告诉了也没用,你也使用不了,因为这个连接对别人来说,已经死了)。这也就是STUN对对称NAT无能为力的原因。因为STUN一旦检测到这个端口,那么它就拥有了这个端口的永久使用权,并且没有办法转让(这要是PLMM多好啊,谁发现是谁的,真好!公产主义来了么?天亮了,起床了),而这不是STUN想要的。

      以上也就构成了RFC中的四种NAT类型。希望我说的你都理解了(理解了就顶吧,呵呵)。




===============================================================================================

NAT爆破者:在不同NAT后的主机间建立TCP连接


原文:NATBLASTER:Establishing TCP Connections Between Hosts Behind NATs
来源: http://www.andrew.cmu.edu/user/ggw/natblaster.pdf
               NATBLASTER:Establishing TCP Connections Between Hosts Behind NATs*
                 Andrew Biggadike, Daniel Ferullo, Geoff Wilson, Adrian Perrig
                              Information Networking Institute
                                Carnegie Mellon University
                                Pittsburgh, PA 15213, USA
                      {biggadike, ferullo, ggw,  perrig}@.cmu.edu
                        NAT爆破者:在不同NAT后的主机间建立TCP连接
                         译(初稿): dragonimp@fzu 2005-6-14
                         增译并修改(初稿):weljin 2006-9-1
         译文来源: http://blogs.impx.net/dragonimp/archive/2004/10/20/487.html
         修改: http://15038084.qzone.qq.com
摘要
    防火墙和网络地址转换(NAT)设备正变得越来越流行了,同时他们也对使用点对点协议建立连接造成一个非常大的问题。合适地限制时,这些中间件设备将抑制来自外部本地网络到内部网络的TCP请求。这篇文章的目的是提出一个能够在两个NAT设备内部的主机间建立直接的TCP连接的方法,同时又尽量不依赖于第三方主机。我们已经在两个普通的硬件条件下实现了这个功能。我们能够为两个不同典型的NAT后面的主机建立TCP连接。一旦建立了这个连接,应用程序就可以跟原来的TCP一样调用这个连接,而不需要任何其他的帮助。
分类及题目描述
    D.4.4[操作系统]:信息管理——网络通信;C.2.5 [计算机通信网络]:本地和大区域网络——英特网;C.2.2[计算机通信网络]:网络协议——协议构建
综合术语
    算法,设计,可靠性
关键字
    TCP连通性,网络地址转换,点对点穿透NAT,状态防火墙,一致转换,未请求过滤,松散源路由,打洞
1. 前言
    NAT技术的出现从某种意义上解决了IPv4的32位地址不足的问题,它同时也对外隐藏了其内部网络的结构。NAT设备(NAT,一般也被称为中间件)把内部网络跟外部网络隔离开来,并且可以让内部的主机可以使用一个独立的IP地址,并且可以为每个连接动态地翻译这些地址。此外,当内部主机跟外部主机通信时,NAT设备必须为它分配一个唯一的端口号并连接到同样的地址和端口(目标主机)。NAT的另一个特性是它只允许从内部发起的连接的请求,它拒绝了所有不是由内部发起的来到外部的连接,因为它根本不知道要把这个连接转发给内部的哪台主机。
    P2P网络已经日益流行。尽管p2p文件共享软件引发了很多争夺站,比如Nepster和KaZaA之间,但是还是有很多有用的并且合法的P2P软件存在着,比如即时消息共享和文件共享。另一个P2P程序是一个叫OpenHash的项目,它为公众提供了一个可用的分布式的哈希表,很多应用程序都在它的基础上开发了出来,比如很多的即时通信软件和可靠的CD标签库。
    不幸的是,两个处于不同NAT后面的主机无法建立TCP连接,因为各自的NAT都只允许外出的连接。NAT销售商在已经为NAT设备开发了端口映射的功能来解决这个问题。NAT管理员可以使用端口映射来为那些需要接受那些不是从内部发起的连接请求的主机指定端口。但是这种解决方法根据情况还需要很多其他的支持。当有的服务器需要动态的分配端口的时候,这种方法就很受限了。再说了,如果一般的用户没有权限或者不懂得如何进入NAT设备为他们指定端口映射,那这种方法就一点用处也没有了。
    P2P协议对此已经阐述了少数通用的方法。第一个可被p2p协议使用的技术是:那些本来不能当作服务器的程序收到了来自请求者的消息后主动向请求者发起连接。这种情况只适用于只有一方在NAT后面的情况。第二种通用的方法是通过两个主机都可以连接得到的代理路由数据,但是这种方法对于两个NAT后面的主机来说效率太低了,因为所有的数据都必须经过代理。其他相关的技术将在第三部分讨论。
    我们努力的目标是找出一个可以让NAT后面的两个主机直接建立TCP连接的解决方案。特别地,我们已经开发出几种方案可以用于那些支持端口分配地NAT和那些支持LSR路由的网络。我们的方法是通过第三方提供建立直接连接需要的信息。根据不同的环境,我们开发了几种不同的方案可以在可以预测和适当的时间的情况下建立连接。这些技巧都需要把数据包的TTL值设置得很小,并且捕捉和分析外传的数据包以提供信息给第三方“媒人”。并且人为地向网络发送一些数据包用来检测NAT所分配地端口。补充一点,如果端口分配是随机地,我们就使用一种叫“birthday paradox”的方法减少检测的次数。这种方法需要的空间是直接的穷举所使用的空间的开方。
2. NAT的类型
    NAT必须考虑路由器的三个重要的特性:透明的地址分配、透明路由、ICMP包负载解析。
    地址分配是指在一个网络会话开始的时候为内部不可以路由的地址建立一个到可路由地址的映射。NAT必须为原地址和目标地址都进行这样的地址分配。NAT的地址分配有静态的和动态的方式。静态的地址分配必须预先在NAT中定义好,就比如每个会话都指派一对<内部地址,外部端口>映射到某对<外部地址,外部端口>。相反地,动态的映射在每次会话的时候才定义的,它并不保证以后的每次会话都使用相同的映射。
    一个相似的特性,NAT必须实现的是透明路由。正如上面提到的,NAT是一种特殊的路由,它在它所路由的数据包中翻译地址。这种转换基于数据流来改变相应的IP地址和端口。其次,这种转换必须是设备透明的,这样才保证对现有网络的兼容性。一个不是很明显的要求是,NAT必须保证内部网络的数据包不被发送到外部网络去。
    最后一个NAT必须实现的特性是当收到ICMP错误包的时候,NAT使用正常的数据包做出同样的转换。当在网络中发生错误时,比如当TTL过期了,一般地,发送人会收到一个ICMP错误包。ICMP错误包还包含了尝试错误的数据包,这样发送者就可以断定是哪个数据包发生了错误。如果这些错误是从NAT外部产生地,在数据包头部的地址将会被NAT分配的外部地址所代替,而不是内部地址。因此,NAT还是有必要跟对ICMP错误一样,对在ICMP错误包中包含的数据包进行一个反向的转换。
    虽然所有的NAT都实现了这三个特性,但是根据他们的特点和他们所支持的网络环境,他们还可以进入分类。NAT可以分为四种:Two-way NATs,Twice NATs,Multi-homed NATs和Traditional NATs。关于Two-way NATs,Twice NATs和Multi-homed NATs的特性讨论请看[12]。Two-way NATs,一般也叫双向NAT(Bidirectional NATs),在外部地址和内部地址间执行双向转换,尽管它个数据包只转换一个IP地址。这种NAT是唯一一种允许从外部发起的连接请求。相反地,Twice NATs,它每路由一个数据包都对内部和外部的地址进行转换。这种NAT用在那些外部地址和内部地址交叠的情况下。Multi-homed NATs对于Twice NATs来说,多了一个功能,它可以允许在内部使用不能路由的地址并且还可以有多个连接到外部网络。之所以Multi-homed NATs能够这样做,是因为它跟另一个保持通信以确定他们的地址映射是不变的。Multi-homed NATs允许大量的内部网络和增加冗余来允许多个连接到Internet。到目前为止,最常用的NAT是传统NAT(Traditional NATs),它可以分为基础NAT(Basic NATs)和NATP(Network Address Port Translation)两种。
    基本NAT和NATP的区别它所能分配给内部地址的外部地址是否比内部地址多。一个Simple NAT用在那种所能分配的外部地址跟内部地址的数量相等或者更多的时候。这种NAT进行端口分配,因为每个内部地址都可以分配到一个唯一的外部地址。NATPS用于当NAT所能用来分配的外部地址的数量比内部地址少的时候,最常见的一种情况是很多的内部机器共享一个外部IP地址。在这种情况下,NAT必须分配端口来补充IP用以消除网络传输的不明确的几率。NAT和NATP共同的地方就是他们都阻止外来的连接,并且都可以进行静态和动态的地址分配。
    NAPT是传统NAT中最普遍的一种,因为它允许很多的内部连接共享很少量的外部网络地址。大部分为小型网络设计的商业NAT都是NAPT。我们选择NATP作为我们研究的对象就是因为它的流行和它通过不允许外来连接限制了P2P协议。后面我们就把NATPs简称为NATs了。
    我们首先要做的是得到商业防火墙以确定他们的特性跟资料上记载的是否一样。我们使用NatCheck这个程序对三个常用的NAT设备进行了测试:Netgear MR814,Linksys BEFSR41,和Linksys BEFW11S4。三个NAT都有相似的行为:他们对UDP和TCP都进行了一致的转换,这表明在内部主机使用<内部IP:内部端口>的时候,NAT是否直接将<内部IP:内部端口>映射到<外部地址:外部端口>,而不管它连接出去的目标主机<目标IP:目标端口>是多少。一致转换是静态NAT与动态NAT截然不同的一个特点,因为这不只是跟内部主机使用的地址有关,而且还跟端口有关。RFC3002明确指出要支持一致转换。没有一个NAT支持环路转换,不管是TCP还是UDP,这表明NAT是否可以正确的处理两个只知道对方外部地址的内部主机之间的连接。在我们的项目中,我们假设两个主机是在不同的NAT后面的,所以这个测试跟我们的目标是无关的。最后,所有的NAT都提供了对非主动请求的外来TCP和UDP包都进行过滤,这个测试可以表明NAT是否预防非主动恳求的外来包进入到内部网络。非主动请求过滤发生在除了Two-way NAT之外的所有NAT中,这也是在不同NAT后面的主机建立P2P连接最主要的障碍了。
3. 相关研究工作
    三个来自科内尔的作者独立于我们做了关于穿过NAT的TCP直接连接工作并且结果和我们的类似。他们的被称为NUTSS[4]的框架为不同的NAT后的主机的UDP和TCP连接性作了准备,但是他们的TCP技术有一个重大的缺点。协议依靠于为了能够TCP连接的欺骗包,这包在真实的网络作了限制。许多ISP作了进入过滤以防止欺骗包进入他们的网络,这将导致作者的协议失败。欺骗不能是真实连接主机的组成部分。为了他们所信任的,作者提出了一个不依靠于欺骗的方案。然而,这技术依靠于平台相关的TCP堆栈行为。我们在这里所描述的技术在相当于NUTSS[4]环境中为连接性做真实的假设时避免了欺骗。
    为了解决NAT给很多协议带来的困难,一种MIDCOM的架构被提了出来[13]。MIDCOM是一种可以允许NAT或者防火墙后面的用户根据需要改变NAT行为的而允许连接的一种协议。这个系统虽然在某些情况下是适用的,当它却不能保证每个时候都可以。在那些用户没有办法控制NAT的情况下,这种方法对于P2P的连接还是行不通的。
    很多时候NAT或者防火墙后面的用户通过代理服务器进行连接。一种商业的代理解决方法是Hopster[6]提供的,Hopsetr的代理在连接方本地以隧道级别的传输在本地运行,它通过HTTPS(端口443)连接到Hopster自己的机器。但是,因为Hopster的代理需要所有的传输都经过他们的机器,所有他们的方法跟我们的比起来就显得低效了,具体见第5部分。
    为了能够进行直接的P2P连接,出现了针对UDP的解决方法。UDP打洞技术[5]允许在有限的范围内建立连接。STUN(The Simple Traversal of User Datagram Protocol through Network Address Translators)协议实现了一种打洞技术可以在有限的情况下允许对NAT行为进行自动检测然后建立UDP连接[10]。
    在UDP打洞技术中,NAT分配的外部端口被发送给协助直接连接的第三方。在NAT后面的双方都向对方的外部端口发送一个UDP包,这样就在NAT上面创建了端口映射,双方就此可以建立连接。一旦连接建立,就可以进行直接的UDP通信了。在UDP打洞技术可以成功的情况下,我们在这篇文章中所用的有的技术也同样适用。建立TCP连接比UDP连接更有优势。首先,UDP连接不能够依赖建立好的连接,就是不能够持久连接。UDP是无连接的并且没有对明确的通信。一般地,NAT见了的端口映射如果一段时间不活动后就是过期。为了保持UDP端口映射,必须每隔一段时间就发送UDP包,就算没有数据的时候,只有这样才能保持UDP通信正常。第二,很多防火墙都配置成拒绝任何的外来UDP连接。最后,一个单纯的TCP连接的实现更直观,并且现有的代码简单得修改就可以使用我们的技术。
    目前的工作由bryan Ford[2]完成,已经扩展了打洞技术,使能在正规的NAT后的不同主机间进行TCP连接。方法类似于UDP打洞,由于一个映射在各个主机的NAT上被建立以使TCP直接连接能被创建,同步或同时TCP打开。这工作的焦点在于开发一种工作于最多的被定义为其它NAT所需要而描述的NAT的技术来和TCP打洞协调一致。我们工作的不同点在于我们所开发的方案能够在目前各类NAT行为上进行直接地TCP连接,包括不协调于TCP打洞的NAT。
    Gnutella有一个能使在两个不同端点的TCP通信的方案[14],但这仅仅应用于一个端点在NAT后面的情形。这方案被称为Push Proxy并本质地建立多个能够推连接请求到NAT后的服务器的节点。NAT后的服务器发送消息到端点询问他们是否愿意推为代理。当NAT后的服务器指明它有一个文件和询问匹配,服务器包含了一列同意被推为代理的端点。当端点想下载文件,它发送一个Gnutella PUSH消息到一个push proxy,这代理允许消息通过到达NAT后的服务器。NAT后的服务器就打开一个连接到这个发送PUSH消息的端点,所以文件可以被传送。这方法使其更容易建立连接,这仅仅处理了一个端点在NAT之后的情况。我们的方案解决的是当两个端点都在NAT之后的更困难问题。
    Walfish[15]指出,采用间接的服务可以提供NAT或者防火墙后面主机之间的连通性,通过在两主机向间接服务器打开一个连接,并且服务器在他们之间传输所有的通信,在这篇文章中我们会完成一个不需要这样的间接服务器也能建立起这样得连接。
4. 问题陈述和假设
    假设两个主机在不同的NAT后面,并且都知道对方的IP地址。如果这些主机想直接发起TCP连接,那肯定失败。在直接的TCP连接中,必须有一方是发起者(创建初始的SYN包),另一方必须监听。在两个都在NAT后面的情况下,监听者将不可能收到SYN包,因为SYN包在到达NAT的时候就被丢弃了。这是因为NAT或者防火墙不允许来自英特网的未请自来的数据包进入他们的网络内部。所以,为了在不同NAT后面的主机之间建立直接的连接,必须让NAT以为这个连接是经过内部主机发起的。我们可以通过让两边的主机都发起一个TCP连接,也就是创建一个SYN包,这样两边的NAT都会以为这个连接是从内部发起的,是经过内部请求的,因此,就可以允许后续的数据经过它的网络了。注意,虽然两个端点都发送SYN包,但我们没有使用TCP的同时打开。
    A的内部网络                 B的内部网络 
┌────────┐       ┌────────┐
│     主机A                │       │     主机B                │
│192.168.2.2:400        |        │192.168.2.2:500        |
└────────┘       └────────┘
                          ↑↑                 ↑↑
                            ↓ /                 / ↓
┌────────┐       ┌────────┐
│    NAT NA            │       │    NAT NB             │
│128.2.4.1:4000          |        │151.3.43.1:5000        |
└────────┘       └────────┘
                           ↑        /_____/      ↑
                              /         目标        /
                              ↓                   ↓
                      ┌──────────┐
                      │      第三方X                   │
                      │  66.4.2.23:8000                |
                      └──────────┘
                   英特网
          图1:我们所开发的技术环境
    为了在两个端点间成功地建立一个TCP连接,每个端点必须知道它的伙伴在初始连接前的外部表面端口号。一旦一个从NAT的内部网络请求被路由到外部网络的一个IP地址的包到达时,这些端口将被NAT所选择。就像记帐一样,NAT用所选的外部端口号绑定到内部的IP地址和端口号。我们把这绑定称为映射。NAT不会向任何主机共享这个映射。我们的技术展示了为何NAT映射可以被高效地确定。一旦两个端点都知道他们伙伴的外部表面端口号,TCP连接就被两个端点初始化。TCP序列号和应答号是TCP连接同步的完整组成部分。序列号不能够指定,只能捕捉它。我们的文章将会说明怎么协议管理这些参数以成功地在任何的环境下建立一个TCP连接。
    在不同的位于NAT后的主机间建立直接的TCP连接是一个难题,因为NAT选择外部端口不是NAT后的主机能直接理解到的,并且因为成功的TCP连接需要序列号和应答号的协调。没有工作于所有情况的单一方案。NAT的行为依靠于它的实现,并且预测端口的能力依靠于内部网络活动的数量。
    我们所做的两个假设在我们测试过的NAT中都是成立的。第一个假设是我们假设主机都收不到来自外部网络的ICMP TTL过期包。如果这些包被主机接收,那么TCP连接就会被中断。我们的很多解决方法中都依赖于能够为发送SYN的数据包设置一个很低的TTL值。当SYN包被路由丢弃的时候,TCMP TTL过期包会被发送回NAT。用于我们实现的NAT必须没有转发收到的ICMP TTL过期包给内部网络。如果NAT真的把这个包发送回内部网络,也可以配置防火墙来阻止这类包。我们的另一个假设是NAT看到ICMP TTL过期包的时候映射的端口还不会失效。作为另一个选择,我们可以不修改TTL值,那就必须让目标NAT不会产生TCP RST包。而实际上这个选择是可行的,因为很多的NAT为了防止端口扫描一般都不发送TCP RST包。
5. 技术
  使用图1的模型,我们的目标是让在NA和NB后面的A和B建立直接的TCP连接。
  我们已经开发出几种方法可以建立这样的连接,根据NAT和网络情况的不同,我们有对应的方法。考虑以下的信息作为有序的三元组:<NA端口可预测性,NB端口可预测性,源地址可路由>。我们考虑下面几种情况:
情况 1: <可预测的, 可预测的, LSR>
情况 2: <可预测的, 可预测的, no LSR>
情况 3: <随机的, 可预测的, LSR>
情况 4: <随机的, 可预测的, no LSR>
情况 5: <随机的, 随机的, LSR>
情况 6: <随机的, 随机的, no LSR>
//------第一次修改,以后修改将加入测试代码,以下内容由weljin翻译,更新将放在 http://15038084.qzone.qq.com http://blogs.impx.net/dragonimp/archive/2004/10/20/487.html,转载请保持来源的完整性------//
//------Translated by weljin.QQ:15038084.MSN:weljin@hotmail.com.G-mail:weljin.china@gmail.com.E-mail:weljin@163.com.Q-mail:weljin@qq.com.------//
注意:<随机的, 可预测的,X>等同于<可预测的, 随机的,X>。
5.1 连接前诊断
    作为协助者X还有两个端点A和的B,为了确定A和B将试图的连接属于下面的哪种情况,X必须首先对各个端点做一些诊断。
    为了使用1,3,5的情况,两端必须确定在A和X还有B和X之间的LSR(松散源路由)是否是可用的。loose source routing(LSR)是一个允许IP包的创建者指定一列在数据包的路由中使用的托管IP地址的IP选项。这个选项的结果是在路由表里的各个IP地址将按照路由表里指定的顺序收到数据包。LSR选项引来了一个安全性风险,这是因为一个攻击者能监听到在路由表里的会话。由于这一潜在的危险,许多路由器直接抛弃包含LSR选项的数据包。
    为了确定从A穿过X到B时LSR是否可用,A可以简单的尝试用松散源路由穿过X连接到B,如果X收到这个包,这时LSR在A到B的第一半到X的行途是可用的。如果在一段指定的超时后X还没收到任何包,这时可以假定LSR不可用。由于X可以用这种方法得知从A到B的一半行途是否允许LSR可用,所以它必须检查也能收到从B来的LSR包。如果也能收到,则X可以断定从A到B穿过X的LSR是可用的,任何的其它情况都必须假定LSR是无此选项。
    为了确定是否NA可以随机或者可预测地分配端口,A可以用连续的端口打开两个到X的TCP连接。如果X得到的这些连接端口是连续的,则X可以断定NA连续地分配了端口,同时是可预测的。当连接到B时,A必须使用连续的下一个端口来确保NA继续按照X能预测的方式来分配端口。
    如果NA没有连续地分配端口,但如果NA执行了一致地转换,这时仍然可以预测到A的端口。A必须先打开一个在内端口PA到X的合法连接。NA将分配给这个连接一个随机端口。当包被发到X时,X可以非常清楚地看到NA所选择的端口。A可以在相同端口打开第二个到X的不同端口的连接,这时X可以看到这两个连接是否包含了相同的外部端口。如果是,则NA进行了一致性转换。由于现在A必须用内部端口PA连接到B,所以X可以把NA所选的外部端口告诉B。把A到X的连接维持到A被连接到B以使NA不会改变端口映射是很重要的。
    如果尝试了两种端口预测方法后,X不能可靠地预测NA分配的端口,这时X必须假定NA是随机的分配端口。
    当X完成对A的诊断,它可以同时用相同的方法对B进行诊断。一旦X拥有了所需的信息,连接协议就可以开始了。从这个诊断收集信息确保了详细情况的执行。
5.2 序列号和应答号的协调
    每个参与TCP连接者都维持两个变量,一个序列号和一个应答号。在任何给定时刻,在任何主机的序列号是最后包发送的序列号。另外,在任何给定时刻,在任何主机的应答号是下一个预期包的应答号。通过三次握手的分步,初始的序列号和应答号被建立,如下:
    1.在客户端发送SYN包后,
      客户端的 seq#(序列号):P,ack#(应答号):N/A
      服务端的 seq#(序列号):N/A,ack#(应答号):N/A
    2.在服务端接收到SYN包和发送SYN+ACK后,
      客户端的 seq#(序列号):P,ack#(应答号):N/A
      服务端的 seq#(序列号):Q,ack#(应答号):P+1
    3.在客户端接收到SYN+ACK和发送ACK后,
      客户端的 seq#(序列号):P,ack#(应答号):Q+1
      服务端的 seq#(序列号):Q,ack#(应答号):P+1
    4.在服务端接收到ACK后,
      客户端的 seq#(序列号):P,ack#(应答号):Q+1
      服务端的 seq#(序列号):Q,ack#(应答号):P+1
    在三次握手最后的状态必须被我们的方案所复制到,即使两个端点假定为客户的角色。在每个方案的最后,各个端点的应答号必须是大于他们的伙伴的序列号。我们的方案完成这个协调。
5.3 低的TTL值确保
    我们的方案某些是依赖于设置一个TCP包的TTL值,因此包将离开端点的内部网络,但没到达伙伴的NAT。对不同的网络这个值将不同,因此它必须能被动态确定。
    为了确定伙伴距离有多远,一个端点可以使用典型的路由追踪方法。就是,发送从1开始而不断增加的TTL值的SYN包。当TTL失效时各个包将导致ICMP TTL过期包被发回到端点。通过分析返回的ICMP TTL过期包可以为连接中低的TTL值确定一个保险值。
    许多NAT不将ICMP TTL过期包发回内部主机,所以一个端点可以议定当一个ICMP TTL过期包没有被返回时,用一个TTL值来引发一个包离开内部网。
    同样地,在NAT返回ICMP TTL过期包,通过分析伙伴的NAT的消息,端点必须以发现的保险的TTL值为基础。如果伙伴的NAT产生一个RST包,则端点可以使用一个比所产生的RST包小1的TTL值。如果端点没有得到RST包但开始停止接收ICMP TTL过期包,则可以确定伙伴的NAT用了抛弃不请自来的消息而没有响应的保险行为。事实上,这种情况和端点的NAT没有返回ICMP TTL过期包是一样的。
    这个保险TTL值的确定不需要任何其它端点的参与。因此,它可以在保险低的TTL值被用于连接之前就被确定。
5.4 情况1:<可预测的,可预测的,LSR>
                A               X               B
            |-------1a----->|<------1b------ |
            |<------2a------|-------2b------>|
            |-------2b------|---------------->| 
            |<--------------|-------3a--------|
            |-------4a----->|<------4b-------|
     图2:情况1
    我们使用符号“NA:4000→NB:5000,选项/负荷”来表示在英特网上从NA到NB所传送的包内容。这符号意味着包有一个NA的IP源地址,源端口4000,目的地址NB的IP地址,和目的端口5000。此外,在目的端口后是其它的任何重要选项或负荷值。选项包含LSR:X、SYN:P、ACK:Q和SYN+ACK:R,S。LSR:X表明包将可松散源路由到X。SYN:P,ACK:Q表明跟随的序列号或者应答号的TCP包的类型。SYN+ACK:P,Q+1表明包是一个序列号为P和应答号为Q+1的TCP SYN+ACK包。首先我们展示情况1<可预测的,可预测的,LSR>,其中所使用的事件序号在图2中能找得到。
    1.A和B个发送一个可松散源路由的SYN包穿过协助者X到对方
      (a)NA:4000→NB:5000,LSR:X,SYN:P
      (b)NB:5000→NA:4000,LSR:X,SYN:Q
      这两个SYN包由TCP的connect()函数调用产生。SYN在NAT NA和NB上都创建了预期的映射。在NA上的映射将允许后面被转播向A而来自NB:5000的通信并签准。
    2.X缓存两个包并向A和B各自发了对方所用的ISN
      (a)X:1234→NA:3999,B刚用的ISN Q
      (b)X:1235→NB:4999,A刚用的ISN P
      各个端点都需要他们的伙伴的ISN,这样他们才能构造出一个合法的SYN+ACK包。
    3.A和B各向对方发送一个SYN+ACK包
      (a)NB:5000→NA:4000,LSR:X,SYN+ACK:Q,P+1
      (b)NA:4000→NB:5000,LSR:X,SYN+ACK:P,Q+1
      这两个SYN+ACK包是由运行于各个端点的独立线程所产生的。A和B重新使用了原来的序列号P和Q作为在SYN+ACK中的序列号,并保证了所复制的序列号和应答号的最后状态和5.2节讨论的真实的TCP连接一样。
    4.A和B各向对方发送一个ACK包
      (a)NA:4000→NB:5000,LSR:X,ACK:Q+1
      (b)NB:5000→NA:4000,LSR:X,ACK:P+1
      一旦欺骗的SYN+ACK包被接收到,TCP堆栈将自动地为我们做这一步。
    5.X抛弃两个到达的ACK包,因为没有任何一个端点期望接收到这个ACK。
    图2假定了A和B知道他们的伙伴将使用哪个端口来工作;由于各个端点和它的伙伴都必须提前知道对方的信息,所以这一假设是合理的。首先第一步,X必须完成对A和B的端口预测,因此X能预测到NAT设备选择的端口。A必须知道NB工作于5000端口,同时B必须知道NA工作于4000端口。为简单化,可以假定X自己没有在NAT后面,但唯一的假定前提是X必须预先直接连接到A和B。
    情况1存在一种变种的方案。X可以发送在第2和第3步所需的SYN+ACK欺骗包,这似乎好于发送信息到A和B以使他们自己能够伪造SYN+ACK包。我们选择目前的方法是因为如果X发送SYN+ACK欺骗包,他们将被路由器抛弃的总比通过的多。此外,发送从X到A和B的SYN+ACK伪造包需要超级用户权限。而A和B为了其它的目的必须且已经运行于超级用户权限。
    在我们的技术中,从步骤2到5是这样考虑的,我们将申明函数Case1(integer extPortA, integer extPortB)作为执行步骤2到5,把参数extPortA和extPortB各自取代外部端口4000和5000。
5.5 情况2:<可预测的,可预测的,no LSR>
                   A               X               B
             |-------2a----->|<------2b------|
             |-------3a----->|<------3b------|
             |<------4a------|-------4b----->|
             |-------5b------|-------------->|
             |<--------------|-------5a------|
             |-------6a----->|<------6b------|
     图3:情况2
    情况1依靠于可用的松散源路由。许多路由器目前设置了预防松散源路由,同时将具有代表性地抛弃请求服务的包。同样地,依靠于松散源路由的技术在实际中将有很高的概率会失败。如果松散源路由不可利用,SYN序列号可以利用一个外出通道(他们预先连接到X的连接)和X通信,而不是物理性地让X看到包。注意图2的步骤2,X知道TCP序列号P和Q,因为X正确地接收到两个SYN包。没了LSR就不是这种情况了。
    为了初始连接,每端主机发送一个初始的SYN包到他们的伙伴,虽然他们知道将不能到达目的地。他们这时嗅探包离开网络,记录序列号,并报告这个信息给X。X需要这些包的TCP序列号,如此它将能够转播这些信息回到A和B,那样他们就能够产生SYN+ACK包。两条路线所发的包都不会到达他们所标明地址的目的地。
    更简单的方案是每个端点各发送一个不被考虑的SYN到他们伙伴。在接收端适当的设置NAT和防火墙,他们将由于没有映射存在而不会向前发送这些包到内部网络主机。一些NAT和一些防火墙将发送TCP重置包(RST)到这个不请自来的包的源地址。如果NAT产生RST包,A和B不能简单的像图2里步骤1的建议一样发送一个SYN包到对方,因为在接受到RST,NA和NB将终止所建立的洞。如果NAT没有产生RST包,打开的TCP连接将不会突然终止。
    另一种确保SYN包将不会到达它的目的网络的方法是发送带有低于伙伴的NAT路径长度的TTL值的SYN包。这包将会在去目的地的路上被明确地抛弃,并且TCP RST包将不会被任何发送者看到。更确切地说,一个ICMP过期包将被看到,并且产生一个问题,因为ICMP过期包突然地终止一个TCP连接。然而,如果使用者能够设置他们本地的防火墙抛弃ICMP包或者如果NAT不会发送ICMP消息包到它的内部网络,TCP连接的尝试将不会突然终止。
    一个方案不能包括简单的欺骗源地址的SYN包,使发送者不会接收到ICMP包或者RST包。这样做会在中间件创建一个残废的映射。中间件在看到一个SYN包时,将创建一个从内部IP地址和端口到外部IP和端口的映射。然而,由于一个欺骗的SYN包有一个错误的源IP地址,映射将不会对应到内部网络的主机。此外,一个方案不能包括把TTL设置低到连中间件都看不到SYN包,因为这样做不会创建我们需要的允许外部后面发到内网的通信的映射。
    假定一个<可预测的,可预测的,no LSR>环境,这种连接就像我们现在在图3所描述的。
    1.X做了关于5.1节描述的端口预测。X预测到NA的下一端口是4000并预测到NB的下一端口是5000,并经由已经存在的连接告知A和B。
    2.A和B各自发送一个SYN包到对方,他们知道这个包将会被对方的NAT抛弃或者由于TTL过期而被抛弃。
      (a)NA:4000→NB:5000,SYN:P
      (b)NB:5000→NA:4000,SYN:Q
      这一点是在各个端点的真实TCP的connect()函数调用所产生的。SYN包由TCP堆栈产生。这在NAT上创建了允许后面的来自伙伴IP地址和端口的通信通过并到达端点的映射。
    3.A和B各自发送他们自己所留意的ISN(P和Q)到X。
      (a)NA:3999→X:1234,刚使用的ISN号P
      (b)NB:4999→X:1235,刚使用的ISN号Q
      各个端点都需要它的伙伴的ISN,如此他们才能够构造合法的SYN+ACK包从而发到他们的伙伴。
    4.X发送A和B的对方各自所留意的ISN到A和B
      (a)X:1234→NA:3999,B刚使用的ISN号Q
      (b)X:1235→NB:4999,A刚使用的ISN号P
    5.A和B各自发送一个SYN+ACK包到对方
      (a)NB:5000→NA:4000,SYN+ACK:Q,P+1
      (b)NA:4000→NB:5000,SYN+ACK:P,Q+1
      这是三次握手的第二部分。此外,A和B重用了他们原来的序列号P和Q作为在SYN+ACK中的序列号,并保证了所复制的序列号和应答号的最后状态和5.2节讨论的真实的TCP连接一样。
    6.A和B各自发送一个ACK包到对方,他们知道这包将会被对方的NAT抛弃或者由于TTL过期而被抛弃。
      (a)NA:4000→NB:5000,ACK:Q+1
      (b)NB:5000→NA:4000,ACK:P+1
      TCP堆栈将自动地为我们发送这些ACK包来完成三次握手。我们不想ACK包到达他们的目的地,由于没有任何一方在等待ACK包。
    作为步骤4到5而和情况1很相似的另一种方法,是X可以欺骗A和B所需的SYN+ACK包。然而,我们为了某些类似在情况1中的原因选择了目前的方法。
    由于步骤2到6在我们的技术中是可重复利用的,所以我们做了函数Case2(integer extPortA, integer extPortB)作为执行步骤2到6,用参数extPortA和extPortB分别替代了外部端口4000和5000。
5.6 情况3:<随机的,可预测的,LSR>
    A               X               B
    |-------2a----->|               |
    |               |-------2b----->|
    |               |<------2c------|
    |          Case1|(m,5000)   |
     图4:情况3
    情况3<随机的,可预测的,LSR>类似于图2所描述的情况1。然而,X不能够预测到两个NAT中的一个的端口,比方说NA。A首先不得不发送它的SYN包来允许X查看NA所选的端口。X将报告这一信息给B,因此B能够发送它的SYN包到正确的目标IP地址和端口。这个情况1的修改版在图4中描述并解释如下。
    1.X做了关于5.1节描述的端口预测。X不能预测到NA的下一端口,但能够预测到NB的下一端口是5000,并经由已经存在的连接告知A和B。
    2.A和B同步经由X
      (a)NA:m→NB:5000,LSR:X,SYN:P
      (b)X让B知道NA工作于端口m
      (c)NB:5000→NA:m,LSR:X,SYN:Q
      这两个SYN包是由TCP的connect()函数调用所产生。
      这两个SYN在NAT的NA和NB上各自创建了预期的映射。
    3.调用Case1(m,5000)
5.7 情况4:<随机的,可预测的,no LSR>
    A               X               B
    |-------2a----->|               |
    |-------------->|               |
    |-------------->|               |
    |┆             |               |
    |-------------->|-------3------>|
    |<--------------|-------4-------|
    |<--------------|---------------|
    |<--------------|---------------|
    |               |┆             |
    |<--------------|---------------|
    |-------5------>|               |
    |               |-------6------>|
    |          Case2|(m,5000)       |
     图5:情况4
    情况4的环境是<随机的,可预测的,no LSR>。我们已经为这个依靠于随机的且不拒绝一个带了残废的且对应于在NAT后主机在前面的初始连接的ACK或者校验和域的TCP包的NAT环境开发了一个方案。这方案被呈现在图5中并解释如下。
    1.X做了关于5.1节描述的端口预测。X不能预测到NA的下一端口,但能预测到NB的下一端口是5000,并经由已经存在的连接告知A和B。
    2.A发送T个SYN包到B,这些包不是被对方的NAT所抛弃就是由于TTL过期而被抛弃。
      i = 0
      While i < T
          NA:rand →NB:5000, SYN:anything
          i = i+1
      End While
      这会在NAT NA上创建T个映射,B将最后用SYN+ACK猜到的就是其中一个。
    3.X通知B开始发送SYN+ACK包到NA
    4.B发送许多SYN+ACK包到NA直到有一个到达A
      i = 1024
      While A 还没报告成功
          NB:5000 →NA:i,
          SYN+ACK:,anything,anything, Payload:i
          i = i+1
      End While
    5.A报告穿过NAT的包负荷。
      NA:3999→X:1234,工作于端口m
      A通过监听来自NB的任何SYN+ACK包的数据报,将可看到这个残废的SYN+ACK包。
    6.X告诉B连接到A的端口m
      B现在知道SYN包可以发向哪里。
    7.调用Case2(m,5000)
    在步骤2中A所发送的T个SYN包是独立于任何TCP的connect()函数调用。他们仅仅是使用在NAT上创建了T个映射的libnet库所产生的包。另一方面,Case2调用的步骤2所产生的SYN包是由A和B的TCP的connect()函数调用所产生的。这情况4环境的方案依靠于随机生成端口的NAT的行为。这方案依靠于拒绝带有错误的如序列号或者校验和域的TCP包的中间件。
    T值能够被选择,像B在生成T个SYN+ACK到随机目的端口号后有95%的机会猜到一个正确的外部端口。其实,A的NAT随机地选择T的数目(它的外部端口数),这时B必须一直维持猜测直到在A的NAT可选集中B选择到了一个。我们能够使用一个概率分析来构造一个高效的且最小工作量被A和B所预算的设想。设PrG是B猜到在T个实验中最少一个是正确的概率。假设A的NAT已经在[1025,65535]的范围中选择了T个不同的端口号,如果B选择T个不同的端口号,在A的NAT可选集中B不能选择到一个号的可能性是
    Pr_G =n-T/n * n-1-T/n-1 * n-2-T/n-2 * . . . * n-(T -1)-T/n-(T -1)
其中n是可能选择的端口数(n=65535-1024=64511)。
         T-1
    Pr_G =∏n-i-T/n-i
         i=0
反之,在T个实验中猜到一个是正确的可能性是
    PrG = 1-Pr_G
像之前的规定,T可能被选择如
    PrG > 95%
      T-1
    1-∏n-i-T/n-i> 95%
      i=0
计算T的量为T=439的这个乘积。
      439-1
    1-∏64511-i-439/64511-i=0.9506> 95%
      i=0
这结果说明如果A发送439个使在A的NAT外部端口得到不同的随机的映射的SYN包,并且B发送许多不同的随机的到目的端口的SYN+ACK包,B在它发送第440个AYN+ACK包之前就有大于95%的机会正确地猜测到439个外部端口映射的其中一个。
    仅仅发送T个SYN包的原因是这至少要使用两个资源,第一个是网络带宽的使用量,第二是在NAT上创建映射的数量。
5.8 情况5:<随机的,随机的,LSR>
┌─────┐   ┌─────┐
│NA端口了解|    │NB端口了解|
└─────┘   └─────┘
    │      ↑  ↑     │
    │       /  /      │
    │        //       │
    │        //       │
    ↓       /  /      ↓
┌─────┐   ┌─────┐
│    A     │   │    B     │
└─────┘   └─────┘
 图6:资源图表死锁
A               X               B
|-------2a----->|<------2b------|
|<------2c------|-------2c----->|
|-------3a----->|<------3b------|
|          Case1|(m,n)          |
     图7:情况5
    在情况5中的环境是<随机的,随机的,LSR>。为了允许X同步到A和B,B必须要知道NA预先发送它的SYN包时所选的端口号。为了确定NA选择的端口,X不得不看A的SYN包。A的SYN包不能被发送直到X确定NB所选的端口号为止。这个死锁被制成图6的插图。A控制NA端口资源以致不外发SYN包,有效的预防了X知道NA所选的端口号。同样的,B也控制NB端口资源。在他们能够释放所控制资源之前,每端都需要对方的端口。我们让A和B各发送两个SYN包可松散源路由穿过X且不连接到TCP connect()调用的方案来预防这个死锁。这两个SYN包在各自的NAT上创建了所需的映射并允许X获得两个资源,同时等同于情况1和2的类似方式连接。我们情况5的方案展示在图7中并解释如下。
    1.X做了关于5.1节描述的端口预测。X不能预测到NA或者NB的下一端口并经由已经存在的连接告知A和B。
    2.A和B都发送一个SYN包松散源路由穿过X
     (a)NA:m→NB:anything SYN:anything,LSR:X
     (b)NB:n→NA:anything SYN:anything,LSR:X
     (c)X报告m给B并报告n给A。
      这两个SYN将在各自的NAT上创建所需的映射。
    3.A和B发送一个SYN到对方松散源路由穿过X
      (a)NA:m→NB:n,LSR:X,SYN:P
      (b)NB:n→NA:m,LSR:X,SYN:Q
      因为一致转换,即使目标端口和之前的步骤有所不同,NAT仍然为这个包利用所使用的相同映射(因此相同的外部端口号)。
    4.调用Case1(m,n)
    注意,SYN包发送步骤2不是关联到任何TCP的connect()函数调用,而更合适的,步骤3的SYN包发送应归于一个TCP的connect()函数调用。同理,Case1的调用中SYN+ACK包发送步骤3不绑定到TCP的accept()函数子程序。
5.9 情况6:<随机的,随机的,no LSR>
A               X               B
|-------2------>|<------2-------|
|-------------->|<--------------|
|-------------->|<--------------|
|┆             |┆             |
|-------------->|<--------------|
|-------3-------|-------------->|
|<--------------|-------3-------|
|---------------|-------------->|
|<--------------|---------------|
|---------------|-------------->|
|<--------------|---------------|
|┆             |┆             |
|---------------|-------------->|
|<--------------|---------------|
|-------4------>|<------4-------|
|<------5-------|-------5------>|
|          Case2|(m,n)          |
     图8:情况6
    情况6的环境是<随机的,随机的,no LSR>。回顾图6的资源图表死锁,A和B从包不可松散源路由保存端口的资源信息。这情况的方案被画成图8并解释如下。
    1.X做了关于5.1节述的端口预测。X不能预测到NA或者NB的下一端口并经由已经存在的连接告知A和B。
    2.A发送T个SYN包到B同时B发送T个SYN包到A,这些包将被在另一端的NAT抛弃或者由于TTL到期而被抛弃。
      i=0
      While i<T
          NA:rand→NB:rand,SYN:anything
          NB:rand→NA:rand,SYN:anything
          i=i+1
      End While
    这些SYN包在两边的NAT上各创建了T个映射。
    3.B和A发送许多SYN+ACK包到他们的伙伴的NAT直到有一个到达他们的伙伴那里。
      i=1024
      While A 还没报告成功
          NB:rand→NA:i
          SYN+ACK:,anything,anything,Payload:i
          NA:rand→NB:i
          SYN+ACK:,anything,anything,Payload:i
          i=i+1
      End While
    4.A和B报告通过NAT的包负荷。
      NA:3999→X:1234,工作于端口m
      NB:4999→X:1235,工作于端口n
    5.X告诉B连接到A的端口m并告诉A连接到B的端口n。
      A和B现在知道他们的伙伴的外部端口号。
    6.调用Case2(m,n)
    情况6远比情况4困难,因为各个端点必须在对方的NAT上正确地猜到一个完整的映射〈源端口,目的端口〉。在情况4中,在非随机的NAT后的端点能够正确地猜到目的端口。当其中一个NAT是可预测时,源端口就被固定了。情况6的搜索空间是情况4的平方,替代64511种可能的是4161669121种结合需要猜测。
//------Translated by weljin.QQ:15038084.MSN:weljin@hotmail.com.G-mail:weljin.china@gmail.com.E-mail:weljin@163.com.Q-mail:weljin@qq.com.------//
//------第一次修改,以后修改将加入测试代码,以上内容由weljin翻译,更新将放在 http://15038084.qzone.qq.com http://blogs.impx.net/dragonimp/archive/2004/10/20/487.html,转载请保持来源的完整性------//
6. 实现
    我们已经在LINUX工作站,通过调用libnet和libpcap使用C实现了第2种和第4种情况。第1、3、5、6种情况没有实现。
    协助者和端点都连接到由单独功能组成的库。协助者程序natblaster_server()只需提供其协助者所要监听的端口号。端点连接程序需要提供7个参数:(1)协助者的IP地址和(2)端口号,(3)本地端点外部IP地址,(4)内部IP地址,和(5)端口号,(6)伙伴外部IP地址,和(7)端口号。本地端点和伙伴的端口号必须由协助者帮助为试图的连接建立一个唯一标志符。三元组<本地外部IP,伙伴内部IP,伙伴内部端口>被用于在协助者上的唯一标志。库试图提供一个指定端口的套接字,然而,所返回的套接字不被保证是所指定的端口号。假设natblaster_connect()工作,库返回一个有效的套接字句柄。
    为了测试我们的实现,我们在分离的网络上运行两个端点,每个都位于不同的且有效的NAT后面。第三部分的程序被运行于不在NAT之后的第三部计算机上。我们在英特网上做了比本地网络更多的测试,来确保测试更真实。
    为了创建不会到达伙伴那里并不返回错误消息的包,我们设置了TTL值使其低到不会到达伙伴那里。设置低的TTL值由调用IP_TTL选项的setsockopt()系统调用完成。这选项也需要一个TTL值。这个值必须低于到伙伴的跳跃数,但必须要大于到外部最远的NAT的跳跃数。这套接字选项对于套接字的整个生存期来说必须不是持久不变的。例如,在三次握手成功之后,setsockopt()应该被再一次调用来提高TTL值,这样后面的数据才确保能到达端点。依靠的是一个低的TTL只工作于是否一个ICMP TTL过期包不被端点的TCP堆栈看到,因为这过期包会导致在端点的套接字失败。我们所测试的NAT都不向前发送ICMP TTL过期包到内部网络。另一种方法是将发送普通包并希望伙伴的NAT默默地丢弃它们,然而,一些NAT可能发送RST包来响应那些未请自来的数据。这行为是详细的执行过程。我们没使用5.3节所描述的TTL决定技术;而是我们选择了我们知道是合适的并且低而普通的TTL值。
    我们为连接前诊断实现了连续的端口分配方法,但没有实现一致转换。我们的实现没有使用一致转换。
    我们的情况2和情况4的实现都非常成功并且能打开直接的TCP连接。情况2真实的打开了连接,而情况4有很高的几率是成功的(就是上面所讨论的,成功率取决于在预测相位的端口发送SYN和SYNACK的数量)。
    由于5.9节最后所给的原因,我们没有实现情况6。我们没实现情况1,3和5是因为LSR在英特网上不是典型地可用的并且我们相信这在实践中会有较低的成功率。
    如前面所论述的,我们使用了增加系统调用所需的标准的Berkelet网络实现。例如,当我们发送一个SYN包但需要知道序列号时,这个SYN包由使用标准的connect()调用所发送的,之后首先开始一个线程来为所发的SYN包观测数据报。这线程能报告所使用的序列号。
    情况2和情况4必须有root的执行权,因为这是libnet和libpcap所要求的。由于无需欺骗或者探嗅,第三方可以以一般用户的权限运行。
7. 总结
    我们已经证明了如何在两个不同的典型的NAT后面的主机之间建立直接的TCP连接,这些解决方案都没有以任何方式改变TCP/IP栈,而是为这些主机建立连接起到了杠杆作用。我们的方案可以为很多的程序所应用,从点对点的通信到即时消息通信。对于这个问题已经存在解决方法包括代理都没有有效地利用网络资源并且伸缩性不好。
    随着IPv6的到来,NAT也许就不再是个网站的组成了,但是,这些情况包括可预测NAT也可以被应用到使用状态的防火墙后面的主机。跟NAT相似,“状态防火墙”有能力只允许从他们所包含的内部网络发起的连接。我们的解决方案双方都可以初始话一个这些防火墙都允许的TCP连接。我们的几种方案在配置有IDSes的场合是不可取的,因为在第四和第六种情况下,都使用了类似于端口扫描的技术。这种情况下最好关闭这样的网络监视设备。尽管如此,我们地解决方案对于大部分的网络来说一般是足够的,甚至对于那些可能都不存在的使用随机分配地址的NAT来说也是适用的。
8. 参考文献
[1] Bryan Ford. NatCheck: Hosted by the MIDCOM-P2P
    project on SourceForge.
     http://midcom-p2p.sourceforge.net.
[2] Bryan Ford, Pyda Srisuresh, and Dan Kegel. Peer-to-Peer
    Communication Across Network Address Translators. In
    USENIX Annual Technical Conference, Anaheim, CA, April
    2005.
[3] Groove Networks.  http://groove.net.
[4] Saikat Guha, Yutaka Takeday, and Paul Francis. NUTSS: A
    SIP-based approach to UDP and TCP Network Connectivity.
    In SIGCOMM 2004 Workshops, Aug 2004.
[5] M. Holdrege and P. Srisuresh. Protocol Complications with
    the IP Network Address Translator. RFC 3027, Internet
    Engineering Task Force, January 2001.
[6] Hopster: Bypass Firewall Bypass Proxy Software.
     http://www.hopster.com.
[7] Information Sciences Institute. Transmission Control
    Protocol (TCP). RFC 793, Internet Engineering Task Force,
    September 1981.
[8] Brad Karp, Sylvia Ratnasamy, Sean Rhea, and Scott
    Shenker. Spurring Adoption of DHTs with OpenHash, a
    Public DHT Service. In Proceedings of the 3rd International
    Workshop on Peer-to-Peer Systems, Feb 2004.
[9] Y. Rekhter, B. Moskowitz, D. Karrenberg, G. J. de Groot,
    and E. Lear. Address Allocation for Private Internets. RFC
    1918, Internet Engineering Task Force, February 1996.
[10] J. Rosenberg, J. Weinberger, C. Huitema, and R. Mahy.
     STUN - Simple Traversal of User Datagram Protocol (UDP).
     RFC 3489, Internet Engineering Task Force, September
     2003.
[11] P. Srisuresh and K. Egevang. Traditional IP Network
     Address Translator (Traditional NAT). RFC 3022, Internet
     Engineering Task Force, January 2001.
[12] P. Srisuresh and M. Holdrege. IP Network Address
     Translator (NAT) Terminology and Considerations. RFC
     2663, Internet Engineering Task Force, August 1999.
[13] P. Srisuresh, J. Kuthan, J. Rosenberg, A. Molitor, and
     A. Rayhan. Middlebox communication architecture and
     framework. RFC 3303, Internet Engineering Task Force,
     August 2002.
[14] Jason Thomas, Andrew Mickish, and Susheel Daswani. Push
     Proxy: Protocol Document 0.6, June 2003.
[15] Michael Walfish, Jeremy Stribling, Maxwell Krohn, Hari
     Balakrishnan, Robert Morris, and Scott Shenker.
     Middleboxes No Longer Considered Harmful. In
     Proceedings of USENIX Symposium on Operating Systems
     Design and Implementation, December 2004.


===============================================================================================

P2P之UDP穿透NAT的原理与实现(附源代码)


转自http://www.ppcn.net/n1306c2.aspx

 

P2P 之 UDP穿透NAT的原理与实现(附源代码)
原创:shootingstars
参考:http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt

 

论坛上经常有对P2P原理的讨论,但是讨论归讨论,很少有实质的东西产生(源代码)。呵呵,在这里我就用自己实现的一个源代码来说明UDP穿越NAT的原理。

首先先介绍一些基本概念:
    NAT(Network Address Translators),网络地址转换:网络地址转换是在IP地址日益缺乏的情况下产生的,它的主要目的就是为了能够地址重用。NAT分为两大类,基本的NAT和NAPT(Network Address/Port Translator)。
    最开始NAT是运行在路由器上的一个功能模块。
    
    最先提出的是基本的NAT,它的产生基于如下事实:一个私有网络(域)中的节点中只有很少的节点需要与外网连接(呵呵,这是在上世纪90年代中期提出的)。那么这个子网中其实只有少数的节点需要全球唯一的IP地址,其他的节点的IP地址应该是可以重用的。
    因此,基本的NAT实现的功能很简单,在子网内使用一个保留的IP子网段,这些IP对外是不可见的。子网内只有少数一些IP地址可以对应到真正全球唯一的IP地址。如果这些节点需要访问外部网络,那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP然后发送出去。(基本的NAT会改变IP包中的原IP地址,但是不会改变IP包中的端口)
    关于基本的NAT可以参看RFC 1631
    
    另外一种NAT叫做NAPT,从名称上我们也可以看得出,NAPT不但会改变经过这个NAT设备的IP数据报的IP地址,还会改变IP数据报的TCP/UDP端口。基本NAT的设备可能我们见的不多(呵呵,我没有见到过),NAPT才是我们真正讨论的主角。看下图:
                                                Server S1                         
                                          18.181.0.31:1235                          
                                                         |
          ^  Session 1 (A-S1)  ^            |  
          |  18.181.0.31:1235  |              |   
          v 155.99.25.11:62000 v         |    
                                                         |
                                                     NAT
                                               155.99.25.11
                                                         |
          ^  Session 1 (A-S1)  ^            |  
          |  18.181.0.31:1235  |              |  
          v   10.0.0.1:1234    v              |  
                                                         |
                                                  Client A
                                               10.0.0.1:1234


    有一个私有网络10.*.*.*,Client A是其中的一台计算机,这个网络的网关(一个NAT设备)的外网IP是155.99.25.11(应该还有一个内网的IP地址,比如10.0.0.10)。如果Client A中的某个进程(这个进程创建了一个UDP Socket,这个Socket绑定1234端口)想访问外网主机18.181.0.31的1235端口,那么当数据包通过NAT时会发生什么事情呢?
    首先NAT会改变这个数据包的原IP地址,改为155.99.25.11。接着NAT会为这个传输创建一个Session(Session是一个抽象的概念,如果是TCP,也许Session是由一个SYN包开始,以一个FIN包结束。而UDP呢,以这个IP的这个端口的第一个UDP开始,结束呢,呵呵,也许是几分钟,也许是几小时,这要看具体的实现了)并且给这个Session分配一个端口,比如62000,然后改变这个数据包的源端口为62000。所以本来是(10.0.0.1:1234->18.181.0.31:1235)的数据包到了互联网上变为了(155.99.25.11:62000->18.181.0.31:1235)。
    一旦NAT创建了一个Session后,NAT会记住62000端口对应的是10.0.0.1的1234端口,以后从18.181.0.31发送到62000端口的数据会被NAT自动的转发到10.0.0.1上。(注意:这里是说18.181.0.31发送到62000端口的数据会被转发,其他的IP发送到这个端口的数据将被NAT抛弃)这样Client A就与Server S1建立以了一个连接。

    呵呵,上面的基础知识可能很多人都知道了,那么下面是关键的部分了。
    看看下面的情况:
    Server S1                                                                             Server S2
 18.181.0.31:1235                                                          138.76.29.7:1235
        |                                                                                             |
        |                                                                                             |
        +---------------------------------+---------------------------------+
                                                       |
   ^  Session 1 (A-S1)  ^                 |      ^  Session 2 (A-S2)  ^
   |  18.181.0.31:1235  |                   |      |  138.76.29.7:1235  |
   v 155.99.25.11:62000 v              |      v 155.99.25.11:62000 v
                                                       |
                                              Cone NAT
                                            155.99.25.11
                                                       | 
   ^  Session 1 (A-S1)  ^                 |      ^  Session 2 (A-S2)  ^
   |  18.181.0.31:1235  |                   |      |  138.76.29.7:1235  |
   v   10.0.0.1:1234    v                   |      v   10.0.0.1:1234    v
                                                       |
                                                Client A
                                           10.0.0.1:1234

    接上面的例子,如果Client A的原来那个Socket(绑定了1234端口的那个UDP Socket)又接着向另外一个Server S2发送了一个UDP包,那么这个UDP包在通过NAT时会怎么样呢?
    这时可能会有两种情况发生,一种是NAT再次创建一个Session,并且再次为这个Session分配一个端口号(比如:62001)。另外一种是NAT再次创建一个Session,但是不会新分配一个端口号,而是用原来分配的端口号62000。前一种NAT叫做Symmetric NAT,后一种叫做Cone NAT。我们期望我们的NAT是第二种,呵呵,如果你的NAT刚好是第一种,那么很可能会有很多P2P软件失灵。(可以庆幸的是,现在绝大多数的NAT属于后者,即Cone NAT)
   
    好了,我们看到,通过NAT,子网内的计算机向外连结是很容易的(NAT相当于透明的,子网内的和外网的计算机不用知道NAT的情况)。
    但是如果外部的计算机想访问子网内的计算机就比较困难了(而这正是P2P所需要的)。
    那么我们如果想从外部发送一个数据报给内网的计算机有什么办法呢?首先,我们必须在内网的NAT上打上一个“洞”(也就是前面我们说的在NAT上建立一个Session),这个洞不能由外部来打,只能由内网内的主机来打。而且这个洞是有方向的,比如从内部某台主机(比如:192.168.0.10)向外部的某个IP(比如:219.237.60.1)发送一个UDP包,那么就在这个内网的NAT设备上打了一个方向为219.237.60.1的“洞”,(这就是称为UDP Hole Punching的技术)以后219.237.60.1就可以通过这个洞与内网的192.168.0.10联系了。(但是其他的IP不能利用这个洞)。

呵呵,现在该轮到我们的正题P2P了。有了上面的理论,实现两个内网的主机通讯就差最后一步了:那就是鸡生蛋还是蛋生鸡的问题了,两边都无法主动发出连接请求,谁也不知道谁的公网地址,那我们如何来打这个洞呢?我们需要一个中间人来联系这两个内网主机。
    现在我们来看看一个P2P软件的流程,以下图为例:

                                       Server S (219.237.60.1)
                                                      |
                                                      |
   +------------------------------------+------------------------------------+
   |                                                                                                     |
 NAT A (外网IP:202.187.45.3)                                    NAT B (外网IP:187.34.1.56)
   |   (内网IP:192.168.0.1)                                                               | (内网IP:192.168.0.1)
   |                                                                                                    |
Client A  (192.168.0.20:4000)                                       Client B (192.168.0.10:40000)

 

    首先,Client A登录服务器,NAT A为这次的Session分配了一个端口60000,那么Server S收到的Client A的地址是202.187.45.3:60000,这就是Client A的外网地址了。同样,Client B登录Server S,NAT B给此次Session分配的端口是40000,那么Server S收到的B的地址是187.34.1.56:40000。
    此时,Client A与Client B都可以与Server S通信了。如果Client A此时想直接发送信息给Client B,那么他可以从Server S那儿获得B的公网地址187.34.1.56:40000,是不是Client A向这个地址发送信息Client B就能收到了呢?答案是不行,因为如果这样发送信息,NAT B会将这个信息丢弃(因为这样的信息是不请自来的,为了安全,大多数NAT都会执行丢弃动作)。现在我们需要的是在NAT B上打一个方向为202.187.45.3(即Client A的外网地址)的洞,那么Client A发送到187.34.1.56:40000的信息,Client B就能收到了。这个打洞命令由谁来发呢,呵呵,当然是Server S。
    总结一下这个过程:如果Client A想向Client B发送信息,那么Client A发送命令给Server S,请求Server S命令Client B向Client A方向打洞。呵呵,是不是很绕口,不过没关系,想一想就很清楚了,何况还有源代码呢(侯老师说过:在源代码面前没有秘密 8)),然后Client A就可以通过Client B的外网地址与Client B通信了。
    
    注意:以上过程只适合于Cone NAT的情况,如果是Symmetric NAT,那么当Client B向Client A打洞的端口已经重新分配了,Client B将无法知道这个端口(如果Symmetric NAT的端口是顺序分配的,那么我们或许可以猜测这个端口号,可是由于可能导致失败的因素太多,我们不推荐这种猜测端口的方法)。
    
    下面是一个模拟P2P聊天的过程的源代码,过程很简单,P2PServer运行在一个拥有公网IP的计算机上,P2PClient运行在两个不同的NAT后(注意,如果两个客户端运行在一个NAT后,本程序很可能不能运行正常,这取决于你的NAT是否支持loopback translation,详见http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt,当然,此问题可以通过双方先尝试连接对方的内网IP来解决,但是这个代码只是为了验证原理,并没有处理这些问题),后登录的计算机可以获得先登录计算机的用户名,后登录的计算机通过send username message的格式来发送消息。如果发送成功,说明你已取得了直接与对方连接的成功。
    程序现在支持三个命令:send , getu , exit
    
    send格式:send username message
    功能:发送信息给username
    
    getu格式:getu
    功能:获得当前服务器用户列表
    
    exit格式:exit
    功能:注销与服务器的连接(服务器不会自动监测客户是否吊线)
        
    代码很短,相信很容易懂,如果有什么问题,可以给我发邮件zhouhuis22@sina.com  或者在CSDN上发送短消息。同时,欢迎转发此文,但希望保留作者版权8-)。
    
    最后感谢CSDN网友 PiggyXP 和 Seilfer的测试帮助

P2PServer.c

/* P2P 程序服务端
 * 
 * 文件名:P2PServer.c
 *
 * 日期:2004-5-21
 *
 * 作者:shootingstars(zhouhuis22@sina.com)
 *
 */
#pragma comment(lib, "ws2_32.lib")

#include "windows.h"
#include "../proto.h"
#include "../Exception.h"

UserList ClientList;

void InitWinSock()
{
 WSADATA wsaData;

 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
 {
  printf("Windows sockets 2.2 startup");
  throw Exception("");
 }
 else{
  printf("Using %s (Status: %s)/n",
   wsaData.szDescription, wsaData.szSystemStatus);
  printf("with API versions %d.%d to %d.%d/n/n",
   LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),
   LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));
  
 }
}

SOCKET mksock(int type)
{
 SOCKET sock = socket(AF_INET, type, 0);
 if (sock < 0)
 {
        printf("create socket error");
  throw Exception("");
 }
 return sock;
}

stUserListNode GetUser(char *username)
{
 for(UserList::iterator UserIterator=ClientList.begin();
      UserIterator!=ClientList.end();
       ++UserIterator)
 {
  if( strcmp( ((*UserIterator)->userName), username) == 0 )
   return *(*UserIterator);
 }
 throw Exception("not find this user");
}

int main(int argc, char* argv[])
{
 try{
  InitWinSock();
  
  SOCKET PrimaryUDP;
  PrimaryUDP = mksock(SOCK_DGRAM);

  sockaddr_in local;
  local.sin_family=AF_INET;
  local.sin_port= htons(SERVER_PORT); 
  local.sin_addr.s_addr = htonl(INADDR_ANY);
  int nResult=bind(PrimaryUDP,(sockaddr*)&local,sizeof(sockaddr));
  if(nResult==SOCKET_ERROR)
   throw Exception("bind error");

  sockaddr_in sender;
  stMessage recvbuf;
  memset(&recvbuf,0,sizeof(stMessage));

  // 开始主循环.
  // 主循环负责下面几件事情:
  // 一:读取客户端登陆和登出消息,记录客户列表
  // 二:转发客户p2p请求
  for(;;)
  {
   int dwSender = sizeof(sender);
   int ret = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(stMessage), 0, (sockaddr *)&sender, &dwSender);
   if(ret <= 0)
   {
    printf("recv error");
    continue;
   }
   else
   {
    int messageType = recvbuf.iMessageType;
    switch(messageType){
    case LOGIN:
     {
      //  将这个用户的信息记录到用户列表中
      printf("has a user login : %s/n", recvbuf.message.loginmember.userName);
      stUserListNode *currentuser = new stUserListNode();
      strcpy(currentuser->userName, recvbuf.message.loginmember.userName);
      currentuser->ip = ntohl(sender.sin_addr.S_un.S_addr);
      currentuser->port = ntohs(sender.sin_port);
      
      ClientList.push_back(currentuser);

      // 发送已经登陆的客户信息
      int nodecount = (int)ClientList.size();
      sendto(PrimaryUDP, (const char*)&nodecount, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));
      for(UserList::iterator UserIterator=ClientList.begin();
        UserIterator!=ClientList.end();
        ++UserIterator)
      {
       sendto(PrimaryUDP, (const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender)); 
      }

      break;
     }
    case LOGOUT:
     {
      // 将此客户信息删除
      printf("has a user logout : %s/n", recvbuf.message.logoutmember.userName);
      UserList::iterator removeiterator = NULL;
      for(UserList::iterator UserIterator=ClientList.begin();
       UserIterator!=ClientList.end();
       ++UserIterator)
      {
       if( strcmp( ((*UserIterator)->userName), recvbuf.message.logoutmember.userName) == 0 )
       {
        removeiterator = UserIterator;
        break;
       }
      }
      if(removeiterator != NULL)
       ClientList.remove(*removeiterator);
      break;
     }
    case P2PTRANS:
     {
      // 某个客户希望服务端向另外一个客户发送一个打洞消息
      printf("%s wants to p2p %s/n",inet_ntoa(sender.sin_addr),recvbuf.message.translatemessage.userName);
      stUserListNode node = GetUser(recvbuf.message.translatemessage.userName);
      sockaddr_in remote;
      remote.sin_family=AF_INET;
      remote.sin_port= htons(node.port); 
      remote.sin_addr.s_addr = htonl(node.ip);

      in_addr tmp;
      tmp.S_un.S_addr = htonl(node.ip);
      printf("the address is %s,and port is %d/n",inet_ntoa(tmp), node.port);

      stP2PMessage transMessage;
      transMessage.iMessageType = P2PSOMEONEWANTTOCALLYOU;
      transMessage.iStringLen = ntohl(sender.sin_addr.S_un.S_addr);
      transMessage.Port = ntohs(sender.sin_port);
                        
      sendto(PrimaryUDP,(const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr *)&remote, sizeof(remote));

      break;
     }
    
    case GETALLUSER:
     {
      int command = GETALLUSER;
      sendto(PrimaryUDP, (const char*)&command, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

      int nodecount = (int)ClientList.size();
      sendto(PrimaryUDP, (const char*)&nodecount, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

      for(UserList::iterator UserIterator=ClientList.begin();
        UserIterator!=ClientList.end();
        ++UserIterator)
      {
       sendto(PrimaryUDP, (const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender)); 
      }
      break;
     }
    }
   }
  }

 }
 catch(Exception &e)
 {
  printf(e.GetMessage());
  return 1;
 }

 return 0;
}

/* P2P 程序客户端
 * 
 * 文件名:P2PClient.c
 *
 * 日期:2004-5-21
 *
 * 作者:shootingstars(zhouhuis22@sina.com)
 *
 */

#pragma comment(lib,"ws2_32.lib")

#include "windows.h"
#include "../proto.h"
#include "../Exception.h"
#include <iostream>
using namespace std;

UserList ClientList;

 

#define COMMANDMAXC 256
#define MAXRETRY    5

SOCKET PrimaryUDP;
char UserName[10];
char ServerIP[20];

bool RecvedACK;

void InitWinSock()
{
 WSADATA wsaData;

 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
 {
  printf("Windows sockets 2.2 startup");
  throw Exception("");
 }
 else{
  printf("Using %s (Status: %s)/n",
   wsaData.szDescription, wsaData.szSystemStatus);
  printf("with API versions %d.%d to %d.%d/n/n",
   LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),
   LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));
 }
}

SOCKET mksock(int type)
{
 SOCKET sock = socket(AF_INET, type, 0);
 if (sock < 0)
 {
        printf("create socket error");
  throw Exception("");
 }
 return sock;
}

stUserListNode GetUser(char *username)
{
 for(UserList::iterator UserIterator=ClientList.begin();
      UserIterator!=ClientList.end();
       ++UserIterator)
 {
  if( strcmp( ((*UserIterator)->userName), username) == 0 )
   return *(*UserIterator);
 }
 throw Exception("not find this user");
}

void BindSock(SOCKET sock)
{
 sockaddr_in sin;
 sin.sin_addr.S_un.S_addr = INADDR_ANY;
 sin.sin_family = AF_INET;
 sin.sin_port = 0;
 
 if (bind(sock, (struct sockaddr*)&sin, sizeof(sin)) < 0)
  throw Exception("bind error");
}

void ConnectToServer(SOCKET sock,char *username, char *serverip)
{
 sockaddr_in remote;
 remote.sin_addr.S_un.S_addr = inet_addr(serverip);
 remote.sin_family = AF_INET;
 remote.sin_port = htons(SERVER_PORT);
 
 stMessage sendbuf;
 sendbuf.iMessageType = LOGIN;
 strncpy(sendbuf.message.loginmember.userName, username, 10);

 sendto(sock, (const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&remote,sizeof(remote));

 int usercount;
 int fromlen = sizeof(remote);
 int iread = recvfrom(sock, (char *)&usercount, sizeof(int), 0, (sockaddr *)&remote, &fromlen);
 if(iread<=0)
 {
  throw Exception("Login error/n");
 }

 // 登录到服务端后,接收服务端发来的已经登录的用户的信息
 cout<<"Have "<<usercount<<" users logined server:"<<endl;
 for(int i = 0;i<usercount;i++)
 {
  stUserListNode *node = new stUserListNode;
  recvfrom(sock, (char*)node, sizeof(stUserListNode), 0, (sockaddr *)&remote, &fromlen);
  ClientList.push_back(node);
  cout<<"Username:"<<node->userName<<endl;
  in_addr tmp;
  tmp.S_un.S_addr = htonl(node->ip);
  cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;
  cout<<"UserPort:"<<node->port<<endl;
  cout<<""<<endl;
 }
}

void OutputUsage()
{
 cout<<"You can input you command:/n"
  <<"Command Type:/"send/",/"exit/",/"getu/"/n"
  <<"Example : send Username Message/n"
  <<"          exit/n"
  <<"          getu/n"
  <<endl;
}

/* 这是主要的函数:发送一个消息给某个用户(C)
 *流程:直接向某个用户的外网IP发送消息,如果此前没有联系过
 *      那么此消息将无法发送,发送端等待超时。
 *      超时后,发送端将发送一个请求信息到服务端,
 *      要求服务端发送给客户C一个请求,请求C给本机发送打洞消息
 *      以上流程将重复MAXRETRY次
 */
bool SendMessageTo(char *UserName, char *Message)
{
 char realmessage[256];
 unsigned int UserIP;
 unsigned short UserPort;
 bool FindUser = false;
 for(UserList::iterator UserIterator=ClientList.begin();
      UserIterator!=ClientList.end();
      ++UserIterator)
 {
  if( strcmp( ((*UserIterator)->userName), UserName) == 0 )
  {
   UserIP = (*UserIterator)->ip;
   UserPort = (*UserIterator)->port;
   FindUser = true;
  }
 }

 if(!FindUser)
  return false;

 strcpy(realmessage, Message);
 for(int i=0;i<MAXRETRY;i++)
 {
  RecvedACK = false;

  sockaddr_in remote;
  remote.sin_addr.S_un.S_addr = htonl(UserIP);
  remote.sin_family = AF_INET;
  remote.sin_port = htons(UserPort);
  stP2PMessage MessageHead;
  MessageHead.iMessageType = P2PMESSAGE;
  MessageHead.iStringLen = (int)strlen(realmessage)+1;
  int isend = sendto(PrimaryUDP, (const char *)&MessageHead, sizeof(MessageHead), 0, (const sockaddr*)&remote, sizeof(remote));
  isend = sendto(PrimaryUDP, (const char *)&realmessage, MessageHead.iStringLen, 0, (const sockaddr*)&remote, sizeof(remote));
  
  // 等待接收线程将此标记修改
  for(int j=0;j<10;j++)
  {
   if(RecvedACK)
    return true;
   else
    Sleep(300);
  }

  // 没有接收到目标主机的回应,认为目标主机的端口映射没有
  // 打开,那么发送请求信息给服务器,要服务器告诉目标主机
  // 打开映射端口(UDP打洞)
  sockaddr_in server;
  server.sin_addr.S_un.S_addr = inet_addr(ServerIP);
  server.sin_family = AF_INET;
  server.sin_port = htons(SERVER_PORT);
 
  stMessage transMessage;
  transMessage.iMessageType = P2PTRANS;
  strcpy(transMessage.message.translatemessage.userName, UserName);

  sendto(PrimaryUDP, (const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr*)&server, sizeof(server));
  Sleep(100);// 等待对方先发送信息。
 }
 return false;
}

// 解析命令,暂时只有exit和send命令
// 新增getu命令,获取当前服务器的所有用户
void ParseCommand(char * CommandLine)
{
 if(strlen(CommandLine)<4)
  return;
 char Command[10];
 strncpy(Command, CommandLine, 4);
 Command[4]='/0';

 if(strcmp(Command,"exit")==0)
 {
  stMessage sendbuf;
  sendbuf.iMessageType = LOGOUT;
  strncpy(sendbuf.message.logoutmember.userName, UserName, 10);
  sockaddr_in server;
  server.sin_addr.S_un.S_addr = inet_addr(ServerIP);
  server.sin_family = AF_INET;
  server.sin_port = htons(SERVER_PORT);

  sendto(PrimaryUDP,(const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr *)&server, sizeof(server));
  shutdown(PrimaryUDP, 2);
  closesocket(PrimaryUDP);
  exit(0);
 }
 else if(strcmp(Command,"send")==0)
 {
  char sendname[20];
  char message[COMMANDMAXC];
  int i;
  for(i=5;;i++)
  {
   if(CommandLine[i]!=' ')
    sendname[i-5]=CommandLine[i];
   else
   {
    sendname[i-5]='/0';
    break;
   }
  }
  strcpy(message, &(CommandLine[i+1]));
  if(SendMessageTo(sendname, message))
   printf("Send OK!/n");
  else 
   printf("Send Failure!/n");
 }
 else if(strcmp(Command,"getu")==0)
 {
  int command = GETALLUSER;
  sockaddr_in server;
  server.sin_addr.S_un.S_addr = inet_addr(ServerIP);
  server.sin_family = AF_INET;
  server.sin_port = htons(SERVER_PORT);

  sendto(PrimaryUDP,(const char*)&command, sizeof(command), 0, (const sockaddr *)&server, sizeof(server));
 }
}

// 接受消息线程
DWORD WINAPI RecvThreadProc(LPVOID lpParameter)
{
 sockaddr_in remote;
 int sinlen = sizeof(remote);
 stP2PMessage recvbuf;
 for(;;)
 {
  int iread = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(recvbuf), 0, (sockaddr *)&remote, &sinlen);
  if(iread<=0)
  {
   printf("recv error/n");
   continue;
  }
  switch(recvbuf.iMessageType)
  {
  case P2PMESSAGE:
   {
    // 接收到P2P的消息
    char *comemessage= new char[recvbuf.iStringLen];
    int iread1 = recvfrom(PrimaryUDP, comemessage, 256, 0, (sockaddr *)&remote, &sinlen);
    comemessage[iread1-1] = '/0';
    if(iread1<=0)
     throw Exception("Recv Message Error/n");
    else
    {
     printf("Recv a Message:%s/n",comemessage);
     
     stP2PMessage sendbuf;
     sendbuf.iMessageType = P2PMESSAGEACK;
     sendto(PrimaryUDP, (const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&remote, sizeof(remote));
    }

    delete []comemessage;
    break;

   }
  case P2PSOMEONEWANTTOCALLYOU:
   {
    // 接收到打洞命令,向指定的IP地址打洞
    printf("Recv p2someonewanttocallyou data/n");
    sockaddr_in remote;
    remote.sin_addr.S_un.S_addr = htonl(recvbuf.iStringLen);
    remote.sin_family = AF_INET;
    remote.sin_port = htons(recvbuf.Port);

    // UDP hole punching
    stP2PMessage message;
    message.iMessageType = P2PTRASH;
    sendto(PrimaryUDP, (const char *)&message, sizeof(message), 0, (const sockaddr*)&remote, sizeof(remote));
                
    break;
   }
  case P2PMESSAGEACK:
   {
    // 发送消息的应答
    RecvedACK = true;
    break;
   }
  case P2PTRASH:
   {
    // 对方发送的打洞消息,忽略掉。
    //do nothing ...
    printf("Recv p2ptrash data/n");
    break;
   }
  case GETALLUSER:
   {
    int usercount;
    int fromlen = sizeof(remote);
    int iread = recvfrom(PrimaryUDP, (char *)&usercount, sizeof(int), 0, (sockaddr *)&remote, &fromlen);
    if(iread<=0)
    {
     throw Exception("Login error/n");
    }
    
    ClientList.clear();

    cout<<"Have "<<usercount<<" users logined server:"<<endl;
    for(int i = 0;i<usercount;i++)
    {
     stUserListNode *node = new stUserListNode;
     recvfrom(PrimaryUDP, (char*)node, sizeof(stUserListNode), 0, (sockaddr *)&remote, &fromlen);
     ClientList.push_back(node);
     cout<<"Username:"<<node->userName<<endl;
     in_addr tmp;
     tmp.S_un.S_addr = htonl(node->ip);
     cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;
     cout<<"UserPort:"<<node->port<<endl;
     cout<<""<<endl;
    }
    break;
   }
  }
 }
}


int main(int argc, char* argv[])
{
 try
 {
  InitWinSock();
  
  PrimaryUDP = mksock(SOCK_DGRAM);
  BindSock(PrimaryUDP);

  cout<<"Please input server ip:";
  cin>>ServerIP;

  cout<<"Please input your name:";
  cin>>UserName;

  ConnectToServer(PrimaryUDP, UserName, ServerIP);

  HANDLE threadhandle = CreateThread(NULL, 0, RecvThreadProc, NULL, NULL, NULL);
  CloseHandle(threadhandle);
  OutputUsage();

  for(;;)
  {
   char Command[COMMANDMAXC];
   gets(Command);
   ParseCommand(Command);
  }
 }
 catch(Exception &e)
 {
  printf(e.GetMessage());
  return 1;
 }
 return 0;
}

/* 异常类
 *
 * 文件名:Exception.h
 *
 * 日期:2004.5.5
 *
 * 作者:shootingstars(zhouhuis22@sina.com)
 */

#ifndef __HZH_Exception__
#define __HZH_Exception__

#define EXCEPTION_MESSAGE_MAXLEN 256
#include "string.h"

class Exception
{
private:
 char m_ExceptionMessage[EXCEPTION_MESSAGE_MAXLEN];
public:
 Exception(char *msg)
 {
  strncpy(m_ExceptionMessage, msg, EXCEPTION_MESSAGE_MAXLEN);
 }

 char *GetMessage()
 {
  return m_ExceptionMessage;
 }
};

#endif

/* P2P 程序传输协议
 * 
 * 日期:2004-5-21
 *
 * 作者:shootingstars(zhouhuis22@sina.com)
 *
 */

#pragma once
#include <list>

// 定义iMessageType的值
#define LOGIN 1
#define LOGOUT 2
#define P2PTRANS 3
#define GETALLUSER  4

// 服务器端口
#define SERVER_PORT 2280

// Client登录时向服务器发送的消息
struct stLoginMessage
{
 char userName[10];
 char password[10];
};

// Client注销时发送的消息
struct stLogoutMessage
{
 char userName[10];
};

// Client向服务器请求另外一个Client(userName)向自己方向发送UDP打洞消息
struct stP2PTranslate
{
 char userName[10];
};

// Client向服务器发送的消息格式
struct stMessage
{
 int iMessageType;
 union _message
 {
  stLoginMessage loginmember;
  stLogoutMessage logoutmember;
  stP2PTranslate translatemessage;
 }message;
};

// 客户节点信息
struct stUserListNode
{
 char userName[10];
 unsigned int ip;
 unsigned short port;
};

// Server向Client发送的消息
struct stServerToClient
{
 int iMessageType;
 union _message
 {
  stUserListNode user;
 }message;

};

//======================================
// 下面的协议用于客户端之间的通信
//======================================
#define P2PMESSAGE 100               // 发送消息
#define P2PMESSAGEACK 101            // 收到消息的应答
#define P2PSOMEONEWANTTOCALLYOU 102  // 服务器向客户端发送的消息
                                     // 希望此客户端发送一个UDP打洞包
#define P2PTRASH        103          // 客户端发送的打洞包,接收端应该忽略此消息

// 客户端之间发送消息格式
struct stP2PMessage
{
 int iMessageType;
 int iStringLen;         // or IP address
 unsigned short Port; 
};

using namespace std;
typedef list<stUserListNode *> UserList;

工程下载地址:upload/2004_05/04052509317298.rar



===============================================================================================


NAT的完全分析及其UDP穿透的完全解决方案


本文转自 http://blog.csdn.net/colinchan/archive/2006/05/08/712773.aspx

 

一:基本术语
防火墙
防火墙限制了私网与公网的通信,它主要是将(防火墙)认为未经授权的的包丢弃,防火墙只是检验包的数据,并不修改数据包中的 IP地址和TCP/UDP端口信息。
网络地址转换(NAT)
当有数据包通过时,网络地址转换器不仅检查包的信息,还要将包头中的 IP地址和端口信息进行修改。以使得处于NAT之后的机器共享几个仅有的公网IP地址(通常是一个)。网络地址转换器主要有两种类型.
P2P应用程序
P2P应用程序是指,在已有的一个公共服务器的基础上,并分别利用自己的私有地址或者公有地址(或者两者兼备)来建立一个端到端的会话通信。
P2P防火墙
P2P防火墙是一个提供了防火墙的功能的 P2P代理,但是不进行地址转换.
P2P-NAT
P2P-NAT 是一个  P2P代理,提供了NAT的功能,也提供了防火墙的功能,一个最简的P2P代理必须具有锥形NAT对Udp通信支持的功能,并允许应用程序利用Udp打洞技术建立强健的P2P连接。
回环转换
当 NAT的私网内部机器想通过公共地址来访问同一台局域网内的机器的时,NAT设备等价于做了两次NAT的事情,在包到达目标机器之前,先将私有地址转换为公网地址,然后再将公网地址转换回私有地址。我们把具有上叙转换功能的NAT设备叫做“回环转换”设备。
 
二:NAT分类
可以分为基础NAT网络地址和端口转换(NAPT)两大类
(一):基础NAT
基础NAT 将私网主机的私有 IP地址转换成公网IP地址,但并不将TCP/UDP端口信息进行转换。基础NAT一般用在当NAT拥有很多公网IP地址的时候,它将公网IP地址与内部主机进行绑定,使得外部可以用公网IP地址访问内部主机。(实际上是只将IP转换,192.168.0.23 <-> 210.42.106.35,这与直接设置IP地址为公网IP还是有一定区别的,特别是对于企业来说,外部的信息都要经过统一防火墙才能到达内部,但是内部主机又可以使用公网IP)
(二):网络地址和端口转换 (NAPT)
这是最普遍的情况,网络地址 /端口转换器检查、修改包的IP地址和TCP/UDP端口信息,这样,更多的内部主机就可以同时使用一个公网IP地址。
请参考 [RFC1631]和[RFC2993]及[RFC2663]这三个文档了解更多的NAT分类和术语信息。另外,关于NAPT的分类和术语,[RFC2663]做了更多的定义。当一个内部网主机通过NAT打开一个“外出”的TCP或UDP会话时,NAPT分配给这个会话一个公网IP和端口,用来接收外网的响应的数据包,并经过转换通知内部网的主机。这样做的效果是,NAPT在 [私有IP:私有端口] 和[公网IP:公网端口]之间建立了一个端口绑定。
端口绑定指定了 NAPT将在这个会话的生存期内进行地址转换任务。这中间存在一个这样的问题,如果P2P应用程序从内部网络的一个[私有IP地址:端口]对同时发出多条会话给不同的外网主机,那么NAT会怎样处理呢?这又可以分为锥形NAT (CONE NAT)对称NAT (SYMMTRIC NAT)两大类来考虑:
A.锥形NAT
(为什么叫做锥形呢?请看以下图形 ,终端和外部服务器,都通过NAT分派的这个绑定地址对来传送信息,就象一个漏斗一样,筛选并传递信息)
                               
  当建立了一个  [私有IP:端口]-[公网IP:端口] 端口绑定之后,对于来自同一个[私有IP:端口]会话,锥形NAT服务器允许发起会话的应用程序 重复使用这个端口绑定,一直到这个会话结束才解除(端口绑定)。
  例如,假设  Client A(IP地址信息如上图所示)通过一个锥形NAT 同时发起两个外出的连接,它使用同一个内部端口(10.0.0.1:1234)给公网的两台不同的服务器,S1和S2。锥形NAT 只分配一个公网IP和端口(155.99.25.11:62000)给这个两个会话,通过地址转换可以确保 Client使用端口的“同一性”(即这个Client只使用这个端口)。而基础NATs和防火墙却不能修改经过的数据包端口号,它们可以看作是锥形NAT的精简版本。
进一步分析可以将 CONE NAT受限制锥形NAT (RESTRICT CONE) 与端口受限锥形NAT (PORT RESTRICT CONE) 三大类,以下是详细论述: 分为 全双工锥形NAT (FULL CONE) ,
1.全双工锥形NAT
当内部主机发出一个 “外出”的连接会话,就会创建了一个公网/私网 地址,一旦这个地址对被创建,全双工锥形NAT会接收随后任何外部端口传入这个公共端口地址的通信。因此,全双工锥形NAT有时候又被称为"混杂"NAT。
2.受限制锥形NAT
受限制的锥形 NAT会对传入的数据包进行筛选,当内部主机发出“外出”的会话时,NAT会记录这个外部主机的IP地址信息,所以,也只有这些有记录的外部IP地址,能够将信息传入到NAT内部,受限制的锥形NAT 有效的给防火墙提炼了筛选包的原则——即限定只给那些已知的外部地址“传入”信息到NAT内部。
3.端口受限锥形NAT
端口受限制的锥形 NAT,与受限制的锥形NAT不同的是:它同时记录了外部主机的IP地址和端口信息,端口受限制的锥形NAT给内部节点提供了同一级别的保护,在维持端口“同一性”过程中,将会丢弃对称NAT传回的信息。
B.对称NAT
对称 NAT,与Cone NAT是大不相同的,并不对会话进行端口绑定,而是分配一个全新的公网端口 给每一个新的会话。
还是上面那个例子:如果  Client A (10.0.0.1:1234)同时发起两个 "外出" 会话,分别发往S1和S2。对称Nat会分配公共地址155.99.25.11:62000给Session1,然后分配另一个不同的公共地址155.99.25.11:62001给Session2。对称Nat能够区别两个不同的会话并进行地址转换,因为在 Session1 和 Session2中的外部地址是不同的,正是因为这样,Client端的应用程序就迷失在这个地址转换边界线了,因为这个应用程序每发出一个会话都会使用一个新的端口,无法保障只使用同一个端口了。
在 TCP和UDP通信中,(到底是使用同一个端口,还是分配不同的端口给同一个应用程序),锥形NAT和对称NAT各有各的理由。当然锥形NAT在根据如何公平地将NAT接受的连接直达一个已创建的地址对上有更多的分类。这个分类一般应用在Udp通信(而不是Tcp通信上),因为NATs和防火墙阻止了试图无条件传入的TCP连接,除非明确设置NAT不这样做。
三:NAT对session的处理
以下分析 NAPT是依据什么策略来判断是否要为一个请求发出的UDP数据包建立Session的.主要有一下几个策略:
A. 源地址 (内网IP地址)不同,忽略其它因素, 在NAPT上肯定对应不同的Session
B. 源地址 (内网IP地址)相同,源端口不同,忽略其它的因素,则在NAPT上也肯定对应不同的Session
C. 源地址 (内网IP地址)相同,源端口相同,目的地址(公网IP地址)相同,目的端口不同,则在NAPT上肯定对应同一个Session
D. 源地址 (内网IP地址)相同,源端口相同,目的地址(公网IP地址)不同,忽略目的端口,则在NAPT上是如何处理Session的呢?
A,B,C三种情况的都是比较简单的 ,可以很容易的实现.而D的情况就比较复杂了.所以D情况才是我们要重点关心和讨论的问题。
四:完全解决方案
以下针对四种 SESSION与四种NAT的完全解决方案,为了方便将使用以下缩写形式:
C代表  CONE NAT
S代表 SYMMETRIC NAT,
FC代表  FULL CONE NAT,
RC代表  RESTRICT CONE NAT,
PC 代表  PORT RESTRICT CONE NAT.
首先依据 CLIENT (客户)端在NAT后 的个数不同可以分为两大类:
TYPE ONE :一个在NAT后 + 一个在公网中.
这种情况下可以分为两大类 :
A. S VS 公网:此种情况下,由于公网的地址在一个SESSION内是不变的,所以可以打洞是可以成功的.
B. C VS 公网与上面类似,这种情口下打洞是可以成功的.
TYPE TWO:两个客户都在NAT后面.
这种情况下也可以细分为两大类 :
A. 其中一个NAT 是 S(SYMMETRIC NAT) 的,既:S VS C或者是S VS S .
下面论证这种情口下按照常规打洞是行不通的 ,在常规打洞中,所有的客户首先登陆到一个服务器上去.服务器记录下每个客户的[公网IP:端口],然后在打洞过程中就使用这个记录的值,然而对于S的NAT来说,它并不绑定[私网IP:端口][公网IP:端口]的映射.所以在不同的SESSION中,NAT将会重新分配一对[公网IP:端口].这样一来对于S型的NAT来说打洞的[公网IP:端口]与登记在服务器上的[公网IP:端口]是不同的.而且也没有办法将打洞的[公网IP:端口]通知到另一个位于NAT下的客户端, 所以打洞是不会成功的.然而如果另一个客户端是在公网时,打洞是可以的.前面已经论证了这种情况.
这种情况下的解决方案是只能通过端口预测来进行打洞 ,具体解决方法如下:例如 (以两个都是S型的为例) 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的双向连接就被打通了。其结果如下图所示:
明显的,有许多因素会导致这个方法失败:如果这个预言的新端口( 62001和31001) 恰好已经被一个不相关的会话所使用,那么NAT就会跳过这个端口号,这个连接就会宣告失败;如果两个NAT有时或者总是不按照顺序来生成新的端口号,那么这个方法也是行不通的
如果隐藏在 NAT A后的一个不同的 客户端X(或者在NAT B后)打开了一个新的“外出”UDP 连接,并且无论这个连接的目的如何;只要这个动作发生在 客户端A 建立了与服务器S的连接之后 客户端A 与 客户端B 建立连接之前;那么这个无关的 客户端X 就会趁人不备地“偷” 到这个我们渴望分配的端口。所以,这个方法变得如此脆弱而且不堪一击,只要任何一个NAT方包含以上碰到的问题,这个方法都不会奏效。
在处于  cone NAT 系列的网络环境中这个方法还是实用的;如果有一方为 cone NAT 而另外一方为 symmetric NAT,那么应用程序就应该预先发现另外一方的 NAT 是什么类型,再做出正确的行为来处理通信,这样就增大了算法的复杂度,并且降低了在真实网络环境中的普适性。
    最后,如果 P2P的一方处在两级或者两级以上的NAT下面,并且这些NATS 接近这个客户端是SYMMETRIC NAT的话,端口号预言是无效的!
因此,并不推荐使用这个方法来写新的 P2P应用程序,这也是历史的经验和教训!
B. 两个都是CONE NAT型.
这种情况下可以分为六大类型 :
A:  FC + FC
B:  FC + RC
C:  FC + PC
D:  PC + RC
E:  PC + PC
F:  RC + RC
虽然有这么多种情况 ,但是由于CONE NAT 的特性,所以还是很好办的,因为对于CONE NAT 来说,在同一个SESSION中它会绑定一对[私网IP:端口][公网IP:端口]的映射,所以它们打洞用的[公网IP:端口]与登记在服务器上的[公网IP:端口]是一致的,所以打洞是可以行的通的.
综上所述 ,就已经完全的概括了所有类型的NAT之间的可能的通信情况了.并且都提供了可行的解决方案.
五:对前一阶段的总结
1.前一阶段使用的打洞方法是有缺陷的 ,它只适应于两个都是FULL CONE NAT的类型的CLIENT(客户端).以下论证它不适应于两个都是CONE NAT的类型中的
B:  FC + RC
C:  FC + PC
D:  PC + RC
E:  PC + PC
F:  RC + RC
这五种情况 .
因为对于 受限的NAT它登记了外出包的[IP地址&端口],它仅仅接受这些已登记地址发过来的包,所以它们报告服务器的端口只能接受来自服务器的包.不能接受来自另一客户端的包.所以前一阶段的打洞方法是不可行的.
六: 存在的问题
按照理论 .NAT将在一定时间后关闭UDP的一个映射,所以为了保持与服务器能够一直通信,服务器必须要发送UDP心跳包,来保持映射不被关闭.这就需要一个合适的时间值.




===============================================================================================
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值