更新日期: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 规则的高流量网关,这类场景需要专门的旁路与灰度设计。
先看结论
- 先执行 iptables -V 和 iptables -S | head,确认当前到底是 legacy 还是 nf_tables 后端;输出里带 nf_tables 说明内核里跑的已经是 nftables,你要做的是「语法与归属权的整理」,不是「框架切换」。
- 在改动之前,无条件保存两份备份:iptables-save > /root/iptables-backup.rules 和 nft list ruleset > /root/nft-backup.nft,并确认备份文件非空、可读、路径在重启后依然存在。
- 永远不要在跑着 Docker 或 firewalld 的主机上执行 flush ruleset,它会连同 Docker 的 ip nat/ip filter 表和 firewalld 的表一起清空,容器端口映射当场失效。
- 用 iptables-translate / iptables-restore-translate 做初稿,但必须逐条人工复核:顺序、自定义链、--recent、-m string、复杂 NAT 都是翻译器的薄弱点。
- 新规则先以 policy accept + 只记录日志的方式上线观察一个时段,再切成 policy drop,这样丢包会先出现在日志里而不是直接掐断业务。
- 迁移按端口/服务分阶段做:先迁 80/443 这类可被外部快速验证的服务,SSH(22)放在最后,并且迁移期间始终保留一个已经建立好的第二个 SSH 会话。
- 必须准备自动回滚:用 at、systemd-run --on-active 或 cron 在看门狗脚本里安排「N 分钟后如果依然可达就取消,否则恢复旧规则」。
- 最后用 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 -V 带 nf_tables:内核用 nftables,iptables 只是兼容前端。此时 iptables-save 的输出是从 nftables 反向渲染出来的近似结果,不保证无损,尤其自定义链注释与部分 match 会丢失或改写。
- iptables -V 带 legacy:真正的老后端。切到 nftables 属于框架迁移,风险更高,必须保留 legacy 规则文件以便 iptables-legacy-restore 回滚。
- lsmod 里同时出现 ip_tables 和 nf_tables:说明两套框架都在被使用,常见于 Docker 与手工 legacy 规则混用。此时要特别注意:iptables-nft 和 iptables-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/security | family(ip / ip6 / inet / arp / bridge / netdev) | iptables 的表是固定功能分区;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; } | 基链需要显式声明 type、hook、priority;链名可自定义大小写不敏感 |
| 用户自定义链 -N MYCHAIN | 常规链 chain my_chain { } | 用 jump/goto 引用;nftables 里没有「链被引用过就不能删」的限制,但顺序完全由你自己负责 |
| 单条规则 -A INPUT -p tcp --dport 22 -j ACCEPT | tcp dport 22 accept | 一条规则内可以顺序写多个表达式,天然支持多条件而无需多个 -m |
| match(-m conntrack --ctstate、-m limit、-m multiport) | expression(ct state、limit rate、集合与匿名集合) | multiport 用集合替代:tcp dport { 80, 443 };ipset 用命名集合替代 |
| target -j ACCEPT | verdict accept | 直接对应 |
| target -j DROP | verdict drop | 直接对应;丢弃后不回包 |
| target -j REJECT | verdict reject | 可带类型:reject with tcp reset、reject with icmp type port-unreachable |
| target -j LOG --log-prefix "X: " | 语句 log prefix "X: " level warn | log 是语句而非判决,规则要继续匹配需在后面写 drop/accept;支持 limit rate 抑制刷屏 |
| target -j RETURN | verdict return | 返回调用链继续匹配(jump 回来),与 iptables 语义一致 |
| -j SNAT --to-source 1.2.3.4 | snat to 1.2.3.4 | 在 postrouting 链使用 |
| -j MASQUERADE | masquerade | 常用于动态出口地址 |
| -j DNAT --to-destination ip:port | dnat to ip:port | 在 prerouting(或 output)链使用 |
| -m set --match-set NAME src | ip saddr @NAME | 命名集合,支持 interval、timeout、dynamic 等能力 |
| ipset | set / map | map 可以把键直接映射为判决或值,减少规则条数 |
一个容易被忽略的差别:iptables 在 nat 表的 PREROUTING 里,只有第一条匹配的 DNAT 生效,之后继续走 filter;nftables 的 NAT 链同样有「只取第一个 NAT 判决」的语义,但如果你在 inet 族里同时写 IPv4 和 IPv6 的 NAT,需要确认内核版本对 inet 族 NAT 的支持情况,遇到报错就拆成 ip 与 ip6 两张表,以官方文档为准。
自动翻译与人工复核
翻译器能把大部分常见规则转成等价或近似等价的 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 ACCEPT | iifname "lo" accept |
| -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT | ct state established,related accept |
| -A INPUT -m multiport -p tcp --dports 80,443 -j ACCEPT | tcp 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: " |
必须人工复核的五类问题:
- 顺序。iptables 里 -I 插入到链首、-A 追加到链尾,翻译器一般按 iptables-save 的保存顺序输出,语义上通常一致,但一旦你手工调整过顺序或存在多个自定义链的跳转嵌套,务必把翻译后的规则与 iptables -L -n --line-numbers 逐行对照。
- 自定义链。链名会被保留,但跳转关系、RETURN 的返回位置需要确认;有些链在翻译后会变成内联(inlined)表达式,链名消失,后续再想引用就会找不到。
- 注释。-m comment --comment "xxx" 会转成 comment "xxx",但长度受限且不支持任意字符;非 ASCII 注释在部分版本上会被截断或转义。
- NAT。MASQUERADE、REDIRECT、带端口范围的 DNAT、-m addrtype 参与的 NAT 规则是失败高发区,翻译器可能输出带 # Could not translate 注释的行,或直接中断整份文件。遇到这类输出不要放过,逐条手工重写。
- 扩展模块。--recent、-m string、-m geoip、-m connlimit、-m owner 等在 nftables 里有的没有直接等价物:--recent 通常用带 dynamic,timeout 标志的集合或 meter 实现,connlimit 用 ct 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 启动时会做几件事:创建 DOCKER、DOCKER-ISOLATION-STAGE-1/2、DOCKER-USER 等链;在 filter 表的 FORWARD 链里插入 -j DOCKER-USER 和 -j DOCKER-ISOLATION-STAGE-1 的跳转;在 nat 表的 PREROUTING/OUTPUT/POSTROUTING 里插入端口映射与 SNAT 规则;为每个自定义网络的桥接口增加放行规则。使用 iptables-nft 时,这些规则存放在 nftables 的 ip filter、ip nat、ip6 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 -F 或 iptables -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 -V 与 docker 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.conf 里 flush ruleset,也不要用 nft 手工增删它管理的链。要么把规则交给 firewalld(用 firewall-cmd --permanent --add-rich-rule 等接口),要么 systemctl disable --now firewalld 之后由 nftables 独占,两者不要同时启用。
- ufw:底层是 iptables(新版本是 iptables-nft),会创建 ufw-before-input、ufw-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-multiport、nftables-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
灰度切换步骤
核心思想是「叠加而不是替换」:新规则先以旁路方式加载并观察,确认无害后再逐步接管,最后才移除旧规则。整个过程允许随时中断并回到原状。
- 备份与固化现状。把当前状态完整落盘,并确认文件可读。
- 只加载不收权。把新表以 policy accept 加载,所有本该 DROP 的规则先改成 log,观察一个时段(至少覆盖一个业务高峰)。
- add-before-delete。新增放行规则时,先加新规则、验证通过,再删除对应的旧规则,不要反过来。
- 按端口/服务分批接管。一次只把一组端口从旧规则迁到新规则,比如先 80/443,再数据库端口,最后 22。
- 最后收权。全部端口迁移完成、日志中确认没有误杀之后,才把 policy accept 改成 policy drop。
- 全程保有第二条会话。见下一节。
# 步骤 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 规则误杀、或者容器里的跳板机不可达。
- 保持已建立的会话。改动前先开一个 tmux 或 screen 会话并保持连接,同时另开一个全新的 SSH 连接并停留在登录后状态。已建立的连接和新建连接走的是不同的路径,两个都留着才能区分「新连接被拦」和「链路整体不通」。
- 安排自动回滚。用 at 或 systemd-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 三份备份,并确认文件非空且路径重启后仍然存在
- 已写入自动回滚任务(at 或 systemd-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.conf 并 systemctl enable nftables
- 已完成一次重启验证,确认规则与容器网络在重启后自动恢复
- 观察窗口内无新增误杀日志,回滚任务已取消并记录
完成以上流程后,建议把 /etc/nftables.conf 纳入版本控制(备份到内网 Git 或对象存储),并在文件头部写清负责人、变更原因与回滚方式。防火墙规则是基础设施里最需要「可追溯」的部分之一,一次没有记录的改动,会在半年后变成谁也不敢碰的黑盒。对于同时维护多台主机的场景,可以先把单机流程跑通,再把配置模板化下发,这比一开始就上配置管理工具更容易定位问题。
官方资料与继续阅读
外部官方资料:
- nftables 官方 Wiki(语法、集合、映射、迁移指南):https://wiki.nftables.org/
- netfilter 项目 nftables 主页(工具与源码):https://netfilter.org/projects/nftables/
- Docker 官方文档:数据包过滤与防火墙(DOCKER-USER 与规则顺序):https://docs.docker.com/network/packet-filtering-firewalls/
- Debian Wiki 的 nftables 页面(发行版默认后端与持久化配置):https://wiki.debian.org/nftables
- 发行版手册页:man nft、man iptables-translate、man iptables-restore-translate、man nftables.conf(随软件包提供,版本差异以本机手册为准)
站内相关文章:
- Alibaba Cloud Linux 4 安装 Docker:Moby、Docker CE 与 Compose 完整教程:在 Alibaba Cloud Linux 3 上安装 Docker CE,确认容器运行时的 iptables 后端
- Docker Copy Fail 漏洞修复指南:CVE-2026-31431 内核升级与容器加固:容器隔离与运行时升级的加固思路
- 用国内软件源加速 Docker-CE 安装:通用发行版安装 Docker CE 的步骤与常见坑
- Linux 如何确认 BBR 已经开启:sysctl、ss、tc 三种验证方式:确认内核网络参数与拥塞控制是否生效,常与防火墙变更一起排查


