Moby 桥接网络 nftables 规则详解:回环地址端口映射下的 DROP REMOTE LOOPBACK 规则是如何生成的
本文以 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
TestBridgeNftablesDocby 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 的工作流程是:
- 在独立的网络命名空间中启动一个 dockerd 实例;
- 按照 测试索引 中的
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"}}},
},
},
}},
},
- 执行
nft -s list table ip docker-bridges抓取完整规则集,并把它拆成"整表"与各 chain/map 块(以块首行为键,例如chain raw-PREROUTING),存入map[string]string; - 将该数据填充到 templates/usernet-portmap-lo.md 这个
text/template模板中,生成 generated/usernet-portmap-lo.md; - 用
golden.Assert将生成结果与仓库中的 golden 文件做 diff,规则有任何变化测试即失败。
因此这份文档中展示的每条规则都是"真实 daemon 在真实内核上产生的规则",与手工整理文档相比具备很强的事实性。同时 index.md 也给出重要前提:
- 该文档仅面向开发用途(development use),Docker 的 nftables 规则结构不是稳定接口,会在版本间变化;
- IPv6 规则与 IPv4 遵循相同模式,只是位于不同的表(
ip docker-bridges与ip6 docker-bridges),文档中只展示 IPv4 规则; docker-bridges表在每次 Docker 启动时都会重建;- Docker 不使用
filter-INPUT与filter-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),再放行已发布容器端口,最后drop(UNPUBLISHED 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.go 的 filterPortMappedOnLoopback 函数,其调用链为:端点加入网络时的端口绑定触发 AddPorts → modPorts → setPerPortRules(内部按顺序调用 setPerPortForwarding、setPerPortDNAT、setPerPortHairpinMasq,最后调用 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"`,
},
})
}
}
从源码可以确认文档规则的几个事实边界:
- 仅 IPv4 生效:函数开头对 IPv6 直接返回,注释说明原因是 IPv6 回环地址不可路由(remote 流量根本到不了
::1),因此不需要 drop 规则。 - 触发条件:只有
HostPort != 0(即真正发布了端口)且HostIP.IsLoopback()的绑定才会生成规则。pb.HostIP就是-p 127.0.0.1:8080:80中的127.0.0.1;容器地址与容器端口则来自 endpoint 的端口绑定信息(pb.IP、pb.Port),这与 DNAT 规则中192.0.2.2:80的取值一致。 - WSL2 mirrored 模式的例外:在 Windows 的 WSL2 mirrored 模式下,会先插入一条
iifname loopback0 ... counter accept comment "ACCEPT WSL2 LOOPBACK"放行规则再执行 drop,用于支持该环境下的回环访问路径。本文档场景(标准 Linux 环境)下不存在这条 accept 规则,这也解释了为什么生成的 golden 文件中raw-PREROUTING只有两条 drop 规则。 - 规则分组:该规则写入
rawPreroutingPortsRuleGroup规则组,raw-PREROUTING链名与 raw 优先级基础链的创建见 nftabler.go(rawPreroutingChain = "raw-PREROUTING",BaseChainHookPrerouting+BaseChainPriorityRaw)。
另外值得注意的是 setPerPortDNAT 中 daddrMatch 的生成逻辑(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 规则。前者由 setPerPortDNAT 的 daddrMatch 逻辑生成,后者由 filterPortMappedOnLoopback 生成,确保只有宿主本机能访问发布在回环地址上的容器端口。仓库通过 TestBridgeNftablesDoc 把真实内核规则与文档做 golden diff,保证了这份 nftables 参考文档与实现始终一致——这也是阅读该文档时其可信度的来源。
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 StartedRust0624
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