首页
/ Moby bridge nat-unprotected 网关模式:发布端口、默认 ACCEPT 规则与 iptables 全解析

Moby bridge nat-unprotected 网关模式:发布端口、默认 ACCEPT 规则与 iptables 全解析

2026-09-06 13:52:01作者:翟萌耘Ralph

本篇基于 Moby 仓库中由集成测试自动生成的场景文档,完整解析 bridge 驱动 gateway_mode=nat-unprotected 模式下"容器发布端口"时 Docker 生成的 iptables 规则:如何在 filter 表的 DOCKER 链中把默认 DROP 换为默认 ACCEPT、为何不再生成按端口的 ACCEPT 规则、nat 表为何与默认 nat 模式完全一致,并给出全部可复制的复现命令、逐条规则说明与源码级实现路径(setDefaultForwardRulesetPerPortForwarding),帮助你在理解 bridge 网络"最小保护"语义的同时,掌握用 golden 测试维护 iptables 文档的方法。

文档定位:TestBridgeIptablesDoc 生成的场景之一

模板文件 integration/network/bridge/iptablesdoc/templates/usernet-portmap-natunprot.md 属于 index.md 所列的九个 iptables 场景之一,对应"Container on a nat-unprotected network, with a published port"。

整个 iptablesdoc 目录的工作机制是(见 iptablesdoc_linux_test.go 的包注释):

  1. TestBridgeIptablesDoc 启动一个真实的 dockerd,逐个场景创建 bridge 网络并运行容器;
  2. 对每个场景执行 iptables -vL --line-numbersiptables -S 等命令捕获 filter/nat/raw 三张表(命令映射见 iptCmds 变量,如 LFilter4SFilter4LNat4SNat4);
  3. 将捕获结果作为数据填充 templates/ 下对应的 text/template 模板,生成 Markdown 文档;
  4. 生成的文档与 generated/ 目录下的"golden"参考文档做 diff,任何规则差异都会导致测试失败;此时需要人工检查 diff、更新对应模板描述,并以 TESTFLAGS='-update' 重新运行以刷新参考文档。

需要注意两点边界(来自 index.md 的告警):

  • 该文档系列仅供开发使用:Docker 的 iptables/ip6tables 规则结构会在版本间变化,不是稳定接口;
  • 测试通过 golden.Assert 比较生成物与 golden 文件,但模板文字本身的修改可能无法被测试发现
  • 文档中代码链接是历史 permalink,会随代码演进过期——下文已改用当前仓库的实际源码路径替代。

测试自身的运行限制也在源码中明确:firewalld 运行中、rootless 模式、防火墙后端为 nftables 时会被 skip(TestBridgeIptablesDoc 开头的 skip.If 三连);每个场景在独立的 L3Segment 网络命名空间中运行一个 dockerd,且先 ip link set eth0 down 以避免杂散流量污染包计数,最后用 iptables -Z 清零计数并正则归一化 0 packets, 0 bytes,保证输出可复现。

复现该场景:两条命令

文档给出的等效操作为:

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

其中三个 -o 选项的常量定义见 labels.go

选项 常量 作用
com.docker.network.bridge.name BridgeName 指定桥接口名为 bridge1
com.docker.network.bridge.gateway_mode_ipv4 IPv4GatewayMode 将 IPv4 网关模式设为 nat-unprotected
--subnet/--gateway 指定 IPAM 子网 192.0.2.0/24、网关 192.0.2.1

网关模式的解析入口是 newGwMode,合法取值为 natnat-unprotectedroutedisolated

const (
	gwModeDefault   gwMode = ""
	gwModeNAT       gwMode = "nat"
	gwModeNATUnprot gwMode = "nat-unprotected"
	gwModeRouted    gwMode = "routed"
	gwModeIsolated  gwMode = "isolated"
)

随后 makeNetworkConfigFam 把模式翻译为防火墙层的布尔标志:Routed: gwm.routed()Unprotected: gwm.unprotected(),写入 firewaller.NetworkConfigFam。其 Unprotected 字段的注释正是该模式的语义定义:

Unprotected is true if no rules to filter unpublished ports or direct access from any remote host are required.

即:不生成任何用于过滤"未发布端口直接访问"的规则。一个值得注意的细节是:文档模板开头描述"Running the daemon with the userland proxy disable",而在测试索引 indexusernet-portmap-natunprot 段并未设置 noUserlandProxy(只有 usernet-portmap-noproxy 段设置了),因此该场景实际以默认参数(userland proxy 启用)运行——这也与文档后文"If the userland proxy is enabled, it is still started"的强调相呼应。

filter 表:默认 ACCEPT 取代默认 DROP

场景生效后捕获到的 filter 表全文如下(来自 golden 文件 generated/usernet-portmap-natunprot.md):

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DOCKER-USER  all  --  any    any     anywhere             anywhere
2        0     0 DOCKER-FORWARD  all  --  any    any     anywhere             anywhere

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination

Chain DOCKER (2 references)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DROP       all  --  !docker0 docker0  anywhere             anywhere
2        0     0 ACCEPT     all  --  !bridge1 bridge1  anywhere             anywhere

Chain DOCKER-BRIDGE (1 references)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DOCKER     all  --  any    docker0  anywhere             anywhere
2        0     0 DOCKER     all  --  any    bridge1  anywhere             anywhere

Chain DOCKER-CT (1 references)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 ACCEPT     all  --  any    docker0  anywhere             anywhere             ctstate RELATED,ESTABLISHED
2        0     0 ACCEPT     all  --  any    bridge1  anywhere             anywhere             ctstate RELATED,ESTABLISHED

Chain DOCKER-FORWARD (1 references)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DOCKER-CT  all  --  any    any     anywhere             anywhere
2        0     0 DOCKER-INTERNAL  all  --  any    any     anywhere             anywhere
3        0     0 DOCKER-BRIDGE  all  --  any    any     anywhere             anywhere
4        0     0 ACCEPT     all  --  docker0 any     anywhere             anywhere
5        0     0 ACCEPT     all  --  bridge1 any     anywhere             anywhere

Chain DOCKER-INTERNAL (1 references)
num   pkts bytes target     prot opt in     out     source               destination

Chain DOCKER-USER (1 references)
num   pkts bytes target     prot opt in     out     source               destination

对应的 iptables -S -t filter 等价命令为:

-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-BRIDGE
-N DOCKER-CT
-N DOCKER-FORWARD
-N DOCKER-INTERNAL
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-FORWARD
-A DOCKER ! -i docker0 -o docker0 -j DROP
-A DOCKER ! -i bridge1 -o bridge1 -j ACCEPT
-A DOCKER-BRIDGE -o docker0 -j DOCKER
-A DOCKER-BRIDGE -o bridge1 -j DOCKER
-A DOCKER-CT -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-CT -o bridge1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-FORWARD -j DOCKER-CT
-A DOCKER-FORWARD -j DOCKER-INTERNAL
-A DOCKER-FORWARD -j DOCKER-BRIDGE
-A DOCKER-FORWARD -i docker0 -j ACCEPT
-A DOCKER-FORWARD -i bridge1 -j ACCEPT

逐链解读(对照 index.md 中"filter-INPUT/OUTPUT 不被 Docker 使用"的说明):

  • DOCKER 链:这是本场景的核心差异所在。默认 nat 模式下每个用户网络会追加一条"未被按端口规则 ACCEPT 的包一律 DROP"的兜底规则;而 nat-unprotected 网络追加的是 ! -i bridge1 -o bridge1 -j ACCEPT——进入或离开 bridge1 的流量无条件放行。对比同一 DOCKER 链中 docker0(默认桥)那条 DROP 规则,两种模式的差别一目了然。
  • DOCKER-CT 链:对发往 bridge1 的 RELATED/ESTABLISHED 回包放行,保证容器入站连接的回程流量不被后续规则拦截。
  • DOCKER-BRIDGE 链:把所有发往各桥(docker0bridge1)的出站流量跳转到 DOCKER 链,再执行上述默认动作。
  • DOCKER-FORWARD 链:顺序为 DOCKER-CT → DOCKER-INTERNAL → DOCKER-BRIDGE,随后按网桥放行出站(-i docker0-i bridge1 的 ACCEPT 规则),DOCKER-INTERNAL 链在本场景中为空(没有 --internal 网络)。
  • FORWARD 链:仅两条跳转,所有判定都封装在 Docker 自定义链内,不污染宿主 FORWARD 主链。

与 nat 模式的核心差异

模板文档明确列出与 nat 模式场景 的差异,全部集中在 DOCKER 链:

  1. 默认规则从 DROP 变为 ACCEPT。nat 模式为"任何未被按端口/协议规则接受的包"追加默认 DROP;nat-unprotected 追加默认 ACCEPT。源码中这个分支就在 setDefaultForwardRule

    // Normally, DROP anything that hasn't been ACCEPTed by a per-port/protocol
    // rule. This prevents direct access to un-mapped ports from remote hosts
    // that can route directly to the container's address (by setting up a
    // route via the host's address).
    action := "DROP"
    if unprotected {
        // If the user really wants to allow all access from the wider network,
        // explicitly ACCEPT anything so that the filter-FORWARD chain's
        // default policy can't interfere.
        action = "ACCEPT"
    }
    

    注释点明了显式 ACCEPT 的必要性:即使 filter-FORWARD 链的默认 policy 是 DROP,这条规则也能保证容器间及容器对外流量不被宿主策略意外拦截。

  2. 不生成按端口/协议的 ACCEPT 规则。由于该网络默认即 ACCEPT,发布端口 80/tcp 无需再有一条专门的放行规则,因此 setPerPortForwarding(它负责向 DOCKER 链顶部插入 ! -i <bridge> -o <bridge> -p <proto> -d <containerIP> --dport <port> -j ACCEPT)不会被调用。调用点在 port.go 的 AddPort 流程 中可见:setPerPortNAT 先行执行,随后 if !config.Unprotected { ... setPerPortForwarding ... }

  3. userland proxy 行为不变。文档特别注明:If the userland proxy is enabled, it is still started——网关模式只改变防火墙规则,不影响宿主端口代理进程的启动逻辑。

另一个源码层面可直接印证"unprotected = 不过滤直接访问"的差异点:raw 表中用于丢弃"绕过 DNAT、直接路由到容器 IP"流量的过滤规则(filterDirectAccess)在 nat-unprotected 与 routed 模式下直接返回:

func (n *network) filterDirectAccess(ctx context.Context, ipv iptables.IPVersion, config firewaller.NetworkConfigFam, epIP netip.Addr, enable bool) error {
	if n.config.Internal || config.Unprotected || config.Routed {
		return nil
	}

这也是 golden 文件中 raw 表完全缺席的原因——nat 模式下这里本应存在 direct-access DROP 规则,nat-unprotected 下从源头就不创建。nftables 后端有对应实现,nftabler/network.goconf.Unprotected 时写入 counter accept comment "UNPROTECTED" 规则,语义与 iptables 路径一致(该文档系列仅在 iptables 后端下生成,见测试的 skip 条件)。

nat 表:与 nat 模式完全一致

模板文档的结论是:nat 表与 nat 模式逐字节相同,端口发布的全部 DNAT/SNAT 机制不受网关模式影响:

Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DOCKER     all  --  any    any     anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DOCKER     all  --  any    any     anywhere            !loopback/8           ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 MASQUERADE  all  --  any    !bridge1  192.0.2.0/24         anywhere
2        0     0 MASQUERADE  all  --  any    !docker0  172.17.0.0/16        anywhere

Chain DOCKER (2 references)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DNAT       tcp  --  !bridge1 any     anywhere             anywhere             tcp dpt:http-alt to:192.0.2.2:80

等价命令:

-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 192.0.2.0/24 ! -o bridge1 -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER ! -i bridge1 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80

要点:

  • -p 8080:80 映射为 nat-DOCKER 链中的 DNAT tcp dpt:http-alt to:192.0.2.2:80——宿主机 8080 端口入站的 TCP 被改写为容器 IP(192.0.2.2,网关 192.0.2.1 之后的首个地址)的 80 端口;
  • POSTROUTING 的两条 MASQUERADE 分别覆盖 bridge1 子网(192.0.2.0/24)与默认 docker0 子网(172.17.0.0/16)的出站 SNAT,规则来自 setupNonInternalNetworkRules 中按 Masquerade/HostIP 生成的 natRule;
  • PREROUTING/OUTPUT 的 ADDRTYPE match dst-type LOCAL 跳转保证只有目的地址是本机时才进入 DNAT 判定,避免影响其他主机经过的流量。

源码调用链小结

把文档现象串回实现,nat-unprotected 场景的完整链路是:

  1. docker network create -o com.docker.network.bridge.gateway_mode_ipv4=nat-unprotectednewGwMode 解析出 gwModeNATUnprot
  2. makeNetworkConfigFamUnprotected: true 写入 firewaller.NetworkConfigFam
  3. 建网时 setupIPTables 调用 setDefaultForwardRule(..., config.Unprotected, ...) 追加 ! -i bridge1 -o bridge1 -j ACCEPT,并注册 cleanup 函数以便删网时移除;
  4. 容器端口发布时,AddPort 始终执行 setPerPortNAT(nat 表 DNAT),但跳过 setPerPortForwarding(filter 表按端口 ACCEPT),同时 filterDirectAccess 对 raw 表的 direct-access 过滤整体 no-op;
  5. 集成测试 TestBridgeIptablesDoc 捕获全部三张表、渲染模板、与 golden 文件比对,锁定上述规则集合。

使用边界与注意事项

  • 安全语义nat-unprotected 意味着放弃"阻止远程主机直连容器未发布端口/容器 IP"的过滤。源码注释的表述是 "If the user really wants to allow all access from the wider network"——该模式适用于信任网络或容器需要被直接寻址的场景,默认 nat 模式仍应作为首选。
  • 规则结构不稳定index.md 明确声明这是开发用途文档;且桥驱动在初始化时会删除自建链、随网络恢复重建,filter-FORWARD 链本身不清空,守护进程重启后规则顺序可能与首次创建时不同
  • 环境限制:golden 文档只在 iptables 后端下生成(firewalld 运行、rootless、nftables 后端下测试直接 skip);firewalld reload 时规则会被清空,守护进程通过 dbus 事件触发重建。
  • 验证方式:若需自行核对规则变化,可参考 iptablesdoc_linux_test.go 的流程——运行测试后用 TESTFLAGS='-update' 刷新 generated/ 下的参考文档,并同步更新 templates/ 中的文字说明(模板文字改动不被 golden 比对覆盖,需人工评审 diff)。
登录后查看全文
热门项目推荐
相关项目推荐