Moby 中 routed 模式桥接网络已发布端口的 nftables 规则全景解析
本文基于 Moby(Docker 引擎上游)仓库中的自动生成文档 usernet-portmap-routed.md 展开,完整解读“容器运行在 routed 模式桥接网络上、且带有已发布端口”这一场景下 Docker 生成的 nftables 规则表。读完本文,你将掌握 routed 网关模式与默认 nat 模式在 NAT、转发过滤、直连访问控制上的规则级差异,并能对照 bridge 驱动的 nftables 实现源码 理解每条规则的来源与验证方法。
这份文档从哪里来:自动化生成的“规则快照”
usernet-portmap-routed.md 是一份生成文件(文件首行标注 <!-- This is a generated file; DO NOT EDIT. -->)。它由集成测试 nftablesdoc_linux_test.go 中的 TestBridgeNftablesDoc 产出:测试启动一个运行在独立网络命名空间中的 dockerd,按预设场景创建网络与容器,然后执行 nft -s list table ip docker-bridges 捕获真实规则,再与 templates/usernet-portmap-routed.md 模板合并生成 markdown,最后与仓库中的“golden”参考文件做 diff,规则发生变化时测试即失败。
需要说明两个前提(见 index.md):
- Docker 使用的 nftables 规则结构不是稳定接口,版本之间可能变化,文档仅面向开发排查用途;
- 测试只展示 IPv4 规则(表
ip docker-bridges),IPv6 规则遵循同样模式,位于独立的ip6 docker-bridges表中; - Docker 不使用 filter-INPUT / filter-OUTPUT 钩子,来自宿主机物理网络或宿主机自身的包在路由进桥接网络后都会经过 filter-FORWARD 链;
- 整个表在每次 Docker 启动时重建。
复现场景:创建 routed 模式网络并发布端口
文档描述的操作等价于以下命令:
docker network create \
-o com.docker.network.bridge.name=bridge1 \
-o com.docker.network.bridge.gateway_mode_ipv4=routed \
--subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
docker run --network bridge1 -p 8080:80 --name c1 busybox
各参数的含义(选项键名定义见 labels.go):
| 选项 | 作用 |
|---|---|
-o com.docker.network.bridge.name=bridge1 |
指定桥接设备名为 bridge1(对应源码常量 BridgeName) |
-o com.docker.network.bridge.gateway_mode_ipv4=routed |
将 IPv4 网关模式设为 routed(对应 IPv4GatewayMode),取值还可为 nat(默认)与 nat-unprotected |
--subnet 192.0.2.0/24 --gateway 192.0.2.1 |
子网与网关地址。容器 c1 被分配到 192.0.2.2(见下文规则中的 ip daddr 192.0.2.2) |
-p 8080:80 |
发布容器 80 端口。注意:routed 模式下这条映射不会在宿主机上做 DNAT,也不启动 userland proxy(见文末对比) |
测试代码中对应的场景定义在 nftablesdoc_linux_test.go 的 index 表中:网络 bridge1、gwMode: "routed"、容器 c1 映射 80/tcp -> 8080。
完整的 nftables 规则表
文档给出的完整 table ip docker-bridges(此处同时存在默认桥 docker0 与新建的 bridge1)如下:
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 {
}
chain raw-PREROUTING {
type filter hook prerouting priority raw; policy accept;
}
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
ip protocol icmp counter accept comment "ICMP"
iifname "bridge1" counter accept 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 {
}
}
表结构的通用机制
- vmap 分发:
filter-FORWARD、nat-POSTROUTING等钩子链本身不含具体业务规则,而是通过iifname/oifname上的 vmap(虚拟映射表)把包按“入接口 / 出接口”跳转到每个桥各自的filter-forward-in__<桥名>、nat-postrouting-out__<桥名>等链。这样一张表可以同时管理多个用户网络,互不干扰。这些 map 元素的创建逻辑见 network.go。 - 连接跟踪先行:每个桥的入/出链第一条规则都是
ct state established,related counter accept,已建立与关联连接直接放行(network.go)。 - 默认桥
docker0的规则不受 routed 网络影响:它仍然带有172.17.0.0/16的 masquerade 规则和 ICC 规则,说明网关模式的差异化处理是按网络(按桥接口)进行的。
bridge1 入方向链的逐条解读
filter-forward-in__bridge1 是这个场景的核心:
chain filter-forward-in__bridge1 {
ct state established,related counter accept
ip protocol icmp counter accept comment "ICMP"
iifname "bridge1" counter accept 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:放行已有连接的后续包;ip protocol icmp counter accept comment "ICMP":routed 模式独有的 ICMP 放行规则(原因见下节);iifname "bridge1" counter accept comment "ICC":同网容器间通信(inter-container communication),入接口就是本桥的包直接放行;ip daddr 192.0.2.2 tcp dport 80 counter accept:这就是-p 8080:80留下的规则——注意它匹配的是容器 IP 192.0.2.2 的 80 端口,而不是宿主机的 8080 端口。routed 模式不做 DNAT,“发布端口”在这里的实际含义是“允许外部直接访问容器 IP 的该端口”;counter drop comment "UNPUBLISHED PORT DROP":兜底丢弃,未发布的端口一律拒绝。该规则由 network.go 在非Unprotected网络时创建。
出方向链 filter-forward-out__bridge1 只有连接跟踪放行加一条 OUTGOING 全放行,即容器出网在转发层不设限(出网是否改写源地址则取决于 NAT 规则,见下节)。
与 nat 模式的四个关键差异
文档明确指出,其余大部分规则与 nat 模式网络 相同,真正的区别在以下四处。
1. raw-PREROUTING 中没有 "DROP DIRECT ACCESS" 规则
chain raw-PREROUTING {
type filter hook prerouting priority raw; policy accept;
}
nat 模式下,Docker 会在 raw-PREROUTING 中为容器 IP 添加 “DROP DIRECT ACCESS” 规则,阻止外部流量直接寻址容器 IP,强制走发布端口。routed 模式下该链为空,因此容器可以从宿主机外部直接访问。对应源码是 endpoint.go 的 filterDirectAccess:当网络 conf.Routed(或 Unprotected、internal、daemon 级允许直连)为真时,该函数直接返回、不生成任何 drop 规则。这正是 routed 模式的语义核心:容器 IP 对外可达,宿主机作为网关为其路由。
2. filter-forward-in 中多了一条 ICMP 放行规则
同样在 network.go 中:只有 conf.Routed 为真时才会创建 ICMP 放行规则(IPv4 表用 ip protocol icmp,IPv6 表则用 meta l4proto ipv6-icmp)。
从源码结构看这条规则的必要性:routed 模式下容器 IP 对外部直接可达,且不再有 masquerade 兜底,ICMP(包括 ICMP 错误消息,如目的不可达)对于外部正确路由和诊断至关重要,故在丢弃未发布端口之前显式放行。
3. 没有 masquerade 规则
chain nat-prerouting-and-output {
}
chain nat-postrouting-out__bridge1 {
}
两个与 bridge1 相关的 NAT 链都是空的:
nat-prerouting-and-output为空 → 没有 DNAT,外部访问 8080 不会被改写成容器地址;nat-postrouting-out__bridge1为空 → 容器出网不做 SNAT/masquerade,出网包以容器自身的192.0.2.x源地址离开宿主机。
对照源码 network.go:masquerade(或 snat)规则仅在 n.config.Masquerade && !conf.Routed 时创建,conf.Routed 直接排除了它。这与文档中 docker0 仍保留 masquerade 规则形成鲜明对比——默认桥照常 NAT,routed 桥则完全不做源地址改写。
4. 发布端口不会启动 userland proxy
文档结尾强调:“And, the userland proxy won't be started for mapped ports.”。这与 port.go 中的注释一致:routed 模式下的端口绑定(port bindings)“不在宿主机上映射端口”——既然规则层面已经允许直接访问容器 IP 的对应端口,宿主机上就没有需要 docker-proxy 监听的本地端口,userland proxy 自然无从启动。这也意味着该场景的端口连通性完全依赖内核规则,不再有用户态代理的兜底路径。
三种网关模式的规则形态速览
同一套文档集还生成了其他网关模式的快照,便于对照:
| 网关模式 | 外部直连容器 IP | bridge1 出网 NAT | 端口映射方式 | 参考文档 |
|---|---|---|---|---|
nat(默认) |
被 raw 链 drop | 有 masquerade | DNAT 到容器端口 + 端口级转发放行 | usernet-portmap.md |
nat-unprotected |
允许(raw 链为空) | 有 masquerade | DNAT 保留,转发层对任意端口放行(UNPROTECTED) |
usernet-portmap-natunprot.md |
routed(本文) |
允许(raw 链为空) | 无 masquerade | 无 DNAT、无 proxy,转发层按“容器 IP + 端口”放行 | usernet-portmap-routed.md |
典型数据包的走向
结合上面的规则,可以推演 routed 模式下两类流量的路径:
外部主机访问 192.0.2.2:80(发布端口):
包经宿主机外部接口进入 → nat-PREROUTING 命中 fib daddr type local 跳入空的 nat-prerouting-and-output(不改写地址)→ 路由决策转发至 bridge1 → filter-FORWARD 按 oifname vmap 跳入 filter-forward-in__bridge1 → 依序经过连接跟踪放行、ICC 规则、ip daddr 192.0.2.2 tcp dport 80 accept 命中放行 → 送达容器。回程包由 ct state established,related 直接放行。
容器主动出网(访问外部):
filter-forward-out__bridge1 全放行 → nat-POSTROUTING 按 iifname vmap 跳入空的 nat-postrouting-out__bridge1,不做 masquerade,源地址保持 192.0.2.x 离开宿主机。从源码结构看,这要求外部路由能够回流 192.0.2.0/24 到该宿主机,否则容器出网将不可达——这也是 routed 模式适用前提是“容器子网可路由”的原因(filterDirectAccess 与 ICMP 规则的注释可佐证直连路由是预期能力,见 endpoint.go)。
如何验证与再生成这份文档
上述规则并非手写,而是由 nftablesdoc_linux_test.go 驱动的 golden 测试维护:
- 测试前置条件:firewalld 未运行、非 rootless、防火墙后端为 nftables(
testEnv.FirewallBackendDriver() == "nftables"),否则跳过; - 每个场景(section)在独立 netns 中启动 daemon,创建网络/容器后执行
nft -s list table ip docker-bridges,按 map/chain 切块填入模板(runNftables 函数),再与generated/下参考文件做 golden 比对; - 若引擎侧规则变更导致 diff,需要按包注释的流程检查 diff、更新对应模板描述,并以
TESTFLAGS='-update'重跑刷新参考文档(见 nftablesdoc_linux_test.go 包注释); - bridge 驱动 nftables 实现另有大量参数组合的单测 golden 文件,覆盖
gwm=nat / nat-unprotected / routed、hairpin、internal、icc、masquerade、SNAT 等维度,见 testdata/TestNftabler 目录,可从中检索gwm=routed相关的.golden文件进一步对照。
小结
routed 网关模式的 nftables 规则可以用四句话概括:不做 DNAT(发布端口变成容器 IP 端口的直连放行)、不做 masquerade(容器以自身源地址出网)、不阻止外部直连容器 IP(raw-PREROUTING 无 DROP DIRECT ACCESS)、并额外放行 ICMP;同时发布端口场景不再启动 userland proxy。排查 routed 网络连通性问题时,对照 usernet-portmap-routed.md 的完整表与 nftabler 实现 中的规则创建逻辑,可以快速定位“哪条规则该有而没有”或“哪条规则因网关模式不同而缺失”。需要再次提醒:这些规则结构属于开发期文档,Docker 各版本之间可能变化,请以本机 nft list table ip docker-bridges 的实际输出为准。
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