Moby 桥接网络:关闭 userland proxy 后发布端口的 iptables 规则全集与源码印证
本篇基于 Moby(Docker Engine)仓库中由集成测试自动生成的黄金文档 usernet-portmap-noproxy.md,完整解读「容器位于用户自定义 bridge 网络、发布了端口、且守护进程禁用 userland proxy」这一场景下,Docker 实际写入 iptables 的 filter 表与 nat 表规则。读完后,你不仅能拿到该场景的可复现命令与逐条规则清单,还能把每条差异规则对应到当前仓库中 daemon/libnetwork/drivers/bridge 下的具体源码位置。
文档从何而来:TestBridgeIptablesDoc 的黄金文件机制
integration/network/bridge/iptablesdoc/ 目录下的文档不是人工撰写的,而是由集成测试 TestBridgeIptablesDoc 生成的:测试启动一个独立的 dockerd、按预设场景创建网络与容器、抓取 iptables -vL/iptables -S 输出,再用 templates 中对应的 text/template 渲染成 Markdown,最后与 generated/ 目录中的黄金文件做 diff——规则一旦发生变化测试即失败。
本文对应的场景在测试索引中定义于 iptablesdoc_linux_test.go:noUserlandProxy: true,网络 bridge1(192.0.2.0/24),容器 c1 发布 80/tcp 到宿主 8080。测试在 runTestNet 中据此追加守护进程启动参数 --userland-proxy=false。抓取 iptables 前会先执行 iptables -Z 清零计数器,并用正则把包计数统一替换为 0 packets, 0 bytes(见 runIptables),保证黄金文件稳定可比。
需要注意的两点适用前提(均见 index.md 与测试头部的 skip 条件):
- 该文档仅供开发参考,Docker 的 iptables/ip6tables 规则结构不是稳定接口,会在版本间变化;
- 测试在 firewalld 运行中、rootless 模式或
nftables防火墙后端下会跳过(iptablesdoc_linux_test.go#L216-L218),因此以下规则以 iptables(legacy/nft 兼容模式)后端为准,且只展示 IPv4;ip6tables 规则遵循相同模式。
场景复现:等效操作命令
文档给出的等效操作序列为:
dockerd --userland-proxy=false
docker network create \
-o com.docker.network.bridge.name=bridge1 \
--subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
docker run --network bridge1 -p 8080:80 --name c1 busybox
其中 -o com.docker.network.bridge.name=bridge1 通过 bridge 驱动的网络选项指定网桥接口名为 bridge1,--subnet/--gateway 指定网段与网关(容器 c1 获得 192.0.2.2)。-p 8080:80 即在宿主 8080 端口建立到容器 80 端口的发布映射。
filter 表:与启用 userland proxy 时完全一致
文档指出:禁用 userland proxy 时,filter 表与启用 proxy 时相同(即 usernet-portmap.md 场景)。完整规则如下:
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER-USER all -- any any anywhere anywhere
2 0 0 DOCKER-FORWARD all -- any any anywhere anywhere
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain DOCKER (2 references)
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT tcp -- !bridge1 bridge1 anywhere 192.0.2.2 tcp dpt:http
2 0 0 DROP all -- !docker0 docker0 anywhere anywhere
3 0 0 DROP all -- !bridge1 bridge1 anywhere anywhere
Chain DOCKER-BRIDGE (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER all -- any docker0 anywhere anywhere
2 0 0 DOCKER all -- any bridge1 anywhere anywhere
Chain DOCKER-CT (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT all -- any docker0 anywhere anywhere ctstate RELATED,ESTABLISHED
2 0 0 ACCEPT all -- any bridge1 anywhere anywhere ctstate RELATED,ESTABLISHED
Chain DOCKER-FORWARD (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER-CT all -- any any anywhere anywhere
2 0 0 DOCKER-INTERNAL all -- any any anywhere anywhere
3 0 0 DOCKER-BRIDGE all -- any any anywhere anywhere
4 0 0 ACCEPT all -- docker0 any anywhere anywhere
5 0 0 ACCEPT all -- bridge1 any anywhere anywhere
Chain DOCKER-INTERNAL (1 references)
num pkts bytes target prot opt in out source destination
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-BRIDGE
-N DOCKER-CT
-N DOCKER-FORWARD
-N DOCKER-INTERNAL
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-FORWARD
-A DOCKER -d 192.0.2.2/32 ! -i bridge1 -o bridge1 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER ! -i docker0 -o docker0 -j DROP
-A DOCKER ! -i bridge1 -o bridge1 -j DROP
-A DOCKER-BRIDGE -o docker0 -j DOCKER
-A DOCKER-BRIDGE -o bridge1 -j DOCKER
-A DOCKER-CT -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-CT -o bridge1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-FORWARD -j DOCKER-CT
-A DOCKER-FORWARD -j DOCKER-INTERNAL
-A DOCKER-FORWARD -j DOCKER-BRIDGE
-A DOCKER-FORWARD -i docker0 -j ACCEPT
-A DOCKER-FORWARD -i bridge1 -j ACCEPT
要点解读:
filter-INPUT与filter-OUTPUT空链,Docker 不使用它们——从宿主物理网络进入或宿主本机发出的包,因被路由进 bridge 网络而命中filter-FORWARD(index.md 亦明确说明这一点);DOCKER-FORWARD是转发总入口,依次跳转DOCKER-CT(放行 conntrack 已建立/关联的回包)、DOCKER-INTERNAL、DOCKER-BRIDGE,最后以-i docker0/-i bridge1两条 ACCEPT 规则放行来自任一网桥的出站流量;DOCKER链中第一条ACCEPT ... ! -i bridge1 -o bridge1 -d 192.0.2.2 tcp dpt:http是为发布端口单独插入的放行规则(每端口一条),源码位置为 setPerPortForwarding——该函数注释说明这些每端口规则插入在链头、位于网络级 DROP 规则之前;随后的两条DROP ! -i <br> -o <br>则分别封锁docker0与bridge1的容器间直连(ICC 关闭时的默认行为)。
nat 表:发布端口场景下的关键规则
nat 表是禁用 proxy 场景下差异最大的部分,完整规则如下:
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER all -- any any anywhere anywhere ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER all -- any any anywhere anywhere ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 MASQUERADE all -- any bridge1 anywhere anywhere ADDRTYPE match src-type LOCAL
2 0 0 MASQUERADE all -- any !bridge1 192.0.2.0/24 anywhere
3 0 0 MASQUERADE all -- any docker0 anywhere anywhere ADDRTYPE match src-type LOCAL
4 0 0 MASQUERADE all -- any !docker0 172.17.0.0/16 anywhere
5 0 0 MASQUERADE tcp -- any any 192.0.2.2 192.0.2.2 tcp dpt:http
Chain DOCKER (2 references)
num pkts bytes target prot opt in out source destination
1 0 0 DNAT tcp -- any any anywhere anywhere tcp dpt:http-alt to:192.0.2.2:80
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -o bridge1 -m addrtype --src-type LOCAL -j MASQUERADE
-A POSTROUTING -s 192.0.2.0/24 ! -o bridge1 -j MASQUERADE
-A POSTROUTING -o docker0 -m addrtype --src-type LOCAL -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 192.0.2.2/32 -d 192.0.2.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -p tcp -m tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80
逐条说明:
- nat-PREROUTING / nat-OUTPUT → DOCKER 跳转:凡目的地址为本地(LOCAL)的包都进入 nat-DOCKER 链做 DNAT 匹配。注意 OUTPUT 链这条规则没有
! -d 127.0.0.0/8的排除条件——这正是禁用 proxy 的显著特征,见下文差异一; - nat-DOCKER 的 DNAT:
-p tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80,把宿主的 8080 重定向到容器 IP 的 80 端口。由于没有了 docker-proxy 这个"本地监听者",这条 DNAT 不携带! -i bridge1条件(启用 proxy 时是-A DOCKER ! -i bridge1 -p tcp --dport 8080 -j DNAT ...),见下文差异四; - POSTROUTING 规则 1/3(
-o bridge1/-o docker0+--src-type LOCAL):来自宿主本机、且经由网桥出口的流量做 MASQUERADE。这是 hairpin 模式专属,仅当 proxy 关闭时才安装(见下文差异三); - POSTROUTING 规则 2/4(
-s <网段> ! -o <br>):容器网段的出站流量做 MASQUERADE,实现容器访问外部世界的源地址伪装; - POSTROUTING 规则 5(
-s 192.0.2.2 -d 192.0.2.2 --dport 80 -j MASQUERADE):容器访问"自己"的发布端口时伪装源地址,避免发往自身 IP 的包因 conntrack 状态问题无法回程(hairpin 场景),见下文差异二。
与启用 userland proxy 场景的四大差异(逐条对照源码)
原文档列出了相对 启用 proxy 的场景 的四点差异。这些差异在源码中对应一个核心标志:Hairpin。在 bridge_linux.go 中,bridge 驱动构建防火墙配置时写作 Hairpin: !config.EnableProxy,且 firewaller.go 的注释直接说明:"Hairpin means the userland proxy will not be running."——即 关闭 userland proxy 等价于进入 hairpin 模式,以下四条差异全部由该布尔值驱动。
差异一:nat-OUTPUT 到 DOCKER 的跳转覆盖回环地址
启用 proxy 时,OUTPUT 跳转规则带有 ! -d 127.0.0.0/8(回环地址不进入 DOCKER 链,因为本地回环上的 8080 由 docker-proxy 进程直接接管);禁用 proxy 后该排除消失,-A OUTPUT -m addrtype --dst-type LOCAL -j DOCKER 对包括回环在内的所有本地目的地址生效。
源码印证在 addNATJumpRules:
output := iptables.Rule{IPVer: ipVer, Table: iptables.Nat, Chain: "OUTPUT", Args: []string{
"-m", "addrtype",
"--dst-type", "LOCAL",
"-j", dockerChain,
}}
if !hairpinMode {
output.Args = append(output.Args, "!", "--dst", loopbackAddress(ipVer))
}
只有非 hairpin 模式(proxy 启用)才追加 ! --dst 127.0.0.0/8(IPv6 下为 ! --dst ::1/128,见 loopbackAddress)。单元测试的黄金文件也验证了这一点:hairpin=false 的基线包含 -A OUTPUT ! -d 127.0.0.0/8 ...,而 hairpin=true 的基线中没有(见 testdata/cleaned,hairpin=false__iptables.golden 与 testdata/cleaned,hairpin=true__iptables.golden)。
差异二:新增"容器访问自身发布端口"的 MASQUERADE
即上文 nat-POSTROUTING 规则 5:-A POSTROUTING -s 192.0.2.2/32 -d 192.0.2.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE。它保证容器 c1 主动访问 192.0.2.2:80(即自己发布的端口映射回宿主)时,源地址被伪装,回包能正确路由回来。
源码印证在 setPerPortNAT:该函数为每个端口绑定追加 DNAT 规则,并构造一条 -s <容器IP> -d <容器IP> --dport <端口> -j MASQUERADE 的 POSTROUTING 规则,但其是否真正安装取决于 hairpin:
if err := appendOrDelChainRule(rule, "MASQUERADE", n.ipt.config.Hairpin && enable); err != nil {
即仅当 Hairpin == true(proxy 关闭)时才写入该规则。
差异三:POSTROUTING 中包含 LOCAL 源地址的 MASQUERADE
即上文规则 1/3:-o bridge1 -m addrtype --src-type LOCAL -j MASQUERADE 与 docker0 的同款规则。它把"宿主本机发出、经网桥出去"的流量也做伪装,配合差异一让宿主机直接以本地地址访问容器发布端口时(如 curl 127.0.0.1:8080 之后容器看到源是本机地址)仍能正常往返。
源码印证在 setupNonInternalNetworkRules:
hpNatArgs = []string{"-m", "addrtype", "--src-type", "LOCAL", "-o", n.config.IfName, "-j", "MASQUERADE"}
...
// In hairpin mode, masquerade traffic from localhost. If hairpin is disabled or if we're tearing down
// that bridge, make sure the iptables rule isn't lying around.
if err := programChainRule(hpNatRule, "MASQ LOCAL HOST", enable && n.ipt.config.Hairpin); err != nil {
注释明确写着"In hairpin mode, masquerade traffic from localhost"——同样是 enable && n.ipt.config.Hairpin 双重条件门控。
差异四:DNAT 规则不再限定来源网桥
启用 proxy 时,nat-DOCKER 的 DNAT 规则带 ! -i bridge1 条件(排除"从 bridge1 自己进来"的包,防止 hairpin 回环);禁用 proxy 后条件消失,规则变为对任意入接口生效的 -A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80。
源码印证仍在 setPerPortNAT:
if !n.ipt.config.Hairpin {
args = append(args, "!", "-i", n.config.IfName)
}
非 hairpin 模式才追加 ! -i <网桥名>。这与黄金文件基线的对照一致:hairpin=false 基线含 -A DOCKER ! -i ... --dport ... -j DNAT,hairpin=true 基线则无 ! -i 限定。
说明:原关联文档末尾引用的
setup_ip_tables_linux.go、port_mapping_linux.go等 permalinks 是指向旧提交的历史链接(index.md 亦声明"links are intended as hints")。在当前仓库中,对应逻辑已重构至daemon/libnetwork/drivers/bridge/internal/iptabler/包内,上文链接即为现行位置。
顺带观察:noproxy 场景为何没有 raw 表章节
对照启用 proxy 的 usernet-portmap.md,后者额外包含 raw 表:-A PREROUTING -d 192.0.2.2/32 ! -i bridge1 -j DROP,用于阻断外部对容器 IP 发布端口的"直连",迫使流量先经过 DNAT 交给 docker-proxy。而在 usernet-portmap-noproxy.md 及其模板中,raw 表章节整体不存在——从源码结构看,port.go 中相关逻辑的注释提到:当 proxy 处理该绑定时"无需进一步 iptables 规则",即 hairpin 模式下没有需要"让路"的本地代理进程,也就无需这条 raw-DROP。
适用前提与注意事项
- 规则顺序不稳定:bridge 驱动初始化时会删除自定义链,之后随网络恢复重建;网络重建顺序未必与创建顺序一致,因此守护进程重启后规则排列可能不同(index.md)。
- firewalld 交互:firewalld 重新加载会清空 iptables 规则,daemon 通过 dbus 注册 reload 事件处理器来重建规则;本文场景的生成测试也在 firewalld 运行时直接跳过。
- 仅覆盖 IPv4:ip6tables 规则遵循相同模式,文档只展示 IPv4 部分。
- 非稳定接口:以上链名、规则结构可能随版本变化,不要在生产中以这些规则作为长期契约。
相关场景文档(同目录黄金文件)
同一测试还覆盖了其余 bridge 场景,可与本文对照阅读:
- New daemon
- Container on a user-defined network, with a published port
- Container on a user-defined network, with a port published on a loopback address
- Container on a user-defined network with inter-container communication disabled, with a published port
- Container on a user-defined --internal network
- Container on a routed-mode network, with a published port
- Container on a nat-unprotected network, with a published port
- Swarm service, with a published port
配合 index.md 中关于初始化行为、firewalld 重建机制与 FORWARD 链语义的说明,可以完整掌握 Moby bridge 驱动的 iptables 行为全貌。
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