首页
/ Moby (Docker Engine) 深入解析:daemon 启动时如何用 nftables 初始化 bridge 网络防火墙规则

Moby (Docker Engine) 深入解析:daemon 启动时如何用 nftables 初始化 bridge 网络防火墙规则

2026-09-06 14:41:33作者:柏廷章Berta

本文以 Moby 仓库中 integration/network/bridge/nftablesdoc/templates/new-daemon.md 文档为主体,完整讲解 Docker Engine 以 nftables 为防火墙后端时,daemon 首次启动、创建默认 bridge 网络(docker0)后写入内核的整套 nftables 规则:两张表、基础链、verdict map 的分发机制、逐网络链的规则语义,以及 policy 与内核 IP 转发 sysctl 的联动关系。读完本文,你既能读懂 nft list table ip docker-bridges 的完整输出,也能对照仓库源码(nftabler 初始化逻辑逐网络规则IP 转发配置)理解每条规则的来龙去脉,并知道如何用仓库自带的黄金文件(golden file)测试机制验证和复现这套规则。

一、这套 nftables 规则文档是怎么来的:doc-as-test 机制

在展开规则细节之前,先说明本文所依据文档的产出方式,这直接关系到如何验证文中内容:

  • 该文档是模板 templates/new-daemon.md,模板中通过 {{index . "Ruleset4"}} 等占位符插入实际捕获的 nftables 输出;
  • 生成它的测试是 TestBridgeNftablesDoc(包注释见 nftablesdoc_linux_test.go 头部):测试在独立网络命名空间中启动一个真实的 dockerd,创建网络与容器,然后执行 nft -s list table ip docker-bridges 捕获规则(见 runNftables),把输出按链/映射分块后交给 text/template 渲染出 Markdown;
  • 渲染结果会与 generated/new-daemon.md(golden 参考文件)做 diff,不一致则测试失败;变更规则后需用 TESTFLAGS='-update' 重新生成参考文档;
  • 测试有明确前提:Linux、root 权限、firewall 后端驱动为 nftables(firewalld 运行中会跳过),见 测试入口处的 skip 条件

该目录的 index.md 还给出了几条适用于整份 nftables 文档集的关键事实:

  1. 表在每次 Docker 启动时都会重建(The tables are re-created each time Docker starts);
  2. Docker 不使用 filter-INPUT 和 filter-OUTPUT hook:来自宿主机物理网络或宿主机自身的包会被路由进 bridge 网络,因此它们在 FORWARD 链上被处理;
  3. IPv6 规则与 IPv4 规则遵循相同模式,只是位于不同的表(ip docker-bridgesip6 docker-bridges),因此文档集只展示 IPv4 规则;
  4. 文档头部有开发用途警告:Docker 的 nftables 规则结构会在不同版本之间变化,这不是稳定接口

二、daemon 启动后创建的表:结构总览

new-daemon.md 模板 的原始描述与 golden 输出:daemon 启动时会创建两张表——ip docker-bridges(IPv4 规则)和 ip6 docker-bridges(IPv6 规则)。每张表包含若干基础链(base chain)和空的 verdict map(判决映射),随后再为默认 bridge 网络 docker0 添加规则。new-daemon.md 对应的场景就是"裸 daemon 刚启动、只有 docker0",完整捕获输出如下:

table ip docker-bridges {
    map filter-forward-in-jumps {
        type ifname : verdict
        elements = { "docker0" : jump filter-forward-in__docker0 }
    }

    map filter-forward-out-jumps {
        type ifname : verdict
        elements = { "docker0" : jump filter-forward-out__docker0 }
    }

    map nat-postrouting-in-jumps {
        type ifname : verdict
        elements = { "docker0" : jump nat-postrouting-in__docker0 }
    }

    map nat-postrouting-out-jumps {
        type ifname : verdict
        elements = { "docker0" : jump nat-postrouting-out__docker0 }
    }

    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 {
    }

    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"
    }
}

这套结构的表名与所有基础链、映射名均在源码常量中定义,见 nftabler.go 的常量块docker-bridgesfilter-FORWARDnat-POSTROUTINGnat-PREROUTINGnat-OUTPUTnat-prerouting-and-outputraw-PREROUTING,以及四个 *-jumps 映射。

从源码结构看,整体设计思路可以概括为"基础链只做分发,具体逻辑放在逐网络链中":

  • filter-FORWARD 基础链只有两条规则,把"进入某个 bridge 网络"的包跳入该网络的 ingress 链、把"离开某个 bridge 网络"的包跳入该网络的 egress 链。与 Docker 无关的包不需要遍历任何逐网络 filter-forward 规则;属于 Docker 网络的包也只需要遍历与本网络相关的规则;
  • nat-POSTROUTING 基础链同理,只有两条跳转到逐网络 NAT 链的规则。

这一设计在 init 函数的注释中有明确说明。底层表/链/映射的创建则委托给内部封装 daemon/libnetwork/internal/nftables/nftables_linux.go

三、filter-FORWARD 基础链:用 verdict map 做 O(1) 分发

filter-FORWARD 是一条基础链(base chain),type 为 filter,hook 为 forward——等价于 iptables filter 表中的内建 FORWARD。它初始化时只带两条规则,分别以出接口名、入接口名作为 verdict map 的查找键:

chain filter-FORWARD {
    type filter hook forward priority filter; policy accept;
    oifname vmap @filter-forward-in-jumps
    iifname vmap @filter-forward-out-jumps
}

两条 nft 规则对应源码中的 init 中创建的两个 Ruleoifname vmap @filter-forward-in-jumpsiifname vmap @filter-forward-out-jumps。这里有个容易混淆的点:in 映射匹配的是 oifname(出接口),即"要进入该 bridge 网络的包";out 映射匹配的是 iifname(入接口),即"从该 bridge 网络出去的包"。

verdict map 中每个 bridge 网络一个元素,各跳转到包含该 bridge 规则的链。以 docker0 为例,映射元素为 "docker0" : jump filter-forward-in__docker0。因此:对于既不进入也不来自任何 Docker bridge 设备的包,verdict map 中查不到跳转,包就不再需要这条基础链的任何后续处理——这正是该方案相比"一个大 FORWARD 链"的关键优化。

映射元素本身是"网络级"的,随网络创建动态加入,见 NewNetwork/configure 中为四个映射创建的 MapElement

policy 不是固定的 accept:与内核 IP 转发 sysctl 的联动

上述 filter-FORWARD 链的 policy 是 accept,但原始文档特别指出存在例外:

  • 对 IPv4:若 sysctl net.ipv4.ip_forward 之前没有设为 1,而 daemon 在创建启用 IPv4 的 bridge 网络时自己把它设成了 1,则 policy 变为 drop
  • 对 IPv6:同理,但对应的 sysctl 是 /proc/sys/net/ipv6/conf/default/forwarding/proc/sys/net/ipv6/conf/all/forwarding

这个行为可以在 setup_ip_forwarding.go 中得到完整印证:

  • checkIPv4Forwarding 检查 net.ipv4.ip_forward,若未开启会报错并提示:自行开启转发、配好主机防火墙,或用 --ip-forward=false 关闭该检查(见 checkIPv4Forwarding);
  • setupIPv4Forwarding 在 daemon 代为把 ip_forward 改为 1(即发生了 changed)且需要默认拒绝时,调用 FilterForwardDrop 把 forward 链的默认策略设为 drop(见 setupIPv4Forwarding);IPv6 的 setupIPv6Forwardingdefault.forwardingall.forwarding 两个文件做同样处理,且任一项失败会回滚已改的 sysctl。

设计意图一目了然:daemon 只有在"替用户打开了内核转发开关"这种改变了主机安全姿态的情况下,才把 FORWARD 默认策略收紧为 drop,避免默认放行带来额外风险;而逐网络链自身也带有兜底的 drop 规则(见下节),因此具体流量不依赖该 policy。

四、逐网络的 filter-FORWARD 规则:docker0 的 ingress/egress 链

为默认 bridge 网络添加的链,命名规则是"基础链 hook 名 + 该网络的 bridge 设备名"。以 docker0 为例,共四条链(filter-forward-in__docker0filter-forward-out__docker0nat-postrouting-in__docker0nat-postrouting-out__docker0),命名函数见 chainFilterFwdIn/chainNatPostRtOut 等

进入网络方向的链 filter-forward-in__docker0

凡是被 filter-forward-in__* 处理的包,若被 accept 就会被投递进该 bridge 网络。docker0 的链为:

chain filter-forward-in__docker0 {
    ct state established,related counter accept
    iifname "docker0" counter accept comment "ICC"
    counter drop comment "UNPUBLISHED PORT DROP"
}

三条规则逐一说明:

  1. conntrack accept established/related 流。注意一个 nftables 语义细节(原始文档特别强调):accept 仅终结"这条基础链"的处理,被接受的包仍可能继续被同一 hook 上注册的其他基础链处理;
  2. accept 源自网络内部的包iifname "docker0"),因为容器间通信(Inter-Container Communication,ICC)默认开启;
  3. drop 其余所有包,因为此时网络内没有任何容器发布了端口。这一点很重要:它意味着不依赖 filter-FORWARD 链的默认 policy——即使 policy 是 ACCEPT,只要容器没有发布端口/协议,包也会被丢弃。

这三条规则与源码逐条对应。在 network.go 的 configure 中:

  • 首条是 ct state established,related counter acceptL99-L108);
  • ICC 规则的判决由配置决定:iccVerdict 默认 accept,若 noICC 则改为 dropL110-L113),对应 --icc=false 网络的行为;
  • 末尾规则在正常网络上是 counter drop comment "UNPUBLISHED PORT DROP";只有当网络标记为 Unprotectednat-unprotected 网关模式)时才变为 counter accept comment "UNPROTECTED"L162-L175)。发布端口的规则会插在 ICC 组与末尾组之间(对应 RuleGroup 分组常量fwdInICCRuleGroupfwdInPortsRuleGroupfwdInFinalRuleGroup)。

此外,若网络开启了 routed 网关模式,ingress 链还会追加一条 ICMP 放行规则(IPv4 为 ip protocol icmp,IPv6 为 meta l4proto ipv6-icmp,comment ICMP),见 routed 分支

离开网络方向的链 filter-forward-out__docker0

凡是被 filter-forward-out__* 处理的包,都源自该 bridge 网络:

chain filter-forward-out__docker0 {
    ct state established,related counter accept
    counter accept comment "OUTGOING"
}

docker0 链中仅两条规则(见 源码中 OUTGOING 规则):

  1. conntrack accept established/related 流;
  2. 一条无条件 accept——网络中的容器可以访问外部网络。

作为对照:--internal 网络的 egress 链则会改为 oifname != <bridge> counter drop comment "INTERNAL NETWORK EGRESS"(丢弃一切非源自我网络的出向流量),ingress 链同样加入 INTERNAL NETWORK INGRESS drop 规则,见 internal 分支

五、nat-POSTROUTING:逐网络的 NAT 分发与 masquerade

filter-FORWARD 相同,nat-POSTROUTING 基础链也通过 verdict map 把包分发到逐网络链:

chain nat-POSTROUTING {
    type nat hook postrouting priority srcnat; policy accept;
    iifname vmap @nat-postrouting-out-jumps
    oifname vmap @nat-postrouting-in-jumps
}

在 docker0 的 nat-postrouting 链中,针对"离开网络"的包只有一条 masquerade 规则:

chain nat-postrouting-in__docker0 {
}

chain nat-postrouting-out__docker0 {
    oifname != "docker0" ip saddr 172.17.0.0/16 counter masquerade comment "MASQUERADE"
}

语义拆解:

  • oifname != "docker0":只处理离开 docker0、从宿主机其他接口出去的包;
  • ip saddr 172.17.0.0/16:源地址属于默认 bridge 子网(默认网段);
  • masquerade:做 SNAT 式源地址伪装,让容器出网流量的源地址变成宿主机的出口地址。

这与源码中 Masquerade/SNAT 分支一致:默认判决是 masquerade(comment MASQUERADE);当配置了 HostIP(routed 模式下由宿主机指定出口 IP)时,判决改为 snat to <HostIP>(comment SNAT)。且只有 Masquerade 开启且非 Routed 模式才创建该 egress masquerade 规则。nat-postrouting-in__docker0 之所以是空的,是因为它只在开启 hairpin(无 userland proxy)时才会填入一条 fib saddr type local ... masquerade comment "MASQUERADE FROM HOST" 规则,让宿主机自身访问已发布端口时也能正确回源——new-daemon 场景未启用,故为空链。

六、其余基础链:nat-PREROUTING、nat-OUTPUT 与 raw-PREROUTING

golden 输出中还有三条链未在模板正文中单独展开,结合 init 源码 可以补全其作用:

  • nat-PREROUTINGhook prerouting priority dstnat):单条规则 fib daddr type local counter jump nat-prerouting-and-output,即只对目的地是"本机地址"的入向包做目的 NAT(DNAT,端口发布规则的落点就在这里);
  • nat-OUTPUThook output priority dstnat):宿主机自身发出的包同样要做端口发布的 DNAT,规则形如 ip daddr != 127.0.0.0/8 fib daddr type local counter jump nat-prerouting-and-output。源码中该规则写作 ip daddr != 127.0.0.1/8 ...skipLoopback 构造),捕获输出中 nft 将其规范化显示为 127.0.0.0/8;当 hairpin 启用时不跳过 loopback 前缀,以便 127.0.0.1 上发布的端口可以从本机访问;
  • nat-prerouting-and-output:空的普通链,作为 PREROUTING 与 OUTPUT 两条基础链共享的跳转目标,端口映射规则将写入此链;
  • raw-PREROUTINGhook prerouting priority raw,policy accept):当前无规则,预留给 raw 表优先级(早于 conntrack)使用,源码中仅创建基础链本身(rawPreroutingChain 创建)。

另外注意:new-daemon 场景下 IPv6 表创建失败(如内核无 IPv6 支持)不会阻止 daemon 启动——IPv4 仍可用,daemon 只记录警告,见 init 中的 IPv6 容错处理

七、如何在自己的机器上验证这套规则

规则是"活"的,最好的理解方式是实际观察。在 nftables 后端下(Linux、root):

# 观察整个表(含计数器)
sudo nft -s list table ip docker-bridges
# IPv6 表
sudo nft -s list table ip6 docker-bridges

与本文 golden 输出对比时应注意:每创建一个用户自定义 bridge 网络,四个 *-jumps 映射会各新增一个元素,并新增对应 __<bridge名> 的四条链;每发布一个端口,nat-prerouting-and-outputfilter-forward-in__<bridge> 中会插入新的 DNAN/放行规则(这些增量场景在 index.md 的场景列表中分别对应 usernet-portmap.mdusernet-portmap-noicc.mdusernet-internal.mdusernet-portmap-routed.mdusernet-portmap-natunprot.md 等文档,模板位于 templates 目录,生成结果位于 generated 目录)。

若要在仓库内重新生成这些文档,运行 TestBridgeNftablesDoc 即可(要求非 rootless、firewall 后端为 nftables 且无 firewalld);规则变更后测试会因与 golden 文件不一致而失败,届时需检查 diff、更新模板说明,并用 TESTFLAGS='-update' 刷新 generated 目录下的参考文档。

八、小结

  • daemon 启动即在 ip/ip6 docker-bridges 表中建立"基础链 + 空 verdict map"骨架,随后为 docker0 注入逐网络链;每次启动都会重建这些表;
  • filter-FORWARDnat-POSTROUTING 两条基础链只做接口名分发,Docker 无关流量零开销穿过;
  • filter-forward-in__docker0UNPUBLISHED PORT DROP 提供了与默认 policy 解耦的兜底拒绝,policy 是否收紧(accept/drop)只取决于 daemon 是否代用户打开了内核 IP 转发(net.ipv4.ip_forward / IPv6 两个 forwarding sysctl,可被 --ip-forward=false 旁路);
  • 出网默认 masquerade,routed 模式改用 snat to HostIPnat-unprotected 网络则以 UNPROTECTED accept 替代末尾 drop;
  • 整套结构由 nftabler 包libnetwork 的 nftables 封装 之上实现,并由 doc-as-test 黄金文件测试 保证文档与实际内核规则持续一致。

需要再次提醒:仓库文档明确声明 Docker 的 nftables 规则结构不是稳定接口,会随版本变化;在生产环境中请将其视为实现细节而非可编程/可依赖的契约。如果你的部署仍使用 iptables 后端,同目录下有结构对应的 iptablesdoc 文档集 可作对照。

登录后查看全文
热门项目推荐
相关项目推荐