iptables 迁移到 nftables 实录:set 黑名单、inet 表与远程不被锁在门外的三招

为什么还要在 2026 年聊 nftables

iptables 已经进入维护末期,Debian 10 之后的发行版把 nftables 作为默认后端,CentOS 8 / Rocky / Alma 上 firewalld 背后跑的也是 nft。很多人迟迟不迁移,原因往往不是不想,而是"规则看着能跑,但不知道对不对"。我这台跑着七八个站的机器上,iptables 规则攒了四十多条,中间夹着几条五年前加的、注释都看不懂的条目。趁一次换机房的机会,我把整套防火墙按 nftables 重写了一遍。这篇记录下真实的迁移步骤、踩到的坑,以及迁移完成后怎么验证"没有把自己关在门外"。

先搞清楚两者不是一一对应

最容易出事的地方,是把 nftables 当成"iptables 换个语法"。它们的设计模型不一样:iptables 是四张表五条链的固定骨架,nftables 是一棵树——table 可以有任意多个,里面放 chain,chain 里放 rule,rule 可以 jump 到别的 chain。关键在于:nftables 的 base chain 必须绑定 hook 和优先级,没有绑定 hook 的 chain 只是一堆不会执行的规则。

第二个差异是 set 和 map。iptables 时代要封几百个 IP,就得写几百条规则,或者上 ipset。nftables 内置匿名和具名 set,一条规则就能匹配几千个地址。这一点在封恶意爬虫时价值极大——后面会讲。

第三个差异是原子性。nftables 加载规则时是整个事务一次性生效,不会出现 iptables-restore 一半失败、机器处于半开状态的尴尬窗口。改防火墙最怕的就是这个窗口,尤其是走 SSH 远程操作的时候。

迁移第一步:先把旧规则导出来看清楚

iptables-save > /root/iptables-$(date +%F).bak
iptables -S > /root/iptables-rules-$(date +%F).txt
ip6tables-save > /root/ip6tables-$(date +%F).bak

导完之后不要急着翻译。先做一件更重要的事:给每条规则标注"为什么存在"。我的做法是把 iptables -S 的输出打印出来,逐条问三个问题——这条规则放行了什么、是哪次故障加的、删了会怎样。四十多条规则里,最后确认还有必要保留的只有十九条。剩下的是历史遗留:某次临时开的一个测试端口、一条重复的 80 放行、一条早就不用的 FTP 被动端口段。

这一步不做,迁移就只是把垃圾从旧箱子搬进新箱子。防火墙规则和代码一样,只有持续删除才不会被熵吞掉。

表结构怎么拆

我的拆法是按"关注点"分表,而不是按 iptables 的 filter/nat/mangle 分:

table inet filter {
    # 入口流量:本机被访问
    chain input {
        type filter hook input priority filter; policy drop;
    }
    # 转发:本机不做路由,直接全丢
    chain forward {
        type filter hook forward priority filter; policy drop;
    }
    # 出口流量:本机主动发起
    chain output {
        type filter hook output priority filter; policy accept;
    }
}

table inet nat {
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
    }
}

注意 table inet:一个表同时处理 IPv4 和 IPv6,不用再维护两套规则。这是 nftables 相对 iptables 最省事的改进之一。很多人的"IPv6 没有防火墙"问题,本质就是 ip6tables 是空的,改用 inet 表之后这个漏洞自然消失。

input 链的默认策略一定要设成 drop,然后显式放行需要的端口(白名单模型)。如果沿用 iptables 那种默认 accept 再逐条封,那么任何一次漏配都是敞开口子。

入口链的完整写法

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
    set blackhole {
        type ipv4_addr
        flags timeout
        timeout 7d
    }

    chain input {
        type filter hook input priority filter; policy drop;

        # 1. 已建立连接直接放行,否则回包会被丢掉
        ct state established,related accept
        # 2. 无效包直接丢,不要回复
        ct state invalid drop
        # 3. 回环
        iif lo accept
        # 4. ICMP:ping 与路径 MTU 发现必须留
        ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } accept
        ip6 nexthdr icmpv6 icmpv6 type { echo-request, nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert, packet-too-big } accept
        # 5. 黑名单
        ip saddr @blackhole drop
        # 6. 业务端口
        tcp dport { 22, 80, 443 } accept
        # 7. 其余全部记录后丢弃
        limit rate 5/minute log prefix "nft-drop: " level warn
        counter drop
    }
}

几个必须解释的点:

第一条一定要是 ct state established,related accept。nftables 的 input 链只会看到"进来的包",出站的 SYN-ACK 是 output 链的事,回来的 ACK 走的是 input 链。漏了这条,SSH 能连上但敲不了命令,症状是"能登录但卡住"。

ICMP 不能全封。封了 destination-unreachable 和 time-exceeded,路径 MTU 发现就废了,表现为"小文件正常、大文件卡死",这种问题排查起来极痛苦。IPv6 那边更严格:邻居发现协议(nd-neighbor-solicit 等)被封,整台机器的 IPv6 会直接不通。

最后那条 limit rate 5/minute log 是限速日志。不加 limit 的话,一次扫描就能把磁盘写满——日志本身变成了攻击面。

counter 关键字很重要。加了之后可以用 nft -a list ruleset 看到每条规则命中了多少个包。没有计数器,你永远不知道某条规则是否真的在用。

黑洞 set:用一条规则封掉整批 IP

被扫描或者被恶意爬虫猛刷的时候,临时封 IP 是常规操作。iptables 时代的做法是 iptables -I INPUT -s IP -j DROP,封多了规则表就臃肿。nftables 的做法是往 set 里加元素:

# 封一个 IP,7 天后自动过期(因为 set 定义了 timeout 7d)
nft add element inet filter blackhole { 45.148.10.0/24 }

# 查看当前黑名单与剩余时间
nft list set inet filter blackhole

# 解封
nft delete element inet filter blackhole { 45.148.10.0/24 }

配合一个 cron 脚本,就能实现"自动封禁 + 自动解封",不用再装 fail2ban 的封禁层(filter 层仍然有用,但执行层可以简化成写 set)。脚本核心逻辑是:从 nginx access log 里按分钟聚合 4xx/5xx 计数,超过阈值的 IP 直接 nft add element。

要注意 set 的 timeout 是在加入时计算的,不是每次命中刷新。nft list set 里显示的是倒计时,看到 expires 5d23h 说明还剩这么久。

持久化与开机加载

nftables 不保存规则,重启即清空。标准做法是把规则写进 /etc/nftables.conf(Debian/Ubuntu 的路径)或 /etc/sysconfig/nftables.conf(RHEL 系),然后:

systemctl enable nftables
systemctl start nftables
nft list ruleset

如果系统里还留着 iptables 服务,务必先禁用,否则两套规则会打架——iptables 和 nftables 在同一个 netfilter 上操作,混用时规则顺序难以预期。systemctl disable --now iptables ip6tables 之后确认 nft list ruleset 里没有 table ip filter 之类的残留。

远程改防火墙,怎么才不会被关在门外

这是我这次迁移最在意的部分。经验是三条:

第一,不要用一条命令改完就走。正确顺序是:先写好配置文件,跑 nft -c -f /etc/nftables.conf 做语法检查(-c 是 check 模式,只校验不加载),确认无误再 nft -f。

第二,改之前开一个"保护进程":systemd-run --on-active=300 nft flush ruleset。它会在 5 分钟后无条件清空所有规则,恢复成全放行。改规则的时候如果 SSH 断了,等 5 分钟就能重新连回去,然后修配置。确认能正常登录后再把这个定时任务取消掉。这个技巧救过我两次。

第三,验证要用新连接,不能用当前会话。当前 SSH 会话是 established 状态,第一条规则就放行了,所以你在会话里 ping、curl 都正常,并不代表新连接能进来。必须另开一个终端 ssh 一次,或者用外部工具(比如手机上的网络工具)访问 80/443。我把这条写进了自己的运维检查单里。

从 iptables 自动翻译的坑

iptables-translate 和 ip6tables-translate 可以逐条转换语法,能省不少时间,但有几个已知的失真点:

一是 -m state --state 会被转成 ct state,语义基本一致但 set 名大小写要自己核对。二是 -m conntrack --ctstate 与 -m state 在旧规则里经常混用,翻译后可能生成重复的 ct 匹配,虽然不影响结果但会让规则变长。三是 --reject-with 的各种子类型在 nft 里对应 reject with,参数写法不同,需要手工调整。四是 ipset 相关的规则无法自动翻译,必须手工改成 nftables 的 set 语法。

所以我的建议是:把 translate 的输出当作草稿,不要直接上线。真正上线前,规则我全部手工重排了一遍,同时把注释补全。

日常怎么读 nftables 的规则

排查问题时最常用的三条命令:

# 带 handle 号列出所有规则,handle 用于插入/删除
nft -a list ruleset

# 只看某个表的 input 链
nft list chain inet filter input

# 实时跟踪被丢弃的包(排查"为什么连不上"最有效)
nft monitor trace

nft monitor trace 需要先给规则加 meta nftrace set 1,否则没有输出。用法是临时在 input 链最前面加一条 tcp dport 443 meta nftrace set 1,然后 nft monitor trace,用外部机器访问一次,就能看到这个包经过了哪些规则、在哪条被 drop 或 accept。这比对着规则表猜快十倍。

收尾:规则要有版本控制

我把 /etc/nftables.conf 放进了 git 仓库,每次改动都提交一条带原因的 commit。理由很简单:防火墙规则是故障时最需要回滚的东西,而回滚的前提是你知道上一次能用的版本长什么样。规则文件不长,几十行,但它是这台机器最外层的一道门。门锁换了要留记录。

小结

nftables 迁移本身不难,难的是借机把历史规则清理干净、把验证流程固化下来。三件事记牢:input 链策略默认 drop、第一条放行 established、远程改动先挂一个自动 flush 的定时任务。做到这三点,迁移过程基本不会出事。至于 iptables,留一份导出的备份在 /root 下就好,半年后确认没用到再删。

Last modification:November 14th, 2026 at 09:25 pm

Leave a Comment