深入理解Linux中的网络地址转换(NAT)和数据包重定向
Source: Imgur
想象一栋房子里所有设备都连接到你的Wi-Fi网络。从任何设备上,尝试通过访问https://www.whatismyip.com/来查找你的公共IP地址。IPv4地址字段对所有设备来说都应相同。这是你的互联网服务提供商(ISP)分配给路由器的IP地址,该路由器作为你互联网的网关。
那么这里发生了什么?如果IPv4地址相同,路由器如何区分这些设备?为什么不能为每个设备分配一个唯一的IPv4地址?
IPv4地址的大小为4字节或32位。因此,全球范围内可以有2^32个唯一的IPv4地址。2^32 = 4,294,967,296。约40亿个地址。现在,想象一个数据中心试图搭建由数千台服务器组成的网络。想象一个家庭拥有5台设备。想象一家拥有数千名员工的公司,每位员工至少拥有一台设备。如果为每个设备分配一个唯一的IPv4地址,我们很快就会耗尽地址资源。
网络入门课程可能会提到IPv6。通过计算(2^128),IPv6可支持多达340万亿亿亿个地址。这确实是海量地址!那么这应该能解决IPv4问题,对吧?只需迁移到IPv6即可大功告成!但现实情况令人失望。要实现这一点,全球每一家互联网服务提供商(ISP)都必须迁移到IPv6。全球所有基于IPv4设计的硬件和软件都必须支持IPv6。过渡过程需要在一段时间内同时运行IPv4和IPv6。应用程序需要更新以支持IPv6。归根结底:这需要大量工作。
那么在此之前我们该怎么办?如果每个路由设备都有一个唯一的、公开可见的IPv4地址,而网络内的每个设备都有一个私有IPv4地址呢?而路由器则负责维护一个映射表,类似于:
${private_ip}:${private_port} -> ${shared_public_ip}:${public_port}`
路由器只需在转发数据包前重写其头部信息。
这就是网络地址转换(NAT)的基本原理。
历史
根据1994年的RFC 1631,NAT被提议作为解决IPv4地址短缺问题的短期解决方案。如今,在2025年,NAT已被广泛应用于各个领域。
一个永久存在的临时方案。这听起来是否很熟悉?
本应埋设地下的电缆却暴露在外。来源:Twitter.
类型
- 基本/静态NAT
最简单的NAT类型提供IP地址的一对一转换。以下是两个使用不同IP地址范围的网络示例:
Network A: uses 192.168.1.0/24
Network B: uses 10.0.0.0/24
如果您需要网络 A 上的设备与网络 B 进行通信,可以使用 NAT 设备来转换 IP 地址。
一个示例映射如下所示:
192.168.1.10 (Network A) <-> 10.0.0.10 (Network B equivalent)
- 端口地址转换(PAT)
基本NAT过于简单,无法支持同一网络中的多个设备。端口地址转换(PAT)是一种技术,允许私有网络中的多个设备通过使用不同端口共享单个公共IP地址。与仅使用单个私有IP到公共IP映射不同,PAT通过结合IP地址和端口号来唯一标识每个设备。
- 全锥形NAT / 一对一NAT / NAT 1
一旦内部设备向外部主机发送数据包,任何外部主机只要知道公共IP地址和端口号,即可向该内部设备发送数据包。
- 受 限锥形NAT / NAT 2
与全锥形NAT类似,但增加了限制。只有之前曾向其发送过数据包的外部主机才能向您发送数据包。
- 端口受限锥形NAT / NAT 3
这是受限锥形NAT的更严格版本。外部主机不仅必须与IP地址匹配,还必须与出站通信中指定的端口号匹配。
- 对称NAT / NAT 4
这是最严格的NAT类型。来自同一私有IP地址和端口发送到特定目标IP地址和端口的请求将映射到相同的IP地址和端口。如果主机向不同目标发送具有相同源IP地址和端口号的数据包,则使用不同的NAT映射。
对于构建依赖于WebRTC的应用程序的人来说,此类NAT会导致WebRTC无法通过STUN正常工作。您需要使用TURN服务器在两个设备之间中继数据包。
您可以通过使用Check My NAT等工具来查看您当前的NAT类型。
需要注意的是,上述列表中的类型并非互斥。例如,受限锥形NAT与PAT行为可能同时存在。
路由器是如何实现NAT的?
像OpenWrt这样的路由器操作系统项目在底层使用Linux,具体来说是nftables
模块。我深入研究了Linux中的nftables
源代码。
由于NAT涉及对IP地址和端口的操作,因此只有在涉及TCP和UDP时讨论它才有意义。这与我们在nfnathelper.h中看到的内容一致。该头文件中包含两个核心函数:nf_nat_mangle_udp_packet
和 nf_nat_mangle_tcp_packet
。
让我们来看看 nf_nat_mangle_udp_packet
。
if (skb_ensure_writable(skb, skb->len)) return false;
在NAT进行任何编辑操作之前,它会确保数据包内存可写。我喜欢这种不做任何假设的处理方式。这种对可变性的明确检查非常简洁。
if (rep_len > match_len &&
rep_len - match_len > skb_tailroom(skb) &&
!enlarge_skb(skb, rep_len - match_len))
return false;
如果替换字符串比原字符串更长,且空间不足,请尝试扩展缓冲区。如果扩展缓冲区失败,则放弃操作。
mangle_contents(skb, protoff + sizeof(*udph),
match_offset, match_len, rep_buffer, rep_len);
这是核心部分 :将位于 match_offset
处的 match\_len
个字节替换为来自 rep_buffer
的 rep_len
个字节。此操作会修改数据包中实际的有效负载内容。
udph->len = htons(datalen);
当您更改有效负载大小时,必须更新头部的 .len
字段以匹配新的数据包长度。
nf_nat_csum_recalc(...)
NAT 不仅仅涉及 IP 地址和端口。它必须更新校验和,以确保数据包保持有效。否则,接收主机将将其视为损坏并拒绝。
因此,NAT 可以说是一种实时进行的数据包处理操作,直接在数据传输路径中完成!
作为工程师,你身边的 NAT 应用——Docker!
即使你从未编写过防火墙规则,只要使用过 Docker,你就依赖于 Linux NAT。每次你运行:
docker run -p 8080:80 nginx # ${host_port}:${container_port}
根据 Docker 的 文档,他们使用 iptables
。因此,我们应该期望看到类似以下的规则:
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
这告诉 Linux 内核:“如果有一个数据包到达主机的 8080 端口,将其目标 IP 和端口重写,然后将其发送到容器中。”
重点是:NAT 不仅仅是数据中心或 ISP 的事情。它无处不在!
限制
尽管NAT(网络地址转换)帮助互联网突破了IPv4的限制,但它并非完美解决方案。
问题:
- 它破坏了端到端连接。
- 它使加密变得更加困难,因为它会修改数据包头。
- 它使点对点应用程序变得复杂。增加了复杂性,有时甚至增加了延迟。
- 它需要内存来存在,因为它必须维护所有连接的映射。
IPv6
IPv6 可以真正解决 NAT 的限制。它提供了更大的地址空间,允许更多设备连接到互联网而无需 NAT。
根据https://www.google.com/intl/en/ipv6/statistics.html,我们尚未达到这一目标。