客户端连接 203.0.113.9:443,服务端抓包看到的来源却不是客户端地址;访问网关的 8443 端口,内部服务收到的目标又变成 10.0.0.10:443。如果中途超时,返回包可能再也找不到原来的内部端点。地址转换、状态跟踪和过滤共同出现在一台网关上,但它们解决的是三件不同的事。

第 07 篇给出了路由查找,第 16 篇讨论 TCP 连接状态,第 30 篇区分主机里的观察点。本篇只讨论传统 NAT 与有状态防火墙怎样改变一次转发的可见五元组和处理结果。NAT 不等于防火墙,conntrack 也不等于 TCP 自己的状态机。

五元组必须带方向读取

一条常见传输流用五元组表示:

{协议, 源地址, 源端口, 目标地址, 目标端口}

协议字段不能省略,因为 TCP 与 UDP 可以使用相同的四个地址和端口而仍是不同的流。方向也不能省略:返回方向会交换源与目标,经过 NAT 后还会出现转换前后的两个版本。

假设内部客户端 10.0.0.2:49152 访问 203.0.113.9:443,网关把来源改为 198.51.100.7:62001。内部侧看到:

TCP 10.0.0.2:49152 -> 203.0.113.9:443

外部侧看到:

TCP 198.51.100.7:62001 -> 203.0.113.9:443

服务端的返回包以 203.0.113.9:443 -> 198.51.100.7:62001 到达网关,再被还原为 203.0.113.9:443 -> 10.0.0.2:49152。RFC 3022 将只改地址的 Basic NAT 与同时转换 TCP/UDP 端口的 NAPT 分开定义;日常口语里的“NAT”经常把两者混在一起。

模式提炼:用双向元组记录改写

映射 = original tuple + translated tuple + reply tuple + reverse-translated tuple

只记“内网地址变公网地址”无法排查返回路径。任何代理、隧道或地址重写问题,都应分别记录处理点两侧和两个方向的元组。

SNAT 与 DNAT 改写不同字段

SNAT(Source NAT)改写源地址或源端口,常用于内部端点主动访问外部网络。DNAT(Destination NAT)改写目标地址或目标端口,常用于把公开入口转给内部服务。名称描述的是被改写字段,不代表报文方向必然是“出站”或“入站”;具体生效位置由规则所在 hook 和路由路径决定。

DNAT 示例中,外部客户端发送:

TCP 192.0.2.44:53000 -> 198.51.100.7:8443

网关转给内部服务时变成:

TCP 192.0.2.44:53000 -> 10.0.0.10:443

内部服务回复 192.0.2.44:53000 时,网关还必须把来源从 10.0.0.10:443 还原成客户端最初访问的 198.51.100.7:8443。缺少反向改写,客户端会收到来自意外端点的报文,原连接无法成立。

nftables 官方手册说明,nat 类型链依据 conntrack 条目执行转换,通常只有连接的第一个包遍历该链并建立 NAT 决策,后续包使用已有绑定。这意味着改完 NAT 规则后,已有连接和新连接可能表现不同。规则文本是控制面证据,实际 conntrack 条目和双侧报文才是数据面证据。

conntrack 跟踪的是网关视角的流

连接跟踪为经过主机的流保存原始方向、回复方向、协议状态和超时等信息,使返回包能够匹配已有流,并让 NAT 执行反向转换。有状态防火墙也可据此允许属于已建立流的返回流量。

conntrack 的“已建立”不能直接替代 TCP 端点的 ESTABLISHED。前者是中间设备根据所见报文维护的分类,后者是 TCP 实现内部的连接状态。网关可能漏看报文、状态已过期,或者配置为宽松拾取已有流;端点和网关由此可能对同一通信持有不同状态。

UDP 没有 TCP 握手和关闭状态,NAT 仍需保存映射并用定时器回收。RFC 4787 对 UDP 映射、过滤和刷新行为作出要求;RFC 7857 则补充 NAT 的 TCP 会话跟踪行为。具体 Linux 超时还受协议状态、内核版本和命名空间 sysctl 影响,不能把某个默认秒数写成所有设备的固定事实。

超时后的返回报文不能仅靠旧五元组恢复内部端点。它可能被当作无对应状态的新流,再由当前 NAT 与过滤规则决定去向。应用层看到的现象可能只是响应丢失或连接超时,原因却位于中间状态已经回收。

防火墙过滤与 NAT 是正交判断

NAT 回答“地址和端口如何改写”,过滤回答“这个报文是否允许继续”。一条报文可以路由正确、无需 NAT,却被过滤规则丢弃;也可以命中 DNAT,但随后因转发策略被拒绝。存在 conntrack 条目同样不保证放行,最终 verdict 仍由实际规则集决定。

诊断时至少区分三类失败:

失败位置 最直接的证据 不能据此声称
路由失败 查无下一跳、邻接失败或对应错误 防火墙一定丢包
过滤拒绝 命中规则及计数器、日志或 trace verdict 路由不存在
状态过期 原 conntrack/NAT 绑定消失,返回包无法匹配旧映射 一定存在显式 drop 规则

超时和过滤还可能表现相同:调用方都只看到没有响应。只在一侧抓包无法区分“未到网关”“到达后被过滤”“转换后未到服务”“服务回复但反向映射已消失”。证据需要沿路径排列。

模式提炼:把网关判定拆成独立步骤

处理结果 = 路由可达性 + 状态匹配 + 元组改写 + 过滤 verdict

这些步骤在具体 hook 上可能交错,但诊断问题必须分别提出。把所有超时统称为“防火墙问题”,会掩盖路由、NAT 状态和应用端口的差别。

静态五元组演算

当前环境没有 ip、nft 和 tcpdump,无法安全建立三命名空间网关并在两侧抓包。同名素材目录中的 tuple_model.py 因而只做确定性的静态演算:

  • 计算一条 SNAT 流的转换前后与回复方向元组;
  • 计算一条 DNAT 流的转换前后与回复方向元组;
  • 对路由缺失、过滤拒绝、状态过期和正常转发给出不同判定;
  • 断言反向转换能够恢复最初端点,协议字段始终参与键值。

运行命令:

1
2
python3 source/_posts/2026-09-24-计算机网络31-NAT与防火墙改变了什么/tuple_model.py --help
python3 source/_posts/2026-09-24-计算机网络31-NAT与防火墙改变了什么/tuple_model.py

素材包括五元组演算脚本、固定输出、资料与实验记录和审阅记录。

静态模型没有模拟校验和增量更新、端口冲突分配、分片、ICMP 差错、hairpin、ALG、IPv6、nftables hook 优先级或真实超时。它只能检查元组关系和诊断分类,不能冒充 Linux conntrack 实现。

条件、限制和反例

“用了 NAT 就隐藏了内网并获得安全性”不成立。地址转换改变可见端点,但访问控制取决于过滤规则、映射行为和入口配置。DNAT 正是在创建从外部到内部的入口。

“返回包五元组交换一下就能回去”只在没有地址转换时成立。经过 SNAT 或 DNAT 后,网关还需要已有映射或等价静态规则执行反向转换。

“同一私网端口总映射到同一公网端口”也不是通用规律。映射是否复用、是否依赖外部端点及端口如何分配属于 NAT 行为和资源状态。RFC 4787 专门区分这些条件。

HTTP 200、单侧抓包或规则加载成功都不能单独证明双向转发成立。至少要对齐网关两侧元组、conntrack 状态和应用响应。

练习

练习一:内部 UDP 端点 10.0.0.8:40000 经 NAPT 变成 198.51.100.7:61000,向 203.0.113.20:53 发包。写出外侧请求、外侧回复和内侧回复的五元组。若映射在回复到达前过期,哪一步失去依据?

练习二:公开入口的 DNAT 计数增加,但内部服务没有收到请求。列出至少三个仍成立的解释,并为每个解释指定一个能区分它的观察点。不得只写“防火墙拦了”。

模式速查表

观察 优先核对 常见误判
两侧源地址不同 SNAT 映射与原始/回复元组 服务端伪造来源
公开端口转到内部端口 DNAT 与返回方向还原 只需改请求方向
旧连接失败、新连接正常 conntrack/NAT 绑定与规则变更时间 应用必然故障
无响应 路由、过滤、状态、对端应用分别取证 一律归因防火墙
UDP 间歇断流 映射计时与刷新条件 UDP 自带连接状态

官方参考资料

验证边界

已验证:Python 3 标准库对固定 TCP/UDP 五元组执行 SNAT、DNAT、反向恢复和四类诊断分支,属于 HAND_CALC。输出不依赖主机网络配置。

NOT_RUN:当前环境缺少 ip、nft、tcpdump 以及可确认的网络管理/抓包能力,未建立自建网关,未加载过滤或 NAT 规则,未在两侧抓包,也未等待真实 conntrack 超时。未来补证应只在自建命名空间或专用虚拟机中执行,保存两侧 pcap、规则集、conntrack 事件和应用日志;不得修改宿主默认路由或访问生产系统。