WebRTC源码研究 NAT打洞原理

文章目录
WebRTC源码研究(25)NAT打洞原理

  1. NAT
    1.1 NAT 简介
    1.1.1 IPv4协议和NAT的由来
    1.1.2 NAT的概念模型
    1.2 NAT 网络地址转换
    1.3 NAT 的副作用以及解决方案
    1.4 四种不同类型的NAT
    1.4.1 完全圆锥型NAT
    1.4.2 受限圆锥型NAT
    1.4.3 端口受限圆锥型NAT
    1.4.4 对称NAT
    1.5 NAT 的用途
  2. NAT 打洞流程
    2.1 NAT穿越原理
    2.1.1 C1,C2向STUN发消息
    2.1.2 C1->C2,C2->C1,甚至是端口猜测
    2.2 NAT穿越成功的条件
    2.2.1 对称NAT穿越问题
    2.2.2 NAT穿透流程
    2.3 STUN和TURN的实现打洞
    2.3.1 STUN 服务器做的打洞流程
    2.3.2 TURN 服务器做的事情
  3. NAT 类型检测算法
  4. ICE进行NAT穿透原理
    4.1 ICE进行NAT穿透代码实现
    WebRTC源码研究(25)NAT打洞原理
    要理解NAT打洞原理,需要先理解NAT的相关知识,关于这块的知识已经在前面的博客中讲述的比较详细,具体可以参考这篇博客:WebRTC源码研究(23)WebRTC网络传输基本知识

这里再简单回顾一下,NAT是什么,有几种类型

这里有些很好的穿透相关的博客:

n2n内网穿透打洞部署全过程 + nginx公网端口映射
如何使用开源SFU构建RTC云服务
内网穿透工具 frp

ICE

SDP

SIP:

NAT:

STUN:

TURN:

ICE4J:

开源项目:
coturn

cumulus

monaserver-cumulus升级版

  1. NAT
    在计算机科学中,NAT穿越(NAT traversal)涉及TCP/IP网络中的一个常见问题,即在处于使用了NAT设备的私有TCP/IP网络中的主机之间创建连接的问题。

会遇到这个问题的通常是那些客户端网络交互应用程序的开发人员,尤其是在对等网络和VoIP领域中。IPsec VPN客户普遍使用NAT-T来达到使ESP包通过NAT的目的。

尽管有许多穿越NAT的技术,但没有一项是完美的,这是因为NAT的行为是非标准化的。这些技术中的大多数都要求有一个公共服务器,而且这个服务器使用的是一个众所周知的、从全球任何地方都能访问得到的IP地址。一些方法仅在创建连接时需要使用这个服务器,而其它的方法则通过这个服务器中继所有的数据——这就引入了带宽开销的问题。

两种常用的NAT穿越技术是:UDP路由验证和STUN。除此之外,还有TURN、ICE、ALG,以及SBC。

1.1 NAT 简介
什么NAT ?
网络地址转换(英语:Network Address Translation,缩写:NAT;又称网络掩蔽、IP掩蔽)在计算机网络中是一种在IP数据包通过路由器或防火墙时重写来源IP地址或目的IP地址的技术。这种技术被普遍使用在有多台主机但只通过一个公有IP地址访问互联网的私有网络中。它是一个方便且得到了广泛应用的技术。当然,NAT也让主机之间的通信变得复杂,导致了通信效率的降低。
更多NAT详解参考这篇博客:P2P技术详解(一):NAT详解——详细原理、P2P简介

我们以传统的邮件作为例子,给大家说明NAT是什么?比如A和B两个人要发信,那B告诉A它在某个楼的某层时,这个时候A 可以给B发消息或者信件吗?这肯定不行,因为它并不知道一个具体的地址是多少?你必须告诉它具体哪个省哪个市哪个区哪个小区哪号楼哪层时,只有这种公共的地址,也就是大家都认识的地址,邮局才能帮你把这封信送达。你说哪号楼哪层这个只有你小区内的人才知道。那这个就和我们的网络是相关的。
那对于网络上的主机,你必须要有个公网的地址,那相互之间才能进行通讯,如果告诉它一个私网(内网)的地址,那它根本 找不到你。那对于我们现实中大部分主机都是在网关之后的,他们之间都是有自己的内网IP地址,并不知道自己的外网是多少。那怎么办呢?实际是有一个映射,在网关上有个NAT功能,它可以使你的内网地址变成外网地址。所以他就是一个资源组,映射之后就将你的内网IP端口映射成外网IP端口。那有了外网的IP端口之后,其他的主机就可以通过内网的IP地址与你通讯了。这就是NAT。

首先是NAT,这张图就表现的非常清楚,这就是一个地址映射,左边的分别 是内网的几台机子,通过内网的IP他们之间是可以相互通信的,但是与互联网之间是不通的,如何访问互联网的资源呢,就必须通过NAT,将我们的内网地址转换成外网地址。

由于每台主机都要映射不同的端口,NAT产生的原因有两种:

第一种是IPv4的地址不够,解决IPv4地址不够有两种方案,其中最好的是使用IPv6,IPv6的地址池更多,基本上每台主机都有自己的IP地址。还有一种就是进行NAT穿越,就是内网数万台主机都有自己的IP地址,但是映射到外网只有一个IP地址或者几个IP地址,它通过端口好来区分每一台主机,那就形成了一对几百或者以对几万,大大减少了公网IP地址的使用。
第二个是处于网络安全的考虑。
1.1.1 IPv4协议和NAT的由来
2011年2月3日中国农历新年, IANA对外宣布:IPv4地址空间最后5个地址块已经被分配给下属的5个地区委员会。2011年4月15日,亚太区委员会APNIC对外宣布,除了个别保留地址外,本区域所有的IPv4地址基本耗尽。一时之间,IPv4地址作为一种濒危资源身价陡增,各大网络公司出巨资收购剩余的空闲地址。其实,IPv4地址不足问题已不是新问题,早在20年以前,IPv4地址即将耗尽的问题就已经摆在Internet先驱们面前。这不禁让我们想去了解,是什么技术使这一危机延缓了尽20年。

要找到问题的答案,让我们先来简略回顾一下IPv4协议。

IPv4即网际网协议第4版——Internet Protocol Version 4的缩写。IPv4定义一个跨越异种网络互连的超级网,它为每个网际网的节点分配全球唯一IP地址。如果我们把Internet比作一个邮政系统,那么IP地址的作用就等同于包含城市、街区、门牌编号在内的完整地址。IPv4使用32bits整数表达一个地址,地址最大范围就是232 约为43亿。以IP创始时期可被联网的设备来看,这样的一个空间已经很大,很难被短时间用完。然而,事实远远超出人们的设想,计算机网络在此后的几十年里迅速壮大,网络终端数量呈爆炸性增长。

更为糟糕的是,为了路由和管理方便,43亿的地址空间被按照不同前缀长度划分为A,B,C,D类地址网络和保留地址。其中,A类网络地址127段,每段包括主机地址约1678万个。B类网络地址16384段,每段包括65536个主机地址。

IANA向超大型企业/组织分配A类网络地址,一次一段。向中型企业或教育机构分配B类网络地址,一次一段。这样一种分配策略使得IP地址浪费很严重,很多被分配出去的地址没有真实被利用,地址消耗很快。以至于二十世纪90年代初,网络专家们意识到,这样大手大脚下去,IPv4地址很快就要耗光了。于是,人们开始考虑IPv4的替代方案,同时采取一系列的措施来减缓IPv4地址的消耗。正是在这样一个背景之下,本期的主角闪亮登场,它就是网络地址转换——NAT。

NAT是一项神奇的技术,说它神奇在于它的出现几乎使IPv4起死回生。在IPv4已经被认为行将结束历史使命之后近20年时间里,人们几乎忘了IPv4的地址空间即将耗尽这样一个事实——在新技术日新月异的时代,20年可算一段漫长的历史。更不用说,在NAT产生以后,网络终端的数量呈加速上升趋势,对IP地址的需求剧烈增加。此足见NAT技术之成功,影响之深远。

说它神奇,更因为NAT给IP网络模型带来了深远影响,其身影遍布网络每个角落。根据一份最近的研究报告,70%的P2P用户位于NAT网关以内。因为P2P主要运行在终端用户的个人电脑之上,这个数字意味着大多数PC通过NAT网关连接到Internet。如果加上2G和3G方式联网的智能手机等移动终端,在NAT网关之后的用户远远超过这个比例。

然而当我们求本溯源时却发现一个很奇怪的事实:NAT这一意义重大的技术,竟然没有公认的发明者。NAT第一个版本的RFC作者,只是整理归纳了已被广泛采用的技术。

1.1.2 NAT的概念模型
NAT名字很准确,网络地址转换,就是替换IP报文头部的地址信息。NAT通常部署在一个组织的网络出口位置,通过将内部网络IP地址替换为出口的IP地址提供公网可达性和上层协议的连接能力。那么,什么是内部网络IP地址?

RFC1918规定了三个保留地址段落:10.0.0.0-10.255.255.255;172.16.0.0-172.31.255.255;192.168.0.0-192.168.255.255。这三个范围分别处于A,B,C类的地址段,不向特定的用户分配,被IANA作为私有地址保留。这些地址可以在任何组织或企业内部使用,和其他Internet地址的区别就是,仅能在内部使用,不能作为全球路由地址。这就是说,出了组织的管理范围这些地址就不再有意义,无论是作为源地址,还是目的地址。对于一个封闭的组织,如果其网络不连接到Internet,就可以使用这些地址而不用向IANA提出申请,而在内部的路由管理和报文传递方式与其他网络没有差异。

对于有Internet访问需求而内部又使用私有地址的网络,就要在组织的出口位置部署NAT网关,在报文离开私网进入Internet时,将源IP替换为公网地址,通常是出口设备的接口地址。一个对外的访问请求在到达目标以后,表现为由本组织出口设备发起,因此被请求的服务端可将响应由Internet发回出口网关。出口网关再将目的地址替换为私网的源主机地址,发回内部。这样一次由私网主机向公网服务端的请求和响应就在通信两端均无感知的情况下完成了。依据这种模型,数量庞大的内网主机就不再需要公有IP地址了。

虽然实际过程远比这个复杂,但上面的描述概括了NAT处理报文的几个关键特点:

1)网络被分为私网和公网两个部分,NAT网关设置在私网到公网的路由出口位置,双向流量必须都要经过NAT网关;
2)网络访问只能先由私网侧发起,公网无法主动访问私网主机;
3)NAT网关在两个访问方向上完成两次地址的转换或翻译,出方向做源信息替换,入方向做目的信息替换;
4)NAT网关的存在对通信双方是保持透明的;
5)NAT网关为了实现双向翻译的功能,需要维护一张关联表,把会话的信息保存下来。

随着后面对NAT的深入描述,读者会发现,这些特点是鲜明的,但又不是绝对的。其中第二个特点打破了IP协议架构中所有节点在通讯中的对等地位,这是NAT最大的弊端,为对等通讯带来了诸多问题,当然相应的克服手段也应运而生。事实上,第四点是NAT致力于达到的目标,但在很多情况下,NAT并没有做到,因为除了IP首部,上层通信协议经常在内部携带IP地址信息。这些我们稍后解释。

1.2 NAT 网络地址转换
基本网络地址转换(Basic NAT)
这一种也可称作NAT或“静态NAT”,在RFC 2663中提供了信息。它在技术上比较简单,仅支持地址转换,不支持端口映射。Basic NAT要求对每一个当前连接都要对应一个公网IP地址,因此要维护一个公网的地址池。宽带(broadband)路由器通常使用这种方式来允许一台指定的设备去管理所有的外部链接,甚至当路由器本身只有一个可用外部IP时也如此,这台路由器有时也被标记为DMZ主机。由于改变了IP源地址,在重新封装数据包时候必须重新计算校验和,网络层以上的只要涉及到IP地址的头部校验和都要重新计算。

Basic NAT要维护一个无端口号NAT表,结构如下。

网络地址端口转换(NAPT)
这种方式支持端口的映射,并允许多台主机共享一个公网IP地址。
支持端口转换的NAT又可以分为两类:源地址转换和目的地址转换。前一种情形下发起连接的计算机的IP地址将会被重写,使得内网主机发出的数据包能够到达外网主机。后一种情况下被连接计算机的IP地址将被重写,使得外网主机发出的数据包能够到达内网主机。实际上,以上两种方式通常会一起被使用以支持双向通信。

NAPT维护一个带有IP以及端口号的NAT表,结构如下:

1.3 NAT 的副作用以及解决方案
NAT 的副作用主要有下面几点:

对等网络传输需穿透NAT:IP协议的定义中,在理论上,具有IP地址的每个站点在协议层面有相当的获取服务和提供服务的能力,不同的IP地址之间没有差异。但NAT工作原理破坏了这个特征,如需实现真正意义上的对等网络传输,则需要穿透NAT。这是本文重点。
应用层需保持UDP会话连接:由于NAT资源有限,会根据一定规则回收转换出去的资源(即ip/port组合),UDP通信又是无连接的,所以基于UDP的应用层协议在无数据传输、但需要保持连接时需要发包以保持会话不过期,就是通常的heartbeat之类的。
基于IP的访问限制策略复杂化
国内移动无线网络运营商在链路上一段时间内没有数据通讯后, 会淘汰NAT表中的对应项, 造成链路中断。

NAT带来的第一个副作用:NAT超时:
而国内的运营商一般NAT超时的时间为5分钟,所以通常我们TCP长连接的心跳设置的时间间隔为3-5分钟。**

NAT带来的第二个副作用就是:NAT墙。
NAT会有一个机制,所有外界对内网的请求,到达NAT的时候,都会被NAT所丢弃,这样如果我们处于一个NAT设备后面,我们将无法得到任何外界的数据。

但是这种机制有一个解决方案:就是如果我们A主动往B发送一条信息,这样A就在自己的NAT上打了一个B的洞。这样A的这条消息到达B的NAT的时候,虽然被丢掉了,但是如果B这个时候在给A发信息,到达A的NAT的时候,就可以从A之前打的那个洞中,发送给到A手上了。

简单来讲,就是如果A和B要进行通信,那么得事先A发一条信息给B,B发一条信息给A。这样提前在各自的NAT上打了对方的洞,这样下一次A和B之间就可以进行通信了。

1.4 四种不同类型的NAT
上面介绍了NAT的来由和优缺点,下面来看看NAT的分类:
实际上NAT分为基础型NAT(静态NAT即Static NAT,动态NAT即Dynamic NAT/Pooled NAT)和NAPT(Network Address Port Translation)两种,但由于基础型NAT已不常用,我们通常提到的NAT就代指NAPT。NAPT是指网络地址转换过程中使用了端口复用技术,即PAT(Port address Translation)。

如下图所示:

1.4.1 完全圆锥型NAT
完全圆锥型NAT(Full cone NAT),即一对一(one-to-one)NAT
一旦一个内部地址(iAddr:port)映射到外部地址(eAddr:port),所有发自iAddr:port的包都经由eAddr:port向外发送。任意外部主机都能通过给eAddr:port发包到达iAddr:port(注:port不需要一样)
更通俗一点讲:什么是完全锥型?

就是当内网中的某一台主机经过NAT映射形成一个外网的IP地址和端口,也就是外网所有的主机,只要知道这个地址都可以向这个地址发送数据,基本上就是没有什么限制,这就是安全性比较低的一种类型,也就是谁都能来。

完全锥型是非常简单的 ,左边是内网的主机,它有自己的内网IP地址和端口 ,通过防火墙之后,它形成一个外网的IP地址,那么外网的三台主机要想与内网的主机进行通信的时候,首先要由内网的主机向外发送一个请求,请求外网中的其中一台主机,这样会形成的结果就是它会在NAT服务上打 一个洞,这样会形成一个外网的IP地址和端口,那么形成了外网的IP地址和端口之后,其他的主机只要获得了这个IP地址和端口它都可以向它发送数据。并且可以顺利的通过防火墙发送给内网的主机。这样就可以进行通讯了,这是完全锥型,也是最好穿越的一种 NAT类型。但是安全性就差很多。

1.4.2 受限圆锥型NAT
受限圆锥型NAT(Address-Restricted cone NAT):

内部客户端必须首先发送数据包到对方(IP=X.X.X.X),然后才能接收来自X.X.X.X的数据包。在限制方面,唯一的要求是数据包是来自X.X.X.X。
内部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有发自iAddr:port1的包都经由eAddr:port2向外发送。外部主机(hostAddr:any)能通过给eAddr:port2发包到达iAddr:port1。(注:any指外部主机源端口不受限制,但是目的端口必须是port2。只有外部主机数据包的目的IP 为 内部客户端的所映射的外部ip,且目的端口为port2时数据包才被放行。)
更通俗一点讲:
什么是受限圆锥型NAT?

受限圆锥型NAT 又称为地址限制锥型NAT:
也就是大家觉得完全锥型安全性问题太大了,那就做一些限制,也就是请求出去的时候会记录一下出去的IP地址,那么当你回来的时候只有这台地址的主机才能给我回消息,对于公网上的其他地址来说,我一检查IP地址不对,就给PASS掉。这种就是地址限制型。只要我没向你发送过请求,你直接向我发数据,这是不允许的。

它的安全性好一些,它会在防火墙上形成一个五元组,就是内网主机的IP地址和端口和映射后的公网IP地址和端口以及我要请求的这个主机IP地址,他们首先有一个公共的步骤,第一步就是要先由内网的主机向外网发送一个请求,在这个防火墙上或者NAT服务上形成一个映射表,那形成之后外网的主机就可以和内网的主机进行通讯了。

如图所示,它首先向P的主机发送请求,那么P就可以通过不同的端口向内网的主机发送消息它都是可以接受到的,但是对于其他主机来说,由于IP地址的限制,它返回来的时候,一看IP地址不对,就会被拦掉。只有P的主机是可以通过的,而且它的各个主机都可以跟它进行通讯。这就是地址限制型,这个地址限制型的打通,首先就是这个内网主机无论跟哪个主机进行打通,首先它都要发送 一个请求,发送请求之后就形成了所谓的映射表在我们的网关上。其他主机就可以给它通讯了。这是地址限制型NAT。

1.4.3 端口受限圆锥型NAT
端口受限圆锥型NAT(Port-Restricted cone NAT):
类似受限制锥形NAT(Restricted cone NAT),但是还有端口限制。

一旦一个内部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有发自iAddr:port1的包都经由eAddr:port2向外发送。
在受限圆锥型NAT基础上增加了外部主机源端口必须是固定的。
更通俗一点讲:
什么是端口受限圆锥型NAT?

端口限制型就是在IP地址的限制基础上又增加了对端口的限制,也就是我发送信息的时候会给主机的某个应用的某个端口发送数据,那么你回来的时候 ,只有这个端口回来的数据才接受,对于同一台主机其他端口发送过来的数据都拒绝接收。更何况 是其他的主机 了,所以它的限制更加严格。

端口限制型就更加严格一些了,不光是对IP地址,还要对端口做限制,那所以在这个防火墙上就形成了六元组,不光有内网的IP地址和端口以及映射后的公网的IP地址和端口,还有你请求的主机的IP地址和端口,那么在在这种情况下P这台主机,它发送消息的时候,如果请求的是P这台主机的这个q这个端口的服务,只有它这个服务才能返来,其他的端口(如:r端口)发送数据就不行了。

那如果内网的主机没有向S这个主机发送请求的话,S主机发送信息到内网的主机是肯定不通的;但是如果内网的主机给M这台主机的n端口已经发送了请求,那么M主机的n端口也是可以打通这个数据防火墙然后与这个内网主机进行通讯的。这就是端口限制锥型NAT。

1.4.4 对称NAT
对称NAT(Symmetric NAT):

每一个来自相同内部IP与端口,到一个特定目的地地址和端口的请求,都映射到一个独特的外部IP地址和端口。
同一内部IP与端口发到不同的目的地和端口的信息包,都使用不同的映射
只有曾经收到过内部主机数据的外部主机,才能够把数据包发回
更通俗一点讲:
什么是对称NAT?

当我进行NAT转换的时候,内网的主机出外网的时候形成的映射,并不是形成一个IP地址和端口,它会形成多个,对于访问不同的主机,它会形成不同的IP地址和端口,这就更加严格,想知道IP地址都很困难,比如说你通过这个地址请求A,那么A告诉B通过这个IP地址是可以访问,那B实际上是访问不通的。内网的主机与第三个主机连接的时候,它会新建一个IP地址 和端口,这个就更加复杂,这个对NAT穿越就提出了更高的要求,对于对称型的NAT基本上都是不能穿越的。

对称限制型就更加严格了,以前的类型是在防火墙上形成映射后的公网的IP地址是保持不变的,大家要找还是能找到它的,虽然不 通,但是对于 这个对称型它就不一样了,它就发生了变化,不光是形成了这个一个IP地址和端口,而且还会形成多个,对于每一台主机都会形成一个不同的IP地址和端口对,所以这个 时候当内网主机给Pq发送请求的时候,Pq可以回来信息,其他的都回不来。但是给M主机 n端口发送数据的时候,又形成一个C和d,这个M,n在发数据的时候不会像A,b发送数据,必须向C,d发送数据 才可以过来,这样才能进行互通,这个就是对称型的NAT穿越。这个就是对称型的NAT穿越。

1.5 NAT 的用途
负载均衡:目的地址转换NAT可以重定向一些服务器的连接到其他随机选定的服务器。
失效终结:目的地址转换NAT可以用来提供高可靠性的服务。如果一个系统有一台通过路由器访问的关键服务器,一旦路由器检测到该服务器宕机,它可以使用目的地址转换NAT透明的把连接转移到一个备份服务器上。
透明代理:NAT可以把连接到因特网的HTTP连接重定向到一个指定的HTTP代理服务器以缓存数据和过滤请求。一些因特网服务提供商就使用这种技术来减少带宽的使用而不用让他们的客户配置他们的浏览器支持代理连接。

  1. NAT 打洞流程
    NAT 穿透方式:

应用层网关(ALG):一般都内置在NAT装置中,ALG会随着协议的扩充,不断更新和支持新的协议,但为每个应用协议开发ALG代码并跟踪最新标准是不可行的,ALG只能解决用户最常用的需求。此外,出于安全性需要,有些应用类型报文从源端发出就已经加密,这种报文在网络中间无法进行分析,所以ALG无能为力。
应用层网关(ALG)是解决NAT对应用层协议无感知的一个最常用方法,已经被NAT设备厂商广泛采用,成为NAT设备的一个必需功能。因为NAT不感知应用协议,所以有必要额外为每个应用协议定制协议分析功能,这样NAT网关就能理解并支持特定的协议。
ALG与NAT形成互动关系,在一个NAT网关检测到新的连接请求时,需要判断是否为已知的应用类型,这通常是基于连接的传输层端口信息来识别的。
在识别为已知应用时,再调用相应功能对报文的深层内容进行检查,当发现任何形式表达的IP地址和端口时,将会把这些信息同步转换,并且为这个新连接创建一个附加的转换表项。这样,当报文到达公网侧的目的主机时,应用层协议中携带的信息就是NAT网关提供的地址和端口。一旦公网侧主机开始发送数据或建立连接到此端口,NAT网关就可以根据关联表信息进行转换,再把数据转发到私网侧的主机。
很多应用层协议实现不限于一个初始连接(通常为信令或控制通道)加一个数据连接,可能是一个初始连接对应很多后续的新连接。比较特别的协议,在一次协商中会产生一组相关连接,比如RTP/RTCP协议规定,一个RTP通道建立后占用连续的两个端口,一个服务于数据,另一个服务于控制消息。此时,就需要ALG分配连续的端口为应用服务。
ALG能成功解决大部分协议的NAT穿越需求,但是这个方法也有很大的限制。因为应用协议的数量非常多而且在不断发展变化之中,添加到设备中的ALG功能都是为特定协议的特定规范版本而开发的,协议的创新和演进要求NAT设备制造商必须跟踪这些协议的最近标准,同时兼容旧标准。
尽管有如Linux这种开放平台允许动态加载新的ALG特性,但是管理成本仍然很高,网络维护人员也不能随时了解用户都需要什么应用。因此为每个应用协议开发ALG代码并跟踪最新标准是不可行的,ALG只能解决用户最常用的需求。
此外,出于安全性需要,有些应用类型报文从源端发出就已经加密,这种报文在网络中间无法进行分析,所以ALG无能为力。

探针技术——NAT探测和穿透协议:NAT网关无需任何修改,通过协议探测NAT类型,并对不同类型NAT实行穿透,比如STUN/TURN。当NAT可以直接穿透时,服务器协助完成穿透以及连接的建立,当NAT无法穿透时,TURN服务与STUN绑定,形成“ip-port”对,将链路中的数据包在TURN服务器上转发。相对于ALG方式有一定普遍性。但是TURN中继服务会成为通信瓶颈。而且在客户端中增加探针功能要求每个应用都要增加代码才能支持。
所谓探针技术,是通过在所有参与通信的实体上安装探测插件,以检测网络中是否存在NAT网关,并对不同NAT模型实施不同穿越方法的一种技术。
STUN服务器被部署在公网上,用于接收来自通信实体的探测请求,服务器会记录收到请求的报文地址和端口,并填写到回送的响应报文中。客户端根据接收到的响应消息中记录的地址和端口与本地选择的地址和端口进行比较,就能识别出是否存在NAT网关。如果存在NAT网关,客户端会使用之前的地址和端口向服务器的另外一个IP发起请求,重复前面的探测。然后再比较两次响应返回的结果判断出NAT工作的模式。
由前述的一对多转换模型得知,除对称型NAT以外的模型,NAT网关对内部主机地址端口的映射都是相对固定的,所以比较容易实现NAT穿越。
而对称型NAT为每个连接提供一个映射,使得转换后的公网地址和端口对不可预测。此时TURN可以与STUN绑定提供穿越NAT的服务,即在公网服务器上提供一个“地址端口对”,所有此“地址端口对”接收到的数据会经由探测建立的连接转发到内网主机上。TURN分配的这个映射“地址端口对”会通过STUN响应发给内部主机,后者将此信息放入建立连接的信令中通知通信的对端。
这种探针技术是一种通用方法,不用在NAT设备上为每种应用协议开发功能,相对于ALG方式有一定普遍性。但是TURN中继服务会成为通信瓶颈。而且在客户端中增加探针功能要求每个应用都要增加代码才能支持。

中间件技术:开发通用方法解决NAT穿越问题的努力。与前者不同之处是,NAT网关是这一解决方案的参与者。与ALG的不同在于,客户端会参与网关公网映射信息的维护,此时NAT网关只要理解客户端的请求并按照要求去分配转换表,不需要自己去分析客户端的应用层数据。其中UPnP就是这样一种方法。这种方案需要网关、内部主机和应用程序都支持UPnP技术,且组网允许内部主机和NAT网关之间可以直接交换UPnP信令才能实施。(好像经常有安全性问题)
这也是一种通过开发通用方法解决NAT穿越问题的努力。
与前者不同之处是,NAT网关是这一解决方案的参与者。
与ALG的不同在于,客户端会参与网关公网映射信息的维护,此时NAT网关只要理解客户端的请求并按照要求去分配转换表,不需要自己去分析客户端的应用层数据。其中UPnP就是这样一种方法。
UPnP中文全称为通用即插即用,是一个通用的网络终端与网关的通信协议,具备信息发布和管理控制的能力。
其中,网关映射请求可以为客户动态添加映射表项。此时,NAT不再需要理解应用层携带的信息,只转换IP地址和端口信息。而客户端通过控制消息或信令发到公网侧的信息中,直接携带公网映射的IP地址和端口,接收端可以按照此信息建立数据连接。NAT网关在收到数据或连接请求时,按照UPnP建立的表项只转换地址和端口信息,不关心内容,再将数据转发到内网。这种方案需要网关、内部主机和应用程序都支持UPnP技术,且组网允许内部主机和NAT网关之间可以直接交换UPnP信令才能实施。

中继代理技术:准确说它不是NAT穿越技术,而是NAT旁路技术。简单说,就是在NAT网关所在的位置旁边放置一个应用服务器,这个服务器在内部网络和外部公网分别有自己的网络连接。客户端特定的应用产生网络请求时,将定向发送到应用代理服务器。应用代理服务器根据代理协议解析客户端的请求,再从服务器的公网侧发起一个新的请求,把客户端请求的内容中继到外部网络上,返回的相应反方向中继。这项技术和ALG有很大的相似性,它要求为每个应用类型部署中继代理业务,中间服务器要理解这些请求。

特定协议穿越:在所有方法中最复杂也最可靠的就是自己解决自己的问题。比如IKE和IPsec技术,在设计时就考虑了到如何穿越NAT的问题。因为这个协议是一个自加密的协议并且具有报文防修改的鉴别能力,其他通用方法爱莫能助。因为实际应用的NAT网关基本都是NAPT方式,所有通过传输层协议承载的报文可以顺利通过NAT。IKE和IPsec采用的方案就是用UDP在报文外面再加一层封装,而内部的报文就不再受到影响。IKE中还专门增加了NAT网关是否存在的检查能力以及绕开NAT网关检测IKE协议的方法。

实际应用中主要有下面三种:

RTMFP
Adobe定义的一种UDP穿透NAT协议,在flash p2p中使用,也可以用openrtfmp的开源项目实现其他设备上的nat穿透。

STUN/TURN
IETF定义的一种UDP穿透规范,最为普遍。
STUN/TURN协议详解

ICE
包括ALG,STUN,TURN等各种穿透方式的组合。

2.1 NAT穿越原理
各种网络环境下的P2P通信解决方法:

(1)如果通信双方在同一个局域网内,这种情况下可以不借助任何外力直接通过内网地址通信即可;

(2)如果通信双方都在有独立的公网地址,这种情况下当然可以不借助任何外力直接通信即可;

(3)如果通信双方一方拥有独立的公网地址另一方在NAT后面,那么可以由位于NAT后面的一方主动发起通信请求;

(4)如果通信双方都位于NAT后面,且双方的NAT类型都是cone NAT,那么可以通过一个STUN服务器发现自己的NAT类型以及内网和外网传输地址映射信息,然后通过Signaling(信令服务器,实现了SIP协议的主机)交换彼此的NAT类型及内网和外网传输地址映射信息,然后通过UDP打洞的方式建立通信连接;

(5)如果通信双方有一方的NAT类型是Symmetric NAT,则无法直接建立P2P连接,这个时候就需要借助TURN(Traversal Using Relay NAT)即转发服务器来实现间接通信;

2.1.1 C1,C2向STUN发消息
交换公网IP及端口
我们要进行穿越, 其实是两台主机直接进行穿越,也就是C1,C2之间进行穿越。C1和C2之间进行穿越首先C2要知道C1的地址,C1要知道C2的地址,那就要通过STUN服务发送消息,STUN收到他们的消息之后就会拿到它们对应的公网的IP和端口;所以这个时候要进行信息交换,就是将C1的公网IP和端口交给C2,将C2的公网IP和端口交给C1;交换完这些信息之后,我们就要按照类型进行打通,如果是完全锥型的,他们就直接可以进行通讯了,已经具有公网的IP和端口了。

对于完全锥型,只要我在防火墙上建立一个映射,那任何一台主机,如果我们把STUN当作一台主机的话,那C2就可以通过C1和STUN之间的这个公网的映射然后去发送数据。

对于IP地址限制锥型,STUN 不能用这个IP地址给这个C1发消息,那因为C1是知道C2的公网IP和端口,所以首先C1向C2发送一个请求 ,C2在利用C1形成的这个IP地址和端口给它返回数据,这样他们也是可以互通的。

对于端口限制型其实也一样的,都可以通过这种方式。

2.1.2 C1->C2,C2->C1,甚至是端口猜测
对于对称型NAT就比较麻烦了,对于对称型NAT来说,由于是IP地址和端口的变化 ,那更多的是端口的随机性和增长,就是说线性增长的变化,所以并不能直接进行互通,那怎么才能通呢?其实也是有办法的,就是通过端口猜测的方式,就是通过几次的探测,然后找到它端口分配的规律,它是线性增长的,比如每次增1,或者每次增2等,还是一个 随机的,随机数是在 一定范围的,不可能所有端口都随机,在这些随机的端口范围内,实际它每一个都做一次尝试,那么这个时候就有可能打通,那么这种打通的概率就低很多。

2.2 NAT穿越成功的条件

C1的NAT类型C2的NAT类型能否穿越防火墙
完全锥型完全锥型
完全锥型受限锥型
完全锥型端口受限锥型
完全锥型对称型
受限锥型受限锥型
受限锥型端口受限锥型
受限锥型对称型
端口受限锥型端口受限锥型
端口受限锥型对称型无法打通
对称型对称型无法打通


通过上面的表,我们可以很容易看出哪些是可以穿越成功,哪些是不成的,当我们检测到一端是端口受限锥型一端是对称型或者两端都是对称型,那肯定是不可以打通的,肯定是不可以的。

2.2.1 对称NAT穿越问题
为什么对称NAT无法打洞?

假如A、B进行通信,而B处于对称NAT之下,那么A与B通信,STUN拿到A,B的公网地址和端口号都为10000,然后去协调TURN打洞,那么TURN去命令A发信息给B,则A就在NAT打了个B的洞,但是这个B的洞是端口号为10000的洞,但是下次B如果给A发信息,因为B是对称NAT,它给每个新的IP发送信息时,都重新对应一个公网端口,所以给A发送请求可能是公网10001端口,但是A只有B的10000端口被打洞过,所以B的请求就被丢弃了。
显然Server是无法协调客户端打洞的,因为协调客户端打得洞仅仅是上次对端为Server发送端口的洞,并不适用于另一个请求。此外就是NAT打的洞也是具有时效性的,如果NAT超时了,那么还是需要重新打洞的。

目前主要有下面两种方式去尝试穿越对称NAT:

同时开放TCP(SimultaneousTCP open)策略:
如果 Client A-1和 Client B-1能够彼此正确的预知对方的NAT将会给下一个TCP连接分配的公网TCP端口,并且两个客户端能够同时地发起一个面向对方的“外出”的TCP连接请求,并在对方的 SYN 包到达之前,自己刚发送出去的SYN包都能顺利的穿过自己的NAT的话,一条端对端的TCP连接就能成功地建立了。

这种策略存在的问题: 时钟严格一致,很难做到

UDP端口猜测策略:
通常,对称NAT分配端口有两种策略,一种是按顺序增加,一种是随机分配。如果这里对称NAT使用顺序增加策略,那么,ClientB-1将两次被分配的Tuples发送给Server后,Server就可以通知ClientA-1在这个端口范围内猜测刚才ClientB-1发送给它的socket-1中被NAT映射后的Tuples,ClientA-1很有可能在孔有效期内成功猜测到端口号,从而和ClientB-1成功通信。

这种策略存在的问题:不能为随机分配端口的对称型NAT打洞。

这里有篇关于对称NAT穿透技术的博客:对称NAT穿透的一种新方法

2.2.2 NAT穿透流程

如上图所示NAT穿越的基本流程主要分为5步:

实际过程会复杂很多。不同NAT类型的打洞原理不一样,不同NAT之间的穿透成功率也不一样,所以实际工程经验中,打洞流程会非常复杂。

搭建NAT穿透服务器(俗称打洞服务器)
NAT穿透的基本前提是打洞服务器,该server需要部署在公网,并有两个公网IP。Server做监听(IP-1,Port-1),(IP-2,Port-2)并根据客户端的要求进行应答。
检测客户端是否有能力进行UDP通信以及客户端是否位于NAT后
客户端向打洞服务器发起UDP请求若干次,如每次超时未返回,即认为该客户端不具备UDP通信能力(可能是防火墙或NAT阻止UDP通信);若收到打洞服务器返回的数据包,且其中携带的客户端(IP,Port)和这个客户端socket的 (LocalIP,LocalPort)比较。如果完全相同则客户端不在NAT后,这样的客户端具有公网IP可以直接监听UDP端口接收数据进行通信(检 测停止)。否则客户端在NAT后要做进一步的NAT类型检测(继续)。
NAT类型探测
基于本文第二部分中不同NAT类型的特征及打洞原理,对于全锥型、受限锥型和端口受限锥型可以较容易实现P2P通信,而对称型NAT需要额外支持别的穿透方法(且不一定能成功)。
节点间借助打洞服务器提供的信息,尝试打洞连接
打洞不成功,则使用RELAY服务
2.3 STUN和TURN的实现打洞
2.3.1 STUN 服务器做的打洞流程
STUN Server主要做了两件事:

接受客户端的请求,并且把客户端的公网IP、Port封装到ICE Candidate中。
通过一个复杂的机制,得到客户端的NAT类型。
完成了这些STUN Server就会这些基本信息发送回客户端,然后根据NAT类型,来判断是否需要TURN服务器协调进行下一步工作。

STUN是如何判断NAT的类型的:

主要可分为以下四步:

STEP1.判断客户端是否在NAT后:
B向C的IP1的pot1端口发送一个UDP 包。C收到这个包后,会把它收到包的源IP和port写到UDP包中,然后把此包通过IP1和port1发还给B。这个IP和port也就是NAT的外网 IP和port(如果你不理解,那么请你去看我的BLOG里面的NAT的原理和分类),也就是说你在STEP1中就得到了NAT的外网IP。
熟悉NAT工作原理的朋友可以知道,C返回给B的这个UDP包B一定收到。如果在你的应用中,向一个STUN服务器发送数据包后,你没有收到STUN的任何回应包,那只有两种可能:1、STUN服务器不存在,或者你弄错了port。2、你的NAT拒绝一切UDP包从外部向内部通过。
当B收到此UDP后,把此UDP中的IP和自己的IP做比较,如果是一样的,就说明自己是在公网,下步NAT将去探测防火墙类型,我不想多说。如果不一样,说明有NAT的存在,系统进行STEP2的操作。
STEP2.判断是否处于Full Cone Nat下:
B向C的IP1发送一个UDP包,请求C通过另外一个IP2和PORT(不同与SETP1的IP1)向B返回一个UDP数据包(现在知道为什么C要有两个IP了吧,虽然还不理解为什么,呵呵)。
我们来分析一下,如果B收到了这个数据包,那说明什么?说明NAT来着不拒,不对数据包进行任何过滤,这也就是STUN标准中的full cone NAT。遗憾的是,Full Cone Nat太少了,这也意味着你能收到这个数据包的可能性不大。如果没收到,那么系统进行STEP3的操作。
STEP3.判断是否处于对称NAT下:
B向C的IP2的port2发送一个数据包,C收到数据包后,把它收到包的源IP和port写到UDP包中,然后通过自己的IP2和port2把此包发还给B。
和step1一样,B肯定能收到这个回应UDP包。此包中的port是我们最关心的数据,下面我们来分析:
如果这个port和step1中的port一样,那么可以肯定这个NAT是个CONE NAT,否则是对称NAT。道理很简单:根据对称NAT的规则,当目的地址的IP和port有任何一个改变,那么NAT都会重新分配一个port使用,而在step3中,和step1对应,我们改变了IP和port。因此,如果是对称NAT,那这两个port肯定是不同的。
如果在你的应用中,到此步的时候PORT是不同的,那么这个它就是处在一个对称NAT下了。如果相同,那么只剩下了restrict cone 和port restrict cone。系统用step4探测是是那一种。
STEP4.判断是处于Restrict Cone NAT还是Port Restrict NAT之下:
B向C的IP2的一个端口PD发送一个数据请求包,要求C用IP2和不同于PD的port返回一个数据包给B。
我们来分析结果:如果B收到了,那也就意味着只要IP相同,即使port不同,NAT也允许UDP包通过。显然这是Restrict Cone NAT。如果没收到,没别的好说,Port Restrict NAT.
到这里STUN Server一共通过这4步,判断出客户端处于什么类型的NAT下,然后去做后续的处理:
这4步都会返回给客户端它的公网IP、Port和NAT类型,除此之外:

如果A处于公网或者Full Cone Nat下,STUN不做其他的了,因为其他客户端可以直接和A进行通信。

  1. 如果A处于Restrict Cone或者Port Restrict NAT下,STUN还会协调TURN进行NAT打洞

  1. 如果A处于对称NAT下,那么点对点连接下,NAT是无法进行打洞的。所以为了通信,只能采取最后的手段了,就是转成C/S架构了,STUN会协调TURN进行消息转发。

2.3.2 TURN 服务器做的事情
为NAT打洞:如果A和B要互相通信,那么TURN Server,会命令A和B互相发一条信息,这样各自的NAT就留下了对方的洞,下次他们就可以之间进行通信了。
为对称NAT提供消息转发:当A或者B其中一方是对称NAT时,那么给这一方发信息,就只能通过TURN Server来转发了。

  1. NAT 类型检测算法
    上面的NAT穿透原理中,就是需要根据NAT的类型来判断是否能够穿越,那么我们如何去判断NAT网络的类型呢?

接下来我讲一下NAT类型的检测,看看我们自己是属于哪种NAT类型,是否可以打洞 成功,
下面我就看一下这个整体的判断逻辑,当然在这之前我要有一个限定条件,就是在我们的云端一定要部署一个STUN服务。这个STUN服务要有两个IP地址和端口,这两个IP地址的作用稍后我们会在逻辑判断的过程中给大家介绍。

下来卡一张NAT类型判断的算法流程图:

上面的流程大致如下:

首先客户端要发送一个ECHO请求给服务端,服务端收到请求之后会通过同样的IP地址和端口在给我们返回一个信息回来。
那在客户端就要等这个消息回复,那么设置一个超时器,看每个消息是否可以按时回来,那如果我们发送的数据没有回来,则说明这个UDP是不通的,我们就不要再进行判断了。
如果我们收到了服务端的响应,那么就能拿到我们这个客户端出口的公网的IP地址和端口,这个时候要判断一下公网的IP地址和本机的IP地址是否是一致的,如果是一致的,说明我就没有在NAT之后而是一个公网地址;
接下来要做进一步判断,就是判断我们的公网地址是不是一个完全的公网地址,这时我们再发送一个信息到第一个IP地址和端口,那服务端收到这个请求之后呢,它使用第二个IP地址和端口给我们回消息,如果我们真是一个完全的公网IP地址和端口提供一个服务的话,那其他任何公网上的主机都可以向我发送请求和回数据,这时候我都是能收到的,那如果我能收到,那就说明就是一个公网的地址,所以我们就没有在NAT之后就完全可以接收数据了。
如果我们收不到,那说明我是在一个防火墙之后,而且一个对称的防火墙。如果我收到的公网的IP与我本地的IP不一致,那就说明我们确实是在NAT之后,那既然是在NAT之后我们就要对各种类型作判断了。
这时我们再发送一个请求到第一个IP地址和端口,那它是通过第二个IP地址和端口给我们回消息,那这时候我们要判断我们的类型是不是完全锥型,如果我们出去一个请求,在我们的NAT服务和网关上建立了一个内网地址和外网地址的映射表之后,那其他公网 上 的主机都可以向我这个公网IP地址发送消息,并且我可以接收到,那么这个时候可以收到的话,我们就是一个完全锥型NAT。
如果收不到的话,需要做进一步的判断,这时候需要向服务端的第二个IP地址和端口发送数据,那么此时服务端回用同样的IP地址和端口给我们回数据,那么这时候它也会带回一个公网的IP地址来,但是如果我们的出口,就是向第二个IP地址发送了请求带的出口的IP地址与我们第一发送的请求带回的IP地址如果是不一样的,那就说明是对称型NAT;对称型NAT每次出去都会形成 不同的IP地址和端口。
如果一样就说明是限制型的,限制型分为两种一种是IP限制型一种是端口限制型,所以还需要做进一步的检测,所这个时候我们再向第一个IP地址和端口发送一个请求,那么回信息的时候是同一个IP地址和不同的端口号,那么这时候我们就可以判断是否可以接收到,如果不能接收到,说明是对端口做了限制,所以是端口限制型的NAT,如果可以收到就说明是一个IP地址限制型的NAT。
经过这样一个逻辑判断之后 ,我就可以知道我们自己这台在内网的主机是什么NAT类型了
了解了逻辑之后,我们再来看一下检测过程:

首先是我们的客户端,通过第一IP地址和端口,发送一个请求过去回来一个响应,如果回来这个地址和 我们之前发送的地址是一致的,那就是公网的。如果不一致说明我们是在NAT之后,这是第一次检测。 如下图所示:

再接下来就是向第一个IP地址发送了一个请求 ,然后它通过第二个IP地址给我回一个请求,如果这次回来的IP地址与上次回来的IP是不一样的,它就是对称型NAT;如果一样还需要进一步判断,如下图:

紧接着再发送一个请求到第一个这个地址,那么它用这个 地址的第二个端口向我回消息,如果这时候我是能收到的,说明是IP地址限制锥型NAT,如果不能收到说明是端口限制锥型。如下图:

  1. ICE进行NAT穿透原理
    在通常的ICE部署环境中,我们有两个客服端想要建立通信连接,他们可以直接通过signaling服务器(如SIP服务器)执行offer/answer过程来交换SDP消息。

在ICE过程开始的时候,客服端忽略他们各自的网络拓扑结构,不管是不是在NAT设备后面或者多个NAT后面,ICE允许客服端发现他们的所在网络的拓扑结构的信息,然后找出一个或者更多的可以建立通信连接的路径。

如上图显示了一个典型的ICE部署环境,客服端L和R都在各自的NAT设备后面,下面简单描述下ICE建立通信的过程:

(1)L和R先分别通过STUN和TURN服务器获取自己的host address,server-reflexive address、relayed address(和TURN转发服务器相对应的地址),其中server-reflexive address和relayed address通过定时刷新保证地址不过期。这些地址通常叫做candinate地址。

(2)给这些candinate地址分配优先级排序并格式化成SDP格式,通过SIP服务器交换彼此的SDP;

(3)交换完成后根据一定的原则把本地的候选和远程的候选进行配对,每一对都有自己的优先级并根据优先级进行排序后放入Check列表里面(两边都会有相同的Check列表)。

(4)然后进行连接性测试,测试前会选择一个客服端扮演Controlled角色和另一个扮演Controling角色,连通性检查完成后扮演Controling角色的客服端负责在有效的Candinate对列表里面选择一个作为一个被选中的传输通道并通知Controlled的客服端。

(5)利用被选中的candinate地址对进行通信。

4.1 ICE进行NAT穿透代码实现
这里的样例代码采用ICE4J来实现,ICE4J的API文档可以参考http://bluejimp.com/jitsi/ice4j/javadoc/,在这个实现里面没有利用SIP服务器进行SDP信息的交换而是采用手动输入的方式,在生产环境中可以部署一台socket.io或者其他SIP服务器

代码如下:

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.ice4j.Transport;
import org.ice4j.TransportAddress;
import org.ice4j.ice.Agent;
import org.ice4j.ice.Component;
import org.ice4j.ice.IceMediaStream;
import org.ice4j.ice.IceProcessingState;
import org.ice4j.ice.LocalCandidate;
import org.ice4j.ice.NominationStrategy;
import org.ice4j.ice.RemoteCandidate;
import org.ice4j.ice.harvest.StunCandidateHarvester;
import org.ice4j.ice.harvest.TurnCandidateHarvester;
import org.ice4j.security.LongTermCredential;

import test.SdpUtils;

public class IceClient {

 private int port;

 private String streamName;

 private Agent agent;

 private String localSdp;

 private String remoteSdp;

 private String[] turnServers = new String[] { "stun.jitsi.net:3478" };

 private String[] stunServers = new String[] { "stun.stunprotocol.org:3478" };

 private String username = "guest";

 private String password = "anonymouspower!!";

 private IceProcessingListener listener;

 static Logger log = Logger.getLogger(IceClient.class);

 public IceClient(int port, String streamName) {
      this.port = port;
      this.streamName = streamName;
      this.listener = new IceProcessingListener();
 }

 public void init() throws Throwable {

      agent = createAgent(port, streamName);

      agent.setNominationStrategy(NominationStrategy.NOMINATE_HIGHEST_PRIO);

      agent.addStateChangeListener(listener);

      agent.setControlling(false);

      agent.setTa(10000);

      localSdp = SdpUtils.createSDPDescription(agent);

      log.info("=================== feed the following"
                + " to the remote agent ===================");

      System.out.println(localSdp);

      log.info("======================================"
                + "========================================\n");
 }

 public DatagramSocket getDatagramSocket() throws Throwable {

      LocalCandidate localCandidate = agent
                .getSelectedLocalCandidate(streamName);

      IceMediaStream stream = agent.getStream(streamName);
      List<Component> components = stream.getComponents();
      for (Component c : components) {
           log.info(c);
      }
      log.info(localCandidate.toString());
      LocalCandidate candidate = (LocalCandidate) localCandidate;
      return candidate.getDatagramSocket();

 }

 public SocketAddress getRemotePeerSocketAddress() {
      RemoteCandidate remoteCandidate = agent
                .getSelectedRemoteCandidate(streamName);
      log.info("Remote candinate transport address:"
                + remoteCandidate.getTransportAddress());
      log.info("Remote candinate host address:"
                + remoteCandidate.getHostAddress());
      log.info("Remote candinate mapped address:"
                + remoteCandidate.getMappedAddress());
      log.info("Remote candinate relayed address:"
                + remoteCandidate.getRelayedAddress());
      log.info("Remote candinate reflexive address:"
                + remoteCandidate.getReflexiveAddress());
      return remoteCandidate.getTransportAddress();
 }

 /**
 * Reads an SDP description from the standard input.In production
 * environment that we can exchange SDP with peer through signaling
 * server(SIP server)
 */
 public void exchangeSdpWithPeer() throws Throwable {
      log.info("Paste remote SDP here. Enter an empty line to proceed:");
      BufferedReader reader = new BufferedReader(new InputStreamReader(
                System.in));

      StringBuilder buff = new StringBuilder();
      String line = new String();

      while ((line = reader.readLine()) != null) {
           line = line.trim();
           if (line.length() == 0) {
                break;
           }
           buff.append(line);
           buff.append("\r\n");
      }

      remoteSdp = buff.toString();

      SdpUtils.parseSDP(agent, remoteSdp);
 }

 public void startConnect() throws InterruptedException {

      if (StringUtils.isBlank(remoteSdp)) {
           throw new NullPointerException(
                     "Please exchange sdp information with peer before start connect! ");
      }

      agent.startConnectivityEstablishment();

      // agent.runInStunKeepAliveThread();

      synchronized (listener) {
           listener.wait();
      }

 }

 private Agent createAgent(int rtpPort, String streamName) throws Throwable {
      return createAgent(rtpPort, streamName, false);
 }

 private Agent createAgent(int rtpPort, String streamName,
           boolean isTrickling) throws Throwable {

      long startTime = System.currentTimeMillis();

      Agent agent = new Agent();

      agent.setTrickling(isTrickling);

      // STUN
      for (String server : stunServers){
           String[] pair = server.split(":");
           agent.addCandidateHarvester(new StunCandidateHarvester(
                     new TransportAddress(pair[0], Integer.parseInt(pair[1]),
                               Transport.UDP)));
      }

      // TURN
      LongTermCredential longTermCredential = new LongTermCredential(username,
                password);

      for (String server : turnServers){
           String[] pair = server.split(":");
           agent.addCandidateHarvester(new TurnCandidateHarvester(
                     new TransportAddress(pair[0], Integer.parseInt(pair[1]), Transport.UDP),
                     longTermCredential));
      }
      // STREAMS
      createStream(rtpPort, streamName, agent);

      long endTime = System.currentTimeMillis();
      long total = endTime - startTime;

      log.info("Total harvesting time: " + total + "ms.");

      return agent;
 }

 private IceMediaStream createStream(int rtpPort, String streamName,
           Agent agent) throws Throwable {
      long startTime = System.currentTimeMillis();
      IceMediaStream stream = agent.createMediaStream(streamName);
      // rtp
      Component component = agent.createComponent(stream, Transport.UDP,
                rtpPort, rtpPort, rtpPort + 100);

      long endTime = System.currentTimeMillis();
      log.info("Component Name:" + component.getName());
      log.info("RTP Component created in " + (endTime - startTime) + " ms");

      return stream;
 }

 /**
 * Receive notify event when ice processing state has changed.
 */
 public static final class IceProcessingListener implements
           PropertyChangeListener {

      private long startTime = System.currentTimeMillis();

      public void propertyChange(PropertyChangeEvent event) {

           Object state = event.getNewValue();

           log.info("Agent entered the " + state + " state.");
           if (state == IceProcessingState.COMPLETED) {
                long processingEndTime = System.currentTimeMillis();
                log.info("Total ICE processing time: "
                          + (processingEndTime - startTime) + "ms");
                Agent agent = (Agent) event.getSource();
                List<IceMediaStream> streams = agent.getStreams();

                for (IceMediaStream stream : streams) {
                     log.info("Stream name: " + stream.getName());
                     List<Component> components = stream.getComponents();
                     for (Component c : components) {
                          log.info("------------------------------------------");
                          log.info("Component of stream:" + c.getName()
                                    + ",selected of pair:" + c.getSelectedPair());
                          log.info("------------------------------------------");
                     }
                }

                log.info("Printing the completed check lists:");
                for (IceMediaStream stream : streams) {

                     log.info("Check list for  stream: " + stream.getName());

                     log.info("nominated check list:" + stream.getCheckList());
                }
                synchronized (this) {
                     this.notifyAll();
                }
           } else if (state == IceProcessingState.TERMINATED) {
                log.info("ice processing TERMINATED");
           } else if (state == IceProcessingState.FAILED) {
                log.info("ice processing FAILED");
                ((Agent) event.getSource()).free();
           }
      }
 }

}

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.util.concurrent.TimeUnit;

public class PeerA {

 public static void main(String[] args) throws Throwable {
      try {
           IceClient client = new IceClient(2020, "audio");
           client.init();
           client.exchangeSdpWithPeer();
           client.startConnect();
           final DatagramSocket socket = client.getDatagramSocket();
           final SocketAddress remoteAddress = client
                     .getRemotePeerSocketAddress();
           System.out.println(socket.toString());
           new Thread(new Runnable() {

                public void run() {
                     while (true) {
                          try {
                               byte[] buf = new byte[1024];
                               DatagramPacket packet = new DatagramPacket(buf,
                                         buf.length);
                               socket.receive(packet);
                               System.out.println("receive:"
                                         + new String(packet.getData(), 0, packet
                                                   .getLength()));
                          } catch (IOException e) {
                               // TODO Auto-generated catch block
                               e.printStackTrace();
                          }

                     }
                }
           }).start();

           new Thread(new Runnable() {

                public void run() {
                     int count = 1;
                     while (true) {
                          try {
                               byte[] buf = ("send msg " + count++ + "").getBytes();
                               DatagramPacket packet = new DatagramPacket(buf,
                                         buf.length);

                               packet.setSocketAddress(remoteAddress);
                               socket.send(packet);
                               System.out.println("send msg");
                               TimeUnit.SECONDS.sleep(10);
                          } catch (Exception e) {
                               // TODO Auto-generated catch block
                               e.printStackTrace();
                          }

                     }
                }
           }).start();
      } catch (Exception e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
      }

 }

}
参考:https://www.jianshu.com/p/84e8c78ca61d

NAT穿透技术、穿透原理和方法详解
IPSec及IKE原理
NAT概述
Peer-to-Peer Communication Across Network Address Translators
P2P原理和常见实现方式

未经允许不得转载:智慧,启迪人生 » WebRTC源码研究 NAT打洞原理

打赏

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏