首页
/ Moby 桥接网络 nftables 规则详解:回环地址端口映射下的 DROP REMOTE LOOPBACK 规则是如何生成的

Moby 桥接网络 nftables 规则详解:回环地址端口映射下的 DROP REMOTE LOOPBACK 规则是如何生成的

2026-09-06 14:51:40作者:魏献源Searcher

本文以 Moby(Docker Engine)仓库中 integration/network/bridge/nftablesdoc/templates/usernet-portmap-lo.md 这份自动生成文档模板为核心,完整解析"容器运行在用户定义网络、且端口发布到回环地址(127.0.0.1)"这一场景下 ip docker-bridges nftables 表的全貌:为什么绝大多数规则与普通宿主地址端口映射相同、raw-PREROUTING 中多出来的 DROP REMOTE LOOPBACK 规则长什么样、它由哪段源码生成、以及如何在本地复现与验证这些规则。

这份文档从哪里来:nftablesdoc 的生成机制

usernet-portmap-lo.md 并非手写文档,而是由集成测试 TestBridgeNftablesDoc 生成的"黄金参考"文档之一。总览文档 明确说明了生成方式:

This document is generated by TestBridgeNftablesDoc by running a daemon, creating networks and containers, and capturing nftables. The nftables are then merged with a text/template for each section.

测试 nftablesdoc_linux_test.go 的工作流程是:

  1. 在独立的网络命名空间中启动一个 dockerd 实例;
  2. 按照 测试索引 中的 section 描述创建网络并运行容器。回环端口映射场景对应的配置为:
{
    name: "usernet-portmap-lo.md",
    networks: []networkDesc{{
        name: "bridge1",
        containers: []ctrDesc{
            {
                name:         "c1",
                portMappings: networktypes.PortMap{networktypes.MustParsePort("80/tcp"): {{HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: "8080"}}},
            },
        },
    }},
},
  1. 执行 nft -s list table ip docker-bridges 抓取完整规则集,并把它拆成"整表"与各 chain/map 块(以块首行为键,例如 chain raw-PREROUTING),存入 map[string]string
  2. 将该数据填充到 templates/usernet-portmap-lo.md 这个 text/template 模板中,生成 generated/usernet-portmap-lo.md
  3. golden.Assert 将生成结果与仓库中的 golden 文件做 diff,规则有任何变化测试即失败。

因此这份文档中展示的每条规则都是"真实 daemon 在真实内核上产生的规则",与手工整理文档相比具备很强的事实性。同时 index.md 也给出重要前提:

  • 该文档仅面向开发用途(development use),Docker 的 nftables 规则结构不是稳定接口,会在版本间变化;
  • IPv6 规则与 IPv4 遵循相同模式,只是位于不同的表(ip docker-bridgesip6 docker-bridges),文档中只展示 IPv4 规则;
  • docker-bridges 表在每次 Docker 启动时都会重建;
  • Docker 不使用 filter-INPUTfilter-OUTPUT 钩子:从宿主物理网络或宿主自身到达的包会被路由进桥接网络,因此走的是 filter-FORWARD 链。

测试的运行前提(从测试源码的 skip 条件看):系统上没有 firewalld 接管防火墙、非 rootless 模式、且 daemon 的防火墙后端驱动为 nftables

场景复现:把容器端口发布到 127.0.0.1

模板文档给出的等价命令行操作是:

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 是一个用户定义桥接网络,子网 192.0.2.0/24、网关 192.0.2.1(测试使用 RFC 5737 文档地址段 192.0.2.0/24,避免与真实网段冲突);
  • -p 127.0.0.1:8080:80 将容器 80/tcp 端口发布到宿主的回环地址 8080 端口,而非所有地址;
  • 容器 c1 会获得 192.0.2.2 这一容器内网地址。

0.0.0.0:8080:80(发布到所有宿主地址,即 usernet-portmap.md 描述的场景)相比,这条映射的语义是:只允许宿主本机通过 127.0.0.1:8080 访问容器,外部主机不应能到达该端口。这个语义就是后续 raw-PREROUTING 中那条额外 drop 规则存在的目的。

完整规则表:ip docker-bridges

生成文档中的完整规则集(Ruleset4)如下,这是"添加 bridge1 网络并运行带回环端口映射的容器"之后内核中的最终状态:

table ip docker-bridges {
    map filter-forward-in-jumps {
        type ifname : verdict
        elements = { "docker0" : jump filter-forward-in__docker0,
                     "bridge1" : jump filter-forward-in__bridge1 }
    }

    map filter-forward-out-jumps {
        type ifname : verdict
        elements = { "docker0" : jump filter-forward-out__docker0,
                     "bridge1" : jump filter-forward-out__bridge1 }
    }

    map nat-postrouting-in-jumps {
        type ifname : verdict
        elements = { "docker0" : jump nat-postrouting-in__docker0,
                     "bridge1" : jump nat-postrouting-in__bridge1 }
    }

    map nat-postrouting-out-jumps {
        type ifname : verdict
        elements = { "docker0" : jump nat-postrouting-out__docker0,
                     "bridge1" : jump nat-postrouting-out__bridge1 }
    }

    chain filter-FORWARD {
        type filter hook forward priority filter; policy accept;
        oifname vmap @filter-forward-in-jumps
        iifname vmap @filter-forward-out-jumps
    }

    chain nat-OUTPUT {
        type nat hook output priority dstnat; policy accept;
        ip daddr != 127.0.0.0/8 fib daddr type local counter jump nat-prerouting-and-output
    }

    chain nat-POSTROUTING {
        type nat hook postrouting priority srcnat; policy accept;
        iifname vmap @nat-postrouting-out-jumps
        oifname vmap @nat-postrouting-in-jumps
    }

    chain nat-PREROUTING {
        type nat hook prerouting priority dstnat; policy accept;
        fib daddr type local counter jump nat-prerouting-and-output
    }

    chain nat-prerouting-and-output {
        iifname != "bridge1" ip daddr 127.0.0.1 tcp dport 8080 counter dnat to 192.0.2.2:80 comment "DNAT"
    }

    chain raw-PREROUTING {
        type filter hook prerouting priority raw; policy accept;
        ip daddr 192.0.2.2 iifname != "bridge1" counter drop comment "DROP DIRECT ACCESS"
        iifname != "lo" ip daddr 127.0.0.1 tcp dport 8080 counter drop comment "DROP REMOTE LOOPBACK"
    }

    chain filter-forward-in__docker0 {
        ct state established,related counter accept
        iifname "docker0" counter accept comment "ICC"
        counter drop comment "UNPUBLISHED PORT DROP"
    }

    chain filter-forward-out__docker0 {
        ct state established,related counter accept
        counter accept comment "OUTGOING"
    }

    chain nat-postrouting-in__docker0 {
    }

    chain nat-postrouting-out__docker0 {
        oifname != "docker0" ip saddr 172.17.0.0/16 counter masquerade comment "MASQUERADE"
    }

    chain filter-forward-in__bridge1 {
        ct state established,related counter accept
        iifname "bridge1" counter accept comment "ICC"
        ip daddr 192.0.2.2 tcp dport 80 counter accept
        counter drop comment "UNPUBLISHED PORT DROP"
    }

    chain filter-forward-out__bridge1 {
        ct state established,related counter accept
        counter accept comment "OUTGOING"
    }

    chain nat-postrouting-in__bridge1 {
    }

    chain nat-postrouting-out__bridge1 {
        oifname != "bridge1" ip saddr 192.0.2.0/24 counter masquerade comment "MASQUERADE"
    }
}

各部分职责(结合生成文档与 nftabler.go 中的注释可以相互印证):

  • 四个 verdict map + filter-FORWARD / nat-POSTROUTING 两条基础链:基础链本身只有"按接口名查 map、跳转"的规则。这样与 Docker 无关的包不会遍历任何 per-network 规则;进出某个桥接网络的包只经过该网络自己的链。这是 Moby nftables 设计区别于 iptables 时代逐链串行检查的关键优化。
  • nat-PREROUTING / nat-OUTPUT:通过 fib daddr type local(目的地址是本机路由可达地址)跳转到公共的 nat-prerouting-and-output 链执行 DNAT;nat-OUTPUT 中的 ip daddr != 127.0.0.0/8 条件用于跳过 hairpin 未启用时宿主自身到 127.0.0.1 的流量。
  • raw-PREROUTING:filter 类型、raw 优先级的 prerouting 链,本场景中承担两条 drop 规则(下节详述)。
  • per-network 的 filter-forward-in__*:先放行 established,related 状态,再放行桥内互通(ICC),再放行已发布容器端口,最后 dropUNPUBLISHED PORT DROP)兜底。
  • per-network 的 nat-postrouting-out__*:容器出站流量做 masquerade,实现容器访问外网。

与普通宿主地址端口映射的差异:一条额外的 raw 规则

模板文档的核心论点只有一句话(templates/usernet-portmap-lo.md):

Most rules are the same as for a port published to a regular host address. But, there's an extra rule in raw-PREROUTING to drop remote traffic destined to the port mapped on the loopback address.

把本场景的 ip docker-bridges 表与 usernet-portmap.md(发布到所有宿主地址的对照场景)的规则逐条对比,差异恰好只有一处——raw-PREROUTING 链多出的第二行:

chain raw-PREROUTING {
    type filter hook prerouting priority raw; policy accept;
    ip daddr 192.0.2.2 iifname != "bridge1" counter drop comment "DROP DIRECT ACCESS"
    iifname != "lo" ip daddr 127.0.0.1 tcp dport 8080 counter drop comment "DROP REMOTE LOOPBACK"
}

DROP REMOTE LOOPBACK 规则逐字段解读

iifname != "lo" ip daddr 127.0.0.1 tcp dport 8080 counter drop comment "DROP REMOTE LOOPBACK" 的含义是:

  • iifname != "lo":仅针对从外部网卡进入的包。宿主本机发出的包走 nat-OUTPUT(lo 接口),不受此规则影响,因此 curl 127.0.0.1:8080 依然可用;
  • ip daddr 127.0.0.1 tcp dport 8080:精确匹配"目的为回环地址上已发布端口"的流量;
  • counter drop:丢弃并计数。放在 raw 表中意味着这类包在进入连接跟踪(conntrack)之前就被丢弃——从 raw 表的定位看,它先于 conntrack 处理,因此被丢弃的包不会留下连接跟踪条目,也不会产生对端口发布方无意义的 conntrack 资源占用。

而对照场景中不存在这条规则,外部主机访问 0.0.0.0:8080 发布的服务是合法行为,DNAT 之后照常转发到容器。

同一条链上的另外两条规则

raw-PREROUTING 的第一条 ip daddr 192.0.2.2 iifname != "bridge1" counter drop comment "DROP DIRECT ACCESS" 与本场景的回环特性无关,它是默认网关模式 nat 的通用产物(在对照场景 usernet-portmap.md 中同样存在):禁止远程主机绕过宿主直接访问容器内网地址 192.0.2.2,只能经由宿主的已发布端口进入。

DNAT 规则同样体现了回环绑定的差异。本场景:

chain nat-prerouting-and-output {
    iifname != "bridge1" ip daddr 127.0.0.1 tcp dport 8080 counter dnat to 192.0.2.2:80 comment "DNAT"
}

对照场景(发布到所有地址)中没有 ip daddr 127.0.0.1 匹配项:

chain nat-prerouting-and-output {
    iifname != "bridge1" tcp dport 8080 counter dnat to 192.0.2.2:80 comment "DNAT"
}

即:回环绑定的端口只对"目的地址恰好是 127.0.0.1"的流量做 DNAT。外部网卡上目的为宿主物理 IP 的 8080 流量不会命中 DNAT,随后在 filter-FORWARD 路径上因 UNPUBLISHED PORT DROP 被丢弃——而即便假设它能到达,DROP REMOTE LOOPBACK 也已在更早的 raw 阶段将其丢弃。两层防护叠加,保证 -p 127.0.0.1:8080:80 的"仅限本机"语义。

源码实现:filterPortMappedOnLoopback

生成上述规则的实现位于 nftabler/port.gofilterPortMappedOnLoopback 函数,其调用链为:端点加入网络时的端口绑定触发 AddPortsmodPortssetPerPortRules(内部按顺序调用 setPerPortForwardingsetPerPortDNATsetPerPortHairpinMasq,最后调用 filterPortMappedOnLoopback,见 port.go)。

filterPortMappedOnLoopback 的关键逻辑:

// filterPortMappedOnLoopback adds a rule that drops remote connections to ports
// mapped to 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 (n *network) filterPortMappedOnLoopback(pbs []types.PortBinding, updater nftables.ObjUpdater, ipv nftables.Family) {
	if ipv == nftables.IPv6 {
		return
	}
	for _, pb := range pbs {
		// Nothing to do if not binding to the loopback address.
		if pb.HostPort == 0 || !pb.HostIP.IsLoopback() {
			continue
		}
		// Mappings from host IPv6 to container IPv4 are handled by docker-proxy.
		if pb.HostIP.To4() == nil {
			continue
		}
		if n.fw.config.WSL2Mirrored {
			updater(nftables.Rule{
				Chain: rawPreroutingChain,
				Group: rawPreroutingPortsRuleGroup,
				Rule: []string{
					"iifname loopback0 ip daddr", pb.HostIP.String(), pb.Proto.String(),
					"dport", strconv.Itoa(int(pb.HostPort)),
					`counter accept comment "ACCEPT WSL2 LOOPBACK"`,
				},
			})
		}
		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"`,
			},
		})
	}
}

从源码可以确认文档规则的几个事实边界:

  1. 仅 IPv4 生效:函数开头对 IPv6 直接返回,注释说明原因是 IPv6 回环地址不可路由(remote 流量根本到不了 ::1),因此不需要 drop 规则。
  2. 触发条件:只有 HostPort != 0(即真正发布了端口)且 HostIP.IsLoopback() 的绑定才会生成规则。pb.HostIP 就是 -p 127.0.0.1:8080:80 中的 127.0.0.1;容器地址与容器端口则来自 endpoint 的端口绑定信息(pb.IPpb.Port),这与 DNAT 规则中 192.0.2.2:80 的取值一致。
  3. WSL2 mirrored 模式的例外:在 Windows 的 WSL2 mirrored 模式下,会先插入一条 iifname loopback0 ... counter accept comment "ACCEPT WSL2 LOOPBACK" 放行规则再执行 drop,用于支持该环境下的回环访问路径。本文档场景(标准 Linux 环境)下不存在这条 accept 规则,这也解释了为什么生成的 golden 文件中 raw-PREROUTING 只有两条 drop 规则。
  4. 规则分组:该规则写入 rawPreroutingPortsRuleGroup 规则组,raw-PREROUTING 链名与 raw 优先级基础链的创建见 nftabler.gorawPreroutingChain = "raw-PREROUTING"BaseChainHookPrerouting + BaseChainPriorityRaw)。

另外值得注意的是 setPerPortDNATdaddrMatch 的生成逻辑(port.go):仅当绑定的宿主 IP 不是 unspecified(0.0.0.0/::)时,才在 DNAT 规则中追加 ip daddr <HostIP> 匹配——这正是回环场景的 DNAT 规则比普通场景多出 ip daddr 127.0.0.1 的根源。

实操:在自己的机器上查看与验证

在一台 nftables 防火墙后端的 Linux 主机上,按上面的 docker 命令创建 bridge1 并启动容器后,可直接查看文档对应的规则表:

nft -s list table ip docker-bridges

预期能看到与 generated/usernet-portmap-lo.md 一致的 raw-PREROUTING 链;-s 参数还会显示各规则 counter 的命中数,可用于观察 drop 是否被触发(例如从另一台主机 curl <宿主IP>:8080 失败的同时,DROP REMOTE LOOPBACK 计数器增长)。

若要复现仓库的文档生成流程,则运行 TestBridgeNftablesDoc 集成测试;当内核/规则变化导致 golden diff 时,按 测试文件头部注释 的指引:先检查 diff 中的规则变化,更新对应 templates/ 下的模板描述,再用 TESTFLAGS='-update' 重跑以更新 golden 参考。注意该测试要求 firewalld 未运行、非 rootless、防火墙驱动为 nftables,否则会自动跳过。

适用边界与注意事项

  • 本场景的规则属于 Moby 开发用文档范畴,index.md 明确声明 docker 的 nftables 规则结构会在版本间变化,不是稳定接口,不要依赖具体链名或规则布局做生产环境的自定义防火墙策略。
  • docker-bridges 表每次 daemon 启动时重建;容器与网络的增删都会增量修改对应链(端口规则的增删逻辑见 port.go 中的 AddPorts/DelPorts)。
  • 文档只展示 IPv4 规则;IPv6 规则模式相同,位于 ip6 docker-bridges 表,且如源码所示,IPv6 回环绑定不会生成 DROP REMOTE LOOPBACK 规则。
  • Docker 不使用 filter-INPUT / filter-OUTPUT 钩子,宿主到容器的流量经路由进入桥接网络后由 filter-FORWARD 路径处理;这一点对理解"为什么回环流量走 lo 接口被 iifname != lo 豁免"很重要。
  • 网关模式为 routed 的网络不做宿主端口映射,该场景下亦无此类回环 drop 规则(见 filterPortMappedOnLoopback 注释)。

小结

回环地址端口映射(-p 127.0.0.1:8080:80)在 Moby 的 nftables 桥接防火墙中,相对普通宿主地址映射只带来两处变化:DNAT 规则增加 ip daddr 127.0.0.1 限定,以及 raw-PREROUTING 中新增一条 DROP REMOTE LOOPBACK 规则。前者由 setPerPortDNATdaddrMatch 逻辑生成,后者由 filterPortMappedOnLoopback 生成,确保只有宿主本机能访问发布在回环地址上的容器端口。仓库通过 TestBridgeNftablesDoc 把真实内核规则与文档做 golden diff,保证了这份 nftables 参考文档与实现始终一致——这也是阅读该文档时其可信度的来源。

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