近期在接手一批新上线的 Debian 12 宿主机时,遇到了一个极其隐蔽的网络黑洞问题。业务侧反馈,将服务从 CentOS 7 迁移到新环境后,宿主机自身网络一切正常,但 Docker 容器内的所有 Outbound 流量(包括 DNS 解析、外部 API 调用)全部超时。
简单看一下背景和结论:为了对齐基础安全基线,系统组在新系统上摒弃了老旧的 iptables,转而使用原生的 nftables 编写了主机防火墙策略,并将 forward 链的默认策略设置为 drop。故障的根本原因在于对 nftables 的 Base Chain(基础链)和 accept 动作语义理解不到位。 在配合 iptables-nft(Docker 默认的底层网络驱动)工作时,nftables 中不同表里的 Base Chain 会发生叠加。Docker 规则里的 ACCEPT 仅仅中断了当前链的匹配,报文随后又掉进了原生安全策略的 drop 陷阱中。
把两种时代的产物混用,又不仔细看 Netfilter 底层 Hook 的运转机制,就像在同一个路口安排了两个互不理睬的交警,一个挥手让你走,另一个直接把你的车按死。
现场还原与报错表现
排查过程从最基本的抓包开始。在容器内执行 curl 8.8.8.8,同时在宿主机的几个关键网卡上抓包:
# 容器内 veth 接口看到 SYN 发出,但没有 SYN-ACK
tcpdump -i veth_xxx -nn host 8.8.8.8
# docker0 网桥上能看到报文进入
tcpdump -i docker0 -nn host 8.8.8.8
# 物理网卡 eth0 上毫无动静
tcpdump -i eth0 -nn host 8.8.8.8
报文在路由判决后,准备进行转发(Forward)时凭空消失了。习惯性地敲下 iptables -nL FORWARD,看到 Docker 生成的规则依然健在:
Chain FORWARD (policy DROP)
target prot opt source destination
DOCKER-USER all -- 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-1 all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
DOCKER all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
表面上看,Docker 已经放行了跨网桥的流量。负责实施的同事一口咬定:“Docker 自己管理的 iptables 规则没有任何问题,肯定是内核路由参数 ip_forward 没开!”
然而 sysctl net.ipv4.ip_forward 明晃晃地显示着 1。
抽丝剥茧:nftables 里的“平行宇宙”
问题出在哪里?在较新的发行版中,iptables 命令实际上只是 iptables-nft 的一个软链接。Docker 以为自己在操作传统的 iptables,实际上底层被翻译成了 nftables 的规则存入内核。
此时我们看一眼主机上真正生效的全量规则表:nft list ruleset。
精简后的输出如下:
# 这是 Docker 经由 iptables-nft 生成的表
table ip filter {
chain FORWARD {
type filter hook forward priority filter; policy drop;
jump DOCKER-USER
jump DOCKER-ISOLATION-STAGE-1
oifname "docker0" ct state related,established counter accept
oifname "docker0" jump DOCKER
iifname "docker0" oifname != "docker0" counter accept # <-- 注意这里,Docker 决定 ACCEPT
iifname "docker0" oifname "docker0" counter accept
}
}
# 这是系统组手写的原生 nftables 主机防火墙
table inet my_sec_firewall {
chain base_forward {
type filter hook forward priority filter; policy drop; # <-- 这里是罪魁祸首
ct state established,related accept
# 这里仅仅放行了部分特定网段的内网互访,没有提及 docker0
}
}
这里隐藏着一个巨大的语义陷阱:在旧的 iptables 架构中,一个包在一个 Table/Hook 中如果匹配到了 ACCEPT 规则,它的遍历就彻底结束了,直接进入下一阶段。但在 nftables 架构中,你可以定义无数个挂载在同一 Hook 点的 Base Chain(基础链)。
上述配置中,ip filter 和 inet my_sec_firewall 都注册了针对 forward hook 的 Base Chain,且优先级都是 filter(数值为 0)。
当容器的流量进入 Netfilter 的 forward hook 时,发生了什么?
-
报文进入 Docker 的
FORWARD链。 -
匹配到
iifname "docker0" oifname != "docker0" counter accept。 -
关键点来了:在 nftables 中,Base Chain 里的
accept叫做Verdict: accept。它的完整语义是“停止遍历当前 Base Chain,允许该包继续走向下一个处于同等或更低优先级的 Base Chain”。 -
于是,报文带着 Docker 赐予的“通行证”,继续走进了系统组手写的
base_forward链。 -
base_forward链左看右看,发现这条流量不符合任何放行规则,直接走默认策略policy drop,报文被无情丢弃。
这就是典型的“知其然而不知其所以然”。抄袭旧时代的防火墙规范,用新语法包装了一下,结果搞出了网络黑洞。
现场 Debug 铁证:nftrace 的降维打击
为了让同事彻底死心并理解这个过程,直接上 nftables 的杀手锏工具 nftrace 进行数据包流向跟踪。
在我们的防火墙表里加一条 trace 规则:
nft add rule inet my_sec_firewall base_forward meta nftrace set 1
然后在另一个终端启动监听,并再次在容器内触发 curl 8.8.8.8:
nft monitor trace
日志无情地揭露了报文的死亡现场:
trace id 75b42d1f ip filter FORWARD packet: iif "docker0" oif "eth0" src 172.17.0.2 dst 8.8.8.8 ...
trace id 75b42d1f ip filter FORWARD rule iifname "docker0" oifname != "docker0" counter packets 12 bytes 720 accept (verdict accept)
...
trace id 75b42d1f inet my_sec_firewall base_forward packet: iif "docker0" oif "eth0" src 172.17.0.2 dst 8.8.8.8 ...
trace id 75b42d1f inet my_sec_firewall base_forward rule meta nftrace set 1 (verdict continue)
trace id 75b42d1f inet my_sec_firewall base_forward verdict drop # <-- 在这里被默认策略处决!
日志清楚地表明,报文先被 Docker 的链 accept,紧接着落入 my_sec_firewall 的链,并命中 drop 策略。
解决代码与重构建议
想要修复这个问题非常简单,既然它要过两道关,那就在原生安全策略里把 Docker 的网桥放行即可:
nft add rule inet my_sec_firewall base_forward iifname "docker0" accept
但是,作为架构师,这种补丁式的做法是不合格的。 因为 Docker 的网络隔离策略(比如容器间不可见、端口映射暴露限制)本身就非常复杂,如果强行用另一套独立表的策略去叠加,极易造成后续排查的灾难。
最终的整改落地方案:
-
停止混用策略:如果系统中存在需要深度接管底层网络的组件(如 Docker、K8S kube-proxy),主机级的防火墙防护应尽量下放给外部设施(如云厂商的安全组、物理防火墙)。
-
Hook 优先级规避:必须写本机策略时,确保你的防火墙 Base Chain 优先级数值不要和 Docker 的产生竞争。Docker 默认的 priority 是 0。如果你只做简单的黑名单前置拦截,可以建一个 priority -100 的链;如果你想要兜底,可以建一个 priority 100 的链。
-
放弃纯净洁癖:不要在运行了遗留 iptables/Docker 逻辑的机器上,强制推行所谓的“纯原生 nftables 架构”。要么让 Docker 禁用 iptables (
"iptables": falsein daemon.json) 完全靠你自己手写路由转发,要么老老实实顺从iptables-nft的兼容模式,把你的安全规则也用iptables语法追加进去。
总结与排查清单
在系统底层的迭代中,“兼容”往往是最危险的词汇。iptables-nft 给了一个完美的语法兼容幻觉,却暗改了多链并行的核心逻辑。
同类问题速查清单:
-
辨别真伪 iptables:执行
update-alternatives --display iptables确认系统当前底层是iptables-legacy还是iptables-nft。 -
全局视角查规则:抛弃
iptables -nL,排查网络不通时必须看全景:nft list ruleset,重点寻找包含policy drop的自定义 Base Chain。 -
理解 priority 与 accept 的关系:同一 Hook 点存在多个 Base Chain 时,
accept只是“出当前链”,不是“出整个 Hook”。只有drop才是真正的一票否决。 -
抓包查死因:如果
tcpdump看到包进了某网卡但出不来,直接开启nftables trace(meta nftrace set 1) 跟踪,看包死在哪个 Table 的哪条 Rule,比瞎猜高效百倍。