Chips

iptables、nftables 与 legacy 表:一次封禁规则「消失」的排查

· 约 7 分钟 · 2405 字

现代发行版里 iptables 命令指向的其实是 iptables-nft,规则被翻译进 nftables 表;fail2ban 的链则要到第一次封禁才创建。常有人说规则不见了,多半是观测后端选错,或链本就还没被创建。

现象

同一台主机上,几句话给出互相矛盾的说法。先是 fail2ban 自己的状态:

fail2ban-client status sshd
|- Filter
|  |- Currently failed: 0
|  `- Journal matches: _SYSTEMD_UNIT=sshd.service
`- Actions
   |- Currently banned: 0
   |- Total banned: 1

jail 本身一切正常,累计封禁计数是 1——这个机制确实工作过。但到内核里去找对应的链:

iptables -S INPUT | grep f2b        # 无输出:INPUT 上没有 f2b-* 的跳转
nft list chain ip filter f2b-sshd
# Error: No such file or directory

而在链确实存在(封禁生效中)的时候,按名字查同样可能得到另外两种说法:

iptables-legacy -S f2b-sshd
# iptables: No chain/target/match by that name.
iptables -S f2b-sshd         # 当这条链是 nft 原生创建的时候
# iptables v1.8.7 (nf_tables): chain `f2b-sshd' in table `filter' is
# incompatible, use 'nft' tool.

「不存在」「用错工具」「工具不认识」——三种说法里只有一种是真故障。要分开它们,得先知道 iptables 这个命令背后站着谁。

背景:iptables 只是一个入口名

/usr/sbin/iptables 在现代发行版上是一串 alternatives 软链:

/usr/sbin/iptables        -> /etc/alternatives/iptables -> /usr/sbin/iptables-nft
/usr/sbin/iptables-nft    -> xtables-nft-multi
/usr/sbin/iptables-legacy -> xtables-legacy-multi
iptables -V          # iptables v1.8.7 (nf_tables)
iptables-legacy -V   # iptables v1.8.7 (legacy)

两个命令的版本号一模一样,差别只在括号里。iptables-nft 不碰旧的内核 iptables 模块,而是把你写的规则翻译成 nftables 表达式,写进 nft 的表里。所以真正的权威视图是 nft 那边:

nft list tables
# table ip filter
# table ip6 filter
# table ip nat
# table ip raw

iptables -S 读写的正是这些表。与此同时,legacy 后端可能也被加载了,于是系统里存在第二套彼此独立的表:

iptables-legacy -S
# -P INPUT ACCEPT
# -P FORWARD ACCEPT
# -P OUTPUT ACCEPT

只有三条默认链、零条规则——表存在但为空。这是 iptable_filter 模块被加载的痕迹,不参与任何过滤。两组表并存,是所有混乱的根:每条 iptables 命令都得先问清楚它操作的是哪一套。

三种「看不到」,只有一种是故障

情况 观测命令 典型输出 性质
后端选错 iptables-legacy -S f2b-sshd iptables: No chain/target/match by that name. 命令用错了
对象真不存在 nft list chain ip filter f2b-sshd Error: No such file or directory 尚未创建,见下一节
存在但形态不符 iptables -S <base chain> chain ... is incompatible, use 'nft' tool. 观测工具读不了

判断属于哪一种,靠的不是重试同一条命令,而是换一个正交的观测手段:nft 原生命令。它不经过 xtables 的翻译层,看到的就是内核里的真实对象。三条命令各自回答一个不同的问题:

命令 回答的问题
iptables -V 我手上的工具是哪个后端
iptables-legacy -S 旧后端那套表里有什么(本机:什么都没有)
nft list ruleset 内核里实际生效的是什么

把「工具报错」和「真实状态」当成两件事,这一步比记住任何一条命令都重要。工具只报它自己看到的世界:后端选错时它看不到链,形态不认识时它又说链非法——两种都可能在链明明正常工作时报出「有问题」的字样。

首行那句 Warning 不是线索

只要系统里存在另一套表,每次 iptables -S 都会打印:

# Warning: iptables-legacy tables present, use iptables-legacy to see them

它只说明「有另一套表存在」,不表示你的规则跑到 legacy 里去了。跟着它去翻 iptables-legacy -S,永远只会看到那三条空的默认链。把它当噪声,别当病因。

incompatible 报错:链在,只是不该用这个工具读

iptables v1.8.7 (nf_tables): chain `f2b-sshd' in table `filter' is incompatible, use 'nft' tool.

iptables-nft 只认识自己写进去的链,那些链带一层兼容元数据。nft 侧的链分两类,实测区别很干净:

nft add chain ip filter probe-reg                    # 普通自定义链
iptables -S probe-reg
# -N probe-reg                                       ← 能读

nft add chain ip filter probe-base '{ type filter hook input priority 10 ; }'
iptables -S probe-base
# iptables v1.8.7 (nf_tables): chain `probe-base' in table `filter' is
# incompatible, use 'nft' tool.
iptables -L probe-base
# 同样的报错

nft delete chain ip filter probe-base probe-reg      # 清理

差别在是不是 base chain(带 hook。普通链的模型两边一致,前端能翻译;base chain 有自己的 hook 与 priority 语义,xtables 的模型表达不了,前端就干脆拒读。所以这条消息的信息量是:对象存在,而且大概率是被 nft 原生命令创建或改写过的。它既不是规则丢失,也不是权限问题。

遇到它就换工具,-a 会带上 handle:

nft -a list chains
nft list chain ip filter f2b-sshd
nft list ruleset | grep -n f2b

handle 不只是给眼睛看的——当前端拒绝操作某个链时,nft delete rule ip filter <链> handle <N> 是绕开它、直接删掉那条规则的退路。

怎么判断一条规则是谁写进去的

nft 输出里,凡是由 iptables 前端写入的规则都自动带一个 counter

counter packets 82654 bytes 58210977 jump ufw-before-input

nft 原生写的规则只有在显式写 counter 时才带。这是个粗糙但实用的指纹——它来自前端的实现选择,不是文档承诺的语义,换版本可能变,所以只能当线索。再配合链名的来源(ufw-*f2b-*DOCKER-* 各出自一个具体工具)和 handle 号,基本能还原出每条规则是谁写的。

备份与恢复最容易写错地方

iptables-saveiptables-legacy-save 是两个命令,各导一套表。审计时只跑前者,拿到的只是 nft 那套;万一 legacy 那边真有人手工加过规则,这部分永远不出现在备份里。

反向的坑更隐蔽:从 legacy 导出,再用 iptables-restore(nft 后端)恢复。两边的规则语法兼容,恢复过程没有任何报错,规则却全部落进了另一套表。看起来「已恢复」,实际不生效——和开头的「规则看不见」是同一类现象,只是方向相反。所以改完规则后的验收标准只有一句:换一个工具再看一眼,写入用一个,复核用另一个。

第二个原因:actionstart 是懒执行的

即便后端选对了,开机后看不到 f2b-sshd可能本来就是正常的。看 /etc/fail2ban/action.d/iptables-multiport.conf 里的定义:

# Notes.:  command executed on demand at the first ban
#          (or at the start of Fail2Ban if actionstart_on_demand is set to false).
actionstart = <iptables> -N f2b-<name>
              <iptables> -A f2b-<name> -j <returntype>
              <iptables> -I <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>

链、RETURN 兜底、以及 INPUT 上的跳转,全都在 actionstart 里,而它默认「第一次真正要封人时才执行」。每次操作前跑的那条自检是:

actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'

自检失败就自动补一次 actionstart。也就是说:开机后没人攻击,链就不存在;来第一个坏人,链和跳转被当场建出来。这是设计,不是故障。

actionstart_on_demand 在 action 文件里通常不写死。fail2ban 未显式设置时的判定规则是「该 action 是否含按 family 分段的条件配置」——iptables 系列经 iptables-common.conf 引入了 [Init?family=inet6] 段,于是这个开关自动取真。想让链在启动时就建好,可以显式设成 false;但那样每次启动都会先写一遍规则,懒加载本身没什么值得修的地方。

验证方式(先记录现状,做完还原):

systemctl restart fail2ban && sleep 3
nft list chain ip filter f2b-sshd
# Error: No such file or directory      ← 重启后链确实不在

fail2ban-client set sshd banip 198.51.100.7     # 人为触发一次封禁
iptables -S INPUT | grep f2b
# -A INPUT -p tcp -m multiport --dports <端口> -j f2b-sshd

nft -a list chain ip filter f2b-sshd
# table ip filter {
#     chain f2b-sshd { # handle 180
#         ip saddr 198.51.100.7 counter packets 0 bytes 0 reject # handle 183
#         counter packets 0 bytes 0 return # handle 181
#     }
# }

fail2ban-client set sshd unbanip 198.51.100.7   # 还原

一次实验同时确认三件事:

--dports <端口> 取自该 jail 的 port 设置。jail 里写的端口和守护进程实际监听的端口不一致时,链看起来一切正常,封禁却一条也命中不了——改了服务端口就要同步改 jail,这是同一类「规则在位但不起作用」的另一种形态。

操作要点

相关:/posts/docker-bypasses-ufw.html 里的链顺序问题。

← 全部文章