Docker 基础体系 · 第 48/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker IPv6、Macvlan 与 Host 网络:场景、隔离和路由边界

Docker 网络的关键不是“容器有没有 IP”,而是要回答三个问题:

  1. 这个 IP 属于哪个网络命名空间和地址域?
  2. 数据包经过哪些内核设备、路由表和防火墙规则?
  3. 隔离边界位于二层、三层、网络命名空间,还是根本没有建立?

IPv6、Macvlan 和 Host 网络分别改变了这些问题的不同部分:

  • Docker IPv6主要改变地址分配、路由和邻居发现,通常仍然运行在 Linux 网络命名空间隔离模型中。
  • Macvlan把容器接入宿主机的二层网络,使容器拥有独立的 MAC 和同一物理网络上的地址。
  • Host 网络直接移除容器自己的网络命名空间隔离,让容器进程使用宿主机的网络栈。

这三者不是互相替代的“网络模式”,也不能只用“性能更好”或“隔离更强”概括。下面从 Linux 容器边界、数据流和失败路径分别展开。


一、先建立 Docker 网络的 Linux 基础模型

1. 网络命名空间决定“谁看得到什么”

Linux 网络命名空间(network namespace)拥有一组独立的网络资源,包括:

  • 网卡和虚拟网卡;
  • IPv4、IPv6 地址;
  • 路由表;
  • 邻居表,例如 ARP 和 IPv6 NDP;
  • iptables/nftables 相关规则;
  • socket 端口空间;
  • lo 回环接口。

普通 Docker bridge 网络大致包含如下结构:

flowchart LR
    C[容器网络命名空间<br/>eth0: 172.20.0.2] 
    V1[veth pair]
    B[宿主机 docker bridge<br/>docker0 或 br-app]
    F[宿主机 netfilter<br/>iptables/nftables]
    R[宿主机路由表]
    E[外部网络]

    C --- V1 --- B
    B --> F
    F --> R
    R --> E

容器中的 eth0 和宿主机中的 veth 端点是一对虚拟以太网设备。数据包离开容器后进入宿主机 bridge,再由宿主机路由和过滤。

因此,容器中的:

ip addr
ip route
ip -6 route

看到的并不是宿主机的状态,而是容器网络命名空间的状态。

2. 路由选择先于大多数转发行为

对一个目的地址,Linux 通常先在当前网络命名空间中进行路由查找:

目的地址
  ↓
最长前缀匹配
  ↓
选择出口接口和下一跳
  ↓
邻居解析
  ↓
发送到链路

例如容器拥有:

IPv6 地址:2001:db8:10::2/64
默认路由:default via 2001:db8:10::1 dev eth0

当它访问 2001:db8:20::5 时:

  1. 2001:db8:20::5 不属于 2001:db8:10::/64
  2. 因此使用默认路由;
  3. 下一跳是 2001:db8:10::1
  4. 容器需要通过 IPv6 Neighbor Discovery Protocol(NDP)解析下一跳的链路层地址;
  5. 数据包进入宿主机 bridge 或其他网络设备。

如果没有默认路由,或者下一跳不可通过 NDP 解析,问题出在路由或邻居发现阶段,而不是 Docker DNS。

3. IPv6 与 IPv4 的一个重要差异

IPv4 中常见的 Docker 出站流程是:

容器私有 IPv4
  → bridge
  → 宿主机 IPv4
  → MASQUERADE
  → 外部网络

IPv6 通常不应简单套用这个模型。Docker 可以为 bridge 网络分配 IPv6 地址并配置路由,但一个可用的 IPv6 出站网络还必须满足外部网络能够把该 IPv6 前缀路由回宿主机

例如:

容器地址:2001:db8:100::2/64
Docker bridge:2001:db8:100::1/64

外部路由器必须知道:

2001:db8:100::/64 via 宿主机的 IPv6 地址

否则容器虽然能够把请求发出去,返回包却无法到达宿主机。

这里的核心条件是:

双向可达=容器到外部的正向路由外部到容器前缀的反向路由\text{双向可达} = \text{容器到外部的正向路由} \land \text{外部到容器前缀的反向路由}

只满足前半部分,连接仍然失败。


二、Docker IPv6:地址分配、路由和端口发布

1. Docker IPv6 功能的组成

Docker IPv6 支持不是一个单独的开关就能解释完整的功能,它至少涉及:

  1. Docker daemon 是否启用 IPv6;
  2. 网络是否启用 IPv6;
  3. Docker 为网络分配的 IPv6 前缀;
  4. Linux 内核是否启用 IPv6 转发;
  5. 宿主机和上游路由器是否能路由该前缀;
  6. 防火墙是否允许转发和入站流量;
  7. 应用是否监听 IPv6 地址。

常见 daemon 配置示例:

{
  "ipv6": true,
  "fixed-cidr-v6": "2001:db8:100::/64"
}

通常保存于 Linux 上的 /etc/docker/daemon.json。修改后需要重启 Docker daemon:

sudo systemctl restart docker

2001:db8::/32 是文档示例专用前缀,不可作为真实公网地址使用。生产环境应使用运营商或云平台实际委派的 IPv6 前缀;内部网络也可以使用 ULA,例如 fd00::/8,但 ULA 本身不会自动获得公网可达性。

验证 daemon 和网络状态:

docker info
docker network ls
docker network inspect bridge

如果 daemon 尚未启用 IPv6,直接在网络配置中写 enable_ipv6: true 可能失败,或者网络能够创建但无法获得预期的全局 IPv6 路由。具体行为取决于 Docker Engine 版本和网络驱动配置,不能只看 Compose 文件判断能力是否完整。

2. 用户定义 bridge 网络中的 IPv6

一个可读性较好的 Compose 示例:

services:
  web:
    image: nginx:alpine
    networks:
      app:
        ipv4_address: 172.30.0.10
        ipv6_address: 2001:db8:100::10
    ports:
      - "8080:80"

networks:
  app:
    driver: bridge
    enable_ipv6: true
    ipam:
      config:
        - subnet: 172.30.0.0/24
          gateway: 172.30.0.1
        - subnet: 2001:db8:100::/64
          gateway: 2001:db8:100::1

启动:

docker compose up -d

检查地址和路由:

docker compose exec web ip -6 addr show dev eth0
docker compose exec web ip -6 route
docker network inspect app

典型结果应包含:

inet6 2001:db8:100::10/64 scope global
default via 2001:db8:100::1 dev eth0

每一步成立的原因是:

  • enable_ipv6: true 允许该 bridge 网络配置 IPv6;
  • subnet 定义容器地址所在的 IPv6 前缀;
  • gateway 指定容器的下一跳;
  • ipv6_address 请求静态地址;
  • ports 配置的是宿主机发布端口,不等于取消容器地址本身;
  • ip -6 route 可以确认地址配置之后是否真正拥有默认路由。

静态 IPv6 地址必须位于该网络声明的前缀内,且不能和其他容器或网关冲突。Docker 的 IPAM 负责分配动态地址,但静态地址冲突仍然可能由手工配置造成。

3. IPv6 地址不等于公网可达

下面是一个常见反例:

容器:2001:db8:100::10
宿主机:2001:db8:1::20
上游路由器:知道 2001:db8:1::/64
上游路由器:不知道 2001:db8:100::/64

容器向外发送请求时,宿主机可能成功转发:

容器 → 宿主机 → 上游路由器 → 互联网

但返回包按照目标地址 2001:db8:100::10 查找路由时,没有任何设备知道该前缀位于哪台宿主机后面,于是丢失。

这和以下错误不同:

  • 没有默认路由:容器一开始就无法发出包;
  • NDP 失败:找不到同链路下一跳的 MAC;
  • 上游无回程路由:请求发出但响应回不来;
  • 防火墙丢包:路由存在,但转发或入站规则拒绝;
  • 应用未监听 IPv6:网络层可达,但 TCP 连接没有监听者。

生产中使用全局 IPv6 前缀时,应明确前缀的来源和回程路由方式。云环境可能要求为子网添加路由、绑定委派前缀,或者使用平台提供的 IPv6 地址分配机制。不能假设 Docker 会替你向物理路由器发布 BGP、静态路由或 NDP 代理。

4. ULA、NAT66 与路由

使用 ULA 的示例:

networks:
  private6:
    driver: bridge
    enable_ipv6: true
    ipam:
      config:
        - subnet: fd42:42:42::/64

这种网络适合内部服务之间通信,但不能直接访问公网 IPv6,除非存在以下之一:

  1. 上游网络知道 fd42:42:42::/64 的回程路由;
  2. 宿主机或边界设备配置了 IPv6 转换机制;
  3. 容器通过代理或其他应用层出口访问外部。

IPv6 的设计倾向于端到端路由,而不是默认依赖 IPv4 式 NAT。Docker 的 IPv6 bridge 配置不应被理解为“自动提供完整 NAT66 出口”。如果业务确实需要 NAT66,应由明确的边界网络设备或 Linux 防火墙规则实现,并接受其可观测性、地址语义和排障复杂度。

5. IPv6 端口发布的边界

services:
  api:
    image: example/api:latest
    ports:
      - "8080:8080"

端口发布的含义是:Docker 在宿主机上接收访问 8080 的流量,并根据规则转发到容器的 8080

它不等于:

容器获得宿主机的 IPv6 地址

也不等于:

容器自动获得一个独立公网监听入口

验证发布状态:

docker ps
docker port <container>
ss -lnt
sudo nft list ruleset

实际规则可能由 iptables-legacy、iptables-nft 或 Docker 当前使用的防火墙后端呈现,不能假设一定存在某个固定链名。重点是验证三层状态:

  1. Docker 是否记录了端口映射;
  2. 宿主机是否有对应监听或转发路径;
  3. 容器进程是否监听目标端口。

应用如果只监听:

127.0.0.1:8080

那么即使 Docker 端口发布正确,来自容器网络接口的转发流量也可能无法访问。要同时提供 IPv4 和 IPv6 服务,应用通常需要监听 0.0.0.0::,但具体行为受应用框架和 IPV6_V6ONLY 等系统设置影响,不能只根据配置字符串推断。


三、IPv6 中的 NDP:为什么 ARP 经验不完全适用

IPv4 使用 ARP 将 IP 地址解析为 MAC 地址。IPv6 使用 ICMPv6 Neighbor Discovery Protocol,包括:

  • Neighbor Solicitation(NS);
  • Neighbor Advertisement(NA);
  • Router Solicitation(RS);
  • Router Advertisement(RA)。

例如容器向网关 2001:db8:100::1 发送包时,内核可能先发出 NS:

谁拥有 2001:db8:100::1?
请把你的 MAC 告诉 2001:db8:100::10

如果 ICMPv6 被错误防火墙规则过滤,表现可能不是“IPv6 完全没有地址”,而是:

ping -6 2001:db8:100::1
# Destination unreachable
# 或长时间超时

诊断时应同时检查:

docker compose exec web ip -6 neigh
docker compose exec web ping -6 -c 3 2001:db8:100::1
sudo tcpdump -ni any icmp6

ip -6 neigh 可能显示:

2001:db8:100::1 dev eth0 FAILED

这表示邻居解析失败。此时继续修改 Docker DNS 或应用配置没有意义。

在 Macvlan 中,NDP 更接近真实物理网络:容器的 IPv6 地址可能直接通过宿主机物理接口对外发送 NDP。交换机、云平台的端口安全策略、虚拟化平台的 MAC/ND 限制,都可能影响结果。


四、Macvlan:把容器放入二层网络

1. Macvlan 的工作模型

Macvlan 是 Linux 内核提供的虚拟网络设备类型。Docker 使用它时,宿主机在指定父接口上创建逻辑网络,使每个容器拥有一个独立的虚拟接口和 MAC 地址。

典型结构:

flowchart LR
    C1[容器 A<br/>192.168.50.10<br/>MAC A]
    C2[容器 B<br/>192.168.50.11<br/>MAC B]
    M[Macvlan 子接口/内核转发]
    P[宿主机物理接口 eth0]
    S[交换机]
    G[局域网网关]

    C1 --- M
    C2 --- M
    M --- P
    P --- S
    S --- G

与 bridge 的主要区别是:

Bridge:
容器通常连接私有子网,宿主机承担三层网关、NAT 或端口发布。

Macvlan:
容器可以直接使用物理网络中的地址和默认网关,通常不需要 Docker bridge NAT。

这里的“直接”不是指容器绕过 Linux 内核,而是指容器的二层帧通过 macvlan 逻辑设备和父接口进入外部 LAN,不再先进入 Docker 的私有 bridge 地址域。

2. 创建一个 Macvlan 网络

假设:

物理接口:eth0
局域网:192.168.50.0/24
局域网网关:192.168.50.1
Docker 可用地址:192.168.50.192/27

创建网络:

docker network create -d macvlan \
  --subnet=192.168.50.0/24 \
  --gateway=192.168.50.1 \
  --ip-range=192.168.50.192/27 \
  -o parent=eth0 \
  lan_macvlan

启动容器:

docker run -d \
  --name lan-nginx \
  --network lan_macvlan \
  --ip 192.168.50.200 \
  nginx:alpine

检查:

docker network inspect lan_macvlan
docker exec lan-nginx ip addr
docker exec lan-nginx ip route

预期:

eth0: 192.168.50.200/24
default via 192.168.50.1

这些配置成立的前提是:

  • eth0 确实是连接目标 LAN 的父接口;
  • 192.168.50.200 没有被其他设备使用;
  • 网关允许该地址通过;
  • 交换机和虚拟化平台允许额外的源 MAC;
  • 物理网络对该容器地址存在正常的 ARP 或 NDP 行为。

--ip-range 只影响 Docker 从哪个地址池分配地址,不会自动在 DHCP 服务器上登记租约,也不会替你修改交换机或路由器配置。

3. 802.1Q trunk 模式

如果父接口写成:

-o parent=eth0.50

表示使用已存在的 VLAN 子接口。Docker 也支持通过带点号的 parent 名称使用 802.1Q VLAN 场景,例如:

docker network create -d macvlan \
  --subnet=192.168.50.0/24 \
  --gateway=192.168.50.1 \
  -o parent=eth0.50 \
  lan_vlan50

这要求:

  1. 宿主机已经能够正确处理 VLAN 50;
  2. 交换机端口配置与 VLAN 模式一致;
  3. 网关位于同一个 VLAN;
  4. 不存在把 access 端口和 trunk 配置混用的情况。

如果 VLAN 配置错误,容器通常会表现为地址存在但网关不可达。此时可以检查:

ip -d link show eth0.50
tcpdump -eni eth0 vlan

4. Macvlan 的模式和真实隔离能力

常见 Macvlan 模式包括:

  • bridge:同一父接口上的 Macvlan 端点可以直接互通,并可与外部网络通信;
  • private:限制同一父接口上的端点互通,但仍可按配置访问外部网络;
  • vepa:依赖外部交换机回流处理同一端口上的流量;
  • passthru:通常用于单个端点的特殊场景,约束更多。

Docker 最常见的是 bridge 模式。它提供的是二层接入方式,不是自动的安全分区。若多个容器处于同一个 Macvlan 子网:

容器 A 192.168.50.10
容器 B 192.168.50.11

它们通常位于同一个广播域,可以直接进行二层或三层通信。要实现安全隔离,需要使用不同 VLAN、不同子网、外部防火墙或其他网络策略。

5. Macvlan 的宿主机不可达陷阱

Macvlan 最容易误解的行为是:

Macvlan 容器通常不能直接访问创建它的宿主机父接口地址,
宿主机也通常不能直接通过父接口访问 Macvlan 容器。

例如:

宿主机 eth0:192.168.50.20
容器:192.168.50.200

从局域网其他机器访问 192.168.50.200 可能成功,但在宿主机上执行:

ping 192.168.50.200

却可能失败。

原因不是 Docker 防火墙拒绝,而是 Linux Macvlan 的端点隔离语义:父接口本身不能像普通交换机端口一样直接与其 Macvlan 子端点互通。

常见解决方式是在宿主机创建一个同一 Macvlan 网络中的宿主端点:

sudo ip link add macvlan-host link eth0 type macvlan mode bridge
sudo ip addr add 192.168.50.201/32 dev macvlan-host
sudo ip link set macvlan-host up
sudo ip route add 192.168.50.192/27 dev macvlan-host

之后宿主机访问容器:

ping 192.168.50.200

数据路径变为:

宿主机进程
  → macvlan-host
  → eth0 上的 macvlan 逻辑转发
  → 容器

这里使用独立的 192.168.50.201,不能把宿主机原有 eth0 地址复制到新接口上,否则会产生地址重复和路由歧义。

该配置通常不会自动持久化。重启宿主机、网络服务重载或父接口变化后,接口和路由可能消失。生产系统应通过 NetworkManager、systemd-networkd 或发行版对应的网络配置持久化,并在恢复脚本中验证接口、地址和路由。

6. Macvlan 的外部网络约束

每个 Macvlan 容器通常拥有不同的 MAC 地址。这带来几个边界:

  • 交换机可能启用了端口安全,只允许一个或有限数量的 MAC;
  • 云厂商可能禁止虚拟机发送未登记的源 MAC;
  • 虚拟化平台可能要求开启混杂模式、Promiscuous Mode 或 Forged Transmit;
  • 无线网卡通常不适合作为承载多个独立 MAC 的 Macvlan 父接口;
  • 大量容器会增加交换机 MAC 表、邻居表和广播处理压力。

因此,Macvlan 在裸机或受控物理网络中比较自然;在公有云虚拟机中,是否可用取决于云平台的二层虚拟化策略,而不是 Docker 命令本身。

如果目标只是让容器拥有独立的 IP,同时减少额外 MAC,通常应评估 Ipvlan。Ipvlan 可以让多个端点共享父接口的 MAC,在某些网络环境中更容易通过端口安全限制。但 Ipvlan 仍然受上游路由、VLAN、地址规划和宿主机可达性等约束,不能把它当作 Macvlan 的无条件替代。


五、Macvlan 与 IPv6 一起使用时的边界

Macvlan 和 IPv6 可以组合使用:

docker network create -d macvlan \
  --ipv6 \
  --subnet=2001:db8:50::/64 \
  --gateway=2001:db8:50::1 \
  -o parent=eth0 \
  lan6_macvlan

容器可以获得:

2001:db8:50::10/64

但这会把问题直接推入真实二层网络:

  1. 容器需要正常发送 NDP;
  2. 网关需要接受并响应容器的 NDP;
  3. 交换机和虚拟化平台需要允许对应的 MAC 和 ICMPv6;
  4. 外部路由器必须知道该地址或前缀的归属;
  5. 防火墙不能错误阻断 ICMPv6 的必要消息。

IPv6 中的 ICMPv6 不是普通“可选 ping 协议”。NDP、路径 MTU 发现等机制依赖 ICMPv6。粗暴使用:

ip6tables -A FORWARD -p ipv6-icmp -j DROP

可能导致地址看似配置完成,但邻居发现或大报文传输异常。

诊断 Macvlan IPv6 时,应从容器逐层测试:

docker exec <container> ip -6 addr
docker exec <container> ip -6 route
docker exec <container> ip -6 neigh
docker exec <container> ping -6 -c 3 <gateway>
docker exec <container> ping -6 -c 3 <external-ipv6>

判断顺序是:

  • 没有地址:Docker 网络或 IPAM 配置问题;
  • 没有默认路由:网关配置问题;
  • 网关邻居为 FAILED:NDP、VLAN、链路或防火墙问题;
  • 网关可达但外部不可达:转发或上游路由问题;
  • 外部 IP 可达但域名失败:DNS 或应用配置问题;
  • TCP 失败但 ICMPv6 成功:监听、端口或防火墙问题。

六、Host 网络:共享宿主机网络命名空间

1. Host 模式的定义

Host 网络模式不创建独立的容器网络命名空间,而是让容器进程使用宿主机的网络命名空间。

docker run -d \
  --name host-nginx \
  --network host \
  nginx:alpine

此时容器内执行:

ip addr
ip route
ss -lnt

看到的基本上是宿主机的网络状态。容器不会获得一个独立的 eth0,也不存在 Docker bridge 到容器的 veth 路径。

数据流可以简化为:

flowchart LR
    P[外部客户端]
    H[宿主机网络接口<br/>eth0 / lo / IPv4 / IPv6]
    S[共享网络命名空间中的 socket]
    C[Host 模式容器进程]

    P --> H --> S --> C

这意味着 Host 模式改变的不是“容器使用哪段地址”,而是直接移除了网络命名空间隔离。

2. -p 在 Host 模式下没有作用

下面的命令虽然语法可能被接受:

docker run -d \
  --network host \
  -p 8080:80 \
  nginx:alpine

-p 8080:80 不会建立普通 bridge 网络中的端口映射。容器内 Nginx 监听哪个端口,宿主机就直接暴露哪个端口。

如果 Nginx 监听:

0.0.0.0:80
[::]:80

外部访问宿主机的 80 端口即可到达容器进程。如果应用监听:

127.0.0.1:8080

则它只绑定宿主机网络命名空间中的回环地址,不会自动出现在宿主机物理接口的 8080 上。

检查方式:

docker inspect host-nginx \
  --format '{{.HostConfig.NetworkMode}}'

docker exec host-nginx ss -lnt
ss -lnt

两处看到的监听 socket 会属于同一网络命名空间。端口冲突也发生在同一端口空间中:

docker run --rm --network host nginx:alpine
docker run --rm --network host nginx:alpine

如果两个进程都尝试监听宿主机的 80,后启动者通常会收到:

bind: address already in use

这不是 Docker bridge 端口发布冲突,而是普通 Linux socket bind 冲突。

3. Host 模式中的 IPv6

Host 模式下,容器使用宿主机已经拥有的 IPv6 地址和路由:

宿主机:
eth0 = 2001:db8:1::20/64
default via 2001:db8:1::1

Host 容器:
不获得独立 IPv6
直接使用上述网络栈

因此,Host 容器访问外部 IPv6 时,不需要 Docker 为它分配新的 IPv6 子网。它的连通性等同于宿主机进程的连通性。

但这也意味着:

  • 无法为容器单独分配一个 Docker 管理的 IPv6 地址;
  • 无法依赖 Docker bridge 的容器级地址边界;
  • 容器中的进程可能监听宿主机的所有接口;
  • 容器访问 localhost 时,看到的是宿主机网络命名空间上的服务;
  • 宿主机其他服务和 Host 容器共享端口资源。

一个重要的安全后果是:

Host 模式不是“绕过 Docker 端口映射”,而是“直接使用宿主机网络栈”。

因此,应用本身的监听地址、宿主机防火墙和进程权限变得更加重要。

4. Host 模式与 Compose

Compose 中可以这样声明:

services:
  metrics-agent:
    image: example/metrics-agent:latest
    network_mode: host

此时不要同时写:

ports:
  - "9100:9100"

因为 Host 模式不存在需要发布的容器私有端口。也不能把 network_mode: host 与普通 networks: 配置混用来期待容器同时连接 bridge、Macvlan 等网络。network_mode 选择的是容器使用的网络命名空间,和加入一个或多个 Docker 网络是两种不同机制。

此外,Compose 中的服务名 DNS 只对加入 Docker 用户定义网络的服务成立。Host 模式服务不应被假设拥有普通 Compose 网络中的容器别名解析行为。

5. Linux 边界与平台差异

本文讨论的是 Linux 容器边界。Docker Desktop 在 macOS 和 Windows 上通常运行一个 Linux VM,容器实际位于该 VM 的 Linux 网络环境中。宿主操作系统看到的网络路径还会经过 Desktop 的虚拟化和转发层,因此不能把 Linux 裸机上的:

--network host

直接等同于“共享 macOS 或 Windows 主机的物理网络命名空间”。

Host 网络支持还可能受 Docker Engine、rootless 模式和平台实现影响。部署前应使用目标环境实际验证:

docker version
docker info
docker run --rm --network host alpine ip addr

尤其在 rootless Docker 中,网络由用户态组件和权限边界参与实现,Host 模式的语义和性能不能假设与 rootful Engine 完全一致。


七、三种模式的隔离边界对比

维度 IPv6 bridge Macvlan Host
是否有独立网络命名空间 通常是
是否有独立容器接口 通常有 eth0 有 Macvlan 接口 没有独立接口
地址来源 Docker IPAM 分配 IPv6 前缀 外部 LAN 或 Docker 配置的地址池 宿主机地址
是否经过 Docker bridge 否,经过 Macvlan/父接口
是否通常需要端口发布 对外访问通常需要 通常不需要,可直接访问容器地址 不需要,直接使用宿主机端口
容器间隔离 网络命名空间和 bridge 规则 网络命名空间,但同一二层网络可互通 无网络命名空间隔离
宿主机直接访问容器 通常可通过 bridge 地址访问 父接口默认不可直接访问,需额外 Macvlan 端点 通过共享网络栈直接访问
IPv6 回程路由要求 使用全局前缀时需要 使用真实 LAN 地址时需要 使用宿主机已有路由
主要故障域 路由、NDP、Docker 防火墙、上游路由 VLAN、MAC 限制、NDP、地址冲突 端口冲突、宿主机暴露、应用绑定

表中的“隔离”需要谨慎理解:

  • bridge 的网络命名空间隔离不等于应用安全隔离;
  • Macvlan 的独立 MAC 不等于安全隔离;
  • Host 模式基本没有网络命名空间隔离,但仍可能有容器进程、文件系统和权限方面的其他隔离;
  • IPv6 地址是全局地址还是 ULA,不决定容器是否拥有独立网络命名空间。

八、数据流算例:从容器访问外部 IPv6

假设使用 IPv6 bridge:

容器 eth0:
2001:db8:100::10/64

Docker bridge:
2001:db8:100::1/64

容器默认路由:
default via 2001:db8:100::1

容器访问:

2001:db8:200::20:443

完整过程如下:

第一步:容器路由查找

目标 2001:db8:200::20 不属于本地 2001:db8:100::/64,选择默认路由:

下一跳:2001:db8:100::1
出口:eth0

第二步:NDP 解析网关

容器发出 ICMPv6 Neighbor Solicitation,获得网关的链路层地址。若邻居表进入 FAILED,数据包不会有效离开容器。

第三步:进入宿主机 bridge

容器的 veth 对端收到帧,并进入 Docker bridge。宿主机内核根据转发规则判断是否允许从 bridge 转向外部接口。

第四步:宿主机 IPv6 路由

宿主机查找 2001:db8:200::20 的路由。可能经由 eth0 和上游网关转发。

第五步:外部返回

外部设备必须知道:

2001:db8:100::/64 位于该宿主机后方

否则响应无法返回容器。

因此,连接失败的位置可以表示为:

容器路由失败
  → NDP 失败
  → Docker/宿主机转发失败
  → 宿主机上游路由失败
  → 外部回程路由失败
  → 应用端口或防火墙失败

使用 tcpdump 可以定位方向:

sudo tcpdump -ni any 'icmp6 or tcp port 443'

解释抓包位置时要注意:-i any 适合快速观察,但在复杂环境中会重复显示或隐藏接口细节。需要确认具体路径时,应分别抓:

sudo tcpdump -ni docker0 icmp6
sudo tcpdump -ni eth0 icmp6

九、常见误解与反例

误解一:给容器启用 IPv6 就自动获得公网访问

错误。启用 IPv6 只解决 Docker 内部地址配置的一部分。公网可达还需要:

地址唯一
+ 容器默认路由
+ 宿主机 IPv6 转发
+ 上游回程路由
+ ICMPv6/NDP 正常
+ 防火墙允许
+ 应用监听 IPv6

缺少任一项,都可能失败。

误解二:Macvlan 容器一定能访问宿主机

错误。Macvlan 的父接口与子端点之间存在特殊隔离。宿主机访问容器需要额外创建 Macvlan host 端点,或者让流量经由外部网络绕行。

误解三:Macvlan 比 bridge 更安全

错误。Macvlan 主要改变二层接入和地址呈现方式。处于同一 Macvlan 网络的容器通常共享同一广播域,安全策略仍然需要由 VLAN、路由器、防火墙或应用自身提供。

误解四:Host 网络性能好,所以应该默认使用

错误。Host 模式减少了网络虚拟化层,但同时放弃了容器级网络命名空间隔离和独立端口空间。它适合确实需要观察宿主机接口、绑定大量端口或使用某些低层网络能力的程序,不适合作为普遍默认值。

误解五:端口发布和容器地址是同一件事

例如:

ports:
  - "8080:80"

表达的是:

宿主机 8080 → 容器 80

而 Macvlan 中直接访问:

192.168.50.200:80

不需要 Docker 端口发布。Host 模式中访问:

宿主机地址:80

实际连接的是共享网络命名空间中的监听 socket。三者的数据路径不同,排障方式也不同。


十、按故障层次进行诊断

1. 先确认模式和地址

docker inspect <container> \
  --format 'mode={{.HostConfig.NetworkMode}} networks={{json .NetworkSettings.Networks}}'

然后检查:

docker exec <container> ip addr
docker exec <container> ip -6 addr
docker exec <container> ip route
docker exec <container> ip -6 route

需要确认:

  • 容器到底使用 bridge、Macvlan 还是 Host;
  • IPv6 地址是否存在;
  • 默认路由是否存在;
  • 地址是否位于预期前缀;
  • Host 模式下是否误以为应该存在独立 eth0

2. 再测试链路和网关

IPv4:

docker exec <container> ping -c 3 <ipv4-gateway>

IPv6:

docker exec <container> ping -6 -c 3 <ipv6-gateway>
docker exec <container> ip -6 neigh

如果网关不可达,先不要测试 DNS 和应用端口。网关不可达说明问题位于容器接口、VLAN、bridge、Macvlan、ARP/NDP 或防火墙层。

3. 区分 DNS 与路由问题

docker exec <container> getent hosts example.com
docker exec <container> getent ahosts example.com

如果域名解析出 IPv6 地址但连接失败,可以直接测试字面量地址:

docker exec <container> wget -6 -O- http://[2001:db8:200::20]/

IPv6 字面量放在 URL 中时必须使用方括号,否则冒号会与端口分隔符混淆。

4. 检查端口监听与发布

Bridge 或 Macvlan:

docker exec <container> ss -lntp
docker port <container>

Host:

docker exec <container> ss -lntp
ss -lntp

如果 Host 模式下端口冲突,直接查看宿主机监听进程;如果 bridge 模式下端口发布异常,还要检查 Docker 防火墙规则和宿主机转发策略。

5. 观察实际数据包

Bridge:

sudo tcpdump -ni docker0 'ip6 or arp'
sudo tcpdump -ni any 'icmp6 or tcp'

Macvlan:

sudo tcpdump -eni eth0 'arp or icmp6'

重点观察:

  • 是否发出 NS/NA;
  • 是否看到网关响应;
  • 请求是否从父接口离开;
  • 是否收到返回包;
  • 返回包是否到达容器接口。

如果请求能在 eth0 上看到、响应也到达 eth0,但容器仍收不到,问题可能位于宿主机过滤、Macvlan 模式或邻居状态,而不是上游路由。


十一、如何选择网络模式

可以用网络边界来选择,而不是按“功能多少”选择。

选择 IPv6 bridge

适合:

  • 容器之间需要 Docker 网络隔离;
  • 需要 Compose 服务发现;
  • 希望由 Docker 管理地址和网络生命周期;
  • 可以接受端口发布或已规划 IPv6 路由;
  • 不要求容器直接作为物理 LAN 节点出现。

它的关键工作是规划:

Docker IPv6 前缀
宿主机转发
上游回程路由
防火墙
NDP

选择 Macvlan

适合:

  • 容器必须直接出现在现有二层 LAN;
  • 网络设备需要看到容器独立地址;
  • 不希望通过宿主机端口映射访问服务;
  • 可以控制 VLAN、交换机和虚拟化平台;
  • 能接受宿主机访问限制和额外 MAC 的运维成本。

不适合:

  • 父接口是普通 Wi-Fi;
  • 云平台禁止额外 MAC;
  • 需要大量容器但交换机端口安全严格;
  • 把“独立 MAC”误当成安全隔离。

选择 Host

适合:

  • 程序需要宿主机所有接口和路由;
  • 端口范围动态或数量较多,难以逐个发布;
  • 应用确实需要低层网络观察能力;
  • 可以接受宿主机端口空间和网络隔离的消失。

不适合:

  • 需要多个实例监听同一端口;
  • 需要容器级网络策略;
  • 需要每个容器独立 IPv6 地址;
  • 希望通过 Docker 网络提供清晰的服务边界。

十二、最终边界

三种模式的本质可以压缩为三句话:

  1. IPv6 bridge:容器仍然处在独立网络命名空间中,Docker 管理地址和虚拟链路,但真实 IPv6 可达性最终取决于路由、NDP、转发和防火墙。
  2. Macvlan:容器被接入宿主机的二层网络,拥有独立 MAC 和通常独立的 LAN 地址,但会受到交换机、VLAN、云平台和宿主机不可直达语义的约束。
  3. Host:容器进程直接使用宿主机网络命名空间,没有独立容器网络地址和端口空间,换来的简单路径同时意味着更弱的网络隔离。

因此,判断一个 Docker 网络方案是否正确,不能只检查:

容器是否拿到了 IP

还必须检查:

地址属于哪个网络
默认路由指向哪里
邻居解析是否成功
数据包经过哪些接口
返回路径是否存在
端口由谁监听
防火墙在哪一层生效
宿主机与容器之间是否存在预期的可达性

只有把这些边界逐层验证,IPv6、Macvlan 和 Host 网络才不会从配置选项变成难以解释的故障来源。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。