首页
/ Moby Bridge 网络端口映射 iptables 规则详解:禁用 Userland Proxy 的 Hairpin NAT 场景

Moby Bridge 网络端口映射 iptables 规则详解:禁用 Userland Proxy 的 Hairpin NAT 场景

2026-09-06 13:58:08作者:何将鹤

本篇技术文章基于 Moby(Docker Engine 上游项目)仓库中 integration/network/bridge/iptablesdoc/templates/usernet-portmap-noproxy.md 文档模板及其自动生成的规则快照,完整解析「用户自定义 bridge 网络 + 发布端口 + 禁用 userland proxy」场景下 Docker 在 iptables 中生成的全部规则。读完后,你将能够独立读懂并验证 Docker Engine 在禁用用户态代理后,如何通过 hairpin(发夹式)NAT 规则让宿主机直接访问容器发布端口,并理解每条规则对应的源码实现位置。

文档背景:这是一套自动生成的 iptables 快照

该模板位于仓库 integration/network/bridge/iptablesdoc/ 目录,属于 Docker Engine 的 iptables 使用文档套件的一部分。根据 index.md 的说明,这套文档由测试用例 TestBridgeIptablesDoc 自动生成:测试会真实启动一个 dockerd 守护进程,按场景创建网络和容器,然后抓取 iptables 输出,再把抓取结果与 templates/ 目录下的文本模板(text/template)合并,最后与 generated/ 目录中的产物做 diff——如果生成的规则与仓库中的快照不一致,测试就会失败。

原文档同时给出两点重要前提,这里必须继承:

  • 仅供开发参考,不是稳定接口:Docker 的 iptables(以及 ip6tables)规则结构会在版本之间变化,不属于稳定接口,不应依赖具体规则布局编写防火墙脚本。
  • ip6tables 规则与 iptables 模式一致:文档只展示 IPv4 规则。
  • 规则顺序不保证稳定:bridge 驱动在初始化时会删除自建链并在网络恢复时重建规则(见 bridge_linux.goconfigure),而 filter 表的 FORWARD 链不会被清空,且网络重建顺序与创建顺序不同,因此守护进程重启后规则排列可能变化。firewalld 重载会清空 iptables,守护进程通过 dbus 监听其重载事件并重建规则。
  • filter 表的 INPUT/OUTPUT 链不被 Docker 使用:来自宿主机物理网络或宿主机自身的包,会被路由进 bridge 网络后命中 filter 表的 FORWARD 链。

当前仓库中,本场景的模板 usernet-portmap-noproxy.md 与生成产物 usernet-portmap-noproxy.md 一一对应,生成产物内嵌了真实的 iptables 表格,本文据此展开。仓库中还有同族场景文档可对照阅读:带 userland proxy 的 usernet-portmap.md、发布到 loopback 的 usernet-portmap-lo.md、禁用容器间通信的 usernet-portmap-noicc.md 等。

场景搭建:禁用 userland proxy 后发布端口

文档定义的操作步骤等价于以下命令:

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

要点说明:

  • --userland-proxy=false:禁用 userland proxy。启用时,Docker 会为每个发布端口启动一个用户态代理进程(docker-proxy),由它监听宿主机端口并把连接转发进容器;禁用后,端口发布完全依赖内核的 DNAT(hairpin NAT)路径,宿主机访问 192.0.2.2:80(或宿主机 IP 的 8080 端口)的包直接由 iptables 完成地址转换。
  • -o com.docker.network.bridge.name=bridge1:把自定义网络绑定到名为 bridge1 的网桥设备,便于规则中直观看到网桥名。
  • --subnet 192.0.2.0/24 --gateway 192.0.2.1:使用文档约定的 192.0.2.0/24 测试网段(TEST-NET-1),容器 c1 得到地址 192.0.2.2
  • 端口映射 -p 8080:80:宿主机 8080 → 容器 80(tcp)。

在源码层面,「是否启用 userland proxy」直接影响 iptables 的配置结构。从 bridge_linux.go 第 199 行可以看到,防火墙配置中的 Hairpin 字段就是由 proxy 开关取反得到的:

Hairpin: !config.EnableProxy,

firewaller.go 第 27~28 行的注释明确解释了二者的等价关系:

// Hairpin means the userland proxy will not be running.
Hairpin bool

也就是说,禁用 userland proxy 等价于开启 hairpin 模式:发布端口的 DNAT/MASQUERADE 规则需要让「宿主机自己发出的包」也能被转换和转发,这正是下文 nat 表差异的根源。

filter 表:与启用 userland proxy 时完全相同

原文档结论:filter 表与启用 userland proxy 时一致——用户态代理只影响 nat 表(以及一个 proxy 进程本身),不改变过滤规则。生成产物中捕获到的完整 filter 表如下(保留原文全貌):

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

对应规则的含义(与源码 port.gosetPerPortForwarding 的构造逻辑一致):

  • DOCKER 链第 1 条:-A DOCKER -d 192.0.2.2/32 ! -i bridge1 -o bridge1 -p tcp -m tcp --dport 80 -j ACCEPT —— 以容器 IP 192.0.2.2 为目的地址、从网桥 bridge1 出去的 80 端口包放行,实现发布端口的「开放端口」访问;
  • DOCKER 链第 2、3 条 DROP:进入 docker0 / bridge1 的包若未从该网桥进入则丢弃,防止绕过上述放行规则直接访问网桥;
  • DOCKER-CT:对已建立/相关连接(RELATED,ESTABLISHED)出网桥方向直接放行,加速返回路径;
  • DOCKER-FORWARDDOCKER-CT → DOCKER-INTERNAL → DOCKER-BRIDGE 的固定顺序串联各子链,最后无条件放行从 docker0bridge1 进入的包。

nat 表:hairpin 模式下的完整规则

这是本场景的核心。原文档给出的完整 nat 表如下:

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

等价的 iptables 命令行(原文档以折叠块形式提供,此处完整保留):

-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

结合源码可以逐条定位这些规则的来源:

  • PREROUTING/OUTPUT → DOCKER 的跳转规则:由 iptabler.go 中的 addNATJumpRules 编程(约第 194 行),是否覆盖 loopback 正是由 Hairpin(即无 proxy)开关决定的,见下文差异一。
  • POSTROUTING 的 LOCAL 源地址 MASQUERADE-A POSTROUTING -o bridge1 -m addrtype --src-type LOCAL -j MASQUERADEnetwork.go 第 306 行写入,标记为 MASQ LOCAL HOST,且仅在 Hairpin 为真时生效(enable && n.ipt.config.Hairpin)——这就是原文档所称的 setupIPTablesInternal(旧代码路径,现已重构进 internal/iptabler 包)行为。
  • 每端口 DNAT + hairpin MASQUERADE:由 port.gosetPerPortNAT(第 79~121 行)生成,对应差异三与差异四。

与启用 userland proxy 时的四项差异

原文档的核心结论部分列出了与 usernet-portmap 场景(userland proxy 启用)的 4 点差异。下面逐条继承并结合当前仓库源码展开。

差异一:OUTPUT 链对 loopback 地址也会跳转到 DOCKER 链

原文:The jump from the OUTPUT chain to DOCKER happens even for loopback addresses.

nat 表中的 -A OUTPUT -m addrtype --dst-type LOCAL -j DOCKER 意味着:宿主机进程访问本机任意本地地址(包括 127.0.0.1:8080 这种 loopback 目标)时,包在 OUTPUT 阶段就进入 DOCKER 链做 DNAT。启用 proxy 时,127.0.0.1:8080 上的监听者是 proxy 进程本身,内核不需要为 loopback 目标做 DNAT,因此该跳转被限制;禁用 proxy 后没有任何用户态监听者,只有靠内核 DNAT 才能让 curl 127.0.0.1:8080 到达容器,所以规则放开到 loopback。

差异二:容器访问自己发布端口时的 MASQUERADE

原文:A MASQUERADE rule is added for packets sent from the container to one of its own published ports on the host.

对应 POSTROUTING 第 5 条:

-A POSTROUTING -s 192.0.2.2/32 -d 192.0.2.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE

这是典型的 hairpin NAT 需求:容器 c1192.0.2.2)访问宿主机映射的 8080 端口时,包经 DNAT 后源/目的都是 192.0.2.2:80,若不把源地址伪装掉,容器网络栈无法正确完成回环路径。该规则在 port.go 第 109~118 行构造,且明确以 n.ipt.config.Hairpin && enable 为条件——只有禁用 userland proxy 时才会被写入,与原文档结论完全吻合。

差异三:POSTROUTING 中包含 LOCAL 源地址的 MASQUERADE

原文:A MASQUERADE rule for packets from a LOCAL source address is included in POSTROUTING.

对应 POSTROUTING 第 1 条(针对 bridge1,另有 docker0 的同类规则第 3 条):

-A POSTROUTING -o bridge1 -m addrtype --src-type LOCAL -j MASQUERADE

它的作用:当宿主机自身发出的包(源地址属于宿主机 LOCAL 地址)被 DNAT 进容器后,出网桥时再做一次 MASQUERADE,把源地址改成网关 192.0.2.1,容器回包才能被路由回来。启用 proxy 时宿主机流量由 proxy 进程发起并被单独处理,此规则不必要;hairpin 模式下则是必需。其源码位置见 network.go 第 306 行(MASQ LOCAL HOST 标记,同样受 Hairpin 门控)。

差异四:DOCKER 链的 DNAT 规则不再限定目的网桥

原文:In the DOCKER chain's DNAT rule, there's no destination bridge.

对比两种场景的最终 DNAT 规则:

  • 有 proxy:-A DOCKER ! -i bridge1 -p tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80(带 ! -i bridge1,排除从 bridge1 进入的包);
  • 无 proxy:-A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80没有接口条件)。

源码依据在 port.go 第 98~100 行:

if !n.ipt.config.Hairpin {
    args = append(args, "!", "-i", n.config.IfName)
}

Hairpin=false(有 proxy)时才追加 ! -i <网桥名> 排除条件。去掉该条件后,从 bridge1 进来的包(例如容器访问宿主机映射端口的 hairpin 流量)同样会命中 DNAT,这正是差异二、三共同支撑的「无代理自访问」路径的入口。

小结与验证方式

本场景的关键在于理解一个源码级事实:在 Moby 的 bridge 驱动内部,「禁用 userland proxy」与「开启 Hairpin」是同一个布尔量的两面bridge_linux.go 第 199 行 Hairpin: !config.EnableProxyfirewaller.go 第 27 行注释)。由此派生出 nat 表的全部差异:

差异点 有 userland proxy 无 userland proxy(本文场景)
OUTPUT → DOCKER 跳转 不覆盖 loopback 覆盖 loopback
容器访问自身发布端口 无对应规则 增加源/目的均为容器 IP 的 MASQUERADE
POSTROUTING LOCAL 源 MASQUERADE 每个网桥一条 -m addrtype --src-type LOCAL
DOCKER 链 DNAT ! -i <网桥> 条件 无接口条件,hairpin 流量也命中 DNAT
filter 表 相同 相同

验证方式(与文档生成机制一致):在具备 iptables 权限的 Linux 环境按「场景搭建」一节操作后,执行 iptables-save -t filteriptables-save -t nat,与 生成产物 中的表格逐条比对;仓库内的 TestBridgeIptablesDoc 测试(入口见 iptablesdoc_linux_test.go)即以此机制在 CI 中守护规则快照。单元测试层面的规则编程行为(含 hairpin 开关的对照结果)可参考 iptabler_test.go 中按 hairpin=%v 生成的 golden 文件。

最后重申原文档的两条边界:该规则结构是开发参考而非稳定接口,跨版本不可依赖;ip6tables 规则模式与 iptables 相同,本文只展示 IPv4。

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