首页
/ Moby(Docker Engine)桥接网络端口映射的 iptables 规则详解:从 `-p 8080:80` 到 filter/nat/raw 三张表

Moby(Docker Engine)桥接网络端口映射的 iptables 规则详解:从 `-p 8080:80` 到 filter/nat/raw 三张表

2026-09-06 13:27:15作者:毕习沙Eudora

本文基于 Moby 仓库中自动生成的 integration/network/bridge/iptablesdoc 文档集,聚焦其中"用户自定义网络上带发布端口"这一场景,完整展示 docker run -p 8080:80 之后 iptables filter、nat、raw 三张表中的全部规则,并逐条对照 bridge 驱动源码 解释每条规则的产生时机与插入位置,帮助读者理解端口发布背后的 DNAT、MASQUERADE 与默认 DROP 机制,以及这套文档如何由集成测试自动生成并做 golden diff 校验。

1. 文档定位:这是怎样一份"活"的参考

integration/network/bridge/iptablesdoc/ 目录记录了 Docker Engine 在多种网络场景下实际写入 iptables/ip6tables 的规则。index.md 明确说明:

  • 这份文档仅供开发参考——docker 的 iptables(以及 ip6tables)规则结构会随版本变化,不是稳定接口;
  • 文档由测试 TestBridgeIptablesDoc 生成:测试启动一个真实 daemon,创建网络与容器,然后捕获 iptables 输出,再与各 section 对应的 text/template 合并渲染,最后与仓库中 generated/ 目录下的 golden 文件做 diff,规则有差异测试即失败;
  • ip6tables 规则与 iptables 模式相同,因此文档只展示 IPv4 规则;
  • 一个容易踩坑的事实:filter 表的 INPUT 链不被 Docker 使用——来自宿主物理网络或宿主本机的数据包因为是路由进 bridge 网络,命中的是 filter-FORWARD 链;同理 filter-OUTPUT 也不被使用。

该测试还说明了重启行为:bridge 驱动在初始化时会删除自建链并在网络恢复时重建规则,但 filter-FORWARD 链不会被清空,且网络重建顺序与原始创建顺序无关,因此 daemon 重启后规则排列可能不同;当 firewalld 运行时,其重载会清空 iptables 规则,daemon 通过 dbus 注册重载事件处理器来重建规则。

本文聚焦的场景文档是 generated/usernet-portmap.md,同系列还覆盖新 daemonloopback 发布端口无 userland proxy禁用 ICCinternal 网络routed 模式nat-unprotectedSwarm service 等场景。

2. 场景等价命令

文档描述的等价操作是:创建一个名为 bridge1 的用户自定义桥接网络(子网 192.0.2.0/24、网关 192.0.2.1),并在其上运行一个发布 80 端口到宿主 8080 的容器:

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

这个场景在测试代码 iptablesdoc_linux_test.goindex 变量中有声明(第 78~89 行):networks: bridge1 + containers: c1(80/tcp -> 8080),网络地址取自测试专用的 docNetworks 序列(192.0.2.0/24198.51.100.0/24203.0.113.0/24)。测试通过 createBridgeNetworks 用 daemon client 以 com.docker.network.bridge.namecom.docker.network.bridge.gateway_mode(默认 nat)等 option 创建网络,容器 IP 因此分配为 192.0.2.2

3. filter 表:该场景的完整规则

场景文档给出的 filter 表状态(iptables -vL --line-numbers -t 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

对应的 iptables -S -t filter 命令:

-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

3.1 规则链的转发路径

数据面视角下,一条从外部主机进入容器 80 端口的转发包会经历:

  1. filter-FORWARD 首先生效:DOCKER-USER 链为空(留给用户自定义规则),随后进入 DOCKER-FORWARD;
  2. DOCKER-FORWARD 依次跳入 DOCKER-CT(放行 conntrack 状态为 RELATED,ESTABLISHED 的回程/相关包,保证入站连接的双向放行不依赖 conntrack 之前的匹配)、DOCKER-INTERNAL(空)、DOCKER-BRIDGE(按出接口 docker0/bridge1 二次跳入 DOCKER 链),最后由按入接口排列的 ACCEPT(第 4、5 条)放行容器出站流量;
  3. DOCKER 链完成核心判定:每端口 ACCEPT 与每网络 DROP 的先后顺序决定了"只放行已发布端口"。

3.2 场景文档指出的三个关键变化

创建 bridge1 + 容器 c1 之后,相对新 daemon 状态(filter 表变化)要点如下,与原文档一致:

  • DOCKER-FORWARD 链中,针对新网络的出站 ACCEPT 规则(第 5 条,-i bridge1)被追加到链尾;
  • DOCKER-CTDOCKER-FORWARD 链各自为 bridge1 新增了一条规则;
  • DOCKER 链中出现一条对"路由到容器地址 192.0.2.2 的 TCP 80 端口包"的 ACCEPT 规则。这条规则是在容器创建时加入的(其余规则均在驱动初始化或网络创建时加入),即 setPerPortForwarding 的职责;
    • 每端口规则都插入链首,而每网络的 DROP 规则 setDefaultForwardRule 总是追加到链尾,所以 ACCEPT 一定先于 DROP 生效。本例中由于 docker0 先于 bridge1 创建,bridge1 的规则分别出现在 docker0 DROP 规则的上方与下方——这正是 DOCKER 链第 1~3 条排列(ACCEPT 80、DROP docker0、DROP bridge1)的由来。

源码上,setPerPortForwarding 构造的规则参数为 ! -i <bridge> -o <bridge> -p tcp -d <容器IP> --dport <端口> -j ACCEPT,并通过 programChainRule 以 "OPEN PORT" 标记插到 filter 表 DOCKER 链顶部;而 setDefaultForwardRule 对每个非 internal 网络追加 ! -i <bridge> -o <bridge> -j DROP(若网关模式为 nat-unprotected 则改为 ACCEPT),注释明确写道该 DROP 规则"必须位于插在链首的每端口 ACCEPT 规则之后"。

其余网络级规则来自 setupIPTables:DOCKER-CT 的 conntrack ACCEPT(ctRule)、DOCKER-BRIDGE 的按桥跳转(jumpToDockerRule),以及 setupNonInternalNetworkRules 中 ICC 开启时的出站规则 outRuleICC(-i <bridge> -j ACCEPT,即 DOCKER-FORWARD 第 5 条,标记为 "ACCEPT OUTGOING",追加到链尾)。

4. nat 表:DNAT 与 MASQUERADE

场景文档给出的 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            !loopback/8           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  192.0.2.0/24         anywhere
2        0     0 MASQUERADE all  --  any    !docker0  172.17.0.0/16        anywhere

Chain DOCKER (2 references)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DNAT       tcp  --  !bridge1 any     anywhere             anywhere             tcp dpt:http-alt to:192.0.2.2:80

对应的 iptables -S -t nat 命令:

-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 ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 192.0.2.0/24 ! -o bridge1 -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER ! -i bridge1 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80

各部分来源与含义:

  • PREROUTING/OUTPUT 的 dst-type LOCAL 跳转:由 addNATJumpRules 在驱动初始化时追加,确保发往宿主本地地址的包(包括本机 curl 127.0.0.1:8080)都会进入 nat-DOCKER 链做 DNAT 判定。OUTPUT 规则带 ! -d 127.0.0.0/8(非 hairpin 模式时排除回环地址);
  • DOCKER 链的 DNAT 规则:8080 -> 192.0.2.2:80,由 setPerPortNAT 在容器端口发布时创建。源码中有两个细节值得注意:
    • 宿主机绑定地址未指定时,写入规则用的是 0/0 而非 0.0.0.0——因为 "iptables 会把 0.0.0.0 解释为 0.0.0.0/32,而 0/0 才会被 iptables 与 ip6tables 一致地解释为任意值"(见 port.go 第 84~90 行);
    • ! -i bridge1 参数在 Hairpin=false 时追加(即默认情况),意味着从 bridge 接口本身进入的包不做 DNAT——容器之间或网关侧直连不会误触发端口重定向;
  • POSTROUTING 的 MASQUERADE 规则:-s <子网> ! -o <bridge> -j MASQUERADE 对每个启用了 Masquerade 的网络一条(本例为 192.0.2.0/24docker0172.17.0.0/16),来自 setupNonInternalNetworkRules。它把容器出站的源地址改写为主机对外地址,使回程流量经 conntrack 正确还原 DNAT;若用户指定了 host_ip,则改用 SNAT --to-source

至此完整链路为:外部包进入 PREROUTING -> nat-DOCKER 命中 DNAT 改写目的地址为 192.0.2.2:80 -> filter-FORWARD 经 DOCKER-BRIDGE/DOCKER 链因每端口 ACCEPT 放行 -> 路由至 bridge1 -> 回程包在 POSTROUTING 被 MASQUERADE,经 conntrack 还原后回到外部主机。

5. raw 表:屏蔽对容器地址的直连访问

场景文档给出的 raw 表状态:

Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DROP       all  --  !bridge1 any     anywhere             192.0.2.2

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination

对应的 iptables -S -t raw 命令:

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

这条规则由 filterDirectAccess 在端点(容器)创建时加入 raw-PREROUTING 链,作用是阻断远程主机绕过端口映射、直接路由到容器 IP 的访问——只有从 bridge 接口进入(-i bridge1)的合法流量放行,其余一律 DROP,且发生在 conntrack 之前。这与 filter-DOCKER 链里的每网络 DROP 形成双保险:raw 表拦截的是"直接路由到容器地址"的包(无论目的端口),filter 表 DROP 拦截的是经 NAT 后未被每端口规则放行的包。

从源码注释(endpoint.go 第 34~49 行)还可以读出几条适用边界:

  • 网络为 internalnat-unprotectedrouted 模式时不添加该规则;
  • daemon 级开启 direct routing 或通过 DOCKER_INSECURE_NO_IPTABLES_RAW=1 禁用 raw 规则(如内核缺少相应支持)时,规则同样会被跳过/删除,见 rawRulesDisabled;
  • TrustedHostInterfaces 列出的接口会额外插入 ACCEPT,使受信任接口与 bridge 本身同等对待;
  • 宿主对本机容器始终保留直连能力(来自 bridge 接口自身的包不受该 DROP 影响)。

此外,dropLegacyFilterDirectAccess 说明了一段版本演进:28.0.0 曾引入按端口的 filter 直连 DROP 规则;自 28.2.0 起改为不按端口的 raw-PREROUTING DROP(规则更少,且可与端点同时创建),旧规则在每次端口发布时被删除,该清理函数计划在后续版本移除。

6. 规则 -> 源码定位速查

场景中的规则 产生时机 源码位置
filter-DOCKER 每端口 ACCEPT(链首) 容器端口发布时 setPerPortForwarding
filter-DOCKER 每网络 DROP(链尾) 网络创建时 setDefaultForwardRule
nat-DOCKER 每端口 DNAT 容器端口发布时 setPerPortNAT
nat-POSTROUTING 每网络 MASQUERADE 网络创建时 setupNonInternalNetworkRules
nat-PREROUTING/OUTPUT dst-type LOCAL 跳转 驱动初始化时 addNATJumpRules
filter-DOCKER-CT / DOCKER-BRIDGE 规则 网络创建时 setupIPTables
filter-DOCKER-FORWARD 出站 ACCEPT 网络创建时 setupNonInternalNetworkRules
raw-PREROUTING 容器地址 DROP 端点创建时 filterDirectAccess

7. 文档生成机制:测试即文档

这套文档的可靠性来自 TestBridgeIptablesDoc 的完整流水线:

  1. 隔离环境:通过 networking.NewL3Segment 搭建 L3 测试段(192.168.124.0/24 与 IPv6 前缀),每个 section 一个独立 netns "宿主",并 ip link set eth0 down 降低随机报文干扰计数;
  2. 启动真实 daemon:在每个 netns 内以 busybox 镜像启动 daemon(禁用 OTEL 导出,swarm 场景附加 --swarm-default-advertise-addr);
  3. 执行场景:按 index 中声明的网络/容器/端口映射创建资源;
  4. 捕获 iptables:runIptablesiptables -Z 清零计数,再分别执行 iptables -vL --line-numbers -t filter/nat/rawiptables -S -t filter/nat/raw 六组命令(常量定义见 iptCmds),并用正则把 \d+ packets, \d+ bytes 统一替换为 0 packets, 0 bytes 以消除 CI 波动,然后缩进 4 空格嵌入 markdown;
  5. 模板渲染与 golden diff:generatetemplates/usernet-portmap.md 这类 Go text/template(其中 {{index . "LFilter4"}}{{index . "SNat4"}} 等占位符对应上述命令输出)渲染文档,golden.Assertgenerated/ 下同名文件比对,不一致则测试失败。

前置约束:测试在 firewalld 运行中、rootless 模式或 nftables 后端下会跳过(第 216~218 行)。规则变更后,按包注释流程:检查 diff 中的规则变化、更新对应模板文件的描述、再以 TESTFLAGS='-update' 重新运行以刷新 golden 文档。

8. 小结与使用提示

  • 阅读本文档集前请记住其免责声明:规则结构随版本变化,不属于稳定接口,不要基于具体规则顺序编写外部脚本;
  • 阅读 generated/ 下任何场景文档时,可先对照 index.md 的场景清单确认前置状态(例如 usernet-portmap 依赖 daemon 已有默认 docker0 网络,故 nat-POSTROUTING 中出现 172.17.0.0/16 的 MASQUERADE);
  • 端口发布的三张表分工可概括为:nat 表负责"改写"(DNAT/MASQUERADE),filter 表负责"放行与默认拒绝"(每端口 ACCEPT + 每网络 DROP + conntrack 回程),raw 表负责"在连接跟踪之前屏蔽容器地址直连";
  • 若想验证本地规则与文档一致,可执行 iptables -vL --line-numbers -t filter/nat/rawiptables -S 对照本文第 3~5 节;若规则顺序不同,优先检查是否发生过 daemon 重启或 firewalld 重载。
登录后查看全文
热门项目推荐
相关项目推荐