Moby Docker Engine 中“用户自定义网络发布端口“的 nftables 规则逐条解析

发布时间:2026/9/7 18:44:49
Moby Docker Engine 中“用户自定义网络发布端口“的 nftables 规则逐条解析 Moby Docker Engine 中用户自定义网络发布端口的 nftables 规则逐条解析【免费下载链接】mobyThe Moby Project - a collaborative project for the container ecosystem to assemble container-based systems项目地址: https://gitcode.com/GitHub_Trending/mo/moby本文以 MobyDocker Engine 上游项目仓库中自动生成的 nftables 文档integration/network/bridge/nftablesdoc/templates/usernet-portmap.md为核心素材完整解读当你在一个用户自定义 bridge 网络上运行带端口映射published port的容器时Docker Engine 在ip docker-bridges表中写入的全部 nftables 规则jumps 映射、filter-FORWARD分流、DNAT 端口转发、DROP DIRECT ACCESS安全丢弃与MASQUERADE源地址转换。读完后你能独立读懂nft list table ip docker-bridges的输出并能把每条规则对应到 Moby 源码中的具体实现位置。这份文档是怎么来的由集成测试自动生成的活文档与手工维护的文档不同nftablesdoc目录下的文档是由测试TestBridgeNftablesDoc自动生成的测试会启动一个 Docker daemon、创建网络并运行容器、捕获真实生效的 nftables 输出再将其与templates/目录下的 markdowntext/template合并生成各场景的文档并与generated/中的黄金参考文件做 diff——规则一旦变化而文档未更新测试即失败。这一点在 nftablesdoc_linux_test.go 的文件头注释中有明确说明。本文对应的场景模板是 usernet-portmap.md模板文件对应的生成产物是 generated/usernet-portmap.md总索引在 index.md。需要留意两个前提来自 index.md 的警示说明该文档面向开发用途Docker 的 nftables 规则结构在版本之间可能变化不是稳定接口IPv6 规则遵循与 IPv4 相同的模式只是放在ip6 docker-bridges表中文档中仅展示 IPv4 规则这些表在每次 Docker 启动时都会重建。等价 CLI 操作场景是如何被构造的模板文件给出的场景定义是创建一个名为bridge1的用户自定义网络子网192.0.2.0/24网关192.0.2.1并在其上运行一个发布了 8080→80 端口的容器$ docker network create \ -o com.docker.network.bridge.namebridge1 \ --subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1 $ docker run --network bridge1 -p 8080:80 --name c1 busybox在测试代码中这个场景由 nftablesdoc_linux_test.go 中index切片的usernet-portmap.md条目驱动网络地址取自固定的文档子网docNetworks []string{192.0.2.0/24, ...}第 47 行。真正的创建过程在createBridgeNetworks第 277–315 行通过network.WithIPAM(docNetworks[i], docGateways[i])指定子网与网关network.WithOption(bridge.BridgeName, nw.name)指定桥名再以container.WithPortMap(ctr.portMappings)运行容器端口映射为80/tcp→ 宿主8080。值得注意的是测试的运行环境约束TestBridgeNftablesDoc 第 188–191 行系统防火墙后端必须是nftables、daemon 必须 rootful 运行、且不能在 firewalld 之下每个场景还会被放进独立的 L3 网段netns中以避免场景之间互相污染。完整的ip docker-bridges表执行上述两条命令后ip docker-bridges表的内容如下取自生成文档 generated/usernet-portmap.md其中counter计数为 0 已省略展示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 } }新网络拥有自己的一组链*__bridge1结构与默认网络docker0的链相似此场景中docker0上尚无容器所以其filter-forward-in__docker0里没有放行任何已发布端口的规则。下面按数据包流向逐块解析。filter-FORWARD 与 jumps 映射按网桥分流模板文档指出filter-INPUT钩子不被 Docker 使用来自宿主物理网络或宿主本身的包会被路由进 bridge 网络因此命中filter-FORWARD同理filter-OUTPUT也不被使用。filter-FORWARD链只有两条vmap虚拟映射指令oifname vmap filter-forward-in-jumps按出接口包要转发到的网桥查映射跳入该桥的filter-forward-in__br链做入向过滤iifname vmap filter-forward-out-jumps按入接口包来自哪个网桥跳入filter-forward-out__br链做出向过滤。四个mapfilter-forward-in/out-jumps、nat-postrouting-in/out-jumps都是type ifname : verdict的接口名→verdict 映射每个 bridge 网络注册时追加一条元素。这种一个表 每桥一组链 映射分流的结构避免了为每个网络单独安装 hook 链。nat-PREROUTING / nat-OUTPUT 与 DNAT8080 → 容器 80发布端口的核心是 DNAT。nat-PREROUTING处理进入宿主的数据包和nat-OUTPUT处理宿主本地发出的数据包条件是ip daddr ! 127.0.0.0/8 fib daddr type local都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 }即把目的为宿主地址、协议端口为 8080 的 TCP 包 DNAT 到容器c1192.0.2.2的 80 端口。iifname ! bridge1前缀在关闭 hairpin 时用于防止同桥回环匹配——这一点可以从源码 port.go 的setPerPortDNAT得到印证func (n *network) setPerPortDNAT(pbs []types.PortBinding, updater func(nftables.Obj), ipv nftables.Family) { var proxySkip string if !n.fw.config.Hairpin { proxySkip fmt.Sprintf(iifname ! %s , n.config.IfName) } ... Rule: []string{ proxySkip, v6LLSkip, daddrMatch, pb.Proto.String(), dport, strconv.Itoa(int(pb.HostPort)), counter dnat to, net.JoinHostPort(pb.IP.String(), strconv.Itoa(int(pb.Port))), comment DNAT, },从源码结构看DNAT 规则由addPortMapping统一编排port.go 第 66–72 行setPerPortForwarding非 nat-unprotected 时写入过滤链的放行规则、setPerPortDNAT、setPerPortHairpinMasqhairpin 场景的 MASQUERADE、filterPortMappedOnLoopback端口绑定到 loopback 时的处理见 usernet-portmap-lo.md 场景。raw-PREROUTING 中的 DROP DIRECT ACCESS禁止绕过 DNAT 直连容器默认网关模式gw_mode为nat下容器 IP 对宿主外网不可路由——外部访问必须走 DNAT 路径。为此raw-PREROUTING中有一条丢弃规则raw 表优先级高于 filter/nat确保在 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 }含义是目的地址为容器 IP192.0.2.2且不是从bridge1网桥进来的包一律丢弃——外部主机无法绕过 8080 发布端口直接访问容器。该规则的源码位于 endpoint.goChain: rawPreroutingChain, Group: rawPreroutingPortsRuleGroup, Rule: []string{ string(fam), daddr, epIP.String(), iifname ! {, n.config.IfName, ,, ifNames, } counter drop comment DROP DIRECT ACCESS, },该文件注释第 56 行附近说明此规则在网关模式为nat-unprotected或routed时不会生成——这正是 usernet-portmap-natunprot.md 与 usernet-portmap-routed.md 两个对照场景存在的原因。网关模式通过网络选项标签传入定义见 labels.gocom.docker.network.bridge.gateway_mode_ipv4以及 IPv6 版本合法取值nat/nat-unprotected/routed的解析在 bridge_linux.go 中。filter-forward-in__bridge1入向过滤链的三段式规则跳入filter-forward-in__bridge1的是出接口为 bridge1的转发包即发往该网络的流量。链内四条规则按顺序构成一个白名单模型chain filter-forward-in__bridge1 { ct state established,related counter accept // 1. 已建立/关联的返回流量放行 iifname bridge1 counter accept comment ICC // 2. 同桥容器互通inter-container communication ip daddr 192.0.2.2 tcp dport 80 counter accept // 3. 放行该容器的已发布端口 counter drop comment UNPUBLISHED PORT DROP // 4. 其余一律丢弃 }模板文档特别指出第 3 条开放容器已发布端口的规则ip daddr 192.0.2.2 tcp dport 80 counter accept——DNAT 之后的数据包目的已被改写为192.0.2.2:80正是在这里被放行的与第 4 条兜底的UNPUBLISHED PORT DROP形成对比未发布端口的直连请求会被过滤链丢弃。第 4 条兜底规则的写入位置见 network.go 中fwdInFinalRuleGroup组的counter drop comment UNPUBLISHED PORT DROP。若网络创建时指定com.docker.network.bridge.enable_iccfalse第 2 条 ICC 规则将被移除见 usernet-portmap-noicc.md 场景。filter-forward-out__bridge1则处理容器出向流量结构对所有网桥一致chain filter-forward-out__bridge1 { ct state established,related counter accept counter accept comment OUTGOING }即容器访问外网默认放行除非网络是--internal见 usernet-internal.md 场景。nat-postrouting-out__bridge1出网 MASQUERADE容器访问外部网络时源地址需要改写为宿主地址由nat-POSTROUTING经nat-postrouting-out-jumps映射分发的出向链完成chain nat-postrouting-out__bridge1 { oifname ! bridge1 ip saddr 192.0.2.0/24 counter masquerade comment MASQUERADE }含义源地址位于该网络子网192.0.2.0/24、且出接口不是本网桥的包执行 MASQUERADESNAT。oifname ! bridge1确保桥内互访不做地址转换。对照可见默认网络docker0的等价规则对的是172.17.0.0/16——即每个 bridge 网络各自维护一条以其子网为源前缀的 MASQUERADE 规则。端到端数据包流小结把上面的规则串起来一次外部客户端 → 8080 → 容器 80 端口的完整路径是包进入宿主raw-PREROUTING放行目的不是容器 IP 本身或来源即bridge1nat-PREROUTING命中nat-prerouting-and-output的 DNAT 规则目的地址改写为192.0.2.2:80包被路由进bridge1filter-FORWARD按oifname跳入filter-forward-in__bridge1由ip daddr 192.0.2.2 tcp dport 80 accept放行容器回包的源192.0.2.2经nat-postrouting-out__bridge1的 MASQUERADE 改写为宿主地址回程走ct state established,related accept快速放行。如何在自己的机器上验证若 Docker 以 nftables 作为防火墙后端可用以下命令查看同一张表与测试中 runNftables 使用的命令一致$ nft -s list table ip docker-bridges注意适用前提系统存在nft工具、daemon 以 nftables 后端运行默认行为受--iptables/防火墙驱动影响、非 rootless 且未运行 firewalld测试跳过条件即来源于此。规则中的counter计数随真实流量变化测试生成文档时会一并捕获计数值这正是文档中大量counter关键字的来源。相关场景文档与源码入口同一套模板机制还覆盖了多个对照场景全部可在 nftablesdoc/index.md 中找到均位于 generated/ 目录场景说明new-daemon.mddaemon 启动后的初始规则仅 docker0usernet-portmap-lo.md端口发布到 loopback 地址usernet-portmap-noproxy.md--userland-proxyfalse时无用户态代理usernet-portmap-noicc.md禁用容器互通enable_iccfalseusernet-internal.md--internal网络无 MASQUERADE/DNAT 出网usernet-portmap-routed.md网关模式routed无 DROP DIRECT ACCESSusernet-portmap-natunprot.md网关模式nat-unprotected对应的规则写入实现集中在 daemon/libnetwork/drivers/bridge/internal/nftabler/ 包network.go每桥的 filter/nat 链骨架、endpoint.go端点级 raw 表规则、port.go端口映射的 forwarding/DNAT/hairpin 规则、link.go与cleaner.go链路级规则与清理。其单元测试TestNftabler覆盖nat/nat-unprotected/routed等网关模式组合nftabler_test.gotestdata/下按参数组合保存了 golden 规则文件可作为更多组合形态的参考样本。【免费下载链接】mobyThe Moby Project - a collaborative project for the container ecosystem to assemble container-based systems项目地址: https://gitcode.com/GitHub_Trending/mo/moby创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考