Moby 中 nat-unprotected 桥接网络的 nftables 规则解析:发布端口的“无保护“模式
2026/9/7 4:05:41 网站建设 项目流程

Moby 中 nat-unprotected 桥接网络的 nftables 规则解析:发布端口的"无保护"模式

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

本文以 Moby(Docker 引擎)仓库中由集成测试自动生成的参考文档 usernet-portmap-natunprot.md 为主体,完整解读在"发布端口"的容器位于nat-unprotected模式桥接网络时,Docker 实际下发的 nftables 规则集。读完本文,你能:准确理解gateway_mode_ipv4=nat-unprotected与默认nat模式在防火墙规则上的具体差异(按端口放行规则缺失、"DROP DIRECT ACCESS" 规则缺失);掌握该场景下完整的docker-bridges表结构,并能结合仓库中 bridge 驱动的 nftabler 源码定位每条规则的生成位置。

场景定义:一个 nat-unprotected 网络上运行带发布端口的容器

该文档描述的场景等价于以下操作(在禁用 userland proxy 的守护进程上执行):

docker network create \ -o com.docker.network.bridge.name=bridge1 \ -o com.docker.network.bridge.gateway_mode_ipv4=nat-unprotected \ --subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1 docker run --network bridge1 -p 8080:80 --name c1 busybox

三个网络选项的含义:

选项取值作用
com.docker.network.bridge.namebridge1固定桥接设备名,便于在 nftables 表中对照
com.docker.network.bridge.gateway_mode_ipv4nat-unprotectedIPv4 网关模式:保留 NAT,但不对未发布端口和外部直接访问施加过滤规则
--subnet/--gateway192.0.2.0/24/192.0.2.1显式指定子网与网关,保证规则文档可复现

在源码中,网关模式是 bridge 驱动的一个枚举常量。bridge_linux.go 中定义了五种模式:

const ( gwModeDefault gwMode = "" gwModeNAT gwMode = "nat" gwModeNATUnprot gwMode = "nat-unprotected" gwModeRouted gwMode = "routed" gwModeIsolated gwMode = "isolated" )

nat-unprotected最终被解析为firewaller.NetworkConfigFam.Unprotected布尔位(见同文件约 L491)。firewaller.go 对该字段的注释给出了最权威的定义:

// Unprotected is true if no rules to filter unpublished ports or direct access from // any remote host are required. Unprotected bool

即:不需要"过滤未发布端口"的规则,也不需要"阻止任意远程主机直接访问容器 IP"的规则。这正是该模式与routed(容器 IP 直接可达、不经 NAT)的关键区别——nat-unprotected的 DNAT 和 masquerade 依然生效,只是放宽了入站方向的默认保护。

与默认 nat 模式规则集的公共部分

文档明确指出,绝大部分规则与 nat 模式的发布端口场景(usernet-portmap.md)相同。完整表如下(摘自生成文档,IPv4docker-bridges表):

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" tcp dport 8080 counter dnat to 192.0.2.2:80 comment "DNAT" } chain raw-PREROUTING { type filter hook prerouting priority raw; policy accept; } 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" counter accept comment "UNPROTECTED" } 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" } }

这张表值得注意的结构特征(与 nftablesdoc 总览 的说明一致):

  • 按接口分派的 vmap 跳转filter-FORWARD不直接写死规则,而是通过oifname vmap @filter-forward-in-jumps(目的接口)与iifname vmap @filter-forward-out-jumps(源接口)两张ifname : verdict映射表,把每个桥接设备路由到各自的filter-forward-in__<bridge>/filter-forward-out__<bridge>链。这样每个网络一条链,互不干扰,删除网络时只需删对应链。
  • NAT 方向对称:入站 DNAT 挂在nat-PREROUTING(外部到达)与nat-OUTPUT(本机发起)共用的nat-prerouting-and-output链上;出站 SNAT 由nat-POSTROUTING经两张 nat-postrouting 跳转分派。
  • IPv4/IPv6 平行:IPv6 规则遵循相同模式,位于ip6 docker-bridges表,文档仅展示 IPv4。

差异一:filter-forward-in__bridge1没有按端口规则,改为整体放行

对比默认nat模式,filter-forward-in链通常会在 ICC 放行之后,为每个发布端口写一条tcp dport <port> accept,最后以drop comment "UNPUBLISHED PORT DROP"收尾(上表中默认桥docker0的链就是这个形态)。而bridge1的入向链是:

chain filter-forward-in__bridge1 { ct state established,related counter accept iifname "bridge1" counter accept comment "ICC" counter accept comment "UNPROTECTED" }

没有逐端口的dport 8080放行规则,末尾的兜底不是drop而是counter accept comment "UNPROTECTED"——任何端口bridge1转发进来的包都被接受。文档给出的原因是:若系统层面filter-FORWARD链的默认策略为drop,则只有发布端口的流量才能被转发进容器,未发布端口全部不可达;nat-unprotected模式恰恰要取消这种限制。

这条规则由 nftabler 的 network.go 生成:

if conf.Unprotected { // ... Rule: []string{`counter accept comment "UNPROTECTED"`}, }

同目录的 golden 测试数据(如 nftabler/testdata 下大量gwm=nat-unprotected命名的.golden文件)中均可看到counter packets 0 bytes 0 accept comment "UNPROTECTED"这一行,与集成文档相互印证。而对应的iptabler实现里,port.go 在config.Unprotected为真时直接跳过逐端口的过滤规则生成——两个防火墙后端(iptables / nftables)在此行为上保持一致。

差异二:raw-PREROUTING中没有 "DROP DIRECT ACCESS" 规则

nat模式下,raw-PREROUTING链里会有一条针对"外部主机直接访问容器 IP"的 DROP 规则,防止绕过宿主机的 DNAT 直接命中容器地址。而本场景中该链是空的:

chain raw-PREROUTING { type filter hook prerouting priority raw; policy accept; }

文档明确说明:由于不存在 "DROP DIRECT ACCESS" 规则,容器可以从宿主机外部直接访问(例如192.0.2.2可以从外部网卡直接被路由到)。在 nftabler 源码中,endpoint.go 的条件正体现了这一点:

if n.config.Internal || conf.Unprotected || conf.Routed || n.fw.config.AllowDirectRouting { // 跳过直接访问过滤(DROP DIRECT ACCESS)相关规则的写入 }

internalnat-unprotectedrouted、或守护进程级AllowDirectRouting四种情况之一成立时,都会放行对容器 IP 的直接访问——但只有nat-unprotected仍保留 NAT,因此它实际是"routed 的可达性 + nat 的地址转换"的组合。

DNAT 与 MASQUERADE 规则原样保留,userland proxy 行为不变

文档最后强调了一点容易被忽略的事实:

"dnat" and "masquerade" rules are still in-place. And, if the userland proxy is enabled, it is still started.

对照上表可以确认两条 NAT 规则都在:

chain nat-prerouting-and-output { iifname != "bridge1" tcp dport 8080 counter dnat to 192.0.2.2:80 comment "DNAT" } chain nat-postrouting-out__bridge1 { oifname != "bridge1" ip saddr 192.0.2.0/24 counter masquerade comment "MASQUERADE" }
  • DNAT:外部/本机到:8080的 TCP 流量被重写到容器192.0.2.2:80iifname != "bridge1"排除来自桥内(容器互访)的流量,避免 hairpin 循环。
  • MASQUERADE:源地址属于192.0.2.0/24且出接口不是bridge1的包做源地址伪装,保证回程报文经宿主机回送。

也就是说,nat-unprotected放松的只是入站过滤(未发布端口、直接 IP 访问),出站 SNAT 与入站 DNAT 的地址转换链路完全不受影响。文档开头的场景说明"在禁用 userland proxy 的守护进程上运行",只是为了让行为完全由内核规则决定、便于抓规则;若启用了 userland proxy(dockerd --userland-proxy=true为默认),代理进程仍会照常启动并监听宿主端口。

文档的生成机制:由集成测试从真实守护进程捕获

这份文档不是人工编写的,而是测试 nftablesdoc_linux_test.go 的TestBridgeNftablesDoc生成的。其流程在 index.md 与测试文件头注释中都有说明:

  1. 为本文档对应的 section(usernet-portmap-natunprot.md)单独创建一个网络命名空间,在其中启动真实 dockerd;对应配置见 nftablesdoc_linux_test.go 的index表:gwMode: "nat-unprotected"、端口映射80/tcp → 8080
  2. 按 section 描述创建网络与容器(createBridgeNetworks会把gwModebridge.IPv4GatewayMode选项传给docker network create,与上文命令行等价)。
  3. nft -s list table ip docker-bridges捕获规则,拆分成以链/映射名为键的片段(runNftables函数)。
  4. 套用 templates/usernet-portmap-natunprot.md 这份 markdown 模板(其中的{{index . "Ruleset4"}}{{index . "chain filter-forward-in__bridge1"}}等占位符被真实规则替换),生成 generated/usernet-portmap-natunprot.md。
  5. 生成结果与仓库中的"金标"文件 diff,不一致则测试失败——这保证每次规则引擎改动都会被显式审查。

模板中两处占位符恰好就是本文重点展开的两个差异链:

{{index . "chain filter-forward-in__bridge1"}} ... {{index . "chain raw-PREROUTING"}}

因此这份文档可以视为"当前版本 Docker 在该场景下的规则事实";需要留意其开头的警示(见 index.md):这是面向开发者的文档,Docker 的 nftables 规则结构在版本之间会变,不构成稳定接口,不要在生产环境依赖具体链名。

小结

  • nat-unprotected= 保留 DNAT/MASQUERADE 的 NAT 语义 + 取消两类入站保护:入向链兜底从drop(UNPUBLISHED PORT DROP)变为accept(UNPROTECTED),raw-PREROUTING不再 DROP 对容器 IP 的直接访问。
  • 规则生成入口可追溯:模式枚举与解析在 bridge_linux.go,语义定义在 firewaller.go,nftables 落地在 nftabler/network.go 与 nftabler/endpoint.go,单元测试的 golden 数据在 nftabler/testdata。
  • 若你只想把容器 IP 变成外部可直达、但不需要发布端口,应比较 usernet-portmap-routed.md(routed 模式);本文场景适合"既要发布端口映射、又不想为未发布端口维持严格过滤"的桥接网络。

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询