news 2026/9/5 17:50:44

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Moby Docker Engine iptables 规则剖析:容器端口发布到回环地址的安全隔离机制

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

【免费下载链接】mobyThe Moby Project - a collaborative project for the container ecosystem to assemble container-based systems项目地址: https://gitcode.com/GitHub_Trending/mo/moby

本文以 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 条:对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.md 是text/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 全貌。

【免费下载链接】mobyThe Moby Project - a collaborative project for the container ecosystem to assemble container-based systems项目地址: https://gitcode.com/GitHub_Trending/mo/moby

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!