首页
/ Moby Docker Engine iptables 规则剖析:容器端口发布到回环地址的安全隔离机制

Moby Docker Engine iptables 规则剖析:容器端口发布到回环地址的安全隔离机制

2026-09-04 12:39:22作者:冯爽妲Honey

本文以 Moby 仓库中 TestBridgeIptablesDoc 集成测试自动生成的文档 usernet-portmap-lo.md 为主体,完整解读“用户自定义 bridge 网络上、容器端口发布到 127.0.0.1 回环地址”这一场景下 Docker Engine 生成的 filter、nat、raw 三张 iptables 表;并结合 iptabler/port.gonftabler/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 条:对 docker0bridge1 各有一条“默认 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 规则各管一事:

  1. -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 时代遗留的旧规则。
  2. -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 文件比对。流程如下:

  1. 场景声明:测试文件顶部的 index 列出所有场景小节。本场景的声明位于第 176–186 行name: "usernet-portmap-lo.md",容器 c1 的端口映射为 80/tcpHostIP=127.0.0.1, HostPort=8080,与文档开头的 docker run -p 127.0.0.1:8080:80 完全对应;网络 IPAM 固定使用 192.0.2.0/24 / 192.0.2.1docNetworks/docGateways 变量,第 50–52 行),保证快照中的地址可复现。
  2. 真实运行:为每个小节创建独立 L3 网段与网络命名空间,在其中启动 dockerd,按声明创建 bridge 网络与容器(createBridgeNetworks 通过 API 指定 com.docker.network.bridge.name、子网、网关及 --internal--icc=falsegw_mode 等选项),然后 runIptablesiptables -Z 清零计数器,再采集 filter/nat/raw 三张表的 -vL --line-numbers-S 输出,并用正则把包计数统一替换为 0 以保证可比性。
  3. 模板渲染templates/usernet-portmap-lo.mdtext/template,用 {{index . "LFilter4"}}{{index . "LNat4"}}{{index . "LRaw4"}}{{index . "SRaw4"}} 等占位符嵌入各表输出——这也解释了为什么生成文件里 filter/nat/raw 三块内容呈现“列表 + -S 命令”成对出现的形式。
  4. Golden 比对:渲染结果先写入 bundles 目录,再与 generated/usernet-portmap-lo.md 做 golden 断言;任何规则差异都会导致测试失败,强制维护者在规则变更时同步更新模板描述。测试包注释说明:确认 diff 属于预期变更后,用 TESTFLAGS='-update' 重跑即可刷新参考文档。
  5. 运行前提:测试在 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-PREROUTINGfilterPortMappedOnLoopback 追加的 -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 全貌。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384