网络协议之 NAT 穿透原理
NAT
IPv4 地址只有 32 位,最多只能提供大致 42.9 亿个唯一 IP 地址,当设备越来越多时,IP 地址变得越来越稀缺,不能为每个设备都分配一个 IP 地址。于是,作为 NAT 规范就出现了。NAT(Network Address Translation,网络地址转换)是 1994 年提出的,其当在专用网内部的一些主机本来已经分配到了本地 IP 地址(即仅在本专用网内使用的专用地址),但现在又想和因特网上的主机通信(并不需要加密)时,可使用 NAT 方法。每个 NAT 设备负责维护一个包含本地 IP、端口和外网 IP、端口的映射表。所有使用本地地址的主机在和外界通信时,都要在 NAT 路由器上将其本地地址转换成全球 IP 地址,才能和因特网连接。其大致过程如下:
文章推荐视频:
C++架构师学习地址:C/C++Linux服务器开发高级架构师/Linux后台架构师
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 主机之间建立端至端的连接。
C/C++Linux 后台服务器开发高级架构师学习视频 点击C++架构师学习视频 获取,内容知识点包括 Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux 内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK 等等。免费学习地址:C/C++Linux服务器开发高级架构师/Linux后台架构师
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 会变得越来越重要!
UDP 以及 WebRTC 相关视频讲解:
评论