首页
/ Moby(Docker Engine)中 --internal 网络的 nftables 防火墙规则剖析:从 usernet-internal 文档到 nftabler 源码实现

Moby(Docker Engine)中 --internal 网络的 nftables 防火墙规则剖析:从 usernet-internal 文档到 nftabler 源码实现

2026-09-06 14:46:28作者:余洋婵Anita

本文以 Moby 仓库中 nftables 集成文档的 usernet-internal 场景为骨架,完整讲解在 nftables 防火墙后端下,两个处于不同 --internal 用户自定义网络(ICC 开/关)中的容器会产生哪些 nftables 规则;并结合 nftabler 实现源码golden 测试数据,说明每条规则由哪段代码生成、如何复现,以及如何验证文档与真实规则的一致性。读完后,你可以看懂 docker network create --internal 在 nftables 后端的真实落盘效果,并能独立排查 internal 网络的连通性问题。

场景定义:两个 --internal 网络,ICC 开与关

usernet-internal.md 模板 描述的场景是:两个容器分别位于两个不同的 --internal(内部)桥接网络,其中一个网络允许容器间通信(ICC),另一个禁用 ICC。文档给出的等价操作命令如下:

docker network create \
  -o com.docker.network.bridge.name=bridgeICC \
  --internal \
  --subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
docker run --network bridgeICC --name c1 busybox

docker network create \
  -o com.docker.network.bridge.name=bridgeNoICC \
  --internal \
  --subnet 198.51.100.0/24 --gateway 198.51.100.1 bridge1
docker run --network bridgeNoICC --name c1 busybox

两个网络都使用 --internal 标志,区别在于 ICC 开关状态。需要注意的是:文档模板中的 "Equivalent to" 片段里第二个网络写作 enable_icc=true,而实际生成这份文档的测试定义(nftablesdoc_linux_test.gousernet-internal 一节)为第二个网络 bridgeNoICC 显式设置了 internal: true, noICC: true,即通过选项 com.docker.network.bridge.enable_icc=false 禁用 ICC。golden 规则(下文 counter drop comment "ICC")也印证了第二张网 ICC 是关闭的。

该场景在测试代码中的定义如下(nftablesdoc_linux_test.goindex 变量):

{
    name: "usernet-internal.md",
    networks: []networkDesc{{
        name:     "bridgeICC",
        internal: true,
        containers: []ctrDesc{{name: "c1"}},
    }, {
        name:     "bridgeNoICC",
        internal: true,
        noICC:    true,
        containers: []ctrDesc{{name: "c1"}},
    }},
},

子网地址来自同一文件中的全局变量:

var (
    docNetworks = []string{"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"}
    docGateways = []string{"192.0.2.1", "198.51.100.1", "203.0.113.1"}
)

完整规则集:table ip docker-bridges 的骨架

所有 bridge 网络的规则都集中在 ip docker-bridges 这张表里(IPv6 规则同构,位于 ip6 docker-bridges 表,文档中只展示 IPv4)。按 index.md 的说明:

  • 规则表每次 Docker 启动时重建
  • Docker 不使用 filter-INPUT 钩子——从主机物理网络或主机自身到达的包,因为是"路由进桥接网络",会命中 filter-FORWARD 链;filter-OUTPUT 同样不使用。

完整的 nftables 输出(见 生成后的 golden 文档)骨架如下:

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

    map filter-forward-out-jumps {
        type ifname : verdict
        elements = { "docker0" : jump filter-forward-out__docker0,
                     "bridgeICC" : jump filter-forward-out__bridgeICC,
                     "bridgeNoICC" : jump filter-forward-out__bridgeNoICC }
    }

    map nat-postrouting-in-jumps {
        type ifname : verdict
        elements = { "docker0" : jump nat-postrouting-in__docker0,
                     "bridgeICC" : jump nat-postrouting-in__bridgeICC,
                     "bridgeNoICC" : jump nat-postrouting-in__bridgeNoICC }
    }

    map nat-postrouting-out-jumps {
        type ifname : verdict
        elements = { "docker0" : jump nat-postrouting-out__docker0,
                     "bridgeICC" : jump nat-postrouting-out__bridgeICC,
                     "bridgeNoICC" : jump nat-postrouting-out__bridgeNoICC }
    }

    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;
    }
    ...
}

设计上的关键点是 vmap 动态分发filter-FORWARDnat-POSTROUTING 两条钩子链本身不含具体规则,而是按出接口oifname vmap @filter-forward-in-jumps)或入接口iifname vmap @...)查表,jump 到以桥接口名命名的每网络独立链(如 filter-forward-in__bridgeICC)。每创建一张 bridge 网络,nftabler 源码 就会创建四条链并向四张 map 各插入一个元素(NewNetworkconfigure):

  • filter-forward-in__<ifname> / filter-forward-out__<ifname>:转发方向入/出过滤;
  • nat-postrouting-in__<ifname> / nat-postrouting-out__<ifname>:SNAT/MASQUERADE。

链名拼接规则可直接在 network.gochainFilterFwdIn 等辅助函数中看到("filter-forward-in__" + ifName)。

--internal 网络的核心规则:INGRESS/EGRESS 丢弃 + ICC 判决

internal 网络与带外网访问的普通网络(如 usernet-portmap 场景)大部分规则相同,差异集中在两条 filter-forward-in 链:

chain filter-forward-in__bridgeICC {
    ct state established,related counter accept
    iifname != "bridgeICC" counter drop comment "INTERNAL NETWORK INGRESS"
    counter accept comment "ICC"
}

chain filter-forward-in__bridgeNoICC {
    ct state established,related counter accept
    iifname != "bridgeNoICC" counter drop comment "INTERNAL NETWORK INGRESS"
    counter drop comment "ICC"
}

逐条解读:

  1. ct state established,related counter accept:放行已建立连接的回程流量与相关流量(连接跟踪白名单),这是所有网络链的第一条规则,源码对应 initialRuleGroup 组的 conntrack 规则;
  2. iifname != "bridgeICC" counter drop comment "INTERNAL NETWORK INGRESS"入接口不是本桥的包一律丢弃——这就是 --internal 网络的"入口隔离",外部网络(包括 docker0、其他内部网络)发起的入站包进不来;
  3. 最终判决:ICC 开启时是 counter accept comment "ICC"(同网络容器间流量放行),ICC 关闭时是 counter drop comment "ICC"(容器间通信也被丢弃)。

出方向同样有对称限制:

chain filter-forward-out__bridgeICC {
    ct state established,related counter accept
    oifname != "bridgeICC" counter drop comment "INTERNAL NETWORK EGRESS"
}

oifname != "bridgeICC" counter drop comment "INTERNAL NETWORK EGRESS" 保证从本网络出去的包不能转发到别的桥,实现"出口隔离"。由于没有更多 accept 规则且链尾无兜底 accept(钩子链 policy 为 accept,但这些被 vmap jump 进来的子链默认不匹配即返回),internal 网络的流量既出不去也进不来,只能留在本网络内(且受 ICC 开关约束)。

为什么 nat-postrouting-out 链是空的

internal 网络最直观的 nftables 特征之一,是它的 NAT 出方向链完全为空:

chain nat-postrouting-out__bridgeICC {
}

chain nat-postrouting-out__bridgeNoICC {
}

这与普通网络形成对比——docker0 上有 masquerade 规则:

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

从源码结构看,原因很直接:在 network.goconfigure 函数中,masquerade/SNAT 规则(oifname != ... ip saddr <prefix> counter masquerade)被包在非 internal 的 else 分支里,且条件是 n.config.Masquerade && !conf.Routed。internal 分支(if n.config.Internal)只创建 INGRESS/EGRESS 丢弃规则和 ICC 判决规则,根本不往 nat-postrouting-out 链添加任何规则——因为内部网络本身不允许出站到外部,NAT 也就无从谈起。

ICC 判决值在源码中由 iccVerdict 决定:

iccVerdict := "accept"
if !n.config.ICC {
    iccVerdict = "drop"
}

filter-forward-in__<ifname> 链尾的 counter <accept|drop> comment "ICC" 直接对应 --internal 网络是否禁用了 enable_icc 选项。

源码与 golden 测试数据交叉印证

nftables 后端的 internal 网络规则由 nftabler 实现。configure 函数中 internal 分支生成的三条规则(network.go 关键片段):

iccVerdict := "accept"
if !n.config.ICC {
    iccVerdict = "drop"
}

if n.config.Internal {
    // Drop anything that's not from this network.
    tm.Create(nftables.Rule{
        Chain: fwdInChain,
        Group: initialRuleGroup,
        Rule:  []string{`iifname != `, n.config.IfName, `counter drop comment "INTERNAL NETWORK INGRESS"`},
    })
    tm.Create(nftables.Rule{
        Chain: fwdOutChain,
        Group: initialRuleGroup,
        Rule:  []string{`oifname != `, n.config.IfName, `counter drop comment "INTERNAL NETWORK EGRESS"`},
    })

    // Accept or drop Inter-Container Communication.
    tm.Create(nftables.Rule{
        Chain: fwdInChain,
        Group: fwdInICCRuleGroup,
        Rule:  []string{"counter", iccVerdict, "comment ICC"},
    })
}

单元测试层面,nftabler 包还有一组按配置组合命名的 golden 文件,可与集成文档交叉验证:

文档是怎么生成的:模板 + 实测 nftables + golden diff

templates/usernet-internal.md 并不是一份手工维护的文档,而是 text/template 模板TestBridgeNftablesDoc 的工作流程(index.md 亦有说明):

  1. 为每个"场景"创建一个独立网络命名空间的虚拟主机(L3Segment),在其中启动 dockerd;
  2. index 定义创建网络与容器(internal 场景即创建 bridgeICC/bridgeNoICC 两张网并各跑一个 busybox 容器);
  3. 执行 nft -s list table ip docker-bridges 抓取真实规则,把整表和每个 map/chain 拆成模板可用的键(如 Ruleset4chain filter-forward-in__bridgeICC);
  4. templates 目录 中对应模板渲染出 markdown;
  5. generated 目录 中的 golden 参考文档 diff,规则不一致则测试失败

模板中形如 {{index . "chain filter-forward-in__bridgeICC"}} 的占位符,就是被实测输出替换的位置。若规则变更导致测试失败,模板头注释(nftablesdoc_linux_test.go 顶部)给出的处理方式是:先检查 diff 中 nftables 规则的变化,更新对应模板中的描述,再用 TESTFLAGS='-update' 重新生成参考文档。

运行该测试有明确的环境前提(源码中的 skip 条件):仅 Linux、防火墙后端为 nftables(testEnv.FirewallBackendDriver() == "nftables")、非 rootless、且系统未运行 firewalld。

使用边界与注意事项

  • 这是开发用途的文档index.md 明确警告 Docker 的 nftables 规则结构在不同发行版之间会变化,不是稳定接口——不应把本文列出的链名、map 名写死在运维脚本里;
  • 文档只展示 IPv4 表 ip docker-bridges,IPv6 规则遵循同样模式但位于 ip6 docker-bridges
  • 每条规则都带 counter,可用 nft -s list table ip docker-bridges 查看计数,例如观察 INTERNAL NETWORK INGRESS 上的 drop 计数,可以快速判断是否有外部流量正在被 internal 网络丢弃;
  • internal 网络的隔离完全依赖上述 filter 链的 drop 规则,NAT 层面(空 nat-postrouting-out 链)不提供额外防护,排查"internal 网络为什么访问不了外网"时,应先看 filter-forward-in/out 链的 EGRESS/INGRESS 规则,而不是 NAT 链。

小结

usernet-internal 场景用最精简的规则集说明了 Docker Engine 在 nftables 后端实现 --internal 网络隔离的完整机制:每网络独立链 + vmap 按接口分发,INTERNAL NETWORK INGRESS/EGRESS 两条 drop 规则封死入出口,ICC 判决规则按 enable_icc 配置生成 accept 或 drop,NAT 链保持空白。这套规则既能在集成文档的 golden 输出中逐行对照,也能在 nftabler 源码单元测试 golden 数据 中找到一一对应的生成逻辑,是理解 Moby 桥接网络防火墙行为的典型切面。

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