Moby(Docker Engine)中 --internal 网络的 nftables 防火墙规则剖析:从 usernet-internal 文档到 nftabler 源码实现
本文以 Moby 仓库中 nftables 集成文档的 usernet-internal 场景为骨架,完整讲解在 nftables 防火墙后端下,两个处于不同 --internal 用户自定义网络(ICC 开/关)中的容器会产生哪些 nftables 规则;并结合 nftabler 实现源码 与 golden 测试数据,说明每条规则由哪段代码生成、如何复现,以及如何验证文档与真实规则的一致性。读完后,你可以看懂 docker network create --internal 在 nftables 后端的真实落盘效果,并能独立排查 internal 网络的连通性问题。
场景定义:两个 --internal 网络,ICC 开与关
usernet-internal.md 模板 描述的场景是:两个容器分别位于两个不同的 --internal(内部)桥接网络,其中一个网络允许容器间通信(ICC),另一个禁用 ICC。文档给出的等价操作命令如下:
docker network create \
-o com.docker.network.bridge.name=bridgeICC \
--internal \
--subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
docker run --network bridgeICC --name c1 busybox
docker network create \
-o com.docker.network.bridge.name=bridgeNoICC \
--internal \
--subnet 198.51.100.0/24 --gateway 198.51.100.1 bridge1
docker run --network bridgeNoICC --name c1 busybox
两个网络都使用 --internal 标志,区别在于 ICC 开关状态。需要注意的是:文档模板中的 "Equivalent to" 片段里第二个网络写作 enable_icc=true,而实际生成这份文档的测试定义(nftablesdoc_linux_test.go 中 usernet-internal 一节)为第二个网络 bridgeNoICC 显式设置了 internal: true, noICC: true,即通过选项 com.docker.network.bridge.enable_icc=false 禁用 ICC。golden 规则(下文 counter drop comment "ICC")也印证了第二张网 ICC 是关闭的。
该场景在测试代码中的定义如下(nftablesdoc_linux_test.go 中 index 变量):
{
name: "usernet-internal.md",
networks: []networkDesc{{
name: "bridgeICC",
internal: true,
containers: []ctrDesc{{name: "c1"}},
}, {
name: "bridgeNoICC",
internal: true,
noICC: true,
containers: []ctrDesc{{name: "c1"}},
}},
},
子网地址来自同一文件中的全局变量:
var (
docNetworks = []string{"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"}
docGateways = []string{"192.0.2.1", "198.51.100.1", "203.0.113.1"}
)
完整规则集:table ip docker-bridges 的骨架
所有 bridge 网络的规则都集中在 ip docker-bridges 这张表里(IPv6 规则同构,位于 ip6 docker-bridges 表,文档中只展示 IPv4)。按 index.md 的说明:
- 规则表每次 Docker 启动时重建;
- Docker 不使用 filter-INPUT 钩子——从主机物理网络或主机自身到达的包,因为是"路由进桥接网络",会命中 filter-FORWARD 链;filter-OUTPUT 同样不使用。
完整的 nftables 输出(见 生成后的 golden 文档)骨架如下:
table ip docker-bridges {
map filter-forward-in-jumps {
type ifname : verdict
elements = { "docker0" : jump filter-forward-in__docker0,
"bridgeICC" : jump filter-forward-in__bridgeICC,
"bridgeNoICC" : jump filter-forward-in__bridgeNoICC }
}
map filter-forward-out-jumps {
type ifname : verdict
elements = { "docker0" : jump filter-forward-out__docker0,
"bridgeICC" : jump filter-forward-out__bridgeICC,
"bridgeNoICC" : jump filter-forward-out__bridgeNoICC }
}
map nat-postrouting-in-jumps {
type ifname : verdict
elements = { "docker0" : jump nat-postrouting-in__docker0,
"bridgeICC" : jump nat-postrouting-in__bridgeICC,
"bridgeNoICC" : jump nat-postrouting-in__bridgeNoICC }
}
map nat-postrouting-out-jumps {
type ifname : verdict
elements = { "docker0" : jump nat-postrouting-out__docker0,
"bridgeICC" : jump nat-postrouting-out__bridgeICC,
"bridgeNoICC" : jump nat-postrouting-out__bridgeNoICC }
}
chain filter-FORWARD {
type filter hook forward priority filter; policy accept;
oifname vmap @filter-forward-in-jumps
iifname vmap @filter-forward-out-jumps
}
chain nat-OUTPUT {
type nat hook output priority dstnat; policy accept;
ip daddr != 127.0.0.0/8 fib daddr type local counter jump nat-prerouting-and-output
}
chain nat-POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
iifname vmap @nat-postrouting-out-jumps
oifname vmap @nat-postrouting-in-jumps
}
chain nat-PREROUTING {
type nat hook prerouting priority dstnat; policy accept;
fib daddr type local counter jump nat-prerouting-and-output
}
chain nat-prerouting-and-output {
}
chain raw-PREROUTING {
type filter hook prerouting priority raw; policy accept;
}
...
}
设计上的关键点是 vmap 动态分发:filter-FORWARD 和 nat-POSTROUTING 两条钩子链本身不含具体规则,而是按出接口(oifname vmap @filter-forward-in-jumps)或入接口(iifname vmap @...)查表,jump 到以桥接口名命名的每网络独立链(如 filter-forward-in__bridgeICC)。每创建一张 bridge 网络,nftabler 源码 就会创建四条链并向四张 map 各插入一个元素(NewNetwork → configure):
filter-forward-in__<ifname>/filter-forward-out__<ifname>:转发方向入/出过滤;nat-postrouting-in__<ifname>/nat-postrouting-out__<ifname>:SNAT/MASQUERADE。
链名拼接规则可直接在 network.go 的 chainFilterFwdIn 等辅助函数中看到("filter-forward-in__" + ifName)。
--internal 网络的核心规则:INGRESS/EGRESS 丢弃 + ICC 判决
internal 网络与带外网访问的普通网络(如 usernet-portmap 场景)大部分规则相同,差异集中在两条 filter-forward-in 链:
chain filter-forward-in__bridgeICC {
ct state established,related counter accept
iifname != "bridgeICC" counter drop comment "INTERNAL NETWORK INGRESS"
counter accept comment "ICC"
}
chain filter-forward-in__bridgeNoICC {
ct state established,related counter accept
iifname != "bridgeNoICC" counter drop comment "INTERNAL NETWORK INGRESS"
counter drop comment "ICC"
}
逐条解读:
ct state established,related counter accept:放行已建立连接的回程流量与相关流量(连接跟踪白名单),这是所有网络链的第一条规则,源码对应initialRuleGroup组的 conntrack 规则;iifname != "bridgeICC" counter drop comment "INTERNAL NETWORK INGRESS":入接口不是本桥的包一律丢弃——这就是--internal网络的"入口隔离",外部网络(包括 docker0、其他内部网络)发起的入站包进不来;- 最终判决:ICC 开启时是
counter accept comment "ICC"(同网络容器间流量放行),ICC 关闭时是counter drop comment "ICC"(容器间通信也被丢弃)。
出方向同样有对称限制:
chain filter-forward-out__bridgeICC {
ct state established,related counter accept
oifname != "bridgeICC" counter drop comment "INTERNAL NETWORK EGRESS"
}
oifname != "bridgeICC" counter drop comment "INTERNAL NETWORK EGRESS" 保证从本网络出去的包不能转发到别的桥,实现"出口隔离"。由于没有更多 accept 规则且链尾无兜底 accept(钩子链 policy 为 accept,但这些被 vmap jump 进来的子链默认不匹配即返回),internal 网络的流量既出不去也进不来,只能留在本网络内(且受 ICC 开关约束)。
为什么 nat-postrouting-out 链是空的
internal 网络最直观的 nftables 特征之一,是它的 NAT 出方向链完全为空:
chain nat-postrouting-out__bridgeICC {
}
chain nat-postrouting-out__bridgeNoICC {
}
这与普通网络形成对比——docker0 上有 masquerade 规则:
chain nat-postrouting-out__docker0 {
oifname != "docker0" ip saddr 172.17.0.0/16 counter masquerade comment "MASQUERADE"
}
从源码结构看,原因很直接:在 network.go 的 configure 函数中,masquerade/SNAT 规则(oifname != ... ip saddr <prefix> counter masquerade)被包在非 internal 的 else 分支里,且条件是 n.config.Masquerade && !conf.Routed。internal 分支(if n.config.Internal)只创建 INGRESS/EGRESS 丢弃规则和 ICC 判决规则,根本不往 nat-postrouting-out 链添加任何规则——因为内部网络本身不允许出站到外部,NAT 也就无从谈起。
ICC 判决值在源码中由 iccVerdict 决定:
iccVerdict := "accept"
if !n.config.ICC {
iccVerdict = "drop"
}
即 filter-forward-in__<ifname> 链尾的 counter <accept|drop> comment "ICC" 直接对应 --internal 网络是否禁用了 enable_icc 选项。
源码与 golden 测试数据交叉印证
nftables 后端的 internal 网络规则由 nftabler 实现。configure 函数中 internal 分支生成的三条规则(network.go 关键片段):
iccVerdict := "accept"
if !n.config.ICC {
iccVerdict = "drop"
}
if n.config.Internal {
// Drop anything that's not from this network.
tm.Create(nftables.Rule{
Chain: fwdInChain,
Group: initialRuleGroup,
Rule: []string{`iifname != `, n.config.IfName, `counter drop comment "INTERNAL NETWORK INGRESS"`},
})
tm.Create(nftables.Rule{
Chain: fwdOutChain,
Group: initialRuleGroup,
Rule: []string{`oifname != `, n.config.IfName, `counter drop comment "INTERNAL NETWORK EGRESS"`},
})
// Accept or drop Inter-Container Communication.
tm.Create(nftables.Rule{
Chain: fwdInChain,
Group: fwdInICCRuleGroup,
Rule: []string{"counter", iccVerdict, "comment ICC"},
})
}
单元测试层面,nftabler 包还有一组按配置组合命名的 golden 文件,可与集成文档交叉验证:
- hairpin=false,internal=true,icc=true__ip.golden:
filter-forward-in__br-dummy链尾为counter accept comment "ICC"; - hairpin=false,internal=true,icc=false__ip.golden:同位置变为
counter drop comment "ICC"; - 两份 golden 中
iifname != "br-dummy" counter drop comment "INTERNAL NETWORK INGRESS"与出方向的INTERNAL NETWORK EGRESS规则一致存在,且nat-postrouting-out__br-dummy为空——与集成文档记录完全吻合。
文档是怎么生成的:模板 + 实测 nftables + golden diff
templates/usernet-internal.md 并不是一份手工维护的文档,而是 text/template 模板。TestBridgeNftablesDoc 的工作流程(index.md 亦有说明):
- 为每个"场景"创建一个独立网络命名空间的虚拟主机(L3Segment),在其中启动 dockerd;
- 按
index定义创建网络与容器(internal 场景即创建bridgeICC/bridgeNoICC两张网并各跑一个 busybox 容器); - 执行
nft -s list table ip docker-bridges抓取真实规则,把整表和每个 map/chain 拆成模板可用的键(如Ruleset4、chain filter-forward-in__bridgeICC); - 用 templates 目录 中对应模板渲染出 markdown;
- 与 generated 目录 中的 golden 参考文档 diff,规则不一致则测试失败。
模板中形如 {{index . "chain filter-forward-in__bridgeICC"}} 的占位符,就是被实测输出替换的位置。若规则变更导致测试失败,模板头注释(nftablesdoc_linux_test.go 顶部)给出的处理方式是:先检查 diff 中 nftables 规则的变化,更新对应模板中的描述,再用 TESTFLAGS='-update' 重新生成参考文档。
运行该测试有明确的环境前提(源码中的 skip 条件):仅 Linux、防火墙后端为 nftables(testEnv.FirewallBackendDriver() == "nftables")、非 rootless、且系统未运行 firewalld。
使用边界与注意事项
- 这是开发用途的文档:index.md 明确警告 Docker 的 nftables 规则结构在不同发行版之间会变化,不是稳定接口——不应把本文列出的链名、map 名写死在运维脚本里;
- 文档只展示 IPv4 表
ip docker-bridges,IPv6 规则遵循同样模式但位于ip6 docker-bridges; - 每条规则都带
counter,可用nft -s list table ip docker-bridges查看计数,例如观察INTERNAL NETWORK INGRESS上的 drop 计数,可以快速判断是否有外部流量正在被 internal 网络丢弃; - internal 网络的隔离完全依赖上述 filter 链的 drop 规则,NAT 层面(空
nat-postrouting-out链)不提供额外防护,排查"internal 网络为什么访问不了外网"时,应先看filter-forward-in/out链的 EGRESS/INGRESS 规则,而不是 NAT 链。
小结
usernet-internal 场景用最精简的规则集说明了 Docker Engine 在 nftables 后端实现 --internal 网络隔离的完整机制:每网络独立链 + vmap 按接口分发,INTERNAL NETWORK INGRESS/EGRESS 两条 drop 规则封死入出口,ICC 判决规则按 enable_icc 配置生成 accept 或 drop,NAT 链保持空白。这套规则既能在集成文档的 golden 输出中逐行对照,也能在 nftabler 源码 与 单元测试 golden 数据 中找到一一对应的生成逻辑,是理解 Moby 桥接网络防火墙行为的典型切面。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00