Moby Docker Engine iptables 规则剖析:容器端口发布到回环地址的安全隔离机制
本文以 Moby 仓库中 TestBridgeIptablesDoc 集成测试自动生成的文档 usernet-portmap-lo.md 为主体,完整解读“用户自定义 bridge 网络上、容器端口发布到 127.0.0.1 回环地址”这一场景下 Docker Engine 生成的 filter、nat、raw 三张 iptables 表;并结合 iptabler/port.go 与 nftabler/port.go 的源码,说明 filterPortMappedOnLoopback 是如何在 raw-PREROUTING 链中拦截远程流量,确保“只能从本机访问”的承诺在内核层面真正落地。
场景定义:端口只发布到回环地址
该文档描述的场景等价于以下两条命令:创建一个名为 bridge1 的用户自定义 bridge 网络(子网 192.0.2.0/24、网关 192.0.2.1),再在该网络上运行容器 c1,并把容器 80 端口发布到主机的 127.0.0.1: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 127.0.0.1:8080:80 --name c1 busybox
与普通 -p 8080:80(绑定 0.0.0.0,对全网卡开放)不同,127.0.0.1:8080:80 的语义是“仅本机可访问”。但 iptables 的 DNAT 规则本身只是把 127.0.0.1:8080 的流量重写到容器 IP(本例中为 192.0.2.2:80),并不会主动阻止从外部网卡到达的、目的地址恰好是 127.0.0.1 的报文——如果远程主机构造这类报文,在缺少额外防护时可能绕过“仅本机”的约定。Moby 的解法就是在 raw 表 PREROUTING 链中追加 DROP 规则,在连接跟踪之前就丢弃这类远程报文。这正是本文档相对 普通 NAT 模式文档 多出来的核心差异。
filter 表:与普通 NAT 模式完全一致
原始文档指出:本场景的 filter 表和 nat 表与 nat mode 场景完全相同。以下是完整快照(容器启动后、无实际流量时的状态):
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
关键规则逐条解读:
DOCKER链第 1 条:-d 192.0.2.2/32 ! -i bridge1 -o bridge1 -p tcp --dport 80 -j ACCEPT,即“转发给容器 192.0.2.2:80、但不从 bridge1 进入(也就是经 DNAT 改写的流量)”的报文放行。这条 per-port 放行规则由 iptabler/port.go 中的setPerPortForwarding插入,注释中明确说明:每端口 ACCEPT 规则必须位于该网络创建时追加的 per-network DROP 规则之前。DOCKER链第 2、3 条:对docker0和bridge1各有一条“默认 DROP”规则(! -i xxx -o xxx -j DROP),配合上面的 per-port ACCEPT,实现“未发布端口一律禁止从主机侧直达容器”的最小化暴露。DOCKER-CT链放行与容器网桥相关的RELATED,ESTABLISHED回包;DOCKER-USER空链保留给用户自定义规则。
与回环地址相关的部分在 filter 表中没有任何特殊规则——回环限制完全由后面的 raw 表承担。
nat 表:DNAT 到容器,MASQUERADE 出网
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 localhost 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 ! -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 -d 127.0.0.1/32 ! -i bridge1 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80
回环场景在 nat 表中的体现是 DOCKER 链中这条 DNAT 规则:
-A DOCKER -d 127.0.0.1/32 ! -i bridge1 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80
对应源码位于 iptabler/port.go 的 setPerPortNAT:当 HostIP 不是通配地址(本例为 127.0.0.1)时,-d 参数取 b.HostIP.String() 精确匹配该地址;! -i bridge1 条件则在未开启 hairpin 时排除从本桥进入的流量(n.ipt.config.Hairpin 为 false 时追加该匹配,见源码第 98–100 行)。POSTROUTING 中两条 MASQUERADE 负责容器子网出网伪装,属于所有 bridge 网络的公共规则,与本场景无特殊关系。
raw 表:本场景的独有差异
文档末尾展示的是 raw 表快照与等价的 iptables 命令:
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
2 0 0 DROP tcp -- !lo any anywhere localhost tcp dpt:http-alt
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
-P PREROUTING ACCEPT
-P OUTPUT ACCEPT
-A PREROUTING -d 192.0.2.2/32 ! -i bridge1 -j DROP
-A PREROUTING -d 127.0.0.1/32 ! -i lo -p tcp -m tcp --dport 8080 -j DROP
两条 DROP 规则各管一事:
-d 192.0.2.2/32 ! -i bridge1 -j DROP:凡是目的为容器 IP 192.0.2.2、但没有从容器所在桥bridge1进入的报文(即绕过桥、经其他接口直连容器地址的“routed direct access”)在 PREROUTING 阶段直接丢弃。从源码结构看,iptabler/port.go 的dropLegacyFilterDirectAccess中有一段版本演进说明:此类“直连容器 IP 的流量一律在 raw-PREROUTING 丢弃”的规则自 28.2.0 起不再按端口创建,而是在端点创建时统一建立,本函数只负责清理 28.0.x 时代遗留的旧规则。-d 127.0.0.1/32 ! -i lo -p tcp --dport 8080 -j DROP:这就是本文档的主角——凡是目的为 127.0.0.1:8080、但入口不是回环接口lo的报文,直接 DROP。由于 raw 表在 netfilter 处理链中位于 conntrack 与 filter/nat 之前,远程报文在建立连接跟踪、进入 DNAT 之前就被丢弃,“端口仅绑定回环地址”的语义由此得到内核级强制保证。
源码实现:filterPortMappedOnLoopback
原始文档最后一句点明机制来源:filterPortMappedOnLoopback 会向 raw-PREROUTING 链追加一条 DROP 规则,丢弃发往“发布在回环地址上的端口”的远程流量。仓库中该函数有两个后端实现,分别对应 iptables 与 nftables 防火墙。
iptables 后端
iptabler/port.go 中的实现:
// filterPortMappedOnLoopback adds an iptables rule that drops remote
// connections to ports mapped on loopback addresses.
//
// This is a no-op if the portBinding is for IPv6 (IPv6 loopback address is
// non-routable), or over a network with gw_mode=routed (PBs in routed mode
// don't map ports on the host).
func filterPortMappedOnLoopback(ctx context.Context, b types.PortBinding, hostIP net.IP, wsl2Mirrored, enable bool) error {
if rawRulesDisabled(ctx) {
return nil
}
if b.HostPort == 0 || !hostIP.IsLoopback() || hostIP.To4() == nil {
return nil
}
...
}
要点:
- 触发条件:只有
HostPort != 0(NAT 生效)、HostIP是回环地址且为 IPv4 时才添加规则;IPv6 回环不可路由、gw_mode=routed网络不在主机上映射端口,因此均为 no-op。这与文档注释一致。 - DROP 规则:
-p tcp -d <hostIP> --dport <hostPort> ! -i lo -j DROP,即文档 raw 表第 2 条规则的来源,注释标记为"LOOPBACK FILTERING - DROP"。 - WSL2 mirrored 特例:在
wsl2Mirrored模式下会先追加一条-i loopback0 -j ACCEPT("LOOPBACK FILTERING - ACCEPT MIRRORED"),允许 WSL2 镜像网络经loopback0接口进入的回环流量,再落到 DROP 规则上——规则顺序保证 ACCEPT 优先。 - 逃生开关:
rawRulesDisabled检查环境变量DOCKER_INSECURE_NO_IPTABLES_RAW=1,设置后跳过所有 raw 规则(含本条 DROP 与直连过滤),变量名中的 “INSECURE” 表明这是不推荐的调试手段。 - 该函数在
setPerPortIptables中为每个端口绑定调用(第 50 行),先于 NAT 与 FORWARD 规则的写入;删除端口时以enable=false走同样的匹配删除逻辑(appendOrDelChainRule)。
nftables 后端
nftabler/port.go 中语义完全对应的实现:
updater(nftables.Rule{
Chain: rawPreroutingChain,
Group: rawPreroutingPortsRuleGroup,
Rule: []string{
`iifname != lo ip daddr`, pb.HostIP.String(), pb.Proto.String(),
"dport", strconv.Itoa(int(pb.HostPort)),
`counter drop comment "DROP REMOTE LOOPBACK"`,
},
})
nftables 版本将规则放入统一的 rawPreroutingChain 并按组管理,counter drop comment "DROP REMOTE LOOPBACK" 与 iptables 版的 LOOPBACK FILTERING - DROP 一一对应;WSL2 特例同样存在("ACCEPT WSL2 LOOPBACK")。setPerPortRules(第 61–77 行)中四个规则族的写入顺序为:转发放行 → DNAT → hairpin 伪装 → 回环过滤,与 iptables 后端的调用次序保持同一逻辑。
文档如何生成:TestBridgeIptablesDoc 集成测试
这份“生成文件”(首行标注 <!-- This is a generated file; DO NOT EDIT. -->)并非手写,而是由 integration/network/bridge/iptablesdoc/iptablesdoc_linux_test.go 中的 TestBridgeIptablesDoc 自动生成并与仓库内 golden 文件比对。流程如下:
- 场景声明:测试文件顶部的
index列出所有场景小节。本场景的声明位于第 176–186 行:name: "usernet-portmap-lo.md",容器c1的端口映射为80/tcp→HostIP=127.0.0.1, HostPort=8080,与文档开头的docker run -p 127.0.0.1:8080:80完全对应;网络 IPAM 固定使用192.0.2.0/24 / 192.0.2.1(docNetworks/docGateways变量,第 50–52 行),保证快照中的地址可复现。 - 真实运行:为每个小节创建独立 L3 网段与网络命名空间,在其中启动 dockerd,按声明创建 bridge 网络与容器(
createBridgeNetworks通过 API 指定com.docker.network.bridge.name、子网、网关及--internal、--icc=false、gw_mode等选项),然后runIptables先iptables -Z清零计数器,再采集 filter/nat/raw 三张表的-vL --line-numbers与-S输出,并用正则把包计数统一替换为 0 以保证可比性。 - 模板渲染:templates/usernet-portmap-lo.md 是
text/template,用{{index . "LFilter4"}}、{{index . "LNat4"}}、{{index . "LRaw4"}}、{{index . "SRaw4"}}等占位符嵌入各表输出——这也解释了为什么生成文件里 filter/nat/raw 三块内容呈现“列表 +-S命令”成对出现的形式。 - Golden 比对:渲染结果先写入 bundles 目录,再与 generated/usernet-portmap-lo.md 做 golden 断言;任何规则差异都会导致测试失败,强制维护者在规则变更时同步更新模板描述。测试包注释说明:确认 diff 属于预期变更后,用
TESTFLAGS='-update'重跑即可刷新参考文档。 - 运行前提:测试在 firewalld 运行、rootless 或 nftables 防火墙后端下会自动跳过(第 216–218 行)。另外 index.md 明确提示:Docker 的 iptables 结构是开发用途、非稳定接口,版本之间规则会变化,本文所引用的规则快照仅对应当前仓库版本。
小结
- 在“端口发布到 127.0.0.1”的场景下,filter 表与 nat 表和普通端口发布场景一致:DNAT 把
127.0.0.1:8080改写到192.0.2.2:80,filter 的DOCKER链做 per-port 放行与默认 DROP。 - 真正的差异在 raw-PREROUTING:
filterPortMappedOnLoopback追加的-d 127.0.0.1/32 ! -i lo -p tcp --dport 8080 -j DROP规则在连接跟踪建立前就丢弃远程回环流量,使“仅本机可访问”从约定变为强制。 - 该机制对 IPv6 回环、
gw_mode=routed不生效(no-op),可通过DOCKER_INSECURE_NO_IPTABLES_RAW=1整体禁用 raw 规则(不推荐)。 - 上述全部快照来自
TestBridgeIptablesDoc的真实运行采集与 golden 文件比对,可通过 integration/network/bridge/iptablesdoc/ 目录下的其余场景文档(new-daemon、usernet-portmap、usernet-internal、swarm-portmap 等)对照理解 Moby 在各类网络配置下的 iptables 全貌。
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 StartedRust0622
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