Moby 中 nftables 下容器端口映射的规则生成机制:详解 `docker-bridges` 表与 `-p 8080:80` 的三条关键规则
当 dockerd 使用 nftables 防火墙后端时,每个 -p 端口映射并不是一条孤立的规则,而是 ip docker-bridges 表中一组协同工作的链、映射表与 drop/dnat 规则的产物。本文以仓库中自动生成的文档 usernet-portmap.md 为主体,完整梳理"用户自定义桥接网络上、带已发布端口的容器"这一场景下的全部 nftables 规则,并结合 nftabler 源码说明每条规则是如何被创建的。读完后,你可以独立看懂 nft list table ip docker-bridges 的输出,并能从源码层面解释 DNAT、direct-access 拦截与未发布端口丢弃各自的实现位置。
1. 文档来源与适用边界
usernet-portmap.md 是一个自动生成的文件(文件首行即标注 This is a generated file; DO NOT EDIT),它由集成测试 TestBridgeNftablesDoc 生成:测试启动一个真实的 dockerd 实例,创建网络、运行容器,然后用 nft -s list table ip docker-bridges 抓取规则集,再与 templates/usernet-portmap.md 文本模板合并生成 Markdown,最后与 generated/ 目录下的 golden 文件做 diff,规则不一致则测试失败。
需要明确两点适用边界(来自 nftablesdoc/index.md 与测试代码):
- 这不是稳定接口:Docker 的 nftables 规则结构在版本之间会变化,该文档仅供开发参考;
- 只展示 IPv4 规则:IPv6 规则遵循同样的模式,只是位于不同的表(
ip docker-bridges与ip6 docker-bridges);表会在每次 Docker 启动时重建; - 测试在 firewalld 运行中、rootless 模式或非 nftables 后端下会自动跳过(见 nftablesdoc_linux_test.go 中的
skip.If判断),即本场景要求iptables兼容层由 nftables 提供。
2. 复现场景:两条命令
文档描述的场景等价于以下操作:
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 8080:80 --name c1 busybox
要点:
-o com.docker.network.bridge.name=bridge1指定了网桥设备名(对应 nftables 规则中大量出现的bridge1接口名),其常量在源码中为 bridge.BridgeName;- 测试中通过
network.WithOption(bridge.BridgeName, nw.name)与network.WithIPAM(...)构造同样的选项(见 nftablesdoc_linux_test.go),固定使用192.0.2.0/24/192.0.2.1等文档专用网段,保证生成的规则可复现、可 golden diff; -p 8080:80在测试里表示为networktypes.MustParsePort("80/tcp")到HostPort: "8080"的 PortMap。
执行后,ip docker-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;
ip daddr 192.0.2.2 iifname != "bridge1" counter drop comment "DROP DIRECT ACCESS"
}
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"
}
}
3. 表级结构:基链 + verdict map 的调度模型
Docker 在 nftables 中不采用 iptables 时代"每条规则平铺"的方式,而是把调度逻辑集中在 5 个基链(base chain)中,每个网络只挂一组私有链。链与映射表的名称常量定义在 nftabler.go:
| 名称 | 类型/钩子 | 职责 |
|---|---|---|
filter-FORWARD |
filter, forward | 前向流量的总入口,通过 vmap 按出/入接口分派到各网络链 |
nat-POSTROUTING |
nat, postrouting | SNAT/伪装总入口,按接口 vmap 分派 |
nat-PREROUTING |
nat, prerouting | 目标 IP 为本机时跳入 nat-prerouting-and-output 做 DNAT |
nat-OUTPUT |
nat, output | 本机发起的流量同样走 DNAT(非回环目标),支持主机直连映射端口 |
raw-PREROUTING |
filter, prerouting, priority raw | 在 conntrack 之前丢弃对容器 IP 的"直接访问" |
nat-prerouting-and-output |
普通链 | 存放所有端口映射的 DNAT 规则 |
核心调度规则只有两条,写在 init 中:
chain filter-FORWARD {
oifname vmap @filter-forward-in-jumps # 进入某桥接网络 → 该网络的 forward-in 链
iifname vmap @filter-forward-out-jumps # 离开某桥接网络 → 该网络的 forward-out 链
}
vmap(ifname : verdict 类型的映射表)是性能与隔离的关键:与 Docker 网络无关的流量完全不经过任何 per-network 规则;进入/离开 Docker 网络的流量也只经过对应网络自己的链。新增网络时,NewNetwork 通过 configure() 向四个 vmap 各插入一个 "bridge1" : jump ... 元素(见 network.go),这正是上文表中四个 *-jumps map 出现 "bridge1" 条目的来源。
另一个值得注意的点(index.md 明确指出):Docker 不使用 filter-INPUT 与 filter-OUTPUT 钩子。因为来自宿主物理网络或宿主自身的包是被"路由进"桥接网络的,走的是 filter-FORWARD;这也解释了为什么 nat-OUTPUT 中有一条与 nat-PREROUTING 对称的 fib daddr type local ... jump nat-prerouting-and-output 规则——主机自己访问 8080 时同样要命中 DNAT。
4. 新网络 bridge1 的链集:与 docker0 的对比
新网络拥有自己的一组链,与 docker0 的链结构相同(docker0 上没有容器,所以没有端口规则),差异全部来自端口映射:
入方向 filter-forward-in__bridge1(对比 docker0 同名链):
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-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"
}
filter-forward-in__ / filter-forward-out__ / nat-postrouting-in__ / nat-postrouting-out__ 四段链名分别由 chainFilterFwdIn 等函数拼出。filter-forward-in__* 链的默认行为由 configure 统一建立:
ct state established,related counter accept—— 放行已建立/相关的连接(conntrack 规则,入/出两条链各一条);iifname "bridge1" counter accept comment "ICC"—— 仅允许来自本网络内部的通信(Inter-Container Communication);网络设置了EnableICC=false时该规则动作变为drop(iccVerdict逻辑,network.go);- 最后一条兜底:网络
Unprotected(即gw_mode=nat-unprotected)时为counter accept comment "UNPROTECTED",否则为counter drop comment "UNPUBLISHED PORT DROP"—— 即除已发布端口外,外部到容器 IP 的任何新连接都被丢弃。
出方向与 NAT 侧:
chain filter-forward-out__bridge1 {
ct state established,related counter accept
counter accept comment "OUTGOING"
}
chain nat-postrouting-out__bridge1 {
oifname != "bridge1" ip saddr 192.0.2.0/24 counter masquerade comment "MASQUERADE"
}
出方向默认放行(OUTGOING 规则见 network.go),容器出网的回程报文靠 conntrack 放行;nat-postrouting-out__bridge1 则对离开网桥的流量做 masquerade,把源地址改写成宿主机出口地址(当 gw_mode=routed 即 Routed=true 时不生成该 masquerade 规则,见 network.go)。
5. 已发布端口的三条关键规则
-p 8080:80 在 nftables 中落地为三条职责不同的规则,分别位于三个不同的钩子阶段。
5.1 FORWARD 阶段放行映射端口
chain filter-forward-in__bridge1 {
...
ip daddr 192.0.2.2 tcp dport 80 counter accept
...
}
这条规则由 setPerPortForwarding 生成:对每个端口绑定在 fwdInPortsRuleGroup 组内写入 <ip> daddr <容器IP> <proto> dport <容器端口> counter accept。它的作用是让 DNAT 改写后的包(目的变为 192.0.2.2:80)能通过 forward 链的白名单机制——注意该链的语义是"白名单制":conntrack 放行 + ICC 放行 + 显式 accept 的发布端口,其余一律 UNPUBLISHED PORT DROP。
源码中还有一个细节:多个宿主端口映射到同一容器端口时会生成完全相同的规则,因此添加时用 IgnoreExist: true 去重,删除时容忍规则缺失(port.go 的注释解释了不做引用计数的原因)。
5.2 raw 阶段拦截直接访问(DROP DIRECT ACCESS)
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"
}
这条规则保证:在 gw_mode=nat(默认值)下,外部网络无法直接访问容器 IP 192.0.2.2,外部必须经宿主机的映射端口进入。它由 filterDirectAccess 在每个 endpoint 加入网络时添加(容器启动、获得 IP 192.0.2.2 时),删除容器时移除。从源码可以看到它的完整豁免条件(endpoint.go):
- 网络是
internal时不加(internal 网络本身就无出网/映射语义); gw_mode=nat-unprotected(Unprotected)或gw_mode=routed(Routed)时不加——routed 模式下外部本就路由直达容器网段;- daemon 级启用了 direct routing(
AllowDirectRouting)时不加。
规则条件 iifname != "bridge1" 意味着宿主自身(包从 bridge1 接口进入)始终可以直接访问容器 IP——注释明确说"host 永远可以直接访问自己网络的容器"。若配置了 TrustedHostInterfaces,这些接口会并入 iifname != {...} 的例外集合。选择在 raw 表(priority raw)执行是因为 raw 钩子先于 conntrack 生效,可以干净地把这类包丢弃而不建立连接跟踪条目。
5.3 DNAT:宿主端口 8080 → 容器端口 80
chain nat-prerouting-and-output {
iifname != "bridge1" tcp dport 8080 counter dnat to 192.0.2.2:80 comment "DNAT"
}
该链是 nat-PREROUTING 与 nat-OUTPUT 共同的落点(见第 3 节的 fib daddr type local counter jump nat-prerouting-and-output)。规则由 setPerPortDNAT 生成,各条件的来源是:
iifname != "bridge1":仅当 daemon 未启用 hairpin(config.Hairpin为 false,即存在 userland proxy 的场景)时生成。因为启用 proxy 后,"容器通过宿主映射端口访问自己"的请求由 proxy 处理,内核里排除桥接接口自身的包即可;tcp dport 8080+dnat to 192.0.2.2:80:HostPort 与容器 IP/端口的直接映射。若HostPort == 0(NAT 未启用,例如 routed 模式)则直接跳过;若宿主侧与容器侧地址族不同(如 host IPv6 ↔ 容器 IPv4),NAT 无法完成,交由 docker-proxy 处理,同样不生成内核规则;- 若映射绑定到特定宿主 IP(
HostIP非 any),规则会额外加上<fam> daddr <HostIP>匹配条件(daddrMatch); - IPv6 侧还会附加
ip6 saddr != fe80::/10跳过链路本地源地址(v6LLSkip)。
此外,映射到回环地址的端口(-p 127.0.0.1:8080:80)会由 filterPortMappedOnLoopback 在 raw-PREROUTING 中追加 iifname != lo ... counter drop comment "DROP REMOTE LOOPBACK" 规则,拒绝远端主机连接回环映射端口——这正是同目录文档 usernet-portmap-lo.md 覆盖的场景。
6. 一次完整的请求路径
把上面的规则串起来,外部客户端访问 宿主机IP:8080 的包会依次经历:
- raw-PREROUTING:目的地址是宿主机(经 DNAT 前的视角并非容器 IP),
DROP DIRECT ACCESS规则不命中,放行;(若目的直接是192.0.2.2,则在这里被 drop。) - nat-PREROUTING →
fib daddr type local命中 →nat-prerouting-and-output:tcp dport 8080命中,目的被改写成192.0.2.2:80; - 路由阶段把包送入
bridge1,进入 filter-FORWARD,vmap 分派到filter-forward-in__bridge1:新连接首包不是 established/related,也不是 ICC,最终命中ip daddr 192.0.2.2 tcp dport 80 counter accept放行;其余未发布端口的包落入UNPUBLISHED PORT DROP; - 容器应答包回到宿主:
filter-forward-out__bridge1的ct state established,related counter accept放行; - nat-POSTROUTING → vmap 分派到
nat-postrouting-out__bridge1:源地址在192.0.2.0/24且出接口不是 bridge1 时执行 masquerade,客户端看到的对端地址是宿主机 IP,回包由此被正确路由回 DNAT 后的连接。
这套"DNAT 进、masquerade 出、conntrack 保状态、白名单管准入"的组合,与 iptables 后端行为等价,只是用 verdict map 做了 per-network 隔离。
7. 如何在真实环境验证
在 nftables 后端的 Linux 宿主上(iptables 命令由 iptables-nft 提供,且 firewalld 未托管规则),可以按文档流程复现并核对:
# 创建网络并运行容器(与第 2 节相同)
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 8080:80 --name c1 busybox
# 查看带计数器的完整表(对应文档抓取方式)
nft -s list table ip docker-bridges
# 验证行为
curl http://localhost:8080 # 经 DNAT 可达(nat-OUTPUT 路径)
curl http://<宿主机IP>:8080 # 可达
curl http://192.0.2.2/ # 直接访问容器 IP:被 DROP DIRECT ACCESS 拒绝
仓库内还有两套测试可以交叉印证规则生成逻辑:
- 单元测试 TestNftabler 通过模拟 nftables 对象比对各配置组合(icc/internal/gwm/hairpin 等)下的 golden 规则文件,testdata/TestNftabler 目录下数百个
__ip.golden/__ip6.golden文件可直接查看DROP DIRECT ACCESS、UNPUBLISHED PORT DROP、DNAT等规则在不同gw_mode(nat / nat-unprotected / routed)下的变化; - 集成文档测试 TestBridgeNftablesDoc 用真实 daemon 抓取规则并与 golden 对比,当内核/驱动变更导致规则漂移时失败,维护者按测试包注释的修复流程(检查 diff → 更新对应 templates 描述 →
TESTFLAGS='-update'重跑)更新文档。
8. 小结与注意事项
ip docker-bridges表由 NewNftabler 在 daemon 启动时创建,每次 Docker 启动都会重建;IPv4/IPv6 分别是同构的两张表;-p映射 = 三条规则的组合:filter-forward-in__<bridge>中的白名单 accept(setPerPortForwarding)、nat-prerouting-and-output中的 DNAT(setPerPortDNAT),以及 endpoint 级raw-PREROUTING的 direct-access drop(filterDirectAccess);UNPUBLISHED PORT DROP是默认准入策略,nat-unprotected网络改为UNPROTECTED全放行;routed网络不做 masquerade,external 直连容器网段;- 该规则结构属于开发期接口,不承诺跨版本稳定;排查问题时以当前版本实际
nft list输出为准,并参考 nftablesdoc 下的其他场景文档(loopback 映射、无 userland proxy、no-ICC、internal、routed、nat-unprotected)来对照差异。
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