首页
/ Moby 桥接网络 iptables 规则全解:基于 TestBridgeIptablesDoc 生成的规则文档源码导读

Moby 桥接网络 iptables 规则全解:基于 TestBridgeIptablesDoc 生成的规则文档源码导读

2026-09-06 13:31:31作者:邓越浪Henry

Moby(Docker Engine)的 bridge 网络驱动在初始化、创建网络、发布端口时会在内核中写入大量 iptables/ip6tables 规则,这些规则决定了容器端口映射、网络隔离与内外通信的可达性。本文基于仓库中 bridge iptables 规则文档 逐场景展开:先讲清这套文档的生成与维护机制(它由集成测试 TestBridgeIptablesDoc 真实运行守护进程、创建网络与容器后捕获 iptables 快照生成),再逐条解读新守护进程、用户自定义网络端口映射、--internal 网络、routed 网关模式、Swarm ingress 等场景下 filter/nat/raw 三张表的具体规则,并结合源码定位每条规则的写入函数,帮助运维与内核网络方向的读者把“看到的 iptables 输出”与“moby 哪段代码产生的”对应起来。

文档定位:开发用途,规则结构不是稳定接口

规则文档 开头明确了两点前提,使用文档前必须了解:

  1. 仅用于开发用途:Docker 的 iptables(及 ip6tables)规则结构会在版本之间变化,不是稳定接口。任何依赖具体链名或规则顺序的外部工具、防火墙策略都应视为不受支持的用法。
  2. 文档是测试生成的:本文档由集成测试 TestBridgeIptablesDoc 生成——它启动一个真实的 dockerd,按需创建网络与容器,捕获 iptables -vL/iptables -S 输出,再用 text/template 逐节渲染成 Markdown。生成结果与仓库中 generated/ 下的“黄金参考文件”做 diff,一旦出现差异测试即失败(但模板本身的改动可能不被察觉)。
  3. 仅展示 IPv4 规则:ip6tables 规则与 iptables 规则遵循同样的模式,文档只展示 IPv4。

测试实现的完整逻辑可以在 iptablesdoc_linux_test.go 中看到,关键机制包括:

  • 场景清单即索引:包内变量 index(第 74 行起)定义了全部 9 个文档小节,每节声明网络(子网、网关、noICC/internal/gwMode 选项)与容器端口映射,例如 usernet-portmap.md 一节使用 192.0.2.0/24/192.0.2.1,容器映射 80/tcp → 8080
  • 隔离环境:通过 networking.NewL3Segment 为每个小节创建独立网络命名空间的“主机”,守护进程在各自 netns 中启动(--userland-proxy=false 等参数按小节追加),避免串扰;runIptables 会先 iptables -Z 清零计数、再用正则把“X packets, Y bytes”统一替换为 0 packets, 0 bytes,保证 golden 对比稳定。
  • golden 对比与更新:生成内容先写入 bundles/test-integration/TestBridgeIptablesDoc/,再与 generated/<name>.mdgolden.Assert;规则变更时先检查 diff,再更新对应 templates/<name>.md 的说明文字,最后以 TESTFLAGS='-update' 重新生成参考文档。
  • 跳过条件:firewalld 运行中、rootless 模式、防火墙后端为 nftables 时测试跳过——这正是文档只对“纯 iptables 后端”有效的适用前提。

另外,文档说明:bridge 驱动在初始化时(configure,见当前仓库 daemon/libnetwork/drivers/bridge/bridge_linux.go)会删除自建的自定义链,随后在恢复各网络时重新创建规则;但 filter-FORWARD 主链不会被清空。由于网络恢复顺序并非原始创建顺序,守护进程重启后规则的排列可能不同。当 firewalld 运行时,其 reload 事件(经 dbus 送达)会使 iptables 规则被清空,守护进程注册了 reload 事件处理器以重建规则。

还有一个排查网络问题时常用的事实:Docker 不使用 filter-INPUT 与 filter-OUTPUT 链。来自主机物理网络或主机自身的包被路由进桥接网络后,命中的是 filter-FORWARD 链。

场景一:全新守护进程的初始规则集(new-daemon)

完整规则见 generated/new-daemon.md。守护进程启动时创建自定义链,并为默认 bridge 网络(docker0)建立规则。

filter 表

Chain FORWARD (policy ACCEPT)
    1 DOCKER-USER     all -- any any
    2 DOCKER-FORWARD  all -- any any

Chain DOCKER (1 references)
    1 DROP  all -- !docker0 docker0

Chain DOCKER-BRIDGE
    1 DOCKER  all -- any docker0

Chain DOCKER-CT
    1 ACCEPT all -- any docker0  ctstate RELATED,ESTABLISHED

Chain DOCKER-FORWARD
    1 DOCKER-CT
    2 DOCKER-INTERNAL
    3 DOCKER-BRIDGE
    4 ACCEPT  all -- docker0 any

等价命令:

-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 ! -i docker0 -o docker0 -j DROP
-A DOCKER-BRIDGE -o docker0 -j DOCKER
-A DOCKER-CT -o docker0 -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

各链职责(按文档逐条解释):

  • FORWARD 主链的两条无条件跳转DOCKER-USER 由 libnetwork 的 setupUserChain 建立,Docker 不会往该链里加规则,它专门留给用户自定义规则,且基本保持在 FORWARD 最顶部(每新建一个网络时会删除重建该跳转,同时不影响其他网络流量);DOCKER-FORWARDsetupIPChains 建立。守护进程初始化完成后不再触碰这两条规则,用户可自由向 FORWARD 追加规则(执行在 Docker 规则之后)。
  • DOCKER-FORWARD 是 Docker filter 规则的第一级:初始规则插入链首、此后不再变动,逐网络规则则追加到链尾。第 4 条 ACCEPT -i docker0 是创建该网络时由 setupIPTablesInternal 加入的,放行任何通过 DOCKER 链与隔离链之后离开该网络的包。
  • DOCKER-CT:对发往各 bridge 网络的 RELATED,ESTABLISHED 流量提前 ACCEPT,每个 bridge 网络一条 conntrack 规则。
  • DOCKER-BRIDGE:每个 bridge 网络一条“跳入 DOCKER 链”的规则。
  • DOCKER 链:实现每个容器的按端口/协议过滤。此处只有一条来自 setDefaultForwardRule 的 DROP 规则——丢弃未起源于该网络却被路由到该网络的包。这意味着安全不依赖 FORWARD 主链的默认策略:即使 FORWARD 策略是 ACCEPT,未发布端口/协议的包仍会被丢弃。
  • DOCKER-INTERNAL:服务于 --internal 网络,新守护进程场景下为空。

文档还指出:上述 FORWARD 策略虽然是 ACCEPT,但 setupIPv4Forwarding 会在 sysctl net.ipv4.ip_forward 原本不是 1 而由守护进程在创建 IPv4 bridge 网络时代为设置时,把策略改为 DROP;IPv6 对 /proc/sys/net/ipv6/conf/{default,all}/forwarding 同理。

nat 表

Chain PREROUTING
    1 DOCKER  all -- any any   ADDRTYPE match dst-type LOCAL
Chain OUTPUT
    1 DOCKER  all -- any any   !loopback/8   ADDRTYPE match dst-type LOCAL
Chain POSTROUTING
    1 MASQUERADE all -- any !docker0  172.17.0.0/16
  • PREROUTING/OUTPUTADDRTYPE match dst-type LOCAL 的跳转让所有目的地是本机的流量进入 nat DOCKER 链(端口 DNAT 发生地)。注意 OUTPUT 侧排除了 loopback。
  • POSTROUTING 的 MASQUERADE 规则让 172.17.0.0/16 子网流量离开 docker0 时做源地址转换,这是容器访问外网的基础。

场景二:用户自定义网络 + 发布端口(usernet-portmap)

这是最典型的场景,等价命令见 generated/usernet-portmap.md

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

相对新守护进程的增量规则:

filter 表——每个新网络在 DOCKER-CT、DOCKER-BRIDGE、DOCKER-FORWARD 中各追加一条自己的规则;DOCKER 链头部插入端口 ACCEPT,尾部追加该网络的 DROP:

-A DOCKER -d 192.0.2.2/32 ! -i bridge1 -o bridge1 -p tcp --dport 80 -j ACCEPT
-A DOCKER ! -i bridge1 -o bridge1 -j DROP
-A DOCKER-BRIDGE -o bridge1 -j DOCKER
-A DOCKER-CT -o bridge1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-FORWARD -i bridge1 -j ACCEPT

文档强调的规则插入次序要点:

  • DOCKER-FORWARD 第 5 条(放行离开新网络的流量)追加到链尾
  • DOCKER 链的按端口 ACCEPT 在容器创建时加入(前面所有规则都在驱动/网络初始化期创建),并且按端口规则总是插入链首,保证排在始终追加在链尾的网络 DROP 规则之前。由于 docker0 先于 bridge1 创建,bridge1 的 ACCEPT 出现在 docker0 的 DROP 之上、bridge1 的 DROP 出现在其下。

nat 表——端口映射的 DNAT 与子网 MASQUERADE:

-A DOCKER ! -i bridge1 -p tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80
-A POSTROUTING -s 192.0.2.0/24 ! -o bridge1 -j MASQUERADE

raw 表——filterDirectAccess 在 raw-PREROUTING 加入一条 DROP,阻止远端绕过发布流程直连容器地址上的映射端口:

-A PREROUTING -d 192.0.2.2/32 ! -i bridge1 -j DROP

这些规则在源码中的对应写入点(从源码结构看,位于 daemon/libnetwork/drivers/bridge/internal/iptabler/ 包):

场景三:端口发布在 loopback 地址(usernet-portmap-lo)

generated/usernet-portmap-lo.md 对应 docker run -p 127.0.0.1:8080:80。结论简明:filter 表与 nat 表与默认 nat 模式完全相同(文档直接折叠引用),差别体现在访问路径上——发布地址绑定到 127.0.0.1 后,只有本机可经 loopback 到达映射端口。

场景四:禁用 userland proxy(usernet-portmap-noproxy)

dockerd --userland-proxy=false 启动后重复同一映射,见 generated/usernet-portmap-noproxy.mdfilter 表与启用 proxy 时完全一致,差异全部在 nat 表:

差异点 说明
OUTPUT 跳 DOCKER 不再排除 loopback ProgramChain 使 -A OUTPUT -m addrtype --dst-type LOCAL -j DOCKER 对回环地址也生效,本机到映射端口的流量直接走内核 DNAT
新增自访问 MASQUERADE -A POSTROUTING -s 192.0.2.2/32 -d 192.0.2.2/32 -p tcp --dport 80 -j MASQUERADE,覆盖容器访问自己在宿主机上的发布端口
LOCAL 源地址 MASQUERADE 进 POSTROUTING setupIPTablesInternal 加入 -o bridge1 -m addrtype --src-type LOCAL -j MASQUERADE 等规则
DNAT 规则无目的 bridge 条件 setPerPortNAT 生成的 -A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80 不再带 ! -i bridge1

这组差异解释了为何无 userland proxy 时端口映射完全依赖内核 conntrack/NAT 路径,也解释了用户自行加防火墙规则时两种模式行为不同的常见原因。

场景五:禁用容器间通信(usernet-portmap-noicc)

-o com.docker.network.bridge.enable_icc=false 创建网络后,见 generated/usernet-portmap-noicc.md。与 ICC=true 相比,DOCKER-FORWARD 尾部用两条规则替换了原来单条离开网络的 ACCEPT:

-A DOCKER-FORWARD -i bridge1 -o bridge1 -j DROP     # 由 setIcc 加入:网络内自环流量丢弃
-A DOCKER-FORWARD -i bridge1 ! -o bridge1 -j ACCEPT # 由 setupIPTablesInternal 加入:出向放行

从源码结构看,这条 ICC 控制规则由 setIcc 写入——它是 bridge 驱动里 enable_icc 选项落到 iptables 的唯一入口,其余(nat、raw)规则与默认场景一致。

场景六:--internal 网络(usernet-internal)

两个 --internal 网络(一个允许 ICC、一个禁用)的规则见 generated/usernet-internal.md,等价命令:

docker network create -o com.docker.network.bridge.name=bridgeICC \
  --internal --subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
docker network create -o com.docker.network.bridge.name=bridgeNoICC \
  -o com.docker.network.bridge.enable_icc=true --internal \
  --subnet 198.51.100.0/24 --gateway 198.51.100.1 bridge1

与有外网访问的网络相比的关键差异:

  1. DOCKER-FORWARD 中没有该网络出向流量的 ACCEPT 规则;
  2. DOCKER 链中没有该网络的规则;
  3. DOCKER-INTERNAL 链按子网双向封锁,每个 --internal 网络两条规则:
-A DOCKER-INTERNAL ! -s 198.51.100.0/24 -o bridgeNoICC -j DROP  # 目的在网内、源不在网内 → 丢
-A DOCKER-INTERNAL ! -d 198.51.100.0/24 -i bridgeNoICC -j DROP # 源在网内、目的不在网内 → 丢

规则 1 丢弃“被路由进该网络但源地址不在子网内”的包,规则 2 丢弃“从该网络路由出去但目的地址不在子网内”的包——由此实现与外部网络的双向隔离。而 bridgeICCbridgeNoICC 之间唯一的区别就在 DOCKER-FORWARD 一条规则:ICC 开启时桥内回环包 ACCEPT(-i bridgeICC -o bridgeICC -j ACCEPT),禁用时 DROP(-i bridgeNoICC -o bridgeNoICC -j DROP)。nat 表中同样没有这些网络的 MASQUERADE/DNAT 规则,印证 internal 网络不参与端口发布。

场景七:routed 与 nat-unprotected 网关模式

generated/usernet-portmap-routed.md 对应创建网络时指定 -o com.docker.network.bridge.gateway_mode_ipv4=routed(测试代码中通过 bridge.IPv4GatewayMode 选项注入,见 iptablesdoc_linux_test.go 第 316 行附近)。与 nat 模式相比,filter 表 DOCKER 链中多出一条 ICMP 放行:

-A DOCKER -o bridge1 -p icmp -j ACCEPT

routed 模式下容器直接路由而非 NAT,网关需要放行 ICMP 以维持路径健康检查与可达性探测;其余按端口 ACCEPT/DROP 结构不变。nat-unprotected 模式(gateway_mode_ipv4=nat-unprotected,见 generated/usernet-portmap-natunprot.md)则对应“不做 SNAT 保护”的网关形态,测试用例以同样的子网/端口映射模板生成,便于横向对比三种网关模式的规则差集。

场景八:Swarm 服务发布端口(swarm-portmap)

docker service create -p 8080:80 busybox top 的规则见 generated/swarm-portmap.md。ingress 负载均衡落地在 docker_gwbridge 网关端点上,规则形态与用户网络高度同构:

-A DOCKER -d 172.18.0.2/32 ! -i docker_gwbridge -o docker_gwbridge \
     -p tcp --dport 8080 -j ACCEPT
-A DOCKER-FORWARD -i docker_gwbridge -o docker_gwbridge -j DROP
-A DOCKER-FORWARD -i docker_gwbridge ! -o docker_gwbridge -j ACCEPT

测试源码中的注释给出了一处值得注意的实现细节(iptablesdoc_linux_test.go 第 392-400 行):ingress 端口是作为普通端口映射发布在负载均衡 sandbox 的 docker_gwbridge 网关端点上的,且发生在任务容器启动后的几毫秒内,因此测试通过轮询 iptables -t nat -S DOCKER 中出现 DNAT 规则来判定 ingress 就绪。

参考路径汇总

类型 路径
文档入口 integration/network/bridge/iptablesdoc/index.md
生成测试 integration/network/bridge/iptablesdoc/iptablesdoc_linux_test.go
测试环境 integration/network/bridge/iptablesdoc/main_linux_test.go
黄金参考 generated/(9 个场景 md)
渲染模板 templates/(9 个同名 md 模板)
建链实现 daemon/libnetwork/drivers/bridge/internal/iptabler/iptabler.gosetupIPChains
网络级规则 daemon/libnetwork/drivers/bridge/internal/iptabler/network.gosetDefaultForwardRulesetIcc
端口级规则 daemon/libnetwork/drivers/bridge/internal/iptabler/port.gosetPerPortForwarding

适用前提提醒:本文所有规则快照仅在 iptables(非 nftables)后端、非 rootless、无 firewalld 干扰的环境中成立;规则排列顺序在守护进程重启后可能变化;由于规则结构本身不是稳定接口,生产环境的防火墙集成应以“DOCKER-USER 链 + 用户自定义规则”这一官方预留扩展点为准,而不是解析具体规则内容。

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