首页
/ Moby 桥接网络:关闭 userland proxy 后发布端口的 iptables 规则全集与源码印证

Moby 桥接网络:关闭 userland proxy 后发布端口的 iptables 规则全集与源码印证

2026-09-06 12:53:09作者:谭伦延

本篇基于 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.gonoUserlandProxy: 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-INPUTfilter-OUTPUT 空链,Docker 不使用它们——从宿主物理网络进入或宿主本机发出的包,因被路由进 bridge 网络而命中 filter-FORWARDindex.md 亦明确说明这一点);
  • DOCKER-FORWARD 是转发总入口,依次跳转 DOCKER-CT(放行 conntrack 已建立/关联的回包)、DOCKER-INTERNALDOCKER-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> 则分别封锁 docker0bridge1 的容器间直连(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.goldentestdata/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 DNAThairpin=true 基线则无 ! -i 限定。

说明:原关联文档末尾引用的 setup_ip_tables_linux.goport_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 场景,可与本文对照阅读:

配合 index.md 中关于初始化行为、firewalld 重建机制与 FORWARD 链语义的说明,可以完整掌握 Moby bridge 驱动的 iptables 行为全貌。

登录后查看全文
热门项目推荐
相关项目推荐