Skip to main content

iptables 迁移到 nftables 实战:规则翻译、Docker 兼容与灰度切换

September 18, 2026
面向仍在维护 iptables 规则的 Linux 服务器,讲解当前包过滤后端判断、规则自动翻译与人工复核、nftables 配置编写、Docker 与 fail2ban 兼容、远程失联防护,以及可回滚的灰度切换步骤。
iptables 迁移到 nftables 实战:规则翻译、Docker 兼容与灰度切换

更新日期:2026-09-18

nftables 早已是 Linux 内核默认的包过滤框架,主流发行版的新版本默认把 iptables 命令指向 iptables-nft(即 iptables 兼容前端 + nftables 内核后端)。也就是说,很多服务器其实已经在跑 nftables,只是运维人员还在用 iptables 的语法在操作它。与此同时,Docker、firewalld、fail2ban、CrowdSec 这些组件又各自用自己的方式往这套框架里写规则,一旦有人手工执行 flush ruleset,容器网络和自动化封禁会同时崩掉。

真正让运维出事的从来不是 .conf 的语法,而是在一台只能通过 SSH 访问的远程主机上改变包过滤状态:规则写错一行、policy drop 先于放行规则生效、或者把 Docker 自己管理的链清空,结果就是会话断开、容器端口不再发布、面板登不上,只能去云控制台开 VNC。这篇文章讨论的是「怎么改」和「怎么保证改错了还能回来」,而不是又一份语法速查表。

适用范围:systemd 体系的 Linux 服务器(Debian/Ubuntu、RHEL/CentOS/Alma/Rocky、openSUSE 等),主机上跑 Docker 或容器运行时,装了 firewalld、ufw、fail2ban 或 CrowdSec 中的一种或多种,并且存在手工维护的 iptables 规则。不适用于只跑 KVM/裸机路由且完全不使用容器与自动化封禁工具的场景,也不适用于 Kubernetes 节点上的 kube-proxy 规则迁移(那属于另一套生命周期管理)。

文中的版本相关细节(内核能力、发行版默认后端、Docker 与 fail2ban 的行为差异)请以官方文档和机器实际输出为准;本文只给出可核实的通用方法论。若你还没在目标机上装好容器运行时,可以先参考站内 Alibaba Cloud Linux 4 安装 Docker:Moby、Docker CE 与 Compose 完整教程用国内软件源加速 Docker-CE 安装 把 Docker CE 环境与 iptables-nft 后端准备好,再回到本文做防火墙迁移。

适用范围与不适用场景

迁移前先把边界划清楚,能省掉大量返工:

  • 适用:单机或少量服务器的包过滤改造;需要同时保留 Docker 端口发布;需要保留 fail2ban/CrowdSec 的自动封禁;希望未来用统一的 /etc/nftables.conf 管理规则。
  • 适用:从 iptables-legacy 切到 iptables-nft 或原生 nftables 语法,并且能安排维护窗口。
  • 不适用:主机完全没有带外访问手段(没有云控制台 VNC/串口、没有 IPMI、没有第二个网络入口),这种情况建议先补齐带外通道再动手。
  • 不适用:规则由配置管理工具(Ansible/Salt/Terraform provider)全量下发且不允许手工干预的集群,应该改工具模板而不是改机器。
  • 不适用:想在不停机的前提下一次性重写全部 NAT 规则的高流量网关,这类场景需要专门的旁路与灰度设计。

先看结论

  1. 先执行 iptables -Viptables -S | head,确认当前到底是 legacy 还是 nf_tables 后端;输出里带 nf_tables 说明内核里跑的已经是 nftables,你要做的是「语法与归属权的整理」,不是「框架切换」。
  2. 在改动之前,无条件保存两份备份:iptables-save > /root/iptables-backup.rulesnft list ruleset > /root/nft-backup.nft,并确认备份文件非空、可读、路径在重启后依然存在。
  3. 永远不要在跑着 Docker 或 firewalld 的主机上执行 flush ruleset,它会连同 Docker 的 ip nat/ip filter 表和 firewalld 的表一起清空,容器端口映射当场失效。
  4. iptables-translate / iptables-restore-translate 做初稿,但必须逐条人工复核:顺序、自定义链、--recent-m string、复杂 NAT 都是翻译器的薄弱点。
  5. 新规则先以 policy accept + 只记录日志的方式上线观察一个时段,再切成 policy drop,这样丢包会先出现在日志里而不是直接掐断业务。
  6. 迁移按端口/服务分阶段做:先迁 80/443 这类可被外部快速验证的服务,SSH(22)放在最后,并且迁移期间始终保留一个已经建立好的第二个 SSH 会话。
  7. 必须准备自动回滚:用 atsystemd-run --on-active 或 cron 在看门狗脚本里安排「N 分钟后如果依然可达就取消,否则恢复旧规则」。
  8. 最后用 nft -c -f /etc/nftables.conf 做语法预检,systemctl enable --now nftables 固化配置,并安排一次重启验证,确认规则在重启后自动生效。

先确认你现在跑在哪个后端

这一步是整个迁移的地基。判断错了,后面所有操作都会作用在错误的框架上。

# 1. 版本与后端
iptables -V
# 输出末尾会标注后端类型:legacy 表示老的 x_tables 后端,
# nf_tables 表示底层已经是 nftables,iptables 只是兼容前端

# 2. 发行版默认指向哪个实现
update-alternatives --display iptables 2>/dev/null || true
ls -l /usr/sbin/iptables /usr/sbin/ip6tables /etc/alternatives/iptables 2>/dev/null

# 3. 内核里加载了哪套框架
lsmod | grep -E 'nf_tables|ip_tables|iptable_(filter|nat)|nft_' | sort

# 4. 当前规则来自谁
iptables -S | head -n 40
iptables -t nat -S 2>/dev/null | head -n 40

判据与异常分支:

  • iptables -Vnf_tables:内核用 nftables,iptables 只是兼容前端。此时 iptables-save 的输出是从 nftables 反向渲染出来的近似结果,不保证无损,尤其自定义链注释与部分 match 会丢失或改写。
  • iptables -Vlegacy:真正的老后端。切到 nftables 属于框架迁移,风险更高,必须保留 legacy 规则文件以便 iptables-legacy-restore 回滚。
  • lsmod 里同时出现 ip_tablesnf_tables:说明两套框架都在被使用,常见于 Docker 与手工 legacy 规则混用。此时要特别注意:iptables-nftiptables-legacy两套独立规则集,用 iptables-nft -S 看不到 legacy 的规则,反之亦然。
  • 切换默认实现:
# 在 Debian/Ubuntu 上切换默认前端(会同时影响 iptables/ip6tables/arptables/ebtables)
update-alternatives --set iptables  /usr/sbin/iptables-nft
update-alternatives --set ip6tables /usr/sbin/ip6tables-nft
# 切回 legacy
# update-alternatives --set iptables /usr/sbin/iptables-legacy

关键认知:「iptables 命令能用」不等于「你在用 legacy」。绝大多数新装机器的 iptables 就是 nft 前端,此时手工写 iptables 规则和写 nftables 规则最终落到同一套内核对象里,两者会互相覆盖、互相干扰。反过来,如果你在 legacy 环境里用 nft list ruleset 看不到任何东西,那也是正常现象。

RHEL 系发行版默认由 firewalld 管理规则,nft list ruleset 里看到的大量 firewalld 前缀规则就是它的产物。openSUSE/SUSE 的 nftables 配置路径与 systemd 单元名称可能不同,以官方文档为准。

概念对照表

理解下面的映射关系,比记住语法更重要。iptables 是「表 + 链 + 规则」的三段式,nftables 把这一切统一进「族 + 表 + 链 + 规则 + 表达式 + 判决」,并且用集合与映射替代了 ipset 和大量的重复规则。

iptables 概念nftables 对应物说明与迁移要点
-t filter/nat/mangle/raw/securityfamily(ip / ip6 / inet / arp / bridge / netdeviptables 的表是固定功能分区;nftables 的 family 决定处理的协议栈,inet 可同时处理 IPv4 与 IPv6。功能分区由链的 hook 与优先级决定
表名固定为 filter/nat表名自定义,table inet mf8_filter迁移时建议用独立表名承载自建规则,避免与 Docker/firewalld 的表混在一起
内置链 INPUT/FORWARD/OUTPUT/PREROUTING/POSTROUTING基链 chain input { type filter hook input priority 0; policy drop; }基链需要显式声明 typehookpriority;链名可自定义大小写不敏感
用户自定义链 -N MYCHAIN常规链 chain my_chain { }jump/goto 引用;nftables 里没有「链被引用过就不能删」的限制,但顺序完全由你自己负责
单条规则 -A INPUT -p tcp --dport 22 -j ACCEPTtcp dport 22 accept一条规则内可以顺序写多个表达式,天然支持多条件而无需多个 -m
match(-m conntrack --ctstate-m limit-m multiportexpression(ct statelimit rate、集合与匿名集合)multiport 用集合替代:tcp dport { 80, 443 }ipset 用命名集合替代
target -j ACCEPTverdict accept直接对应
target -j DROPverdict drop直接对应;丢弃后不回包
target -j REJECTverdict reject可带类型:reject with tcp resetreject with icmp type port-unreachable
target -j LOG --log-prefix "X: "语句 log prefix "X: " level warnlog 是语句而非判决,规则要继续匹配需在后面写 drop/accept;支持 limit rate 抑制刷屏
target -j RETURNverdict return返回调用链继续匹配(jump 回来),与 iptables 语义一致
-j SNAT --to-source 1.2.3.4snat to 1.2.3.4postrouting 链使用
-j MASQUERADEmasquerade常用于动态出口地址
-j DNAT --to-destination ip:portdnat to ip:portprerouting(或 output)链使用
-m set --match-set NAME srcip saddr @NAME命名集合,支持 intervaltimeoutdynamic 等能力
ipsetset / mapmap 可以把键直接映射为判决或值,减少规则条数

一个容易被忽略的差别:iptables 在 nat 表的 PREROUTING 里,只有第一条匹配的 DNAT 生效,之后继续走 filter;nftables 的 NAT 链同样有「只取第一个 NAT 判决」的语义,但如果你在 inet 族里同时写 IPv4 和 IPv6 的 NAT,需要确认内核版本对 inet 族 NAT 的支持情况,遇到报错就拆成 ipip6 两张表,以官方文档为准。

自动翻译与人工复核

翻译器能把大部分常见规则转成等价或近似等价的 nftables 规则,它是加速器,不是替代品。

# 单条规则翻译
iptables-translate -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
iptables-translate -A INPUT -s 203.0.113.0/24 -j DROP
iptables-translate -t nat -A PREROUTING -p tcp --dport 8080 -j REDIRECT --to-ports 80

# 整份规则集翻译(iptables-save 的输出为输入)
iptables-save > /root/iptables-current.rules
iptables-restore-translate -f /root/iptables-current.rules > /root/translated.nft

# IPv6 同理
ip6tables-save > /root/ip6tables-current.rules
ip6tables-restore-translate -f /root/ip6tables-current.rules > /root/translated6.nft

翻译结果的形式随版本变化:较老的版本输出 nft add rule ip filter INPUT ... 这种命令行形式,较新的版本输出可直接加载的表格脚本。以你机器上的实际输出为准,不要照抄文章里的示例输出。

典型翻译示例(输入 → 输出含义):

iptables 输入翻译结果(语义)
-A INPUT -i lo -j ACCEPTiifname "lo" accept
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTct state established,related accept
-A INPUT -m multiport -p tcp --dports 80,443 -j ACCEPTtcp dport { 80, 443 } accept
-A INPUT -m limit --limit 3/min --limit-burst 5 -j LOG --log-prefix "DROP: "limit rate 3/minute burst 5 packets log prefix "DROP: "

必须人工复核的五类问题:

  1. 顺序。iptables 里 -I 插入到链首、-A 追加到链尾,翻译器一般按 iptables-save 的保存顺序输出,语义上通常一致,但一旦你手工调整过顺序或存在多个自定义链的跳转嵌套,务必把翻译后的规则与 iptables -L -n --line-numbers 逐行对照。
  2. 自定义链。链名会被保留,但跳转关系、RETURN 的返回位置需要确认;有些链在翻译后会变成内联(inlined)表达式,链名消失,后续再想引用就会找不到。
  3. 注释-m comment --comment "xxx" 会转成 comment "xxx",但长度受限且不支持任意字符;非 ASCII 注释在部分版本上会被截断或转义。
  4. NATMASQUERADEREDIRECT、带端口范围的 DNAT-m addrtype 参与的 NAT 规则是失败高发区,翻译器可能输出带 # Could not translate 注释的行,或直接中断整份文件。遇到这类输出不要放过,逐条手工重写。
  5. 扩展模块--recent-m string-m geoip-m connlimit-m owner 等在 nftables 里有的没有直接等价物:--recent 通常用带 dynamic,timeout 标志的集合或 meter 实现,connlimitct count over N 实现,string 需要用原始载荷匹配(@th 偏移匹配)实现。翻译器不认识的部分必须重写。

复核方法:把翻译稿先写成 /root/translated.nft,用 nft -c -f /root/translated.nft 做 dry-run 检查。注意 nft -c 只检查语法与对象引用,不会告诉你语义是否等价,也不会覆盖生产状态。

手工编写 nftables 配置

自动翻译只能给出初稿。生产环境的配置建议手工整理成一份可读、可注释、可复现的文件。下面是一份完整示例,面向同时跑 Docker 的主机,因此不使用 flush ruleset,只管理自建表。

#!/usr/sbin/nft -f
# /etc/nftables.conf —— 只管理自建表 mf8_filter,避免误删 Docker/firewalld 的表
# 语法检查:nft -c -f /etc/nftables.conf
# 首次执行前请先备份:nft list ruleset > /root/nft-backup.nft

# 幂等初始化:表不存在则创建,存在则清空其中的链与规则
add table inet mf8_filter
flush table inet mf8_filter

define WAN_IF   = "eth0"
define SSH_PORT = 22

table inet mf8_filter {
    # ---- 命名集合:管理网段、需要封禁的来源 ----
    set mgmt_nets {
        type ipv4_addr
        flags interval
        elements = { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 203.0.113.0/24 }
    }

    # 动态集合:SSH 暴力破解来源,10 分钟自动过期
    set ssh_flood {
        type ipv4_addr
        flags dynamic, timeout
        timeout 10m
    }

    # 计数对象:便于用 nft list counters 观察命中
    counter ssh_hits {}
    counter drop_hits {}

    # ---- 输入链:本机流量,默认丢弃 ----
    chain input {
        type filter hook input priority filter; policy drop;

        iifname "lo" accept comment "放行回环"

        ct state established,related accept comment "已建立连接与关联流量"
        ct state invalid counter drop comment "无效状态直接丢"

        # ICMP / ICMPv6:不要盲目全丢,否则 PMTU 发现与邻居发现会出问题
        ip protocol icmp icmp type { destination-unreachable, time-exceeded, parameter-problem } accept
        ip protocol icmp icmp type echo-request limit rate 10/second accept
        meta l4proto ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, echo-request } accept

        # SSH:允许管理网段直连,其余来源做速率限制
        ip saddr @mgmt_nets tcp dport $SSH_PORT ct state new counter name ssh_hits accept
        tcp dport $SSH_PORT ct state new update @ssh_flood { ip saddr limit rate over 6/minute } drop
        tcp dport $SSH_PORT ct state new accept

        # 业务端口
        tcp dport { 80, 443 } ct state new accept comment "HTTP/HTTPS"

        # 丢包前记日志,并做速率限制避免刷爆磁盘
        limit rate 10/minute log prefix "nft-input-drop: " level warn
        counter name drop_hits drop
    }

    # ---- 转发链:容器与网关流量 ----
    # 与 Docker 的 ip filter 表处于同一 hook,Docker 自己会 accept 容器流量。
    # 这里保持 accept 策略,容器策略统一交给 DOCKER-USER,避免相互抢判决。
    chain forward {
        type filter hook forward priority filter; policy accept;

        ct state established,related accept
        iifname "docker0" accept
        iifname "br-*" accept

        # 如确实需要在转发路径上做限制,只对明确的坏流量下手,并保留日志
        ct state invalid counter drop
    }

    # ---- 输出链 ----
    # 生产环境通常保持 accept:出方向全丢极易自断 DNS/NTP/镜像源/日志上报。
    chain output {
        type filter hook output priority filter; policy accept;
    }

    # ---- 端口映射示例:把 8080 转到本机 80 ----
    # 位于 ip 族,与 Docker 的 nat 表共存;如内核不支持 inet 族 NAT 请拆表
    chain prerouting_redirect {
        type nat hook prerouting priority dstnat; policy accept;
        tcp dport 8080 redirect to :80 comment "本机端口重定向"
    }
}

几个设计说明:

  • 优先级数值priority filter 等于 0,priority dstnat 等于 -100,priority mangle 等于 -150,priority raw 等于 -300。Docker 的链同样注册在 filter hook 上,若你的自建链在转发路径上设成 policy drop,容器流量可能被你的链先丢掉;实践上更稳妥的做法是转发链保持 accept,容器侧的准入统一用 DOCKER-USER 做。同 hook 同优先级时,链的执行顺序由注册顺序决定,不要依赖它。
  • 集合与区间flags interval 让集合支持 CIDR 与区间(10.0.0.0-10.0.255.255),没有这个标志就只能放单个地址。区间集合在元素很多时会被自动合并,规则匹配效率远高于写几十条 -s 规则。
  • 动态集合flags dynamic, timeout 配合 update @set { ip saddr limit rate over N/minute } 实现自动封禁与自动解封,是替代 -m recent 的标准做法;不同版本对 update 语句的写法支持略有差异,写完后用 nft -c -f 验证。
  • 计数器。用 counter name xxx 命名计数器,或直接在内联位置写 counter。命名计数器可以直接 nft list counter inet mf8_filter ssh_hits 读取,适合做监控采集。
  • 映射(map)。需要按端口分派到不同链时,用 map 把键映射为判决,比一长串 jump 更清晰:map svc_map { type inet_service : verdict; elements = { 80 : jump web_chain, 443 : jump web_chain, 25 : jump mail_chain } },然后在链内用 tcp dport vmap @svc_map 引用。
  • flush table 还是 flush ruleset。在纯防火墙主机(没有 Docker、没有 firewalld)上,文件开头写 flush ruleset 是常见做法;在有其他管理者的主机上绝对不要这么做。

配置写完后不要直接加载,先做 dry-run:nft -c -f /etc/nftables.conf。它只做解析与对象引用检查,不会改动运行中的规则。

Docker 与容器兼容

Docker 的包过滤由它自己管理,理解它的链结构是迁移不翻车的关键。

Docker 启动时会做几件事:创建 DOCKERDOCKER-ISOLATION-STAGE-1/2DOCKER-USER 等链;在 filter 表的 FORWARD 链里插入 -j DOCKER-USER-j DOCKER-ISOLATION-STAGE-1 的跳转;在 nat 表的 PREROUTING/OUTPUT/POSTROUTING 里插入端口映射与 SNAT 规则;为每个自定义网络的桥接口增加放行规则。使用 iptables-nft 时,这些规则存放在 nftables 的 ip filterip natip6 filter 等表里,nft list ruleset 可以直接看到,但这些表归 Docker 所有,不要手工编辑

关于 DOCKER-USER 链的行为要点:

  • Docker 在 FORWARD 链靠前的位置插入跳转到 DOCKER-USER 的规则,因此进入容器的转发流量会先经过 DOCKER-USER,再进入 Docker 自己的 DOCKER 链。要限制容器可被谁访问,DOCKER-USER 是官方指定的入口。
  • DOCKER-USER 链里通常有一条 -j RETURN 结尾,表示「本链不处理,交回 Docker 继续」。如果你在链尾追加 DROP,一定要放在 RETURN 之前,否则永远不会命中。
  • 顺序决定一切:如果你自己往 FORWARD 链追加规则,而 Docker 的 -j DOCKER-USER 跳转早已插在更前面,你的规则在容器流量上根本不会被评估;反过来,如果你的规则插在 Docker 跳转之前,就可能把容器流量提前丢掉。
  • 绝对不要 flush DOCKER-USER 之外的 Docker 链,也不要 iptables -Fiptables -t nat -F:这会把端口映射规则清空,正在运行的容器立刻失去外部访问,docker restart 之前不会自动恢复。
  • Docker 守护进程重启(或 systemctl restart docker)时会重新下发规则,重写 DOCKER-USER 之外的所有链。因此不要把需要长期存在的规则写进 Docker 管理的链,它们会被覆盖。

推荐的共存方式:自建规则放在自己的表/链里,容器准入放在 DOCKER-USER,两者职责分离。往 DOCKER-USER 加规则时用 iptables-nft 前端即可(它会写进 nftables),也可以迁移到写等价的 nftables 规则,但选择一种后保持统一。

迁移后必须实测容器端口发布:

# 1. 看 Docker 的链是否还在
iptables -t nat -S DOCKER 2>/dev/null | head
nft list ruleset | grep -E 'DOCKER|br-|docker0' | head

# 2. 看端口映射是否登记
docker ps
docker port CONTAINER_NAME_OR_ID

# 3. 从外部实测(在另一台机器上执行,不要在容器宿主机上自测)
curl -sSI http://SERVER_PUBLIC_IP:MAPPED_PORT/ | head -n 1

# 4. 看连接跟踪是否建立了对应的 DNAT 会话
conntrack -L 2>/dev/null | grep -E 'dport=(80|443|8080)' | head

# 5. 看是否有丢包计数在增长
nft list counter inet mf8_filter drop_hits 2>/dev/null

异常分支:如果 nft list ruleset 里完全看不到 Docker 的表,说明 Docker 用的是 legacy 后端而你切到了 nft 前端,或者反过来。用 iptables -Vdocker info | grep -i iptables 交叉确认,并统一到 iptables-nft。如果容器端口映射存在但外部不通,优先检查转发链的 policy、net.ipv4.ip_forward 以及云厂商安全组,而不是先怀疑 nftables 语法。容器逃逸类风险的加固思路可参考站内 Docker Copy Fail 漏洞修复指南:CVE-2026-31431 内核升级与容器加固 一文中的隔离与升级策略。

与 firewalld/ufw/fail2ban/CrowdSec 共存

多管理器共存是规则「莫名其妙消失」的最常见原因。核心原则只有一条:同一时刻只能有一个组件拥有规则集的写权限

  • firewalld:它自己直接操作 nftables,nft list ruleset 里带 firewalld 前缀的规则就是它的。若 firewalld 在运行,请不要在 /etc/nftables.confflush ruleset,也不要用 nft 手工增删它管理的链。要么把规则交给 firewalld(用 firewall-cmd --permanent --add-rich-rule 等接口),要么 systemctl disable --now firewalld 之后由 nftables 独占,两者不要同时启用。
  • ufw:底层是 iptables(新版本是 iptables-nft),会创建 ufw-before-inputufw-user-input 等链。启用 ufw 时手工 nft 规则能加进去,但 ufw reload 会重排链的顺序,可能导致你的规则位置变化。要么全程用 ufw,要么先 ufw disable 再迁移。
  • fail2ban:默认 banaction 是 iptables-multiport,它会通过 iptables-nft 前端在 filter 表里创建以 f2b- 为前缀、后接 jail 名的链,并在 INPUT 链里插入跳转。这意味着两件事:一是你的自建链必须让新建连接能走到 fail2ban 的跳转,否则封禁不生效(如果你的 policy drop 在 fail2ban 跳转之前就把包丢了,f2b 永远看不到这些包);二是 fail2ban 有 nftables 原生的 banaction(例如 nftables-multiportnftables-allports),它直接写 nft 集合,性能更好,也更容易和你的集合式规则共存。迁移到 nftables 后,建议把 banaction 切到 nftables 系列,并用 fail2ban-client status 加 jail 名与 nft list set 交叉验证封禁是否真的落到了集合里。具体的 banaction 名称与可用参数以 fail2ban 官方文档和你机器上的 action.d/ 目录为准。
  • CrowdSec:由 agent(检测)与 bouncer(执行封禁)两部分组成。bouncer 有多种实现,常见的是 iptables、nftables、firewalld 以及云厂商 WAF/CDN 类型的 remediation component。迁移时应选择与当前框架一致的 bouncer:nftables bouncer 通常把封禁 IP 写入专用集合或专用表,与自建规则互不干扰;iptables bouncer 则会继续通过 iptables-nft 写规则。一个主机上不要同时启用多个本地 bouncer,否则同一 IP 会被两套机制重复处理,且解封时容易各自为政。

排查「规则被谁改掉了」的通用手法:

# 看规则前后差异(改动前先存一份)
nft list ruleset > /tmp/ruleset-before.nft
# ... 执行操作 ...
nft list ruleset > /tmp/ruleset-after.nft
diff -u /tmp/ruleset-before.nft /tmp/ruleset-after.nft | head -n 60

# 看谁在调用 nft / iptables
systemctl status firewalld ufw fail2ban nftables 2>/dev/null | grep -E 'Active|Loaded'
journalctl -u fail2ban -n 30 --no-pager

灰度切换步骤

核心思想是「叠加而不是替换」:新规则先以旁路方式加载并观察,确认无害后再逐步接管,最后才移除旧规则。整个过程允许随时中断并回到原状。

  1. 备份与固化现状。把当前状态完整落盘,并确认文件可读。
  2. 只加载不收权。把新表以 policy accept 加载,所有本该 DROP 的规则先改成 log,观察一个时段(至少覆盖一个业务高峰)。
  3. add-before-delete。新增放行规则时,先加新规则、验证通过,再删除对应的旧规则,不要反过来。
  4. 按端口/服务分批接管。一次只把一组端口从旧规则迁到新规则,比如先 80/443,再数据库端口,最后 22。
  5. 最后收权。全部端口迁移完成、日志中确认没有误杀之后,才把 policy accept 改成 policy drop
  6. 全程保有第二条会话。见下一节。
# 步骤 1:备份现状(务必确认文件非空)
mkdir -p /root/fw-backup
iptables-save            > /root/fw-backup/iptables-$(date +%F).rules
ip6tables-save           > /root/fw-backup/ip6tables-$(date +%F).rules
nft list ruleset         > /root/fw-backup/nft-$(date +%F).nft
wc -c /root/fw-backup/*

# 步骤 2:dry-run 后以 log-only 方式加载新表
nft -c -f /etc/nftables.conf
nft -f /etc/nftables.conf
nft list table inet mf8_filter | head -n 40

# 观察日志:确认没有本应放行的流量被记录
journalctl -k -f | grep -i 'nft-input-drop'

# 步骤 4:单个端口验证通过后再收权,例如确认 443 正常后
# 把 input 链策略改为 drop(先确认 SSH 与业务端口都已显式放行)
nft chain inet mf8_filter input '{ policy drop; }'
nft list chain inet mf8_filter input

关于时间窗:选择业务低峰期,避开自动发布、备份、批量任务的时间点。改动前后各留 30 分钟观察窗口,期间不要做其他变更,避免多个变量互相干扰。切换前先在测试机或快照环境完整走一遍流程。

远程失联防护

这是整篇文章里最不能省的一节。规则改错导致的失联通常表现为:SSH 新连接超时、已建立的连接也被 conntrack 规则误杀、或者容器里的跳板机不可达。

  • 保持已建立的会话。改动前先开一个 tmuxscreen 会话并保持连接,同时另开一个全新的 SSH 连接并停留在登录后状态。已建立的连接和新建连接走的是不同的路径,两个都留着才能区分「新连接被拦」和「链路整体不通」。
  • 安排自动回滚。用 atsystemd-run 在改动前设定一个定时回滚任务,确认新规则可用后再取消它。这是最有效的一招。
# 方式一:at
# 前提:atd 正在运行(systemctl enable --now atd)
echo 'nft -f /root/fw-backup/nft-rollback.nft' | at now + 15 minutes
atq          # 查看待执行任务
atrm JOB_ID   # 确认无误后取消

# 方式二:systemd-run(不依赖 atd)
systemd-run --on-active=15min --unit=fw-rollback /usr/sbin/nft -f /root/fw-backup/nft-rollback.nft
systemctl list-timers fw-rollback* --no-pager
systemctl stop fw-rollback.timer 2>/dev/null   # 确认无误后取消

# 方式三:cron 看门狗脚本,网络异常时自动回滚
cat > /usr/local/sbin/fw-watchdog.sh <<'EOF'
#!/bin/bash
# 每隔几分钟检查一次外网连通性,连续失败则回滚防火墙并留痕
CANARY="198.51.100.1"
FAILS=/var/tmp/fw-fail-count

ping -c 2 -W 3 "$CANARY" >/dev/null 2>&1 && { echo 0 > "$FAILS"; exit 0; }

n=$(( $(cat "$FAILS" 2>/dev/null || echo 0) + 1 ))
echo "$n" > "$FAILS"
if [ "$n" -ge 3 ]; then
    logger -t fw-watchdog "canary unreachable $n times, rolling back firewall"
    /usr/sbin/nft -f /root/fw-backup/nft-rollback.nft
    echo 0 > "$FAILS"
fi
EOF
chmod 700 /usr/local/sbin/fw-watchdog.sh
  • 准备带外通道。云主机的 VNC/串口控制台、IPMI/iDRAC、机房 KVM、独立的 4G/带外管理网,至少要有一个可用。动手前先在控制台里登录一次,确认凭据有效——很多人是在失联之后才发现控制台密码早就过期了。
  • 写好恢复命令并复制到控制台可粘贴的位置。失联时没有时间现场想命令,把下面这行提前准备好:
  • 降低被自己误杀的概率。规则里对 SSH 的来源网段先做无条件放行,再做限速;不要在 output 链上设 drop;不要丢弃 ct state established,related;不要全丢 ICMPv6。
# 预置的一键恢复命令(在带外控制台粘贴执行)
nft -f /root/fw-backup/nft-rollback.nft && echo "nft rollback done"
# 若 nftables 也被改坏,改用 iptables 备份恢复(会写入当前内核后端)
iptables-restore < /root/fw-backup/iptables-2026-09-18.rules && echo "iptables rollback done"

验证与持久化

验证要分层做:语法层、加载层、命中层、持久层。任何一层跳过都可能把问题留到重启之后。

# 1. 语法与引用检查(不改变运行状态)
nft -c -f /etc/nftables.conf && echo "syntax OK"

# 2. 查看当前完整规则集与自建表
nft list ruleset | head -n 80
nft list table inet mf8_filter

# 3. 计数器:确认关键规则真的在命中
nft list counters table inet mf8_filter
nft list set inet mf8_filter ssh_flood      # 看动态封禁集合里有没有元素

# 4. 追踪单个包的判决路径(先在规则里打开 nftrace,追踪完记得关掉)
nft add rule inet mf8_filter input meta nftrace set 1
nft monitor trace
# 另一个终端触发:curl -sS http://SERVER_IP/ 或 ssh 测试
# 停止:Ctrl-C 结束 monitor,然后删除 trace 规则
nft -a list chain inet mf8_filter input | grep nftrace
nft delete rule inet mf8_filter input handle HANDLE_ID

# 5. 持久化:enable 后确认 unit 内容确实读取 /etc/nftables.conf
systemctl enable --now nftables
systemctl status nftables --no-pager
systemctl cat nftables | head -n 20

# 6. 重启验证(安排在维护窗口,重启前再次确认带外通道可用)
systemctl reboot
# 回来后
nft list ruleset | head -n 20
systemctl is-enabled nftables

判据与异常分支:

  • nft -c -f 报错但文件看起来没问题:优先检查 flush table 语句与链名大小写,nftables 的链名大小写不敏感但表名在某些版本上区分;再检查 define 变量是否在作用域内被引用。
  • 规则加载了但计数器一直是 0:说明流量没有匹配到这条规则,通常是链/hook/priority 写错,或者上游有另一条更早的规则先判决了。
  • 重启后规则消失:nftables.service 没启用,或者发行版读取的配置文件不是 /etc/nftables.conf(部分发行版使用 /etc/sysconfig/nftables.conf),以官方文档为准。容器类主机还要注意 Docker 服务与 nftables 服务的启动顺序,Docker 会在自己的启动流程里重写它管理的链。
  • 重启后 Docker 端口不通但规则看起来正常:先确认 Docker 服务正常启动,再确认自建转发链没有抢在 Docker 之前丢弃容器流量。

回滚方案与检查清单

回滚要分两种情况准备,因为两种框架的恢复方式不同。

# 情况 A:你的自建 nft 表出问题,想回到迁移前 —— 直接恢复备份
nft -f /root/fw-backup/nft-rollback.nft

# 只删掉自建表、保留 Docker 与 firewalld 的表(更精细的做法)
nft delete table inet mf8_filter

# 情况 B:需要恢复 iptables 规则集(会写入当前 iptables 默认后端)
iptables-restore  < /root/fw-backup/iptables-2026-09-18.rules
ip6tables-restore < /root/fw-backup/ip6tables-2026-09-18.rules

# 若当前默认后端已被切到 nft,而你想彻底回到 legacy 行为:
# update-alternatives --set iptables /usr/sbin/iptables-legacy
# iptables-legacy-restore < /root/fw-backup/iptables-2026-09-18.rules

# 回滚后立刻验证
iptables -S | head -n 20
nft list ruleset | head -n 20
ss -lntp | head
docker ps

注意一个容易被忽略的坑:如果主机原本是 iptables-legacy,备份文件必须用 iptables-legacy-save 保存,恢复也必须用 iptables-legacy-restore;用 iptables-restore(nft 前端)恢复 legacy 备份,规则会写进 nftables,而不是恢复成原来的 legacy 状态。

迁移前检查清单:

  • 已确认当前后端(iptables -V 输出为 legacy 还是 nf_tables),并记录下来
  • 已确认主机上还有哪些组件在写规则(firewalld / ufw / fail2ban / CrowdSec / Docker / 配置管理工具)
  • 已保存 iptables、ip6tables、nftables 三份备份,并确认文件非空且路径重启后仍然存在
  • 已写入自动回滚任务(atsystemd-run 或 cron 看门狗),并确认任务真的处于待执行状态
  • 带外通道(云控制台 VNC/串口、IPMI、机房 KVM)已实测可登录
  • 一键恢复命令已写好,并复制到控制台可粘贴的位置
  • 已保留一个既有 SSH 会话和一个新建的 SSH 会话,两个都处于登录状态
  • 新配置已通过 nft -c -f 语法预检
  • 新规则以 log-only / policy accept 方式观察过至少一个业务高峰
  • SSH 放行规则位于限速规则之前,且管理网段直连不受限速影响
  • ICMP 与 ICMPv6 的必要类型(PMTU、邻居发现)已放行
  • output 链未设置 policy drop
  • Docker 相关链未被 flush,DOCKER-USER 规则顺序正确
  • fail2ban / CrowdSec 的封禁在迁移后实测仍然生效
  • 已验证容器端口映射、数据库端口、管理端口均从外部可达
  • 所有变更都已写入 /etc/nftables.confsystemctl enable nftables
  • 已完成一次重启验证,确认规则与容器网络在重启后自动恢复
  • 观察窗口内无新增误杀日志,回滚任务已取消并记录

完成以上流程后,建议把 /etc/nftables.conf 纳入版本控制(备份到内网 Git 或对象存储),并在文件头部写清负责人、变更原因与回滚方式。防火墙规则是基础设施里最需要「可追溯」的部分之一,一次没有记录的改动,会在半年后变成谁也不敢碰的黑盒。对于同时维护多台主机的场景,可以先把单机流程跑通,再把配置模板化下发,这比一开始就上配置管理工具更容易定位问题。

官方资料与继续阅读

外部官方资料:

站内相关文章: