首页
/ Moby 中"循环地址上发布端口"的 iptables 机制:usernet-portmap-lo 场景与 filterPortMappedOnLoopback 源码解析

Moby 中"循环地址上发布端口"的 iptables 机制:usernet-portmap-lo 场景与 filterPortMappedOnLoopback 源码解析

2026-09-06 13:48:32作者:魏献源Searcher

在 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 定义创建网络与容器(本场景对应 networksHostIP: 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-USERDOCKER-FORWARD
  • DOCKER 链中有一条 ACCEPT 规则放行指向容器的已发布端口转发流量:
-A DOCKER -d 192.0.2.2/32 ! -i bridge1 -o bridge1 -p tcp -m tcp --dport 80 -j ACCEPT
  • 每张 bridge(docker0bridge1)各有一条兜底 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(即 c1bridge1 上获得的 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",
    }}
    ...
}

从源码结构看,该函数有几个明确的边界条件:

  1. 非 loopback 绑定不触发hostIP.IsLoopback() 为 false(如 0.0.0.0 发布)时直接返回——只有 -p 127.0.0.1:8080:80 这类写法才会追加 raw 规则;
  2. IPv6 是 no-ophostIP.To4() == nil 即跳过,注释说明 IPv6 loopback 地址不可路由,远端流量本就无法到达;
  3. DOCKER_INSECURE_NO_IPTABLES_RAW=1 可整体关闭 raw 规则rawRulesDisabled(ctx) 检查该环境变量(见 port.go)。关闭后远端直接访问容器 IP 与 loopback 端口将失去 raw 层的防护,变量名中的 INSECURE 已表明这是排障手段而非生产配置;
  4. WSL2 镜像网络特例:当 daemon 处于 WSL2Mirrored 模式时,会额外追加一条对 loopback0 接口的 ACCEPT 规则("ACCEPT MIRRORED"),让 WSL2 侧的"本地"流量先命中 ACCEPT,再落入 DROP 规则之前的位置生效;
  5. routed 模式不适用gw_mode=routed 网络不在宿主机上映射端口,自然无需过滤。

此外,同文件中的 dropLegacyFilterDirectAccessport.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 的路径是:

  1. 本地 socket 流量进入 OUTPUT 链,被 ! -d 127.0.0.0/8 条件排除在 DNAT 之外;
  2. 请求落到监听 127.0.0.1:8080 的 docker-proxy,由 proxy 转发至容器 IP 192.0.2.2:80
  3. 回程包经 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-OUTPUT127.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 等)的良好起点。

登录后查看全文
热门项目推荐
相关项目推荐