首页
/ Moby(Docker Engine)的 nftables 桥接防火墙规则全解:docker-bridges 表与逐场景规则剖析

Moby(Docker Engine)的 nftables 桥接防火墙规则全解:docker-bridges 表与逐场景规则剖析

2026-09-06 14:37:37作者:明树来

本文以 Moby 仓库中 integration/network/bridge/nftablesdoc/ 目录下的官方文档为核心,系统讲解 Docker Engine 在 nftables 防火墙后端下如何为 bridge 网络创建 ip docker-bridgesip6 docker-bridges 两张表:从新守护进程启动时的基础链与 verdict map,到发布端口、回环地址发布、禁用 ICC、internal 网络、routed / nat-unprotected 网关模式等各场景下的完整规则集,并结合 nftabler 实现源码 印证每条规则的生成逻辑。读完后,你能够独立读懂 nft list table ip docker-bridges 的每一条链、每一条规则的含义,并知道如何用仓库自带的集成测试重新生成和校验这套文档。

需要特别注意:官方文档明确声明这组规则仅面向开发用途,不是稳定接口——Docker 的 nftables 规则结构在不同版本之间可能变化,不要基于它编写外部工具或运维脚本。

一、文档是什么、如何生成

1.1 文档定位

入口文档开宗明义地给出两条重要提示:

  • WARNING:这是开发用途文档,Docker 的 nftables 规则结构在各版本之间会变化,不是稳定接口;
  • NOTE:该文档由集成测试 TestBridgeNftablesDoc 生成——测试会启动一个 dockerd 实例、创建网络与容器、抓取 nftables 输出,再与 templates/ 目录中的 text/template 模板合并为各章节 Markdown。生成的结果会与 generated/ 目录中的“黄金”参考文档做 diff,规则一旦变化测试即失败(但模板本身的文字描述变化则可能无法察觉)。

文档还说明了两点全局事实:

  1. IPv6 规则与 IPv4 规则遵循完全相同的模式,只是位于不同表(ip docker-bridgesip6 docker-bridges),因此文档只展示 IPv4 规则;
  2. 这两张表在每次 Docker 启动时都会重新创建,不会持久化。

1.2 场景目录

index.md 以场景(Scenarios)索引整组文档,每个场景对应一个真实网络拓扑及其抓取的完整规则表:

场景 说明 生成文档
新守护进程 daemon 刚启动、仅默认 bridge(docker0) new-daemon.md
用户定义网络 + 发布端口 基础场景,-p 8080:80 usernet-portmap.md
发布端口到回环地址 -p 127.0.0.1:8080:80 usernet-portmap-lo.md
发布端口、禁用 userland proxy dockerd --userland-proxy=false usernet-portmap-noproxy.md
发布端口、禁用 ICC enable_icc=false usernet-portmap-noicc.md
--internal 网络(ICC 开/关两种) 无外部访问 usernet-internal.md
routed 网关模式 + 发布端口 gateway_mode_ipv4=routed usernet-portmap-routed.md
nat-unprotected 网络 + 发布端口 gateway_mode_ipv4=nat-unprotected usernet-portmap-natunprot.md
Swarm 服务 + 发布端口 依赖 IPVS templates/swarm-portmap.md(测试中该小节当前处于注释状态,generated/ 下暂无对应产出)

1.3 生成机制(测试源码级解读)

整个文档管线实现在 nftablesdoc_linux_test.go 中,关键机制值得逐点说明,因为它解释了“为什么规则如此精确、为什么改了规则必须重新生成文档”:

  1. 前置跳过条件TestBridgeNftablesDoc 会在以下情况跳过——系统运行着 firewalld(firewalld 会干扰/管理 nftables 规则)、rootless 模式、或防火墙驱动不是 nftablesnftablesdoc_linux_test.go 第 188-191 行)。
  2. L3 分段隔离:测试通过 networking.NewL3Segment 建一个名为 gen-iptables-doc 的 L3 分段,为每个场景创建一个独立网络命名空间的“主机”(docker0docker1、...,IPv4 从 192.168.124.1 递增,IPv6 从 fdc0:36dc:a4dd::1 递增),并把 eth0 down 掉以减少杂散流量对计数器规则的影响。
  3. 在隔离 netns 内启动 daemondaemon.StartWithBusybox 启动 dockerd;需要禁代理的场景追加 --userland-proxy=false;Swarm 场景要求内核支持 IPVS 并加 --swarm-default-advertise-addr
  4. 固定的文档网络地址:为避免每次运行地址漂移,测试使用三个文档专用子网 192.0.2.0/24198.51.100.0/24203.0.113.0/24(及对应网关 .1),这正是你在各场景文档中反复看到 192.0.2.2(容器 IP)的原因。
  5. 抓取规则runNftables 执行 nft -s list table ip docker-bridges,把整表作为 Ruleset4 存入数据映射,同时按 {/} 分块,以每块的“首行(去掉 {)”为键拆出各 map/chain(如 map filter-forward-in-jumpschain filter-forward-in__bridge1),供模板通过 {{index . "..."}} 精确引用。
  6. 模板渲染 + 黄金比对generatetext/template 渲染 templates/ 下同名模板,输出写入 bundles/TestBridgeNftablesDoc/,并与 generated/ 目录中的黄金文件 golden.Assert 比对。当规则确实变化导致测试失败时,按包注释的流程处理:检查 diff → 更新对应模板的描述 → 用 TESTFLAGS='-update' 重跑以刷新参考文档。
  7. 测试环境初始化main_linux_test.go 中的 TestMain 负责构建 environment.Execution、确保 Linux 冻结镜像就绪,并在结束后清理环境。

从源码结构看,模板与规则数据严格解耦——模板(如 templates/usernet-portmap.md)只负责文字说明并引用 Ruleset4chain filter-forward-in__bridge1chain raw-PREROUTINGchain nat-prerouting-and-output 等键,规则内容本身永远来自真实 daemon 的抓取结果,这保证了文档与实现不可能“口口相传地漂移”。

二、新守护进程启动时的基础表结构

场景文档:generated/new-daemon.md

daemon 启动时创建 ip docker-bridgesip6 docker-bridges 两张表,各含基础链和若干空的 verdict map,随后为默认 bridge 网络(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"
    	}
    }

2.1 核心设计:verdict map 驱动的按网桥分发

filter-FORWARD 是等价于 iptables filter 表内建 FORWARD 链的 base chain,它只用两条规则,分别以出接口名oifname)和入接口名iifname)为键查 verdict map:

  • oifname vmap @filter-forward-in-jumps:包将被投递到哪个 Docker 桥接口,就跳到该网络的 filter-forward-in__<桥名> 链;
  • iifname vmap @filter-forward-out-jumps:包从哪个 Docker 桥接口发出,就跳到 filter-forward-out__<桥名> 链。

每个新桥网络会在 map 中增加一个 "<桥名>" : jump ... 元素。这样,与任何 Docker 桥都无关的包在 map 中查不到 jump,直接由 base chain 放行,Docker 规则零开销。这是与 iptables “每个网络一串 -A FORWARD -j DOCKER-USER ...” 结构最本质的差异,从源码看正是 network.go 的 configureMapElement(键为 n.config.IfName,值为 "jump " + 链名)实现的。

注意 raw-PREROUTINGnat-OUTPUTnat-PREROUTING 同样被建为 base chain:

  • nat-OUTPUT 对“目标地址为本地、且不是回环网段 127.0.0.0/8”的宿主发出流量执行 fib daddr type local counter jump nat-prerouting-and-output
  • nat-PREROUTING 对外来流量中“目标为本机”的包执行同样的 jump。

两条链合流到 nat-prerouting-and-output,使发布端口的 DNAT 规则只需写一份,宿主本机访问与外部访问都能命中(有 userland proxy 时,proxy 会先截获流量,这条规则主要兜底)。

2.2 filter-FORWARD 的 policy 与 sysctl 的关系

文档中 filter-FORWARD 显示为 policy accept,但实际取值取决于系统状态:

  • IPv4:若 net.ipv4.ip_forward 原本未设为 1、而 daemon 在创建第一个启用 IPv4 的 bridge 网络时自己把它打开,则 policy 为 drop
  • IPv6:同理,取决于 /proc/sys/net/ipv6/conf/default/forwarding/proc/sys/net/ipv6/conf/all/forwarding 是否由 daemon 打开。

这一点在 network.go 的注释和场景文档中被反复强调:每个网络链的最后一条规则总是显式的 acceptdrop(如 docker0 的 UNPUBLISHED PORT DROP),因此转发行为不依赖 FORWARD 链的默认 policy——即使 policy 是 ACCEPT,未发布端口的入向流量也会被链尾显式 drop。

2.3 docker0 的每网络链

filter-forward-in__docker0(入向,将被投递进 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 只对当前 base chain 生效,被接受的包仍可能被挂接在同一 hook 上的其他 base chain 继续处理,例如系统的其他防火墙);
  2. ICC 规则:接受源自同一网络的包——默认 bridge 的 ICC 是开启的;
  3. 兜底 drop:没有容器发布端口,所以其余包全部丢弃。

filter-forward-out__docker0(出向,源自 docker0 的包):

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

接受已建立流 + 无条件接受出站——该网络的容器可以访问外部网络。

nat-postrouting-out__docker0 中有一条 masquerade 规则:

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

即:源地址在默认 bridge 子网(172.17.0.0/16)、且包不是发往 docker0 本身的流量,做 masquerade——容器访问外网时源地址被伪装成宿主接口地址。而 nat-postrouting-in__docker0 此时为空。

从源码看,这些规则的生成逻辑在 network.go 的 configure 函数:conntrack 规则、ICC verdict(iccVerdictn.config.ICCaccept/drop 间切换)、OUTGOINGUNPROTECTED/UNPUBLISHED PORT DROP 兜底规则、ICMP 规则、masquerade 规则都集中在此处按网络配置组合生成,最后通过 table.Apply(ctx, tm) 原子应用;tm.Reverse() 返回的“反向 Modifier”则用于网络删除时精确回滚。

三、用户定义网络 + 发布端口(基准场景)

场景文档:generated/usernet-portmap.md

等价操作:

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

ip docker-bridges 表在初始状态上的增量变化有四处,全部值得逐条理解:

3.1 四个 verdict map 各新增 bridge1 元素

map filter-forward-in-jumps

    elements = { "docker0" : jump filter-forward-in__docker0,
                 "bridge1" : jump filter-forward-in__bridge1 }

四个 map(filter-forward-in/out、nat-postrouting-in/out)对称扩展——这就是“每网络一条链”的注册点。

3.2 发布端口的 accept 规则

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

新增的第三条:ip daddr 192.0.2.2 tcp dport 80 counter accept——只放行目标为该容器 IP、TCP 80 端口 80 的入向流量(经 DNAT 之后包的目的地址已是容器 IP/目标端口)。这是与“未发布任何端口的网络”的关键差别:有发布端口时,入向链从“全部丢弃”变为“逐端口放行 + 其余丢弃”。

3.3 raw-PREROUTING:禁止从宿主外部直接访问容器 IP

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

因为网络使用默认的网关模式 nat,容器 IP 属于 NAT 保护范围:外部(任何非 bridge1 接口进入的)直接以容器 IP 为目的地址的包在 raw 表被丢弃——raw 表先于 conntrack,保证这类直接访问不会留下连接跟踪状态。这一条是理解 nat / routed / nat-unprotected 三种网关模式差异的锚点,后文场景都会围绕它展开。

3.4 DNAT:宿主 8080 → 容器 80

    	chain nat-prerouting-and-output {
    		iifname != "bridge1" tcp dport 8080 counter dnat to 192.0.2.2:80 comment "DNAT"
    	}

注意 iifname != "bridge1" 限定:来自该网络本身的流量不做 DNAT(它们直接访问容器端口即可),只有“其他接口进入”或宿主本机发出的流量才执行 8080→容器 80 的端口重定向。

3.5 出站 masquerade

    	chain nat-postrouting-out__bridge1 {
    		oifname != "bridge1" ip saddr 192.0.2.0/24 counter masquerade comment "MASQUERADE"
    	}

与 docker0 同构:子网 192.0.2.0/24 的流量出桥时伪装源地址。

从源码看,MASQUERADE 的生成在 network.go 第 190-206 行:默认 verdict 是 masquerade;若配置了 conf.HostIP(即 snat 模式),则改用 snat to <HostIP> 并加 SNAT 注释。测试 testdata/TestNftabler/ 下的 .golden 文件覆盖了 masq/snat/gwm 的组合矩阵,可对照验证。

四、发布端口到回环地址

场景文档:generated/usernet-portmap-lo.md

等价操作(-p 127.0.0.1:8080:80):

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 127.0.0.1:8080:80 --name c1 busybox

大部分规则与基准场景相同,关键差异在 raw-PREROUTING

    	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"
    		iifname != "lo" ip daddr 127.0.0.1 tcp dport 8080 counter drop comment "DROP REMOTE LOOPBACK"
    	}

新增的 DROP REMOTE LOOPBACK 规则:绑定在回环地址的发布端口,不允许从远端访问。外部网卡进来的、目的为 127.0.0.1:8080 的包在 raw 表被丢弃,保证回环绑定语义在 NAT 路径上也成立。同时 nat-prerouting-and-output 中的 DNAT 规则变为 iifname != "bridge1" ip daddr 127.0.0.1 tcp dport 8080 counter dnat to 192.0.2.2:80——以 127.0.0.1 为目标地址的匹配由 nat-OUTPUT 链(宿主本机发出的包)触发,因为回环流量走 output 路径。

五、禁用 userland proxy 时的规则变化

场景文档:generated/usernet-portmap-noproxy.md

daemon 以 dockerd --userland-proxy=false 启动后再执行相同的建网/发布端口操作。由于没有用户态代理进程在宿主监听 8080 端口截获连接,内核 NAT 必须完整承担所有路径,因此规则比基准场景更宽、更多:

5.1 nat-OUTPUT 不再排除回环网段

    	chain nat-OUTPUT {
    		type nat hook output priority dstnat; policy accept;
    		fib daddr type local counter jump nat-prerouting-and-output
    	}

基准场景中是 ip daddr != 127.0.0.0/8 fib daddr type local ...;禁代理后去掉了 != 127.0.0.0/8 限定——发往回环地址的宿主包也要走 DNAT,否则一个网络中容器发布到 127.0.0.1:8080 的端口,无法从另一个网络(例如 docker0)的容器访问,因为没有 proxy 兜底。

5.2 DNAT 规则不再限定接口

    	chain nat-prerouting-and-output {
    		tcp dport 8080 counter dnat to 192.0.2.2:80 comment "DNAT"
    	}

去掉了 iifname != "bridge1":即使流量来自 bridge1 自身也要 DNAT——容器访问“自己网络里自己发布的宿主端口”时,同样没有 proxy 来接。

5.3 hairpin masquerade

    	chain nat-postrouting-in__docker0 {
    		fib saddr type local counter masquerade comment "MASQUERADE FROM HOST"
    	}

    	chain nat-postrouting-in__bridge1 {
    		fib saddr type local counter masquerade comment "MASQUERADE FROM HOST"
    		ip saddr 192.0.2.2 ip daddr 192.0.2.2 tcp dport 80 counter masquerade comment "MASQ TO OWN PORT"
    	}

两类新 masquerade 规则:

  • MASQUERADE FROM HOST:源地址是宿主本机地址(fib saddr type local)的包进入桥网络时先伪装——这样容器回给宿主的数据包不会指向“宿主的私有源地址”;
  • MASQ TO OWN PORT:容器访问自己发布在宿主上的那个端口192.0.2.2 → 192.0.2.2:80)时先 masquerade,避免“自己 NAT 给自己”导致 DNAT 失效。

从源码看,前者对应 network.go 第 207-215 行n.fw.config.Hairpin 开关(hairpin 能力使容器可以经宿主已发布端口访问自己);MASQ TO OWN PORT 属于端点级规则,由同包 port.go 在添加端口映射时生成。

六、禁用 ICC 的网络

场景文档:generated/usernet-portmap-noicc.md

等价操作(-o com.docker.network.bridge.enable_icc=false):

docker network create \
  -o com.docker.network.bridge.name=bridge1 \
  -o com.docker.network.bridge.enable_icc=false \
  --subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
docker run --network bridge1 -p 8080:80 --name c1 busybox

唯一变化在入向链的 ICC 规则 verdict:

    	chain filter-forward-in__bridge1 {
    		ct state established,related counter accept
    		iifname "bridge1" counter drop comment "ICC"
    		ip daddr 192.0.2.2 tcp dport 80 counter accept
    		counter drop comment "UNPUBLISHED PORT DROP"
    	}

iifname "bridge1" 的包(即源自同网络的流量)从 accept 变为 drop:同网络容器间互访被禁止,但发布端口的 accept 规则仍然保留,宿主侧通过 8080 访问 80 的链路不受影响。这印证了 ICC 语义只作用于“网桥内部互访”,与发布端口正交。源码上就是 network.go 第 110-113 行iccVerdict 依据 n.config.ICC 选择。

七、--internal 网络(含 ICC 开关对照)

场景文档:generated/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 bridgeICC
docker run --network bridgeICC --name c1 busybox

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

(注:文档中第二段命令的网络名笔误写成了 bridge1,实际创建的桥名为 bridgeNoICC,以生成的规则为准。)

internal 网络的规则特征(对照普通网络可清晰看出隔离手段):

    	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-out__bridgeICC {
    		ct state established,related counter accept
    		oifname != "bridgeICC" counter drop comment "INTERNAL NETWORK EGRESS"
    	}

三个要点:

  1. 双向都限制:入向链丢弃一切非本网络进入的包(INTERNAL NETWORK INGRESS),出向链丢弃一切发往本网络以外的包(INTERNAL NETWORK EGRESS)——注意与 ICC 规则的方向互补:ICC 规则管“网络内部互访”,这两条管“网络与外部隔离”;
  2. ICC 关时最终 verdict 为 dropbridgeNoICC 的入向链末尾是 counter drop comment "ICC" 而非 accept
  3. nat-postrouting-out 链没有任何 masquerade 规则——internal 网络本就不出外部网络,无需伪装(见 network.go 第 115-126 行n.config.Internal 分支:只加 INGRESS/EGRESS 两条 drop 规则与 ICC verdict,不再生成 OUTGOING、UNPROTECTED、ICMP 与 masquerade 规则)。

八、routed 网关模式 + 发布端口

场景文档:generated/usernet-portmap-routed.md

等价操作(-o com.docker.network.bridge.gateway_mode_ipv4=routed):

docker network create \
  -o com.docker.network.bridge.name=bridge1 \
  -o com.docker.network.bridge.gateway_mode_ipv4=routed \
  --subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
docker run --network bridge1 -p 8080:80 --name c1 busybox

routed 模式下容器 IP 对外“可达且无 NAT 保护”,与 nat 模式的差异体现在三处:

8.1 没有 DROP DIRECT ACCESS

    	chain raw-PREROUTING {
    		type filter hook prerouting priority raw; policy accept;
    	}

raw-PREROUTING 为空——容器 IP 可以被宿主外部直接寻址(这是 routed 模式的设计目标:网关做路由转发,而非 NAT)。

8.2 入向链显式接受 ICMP

    	chain filter-forward-in__bridge1 {
    		ct state established,related counter accept
    		ip protocol icmp counter accept comment "ICMP"
    		iifname "bridge1" counter accept comment "ICC"
    		ip daddr 192.0.2.2 tcp dport 80 counter accept
    		counter drop comment "UNPUBLISHED PORT DROP"
    	}

新增 ip protocol icmp counter accept:外部 ping 容器是 routed 模式的常见用法,所以 ICMP 被显式放行(IPv6 表中对应规则为 meta l4proto ipv6-icmp,见 network.go 第 177-188 行conf.Routed 分支)。

8.3 无 masquerade、无 DNAT

nat-prerouting-and-outputnat-postrouting-out__bridge1 均为空:routed 模式下容器入站流量走路由直达(不需要 DNAT 到发布端口之外的地址改写),出站流量保留真实源 IP(不做 masquerade)。另外文档注明:routed 模式下 userland proxy 不会为映射端口启动

九、nat-unprotected 网络 + 发布端口

场景文档:generated/usernet-portmap-natunprot.md

等价操作(-o com.docker.network.bridge.gateway_mode_ipv4=nat-unprotected):

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

nat-unprotected 介于 nat 与 routed 之间:NAT 机制仍在,但网络内的容器 IP 不受“直接访问保护”。规则差异:

  1. 入向链的兜底规则变为 accept
    	chain filter-forward-in__bridge1 {
    		ct state established,related counter accept
    		iifname "bridge1" counter accept comment "ICC"
    		counter accept comment "UNPROTECTED"
    	}

没有逐端口的 accept 规则,改为 UNPROTECTED 全端口接受——文档解释这是为了应对 filter-FORWARD 默认 policy 为 drop 的情况(见 network.go 第 163-175 行conf.Unprotected 分支);

  1. raw-PREROUTING 为空,没有 DROP DIRECT ACCESS——容器 IP 可从外部直接访问;
  2. DNAT 与 masquerade 规则原样保留(与 nat 模式相同),且若启用了 userland proxy,它照常启动。

十、场景规则差异速查

场景 raw-PREROUTING 入向链关键规则 nat-prerouting-and-output masquerade
新 daemon(docker0) ICC accept + UNPUBLISHED PORT DROP 出站 MASQUERADE
nat + 发布端口 DROP DIRECT ACCESS 逐端口 accept + drop 兜底 8080→80 DNAT(限非本桥接口) 出站 MASQUERADE
回环发布 DROP DIRECT ACCESS + DROP REMOTE LOOPBACK 同上 DNAT 匹配 daddr 127.0.0.1 同上
禁 userland proxy DROP DIRECT ACCESS 同上 8080→80 DNAT(不限接口) MASQUERADE FROM HOST + MASQ TO OWN PORT
禁 ICC DROP DIRECT ACCESS ICC 规则 verdict 为 drop 同 nat 模式 同 nat 模式
internal INTERNAL NETWORK INGRESS drop + ICC(accept/drop)
routed 增加 ICMP accept
nat-unprotected UNPROTECTED accept 兜底 同 nat 模式 同 nat 模式

这张表浓缩了 index.md 所列全部活跃场景的差异点,可作为排障时“先定位场景、再对规则”的索引。

十一、源码层面的实现对应

文档中的每条规则都可在 daemon/libnetwork/drivers/bridge/internal/nftabler/ 包中找到生成点,主要对应关系:

  • 表/链命名chainFilterFwdIn/OutchainNatPostRtIn/Out 生成 filter-forward-in__<if> 等链名(network.go 第 255-269 行);
  • 网络级规则network.go 的 configureNetworkConfigICCInternalRoutedUnprotectedMasqueradeAcceptFwMark 等字段)组合生成 conntrack、ICC、OUTGOING、UNPROTECTED/UNPUBLISHED PORT DROP、ICMP、MASQUERADE/SNAT 规则;AcceptFwMark 支持带 /mask 的 firewall mark(meta mark <mark> and <mask> == <mark> 表达式,见 nftFwMark 函数),用于外部系统经 fwmark 放行特定流量;
  • 端口级规则(DNAT、DROP DIRECT ACCESSDROP REMOTE LOOPBACKMASQ TO OWN PORT):由 port.goendpoint.go 在端点/端口变化时增量生成;
  • 删除与回滚:每个网络持有 remover4/remover6(由 tm.Reverse() 产生的反向 Modifier),删除网络时 DelNetworkLevelRules 用它精确移除该网络的规则(network.go 第 235-253 行);ReapplyNetworkLevelRules 在 nftables 后端是空实现,注释说明原因是 firewalld 重载并不会删除 nftables 规则;
  • 单元测试矩阵testdata/TestNftabler/ 下数百个 hairpin=..,icc=..,gwm=.. 命名的 .golden 文件,以“参数组合 → 完整规则表”的黄金文件形式覆盖 nat/routed/nat-unprotectedinternalsnatbindlh 等组合,是验证规则生成逻辑最直接的测试证据;
  • 集成验证:本文第一、二节的 TestBridgeNftablesDoc 是“端到端”层——真实 daemon + 真实 nft 输出 + 黄金文档比对,两者互补。

十二、如何自行验证与更新这套文档

如果你需要确认自己版本上的规则是否与文档一致,或在自己的环境观察规则,可参考(以下仅为查看/运行方式,无需修改仓库内容):

  1. 查看本机规则(root,nftables 后端):

    nft -s list table ip docker-bridges
    nft -s list table ip6 docker-bridges
    

    测试抓取用的正是 nft -s list table ip docker-bridges-s 会显示计数器数值)。

  2. 确认防火墙后端:文档与规则均要求 daemon 使用 nftables 防火墙驱动;若系统运行 firewalld,TestBridgeNftablesDoc 会直接跳过(firewalld 会接管/干扰规则),rootless 环境同样跳过。

  3. 重新生成黄金文档(开发流程,见 nftablesdoc_linux_test.go 包注释):规则变化导致测试失败时,先审查 diff,再更新对应 templates/ 模板的描述文字,然后以 TESTFLAGS='-update' 重跑集成测试刷新 generated/ 参考文档。生成的 Markdown 会同时写入 bundles/TestBridgeNftablesDoc/(由 DOCKER_INTEGRATION_DAEMON_DESTDEST 环境变量定位)便于比对。

十三、局限性与适用前提

  • 非稳定接口:规则结构(表名、链名、注释、组织方式)随版本演进可能变化,运维脚本不应硬编码这些规则文本;
  • 平台前提:nftables 规则仅出现在 Linux 上 nftables 防火墙驱动、非 rootless、无 firewalld 干扰的环境;
  • IPv6 未逐一展示:模式与 IPv4 完全一致,位于 ip6 docker-bridges 表;
  • Swarm 场景index.md 索引中列出的 Swarm 服务场景,其测试小节在 nftablesdoc_linux_test.go 第 160-173 行 当前被注释,generated/ 目录暂无对应产出,模板 templates/swarm-portmap.md 保留在仓库中;
  • 所有“文档由测试生成、测试失败即说明规则变化”的机制,意味着本文引用的规则集以当前仓库快照为准,升级 daemon 后建议重新对照 generated/ 目录确认。

综合来看,Moby 的 nftables 后端用“两张表 + 四个 verdict map + 每桥四链(filter 入/出、nat postrouting 入/出)+ raw/nat 兜底链”的骨架,把 iptables 时代 DOCKER/DOCKER-USER 等功能等价且更清晰地映射到了 nftables 语义:verdict map 使与桥无关的流量零成本穿透,每网络链的显式终局 verdict(accept/drop)使行为不依赖 FORWARD policy,而 DROP DIRECT ACCESSDROP REMOTE LOOPBACKMASQ TO OWN PORT 等 raw/nat 规则则精确支撑了 nat/routed/nat-unprotected 网关模式与回环绑定等语义。这套“测试即文档”的生成机制,也让规则行为成为可被持续回归验证的一等公民事实。

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