从 iptables 迁移到 nftables,这个话题在我身边已经讨论了好几年。早些年大多数人觉得“能用就行,没必要折腾”,但最近两年情况明显变了:新版 Debian、Ubuntu、RHEL/CentOS Stream 默认就用 nftables,很多云镜像里的 iptables 命令其实已经变成了兼容层,规则看着加载成功,实际走的是 nf_tables 内核子系统。等真出了问题再去查,才发现自己写的规则根本没进到你以为的地方。这篇文章就把我这些年做迁移的经验整理一遍,从为什么要迁、语法差异到底在哪,到三种可落地的迁移路径,再给一套完整的实战示例和排坑记录,希望对正在做这件事的朋友有帮助。
我先说一个可能让不少人意外的事实:iptables 和 nftables 根本不是两个“版本”的关系,而是两代架构。iptables 基于老的 Netfilter 框架,用户态工具通过 setsockopt 和 getsockopt 跟内核通信;nftables 换成了全新的 nf_tables 内核子系统,用户态工具 nft 通过 netlink 与内核交互,数据结构和处理模型都重写了。所以“迁移规则”这件事,不是把命令关键字替换一下就完事,而是要理解新的语法框架和规则组织方式,否则很容易迁移出一个“看起来能加载、实际不生效”的僵尸规则集。
1. 为什么要迁移:iptables 的痛点与 nftables 的底气
1.1 iptables 的三座大山:工具割裂、语法分裂、性能瓶颈
iptables 这套体系最大的问题,不是内核里的 Netfilter 框架不行,而是用户态管理工具太旧了。用过的朋友都知道,IPv4 用 iptables,IPv6 用 ip6tables,ARP 用 arptables,网桥过滤用 ebtables,这四套工具语法还不完全一致,管理一套涉及双栈和网桥的规则集,等于同时维护四份不同语法的配置。我见过不少生产服务器的脚本,IPv4 规则写了一大堆,IPv6 的链基本是空的,防火墙等于开了半扇门。
性能上,iptables 也有先天短板。每次新增或删除一条规则,都要把整个规则集从内核导出、用户态修改完再全量灌回去。规则条数多的时候,这个操作明显卡顿。匹配机制上,iptables 后来的扩展模块越加越多,-m state、-m recent、-m hashlimit 这些模块虽然能用,但模块之间的行为并不统一,排查问题时你经常分不清到底是哪个模块在处理这条流量。
还有一点容易被忽略:iptables 的规则没有“命名空间”的概念,所有规则都在固定的 filter、nat、mangle、raw 四张表里,链的类型和挂钩点也被写死了。你想在 PREROUTING 和 INPUT 之间再插一个自定义处理阶段?对不起,iptables 的模型不支持这种灵活组合。
1.2 nftables 重构了什么:统一、原子化、高性能
nftables 把所有地址族统一进了一个框架里。最直观的就是 inet 这个虚拟地址族,可以同时处理 IPv4 和 IPv6 流量,一条规则覆盖双栈,规则数量直接少一半。表也不再是那四张固定表,而是用户自定义的“命名空间”,表里放链,链里放规则,组织方式更像一个树形配置,而不是一堆平铺的规则行。
nftables 的规则集支持原子替换。你可以把整套规则写在一个配置文件里,经过语法检查和加载,要么全部生效,要么全部不生效,不会出现 iptables 那种“flush 到一半断电,防火墙全空”的尴尬时刻。性能方面,nftables 原生支持集合、字典、区间匹配,同样一条“放行内网网段”的规则,传统 iptables 可能要写十几条,nftables 用一个集合就搞定了,规则条数少,内核匹配次数也少。
还有一个对运维比较友好的点:nftables 的规则是真正的“数据”,可以通过 nft list ruleset 完整导出、重新加载,不像 iptables 那样只能借助 iptables-save 生成一种近似表达。
1.3 什么时候必须迁、什么时候可以缓一缓
我的建议是分三种情况看。第一种,新装服务器,直接上 nftables,不要犹豫。第二种,现有服务器跑得好好的、规则不多、也没有双栈和复杂 NAT 需求,那可以先在测试环境做一次迁移演练,不必急于生产切换。第三种,只要你的业务涉及 Docker 默认网络、K8s 的 kube-proxy、或者依赖 iptables 的第三方管理工具,迁移前必须先把共存方案想清楚,这部分我会在后面的常见问题里专门展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迁移之前必须搞懂的核心差异
2.1 表、链、规则的“同名不同义”
这是绝大多数迁移事故的根源。很多人以为 iptables 的 filter 表直接对应 nftables 的同名表,链 INPUT 也直接对应 nftables 的链 INPUT,然后照着改语法就完事。实际上两者的模型差别非常大。
iptables 的表是内核预设的,filter、nat、mangle、raw 各有各的用途。链分两类:内置链(PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING)有固定的挂钩时机,自定义链只能被内置链 jump 过去,不能独立挂钩。优先级和挂钩顺序是隐含的,用户改不了。
nftables 完全不同。表只是规则的容器,链需要自己声明类型和挂钩点,比如 type filter hook input priority 0。而且 priority 是可以自定义的数值,你可以在同一个 hook 上挂多条链,优先级小的先执行。这意味着你可以按自己的需要把处理流程拆得更细,而不只是依赖那五条内置链。
迁移时最容易踩的坑是:在 nftables 里新建了一条名为 INPUT 的链,但忘了声明 type filter hook input priority 0,结果这条链没有任何挂钩,规则静默失效。这种错误 nft -c 语法检查根本看不出来,因为语法是对的,只是语义上空转。
2.2 语法差异速览:从“选项堆叠”到“结构化表达式”
iptables 的语法风格是“命令 + 选项”,一条规则可以写得很长,全靠短选项堆叠。比如禁止某个 IP 访问本机 22 端口:
bash复制iptables -A INPUT -s 203.0.113.5 -p tcp --dport 22 -j DROP
nftables 的语法更接近自然语言,同样一条规则:
bash复制nft add rule inet filter input ip saddr 203.0.113.5 tcp dport 22 drop
拆开看就清楚了:ip saddr 表示匹配 IPv4 源地址,tcp dport 表示匹配 TCP 目标端口,最后直接给动作 drop。它没有 -A、-p、-j 这种“暗号”,关键字和语义是显式的。
我整理了一份常用转换对照表,迁移时可以直接参考:
| iptables 规则 | nftables 等价规则 |
|---|---|
| -A INPUT -s 10.0.0.0/8 -j ACCEPT | ip saddr 10.0.0.0/8 accept |
| -A INPUT -p tcp --dport 22 -j ACCEPT | tcp dport 22 accept |
| -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT | ct state established,related accept |
| -A OUTPUT -p udp --dport 53 -j ACCEPT | udp dport 53 accept |
| -A FORWARD -i eth0 -o eth1 -j ACCEPT | iifname "eth0" oifname "eth1" accept |
| -A INPUT -m mac --mac-source AA:BB:CC:DD:EE:FF -j DROP | ether saddr AA:BB:CC:DD:EE:FF drop |
需要注意,这不是机械替换。iptables 的 -m state 和 nftables 的 ct state 虽然语义接近,但 nftables 还支持 ct state new,established,related,invalid 这样的多值匹配,更灵活。mac 匹配方面,iptables 的 --mac-source 是链路层过滤,nftables 用 ether saddr,作用等价,但 nftables 还能匹配 daddr。
2.3 计数器不是默认开启的
这个坑我几乎每次迁移都会遇到。iptables 里每条规则自动带包计数和字节计数,你 iptables -L -v 就能看到每条规则匹配了多少流量。nftables 里“匹配”和“计数”是分离的,规则默认不会计数,必须显式加 counter 关键字。
bash复制nft add rule inet filter input tcp dport 22 counter accept
加了 counter 之后,用 nft list ruleset 才能看到 packets、bytes 数据。建议在迁移时给需要监控的规则统一加上 counter,否则上线之后你会发现根本没法判断规则有没有真的被命中,排障效率会大打折扣。
3. 迁移实操:三条路径与一套完整案例
3.1 路径一:iptables-translate 自动转换单条规则
如果你只是想把几条现成的 iptables 规则快速转成 nftables 语法,用 iptables-translate 就够了。这个工具通常随 iptables-nft 兼容层一起安装,在装有 nftables 的发行版上一般都有。
bash复制iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
输出是:
bash复制nft add rule ip filter INPUT tcp dport 22 counter accept
注意看细节:翻译结果里的表名是 ip filter,而不是 inet filter。iptables-translate 默认逐条翻译,不会帮你合并 IPv4/IPv6,也不会帮你创建表链结构。它给出的是一条“可参考”的规则,不是一份“可上线”的配置。
3.2 路径二:iptables-restore-translate 批量转换规则文件
如果你的服务器有现成的规则集,先用 iptables-save 导出,再用 iptables-restore-translate 批量翻译,效率比逐条手工快得多。
bash复制iptables-save > iptables.rules
iptables-restore-translate -f iptables.rules > nftables.rules
生成的 nftables.rules 内容长这样:
bash复制# Translated by iptables-restore-translate v1.8.7 on 某年某月某日
add table ip filter
add chain ip filter INPUT { type filter hook input priority 0; policy accept; }
add rule ip filter INPUT tcp dport 22 counter accept
这套脚本不能直接 nft -f 加载。翻译工具生成的是“规则片段”,表链结构虽然带了,但用的是 ip 地址族,而且没有处理链上原有的 INPUT、FORWARD、OUTPUT 策略之外的东西。你需要加上表定义、策略定义、以及 inet 地址族的合并处理,手工整理后才能用。
我实际操作中的流程是:先自动翻译生成初稿,然后打开文件,把所有 ip 地址族改成 inet,检查每条链的 type 和 hook 是否对应,再把自定义链按 nftables 的新语法重建。这一步完成之后,配置才是真正可用的。
3.3 路径三:手工重写,适合复杂场景的最终解
自动转换工具能覆盖 80% 的常规规则,但遇到这几类场景,手工重写反而更高效、也更可靠。第一是双栈规则,iptables 里 IPv4 和 IPv6 各写一套,nftables 用 inet 族可以合并成一套,这种优化自动工具做不到。第二是集合和字典,把一组 IP、一组端口放进命名集合,规则简洁且新增条目不用改规则本身。第三是自定义链的处理逻辑,nftables 的命名链可以有自己的策略和优先级,能按需求重新设计结构。
手工重写还有一个好处:你会被迫重新审视每一条规则是否仍然必要。我见过太多服务器上堆着三五年前加的规则,早就没人记得是干嘛的了。迁移是一次绝佳的规则清理机会。
3.4 实战案例:一台 Web 服务器从 iptables 到 nftables 的完整迁移
下面用一个典型的小型 Web 服务器来演示完整过程。这台服务器跑 Nginx 和 SSH,做简单的端口转发,要求:放行回环、SSH、HTTP/HTTPS,放行内部网段访问数据库端口 3306,允许已建立的连接回包,默认拒绝其它入站流量。
迁移前的 iptables 规则大致如下:
bash复制iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -s 192.168.1.0/24 -p tcp --dport 3306 -j ACCEPT
iptables -A INPUT -j DROP
iptables -A FORWARD -i eth0 -o eth1 -p tcp --dport 8080 -j ACCEPT
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -j DROP
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 192.168.2.10:8080
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
先用 iptables-restore-translate 转初稿,然后手工整理成下面的 nftables 配置:
bash复制# /etc/nftables.conf
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iifname "lo" accept
tcp dport { 22, 80, 443 } accept
ip saddr 192.168.1.0/24 tcp dport 3306 accept
counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
iifname "eth0" oifname "eth1" tcp dport 8080 accept
counter drop
}
}
table inet nat {
chain prerouting {
type nat hook prerouting priority -100;
iifname "eth0" tcp dport 80 dnat to 192.168.2.10:8080
}
chain postrouting {
type nat hook postrouting priority 100;
oifname "eth1" masquerade
}
}
对比一下:iptables 分了 filter 和 nat 两张表、五条链,零零散散写了十几条规则;nftables 一个 inet filter 表加一个 inet nat 表就能承载同样的逻辑,而且 PROUTING/INPUT/POSTROUTING 的 hook 优先级一目了然。加载配置:
bash复制nft -f /etc/nftables.conf
验证配置:
bash复制nft list ruleset
如果你用的是 systemd 管理,还可以设置开机自动加载:
bash复制systemctl enable nftables
systemctl restart nftables
3.5 加载命令与调试工具速查
迁移过程中我最常敲的命令就这几个,整理成速查表方便大家对照:
| 操作 | 命令 |
|---|---|
| 查看当前全部规则 | nft list ruleset |
| 查看指定表 | nft list table inet filter |
| 语法检查配置文件 | nft -c -f /etc/nftables.conf |
| 加载配置文件 | nft -f /etc/nftables.conf |
| 清空全部规则 | nft flush ruleset |
| 清空指定表 | nft flush table inet filter |
| 跟踪某个链的流量 | nft monitor trace |
特别注意 nft -c 这个选项。它只检查语法,不检查语义。一条链忘了写 type filter hook input priority 0,语法检查照样通过,但规则不会执行。所以上线前我建议至少做两轮验证:第一轮 nft -c 查语法,第二轮 nft list ruleset 人工核对表链结构。
4. 常见问题与排查技巧实录
4.1 规则加载成功,但流量没有被拦截
这是迁移后最常见的现象。首先确认链到底有没有挂钩。执行 nft list chain inet filter input,看看输出里有没有 type filter hook input priority 0。如果没有,说明链没挂到网络栈上,规则写了等于白写。
其次是优先级问题。nftables 的 priority 是数值,如果跟系统里其它 nftables 服务(比如 firewalld)冲突,可能出现“你的规则先跑了,但被后面的规则放行了”的情况。排查时把相关服务的配置也拉出来看,别只看自己的文件。
还有一个小陷阱是接口名。iptables 时代 -i eth0 匹配的是进入接口,nftables 的 iifname 也一样,但要注意 iifname 匹配的是“进入的数据包”,如果是本机发出的流量,得用 oifname。我遇到过有人把 iifname 写在 output 链里,规则永远不匹配,查了半天才发现是接口方向写错。
4.2 自动转换后的规则“能加载但不生效”到底差在哪
自动转换工具最大的问题是它严格按 iptables 的语义翻译,不会帮你优化。举几个我实测过的例子。iptables 里 -m state --state NEW 会被翻译成 ct state new,看起来没问题,但 nftables 里更常见的写法是 ct state established,related accept 放在链首,然后只对 new 状态做进一步过滤。直接照搬 iptables 的规则顺序,可能把 established 的包也丢进后面的 drop 链。
另一个常见问题是 DNAT 和 MASQUERADE 在 nftables 里需要区分链的 hook 类型。iptables 的 nat 表规则放在 PREROUTING 和 POSTROUTING,而 nftables 里 nat 类型的链必须声明 type nat hook prerouting 和 type nat hook postrouting。翻译工具会生成,但如果你手工整理时把 nat 链的 type 写成了 filter,DNAT 就不会生效,也没有任何报错。
遇到这种情况,我的排查思路是:先 nft monitor trace 观察包走了哪些链、命中哪些规则,再做针对性修正。记着,trace 要配合 meta nftrace set 1 才能看到具体规则轨迹:
bash复制nft add rule inet filter input meta nftrace set 1
4.3 和 Docker/K8s 共存:别把网络搞垮
Docker 默认网络模式用的是 iptables 兼容层来管理端口映射和网络隔离。它默认加载的是 iptables 命令,在大多数发行版上,这个 iptables 命令已经指向了 nf_tables 的兼容模块。所以理论上 Docker 和 nftables 可以共存,但如果你在 nftables 里写了全局的 drop 策略,Docker 的流量可能被你的规则拦掉。
我的建议是:如果业务依赖 Docker,迁移初期先不要让 nftables 接管全部流量,保留 iptables 兼容层的默认行为,等确认 Docker 网络正常后再收紧策略。如果你用的是 Docker 的 iptables 管理功能,还要注意 nft flush ruleset 会不会把 Docker 创建的链一起清掉。nftables 的 flush ruleset 会把所有表的规则清空,Docker 依赖的那些链也在其中,所以千万别在生产环境随意执行这条命令。
4.4 回滚预案:怎么快速回到 iptables
迁移前一定要保存好 iptables 规则备份:
bash复制iptables-save > /root/iptables-backup.rules
ip6tables-save > /root/ip6tables-backup.rules
如果迁移后发现 nftables 方案有问题,需要回滚,执行:
bash复制systemctl stop nftables
nft flush ruleset
iptables-restore < /root/iptables-backup.rules
ip6tables-restore < /root/ip6tables-backup.rules
4.5 问题排查速查表
| 现象 | 最可能的原因 | 排查命令 |
|---|---|---|
| 规则能加载但流量不受控 | 链没挂钩 / 优先级不对 | nft list chain inet filter input |
| nft -f 报语法错误 | inet 地址族 / 集合语法写错 | 检查 nft -c -f 报错行 |
| IPv6 流量全部放行 | 用了 ip 地址族而不是 inet | nft list ruleset 查看族 |
| 计数器全是 0 | 规则没加 counter 关键字 | 重新加 counter |
| Docker 网络异常 | 全局 drop 策略拦截了 Docker 流量 | 临时加 accept 规则验证 |
最后再分享一个小技巧
迁移完整理规则文件时,nftables 支持 include 语句,把规则拆分成多个文件管理。我通常分成三个文件:base.conf 放默认策略和回环、ssh、状态匹配这些基础规则,services.conf 放按端口开放的规则,custom.conf 放临时的封禁和跳转规则。这样日常加规则只用改对应文件,然后 nft -f /etc/nftables.conf 重新加载即可,不用去翻那几百行的总配置。这一点 iptables 时代确实没有,用了就回不去了。
