互联网一片混乱,其中最大的部分之一就是网络地址转换(NAT),这是一种允许多台设备共享同一网络地址的技术。在本系列文章中,我们将探讨 NAT 和 NAT 穿越。本篇文章主要介绍 NAT,下一篇文章将介绍 NAT 穿越技术。
背景:IP 地址和 IP 地址耗尽
您可能还记得,在以前的文章中,互联网是一个数据包交换网络,它通过路由独立的信息(数据报)来工作:
每个数据包都有一个源地址和目标地址,它们只是数字,每个设备都有自己的地址,因此数据包会被发送(路由)到它而不是其他设备。在最初版本的互联网协议(IP 版本 4 或简称 IPv4)中,这些地址长度为 32 位,这意味着总共有 232 个(约 40 亿)可能的地址。地球上有超过 40 亿人,其中许多人拥有不止一台设备,因此实际上不可能每台设备都有唯一的地址。
互联网工程任务组(IETF)负责维护互联网上的大多数主要网络协议,它有一个正式的解决方案,那就是让每个人都升级到新版本的 IP,即 IP 版本 6(IPv6)。IPv6 有 128 位地址,这至少在理论上意味着有大量的地址。遗憾的是,向 IPv6 过渡的过程并不顺利,原因说来话长,也很令人沮丧,本篇文章就不一一赘述了。结果是,在 IPv6 首次规定 25 年后的今天,使用 IPv6 的互联网流量还不到一半。下图显示了谷歌对其流量中 IPv6 部分的测量结果,反映了客户端的部署情况。服务器端的部署情况也相当糟糕,据 ISOC 报告,排名前 1000 的网站中约有 44% 支持 IPv6。
不用说,这不是什么好事。作为对比,TLS 1.3 于 2018 年发布,目前 ISOC 的数据显示,排名前 1000 的网站中,有 79% 支持 TLS 1.3。在某种程度上,这种比较略显不公平,因为过渡到 IPv6 意味着改变网络连接,而过渡到 TLS 1.3 只需要更新软件,但无论如何,我们离全面部署 IPv6 还很远,尽管我们已经没有足够的 IPv4 地址。事实上,正如下面的时间表所示,地址匮乏已经有一段时间了:
IP 地址是集中分配的,整个地址池由互联网号码分配机构(IANA)管理,IANA 将地址分配给地区互联网注册管理机构(RIR),RIR 再将地址分配给网络提供商,直至主机。从上图可以看出,立即过渡到 IPv6,即关闭 IPv4,在今天看来是不可能的,但在 2000 年代初,当时的部署实际上为零,这是不可能的。我们需要的是另一种技术解决方案,一种可以逐步部署,而不是同时更换大块互联网的解决方案(技术术语:forklift upgrade)。互联网以 NAT 的形式出现了。
网络地址转换 (NAT)
NAT 背后的基本理念很简单:只要有办法将一台机器的流量与另一台机器的流量分离,就可以让多台机器共享同一个地址。幸运的是,这样的机制已经存在:ports。
端口号
考虑这样一种情况:你只有两台电脑,一台客户端,一台服务器,但客户端上同时有两个用户。在 2023 年,基本上所有电脑都是独立的,这种情况让人感觉很奇怪,但所有这些东西都是在多用户分时使用同一台电脑的时代设计的。如果两个用户都想连接到同一台服务器,他们的 IP 地址是一样的,那么服务器如何区分他们呢?
答案是另一个字段,即端口号,它只是一个 16 位整数,可用于区分同一设备(IP 地址)上的多个上下文。端口号有两个主要用途:
在客户机上,用于区分连接到同一服务器的多个类似进程;在服务器上,用于区分多个不同的服务。通常,服务会有特定的指定端口号,如 HTTP 为 80,HTTPS 为 443 等。
端口号不存在于 IP 层,而是存在于 TCP 或 UDP 层,但我们要讨论的几乎所有流量都使用 UDP 或 TCP,所以这通常不是问题。
NAT
端口号允许同一台计算机上的两个用户共享一个 IP 地址。NAT 背后的直觉是,您可以使用相同的机制来允许两台计算机共享 IP 地址,只要您可以确保它们不会也尝试使用相同的端口。[3] 实现此目的的基本方法是让网络网关设备(例如,您的 WiFi 路由器)完成这项工作。基本场景如下图所示:
在此示例中,Alice 和 Bob 都在同一网络上,并且分别具有地址10.0.0.3
和10.0.0.2
。WiFi 路由器有两个地址,一个位于内部,用于与 Alice 和 Bob ( 10.0.0.1
) 通信,另一个位于外部,用于与 Internet 上的计算机通信 ( 192.0.2.1
)。
当 Alice 想要与服务器通话时,她会从自己的 IP 地址发送一个数据包,并使用本地端口 1111(通常写为 10.0.0.3:1111),如上图所示。这个数据包会发送到 WiFi 路由器,路由器会将源地址和端口改写为 192.0.2.1:1234,然后发送给服务器。当服务器作出回应时,它会将数据包发送到 192.0.2.1:1234(这是它知道的唯一地址),然后再将其路由回 WiFi 路由器。路由器会将目标地址改写为 10.0.0.3:1111,然后发送给爱丽丝。Bob 的情况也一样(他甚至使用了相同的端口号!),只是他发送的数据包来自 192.0.2.1:5678。为了实现这一功能,路由器需要维护一个映射表,列出哪些外部端口对应哪些内部机器。表中的每个条目称为 “NAT 绑定”,它将外部地址和端口与内部地址和端口关联起来。
从服务器的角度看,这就像有一台地址为 192.0.2.1 的机器在与服务器对话一样;NAT 只是客户端单方面发生的事情。这是一个非常重要的功能,因为它可以实现增量部署。无法获得足够 IP 地址的网络可以使用 NAT,而无需对服务器进行任何更改。也许不那么明显的是,它也不需要改变客户端:它们只需使用普通 IP 地址,然后由 NAT 转换即可[4]。
当然,NAT 并不神奇,它不能凭空创建 IP 地址;它所做的是通过使用端口号作为 IPv4 地址空间的扩展来延伸 IP 地址。事实上,我们曾拿 IPv7 包头开过一个玩笑,在这个包头中,IPv4 地址字段是地址的 “高阶 “位,而传输端口字段是 “低阶 “位:[5]。
如果 NAT 设备后面有足够多的客户端,它仍然有可能用完端口,但由于 NAT 可以使用同一个端口同时与两个不同的服务器通信(不过这并不是好消息,原因我们将在下文详述),而且大约有 65000 个可能的端口,因此需要很多客户端同时与同一个服务器通信才会出现问题。一般来说,NAT 会在端口不再活动时重复使用这些端口,因此 NAT 绑定不会随着时间的推移而稳定:1234 端口现在可能是 Alice,但 20 分钟后可能就是 Bob。
在实际操作中,你通常不会在服务器上使用 NAT,至少不会这样做,尽管这在技术上并非不可能。特别是 HTTP(S) URI 有一个端口号字段,因此可以使用(例如)https://example.com:4444 来表示客户端应该使用 4444 端口,但这种做法并不常见,部分原因是结果很难看,部分原因是有其他机制可以在同一客户端共享多个服务器,例如 TLS 服务器名称指示 (SNI)。
RFC 1918 地址
当然,即使在 NAT 后面,每个客户端仍然需要自己的 IP 地址,那么这有什么用呢?答案是,这些地址不需要全球唯一,而只需要在给定网络内的本地唯一。这意味着,你的网络中的机器的本地地址可能与我的网络中的机器的本地地址相同,但它们在公共互联网上会被转换为不同的地址。
IETF 在 RFC 1918 中为 “私人 “使用保留了一些地址块。这些地址永远不会出现在公共互联网上,因此在你的网络上使用它们是安全的,只要你在将它们转换为可路由地址后再将其输出到互联网上即可。上面的示例使用了一个地址块中的地址:10.0.0.0/8,意思是 “带有 8 位前缀 10 的所有地址,即 10.0.0.0 至 10.255.255.255(含 10.255.255.255)”。该地址块中可能包含约 1600 万个地址,因此您可以在 NAT 后面拥有一个非常庞大的网络。
维护 NAT 绑定
在内部,NAT 需要保留一个映射表来存储内部和外部地址之间的绑定。在上面的示例中,您会有一个类似以下的表:
内部地址 | 外部端口 |
---|---|
10.0.0.3:1111 | 1234 |
10.0.0.2:1111 | 5678 |
请注意,外部地址是恒定的,因此我们不需要将其列入表中。一些较大的 NAT 系统(见下文的运营商级 NAT)有多个外部 IP 地址,但我们现在不需要担心这个问题。
当 NAT 在传出接口上接收到数据包时,它需要进行查表。如果数据包已经存在绑定,那么 NAT 就会使用表中的条目。如果没有绑定,它就会创建一个未使用端口的表项并转发数据包。在这个例子中,我描述了所谓的 “与地址无关 “的 NAT,在这种 NAT 中,无论远程地址是什么,给定的本地地址/端口组合都只有一个绑定。还有一种 “与地址相关 “的 NAT,使用不同的绑定方式。当我们在第二部分讨论 NAT 穿越时,这一点将变得非常重要。
当 NAT 收到外部接口上的传入数据包时,它也会进行表查询。如果存在表项,它就会按预期转发数据包,但如果不存在表项,就无法知道数据包是发给哪台主机的;在这种情况下,明智的做法是直接丢弃数据包。这样做的结果是,大多数消费者 NAT 只真正支持 NAT 后面的机器首先发言启动流量的情况。这通常被概念化为 “仅发出 “语义集,并与 TCP 连接相对应,在 TCP 连接中,客户端发送第一个数据包(SYN)。事实上,有些 NAT 依赖 TCP SYN 来创建绑定,并在连接中途丢弃与未知流量相对应的 TCP 数据包。这对 UDP 不适用,因此您只需查看第一个发出的数据包,而不必理会它的任何标记。
这种 “仅限出站连接 “的语义通常被视为一种安全功能,因为它意味着,即使 NAT 后面的设备有 “开放的 TCP 端口”,也就是说它们在这些端口上监听连接,外部攻击者也可能无法连接到它们。这种设备出奇地常见,尤其是打印机或扫描仪等你希望本地网络上的任何人都能访问的设备,所以 NAT 在这里确实提供了一个有价值的功能。不过,重要的是要认识到,与明确设计用于阻止特定类型连接的防火墙不同,许多 NAT 只是作为其架构的一种意外副作用来实现这一功能,但也有一些 NAT 明确实现了这一功能,我们稍后会看到这一点,因此这并不是一个值得信赖的保证属性。
绑定生命周期
这给我们带来了一个明显的问题:NAT 何时应删除绑定。清理旧绑定是一项重要功能,否则 NAT 将很快耗尽其可用端口空间。有多种方法可以管理此问题:
保持绑定开放,直到连接被 TCP FIN 或 TCP RST 中断。这种方法不适用于许多基于 UDP 的协议,因为这些协议要么没有表示连接关闭的消息(如 RTP),要么这些消息是加密的(如 QUIC 或 DTLS 1.3)。这种方法甚至对 TCP 也不适用,因为客户端可能在没有发送 FIN 的情况下就关闭了,例如客户端崩溃或用户让笔记本电脑进入睡眠状态。
使用超时并中断闲置时间过长的连接。这可以保证资源最终会被释放,因为如果客户端关闭,它就不会发送数据包。不过,”时间过长 “只是一种启发式方法。网络协议的设计通常是,如果没有数据流,就不会发送任何数据包(TCP 就是这样),在这种情况下,你可能会在客户端正要发送数据时中断连接。更多的现代协议使用 “keepalive “数据包来保持 NAT 绑定的开放性,但请记住,这里的意思是,NAT 应能与 NAT 部署前设计的协议一起使用,所以这并不是一个理想的解决方案。
删除最近最少使用的连接,一旦达到最大连接数并且需要分配新的连接。这与超时有许多相同的问题,但在某些方面略有改进,因为除非表已满,否则它不会删除旧连接。
当然也可以同时使用这些机制中的一种以上。例如,您可能会查看 TCP 控制数据包以丢弃 TCP 连接,但使用计时器作为客户端关闭和其他协议的备份。
非 TCP/UDP 协议
当然,TCP 和 UDP 并不是唯一可以在 Internet 上运行的协议。IP 数据报的“下一个协议”字段是一个 8 位值,并且仅分配了其中大约一半的值, 因此原则上可以引入直接在 IP 上运行的新协议。然而,在实践中,NAT 使这个问题变得极其严重,因为端口字段不在 IP 标头中,而是在位于 IP 之上的协议标头(例如 TCP 或 UDP)中,这意味着 NAT 需要特定于协议的逻辑对于每个新协议。
一个很好的例子是SCTP,它是一种类似 TCP 的协议,它引入了许多新功能,例如在同一连接上进行多路复用。SCTP 的目的是在 IP 上运行,就像 TCP 一样,SCTP 的标头实际上具有与 TCP 和 UDP 相同位置的源端口和目标端口,如下所示:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port Number | Destination Port Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Verification Tag |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
因此,您可能会认为,如果 NAT 总是重写 UDP 或 TCP 的源/目的端口字段中的任何字节,那么 SCTP 也能正常工作,但这是不正确的。它确实会重写这些字段,但这只会带来另一个问题,因为 SCTP 数据包还包括一个校验和(上图中数据头的最后一个字段),它是对整个数据包进行计算的,目的是检测数据包的任何变化,包括端口号。这意味着任何重写源端口和目的端口的 NAT 也需要重写校验和,否则接收方的校验和验证将失败,数据包将被丢弃[6]。
SCTP 校验和的位置与 TCP(或 UDP 校验和)的位置不同,而且是用不同的算法计算的,所以即使你直接使用 TCP 重写代码(出于其他原因,这不是个好主意),最终也只会损坏数据包的其他部分。因此,底线就是,NAT 重写它们不理解的数据包并不安全(尽管在某些情况下可能是安全的),相反,NAT 需要进行修改才能支持每个新协议,这意味着任何此类协议一开始都会在很大一部分客户端上被破坏,因此很难得到推广。
幸运的是,这个问题有一个众所周知的解决方案,即通过 UDP 运行新协议。UDP 标头相对较轻,由 8 个字节组成,其中 4 个字节是主机和端口,无论如何您都需要它们。另外两个是一个两字节长度字段(您通常需要它)和一种过时的校验(仅占用两个字节),因此没有那么多开销。
0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| Source | Destination |
| Port | Port |
+--------+--------+--------+--------+
| | |
| Length | Checksum |
+--------+--------+--------+--------+
如果您通过 UDP 运行您的协议,那么 NAT 通常会大部分正常工作(同样需要注意的是 NAT 不知道连接何时停止和启动),您从大部分工作正常的位置开始,而不是大部分失败(当 QUIC 首次推出时,Google发现大约 95% 的连接成功。)当然,95% 并不是 100%,QUIC 和 DTLS(带有 WebRTC)等新协议的经验表明,任何新协议都会经历一些堵塞;实际上,这意味着如果新的基于 UDP 的协议失败,您需要安排某种方式回退到旧协议(例如 HTTPS)。这里有许多可能的方法,包括并行尝试(一种通常称为Happy Eyeballs的技术),首先尝试新协议并查看它是否失败,或者先尝试旧协议,然后在后台尝试新协议。
因此,在互联网上部署新传输协议的唯一真正实用的方法是通过 UDP,[7] ,这就是最近的协议,例如 QUIC(直接在 UDP 上运行)或 WebRTC 数据通道(在 DTLS 上运行的 SCTP) UDP)做。[8] IP 语音先驱Jonathan Rosenberg (JDR)在 IETF 会议上 有力地阐明了这一原则 ,会上有人提出了一种在 NAT 上运行 SCTP 的机制。JDR 的回应是这样的:
世界上有一些残酷的事实,这就是其中之一。TCP和UDP是 IP协议栈的新腰部。
在这里,”腰部 “指的是 IP 协议套件的一个著名比喻,IPv6 设计者 Steve Deering 在一次演讲中的这幅图片就说明了这一点:
这种观点认为,IP 可以在任何传输方式(无线电、铜缆等)上运行,而且可以在其上运行许多协议,但 IP 是共同的元素,因此沙漏的 “腰 “很窄。罗森伯格的观点(我同意)是,这个位置现在已被 UDP(在较小程度上也被 TCP)占据。可以说,情况比这更糟:在 HTTP 上部署新技术是如此普遍,以至于我看到有人认为 HTTP 是新的 “腰部”,但我们还没到那一步!
应用层网关
NAT 对于仅由一个连接组成的简单协议(例如 HTTP)非常有效。然而,有些协议具有更复杂的模式。例如,文件传输协议 (FTP)是原始协议套件的一部分,在 Web 和 HTTP 占据主导地位之前广泛用于下载数据。FTP 有一个不寻常的(在现代人看来)设计,它使用两个连接:
对于只有一个连接的简单协议(如 HTTP),NAT 的效果非常好。不过,有些协议的模式比较复杂。例如,文件传输协议(FTP)是原始协议套件的一部分,在网络和 HTTP 占据主导地位之前被广泛用于下载数据。FTP 有一个不同寻常的设计(在现代人看来),它使用两个连接:
- 一个控制通道,用于客户端向服务器发出指令;
- 一个数据通道,用于实际传输数据。
使用 FTP [编辑自 “UDP” – 2023-04-17]进行下载的情况如下:
客户端首先会连接到 FTP 服务器,然后发出下载文件的指令。然后,服务器将连接到客户端(默认使用比客户端使用的端口号低一个的端口号,但客户端也可以提供端口号)并发送文件。
当然,如果有 NAT 就不一定行得通,因为端口号可能不对,即使客户端使用默认端口,NAT 也可能没有两个相邻的端口。相反,NAT 会使用所谓的应用层网关 (ALG) 重写客户端的请求,如下所示:
请注意,NAT 基本上不会干扰客户端的数据:它只是对 FTP 有足够的了解,知道端口号在哪里,创建适当的传入 NAT 绑定,然后将其替换到控制通道上。当然,这种方法对未知协议不起作用,对加密协议也完全不起作用(事实上,对加密协议的任何篡改通常都会导致某种失败)。在这一点上,FTP 已不复存在(原因包括不安全、被 HTTP 取代、至少在 Web 浏览器中被取代、对实现质量的担忧),而较新的协议并不采用这种模式,因为它们希望与 NAT 兼容。之所以需要这种 ALG,是为了避免在首次引入 NAT 时破坏现有协议,但现在 NAT 已经普及,情况正好相反,新协议必须避免在现有 NAT 上运行时被破坏。
运营商级 NAT
最初,NAT 主要部署在消费者或企业网络的边界(现在它们已经无处不在)。然而,随着 IP 地址空间变得越来越稀缺,ISP 发现自己无法获得足够的 IP 地址供每个客户拥有。当然,解决方案是拥有一个巨大的 NAT(通常称为运营商级 na (CGN),它将多个订阅者复用到同一个 IP 地址上。当然,客户可能仍然拥有自己的 NAT,因此使用 CGN,您可以拥有多层 NAT 和地址重写,这当然不可能出错。
在 CGN 场景中,分配给订户的地址可以来自不可路由的地址空间(来自 RFC 1918 或来自新的RFC 6598 块),也可以是 IPv6 地址。在后一种情况下,订阅者只拥有 IPv6 地址,NAT 会在出门时将内容重写为 IPv4,采用一种称为 NAT64 的技术。这种情况并不像 IPv4 那么简单,因为网络还需要将 DNS A 记录中的 IPv4 地址重写为 IPv6 AAAA 记录(一种称为 DNS64 的技术),以便仅 IPv6 客户端可以向它们发送;这有其自身的问题,但这是另一篇文章的主题。
IETF 和 NAT
长期以来,IETF 基本上否认 NAT,主要有两个原因:
- 任何数据包重写(更不用说 ALG)都会违反 IP 的端到端设计,其中数据包从 A 到 B 不会受到任何影响。
- 当每个人都应该过渡到 IPv6 时,它被视为延长 IPv4 生命周期的技术(加剧矛盾!)
当时的普遍态度是,规范 NAT 行为只会助长 NAT,人们应该忽略 NAT,并希望在 IPv6 最终到来时 NAT 会消失。这种态度一直延续到 2012 年,当时发布的 RFC 6598 中就有如下表述:
一些运营商表示需要本文件所述的特殊用途 IPv4 地址分配。在讨论过程中,IETF 社区对这一分配方式达成了非常粗略的共识。
虽然包括本文件所述特殊用途地址分配在内的运营权宜之计可能有助于解决短期运营问题,但 IESG 和 IETF 仍将致力于 IPv6 的部署。
结果正如你所想:NAT 无处不在,而我们仍然没有全面部署 IPv6。更糟糕的是,由于缺乏任何指导,NAT 的行为变得极其多变和特异,导致越来越复杂的变通方法。最终,IETF 在 2007 年发布了 RFC 4787 文件,描述了 NAT 的行为方式;当然,当时已经有大量的 NAT 部署没有遵循这些指导原则,但希望这些指导原则对新设备的开发人员有用。
最后的想法
NAT 为互联网的发展方式提供了一个特别好的范例,即变通办法再变通办法。究其原因,谷歌工程师Adam Langley 称之为 “互联网铁律”,即最后接触任何事物的人都会受到指责。最初构建和部署 NAT 的人必须避免破坏现有部署的东西,这迫使他们构建 ALG 和不可预测的空闲超时等黑客手段。现在,NAT 已被广泛部署,新协议必须在这种环境中运行,这就迫使它们在 UDP 上运行,并符合 NAT 转换算法规定的仅流出的流量动态。当然,有一类应用并不适合这种模式,尤其是 VoIP 和游戏等点对点应用。在下一篇文章中,我们将介绍如何让这些应用即使在现有 NAT 的情况下也能正常运行的技术。
- 是的,我知道我还有两个系列没有完成,一个是关于传输协议的,另一个是关于网络安全的。我有点分心,就传输协议系列而言,我对其中的一篇文章有点忘乎所以,但我确实打算继续写下去。第二部分我已经写了一部分,所以应该很快就能完成。
- 理论上,IANA 可以直接分配号码,但这样可以实现区域管理。︎
- 从技术上讲,这种机制被称为 “网络地址/端口转换”(NAPT),但由于这是最常见的方法,因此 NAT 是通用术语。︎
- 我省略了一个细节,那就是你需要从 RFC 1918 空间中给客户端分配所有新地址,但在现代网络中,客户端地址是由本地网络集中分配的,因此这通常很简单。︎
- 实际上,我们还制作了衬衫,正面写着 “32 + 16 > 128″,开玩笑说 32 位地址 + 16 位 IPv4 端口比 128 位 IPv6 地址更好。不过,Cafe Press 似乎已经丢失了这一设计。
- 曾经有一个草案可以让 SCTP 更好地与 NAT 配合使用,但似乎还没有被标准化。︎
- 采用新传输协议的一个重要原因是要有自己的速率限制和可靠性机制,而如果在 TCP 上运行这些机制就行不通,因为 TCP 有自己的机制。︎
- NAT 并不是通过 UDP 部署新协议的唯一原因。您可以完全在应用程序空间而不是通过修改操作系统来实现基于 UDP 的新协议,这也很有帮助。︎
作者:Eric Rescorla
原文:https://educatedguesswork.org/posts/nat-part-1
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/35494.html