Moby(Docker Engine)桥接网络端口映射的 iptables 规则详解:从 `-p 8080:80` 到 filter/nat/raw 三张表
本文基于 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,同系列还覆盖新 daemon、loopback 发布端口、无 userland proxy、禁用 ICC、internal 网络、routed 模式、nat-unprotected和 Swarm 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.go 的 index 变量中有声明(第 78~89 行):networks: bridge1 + containers: c1(80/tcp -> 8080),网络地址取自测试专用的 docNetworks 序列(192.0.2.0/24、198.51.100.0/24、203.0.113.0/24)。测试通过 createBridgeNetworks 用 daemon client 以 com.docker.network.bridge.name、com.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 端口的转发包会经历:
- filter-FORWARD 首先生效:
DOCKER-USER链为空(留给用户自定义规则),随后进入DOCKER-FORWARD; - DOCKER-FORWARD 依次跳入
DOCKER-CT(放行 conntrack 状态为RELATED,ESTABLISHED的回程/相关包,保证入站连接的双向放行不依赖 conntrack 之前的匹配)、DOCKER-INTERNAL(空)、DOCKER-BRIDGE(按出接口docker0/bridge1二次跳入DOCKER链),最后由按入接口排列的ACCEPT(第 4、5 条)放行容器出站流量; - DOCKER 链完成核心判定:每端口
ACCEPT与每网络DROP的先后顺序决定了"只放行已发布端口"。
3.2 场景文档指出的三个关键变化
创建 bridge1 + 容器 c1 之后,相对新 daemon 状态(filter 表变化)要点如下,与原文档一致:
- 在
DOCKER-FORWARD链中,针对新网络的出站 ACCEPT 规则(第 5 条,-i bridge1)被追加到链尾; DOCKER-CT和DOCKER-FORWARD链各自为bridge1新增了一条规则;DOCKER链中出现一条对"路由到容器地址192.0.2.2的 TCP 80 端口包"的 ACCEPT 规则。这条规则是在容器创建时加入的(其余规则均在驱动初始化或网络创建时加入),即 setPerPortForwarding 的职责;- 每端口规则都插入链首,而每网络的 DROP 规则 setDefaultForwardRule 总是追加到链尾,所以 ACCEPT 一定先于 DROP 生效。本例中由于
docker0先于bridge1创建,bridge1的规则分别出现在docker0DROP 规则的上方与下方——这正是 DOCKER 链第 1~3 条排列(ACCEPT 80、DROP docker0、DROP bridge1)的由来。
- 每端口规则都插入链首,而每网络的 DROP 规则 setDefaultForwardRule 总是追加到链尾,所以 ACCEPT 一定先于 DROP 生效。本例中由于
源码上,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/24与docker0的172.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 行)还可以读出几条适用边界:
- 网络为
internal、nat-unprotected或routed模式时不添加该规则; - 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 的完整流水线:
- 隔离环境:通过
networking.NewL3Segment搭建 L3 测试段(192.168.124.0/24 与 IPv6 前缀),每个 section 一个独立 netns "宿主",并ip link set eth0 down降低随机报文干扰计数; - 启动真实 daemon:在每个 netns 内以 busybox 镜像启动 daemon(禁用 OTEL 导出,swarm 场景附加
--swarm-default-advertise-addr); - 执行场景:按 index 中声明的网络/容器/端口映射创建资源;
- 捕获 iptables:runIptables 先
iptables -Z清零计数,再分别执行iptables -vL --line-numbers -t filter/nat/raw与iptables -S -t filter/nat/raw六组命令(常量定义见 iptCmds),并用正则把\d+ packets, \d+ bytes统一替换为0 packets, 0 bytes以消除 CI 波动,然后缩进 4 空格嵌入 markdown; - 模板渲染与 golden diff:generate 用 templates/usernet-portmap.md 这类 Go
text/template(其中{{index . "LFilter4"}}、{{index . "SNat4"}}等占位符对应上述命令输出)渲染文档,golden.Assert与generated/下同名文件比对,不一致则测试失败。
前置约束:测试在 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/raw与iptables -S对照本文第 3~5 节;若规则顺序不同,优先检查是否发生过 daemon 重启或 firewalld 重载。
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 StartedRust0623
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