Moby 桥接网络 nat-unprotected 网关模式:nftables 端口发布规则的完整解析
本篇技术指南基于 Moby 仓库中 nftables 文档模板 usernet-portmap-natunprot.md 展开,深入讲解桥接网络 gateway_mode_ipv4=nat-unprotected 场景下的 nftables 规则布局:如何复现该场景、完整的 ip docker-bridges 规则表长什么样、与标准 NAT 模式相比 filter-forward-in 链和 raw-PREROUTING 链发生了哪些变化,以及这些变化在 firewaller/nftabler 源码中的实现依据。读完后你可以独立读懂 Docker 在该模式下生成的全部 nftables 规则,并理解“非保护 NAT”的确切语义。
场景与前提
nat-unprotected 是 Moby 桥接网络驱动支持的一种网关模式(gateway mode)。在 bridge_linux.go 中定义了全部合法取值:
const (
gwModeDefault gwMode = ""
gwModeNAT gwMode = "nat"
gwModeNATUnprot gwMode = "nat-unprotected"
gwModeRouted gwMode = "routed"
gwModeIsolated gwMode = "isolated"
)
创建网络时通过 com.docker.network.bridge.gateway_mode_ipv4 选项指定,newGwMode 负责解析并在遇到未知值时报 unknown gateway mode 错误。
按仓库文档模板描述,该场景等价于:以禁用用户态代理(userland proxy)的方式运行 dockerd,然后创建一个 nat-unprotected 网络并在其上运行带端口映射的容器:
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
对应参数说明:
-o com.docker.network.bridge.name=bridge1:指定网桥设备名为bridge1,便于在 nftables 中按接口名定位规则;-o com.docker.network.bridge.gateway_mode_ipv4=nat-unprotected:IPv4 网关模式设为“非保护 NAT”;--subnet 192.0.2.0/24 --gateway 192.0.2.1:TEST-NET-1 文档网段,网关为.1;-p 8080:80:发布宿主机 8080 端口到容器 80 端口。
需要注意的前提(来自 index.md 与生成测试 nftablesdoc_linux_test.go):
- 这套 nftables 规则仅面向开发用途,其结构在版本间会变化,不是稳定接口;
- 生成测试要求宿主机防火墙后端为
nftables、未运行 firewalld、且非 rootless; - Docker 每次启动时会重建规则表(tables are re-created each time Docker starts);
- IPv6 规则与 IPv4 模式相同,只是位于
ip6 docker-bridges表,因此文档只展示 IPv4(ip docker-bridges)。
完整 nftables 规则表
该文档模板通过 {{index . "Ruleset4"}} 等占位符注入真实捕获的规则。由 TestBridgeNftablesDoc 生成的 usernet-portmap-natunprot.md 中,完整的 table ip docker-bridges 如下(容器 c1 已获取 192.0.2.2,-p 8080:80 生效):
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"
}
}
几个值得注意的结构点:
- 四个
ifname : verdict类型的 vmap(virtual map)是这套规则的分发机制:filter-FORWARD、nat-POSTROUTING等钩子链只做vmap查表跳转,把流量分发到每个网桥的专属链(__docker0、__bridge1),实现按接口隔离; nat-prerouting-and-output链中的dnat to 192.0.2.2:80 comment "DNAT"就是-p 8080:80的落地规则,iifname != "bridge1"条件排除了来自网桥本接口方向的包;nat-postrouting-out__bridge1的masquerade规则把离开bridge1且源地址在192.0.2.0/24的包做地址伪装,支撑容器出网;- Docker 不使用 filter-INPUT 钩子:来自宿主机物理网络或宿主机自身的包会被路由进桥接网络,命中 filter-FORWARD(见 index.md 的说明)。
与标准 NAT 模式的关键差异
文档模板的核心论点是:与标准 nat 模式网络 相比,大部分规则相同,但有两处关键不同,它们共同定义了 nat-unprotected 的语义——不做未发布端口的过滤,也不阻止按容器 IP 直连。
差异一:filter-forward-in 链没有按端口过滤
对比两张表:
标准 nat 模式(默认桥 docker0)的入方向链末尾是丢弃规则:
chain filter-forward-in__docker0 {
ct state established,related counter accept
iifname "docker0" counter accept comment "ICC"
counter drop comment "UNPUBLISHED PORT DROP"
}
nat-unprotected 网络的入方向链则替换为放行规则:
chain filter-forward-in__bridge1 {
ct state established,related counter accept
iifname "bridge1" counter accept comment "ICC"
counter accept comment "UNPROTECTED"
}
也就是说,filter-forward-in__bridge1 链没有针对发布端口的逐端口放行规则,而是接受任意端口的入方向包。这一点在源码 nftabler/network.go 中一目了然——网络配置里的 Unprotected 标志决定最终落入哪个分支:
// Incoming traffic
if conf.Unprotected {
tm.Create(nftables.Rule{
Chain: fwdInChain,
Group: fwdInFinalRuleGroup,
Rule: []string{`counter accept comment "UNPROTECTED"`},
})
} else {
tm.Create(nftables.Rule{
Chain: fwdInChain,
Group: fwdInFinalRuleGroup,
Rule: []string{`counter drop comment "UNPUBLISHED PORT DROP"`},
})
}
Unprotected 的字段定义在 firewaller.go:
// NetworkConfigFam contains network configuration for a single address family.
type NetworkConfigFam struct {
HostIP netip.Addr
Prefix netip.Prefix
Routed bool
// Unprotected is true if no rules to filter unpublished ports or direct access from
// any remote host are required.
Unprotected bool
}
模板文档特别注明了这条 accept 的实际用途:当 filter-FORWARD 链的默认策略是 "drop" 时,如果没有这条放行规则,转发流量会被整体丢弃;UNPROTECTED 规则承担了“兜底放行”的角色。
差异二:raw-PREROUTING 链中没有 "DROP DIRECT ACCESS" 规则
上面的生成文档里,raw-PREROUTING 链几乎是空的:
chain raw-PREROUTING {
type filter hook prerouting priority raw; policy accept;
}
而在标准 nat 模式下,每当一个端点(容器)加入网络时,Docker 会在这里追加一条按容器 IP 丢弃外部直连包的规则。这条规则的创建逻辑在 nftabler/endpoint.go:
func (n *network) filterDirectAccess(updater func(nftables.Obj), fam nftables.Family, conf firewaller.NetworkConfigFam, epIP netip.Addr) {
if n.config.Internal || conf.Unprotected || conf.Routed || n.fw.config.AllowDirectRouting {
return
}
ifNames := strings.Join(n.config.TrustedHostInterfaces, ", ")
updater(nftables.Rule{
Chain: rawPreroutingChain,
Group: rawPreroutingPortsRuleGroup,
Rule: []string{
string(fam), "daddr", epIP.String(),
"iifname != {", n.config.IfName, ",", ifNames, `} counter drop comment "DROP DIRECT ACCESS"`,
},
})
}
filterDirectAccess 在 conf.Unprotected 为真时直接返回(no-op),因此 nat-unprotected 网络下永远不会写入 DROP DIRECT ACCESS 规则。其效果就是模板文档所写的:In chain raw-PREROUTING, there's no "DROP DIRECT ACCESS" rule, so container can be accessed from outside the host——外部主机可以直接按容器的 IP(如 192.0.2.2)访问该容器,而不必经由 -p 发布的端口做 DNAT。
保持不变的规则:dnat 与 masquerade
文档模板最后强调:dnat 和 masquerade 规则仍然存在,而且如果启用了用户态代理(userland proxy),它依然会被启动。这在规则表里可以直接验证:
- 端口发布的 DNAT:
nat-prerouting-and-output链中的iifname != "bridge1" tcp dport 8080 counter dnat to 192.0.2.2:80 comment "DNAT"; - 出网伪装:
nat-postrouting-out__bridge1链中的oifname != "bridge1" ip saddr 192.0.2.0/24 counter masquerade comment "MASQUERADE",其生成逻辑在 nftabler/network.go(未指定HostIP时选择 masquerade,指定了则退化为固定地址的 SNAT)。
逐端口的规则(端口发布规则、以及 nat 模式下的按端口转发放行)由 nftabler/port.go 中的 setPerPortRules 处理,其中同样接收 n.config.Config4.Unprotected 作为参数——在 nat 模式下它会为发布端口写入 filter 链中的放行条目,而 nat-unprotected 模式下这一层过滤被网络级的 UNPROTECTED 放行所取代。iptables 后端的语义与之对应,见 iptabler/endpoint.go 与 iptabler/port.go(注释明确写道 “gw_mode=nat-unprotected means there's minimal security for NATed ports”),对应的规则文档在 iptablesdoc/generated/usernet-portmap-natunprot.md。
可以把 nat-unprotected 理解为“保留了 NAT 的连通性、移除了 NAT 的隔离性”:地址转换照常工作,但未发布端口不再被屏蔽、容器 IP 不再对外不可达。
这份文档是如何生成和校验的
上述规则表并非手写,而是由集成测试 TestBridgeNftablesDoc 自动捕获生成的,理解其流程有助于确认规则内容的可信度:
- 场景声明:
index切片中的 usernet-portmap-natunprot.md 条目 声明了gwMode: "nat-unprotected"、网络名bridge1、容器c1的端口映射80/tcp -> 8080,网段取自docNetworks/docGateways(192.0.2.0/24); - 隔离执行:每个 section 在自己的网络命名空间里启动一个真实的 dockerd(runTestNet),创建网络、运行容器;
- 规则捕获:runNftables 执行
nft -s list table ip docker-bridges,把输出按 map/chain 切块存入模板数据(Ruleset4、chain filter-forward-in__bridge1等键),并将priority -100规范化为dstnat字样; - 模板渲染:generate 用
text/template渲染templates/下与 section 同名的模板文件(即本篇基于的 templates/usernet-portmap-natunprot.md); - 黄金文件比对:渲染结果写入 bundles 目录,并与 generated/ 中的参考文档做 golden 对比,规则不一致则测试失败;需要更新时先检查 diff、修改对应模板描述,再用
TESTFLAGS='-update'重新生成(见该测试文件的包注释)。
在单元测试层面,nftabler_test.go 遍历 nat、nat-unprotected、routed 三种网关模式,将 Unprotected: gwmode == "nat-unprotected" 传入 firewaller 配置,并与 testdata 中的 golden 文件 逐条比对,其中每个 gwm=nat-unprotected 的 golden 文件都包含 counter accept comment "UNPROTECTED" 行——与上文规则表相互印证。
小结
nat-unprotected通过com.docker.network.bridge.gateway_mode_ipv4选项指定,在源码中解析为 gwModeNATUnprot,并映射为 firewaller 的Unprotected标志;- 该模式下 nftables 规则与标准 NAT 模式几乎一致,唯二区别是:入方向链以
counter accept comment "UNPROTECTED"取代逐端口放行与UNPUBLISHED PORT DROP,以及raw-PREROUTING中不写入DROP DIRECT ACCESS,从而允许外部直接按容器 IP 访问; - DNAT(端口发布)与 masquerade(出网伪装)规则不受影响,用户态代理的启用行为也不变;
- 由于未发布端口不再被过滤、容器地址对外可达,这是一种“最小安全”的 NAT 模式,应在明确接受这一风险面后再用于生产网络;
- 本文所述规则可通过 generated/usernet-portmap-natunprot.md 复核,或参照 nftablesdoc_linux_test.go 描述的
TESTFLAGS='-update'流程重新生成。
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