这套服务器的防火墙规则已经三年没人动过了。前几天接手维护,打开 iptables-save 的输出一看,几百行规则整整齐齐躺在那里,但真正在用的链条可能不到一半。Linux 内核其实早在很多年前就把 iptables 的实现搬到了 nftables 框架上,只是用户空间的命令还保留着老面孔。与其等着哪天踩到兼容性坑,不如趁这次维护,把墙上的规则完整迁到 nftables。
这篇文章就做一件事:教你如何把现有的 iptables 规则安全、有序地迁移到 nftables。我会从内核框架差异讲起,给你一套完整的规则盘点方法、语法对照表、生产级迁移脚本示例、上线验证流程,最后附上我自己实操中踩过的坑。整个过程不依赖图形界面,全部命令行完成,适合有一定 Linux 运维基础、正在做防火墙规则迁移的读者,也适合刚接触 nftables、想快速理解两套工具差异的新手。
1. 迁移前先搞清楚:iptables 和 nftables 的底层差异
1.1 为什么 Linux 要换防火墙框架
很多人以为 iptables 是 Linux 内核里的东西,其实不是。iptables 是用户空间的老工具,内核里真正做包过滤的框架叫 Netfilter。早期的 iptables 把 Netfilter 的能力封装成四张表五条链,这种模型简单直观,但问题也很多。
规则越加越多,性能会下降,因为每新增一条规则都要整个表重新构建;各种扩展模块比如 -m state、-m limit 各自为政,语法不一致;调试和排错全靠一条条数规则,操作体验很差。更要命的是,iptables 的底层实现代码在长时间演进后已经变得非常复杂,问题修复和新特性开发都很难推进。
nftables 从内核 3.8 左右开始引入,目标就是取代 iptables。它不是一个简单的工具替代,而是一整套框架的重构。nftables 把匹配条件和处理动作统一成表达式,支持集合、字典、动态更新规则,规则集可以用脚本文件管理,不用每次变更都重新加载全表。内核模块数量少了很多,更新规则的粒度也更细,还支持 nft monitor trace 这种数据处理跟踪手段。
值得说明的是,现在很多发行版里你敲 iptables 命令,背后其实跑的就是 nftables 的兼容层。比如 Debian 10+、CentOS 8 里默认的 iptables 命令实际是 iptables-nft。这种情况下规则本身还是旧语法,但内核处理路径已经走到了 nftables 里。真正意义上的迁移,是把规则文件从 iptables 语法,改写为 nftables 语法,并直接用 nft 命令加载。
1.2 核心语法差异
直接看语法,iptables 和 nftables 的差别非常明显。
iptables 的规则写在命令行里,从左到右是先指定表、链,再拼一堆 -p、-s、-d、--dport 这样的参数。你看到 iptables -A INPUT -p tcp --dport 22 -j ACCEPT,意思是向 INPUT 链追加一条规则,匹配 TCP 协议、目标端口 22,执行 ACCEPT。括号朝鸡蛋壳一样,参数顺序不能乱。
nftables 的语法逻辑完全不同。规则就是一段可读的表达式,比如 tcp dport 22 accept,翻译过来是“匹配 TCP 协议、目标端口 22,然后接受”。规则写在脚本里,可以加注释,也可以分批添加。nftables 里的“表”必须有地址族(address family),是 ip、ip6、inet 还是 bridge、netdev。inet 族可以同时承载 IPv4 和 IPv6 规则,这一下就把双栈维护的负担砍掉了一半。
链也不是固定的五条链了。nftables 中基础链需要显式声明挂钩点(hook),比如 type filter hook input priority filter。这里 priority filter 是优先级,policy drop 或 policy accept 是链默认策略。普通链则不需要 hook,跟编程语言里的子函数一样,只有被别的链 jump 或 goto 时才会执行。
还有一个概念差别值得注意。iptables 里的 -m state、-m conntrack 这类匹配扩展,在 nftables 里统一为 ct 表达式。ct state established,related accept 就能完成状态跟踪匹配,不用再挂一个独立的模块。整个规则语言的统一性,是 nftables 在设计上最值得肯定的一点。
1.3 迁移可行性评估
并不是所有 iptables 规则都能一键翻译成 nftables。动手之前先盘一盘你的规则分布,大致可以分成三类。
第一类是 filter 表规则,最容易迁移。INPUT、FORWARD、OUTPUT 链里的 ACCEPT、DROP、REJECT、LOG,几乎都能用 nftables 相同语义的表达式直接改写。端口、地址、接口匹配在 nftables 里都有对应的关键词,转换几乎没有损耗。
第二类是 nat 表规则,需要清楚挂钩点和优先级。MASQUERADE、DNAT、SNAT 都能翻译,但你必须把它放到正确的基础链上。POSTROUTING 链里的 MASQUERADE,在 nftables 中要写成带 type nat hook postrouting priority srcnat 的基础链;PREROUTING 链里的 DNAT 则对应 type nat hook prerouting priority dstnat。优先级写错,NAT 就不会按预期工作。
第三类是 mangle 和 raw 表规则,需要额外判断。mangle 表里用来改 TTL、打标记等操作,在 nftables 里要用对应的表达式重写;raw 表里的 -j NOTRACK 在 nftables 里是 ip6? 不,是 notrack 关键字。如果你之前的 mangle 规则只是做一些琐碎的包处理,迁移时正好可以审视一下是否真的还需要。
我的建议是:别一上来就想把全部规则在一天内迁完。先迁 filter 表,跑通了再迁 nat 表,最后处理 mangle 和 raw 这种少数派规则。每迁完一个部分,做一轮连通性测试,让风险可控。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迁移前的准备工作:盘点现有规则集
2.1 导出规则:iptables-save 的正确姿势
任何迁移都从备份开始。别用 iptables -L -v 这种人工看的格式做备份,那是给人看的,不是给恢复脚本用的。正确的导出命令是 iptables-save,它会输出规范化的规则集文本,可以直接通过 iptables-restore 恢复。
登录到服务器,先建一个迁移工作目录,然后执行三条命令:
bash复制mkdir -p /root/fw-migration
iptables-save > /root/fw-migration/ruleset.v4
ip6tables-save > /root/fw-migration/ruleset.v6
iptables-save -t filter > /root/fw-migration/ruleset.filter.v4
第一条命令导出 IPv4 规则,第二条导出 IPv6 规则,第三条单独导出 filter 表作为参考。很多人在迁移时只保存 IPv4,结果 IPv6 的规则在切换后全丢了,防火墙出现“看起来没开,但就是不通”的诡异现象。IPv4 和 IPv6 必须同时处理。
导出后,用 wc -l 看一下行数,再用 grep -E '^:[A-Za-z]+' 查看所有链的名字。花五分钟浏览一下规则文件,确认服务器上现存的链、表都在你预期范围里。如果发现某个链里的规则已经几个月没人引用,可以在迁移时顺手清理,但不要在没有确认业务影响的情况下贸然删链。保守做法是先保留,等迁移完成后再单独清理。
2.2 规则分类逐表梳理
iptables-save 输出内容里有个明显的分段标志,*filter、*nat、*mangle、*raw 四个表是分开的。如果你发现输出根本没有某个表,比如没有 *nat,说明该表未被使用,迁移工作也就不用覆盖那一部分。
用 grep 按表分类提取规则,方便后续对照:
bash复制awk '/^\*/{table=$1; sub(/^\*/,"",table)} /^-A/{print table": "$0}' /root/fw-migration/ruleset.v4
这行命令会在每条 -A 规则前面加一个表名前缀,一目了然。我对一台典型业务服务器做统计时,结果大致是:filter 表占 80% 规则、nat 表占 15%、mangle 表有个别几条、raw 表往往为空。真实生产环境里,大部分规则集中在 filter 表的 INPUT 和 FORWARD 链,这些就是你迁移的主战场。
梳理的时候还要留意自定义链。iptables 里使用 -N 创建的自定义链,在 nftables 里不需要声明 hook,只要在同一个表里定义一个普通链即可。但要注意自定义链的引用关系,比如 -A FORWARD -j DOCKER-USER 这样的调用,转换后要写成 jump DOCKER-USER,不能漏掉链名。
2.3 环境检查:内核版本、工具链、系统服务依赖
迁移前需要确认目标系统满足基本条件。查看内核版本和 nftables 工具版本:
bash复制uname -r
nft --version
内核 4.18 以上的发行版基本都能完整支持 nftables 的常用功能。如果你的内核还很老,比如 CentOS 7 早期内核,建议先升内核再迁,否则一些集合、字典、limit 表达式可能在旧内核上表现不一致。
还要检查系统里现在有哪些防火墙管理工具正在运行。如果你用的是 firewalld,那么它内部的规则本质上是 nftables 规则,但你不能直接跟它抢 /etc/nftables.conf 的控制权。如果你用的是 ufw,同样,它有自己的规则管理逻辑。这类场景更适合保留原工具,通过工具自身的规则接口来配置,而不是自己上手写 nft 脚本。如果你现在是裸跑 iptables 命令或脚本,没有其他防火墙管理工具,那迁移空间就非常干净,直接干就行。
检查 systemctl 里跟防火墙相关的服务状态:
bash复制systemctl list-units | grep -iE 'iptables|ip6tables|firewalld|ufw|nftables'
有的系统开机自动执行 /etc/rc.local 里的 iptables 恢复脚本,这类隐藏在启动流程里的逻辑最容易在迁移后被忽略。务必看一遍启动脚本,确认旧规则加载路径不止 systemd 服务这一条。
2.4 备份与回滚方案
防火墙迁移最怕的不是新规则写不对,而是写错了回不去。在开始改写之前,我必须先把回滚方案做好。
最简单有效的回滚办法,是保存一份 iptables 恢复脚本。下面这段内容放到 /root/fw-migration/rollback.sh 里,迁移失败时直接执行:
bash复制#!/bin/bash
iptables-restore < /root/fw-migration/ruleset.v4
ip6tables-restore < /root/fw-migration/ruleset.v6
还要同时记录当前开放端口和会话状态,作为迁移后的对比基准:
bash复制ss -lntp > /root/fw-migration/ports.before.txt
cat /proc/net/nf_conntrack > /root/fw-migration/conntrack.before.txt 2>/dev/null || true
如果服务器是云主机,建议再做一次云厂商侧的快照。规则恢复脚本只是把防火墙状态拉回以前,但如果迁移过程中网络中断导致你连不上服务器,快照能让你回到迁移前的完整状态。别小看这一步,我见过不止一次,有人手滑 flush 规则集后把自己 SSH 断掉,最终只能靠控制台 VNC 自救。
备份做完了,下面进入真正的语法转换环节。这是整个迁移里最耗时间、也最考验细心的部分。
3. 核心环节:iptables 规则翻译成 nftables 语法
3.1 常用语句语法对照速查表
先把高频语句对照表列出来。这一张表覆盖了绝大多数业务服务器会用到的防火墙规则,迁移时可以直接对照改写。
| 功能描述 | iptables 语法 | nftables 语法 |
|---|---|---|
| 接受目标端口 22 | -A INPUT -p tcp --dport 22 -j ACCEPT |
tcp dport 22 accept |
| 接受已建立连接 | -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT |
ct state established,related accept |
| 接受来自内网的包 | -A INPUT -s 10.0.0.0/8 -j ACCEPT |
ip saddr 10.0.0.0/8 accept |
| 按入站接口过滤 | -A INPUT -i eth0 -j DROP |
iifname "eth0" drop |
| 丢弃并记录日志 | -A INPUT -j LOG --log-prefix "input-drop" + -A INPUT -j DROP |
log prefix "input-drop" drop |
| 接受 ICMP 回显请求 | -A INPUT -p icmp --icmp-type echo-request -j ACCEPT |
icmp type echo-request accept |
| 源地址 NAT | -t nat -A POSTROUTING -s 10.0.0.0/24 -j MASQUERADE |
ip saddr 10.0.0.0/24 masquerade |
| 目的地址 NAT | -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to 10.0.0.5:80 |
tcp dport 8080 dnat to 10.0.0.5:80 |
| 限制新连接速率 | -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m limit --limit 10/min -j ACCEPT |
tcp dport 22 ct state new limit rate 10/minute accept |
| 丢弃无效状态报文 | -A INPUT -m conntrack --ctstate INVALID -j DROP |
ct state invalid drop |
| 接受 UDP 53 | -A OUTPUT -p udp --dport 53 -j ACCEPT |
udp dport 53 accept |
对照表看着简单,但有几个隐藏差异必须提醒。第一条,nftables 的规则默认就是“匹配即执行”,没有 -j 这种目标参数,直接写 accept、drop 或 reject 即可。第二条,nftables 里动作关键字要小写。第三条,写 iifname 时接口名务必加双引号,不加引号在语法检查阶段就会报错。
3.2 分步转换:先拿 filter 表练手
拿一个小型服务器的 INPUT 链做练手。假设原来的 iptables 规则如下:
bash复制iptables -P INPUT DROP
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A INPUT -j LOG --log-prefix "iptables-input-drop: "
iptables -A INPUT -j DROP
这段规则的核心逻辑:默认丢弃 INPUT 链末段流量,放行回环、已建立连接、SSH、HTTP、Ping。转换为 nftables 脚本文件 /root/fw-migration/ruleset.nft:
code复制#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif "lo" accept
tcp dport 22 ct state new accept
tcp dport 80 ct state new accept
icmp type echo-request accept
log prefix "nft-input-drop: " drop
}
chain forward {
type filter hook forward priority filter; policy accept;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
重点解释几个地方。flush ruleset 是 nftables 脚本的标准开头,它会清空当前所有规则表,保证脚本重复加载时不会产生残留。table inet filter 表示创建一张适用于 IPv4 和 IPv6 的 filter 表,inet 族是双栈服务器的最佳选择,不用分别维护 ip 表和 ip6 表。policy drop 写在链定义里,相当于 iptables -P INPUT DROP。
这里我特意把 icmp type echo-request 放在 inet 表里。nftables 在 inet 族下对 icmp 表达式做了统一处理,一个表达式同时覆盖 IPv4 和 IPv6 的 ping 请求,不需要写成两条规则,这是旧框架做不到的便利。
3.3 重点难点:NAT、状态跟踪、自定义链
NAT 规则翻译时最容易出错的是挂钩点和优先级。看一个实际场景,服务器内网网段 10.10.0.0/16 通过 eth0 上网,需要做源地址 NAT;同时要把 8080 端口映射到内网 10.10.0.5 的 80 端口。
原来的 iptables 规则:
bash复制iptables -t nat -A POSTROUTING -s 10.10.0.0/16 -o eth0 -j MASQUERADE
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 10.10.0.5:80
iptables -A FORWARD -p tcp --dport 80 -j ACCEPT
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
转换成 nftables,需要创建 table ip nat:
code复制table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.10.0.0/16 oifname "eth0" masquerade
}
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
tcp dport 8080 dnat to 10.10.0.5:80
}
}
核心点在优先级。MASQUERADE 工作在 postrouting 挂钩点,优先级用 srcnat 表示,因为它在 DNAT 之后执行;DNAT 工作在 prerouting 挂钩点,优先级用 dstnat。如果你把 srcnat 和 dstnat 写反,规则能加载,但 NAT 不会生效,而且系统没有任何明显的报错日志,非常隐蔽。
FORWARD 链的放行也要注意。很多人在 NAT 迁移时只关注 nat 表,却忘了 FORWARD 链默认丢弃或策略收紧后,转发的数据包根本到不了 POSTROUTING。上面示例中的 FORWARD 规则要单独写成 filter 表里的链。
状态跟踪在 nftables 里用的是 ct state。iptables 的 -m conntrack --ctstate NEW,ESTABLISHED,RELATED,INVALID,对应的 nftables 写法是 ct state new,established,related,invalid。一个特别容易踩的坑是:conntrack state 里还有一个 ct status,它是状态标记,比如 ct status expected,跟 ct state 的语义完全不同,不要混用。
自定义链转换也有讲究。比如 iptables 里有:
bash复制iptables -N PROTECT-WEB
iptables -A FORWARD -p tcp --dport 443 -j PROTECT-WEB
iptables -A PROTECT-WEB -m limit --limit 100/s -j ACCEPT
nftables 中用同名的普通链和 jump 调用:
code复制table inet filter {
chain protect_web {
tcp dport 443 limit rate 100/second accept
drop
}
chain forward {
type filter hook forward priority filter; policy accept;
tcp dport 443 jump protect_web
}
}
注意 nftables 的链名不建议带横杠和中文,虽然语法上可能支持,但写进变量环境、脚本传递时容易出问题,统一用下划线更稳妥。
3.4 完整迁移实例:中小型业务防火墙
把所有知识串起来,给一个相对完整的生产示例。假设一台双栈云服务器,公网接口 eth0,内网接口 eth1,上面跑着 Web 和 SSH 服务。需求是默认拒绝入站,仅放行必要端口,限制 SSH 爆破,允许内网主机访问外网。
/etc/nftables.conf 的完整内容可以这样写:
code复制#!/usr/sbin/nft -f
flush ruleset
define lan = 192.168.100.0/24
define ssh_port = 2222
define web_ports = { 80, 443 }
table inet firewall {
set allowed_hosts {
type ipv4_addr
elements = { 192.168.100.10, 192.168.100.20 }
}
set bad_hosts {
type ipv4_addr
timeout 1h
}
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif "lo" accept
ip saddr $lan accept
ip saddr @allowed_hosts accept
tcp dport $ssh_port ct state new limit rate 10/minute accept
tcp dport $web_ports accept
ip protocol icmp icmp type echo-request accept
ip6 nexthdr icmpv6 icmpv6 type echo-request accept
ct state invalid drop
log prefix "nft-drop-input: " drop
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
ip saddr $lan accept
}
chain output {
type filter hook output priority filter; policy accept;
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr $lan oifname "eth0" masquerade
}
}
这个脚本有几个值得抄走的点。define 用来定义变量,迁移后改端口和网段只需要改文件顶部的宏定义,不用整篇搜索替换。set 是 nftables 的集合,bad_hosts 这种集合带 timeout 1h,可以配合 nft add element 实现临时封禁,比 iptables 里手动加规则优雅得多。limit rate 10/minute 直接限制 SSH 每分钟新连接数,是防爆破的最简方案。IPv6 的 ping 放行用的是 ip6 nexthdr icmpv6 icmpv6 type echo-request,这一句经常有人漏掉,导致迁移后 IPv6 ping 不通。
脚本写好后,先做语法检查,再加载:
bash复制nft -c -f /etc/nftables.conf
nft -f /etc/nftables.conf
nft list ruleset
-c 参数是 check,只检查语法不加载规则。确认无误后,再用 -f 实际加载。我强烈建议每次改动都先跑一遍 -c,尤其在你使用 define 和 set 的脚本里,变量拼错或者集合类型写错,系统不会在加载时报出行号,排查起来非常吃力。
4. 实操验证与上线:别直接替换,先并行跑
4.1 在测试环境加载 nft 规则集
即使是老练的运维,我也不推荐直接在线上服务器替换防火墙。至少准备一台同网段、可以独立访问的测试机,或者用容器模拟一台,先把 nft 规则集加载上去验证。
测试环境里先恢复原始 iptables 规则,保证 SSH 始终能通:
bash复制iptables-restore < /root/fw-migration/ruleset.v4
ip6tables-restore < /root/fw-migration/ruleset.v6
然后加载 nft 规则集:
bash复制nft -c -f /etc/nftables.conf
nft flush ruleset
nft -f /etc/nftables.conf
注意这里的加载顺序:先 flush ruleset 清空所有规则,再加载新脚本。如果直接 nft -f 加载,脚本里的 flush ruleset 也会清空,但如果你中途断了一下没完全执行,旧的 iptables 规则可能还在。所以稳妥做法是分两步:先清空,再加载。清空前确认 SSH 连接是复用已有连接状态,否则 flush ruleset 会瞬间把你的控制会话也干掉。
4.2 验证工具与手法
加载完成后不要只看 nft list ruleset 输出就下结论。要验证规则真正产生了预期行为,我一般按顺序做下面这几组测试:
- 验证基础放行。在另一台机器执行
ssh -p 2222 目标IP,能正常连上;curl -I http://目标IP能返回 200 或 301。 - 验证默认拒绝。用未放行端口,比如
telnet 目标IP 3306,预计连接超时或直接被拒绝。 - 验证状态跟踪。先建一个长连接,比如
ping 目标IP,然后加载规则,观察现有ping会话是否中断。正常情况 RELATED 和 ESTABLISHED 匹配会放行已建立的 ping 会话,如果你看到 ping 突然中断,说明ct state established,related accept这一句有问题。 - 验证 NAT。在服务器上执行
tcpdump -i eth0 icmp,同时从内网主机 ping 公网地址,确认源地址已经变成服务器公网 IP。 - 验证 IPv6。在 v6 地址上执行
ping6和 SSH,确认 IPv6 放行规则正常。
每次测试都结合 nft list ruleset 看规则计数。nftables 规则有计数器功能,在链定义里加一句 counter 或在规则后追加 counter,就能看到每条规则被命中了多少次。验证时看到计数器在涨,比干看规则文本靠谱得多。
bash复制nft list ruleset -a
-a 参数会显示每条规则的句柄,方便后续单独删除某条规则。测试过程中如果发现某条规则写错,不需要整表 flush,直接按句柄删规则即可。
4.3 灰度切换:停掉 iptables 服务前必须做的几件事
确认测试环境规则集正常工作后,才轮到线上切换。但切换前有几件小事必须做,否则容易在最后一刻翻车。
先看系统当前的防火墙服务状态:
bash复制systemctl list-units | grep -iE 'iptables|nftables|firewalld'
很多 CentOS 7 机器的 iptables 规则并不是通过 systemd 服务加载的,而是写在 /etc/sysconfig/iptables 里,由 iptables.service 开机时执行。迁移前要确认这个服务的状态,并把它禁用掉:
bash复制systemctl disable iptables
systemctl disable ip6tables
但不要在执行 disable 后立刻停掉服务。你的新规则还没上线,旧规则要是先停了,防火墙就形同虚设。正确顺序是:先把 nftables 服务启用并加载新规则,确认业务流量正常后再禁用旧服务。
bash复制systemctl enable nftables
systemctl restart nftables
systemctl disable iptables
systemctl disable ip6tables
最后再用一次回滚脚本,确认回滚链路是通的。毕竟你永远不知道什么时候会用到它。
4.4 开机自启配置
nftables 规则放进 /etc/nftables.conf 还不够,系统重启后必须自动加载。Debian 和 CentOS 上都提供了 nftables.service,启用方式几乎一致:
bash复制systemctl enable nftables
systemctl start nftables
但有个细节要留意。Debian/Ubuntu 上 /etc/nftables.conf 默认内容很简单,可能只是一个空表定义;CentOS/RHEL 上默认配置文件里带有包括目录的写法。如果你自己的规则文件是用 include 拆分成多个文件的,那么主配置文件里必须加上 include 语句:
code复制include "/etc/nftables.d/*.nft"
Debian 的 nftables.service 默认加载规则文件前会先执行 flush ruleset。而 CentOS 的 service 文件不会替你 flush。为了统一行为,所有自定义规则脚本里都要显式写 flush ruleset 作为首行,这一点在迁移时就要养成习惯。
重启验证也很简单:
bash复制reboot
# 重启后执行
nft list ruleset
看到规则集完整出现,才算真正完成了开机自启这最后一步。很多人迁移时规则加载正常,却没做重启验证,结果下一次机房断电重启后,服务器裸奔在公网里,这才是最大的事故隐患。
5. 常见问题与排查技巧实录
5.1 问题速查表
实际迁移过程中,我把高频问题整理成了一份速查表。你迁移时如果挂在哪里,直接对号入座排查。
| 症状 | 可能原因 | 处理建议 |
|---|---|---|
nft -c -f 报 syntax error |
语法写错,常见于漏了分号、引号、链定义不完整 | 用 nft -c -f -d all 开启调试输出,定位到具体行 |
| 加载后 SSH 断了 | flush ruleset 把旧规则全清了,新规则没包含 SSH 放行 |
通过控制台或带外连接恢复,重新加载完整规则集 |
| IPv6 完全不通 | 只迁移了 ip 表,没有迁移 ip6 表或 inet 表 | 统一使用 inet 族,规则里补上 IPv6 ICMPv6 放行 |
| DNAT 不生效 | nat 表里的优先级写成了 filter 或漏了 prerouting 链 |
检查 nat 链定义,确认 dstnat 优先级 |
| 转发流量到不了外网 | FORWARD 链策略为 drop,没有放行相关流量 | 在 forward 链加 ct state established,related accept |
| 规则集加载成功但计数器不增长 | 规则顺序不对,先匹配到别的 accept/drop 规则 | 用 nft monitor trace 观察数据包匹配路径 |
| 重启后规则丢了 | nftables.service 未启用,或启用了但配置文件路径不对 | systemctl enable nftables 再重启验证 |
| iptables 和 nftables 规则同时生效 | 两套规则都加载了,互相叠加 | 禁用 iptables 服务,统一由 nftables 管理 |
5.2 数据包去哪了:使用 nft monitor trace
排查 iptables 时代,一条条 iptables -L -v 数计数器效率很低。nftables 有一个实测下来非常趁手的功能:nft monitor trace。
开启方法是在目标链里临时加一条 trace 规则:
bash复制nft add rule inet firewall input meta nftrace set 1
然后另一终端执行:
bash复制nft monitor trace
这时你发一个测试包,trace 会完整打印这个数据包从进入链开始,经过每一条规则的命中、跳转、动作执行全过程。我能看到的不仅是“被哪条规则处理了”,还能看到它在各链之间的行走路径。排查规则顺序冲突问题时,这个功能比任何日志都好用。
排查完记得删掉 trace 规则:
bash复制nft delete rule inet firewall input handle 数字
handle 可以从 nft list ruleset -a 输出里看到。
5.3 iptables-nft 兼容层的作用与边界
有些读者可能会问:我能不能不迁移,直接留着 iptables 命令?从内核角度,你现在的 iptables 命令很可能已经是 iptables-nft 兼容层了,它是能用的。那为什么还建议真正迁移?
兼容层适合用来做灰度过渡,但并不适合作为长期形态。第一,通过兼容层加载的规则只是转换到了 nftables 内核接口,你仍然无法在 nft 脚本里看到这些规则,无法利用 nftables 的集合、字典、动态更新能力。第二,两套用户空间工具同时管理同一内核规则集,容易产生混乱。如果你用 nft list ruleset 去看兼容层加载的规则,输出可能不完全等价于 iptables 规则,排查问题时信息总隔着一层。
我的实际建议是:如果规则量小、又急着封堵漏洞,可以先用 iptables-nft 兼容层把旧规则搬到新内核路径上应急。但随后仍然要规划一次彻底迁移,把兼容层里的规则翻译成原生 nft 语法,统一收编到 nftables 脚本里管理。兼容层解决的是“能不能跑”的问题,原生迁移解决的是“好不好维护”的问题,两回事。
5.4 个人实操中的一些体会
最后分享几个我在多次迁移中沉淀下来的办法。
第一,规则文件一定要纳入版本管理。我在项目里会把 /etc/nftables.conf 里的内容提交到 Git 仓库,每次改动都留 commit。iptables 时代大家都是命令行随手敲,改了什么全靠记忆,出了事只能靠 iptables-save 碰运气恢复。nftables 的脚本文件天然适合版本化,放弃这个优势太可惜了。
第二,呈规则结构时多用 define 和 set。迁移的时候你会明显感觉到,iptables 规则里散落在各处的 IP、端口,在 nftables 里都可以抽成变量和集合。我见过一位同事在 nftables 脚本里维护了三个封禁 IP 的集合,配合 crontab 定期更新恶意 IP,几百行规则管理得明明白白,这种能力在旧框架里实现起来很繁琐。
第三,迁移的窗口期一定要选业务低峰。防火墙验证再充分,也无法穷尽所有业务路径。选择凌晨低峰切换,出了问题有足够时间回滚,也不至于影响用户体验。我一般把窗口控制在 30 分钟以内,前 10 分钟加载验证,中间 10 分钟观察,最后 10 分钟处理异常或完成清理。
第四,别忘了迁移完成后的文档更新。旧应急预案里写的“执行 iptables -L -n 查看规则”这类命令,迁移后全部要改成 nft list ruleset。我接手的系统里,至少有三台服务器的运维文档还停留在 iptables 时代,迁移完成后的文档同步工作如果偷懒,几个月后接手的人会一脸茫然。
这次迁移做完,你再回头看就会发现,nftables 的价值不只是语法更清爽,而是让防火墙规则变成了一份可以阅读、可以测试、可以回滚的工程产物。规则集从“敲进终端的命令”变成“躺在仓库里的代码”,这种转变带来的维护收益,往往比迁移本身还要大。
