Moby bridge nat-unprotected 网关模式:发布端口、默认 ACCEPT 规则与 iptables 全解析
本篇基于 Moby 仓库中由集成测试自动生成的场景文档,完整解析 bridge 驱动 gateway_mode=nat-unprotected 模式下"容器发布端口"时 Docker 生成的 iptables 规则:如何在 filter 表的 DOCKER 链中把默认 DROP 换为默认 ACCEPT、为何不再生成按端口的 ACCEPT 规则、nat 表为何与默认 nat 模式完全一致,并给出全部可复制的复现命令、逐条规则说明与源码级实现路径(setDefaultForwardRule、setPerPortForwarding),帮助你在理解 bridge 网络"最小保护"语义的同时,掌握用 golden 测试维护 iptables 文档的方法。
文档定位:TestBridgeIptablesDoc 生成的场景之一
模板文件 integration/network/bridge/iptablesdoc/templates/usernet-portmap-natunprot.md 属于 index.md 所列的九个 iptables 场景之一,对应"Container on a nat-unprotected network, with a published port"。
整个 iptablesdoc 目录的工作机制是(见 iptablesdoc_linux_test.go 的包注释):
TestBridgeIptablesDoc启动一个真实的 dockerd,逐个场景创建 bridge 网络并运行容器;- 对每个场景执行
iptables -vL --line-numbers、iptables -S等命令捕获 filter/nat/raw 三张表(命令映射见iptCmds变量,如LFilter4、SFilter4、LNat4、SNat4); - 将捕获结果作为数据填充
templates/下对应的text/template模板,生成 Markdown 文档; - 生成的文档与
generated/目录下的"golden"参考文档做 diff,任何规则差异都会导致测试失败;此时需要人工检查 diff、更新对应模板描述,并以TESTFLAGS='-update'重新运行以刷新参考文档。
需要注意两点边界(来自 index.md 的告警):
- 该文档系列仅供开发使用:Docker 的 iptables/ip6tables 规则结构会在版本间变化,不是稳定接口;
- 测试通过
golden.Assert比较生成物与 golden 文件,但模板文字本身的修改可能无法被测试发现; - 文档中代码链接是历史 permalink,会随代码演进过期——下文已改用当前仓库的实际源码路径替代。
测试自身的运行限制也在源码中明确:firewalld 运行中、rootless 模式、防火墙后端为 nftables 时会被 skip(TestBridgeIptablesDoc 开头的 skip.If 三连);每个场景在独立的 L3Segment 网络命名空间中运行一个 dockerd,且先 ip link set eth0 down 以避免杂散流量污染包计数,最后用 iptables -Z 清零计数并正则归一化 0 packets, 0 bytes,保证输出可复现。
复现该场景:两条命令
文档给出的等效操作为:
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 选项的常量定义见 labels.go:
| 选项 | 常量 | 作用 |
|---|---|---|
com.docker.network.bridge.name |
BridgeName |
指定桥接口名为 bridge1 |
com.docker.network.bridge.gateway_mode_ipv4 |
IPv4GatewayMode |
将 IPv4 网关模式设为 nat-unprotected |
--subnet/--gateway |
— | 指定 IPAM 子网 192.0.2.0/24、网关 192.0.2.1 |
网关模式的解析入口是 newGwMode,合法取值为 nat、nat-unprotected、routed、isolated:
const (
gwModeDefault gwMode = ""
gwModeNAT gwMode = "nat"
gwModeNATUnprot gwMode = "nat-unprotected"
gwModeRouted gwMode = "routed"
gwModeIsolated gwMode = "isolated"
)
随后 makeNetworkConfigFam 把模式翻译为防火墙层的布尔标志:Routed: gwm.routed()、Unprotected: gwm.unprotected(),写入 firewaller.NetworkConfigFam。其 Unprotected 字段的注释正是该模式的语义定义:
Unprotected is true if no rules to filter unpublished ports or direct access from any remote host are required.
即:不生成任何用于过滤"未发布端口直接访问"的规则。一个值得注意的细节是:文档模板开头描述"Running the daemon with the userland proxy disable",而在测试索引 index 中 usernet-portmap-natunprot 段并未设置 noUserlandProxy(只有 usernet-portmap-noproxy 段设置了),因此该场景实际以默认参数(userland proxy 启用)运行——这也与文档后文"If the userland proxy is enabled, it is still started"的强调相呼应。
filter 表:默认 ACCEPT 取代默认 DROP
场景生效后捕获到的 filter 表全文如下(来自 golden 文件 generated/usernet-portmap-natunprot.md):
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER-USER all -- any any anywhere anywhere
2 0 0 DOCKER-FORWARD all -- any any anywhere anywhere
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain DOCKER (2 references)
num pkts bytes target prot opt in out source destination
1 0 0 DROP all -- !docker0 docker0 anywhere anywhere
2 0 0 ACCEPT all -- !bridge1 bridge1 anywhere anywhere
Chain DOCKER-BRIDGE (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER all -- any docker0 anywhere anywhere
2 0 0 DOCKER all -- any bridge1 anywhere anywhere
Chain DOCKER-CT (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT all -- any docker0 anywhere anywhere ctstate RELATED,ESTABLISHED
2 0 0 ACCEPT all -- any bridge1 anywhere anywhere ctstate RELATED,ESTABLISHED
Chain DOCKER-FORWARD (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER-CT all -- any any anywhere anywhere
2 0 0 DOCKER-INTERNAL all -- any any anywhere anywhere
3 0 0 DOCKER-BRIDGE all -- any any anywhere anywhere
4 0 0 ACCEPT all -- docker0 any anywhere anywhere
5 0 0 ACCEPT all -- bridge1 any anywhere anywhere
Chain DOCKER-INTERNAL (1 references)
num pkts bytes target prot opt in out source destination
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
对应的 iptables -S -t filter 等价命令为:
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-BRIDGE
-N DOCKER-CT
-N DOCKER-FORWARD
-N DOCKER-INTERNAL
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-FORWARD
-A DOCKER ! -i docker0 -o docker0 -j DROP
-A DOCKER ! -i bridge1 -o bridge1 -j ACCEPT
-A DOCKER-BRIDGE -o docker0 -j DOCKER
-A DOCKER-BRIDGE -o bridge1 -j DOCKER
-A DOCKER-CT -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-CT -o bridge1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-FORWARD -j DOCKER-CT
-A DOCKER-FORWARD -j DOCKER-INTERNAL
-A DOCKER-FORWARD -j DOCKER-BRIDGE
-A DOCKER-FORWARD -i docker0 -j ACCEPT
-A DOCKER-FORWARD -i bridge1 -j ACCEPT
逐链解读(对照 index.md 中"filter-INPUT/OUTPUT 不被 Docker 使用"的说明):
- DOCKER 链:这是本场景的核心差异所在。默认
nat模式下每个用户网络会追加一条"未被按端口规则 ACCEPT 的包一律 DROP"的兜底规则;而nat-unprotected网络追加的是! -i bridge1 -o bridge1 -j ACCEPT——进入或离开bridge1的流量无条件放行。对比同一 DOCKER 链中docker0(默认桥)那条DROP规则,两种模式的差别一目了然。 - DOCKER-CT 链:对发往
bridge1的 RELATED/ESTABLISHED 回包放行,保证容器入站连接的回程流量不被后续规则拦截。 - DOCKER-BRIDGE 链:把所有发往各桥(
docker0、bridge1)的出站流量跳转到 DOCKER 链,再执行上述默认动作。 - DOCKER-FORWARD 链:顺序为
DOCKER-CT → DOCKER-INTERNAL → DOCKER-BRIDGE,随后按网桥放行出站(-i docker0、-i bridge1的 ACCEPT 规则),DOCKER-INTERNAL链在本场景中为空(没有--internal网络)。 - FORWARD 链:仅两条跳转,所有判定都封装在 Docker 自定义链内,不污染宿主 FORWARD 主链。
与 nat 模式的核心差异
模板文档明确列出与 nat 模式场景 的差异,全部集中在 DOCKER 链:
-
默认规则从 DROP 变为 ACCEPT。nat 模式为"任何未被按端口/协议规则接受的包"追加默认 DROP;nat-unprotected 追加默认 ACCEPT。源码中这个分支就在 setDefaultForwardRule:
// Normally, DROP anything that hasn't been ACCEPTed by a per-port/protocol // rule. This prevents direct access to un-mapped ports from remote hosts // that can route directly to the container's address (by setting up a // route via the host's address). action := "DROP" if unprotected { // If the user really wants to allow all access from the wider network, // explicitly ACCEPT anything so that the filter-FORWARD chain's // default policy can't interfere. action = "ACCEPT" }注释点明了显式 ACCEPT 的必要性:即使 filter-FORWARD 链的默认 policy 是 DROP,这条规则也能保证容器间及容器对外流量不被宿主策略意外拦截。
-
不生成按端口/协议的 ACCEPT 规则。由于该网络默认即 ACCEPT,发布端口
80/tcp无需再有一条专门的放行规则,因此 setPerPortForwarding(它负责向 DOCKER 链顶部插入! -i <bridge> -o <bridge> -p <proto> -d <containerIP> --dport <port> -j ACCEPT)不会被调用。调用点在 port.go 的 AddPort 流程 中可见:setPerPortNAT先行执行,随后if !config.Unprotected { ... setPerPortForwarding ... }。 -
userland proxy 行为不变。文档特别注明:If the userland proxy is enabled, it is still started——网关模式只改变防火墙规则,不影响宿主端口代理进程的启动逻辑。
另一个源码层面可直接印证"unprotected = 不过滤直接访问"的差异点:raw 表中用于丢弃"绕过 DNAT、直接路由到容器 IP"流量的过滤规则(filterDirectAccess)在 nat-unprotected 与 routed 模式下直接返回:
func (n *network) filterDirectAccess(ctx context.Context, ipv iptables.IPVersion, config firewaller.NetworkConfigFam, epIP netip.Addr, enable bool) error {
if n.config.Internal || config.Unprotected || config.Routed {
return nil
}
这也是 golden 文件中 raw 表完全缺席的原因——nat 模式下这里本应存在 direct-access DROP 规则,nat-unprotected 下从源头就不创建。nftables 后端有对应实现,nftabler/network.go 在 conf.Unprotected 时写入 counter accept comment "UNPROTECTED" 规则,语义与 iptables 路径一致(该文档系列仅在 iptables 后端下生成,见测试的 skip 条件)。
nat 表:与 nat 模式完全一致
模板文档的结论是:nat 表与 nat 模式逐字节相同,端口发布的全部 DNAT/SNAT 机制不受网关模式影响:
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER all -- any any anywhere anywhere ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER all -- any any anywhere !loopback/8 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 MASQUERADE all -- any !bridge1 192.0.2.0/24 anywhere
2 0 0 MASQUERADE all -- any !docker0 172.17.0.0/16 anywhere
Chain DOCKER (2 references)
num pkts bytes target prot opt in out source destination
1 0 0 DNAT tcp -- !bridge1 any anywhere anywhere tcp dpt:http-alt to:192.0.2.2:80
等价命令:
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-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 POSTROUTING -s 192.0.2.0/24 ! -o bridge1 -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER ! -i bridge1 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80
要点:
-p 8080:80映射为nat-DOCKER链中的DNAT tcp dpt:http-alt to:192.0.2.2:80——宿主机 8080 端口入站的 TCP 被改写为容器 IP(192.0.2.2,网关 192.0.2.1 之后的首个地址)的 80 端口;- POSTROUTING 的两条 MASQUERADE 分别覆盖
bridge1子网(192.0.2.0/24)与默认docker0子网(172.17.0.0/16)的出站 SNAT,规则来自 setupNonInternalNetworkRules 中按Masquerade/HostIP生成的 natRule; - PREROUTING/OUTPUT 的
ADDRTYPE match dst-type LOCAL跳转保证只有目的地址是本机时才进入 DNAT 判定,避免影响其他主机经过的流量。
源码调用链小结
把文档现象串回实现,nat-unprotected 场景的完整链路是:
docker network create -o com.docker.network.bridge.gateway_mode_ipv4=nat-unprotected→ newGwMode 解析出gwModeNATUnprot;- makeNetworkConfigFam 置
Unprotected: true写入 firewaller.NetworkConfigFam; - 建网时 setupIPTables 调用
setDefaultForwardRule(..., config.Unprotected, ...)追加! -i bridge1 -o bridge1 -j ACCEPT,并注册 cleanup 函数以便删网时移除; - 容器端口发布时,
AddPort始终执行setPerPortNAT(nat 表 DNAT),但跳过setPerPortForwarding(filter 表按端口 ACCEPT),同时filterDirectAccess对 raw 表的 direct-access 过滤整体 no-op; - 集成测试
TestBridgeIptablesDoc捕获全部三张表、渲染模板、与 golden 文件比对,锁定上述规则集合。
使用边界与注意事项
- 安全语义:
nat-unprotected意味着放弃"阻止远程主机直连容器未发布端口/容器 IP"的过滤。源码注释的表述是 "If the user really wants to allow all access from the wider network"——该模式适用于信任网络或容器需要被直接寻址的场景,默认nat模式仍应作为首选。 - 规则结构不稳定:index.md 明确声明这是开发用途文档;且桥驱动在初始化时会删除自建链、随网络恢复重建,filter-FORWARD 链本身不清空,守护进程重启后规则顺序可能与首次创建时不同。
- 环境限制:golden 文档只在 iptables 后端下生成(firewalld 运行、rootless、nftables 后端下测试直接 skip);firewalld reload 时规则会被清空,守护进程通过 dbus 事件触发重建。
- 验证方式:若需自行核对规则变化,可参考 iptablesdoc_linux_test.go 的流程——运行测试后用
TESTFLAGS='-update'刷新 generated/ 下的参考文档,并同步更新 templates/ 中的文字说明(模板文字改动不被 golden 比对覆盖,需人工评审 diff)。
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