bigywater.tech
网络协议 · 19

NAT / NAPT:端口映射表怎样改写地址

家里的设备都用私有地址,怎么和公网通信?NAT 到底改了什么,端口会不会变?这篇用 STUN 服务器问“你看到我的源地址和端口是什么”,实测本机出口 NAT 的映射行为,再用一个纸面模型演示 NAPT 表。

56336 = 56336
本机 UDP 本地端口与 STUN 服务器看到的外部端口相同(端口保留),三个服务器看到的一致
39 / 40
40 个 UDP 套接字里外部端口与本地端口相同的个数;唯一不同的是本地 4500 → 3319
10 / 10
路径上有回应的 10 个跳,引用的源地址全是本机局域网地址——traceroute 看不出 NAT 位置

STUN 看到的

实测 · 本机执行输出
  本机套接字:地址是私有地址(本机局域网地址),本地端口 56336
  服务器 1看到的:地址是公网地址,端口 56336
  服务器 2看到的:地址是公网地址,端口 56336
  服务器 3看到的:地址是公网地址,端口 56336
  3 个服务器有回应:它们看到的公网地址互相相同(不同的值 1 个),端口相同(不同的值 1 个);服务器看到的公网地址与本机地址不同
  同一个本地套接字发往 3 个不同的目的地,映射到同一个外部地址和端口:True——这叫“与目的无关的映射”(endpoint-independent mapping)
  服务器看到的端口 [56336] 与本地端口 56336相同:NAT 没有改端口(端口保留)

STUN 服务器把收到的 UDP 包的源地址和端口回给你。实测:本机地址是私有的,服务器看到公网地址,端口不变,三个服务器看到的一样,是“与目的无关的映射”。

端口保留

实测 · 本机执行输出
  UDP:40 个套接字,外部端口与本地端口相同 39 个、不同 1 个、没有回应 0 个;不同的:[(4500, 3319)]
  TCP 服务器 1:没有拿到 STUN 回应(服务器可能不支持 TCP STUN)
  TCP 服务器 2:没有拿到 STUN 回应(服务器可能不支持 TCP STUN)
  TCP 服务器 3:没有拿到 STUN 回应(服务器可能不支持 TCP STUN)

40 个 UDP 套接字里 39 个外部端口等于本地端口,只有本地 4500 被改成 3319,原因没有验证;TCP 的 STUN 服务器没有回应,TCP 映射没有测到。

traceroute 看不见 NAT

实测 · 本机执行输出
  TTL=1:3/3 个回应;该跳收到的包源地址是 本机局域网地址;源端口与本机发出的相同
  TTL=2:3/3 个回应;该跳收到的包源地址是 本机局域网地址;源端口与本机发出的相同
  TTL=3:3/3 个回应;该跳收到的包源地址是 本机局域网地址;源端口与本机发出的相同
  TTL=4:3/3 个回应;该跳收到的包源地址是 本机局域网地址;源端口与本机发出的相同
  TTL=5:1/3 个回应;该跳收到的包源地址是 本机局域网地址;源端口与本机发出的相同
  TTL=6:3 个探测都没有回应
  TTL=7:1/3 个回应;该跳收到的包源地址是 本机局域网地址;源端口与本机发出的相同
  TTL=8:3 个探测都没有回应
  TTL=9:1/3 个回应;该跳收到的包源地址是 本机局域网地址;源端口与本机发出的相同
  TTL=10:2/3 个回应;该跳收到的包源地址是 本机局域网地址;源端口与本机发出的相同
  TTL=11:3 个探测都没有回应
  TTL=12:3 个探测都没有回应
  TTL=13:3 个探测都没有回应
  TTL=14:3/3 个回应;该跳收到的包源地址是 本机局域网地址;源端口与本机发出的相同
  TTL=15:1/3 个回应;该跳收到的包源地址是 本机局域网地址;源端口与本机发出的相同
  TTL=16:3 个探测都没有回应
  16 个 TTL 里 10 个有回应(第 [1, 2, 3, 4, 5, 7, 9, 10, 14, 15] 跳);其中引用的源地址是本机局域网地址的有 10 个,不是的有 0 个;引用的源端口与本机发出的相同的有 10 个
  第一节明明显示服务器看到的是改写之后的公网地址,但路径上远处的路由器回的差错里引用的却仍是本机局域网地址。我的解释是:NAT 设备在把差错报文送回内网时,把内嵌的原包头也翻译回了内网地址(NAT 处理 ICMP 差错时需要这样做,否则内网主机认不出这是自己的包)——这个解释没有直接验证。结论:用这种方法看不出 NAT 位于哪一跳

路径上远处路由器回的超时报文引用的源地址仍是内网地址,推测是 NAT 把内嵌的原包头翻译了回去。所以不能靠 traceroute 判断 NAT 在哪一跳。

NAPT 表

实测 · 本机执行输出
    内网连接                                    → 公网映射
    udp 10.0.0.2:5000 → 198.51.100.7:53    →  203.0.113.1:5000
    udp 10.0.0.3:5000 → 198.51.100.7:53    →  203.0.113.1:40000
    tcp 10.0.0.2:52111 → 198.51.100.9:443   →  203.0.113.1:52111
    tcp 10.0.0.4:52111 → 198.51.100.9:443   →  203.0.113.1:40001
    udp 10.0.0.2:5000 → 198.51.100.8:123   →  203.0.113.1:40002
  NAT 表里 5 条记录;两台主机都用源端口 5000 的 UDP:第一台映射到外部端口 5000(保留),第二台因为 5000 已被占用而换成 40000
  回程翻译:来自 198.51.100.7:53 发往 5000 的包 → ('10.0.0.2', 5000);来自没联系过的 198.51.100.99:53 的包 → None(丢弃);发往没有记录的端口 41234 → None(丢弃)
  改写源地址 10.0.0.2 → 203.0.113.1 以后的 IP 头校验和:增量更新 0xd473,整头重算 0xd473,一致(TCP / UDP 的校验和还要同样更新,因为伪头部含地址)

纸面模型:每个内网连接对应一条记录,优先保留源端口,冲突时分配新端口;入站包必须匹配记录才被翻译,否则丢弃;改地址或端口时用增量公式更新校验和。

分步演示

NAT 与 NAPT
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。STUN 服务器是三个公网服务器(Cloudflare、Twilio、小米),只用来问“你看到我的地址”;本机的公网出口地址不写出来,只比较是否相同;第四节是纸面模型,地址为文档地址与私有地址,不是实测
    • 没有验证:TCP 的 NAT 映射(STUN 服务器不支持 TCP);UDP 映射的空闲超时(端口保留使“换端口”无法作为到期信号);入站过滤类型(全锥 / 受限 / 端口受限 / 对称的区分);发夹(hairpin)NAT;NAT 所在的位置与是否多层;其它端口是否也不被保留
    京ICP备2026067502号-1京公网安备11010502063487号