Moby 桥接网络 nftables 规则解析:禁用容器间通信(ICC)并发布端口
当你在 Moby(Docker Engine)中以 enable_icc=false 创建用户自定义桥接网络,同时发布容器端口时,内核防火墙中会出现一组特定的 nftables 规则。本文以仓库中 usernet-portmap-noicc.md 场景文档为主体,完整给出该场景下 ip docker-bridges 表的全部规则、逐条解释其作用,并深入 nftabler 源码 说明「放行 vs 丢弃」这一关键判定的产生位置,最后给出在真实主机上自行验证的方法与前提。
一、文档定位:一个由测试自动生成的规则快照
这篇场景文档属于 integration/network/bridge/nftablesdoc 目录下的一套 nftables 规则文档体系,它记录 Docker Engine 在桥接网络下究竟往内核里写入了什么规则。与手写文档不同,它是测试驱动生成的:
- nftablesdoc_linux_test.go 中的
TestBridgeNftablesDoc会在一个独立网络命名空间里启动 dockerd,按 index 中定义的各场景创建网络和容器,随后执行nft -s list table ip docker-bridges抓取真实规则(见 runNftables); - 抓取到的规则按「map / chain」拆分成块,用
templates/下的text/template模板(本文档模板即 usernet-portmap-noicc.md)渲染成最终 markdown,输出到 generated/usernet-portmap-noicc.md; - 新生成的文档会与
generated/中的 golden 参考做 diff,不一致则测试失败(golden 断言),需要时用TESTFLAGS='-update'刷新参考文件。
这意味着文中规则与当前代码库的实际行为一一对应,而非人工维护的文字描述。
同时要注意 index.md 给出的三条前提,本文规则的解释都建立在这些前提之上:
- 该文档仅供开发参考——Docker 的 nftables 规则结构在不同版本间会变化,不是稳定接口;
- IPv6 规则遵循与 IPv4 完全相同的模式,只是位于不同的表(
ip docker-bridges与ip6 docker-bridges),文档只展示 IPv4; - 这些表在每次 Docker 启动时重建;
filter-INPUT钩子不被使用,从宿主物理网络或宿主本机到达的包会被路由进桥接网络、命中filter-FORWARD链,filter-OUTPUT同理未被使用。
二、场景与等效命令
本文档描述的精确场景是:用户自定义网络上,关闭容器间通信(ICC),且容器发布了端口。等效的手动操作命令为(与 测试中的场景定义 一致,测试中通过 bridge.EnableICC 选项传入 "false"):
docker network create \
-o com.docker.network.bridge.name=bridge1 \
-o com.docker.network.bridge.enable_icc=false \
--subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
docker run --network bridge1 -p 8080:80 --name c1 busybox
要点:
- 网络名
bridge1,子网192.0.2.0/24,网关192.0.2.1;容器c1拿到192.0.2.2(测试里容器 IP 由 IPAM 分配,golden 文档中固定为.2); -p 8080:80发布端口;enable_icc=false是本场景与 默认场景 usernet-portmap.md 的唯一区别。
三、完整规则集(IPv4)
以下是该场景下 ip docker-bridges 表的全量内容(与 generated 文档 完全一致):
table ip docker-bridges {
map filter-forward-in-jumps {
type ifname : verdict
elements = { "docker0" : jump filter-forward-in__docker0,
"bridge1" : jump filter-forward-in__bridge1 }
}
map filter-forward-out-jumps {
type ifname : verdict
elements = { "docker0" : jump filter-forward-out__docker0,
"bridge1" : jump filter-forward-out__bridge1 }
}
map nat-postrouting-in-jumps {
type ifname : verdict
elements = { "docker0" : jump nat-postrouting-in__docker0,
"bridge1" : jump nat-postrouting-in__bridge1 }
}
map nat-postrouting-out-jumps {
type ifname : verdict
elements = { "docker0" : jump nat-postrouting-out__docker0,
"bridge1" : jump nat-postrouting-out__bridge1 }
}
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 {
iifname != "bridge1" tcp dport 8080 counter dnat to 192.0.2.2:80 comment "DNAT"
}
chain raw-PREROUTING {
type filter hook prerouting priority raw; policy accept;
ip daddr 192.0.2.2 iifname != "bridge1" counter drop comment "DROP DIRECT ACCESS"
}
chain filter-forward-in__docker0 {
ct state established,related counter accept
iifname "docker0" counter accept comment "ICC"
counter drop comment "UNPUBLISHED PORT DROP"
}
chain filter-forward-out__docker0 {
ct state established,related counter accept
counter accept comment "OUTGOING"
}
chain nat-postrouting-in__docker0 {
}
chain nat-postrouting-out__docker0 {
oifname != "docker0" ip saddr 172.17.0.0/16 counter masquerade comment "MASQUERADE"
}
chain filter-forward-in__bridge1 {
ct state established,related counter accept
iifname "bridge1" counter drop comment "ICC"
ip daddr 192.0.2.2 tcp dport 80 counter accept
counter drop comment "UNPUBLISHED PORT DROP"
}
chain filter-forward-out__bridge1 {
ct state established,related counter accept
counter accept comment "OUTGOING"
}
chain nat-postrouting-in__bridge1 {
}
chain nat-postrouting-out__bridge1 {
oifname != "bridge1" ip saddr 192.0.2.0/24 counter masquerade comment "MASQUERADE"
}
}
3.1 顶层结构:vmap 分发到「每桥接一个」的链
- 四个
ifname : verdict类型的 map 把「接口名」映射到对应的 jump 链。filter-FORWARD链本体只有两条 vmap 指令:按出接口(oifname)跳入filter-forward-in__<桥>,按入接口(iifname)跳入filter-forward-out__<桥>。这样每个网桥(默认的docker0与用户创建的bridge1)各有一套独立的进出过滤链,互不干扰。 nat-POSTROUTING同理,按出/入接口分发到nat-postrouting-{in,out}__<桥>链。- 注意
filter-FORWARD的policy accept:Docker 依赖显式 drop 规则来封锁流量,而不是收紧默认策略;链内所有未匹配的流量最终都会落到各链末尾的兜底规则(如UNPUBLISHED PORT DROP)。
3.2 DNAT 与「禁止直连容器 IP」
nat-prerouting-and-output中的iifname != "bridge1" tcp dport 8080 ... dnat to 192.0.2.2:80是端口发布的 dstnat 规则:来自桥之外的流量(宿主本机、外部网络)访问8080时被改写到容器192.0.2.2:80。该链同时被nat-PREROUTING(经fib daddr type local判定目标为本机地址后)和nat-OUTPUT(宿主机自身发起的流量)复用,因此curl 127.0.0.1:8080与从外网访问都走同一条 DNAT 规则;raw-PREROUTING中的ip daddr 192.0.2.2 iifname != "bridge1" counter drop comment "DROP DIRECT ACCESS"是关键防护:任何绕过 NAT 直接访问容器 IP(192.0.2.2)的包(例如同 LAN 主机直连容器地址)在进入 conntrack 之前就被丢弃。它保证了容器只能经由发布端口被触达;nat-postrouting-out__bridge1中的 masquerade 规则保证容器发往外部网络(oifname != "bridge1")时,源地址192.0.2.0/24被替换为宿主地址,即常见的 SNAT/MASQUERADE 行为;docker0侧对应172.17.0.0/16。
3.3 每桥接的进出过滤链
以 bridge1 为例:
filter-forward-out__bridge1(容器出站方向):先放行 conntrack 中established,related状态的包(保证回包畅通),其余全部 accept 并打OUTGOING注释——出站不做限制,出网过滤交给后续路由链;filter-forward-in__bridge1(入站方向)是安全策略的核心,下一节单独剖析。
四、核心差异:filter-forward-in__bridge1 中的 ICC drop 规则
文档的核心结论只有一句话:除 ICC 判定外,本场景的规则与 启用 ICC 的网络 完全相同。差异全部集中在这条链:
chain filter-forward-in__bridge1 {
ct state established,related counter accept
iifname "bridge1" counter drop comment "ICC"
ip daddr 192.0.2.2 tcp dport 80 counter accept
counter drop comment "UNPUBLISHED PORT DROP"
}
逐条拆解(规则按顺序匹配):
ct state established,related counter accept:处于已建立/相关状态的连接直接放行。这是 conntrack 的固有语义——即使网络已关闭 ICC,规则变更之前已经建立的连接的回程流量仍能通过;但新连接的初始 SYN 不受此规则影响,会继续向下匹配。iifname "bridge1" counter drop comment "ICC":入接口是本桥的包,即源与目的都在同一bridge1网络上的容器互访,新连接在此被丢弃。这正是enable_icc=false的内核级实现。作为对照,filter-forward-in__docker0中对应位置是iifname "docker0" counter accept comment "ICC"(放行),两条规则唯一的差别就是 verdict。ip daddr 192.0.2.2 tcp dport 80 counter accept:放行已 DNAT 的发布端口流量。从外部来的8080经 DNAT 改写为192.0.2.2:80后被路由进bridge1,命中这条 accept;由于它排在 ICC drop 之后,外部访问不受 ICC 策略影响,而容器间访问192.0.2.2:80会被第 2 条先行丢弃。counter drop comment "UNPUBLISHED PORT DROP":兜底丢弃——凡未发布端口的入站流量(包括试图直连容器上未发布端口的流量)一律丢弃。
counter 关键字使得每条规则都带计数,运维时可用 nft -s list 直接看到每条规则命中了多少包,这对排查「为什么容器 A ping 不通容器 B」这类问题非常有用。
五、源码纵深:iccVerdict 是如何决定的
文档中「drop(而不是 accept)」的差别,在源码里对应一个非常小的分支。在 nftabler/network.go:
iccVerdict := "accept"
if !n.config.ICC {
iccVerdict = "drop"
}
随后该 verdict 被填入规则(非 internal 网络分支):
// Inter-Container Communication
tm.Create(nftables.Rule{
Chain: fwdInChain,
Group: fwdInICCRuleGroup,
Rule: []string{"iifname ==", n.config.IfName, "counter", iccVerdict, "comment ICC"},
})
这解释了 golden 文档中为什么 ICC 行永远是 iifname "bridge1" counter <accept|drop> comment "ICC" 的固定形态。UNPUBLISHED PORT DROP 兜底则来自同一文件的最终规则组(非 --internal 网络一律 drop)。
ICC 配置沿这条链路向上游溯源:
- 用户输入:
docker network create -o com.docker.network.bridge.enable_icc=false ...。选项标签常量定义在 bridge 驱动的 labels.go; - 选项解析:bridge_linux.go 中对
EnableICC选项用strconv.ParseBool(value)解析为布尔值(因此false/0/no等均可接受); - 默认值:默认的
docker0桥接配置中EnableICC默认为true(bridge_linux.go 附近的默认bridgeConfig),即不显式指定时容器间通信是放行的; - 配置传递:firewaller.go 中
ICC字段注释明确其含义——ICC is false if containers on the bridge should not be able to communicate,nftables 后端与 iptables 后端共用该配置; - 持久化:网络配置变更(含
EnableICC)会写入网络 store(bridge_store.go),daemon 重启后按原配置重建规则,与「表在每次 Docker 启动时重建」的描述一致。
顺带一提,同一功能在 iptables 后端的对应实现位于 iptabler/network.go 的 setIcc 相关逻辑,可见 Moby 目前同时维护 nftables 与 iptables 两套防火墙后端,而本系列文档只覆盖 nftables 后端。
六、在真实主机上自行验证
你可以完全复现本节的验证流程。前提(对应 测试的 skip 条件):
- 以 root 运行(非 rootless);
- 宿主防火墙后端为 nftables(
firewalld运行时该测试会跳过,因为 firewalld 会接管/重写规则,无法文档化稳定输出); - 内核支持 nftables(现代发行版默认支持)。
步骤:
# 1. 创建与文档等效的网络和容器
docker network create \
-o com.docker.network.bridge.name=bridge1 \
-o com.docker.network.bridge.enable_icc=false \
--subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
docker run -d --network bridge1 -p 8080:80 --name c1 busybox top
# 2. 查看 Docker 写入的 nftables 表(-s 显示计数器)
sudo nft -s list table ip docker-bridges
预期结果:输出应与 generated/usernet-portmap-noicc.md 中的规则集一致,其中容器 IP、宿主地址等可能因 IPAM 分配而异,但链结构、comment 标记(DNAT、ICC、MASQUERADE、UNPUBLISHED PORT DROP、DROP DIRECT ACCESS)应完全吻合。
行为验证:
# 容器间通信应失败(ICC 已禁用)
docker run --rm --network bridge1 busybox wget -qO- --timeout=3 192.0.2.2 || echo "blocked as expected"
# 经发布端口的访问应成功
curl -s http://127.0.0.1:8080
七、同系列的其他场景
本文档只是整套 nftables 场景文档之一,index.md 列出了全部快照,可按需对比理解规则如何随配置变化:
- New daemon:刚启动、无任何用户网络的基线状态;
- usernet-portmap:与本场景相同但启用 ICC(对照组);
- usernet-portmap-lo:端口发布到 loopback 地址;
- usernet-portmap-noproxy:
--userland-proxy=false关闭用户态代理; - usernet-internal:
--internal网络(对比 ICC 开/关两种); - usernet-portmap-routed:routed 网关模式;
- usernet-portmap-natunprot:nat-unprotected 模式;
- swarm-portmap:Swarm 服务发布端口。
小结
enable_icc=false在内核层面的全部效果,就是让 per-bridge 入站过滤链中那条iifname "<桥>" counter drop comment "ICC"规则的 verdict 从accept变为drop;源码中该判定由 nftabler 的iccVerdict分支 一行代码决定;- 发布端口的 DNAT/MASQUERADE 链路(
nat-prerouting-and-output、raw-PREROUTING的直连拦截、每桥的 masquerade 链)与 ICC 策略正交,不受其影响; - 这套文档由 TestBridgeNftablesDoc 从运行中的 daemon 实时抓取生成并做 golden 比对,因此规则快照与当前代码行为严格一致;但如 index.md 所警告,Docker 的 nftables 结构不是稳定接口,跨版本对比时需留意规则可能变化。
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