
改到第四十条规则时,你就不敢再动了
用 iptables 管过服务器的人都有这个时刻:规则表写到四十条,某天要开一个新端口,你盯着屏幕看了三分钟,不确定插在哪一行——因为不知道哪一条正在挡着 SSH。
这不是能力问题,是工具问题。iptables 把「表、链、规则」拆成三套命令,插入位置靠行号,查一条规则要 `iptables -L -n –line-numbers` 再看一遍,改一条要 `-I` 加数字。规则一多,维护成本就不是线性增长的。
nftables 解决的不是性能——多数场景下两者的转发性能差异可以忽略。它解决的是「你还敢不敢改」这个问题。
1. 两套模型的对应关系
先建立映射,后面才不迷路。iptables 的「表」在 nftables 里还叫 table,但多了一层「族」(family)的概念,而「链」和「规则」被合并进了同一份文本。
| iptables 概念 | nftables 对应 | 关键差异 |
|---|---|---|
filter / nat 表 |
table inet filter |
inet 族可同时处理 IPv4 与 IPv6,不必写两遍 |
INPUT / OUTPUT 链 |
chain input { type filter hook input ... } |
hook 与 type 写在链定义里,不再是隐式约定 |
iptables -A/-I |
直接写在链的花括号里 | 顺序即文件顺序,看得见 |
-m state --state |
ct state |
连接跟踪改为 ct 表达式 |
-m recent |
meter / 动态 set |
原模块没有直接对应,要用集合重写 |
最重要的一条差异在最后:nftables 允许你在一个 table 里放任意多条链,且链与链之间可以互相跳转。这让「按用途分链」成为可能——SSH 一条链、Web 一条链、Docker 一条链,出问题时能一眼定位是哪一块在生效。
2. 一份能直接用的最小配置
下面是整份配置文件,一台只跑 SSH 和 80/443 的机器的完整版本。请注意它是一份**整体**,不是逐条追加——nftables 的加载单位是文件,不是命令行。
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
set blackhole {
type ipv4_addr
flags dynamic, timeout
timeout 1h
}
chain input {
type filter hook input priority filter; policy drop;
iif lo accept
ct state established,related accept
ct state invalid drop
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
tcp dport 22 ct state new limit rate 6/minute burst 4 packets accept
tcp dport { 80, 443 } ct state new accept
ip saddr @blackhole drop
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
几个必须解释的地方:
policy drop 写在链定义那一行,不是最后一条规则。它是默认verdict,所有没命中的包都走这里。这也是最容易出事的地方——一旦 flush 之后没接上新规则,SSH 立刻断。
ct state established,related accept 必须排在所有新连接规则之前。它放行的是「已建立连接的回包」,缺了它,你发出去的请求回不来,表现是能 ping 通但什么都连不上。
iif lo accept 不能省。很多本机服务(数据库、缓存、localhost 调用)走回环,默认 drop 会把它们全部掐死,而报错信息通常指向应用层,查半天查不到防火墙。
ICMP 和 ICMPv6 要分开放。只放 ICMP 的话,IPv6 会因为邻居发现(NDP)失败而整段不通,表现是「IPv4 正常,IPv6 超时」,很容易误判成运营商问题。
3. 六组语法对照,照着换就行
迁移时九成的改写量集中在这六组。左边是你现在写的,右边是等价的 nftables 写法。
| iptables | nftables |
|---|---|
-p tcp --dport 22 -j ACCEPT |
tcp dport 22 accept |
-s 10.0.0.0/8 -j ACCEPT |
ip saddr 10.0.0.0/8 accept |
-m multiport --dports 80,443 |
tcp dport { 80, 443 } |
-m state --state NEW,ESTABLISHED |
ct state new,established |
-j LOG --log-prefix "DROP:" |
log prefix "DROP: " level warn |
-j DNAT --to-destination 10.0.0.5:8080 |
dnat to 10.0.0.5:8080 |
注意第三行:花括号集合替代了 multiport 模块,而且是原生的。端口可以写成区间 `1024-2048`,也可以混合 `{ 22, 80, 443, 8000-8100 }`。这一处改写最省事。
还有一处容易漏:nftables 默认不计包数。iptables 每条规则自带计数器,nftables 需要显式写 `counter`。想看哪条规则在命中,必须加这个词。
4. 三个必然踩到的坑
坑一:Docker 会把你的规则顶掉。Docker 仍然通过 iptables 接口写规则,在 nft 后端下这些规则会落进它自己创建的 `ip filter` 和 `ip nat` 表。你在 `inet filter` 里设了 `policy drop`,但 Docker 发布端口时写的是 DNAT + ACCEPT,两者不在同一个 table,结果是「容器端口对外可达,但你的防火墙规则看起来完全没生效」。排查时一定要 `nft list ruleset` 看全量,不要只看自己那张表。
坑二:flush ruleset 之后连接就断了。因为旧规则被清空的那一瞬间,默认策略回到 accept,紧接着你加载新规则、设成 drop——如果中途 sshd 正好有个包在飞,或者加载失败,你就被锁在门外。正确做法是先挂一个自动回滚,再动手:
# 15 分钟后无条件清空规则,给自己留后路
systemd-run --on-active=15min --unit=nft-rollback \
nft flush ruleset
# 确认新规则工作正常后,取消这个定时器
systemctl stop nft-rollback.timer
这招救过很多人。它不优雅,但比半夜叫机房重启强。
坑三:规则文件加载不是原子的,但整份替换是。`nft -f` 读取文件时是逐条执行的,中途语法错会停在一半,留下一个残缺规则集。所以上线前必须用 `nft -c -f /etc/nftables.conf` 做一次仅检查不执行的语法验证。这一步能拦掉八成事故。
5. 迁移的实操顺序
不要一次性替换。按下面五步走,每一步都可回退:
- 先把现有 iptables 规则导出成文本:`iptables-save > /root/iptables.bak`。这是唯一的退路。
- 用 `iptables-restore-translate` 生成初版:它会输出一份等价的 nft 规则,不要直接用,但要拿它做语法参照,省掉大量查文档的时间。
- 在独立 table 里并行跑:新建 `table inet staging`,只挂 log 不挂 drop,观察一晚命中情况。
- 确认无误后切换到主 table:带上面那个 15 分钟回滚定时器。
- 关掉旧后端:`update-alternatives –set iptables /usr/sbin/iptables-nft`,避免两套规则同时生效互相干扰。
第三步值得多说一句。用 log 代替 drop 做灰度,是防火墙改造里最被低估的手法——你能看到「假如现在就 drop,会错杀哪些流量」,而代价只是日志多几行。
6. 上线后的验收
三条命令,五分钟:
- `nft list ruleset` 输出完整、无报错,且 `policy drop` 出现在 input 链上。
- 从外部扫一遍:`nmap -Pn -p 1-1000 你的IP`,只应看到 22/80/443。
- 本机验证回环没被误伤:`curl -s localhost:你的服务端口`,应当有响应。
第三条最容易被跳过,也最容易在上线两小时后被业务方找上门。防火墙改完,第一个该测的不是外网,是本机。
写在最后
从 iptables 到 nftables,表面是语法变了,实质是把「一堆命令的执行历史」变成了「一份可以被阅读的文档」。前者只能靠记忆和试探维护,后者可以 review、可以进版本库、可以 diff。
这件事的价值不在于你少敲了几个字母,而在于半年后接手的人——可能是别人,也可能是你自己——打开那份文件,能看懂它想干什么。
运维里最难的不是配通,是配通之后还敢改。所有让人不敢改的配置,迟早会烂在那里。
你的服务器上,防火墙规则是写在一个文件里,还是一串散落在历史记录里的命令?

评论 (0)