PHASE 1 · LINUX 网络栈实战

1.3从 iptables 到 nftables

五链模型、conntrack,与 NAT 规则实战

0.6 你理解了 NAT 在做什么(SNAT 改源、DNAT 改目的、conntrack 记账)。这章把它落地:在 Linux 上,这些规则到底写在哪、怎么写。答案是 netfilter——内核里一组"关卡",数据包穿过时被检查、改写、放行或丢弃。你还会看到一个关键真相:0.6 里"DNAT 在路由前、SNAT 在路由后"不是约定,而是这五个关卡的位置决定的。

一句话定义

netfilter:Linux 内核的包处理框架,在数据包路径上挖了几个"钩子(hook)";iptables(老)和 nftables(新)都只是往这些钩子里安装规则的前端工具——nftables 是现代那个。

1netfilter 与两个前端

容易混的关系:很多人以为 iptables 是"防火墙"。其实防火墙逻辑在内核的 netfilter 里;iptables 和 nftables 只是把你的规则翻译、装进 netfilter 钩子的前端工具。换前端,不换引擎。

为什么从 iptables 到 nftables:iptables 历史包袱重(IPv4/IPv6/arp/eb 四套独立工具、规则线性匹配、语法零散)。nftables 用一套统一语法、一个 nft 命令管 IPv4/IPv6,支持原子更新、集合(set)与 map,性能更好。现代发行版默认 nftables(你敲的 iptables 命令现在多半是 nftables 的兼容层)。本章主讲 nftables,但会标出对应的 iptables 概念——你读老脚本、老资料还会遇到它。

路由器关联 router-link

OpenWRT 从 22.03 起防火墙换成 firewall4,后端就是 nftables(取代了老的 firewall3 + iptables)。所以学 nftables 正好对上现在的路由器。2.4 会拆 firewall4 怎么把 UCI 配置生成 nftables 规则。

2五链模型:包穿过内核的五个关卡

数据包进内核后,会依次经过五个钩子。它们相对"路由决策"的位置,决定了每个钩子能干什么:

入站 出站 PREROUTING DNAT 改目的 (路由前) 路由决策 INPUT FORWARD 本机进程 OUTPUT POSTROUTING SNAT 改源 (路由后) 发给本机 转发

顺着包的走向读这张图:

  • PREROUTING:包刚进来,还没路由。DNAT 在这里
  • 路由决策:这个包是发给本机的,还是要转发的?分两路。
  • INPUT:发给本机的包(防火墙拦本机入站)。FORWARD:要转发的包(本机当路由器时,LAN→WAN 等转发流量的过滤)。OUTPUT:本机自己产生的出站包。
  • POSTROUTING:包做完所有处理、即将离开。SNAT / masquerade 在这里

现在揭开 0.6 的两个"为什么":

  • DNAT 为什么在 PREROUTING:它改的是目的地址,必须在路由决策之前改——否则内核按"原来的目的"去路由,就送错地方了。改完目的,路由才能把它导向正确的接口。(端口转发就在这。)
  • SNAT 为什么在 POSTROUTING:它改源地址,要在路由决策之后——得先知道包从哪个接口出,才好改成那个接口的公网 IP;而且改源不影响路由(路由只看目的)。(masquerade 就在这。)
类比:一条流水线上的五个工位

把五个钩子想成流水线上的五个工位,中间夹着一个"分拣站"(路由决策)。每个工位你都能挂规则(检查 / 改写 / 放行 / 丢弃),iptables、nftables 就是给工位写作业指导书。DNAT 必须在分拣前(决定送往哪),SNAT 必须在分拣后(贴上回程地址)——不是规矩,是物理位置使然。

3conntrack:让防火墙有记忆

回想 0.4 / 0.6:conntrack 用四元组记录每条连接。除了 NAT 用它做地址改回,防火墙也用它判断连接状态,从而写出"有状态"规则。一条连接在 conntrack 眼里有几种状态:

NEW
一条连接的第一个包(之前没见过)
ESTABLISHED
属于一条已建立连接(双向都通过的)——回包就是它
RELATED
与某条已建立连接相关的新连接(如 FTP 数据连接、ICMP 错误)
INVALID
对不上任何已知连接(通常丢弃)

有状态防火墙的精髓:你只需放行"出站发起的连接 + 它们的回包(ESTABLISHED/RELATED)",拒绝主动进来的 NEW。这样不必为每个回包单独开规则。

路由器关联 router-link

你家路由器的默认策略正是这套有状态防火墙:LAN 主动连外网 → 允许,回包(ESTABLISHED)→ 允许,外网主动连进来(NEW)→ 默认拒绝。这也呼应 IPv6 章的"NAT 不是防火墙"——真正挡住外部主动连接的,是这条有状态规则 + 没有对应的 DNAT,而不是 NAT 本身。下一节就把它写出来。

4nftables 实战:NAT + 有状态防火墙

nftables 的结构很清晰:table(表)→ chain(链,挂到某个 hook)→ rule(规则)。先建一个 NAT 表,把 0.6 的 masquerade(SNAT)和一条端口转发(DNAT)写出来:

bash · NAT
sudo nft add table ip nat

# POSTROUTING 链:出 WAN 口(eth0)的流量做 masquerade(SNAT)
sudo nft 'add chain ip nat postrouting { type nat hook postrouting priority 100 ; }'
sudo nft add rule ip nat postrouting oifname "eth0" masquerade

# PREROUTING 链:端口转发(DNAT),到本机 25565 → 内网 192.168.1.50
sudo nft 'add chain ip nat prerouting { type nat hook prerouting priority -100 ; }'
sudo nft add rule ip nat prerouting iifname "eth0" tcp dport 25565 dnat to 192.168.1.50:25565

注意每条链挂在哪个 hook:postrouting 链 hook postrouting(SNAT 的位置),prerouting 链 hook prerouting(DNAT 的位置)——正是上一节"为什么 SNAT 在路由后、DNAT 在路由前"的直接落地。

再写有状态防火墙(放在 inet 族的 filter 表,一套同时管 IPv4+IPv6):

bash · 有状态防火墙
sudo nft add table inet filter

# INPUT 链:默认 drop,但放行回包 + 环回
sudo nft 'add chain inet filter input { type filter hook input priority 0 ; policy drop ; }'
sudo nft add rule inet filter input ct state established,related accept   # 回包放行
sudo nft add rule inet filter input iifname "lo" accept                    # 环回放行
sudo nft add rule inet filter input ct state invalid drop
sudo nft add rule inet filter input tcp dport 22 ct state new accept       # 放行 SSH

sudo nft list ruleset                                                      # 看你写的全部规则

这套就是 conntrack 状态机在防火墙里的直接应用:ct state established,related accept 一行,放行所有出站连接的回包;主动进来的 NEW 被 policy drop 拦掉,除非你显式放行(如 SSH)。

路由器关联 router-link

一个 masquerade + 一条有状态 input + 按需放行——这基本就是一台家用路由器防火墙的骨架。OpenWRT 的 firewall4 把你在 LuCI / UCI 里配的"zone、forwarding、rule"翻译成的,正是这种 nftables 规则(2.4 会拿真实生成的规则对照)。而给 1.2 的 ip rule 用的 fwmark 打标也在这里写:… meta mark set 0x1,再配 ip rule add fwmark 0x1 table 100 做分流——nftables 打标、ip rule 选表,1.2 那个伏笔在这闭合。

本章小结

  • netfilter 是内核框架,iptables(老)/ nftables(新)只是写规则的前端;现代用 nftables。
  • 五链按包路径排列:PREROUTING → 路由决策 → INPUT / FORWARD → OUTPUT → POSTROUTING。DNAT 在 PREROUTING(路由前改目的),SNAT 在 POSTROUTING(路由后改源)——0.6 的落地,由钩子位置决定。
  • conntrack 跟踪连接状态(NEW / ESTABLISHED / RELATED / INVALID),让防火墙有状态:放行回包、拦主动进入。
  • nftables 实战:table → chain(挂 hook)→ rule;masquerade = SNAT,dnat to = 端口转发,ct state established,related accept = 有状态防火墙核心。
  • 这套骨架就是家用路由器防火墙;firewall4 是它的 OpenWRT 封装(2.4),fwmark 打标接 1.2 的 ip rule 分流。

动手练习

  1. 对照五链图说出:你 ping 一个外网 IP,出去的包和回来的包各经过哪些链?(出:OUTPUT → POSTROUTING;回:PREROUTING → 路由决策 → INPUT。)
  2. 在一台 Linux(或 namespace 环境)建一个 inet filter 表 + input 链,policy drop,加 ct state established,related accept 和放行 lo;测本机还能不能上网、能不能被 ping。做完 nft flush ruleset 清理。注意:可能把自己锁在外面,先想好恢复方式。
  3. 思考题:为什么有状态防火墙只需一条 ct state established,related accept 就能放行所有出站连接的回包,而不必为每个端口单独开规则?(回想 conntrack 的会话表。)
  4. 进阶:把 0.3 的双子网 namespace 实验加上 NAT——在路由器 namespace rnft 写一条 masquerade,让 h1 的包从 r 出去时被 SNAT;用 conntrack -L 或抓包观察源地址改写。这就把 0.6 + 1.3 + 0.3 串到了一起。