网络协议之NAT穿透
NAT
IPv4地址只有32位,最多只能提供大致42.9亿个唯一IP地址,当设备越来越多时,IP地址变得越来越稀缺,不能为每个设备都分配一个IP地址。于是,作为NAT规范就出现了。NAT(Network Address Translation,网络地址转换)是1994年提出的,其当在专用网内部的一些主机本来已经分配到了本地IP地址(即仅在本专用网内使用的专用地址),但现在又想和因特网上的主机通信(并不需要加密)时,可使用NAT方法。每个NAT设备负责维护一个包含本地IP、端口和外网IP、端口的映射表。所有使用本地地址的主机在和外界通信时,都要在NAT路由器上将其本地地址转换成全球IP地址,才能和因特网连接。其大致过程如下:
NAT的实现方式有如下三种,即:
- 静态转换(Static NAT):将内部网络的私有IP地址转换为公有IP地址,IP地址对是一对一的,是一成不变的,某个私有IP地址只转换为某个公有IP地址;
- 动态转换(Dynamic NAT):将内部网络的私有IP地址转换为公用IP地址时,IP地址是不确定的,是随机的,所有被授权访问上Internet的私有IP地址可随机转换为任何指定的合法IP地址,当ISP提供的合法IP地址略少于网络内部的计算机数量时。可以采用动态转换的方式;
- 端口多路复用(Port NAT):改变外出数据包的源端口并进行端口转换,即端口地址转换。采用端口多路复用方式,内部网络的所有主机均可共享一个合法外部IP地址实现对Internet的访问,从而可以最大限度地节约IP地址资源。同时,又可隐藏网络内部的所有主机,有效避免来自internet的攻击。因此,目前网络中应用最多的就是端口多路复用方式。
UDP连接状态超时
目前,很多网络都使用了NAT技术,而NAT需要保存数据传输的路由表才能完成工作。每个TCP连接有一个明确的协议状态机,开始三次握手,跟着开始数据传输,最后关闭连接,有一个完整的流程。基于这种流程,NAT可以观察到每个连接状态,并可以根据需要创建和删除的路由条目。
然而,UDP是面向无连接的,仅仅只往外发送一个带有载荷的数据报就不再关心其他额外的事情了,但路由响应却需要能从转换表找到本地主机IP和端口,只有如此才能完成数据的传输。UDP既没有握手,也没有连接终止,同时没有任何状态机来监控连接状态。转换器需要保存每个UDP流的状态,进而维护转换表,然而UDP实际上却是无状态的,仅仅只是一个数据报而已,没有提前协商报文,也没有结束状态。由于UDP没有连接终止通告,任何时候,两端都可以停止发送数据包,不带任何通知,就对为维护转换表带来了极大的挑战,因为转换表大多数时候都是动态更新的。为了解决这个问题,UDP路由记录会设置一个定时器进行定时过期,这个时间的设置取决于设备提供商,版本,配置等。因此,有一个事实上的最佳做是引入双向 keepalive报文,周期性的重置路由上所有的NAT设备转换记录计时器。
NAT穿透
不可预知的连接状态管理是NAT的一个严重问题,但对于许多应用程序的一个更大的问题是根本无法建立UDP连接。这对很多应用譬如P2P,如VoIP、游戏、文件共享等,这些应用往往通信双方的角色需要转换,以实现双向通信。
NAT带来的第一个问题是,内部客户端不知道它的外网IP,只知道它的内部IP,NAT设备负责对UDP数据报进行重写,修改UDP包的源端口和地址,以及IP分组中的源IP地址。如果客户端使用内网IP地址与外网主机进行通信,那么连接将不可避免地失败。因此,NAT这种“透明”的转换就有问题了,如果它需要与外网中的主机进行通信,应用程序必须先知道自己的外网IP地址。
然而,仅仅知道的自己的外网IP依然是无法保证UDP传输成功的。任何数据包到达拥有外网IP的NAT设备后,还需要有一个目的端口,才能从NAT转换表中找到对应的内网IP和端口,这样数据才能真正达到目的地址。如果不能在转换表中找到对应的映射,那么数据报就被直接丢弃。也就是说NAT作为一个简单的包过滤器,只有在转换表中找到对应的路由,才能完成信息传递,否则就会不能成功传输数据。
如果隔着NAT设备,那种客户端(内网主机作为服务器)处理来自P2P应用(如VoIP,游戏终端,文件共享等)的入站连接时,就需要面对NAT穿透问题。为了解决这种UDP穿透问题,很多穿越技术(STUN,TURN,ICE)被提出了,用于在UDP主机之间建立端至端的连接。
-
STUN
STUN(Simple Traversal Utilities for NAT)协议允许让位于内网的客户端发现网络中的地址转换器,进而找到NAT为自己配置的外网IP和端口。要想实现这种功能,还需要一个已知的第三方STUN服务器支持,示意图如下:
假设STUN服务器的IP地址是可知的(通过DNS或手动指定),客户端首先发送绑定请求到STUN服务器。相应的,STUN服务器回复一个响应,其中包含为客户端分配的外网IP和端口。这个简单的流程解决我们了我们前面讨论中遇到的几个问题:
- 客户端可以知道自己的外网IP和端口,使用这些信息就能够与对端进行通信;
- 向STUN服务器发送的请求,也同时在NAT上建立了路由映射记录,这确保了外部主机的请求可以发送到内部网络中的客户端;
- STUN协议定义了一个简单keep-alive探测机制来保证NAT上的路由不超时。
有了这个机制,两端需要通过UDP进行通信时,它们会先发送绑定请求到各自的STUN服务器,收到各自STUN服务器的响应,然后分别使用各自的外网IP和端口进行通信。然而,在实际应用中,STUN是不足以处理所有的NAT的拓扑结构和网络配置。在某些情况下,UDP可能会被防火墙或其他一些网络设备完全阻止 ,为了解决这个问题,我们还可以使用TURN协议作为备用方案,它可以运行在UDP上,在最坏的情况下还可以将UDP转换成TCP。
-
TURN
TURN(Traversal Using Relays around NAT)通过Relay方式穿越NAT,TURN应用模型通过分配TURNServer的地址和端口作为客户端对外的接受地址和端口,即私网用户发出的报文都要经过TURNServer进行Relay转发,在报文负载中所描述的地址信息直接填写TURNServer地址的方式进行通信。示意图如下所示:
当然,这种通信方式的最明显的缺点就是它不再是P2P的通信。他需要依赖于TURN服务器来保证可靠的传输,TURN服务器就成为了一个瓶颈,维护TURN的成本将很高,至少TURN服务器需要足够的带宽来保证所有的数据流。因此,TURN方案最好作为最后的备用方案,只有在其他方案都失效的情况下才能使用。
在现实场景中,NAT设备很多,相当一部分用户不能通过STUN直接建立p2p连接,如果想提供可靠的UDP服务,还需要同时支持STUN与TURN。
-
ICE
建立一个有效的NAT穿越解决方案,不是一件简单容易的事情。值得庆幸的是,我们可以借助ICE协议来帮助我们完成这一任务。ICE是一个协议,和一组方法,用来寻求最有效的端与端之间隧道建立方法:如果可能则直接连接,如果不行则通过STUN进行协商,如果都失败了则采取TURN。示意图如下:
UDP相比于TCP最大的特征正是它忽略了的功能:连接状态、握手、重发、重组、重排、拥塞控制、拥塞预防、流量控制,甚至可选的错误检查。任何事情都是有利有弊的,在忽略了这么多特性之后,这个面向消息的传输层能提供了很大的灵活性,当然也为实现者带来了一些麻烦。客户端的应用程序可能从头开始重新实现部分或者大部分缺失的特性,而且每项功能的实现都得保证与网络中其他主机与协议和谐共生。
与TCP不同,内置了流量和拥塞控制、拥塞避免机制,UDP应用程序必须自己实现这些机制。拥塞不敏感的UDP应用程序可以很容易的拥塞网络,可能会导致网络性能降低,在严重的情况下,会导致网络拥塞崩溃。如果想在自己的应用程序中使用UDP,一定要认真研究和学习当下的最佳实践和建议。RFEC5405对设计单播UDP应用程序给了很多建议,简述如下:
- 应用必须忍受变化的互联网路径;
- 应用应控制传输速率;
- 应用应当实现所有流量拥塞控制;
- 应用应该使用和TCP同等的带宽;
- 应用当丢包时应该回退重传计数器;
- 应用不应该发送超过MTU的数据报;
- 应用应该处理数据报的丢失,重复,重新排序;
- 应用应该是确保可以支持两分钟的延迟;
- 应用应该启用IPv4 UDP校验,必须启用IPv6校验;
- 应用可能在需要的时候使用保活(最小间隔15秒)。
随着WebRTC规范的提出,WebRTC为浏览器提供了新的能力,相信UDP会变得越来越重要!
相关文章