漫谈 NAT(一):各种 NAT 类型

好久没有写也没有填系列文章了,正好之前文章提到了 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 数量更少,InEx\left| In \right| \leq \left| Ex \right|,那么每个内部地址都能被映射到一个外部地址。如果外部地址数量少于内部 IP 的话In>Ex\left| In \right| \gt \left| Ex \right|,就不能保证同一时间每个内部设备都能访问外部网络了(可能分配不到外部地址)。

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。

在改写包的过程中,最关键的过程就是确定 eAddrePort。这也是不同 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 表:

  1. (192.168.10.2, 10086, 11451, 6.6.6.6)
  2. (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 表:

  1. (192.168.10.2, 10086, 11451, 6.6.6.6, 23333)
  2. (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 表:

  1. (192.168.10.2, 10086, 11451, 6.6.6.6, 23333)
  2. (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 行为,这里选取其中相对重要的三个进行介绍:映射行为、过滤行为、端口分配行为。

映射行为

映射行为对应于我们之前介绍中的发送行为,也就是从内部网络发送至外部网络,主要可以分为三种:

  1. 端点独立映射(Endpoint-Independent Mapping):发送时通过 (iAddr, iPort) 查表
  2. 地址依赖映射(Address-Dependent Mapping):发送时通过 (iAddr, iPort, dAddr) 查表
  3. 地址与端口依赖映射(Address and Port-Dependent Mapping):发送时通过 (iAddr, iPort, dAddr, dPort) 查表

所有锥型 NAT 都是端点独立映射,而对称 NAT 则是地址与端口依赖映射。当查表使用的信息越多,唯一确定一个映射所需要的条件也就更多,对端点的复用效果就越好,但同时也降低了连通性(不同的连接中无法保持同一个端口映射)。

过滤行为

过滤行为对应于我们之前介绍中的接收行为,也就是从外部网络发送至内部网络,主要也是分为三种:

  1. 端点独立过滤(Endpoint-Independent Filtering):接收时通过 (ePort) 查表
  2. 地址依赖过滤(Address-Dependent Filtering):接收时通过 (ePort, dAddr) 查表
  3. 地址与端口依赖过滤(Address and Port-Dependent Filtering):接收时通过 (ePort, dAddr, dPort) 查表

可以看到,1-3 分别对应了锥型 NAT 的三种类型,而对称 NAT 则是地址与端口依赖过滤。当查表使用的信息越多,在接收外部网络数据包时的条件也就更加严苛,同样也是提高了端点的复用效果,但是降低了连通性。

通过行为分类,可以清楚的了解到 RFC3489 分类法的局限(遗漏了很多种行为组合),也能更好的理解四种 NAT—— 全锥体 NAT 最为宽松、对称 NAT 最为严格。

端口分配行为

端口分配行为是 RFC3489 分类法没有提到的,但是也是 NAT 的一个非常重要的行为。端口分配发生在第一次映射行为时,用来产生映射的目标端口 ePort

  1. 端口保留(port preservation):NAT 将尽可能保证 ePort == iPort。比如在映射时替换掉之前拿到 ePort 的内部端口,或者维护一个地址池,分配不同外部地址的 ePort 端口 eAddr:ePort
  2. 端口重载(port overloading):即使外部端口冲突也依旧进行端口保留。端口重载会影响相关程序的正确性,因此 RFC4787 要求 NAT 程序不可以具备端口重载行为。
  3. 非端口保留(no port preservation):NAT 不刻意保证 ePort == iPort

后记

本篇文章介绍了一些常见的 NAT 类型与实现原理。下一篇文章将结合本文提到的分类介绍 UDP NAT 穿透技术。

勘误

  • 2023/10/27:在翻译 RFC4787 的 “Address-Dependent” 和 “Address and Port-Dependent” 时误将 “Dependent” 翻译为 “独立”,实际应该翻译为 “地址依赖” 和 “地址与端口依赖”。感谢 @NaCl 的指正!

参考文献

  1. RFC2663 – IP Network Address Translator (NAT) Terminology and Considerations(https://datatracker.ietf.org/doc/html/rfc2663
  2. RFC3489 – STUN – Simple Traversal of User Datagram Protocol (UDP) Through Network Address Translators (NATs)(https://datatracker.ietf.org/doc/html/rfc3489
  3. RFC4787 – Network Address Translation (NAT) Behavioral Requirements for Unicast UDP(https://datatracker.ietf.org/doc/html/rfc4787
  4. Network address translation(https://en.wikipedia.org/wiki/Network_address_translation
分享到

KAAAsS

喜欢二次元的程序员,喜欢发发教程,或者偶尔开坑。(←然而并不打算填)

相关日志

评论

  1. joe 2023.09.06 9:47 上午

    等大佬的第二篇 NAT 穿透,

  2. sword 2024.11.14 4:21 下午

    精品博客,爱了

  3. Elior Foy 2025.01.23 9:43 上午

    写得很好!

在此评论中不能使用 HTML 标签。