好久没有写也没有填系列文章了,正好之前文章提到了 Fullcone,所以干脆写一篇文章来好好聊聊 NAT 相关的内容。NAT 作为当今现实网络中不可或缺的一部分,虽然应用广泛,但是对它的介绍却远不及其他网络协议。另一方面,IETF 也把 NAT 视为 IPv4 的权宜之计,在很长一段时间内都寄解决地址短缺问题之希望于大力推广 IPv6。从 RFC 的提出时间就可以看出,很多 NAT 穿透相关的 RFC 提出时间都晚 IPv6 不少。而现在看来,IPv6 的推广乃至 IPv4 的废弃还有相当长的路要走,所以可以预见,NAT 还将陪伴我们不少时日。
NAT 的概念
NAT(Network address translation)就是网络地址转换技术。按照 Wikipedia 的解释,它就是一个在路由设备上修改 IP 首部的地址,从而把一个地址变成另一个地址的技术,简而言之就是针对 IP 地址的重命名。比如在路由器上设定把来自 A 网络的 IP 包中的地址 10.0.0.2
重命名成 10.1.1.3
,然后转发到 B 网络,反之亦然。这样对于 B 网络来说,访问 10.1.1.3
就等同于访问 A 网络中的 10.0.0.2
了。更加复杂的 NAT 技术还可能涉及对 TCP、UDP 协议中端口号的修改,不过总而言之,NAT 就是一个修改数据包头部完成 “重命名” 的技术。
目前 NAT 技术最广泛的应用是解决 IPv4 地址短缺问题。它的思路非常简单,就是重复利用同一个 IP 地址,并在路由器转发数据包的时候进行 “重命名”。比如在常识中,无论在家、学校还是餐厅里网上冲浪,路由器管理页面的地址总是 192.168.0.1
、手机的地址也总是 192.168
开头。而 IP 协议中要求每个设备都有不同的 IP 地址,否则就会混淆不同的设备。之所以我们还能继续网上冲浪,就是因为路由器上使用了 NAT 技术把这些 192.168
开头的内网地址 “重命名” 成路由器自身的地址,然后转发给互联网。这样,不同的内网就可以使用同一个内网地址(比如学校和家里都有可能有 192.168.0.233
这个设备),但也不影响它们接入互联网。而如何完成 “重命名” 并避免可能发生的冲突就是 NAT 技术的关键。
NAT 的种类(主要是传统 NAT)
要进一步理解 NAT,首先就是了解 NAT 的分类。RFC2663 把 NAT 分成了四类:传统 NAT、双向 NAT、两次 NAT、多宿主 NAT。由于最常见的就是传统 NAT,所以我就偷个懒,只介绍传统 NAT 了。
传统 NAT 主要做的就是维护一个内部网络,就像上一节里介绍的那样。它位于内部网络与外部网络(比如互联网)之间,保证内网地址不会被泄露到外网中去。如果再对重命名方式进行细分,传统 NAT 还可以分成两类:基本 NAT(Basic NAT)、NAPT(Network Address Port Translation,网络地址端口转换)。
基本 NAT
基本 NAT 就是只针对 IP 地址的 “重命名”。由于基本 NAT 并不考虑更高层的协议,所以它只是实现了一个内部 IP 地址到外部 IP 地址的一一对应。不妨把已使用的内部 IP、NAT 设备拥有的外部 IP 看成 In、Ex 两个集合。如果内部 IP 数量更少,∣In∣≤∣Ex∣\left| In \right| \leq \left| Ex \right|∣In∣≤∣Ex∣,那么每个内部地址都能被映射到一个外部地址。如果外部地址数量少于内部 IP 的话,∣In∣>∣Ex∣\left| In \right| \gt \left| Ex \right|∣In∣>∣Ex∣,就不能保证同一时间每个内部设备都能访问外部网络了(可能分配不到外部地址)。
NAPT
不难看出,基本 NAT 对于 IP 地址的复用效果相当有限。假设如果 NAT 设备只有一个外部地址的话,同一时间就只能有一个内部设备可以访问外部了,显然这对我们网上冲浪带来了极大地不便。NAPT 对此的解决方法是,考虑高层传输协议 TCP、UDP 的端口(其实不只是端口,任何传输层标志都行,比如 ICMP 的 ID),以 (IP地址, 端口)
为单位进行重命名。这样操作空间就突然变大了 65535 倍,复用效率直接拉满。所以大多数路由设备都实现了 NAPT,日常生活中见到最多的也是 NAPT。平常我们说的 NAT 也基本上就指的是 NAPT。
鉴于 NAPT 的重要性,接下来的文章就着重介绍下不同的 NAPT 类型和它们的实现原理。
例子:NAPT 的基本过程
通过之前对 NAT 大致分类的介绍,相信你对 NAT 已经建立了一个大致的印象。接下来我想通过一个例子来详细介绍下 NAPT 的原理。
常见的 NAPT 拓扑
图示是一种常见的 NAPT 拓扑。当内网设备访问访问目标时,它发送包 [iAddr:iPort -> dAddr:dPort]
给路由器。路由器的 NAPT 程序转换内部地址,改写包为 [eAddr:ePort -> dAddr:dPort]
,之后转发到外部网络。反之,当访问目标答复内网设备时,它发送包 [dAddr:dPort -> eAddr:ePort]
给路由器,路由器接收后通过 NAPT 程序改写包为 [dAddr:dPort -> iAddr:iPort]
然后转发给内网设备。可以看到,发送过程(内部到外部)中 NAPT 程序改写数据包的源地址,进行源 NAT(SNAT)。在接受过程(外部到内部)中改写数据包的目标地址,进行目标 NAT(DNAT)。这两个相对应的过程一并组成了 NAPT。
在改写包的过程中,最关键的过程就是确定 eAddr
与 ePort
。这也是不同 NAPT 实现的主要区别。
RFC3489 分类:锥形与对称
由于 NAPT 用到了传输层协议的标志,因此具体实现无法脱离具体的传输层协议,所以对 NAPT 的分类也是和传输层协议挂钩的。首先介绍的就是最常见的一种对 UDP NAT 的分类:RFC3489 分类。RFC3489 把 UDP NAT 分成了:全锥体 NAT(Full Cone NAT)、地址限制锥体 NAT(Restricted Cone)、端口限制锥体 NAT(Port Restricted Cone)、对称 NAT(Symmetric NAT)。
理解这些不同 NAT 的关键是理解它们各自的实现原理,这就要介绍一个关键的数据结构 ——NAT 表。NAT 表记录了一次 NAT 需要的全部信息,比如内部地址、外部地址、过期时间等等。在发送、接收时,NAT 设备会根据数据包中地址查表或在不存在记录时填表,从而确定改写的 eAddr 与 ePort。
全锥体 NAT
- 全锥体 NAT 的 NAT 表存储:
(iAddr, iPort_[, eAddr]_, ePort)
。由于一般情况下 NAT 设备都只有一个外部地址(除非是运营商 NAT 等等大型网络),所以之后我都会省略eAddr
。之后出现ePort
的地方都可默认为(eAddr, ePort)
。 - 发送时,通过
(iAddr, iPort)
查出(ePort)
。如果查不到相关记录,则分配一个ePort
并记录到 NAT 表。 - 接收时,通过
(ePort)
查出(iAddr, iPort)
可以发现,全锥体 NAT 下一个外部 IP 上的端口 ePort
,会被唯一分给一个内网设备的端口 iAddr:iPort
。所以同一时间内全锥体 NAT 最多只能分配 65535(实际还要少很多)个内网设备端口。而由于端口分配是一一对应的,所以其他外部地址也能通过 ePort
访问到 iAddr:iPort
。
全锥体 NAT(图源 Wikipedia)
地址限制锥体 NAT
- 地址限制锥体 NAT 的 NAT 表存储:
(iAddr, iPort, ePort, dAddr)
- 发送时,通过
(iAddr, iPort)
查出(ePort)
- 接收时,通过
(ePort, dAddr)
查出(iAddr, iPort)
可以看到,比起全锥体 NAT,地址限制锥体 NAT 多存储了一个 dAddr
。这让我们可以在一定程度上复用 NAT 设备的外部端口 ePort
。比如对于这样的 NAT 表:
(192.168.10.2, 10086, 11451, 6.6.6.6)
(192.168.10.3, 10086, 11451, 8.8.8.8)
不难看出,11451 这个 ePort
得以被分给两个映射。不过坏处是,不同的外部服务器 dAddr'
就没办法通过 ePort
访问之前的映射了,只有同一服务器的另一端口 dAddr:dPort'
可以访问。
地址限制锥体 NAT(图源 Wikipedia)
端口限制锥体 NAT
- 端口限制锥体 NAT 的 NAT 表存储:
(iAddr, iPort, ePort, dAddr, dPort)
- 发送时,通过
(iAddr, iPort)
查出(ePort)
- 接收时,通过
(ePort, dAddr, dPort)
查出(iAddr, iPort)
可以发现,它比地址受限型 NAT 还要多查找了一个 dPort
。因此如今同一个 dAddr:dPort
可以连接最多 65535 个内部端口 iAddr:iPort
了。比如对于 NAT 表:
(192.168.10.2, 10086, 11451, 6.6.6.6, 23333)
(192.168.10.3, 10086, 11451, 6.6.6.6, 10000)
可以看到它进一步提升了 ePort
的复用效果。当然代价是同一服务器的其他端口也不能通过 ePort
访问之前的映射了。
端口限制锥体 NAT(图源 Wikipedia)
对称 NAT
- 对称 NAT 的 NAT 表存储:
(iAddr, iPort, ePort, dAddr, dPort)
- 发送时,通过
(iAddr, iPort, dAddr, dPort)
查出(ePort)
- 接收时,通过
(ePort, dAddr, dPort)
查出(iAddr, iPort)
对称 NAT 与锥形 NAT 最大的不同就是对发送的限制。所有锥形 NAT 都有一个特点,就是一旦建立映射,iAddr:iPort
发送的包一定会被映射到 eAddr:ePort
,而限制都只针对接收时的查表。也就是说,锥形 NAT 都是在分配外部端口给一个内部端口。而对称 NAT 就是把外部端口分配个一次 “连接” 了,在发送的时候也关注目标地址。比如 NAT 表:
(192.168.10.2, 10086, 11451, 6.6.6.6, 23333)
(192.168.10.2, 10086, 11452, 8.8.8.8, 10000)
可以看到,同一个内部端口可以映射到多个外部端口。因此端口分配不再是一对多而是多对多,这也是对称 NAT 名字的由来←我猜的。
对称 NAT(图源 Wikipedia)
RFC4787:行为描述
虽然 RFC3489 对 UDP NAT 给出了一个分类,但是这个分类显然不太能涵盖所有种类的 NAT。比如发送时,为什么不能通过 (iAddr, iPort, dAddr)
查表,而是分成了锥型和对称型呢?此外,这个分类还遗漏了其他的 NAT 实现细节,比如在 NAT 表中不存在相关记录时,要怎么生成新的 ePort
?是优先复用还是随机分配?NAT 表项的过期时间到底是多久?基于种种问题,IETF 废弃了这种分类方式,并在 RFC4787 中重新制定了一套对 NAT 行为的描述,以针对各种不同的 NAT 实现。
RFC4787 中描述了多种 NAT 行为,这里选取其中相对重要的三个进行介绍:映射行为、过滤行为、端口分配行为。
映射行为
映射行为对应于我们之前介绍中的发送行为,也就是从内部网络发送至外部网络,主要可以分为三种:
- 端点独立映射(Endpoint-Independent Mapping):发送时通过
(iAddr, iPort)
查表 - 地址依赖映射(Address-Dependent Mapping):发送时通过
(iAddr, iPort, dAddr)
查表 - 地址与端口依赖映射(Address and Port-Dependent Mapping):发送时通过
(iAddr, iPort, dAddr, dPort)
查表
所有锥型 NAT 都是端点独立映射,而对称 NAT 则是地址与端口依赖映射。当查表使用的信息越多,唯一确定一个映射所需要的条件也就更多,对端点的复用效果就越好,但同时也降低了连通性(不同的连接中无法保持同一个端口映射)。
过滤行为
过滤行为对应于我们之前介绍中的接收行为,也就是从外部网络发送至内部网络,主要也是分为三种:
- 端点独立过滤(Endpoint-Independent Filtering):接收时通过
(ePort)
查表 - 地址依赖过滤(Address-Dependent Filtering):接收时通过
(ePort, dAddr)
查表 - 地址与端口依赖过滤(Address and Port-Dependent Filtering):接收时通过
(ePort, dAddr, dPort)
查表
可以看到,1-3 分别对应了锥型 NAT 的三种类型,而对称 NAT 则是地址与端口依赖过滤。当查表使用的信息越多,在接收外部网络数据包时的条件也就更加严苛,同样也是提高了端点的复用效果,但是降低了连通性。
通过行为分类,可以清楚的了解到 RFC3489 分类法的局限(遗漏了很多种行为组合),也能更好的理解四种 NAT—— 全锥体 NAT 最为宽松、对称 NAT 最为严格。
端口分配行为
端口分配行为是 RFC3489 分类法没有提到的,但是也是 NAT 的一个非常重要的行为。端口分配发生在第一次映射行为时,用来产生映射的目标端口 ePort
。
- 端口保留(port preservation):NAT 将尽可能保证
ePort == iPort
。比如在映射时替换掉之前拿到ePort
的内部端口,或者维护一个地址池,分配不同外部地址的ePort
端口eAddr:ePort
。 - 端口重载(port overloading):即使外部端口冲突也依旧进行端口保留。端口重载会影响相关程序的正确性,因此 RFC4787 要求 NAT 程序不可以具备端口重载行为。
- 非端口保留(no port preservation):NAT 不刻意保证
ePort == iPort
。
后记
本篇文章介绍了一些常见的 NAT 类型与实现原理。下一篇文章将结合本文提到的分类介绍 UDP NAT 穿透技术。
勘误
- 2023/10/27:在翻译 RFC4787 的 “Address-Dependent” 和 “Address and Port-Dependent” 时误将 “Dependent” 翻译为 “独立”,实际应该翻译为 “地址依赖” 和 “地址与端口依赖”。感谢 @NaCl 的指正!
参考文献
- RFC2663 – IP Network Address Translator (NAT) Terminology and Considerations(https://datatracker.ietf.org/doc/html/rfc2663](https://datatracker.ietf.org/doc/html/rfc2663))
- RFC4787 – Network Address Translation (NAT) Behavioral Requirements for Unicast UDP(https://datatracker.ietf.org/doc/html/rfc4787](https://datatracker.ietf.org/doc/html/rfc4787))