Moby 中"循环地址上发布端口"的 iptables 机制:usernet-portmap-lo 场景与 filterPortMappedOnLoopback 源码解析
在 Moby(Docker Engine 上游项目)的 bridge 网络中,把容器端口发布到 127.0.0.1(如 docker run -p 127.0.0.1:8080:80)是最常见的"仅本机可达"用法。本文以仓库自带的 iptables 文档场景 usernet-portmap-lo 为主体,完整还原该场景下 filter/nat/raw 三张表的规则,并结合 filterPortMappedOnLoopback 的源码实现,说明"远端流量为何到不了 loopback 上已发布端口"的底层机制。读完后你可以:独立解读 Docker 生成的 raw 表 DROP 规则、在排障时确认规则是否存在,以及理解 IPv6、routed 模式、WSL2 等边界条件下的行为差异。
1. 场景定义:一条 loopback 端口发布命令
仓库中该场景的文档模板是 usernet-portmap-lo.md,其定义的等价操作为:
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
即:创建一个名为 bridge1 的用户自定义 bridge 网络(子网 192.0.2.0/24,网关 192.0.2.1),再在该网络上运行容器 c1,并把容器 80 端口发布到宿主机的 127.0.0.1:8080。
模板文档明确指出:filter 表与 nat 表和普通 nat 模式(-p 8080:80)完全相同,见 usernet-portmap 场景;真正的新增内容在 raw 表——filterPortMappedOnLoopback 会在 raw-PREROUTING 链追加一条规则,DROP 发往该 loopback 已发布端口的远端流量。这是本文的核心。
该文档由集成测试 TestBridgeIptablesDoc 自动生成:测试启动一个真实的 dockerd,按 iptablesdoc_linux_test.go 中的 section 定义创建网络与容器(本场景对应 networks 中 HostIP: 127.0.0.1, HostPort: 8080 的 port mapping),抓取 iptables -vL/-S 输出后套用模板,与 generated/usernet-portmap-lo.md 这份 golden 文件比对,不一致则测试失败。因此下文引用的规则集是当前仓库版本"可验证的实现事实"。
2. filter 表:与 nat 模式一致,无特殊处理
生成的 filter 表中与本场景相关的要点(摘自 generated/usernet-portmap-lo.md):
FORWARD链首尾挂接DOCKER-USER与DOCKER-FORWARD;DOCKER链中有一条 ACCEPT 规则放行指向容器的已发布端口转发流量:
-A DOCKER -d 192.0.2.2/32 ! -i bridge1 -o bridge1 -p tcp -m tcp --dport 80 -j ACCEPT
- 每张 bridge(
docker0、bridge1)各有一条兜底 DROP:
-A DOCKER ! -i docker0 -o docker0 -j DROP
-A DOCKER ! -i bridge1 -o bridge1 -j DROP
DOCKER-CT链对两张 bridge 放行RELATED,ESTABLISHED状态的回程包。
这些规则由 per-port 规则与 per-network 规则共同组成:per-port 的 ACCEPT 插入链头,per-network 的 DROP 追加链尾,因此先建的网络(docker0)的规则会出现在后建网络(bridge1)规则的上、下两侧。loopback 场景不改变这一部分。
3. nat 表:DNAT 只由"非 loopback 流量"触发
nat 表同样是与 nat 模式相同的两条链:nat-PREROUTING 在目的地址为 LOCAL 时跳转 DOCKER 链,nat-OUTPUT 则显式排除 loopback 段:
-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 DOCKER -d 127.0.0.1/32 ! -i bridge1 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80
DOCKER 链中的 DNAT 规则把 127.0.0.1:8080 改写为容器地址 192.0.2.2:80(即 c1 在 bridge1 上获得的 IP)。注意 OUTPUT 链的 ! -d 127.0.0.0/8 条件:宿主机本地发起、目的为 127.0.0.1:8080 的流量不会经过这条 DNAT 规则,它由用户态的 docker-proxy 监听并转发到容器——这就是 loopback 发布与 0.0.0.0 发布在数据路径上的关键差异:前者本地走 proxy,远端理论上走 DNAT(但下一节的 raw 规则会直接丢弃远端流量)。
POSTROUTING 中为每个 bridge 子网保留 MASQUERADE 规则:
-A POSTROUTING -s 192.0.2.0/24 ! -o bridge1 -j MASQUERADE
4. raw 表:filterPortMappedOnLoopback 追加的 DROP 规则
场景新增的全部 iptables 命令如下(raw 表):
-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
两条规则分工明确:
| 规则 | 作用 | 来源 |
|---|---|---|
-d 192.0.2.2/32 ! -i bridge1 -j DROP |
丢弃绕过网关、直接发往容器 IP 的远端流量(所有非 bridge1 入口的包) | 直接访问过滤(direct access filtering),自 28.2.0 起统一为按 endpoint 一条规则 |
-d 127.0.0.1/32 ! -i lo -p tcp --dport 8080 -j DROP |
丢弃从非 loopback 接口进入、目的为 127.0.0.1:8080 的远端流量 |
filterPortMappedOnLoopback(本场景的核心) |
第二条规则正是模板文档所描述的"extra rule"。它挂在 raw 表的 PREROUTING 链上,意味着在路由判定与 nat 表处理之前就生效:远端主机若把包发往 127.0.0.1:8080(例如经第三方路由或地址欺骗到达),会在 conntrack 与 DNAT 介入前被直接丢弃,保证 loopback 上发布的端口在行为上严格"仅本机可达"。
5. 源码级实现:filterPortMappedOnLoopback
规则的实现位于 bridge 驱动的 iptables 后端 daemon/libnetwork/drivers/bridge/internal/iptabler/port.go,由 setPerPortIptables 在每个端口绑定的规则装配阶段最先调用:
// 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
}
// ... WSL2 镜像模式下的 ACCEPT(loopback0 接口)...
drop := iptables.Rule{IPVer: iptables.IPv4, Table: iptables.Raw, Chain: "PREROUTING", Args: []string{
"-p", b.Proto.String(),
"-d", hostIP.String(),
"--dport", strconv.Itoa(int(b.HostPort)),
"!", "-i", "lo",
"-j", "DROP",
}}
...
}
从源码结构看,该函数有几个明确的边界条件:
- 非 loopback 绑定不触发:
hostIP.IsLoopback()为 false(如0.0.0.0发布)时直接返回——只有-p 127.0.0.1:8080:80这类写法才会追加 raw 规则; - IPv6 是 no-op:
hostIP.To4() == nil即跳过,注释说明 IPv6 loopback 地址不可路由,远端流量本就无法到达; DOCKER_INSECURE_NO_IPTABLES_RAW=1可整体关闭 raw 规则:rawRulesDisabled(ctx)检查该环境变量(见 port.go)。关闭后远端直接访问容器 IP 与 loopback 端口将失去 raw 层的防护,变量名中的INSECURE已表明这是排障手段而非生产配置;- WSL2 镜像网络特例:当 daemon 处于 WSL2Mirrored 模式时,会额外追加一条对
loopback0接口的 ACCEPT 规则("ACCEPT MIRRORED"),让 WSL2 侧的"本地"流量先命中 ACCEPT,再落入 DROP 规则之前的位置生效; - routed 模式不适用:
gw_mode=routed网络不在宿主机上映射端口,自然无需过滤。
此外,同文件中的 dropLegacyFilterDirectAccess(port.go)说明第一条 raw 规则(按容器 IP DROP)的历史:28.0.0 曾按"每端口"生成 direct-access DROP,28.2.0 起收敛为按 endpoint 一条规则。
仓库同时提供 nftables 后端,其等价实现在 daemon/libnetwork/drivers/bridge/internal/nftabler/port.go:同样仅处理 IPv4 loopback 绑定,生成 iifname != lo ... counter drop comment "DROP REMOTE LOOPBACK" 规则,WSL2 场景生成 counter accept comment "ACCEPT WSL2 LOOPBACK"。两套后端语义一致,只是规则表达形式不同。
6. 本地访问为何仍然可用
远端流量被 raw 表丢弃后,本机访问 127.0.0.1:8080 的路径是:
- 本地 socket 流量进入
OUTPUT链,被! -d 127.0.0.0/8条件排除在 DNAT 之外; - 请求落到监听
127.0.0.1:8080的 docker-proxy,由 proxy 转发至容器 IP192.0.2.2:80; - 回程包经
DOCKER-CT链的RELATED,ESTABLISHED规则放行。
仓库中与该行为相关的回归测试包括 port_mapping_linux_test.go 中的 loopback 端口映射用例(如 TestMixAnyWithSpecificHostAddrs 混用 0.0.0.0/127.0.0.1 绑定、TestPortMappingOnDistinctLoopbackAddrs 验证两个容器发布到不同 loopback 地址时的路由正确性),以及 raw 规则本身的 golden 比对(-S -t raw 输出断言)。
7. 适用前提与注意事项
- 该文档集合(index.md)明确声明"仅供开发用途":Docker 的 iptables/ip6tables 规则结构不是稳定接口,会随版本变化;且文档仅在非 firewalld、非 rootless、iptables 后端(非 nftables 驱动)环境下生成,
TestBridgeIptablesDoc会在这些情况下跳过; - 只展示 IPv4 规则;ip6tables 遵循相同模式;
- daemon 重启后,bridge 驱动会删除并重建自定义链,filter-FORWARD 中的网络规则顺序可能与创建顺序不同,因此规则的顺序不应作为依赖前提;
- 若启用 nftables 防火墙后端,规则以 nftables 形式表达(见上文第 5 节的 nftabler 实现),
iptables -S抓取的文档不适用。
8. 小结
usernet-portmap-lo 场景揭示了一个简洁的设计:loopback 端口发布在 filter 与 nat 层面完全复用普通端口映射的规则,仅在 raw 表追加一条 ! -i lo -j DROP 的早期丢弃规则,配合 nat-OUTPUT 对 127.0.0.0/8 的排除与 docker-proxy 的本地转发,共同实现"仅宿主机自身可达"的语义。这条规则由 filterPortMappedOnLoopback 生成,只对 IPv4 loopback 绑定生效,并可通过 DOCKER_INSECURE_NO_IPTABLES_RAW=1 在排障时临时关闭——理解这些边界,是阅读 Moby bridge 网络 iptables 文档的其余场景(no-proxy、no-ICC、routed、nat-unprotected 等)的良好起点。
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 StartedRust0625
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