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

Docker 网络故障诊断:Namespace、Bridge、DNS、NAT、MTU 和抓包

Docker 容器网络故障通常表现为“容器访问不了服务”,但这个现象可能来自完全不同的层次:

  • 容器没有正确的网络命名空间或路由;
  • veth 与 Linux bridge 没有连通;
  • Docker 的转发规则或宿主机防火墙丢弃了数据包;
  • 容器内的嵌入式 DNS 无法解析名称;
  • DNS 已经成功,但目标端口没有监听;
  • NAT 或端口发布方向理解错误;
  • 路径 MTU 过大,导致大包丢失;
  • 抓包位置不对,误以为数据包没有发送。

因此,诊断的第一原则不是“先改 Docker 配置”,而是先把故障定位到数据路径中的某一段:

进程
  ↓
容器 network namespace
  ↓
容器 eth0 / veth
  ↓
宿主机 Linux bridge
  ↓
iptables/nftables、路由、NAT
  ↓
物理网卡或其他网络设备
  ↓
目标地址

名称解析则是一条并行但相互依赖的路径:

应用
  ↓
/etc/resolv.conf
  ↓
Docker embedded DNS(通常为 127.0.0.11)
  ↓
容器网络上的服务名解析,或宿主机配置的上游 DNS

下面以 Linux 上的 Docker Engine 为边界讨论。Windows 容器、Docker Desktop 的 Linux VM、Rootless Docker 和 Kubernetes CNI 会改变部分实现细节,不能直接套用所有命令和结论。


一、先建立模型:一次连接到底经过哪些组件

1. Network namespace:容器看到的不是宿主机网络

Linux network namespace 是内核隔离网络资源的机制。每个 namespace 可以拥有独立的:

  • 网络设备;
  • IP 地址;
  • 路由表;
  • 邻居表;
  • iptables/nftables 规则上下文;
  • socket 视图。

Docker 为普通 Linux 容器创建独立的 network namespace。容器内看到的 eth0,通常并不是宿主机真正的物理网卡,而是一个 veth pair 的一端。

一个典型拓扑如下:

容器 namespace                         宿主机 namespace

+---------------------+                +-----------------------------+
| eth0: 172.18.0.2    |                | vethxxxx                    |
| lo: 127.0.0.1       |                |       \                     |
| route: default via  |                |        docker0 / br-xxxx    |
|         172.18.0.1  |                |              |              |
+----------+----------+                |           eth0 / ens33       |
           |                           +-------------+---------------+
           | veth pair                               |
           +------------------------------------------+

veth pair 的两端始终连接在一起:一端收到的数据会从另一端出现。Docker 把容器一端放入容器 namespace,把宿主机一端接入 bridge。

因此,以下命令必须区分执行位置:

# 在容器 namespace 中
docker exec web ip addr
docker exec web ip route

# 在宿主机 namespace 中
ip link
ip addr
ip route

如果容器内执行:

ip addr

看到的是容器自己的 eth0;在宿主机执行同名命令,通常只能看到 veth 的宿主机端,而看不到容器内的 eth0

2. 如何进入容器的 network namespace

docker exec 是最简单的方式:

docker exec -it web sh

如果镜像没有 shell,可以用宿主机上的 nsenter。先取得容器内主进程 PID:

pid=$(docker inspect -f '{{.State.Pid}}' web)
sudo nsenter -t "$pid" -n ip addr
sudo nsenter -t "$pid" -n ip route
sudo nsenter -t "$pid" -n cat /etc/resolv.conf

这里的 -n 表示进入目标进程的 network namespace。这个方法很重要,因为容器可能没有 ipsstcpdump 等诊断工具。

查看容器与网络的 Docker 状态:

docker inspect web
docker network ls
docker network inspect appnet

docker inspect 展示的是 Docker 维护的对象状态;ipbridgenft 展示的是 Linux 内核和防火墙层面的实际状态。两者不一致时,通常意味着网络对象残留、规则未正确刷新、手工修改过宿主机网络,或诊断时机处于状态变更过程中。


二、Bridge:二层转发与三层路由不是同一件事

1. Linux bridge 做什么

Linux bridge 是宿主机上的二层交换设备。它根据目的 MAC 地址转发以太网帧,不根据 IP 地址选择三层路径。

Docker 常见的 bridge 网络包括:

  • 默认 bridge 网络,通常对应宿主机上的 docker0
  • 用户自定义 bridge 网络,通常对应名为 br-<network-id-prefix> 的设备。

查看设备:

ip -br link
ip -br addr
bridge link
bridge fdb show br docker0

典型结果可能类似:

docker0       UP             172.17.0.1/16
veth8a1c@if2   UP
br-91d2c3      UP             172.18.0.1/16

bridge fdb show 显示 bridge 的转发表。它将 MAC 地址映射到端口,例如某个 veth。若 bridge 未学习到目标 MAC,可能发生泛洪;若端口状态错误,帧则无法到达容器。

2. 同一 bridge 上的容器通信

假设:

web:  172.18.0.2
db:   172.18.0.3
网关: 172.18.0.1

web 访问 db 的 172.18.0.3:5432 时,数据路径大致为:

  1. web 根据路由表判断 172.18.0.3 与本机位于同一网段;
  2. web 通过 ARP 请求 172.18.0.3 对应的 MAC;
  3. 数据包从容器 eth0 发出;
  4. 经 veth pair 到达宿主机 bridge;
  5. bridge 根据 MAC 转发表把帧送入 db 对应的 veth;
  6. db namespace 收到数据包;
  7. db 内核把数据交给监听 5432 的 socket。

这种通信不需要经过外部物理网卡,也通常不需要 SNAT。目标地址仍然是 172.18.0.3

可以用下面的命令分别验证各段:

docker exec web ip route
docker exec web ip neigh
docker exec web ping -c 1 172.18.0.3
docker exec web nc -vz -w 2 172.18.0.3 5432

ping 成功只说明 ICMP 可达,不说明 TCP 端口可用;nc 能建立 TCP 连接,才能证明目标端口在当前路径上可达并接受连接。

3. 默认 bridge 与用户自定义 bridge 的重要差异

用户自定义 bridge 网络通常提供 Docker embedded DNS,使容器可以用容器名或 Compose 服务名解析彼此:

docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run --rm -it --network appnet busybox nslookup db

默认 bridge 网络的传统行为不同。容器通常可以通过 IP 通信,但不能依赖现代用户自定义网络提供的同等容器名解析能力。若需要服务发现,应显式创建用户自定义网络,或使用 Compose 管理网络。

一个常见错误是:

docker run -d --name api --network appnet my-api
docker run -d --name db postgres:16

此时 apiappnetdb 在默认 bridge。即使两个容器都运行在同一宿主机上,它们也不自动处于同一二层网络,api 不能把 db 当作同一网络中的服务名使用。

修复方式是让它们加入同一个网络:

docker network connect appnet db

或者在 Compose 中声明共同网络:

services:
  api:
    image: example/api
    networks: [appnet]
  db:
    image: postgres:16
    networks: [appnet]

networks:
  appnet:

Compose 中的 db 是服务名。它通常会解析到该服务当前容器的网络地址,但这不是一个永久固定的 IP。容器重建后 IP 可能变化,应用必须按名称重新解析,而不应把容器 IP 写死。


三、路由、转发与 NAT:数据包为什么能出去,回包为什么能回来

1. 容器路由表决定第一跳

在容器中执行:

docker exec web ip route

常见结果:

172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.2
default via 172.18.0.1 dev eth0

它表示:

  • 访问 172.18.0.0/16 内地址时,直接通过 eth0 发送;
  • 访问其他地址时,交给网关 172.18.0.1
  • src 172.18.0.2 是默认选择的源地址。

形式化地说,对于目的地址 D,路由表选择最长前缀匹配项:

R(D)=argmaxrroutesprefix_length(r)R(D)=\arg\max_{r \in \text{routes}} \operatorname{prefix\_length}(r)

例如同时存在:

172.18.0.0/16 dev eth0
default via 172.18.0.1

访问 172.18.5.10 时匹配 /16,不会走 default;访问 8.8.8.8 时没有更具体的匹配,才走 default。

如果容器没有默认路由,常见现象是:

ping: bad address 'example.com'

这可能是 DNS 问题,也可能是 DNS 请求根本没有可用的出站路径。必须先检查:

docker exec web ip route
docker exec web cat /etc/resolv.conf

2. 宿主机必须允许转发

容器访问外部网络时,数据包通常需要从 bridge 转发到物理网卡。宿主机需要允许 IPv4 转发:

sysctl net.ipv4.ip_forward

正常的转发路径要求该值为 1

sudo sysctl -w net.ipv4.ip_forward=1

但手工修改只是临时状态;永久配置应通过发行版的 sysctl 配置管理。Docker 通常会在需要时设置或依赖此能力,但系统启动脚本、防火墙管理器或安全基线可能再次修改它。

IPv6 要单独检查:

sysctl net.ipv6.conf.all.forwarding

开启 IPv4 转发并不等于 IPv6 也能正常转发。

3. NAT 的必要性:私有源地址如何获得回包

假设容器发送:

源:172.18.0.2:40000
目的:203.0.113.20:443

互联网中的路由器通常不会把 172.18.0.0/16 当作可返回到该 Docker 网络的公共路由。宿主机因此通常执行源地址转换,即 SNAT 或 MASQUERADE:

出站前:172.18.0.2:40000 → 203.0.113.20:443
出站后:宿主机公网IP:52000 → 203.0.113.20:443

回包到达宿主机后,连接跟踪表将:

宿主机公网IP:52000 → 203.0.113.20:443

还原为:

172.18.0.2:40000 → 203.0.113.20:443

NAT 不是单纯修改一个数据包字段,而是依赖 conntrack 保存连接映射。若 conntrack 表满、规则缺失或回程路径绕开了执行 NAT 的主机,连接可能表现为“能发出但收不到回包”。

查看相关状态:

sudo conntrack -L
sudo conntrack -S

系统未安装 conntrack 工具时,需要安装发行版对应的软件包。生产环境中直接清空连接跟踪表风险很高,因为会中断现存连接,不应把 conntrack -F 当作常规修复手段。

4. Docker 的 iptables/nftables 规则

Docker 常见地创建用于过滤、转发和 NAT 的规则链,例如:

  • DOCKER;
  • DOCKER-USER;
  • DOCKER-FORWARD;
  • 与 bridge 网络相关的隔离链。

具体链布局会随 Docker Engine 版本、iptables 后端和发行版变化。现代 Linux 上,iptables 命令可能由 iptables-nft 提供:命令语法仍是 iptables 风格,但规则最终由 nftables 内核框架承载。

检查两种视图:

sudo iptables -S
sudo iptables -t nat -S
sudo nft list ruleset

不要仅因为 iptables -L 输出为空,就断定没有规则;如果系统使用不同的后端,应该确认:

iptables --version
nft --version

DOCKER-USER 的意义是提供一个供管理员添加过滤规则的稳定入口。一般应把自定义限制放在那里,而不是直接改 Docker 自动生成的 DOCKER 链,因为 Docker 重启或网络重建可能覆盖自动生成部分。

例如,阻止某个外部网段访问 Docker 转发流量:

sudo iptables -I DOCKER-USER -s 203.0.113.0/24 -j DROP
sudo iptables -A DOCKER-USER -j RETURN

这条规则的实际影响取决于流量方向、接口和发行版的 FORWARD 策略。生产修改前应先备份规则,并用明确的允许规则验证,不要盲目把默认策略改为 ACCEPT 来“排除故障”,因为这可能绕过主机安全边界。


四、端口发布:DNAT、监听地址与冲突

1. -p 改变的是入站路径,不是容器监听地址

启动容器:

docker run -d --name web -p 8080:80 nginx:alpine

含义通常是:

宿主机地址:8080 → 容器地址:80

外部客户端访问宿主机 8080 时,Docker 通过 DNAT 把目标地址改为容器 IP 和端口:

客户端 → HOST:8080
              ↓ DNAT
        172.17.0.2:80

容器内的 nginx 仍然监听 80,而不是 8080。因此下面两种失败原因不同:

docker exec web ss -lnt

若只监听:

127.0.0.1:80

那么即使发布了 -p 8080:80,来自容器网络接口的连接也可能无法到达,因为服务只绑定在容器自己的 loopback 上。应用通常需要监听 0.0.0.0:80,或明确监听容器 IP。

2. 端口冲突发生在发布端

以下命令不能同时成功:

docker run -d --name a -p 8080:80 nginx:alpine
docker run -d --name b -p 8080:80 nginx:alpine

原因不是两个容器不能都监听容器内的 80,而是同一宿主机地址族和绑定地址上的 8080 发布端口发生冲突。

查看占用:

sudo ss -lntp | grep ':8080'
docker ps --format 'table {{.Names}}\t{{.Ports}}'

如果只需要绑定宿主机回环地址,可以缩小暴露范围:

docker run -d --name web -p 127.0.0.1:8080:80 nginx:alpine

这通常意味着只有宿主机本地和能够访问宿主机 loopback 的进程可访问,不能被外部网卡直接访问。端口发布的默认绑定行为应以当前 Docker Engine 配置和平台为准,不要假设所有环境都拥有相同的暴露范围。

3. 区分三种访问方式

对发布端口的诊断,必须明确客户端位置:

A. 其他机器 → 宿主机:8080
B. 宿主机 → 127.0.0.1:8080 或宿主机地址:8080
C. 容器 → 服务容器:80,或容器 → 宿主机:8080

A 主要验证外部接口、防火墙、DNAT 和回程路径。
B 还会受到宿主机监听地址、主机防火墙和 hairpin 行为影响。
C 如果访问的是同一 Docker 网络中的服务,通常应直接使用服务名和容器端口,而不是绕到宿主机发布端口:

docker exec api curl -v http://db:5432

数据库不是 HTTP 服务,上面的命令只用于展示寻址方式;实际应使用数据库客户端或 nc 检查 TCP:

docker exec api nc -vz -w 2 db 5432

在同一 Docker 网络中,db:5432host.docker.internal:published-port 更直接。后者依赖平台提供的特殊主机名和额外路径,不应作为 Linux 原生 Docker 容器间通信的默认方式。


五、DNS:容器名称解析、外部解析和搜索域

1. /etc/resolv.conf 不是 DNS 服务本身

容器内查看:

docker exec web cat /etc/resolv.conf

现代用户自定义网络中,常见结果包含:

nameserver 127.0.0.11
options ndots:0

127.0.0.11 是 Docker 为容器提供的 embedded DNS 地址。这里的 127.0.0.11 位于容器 network namespace 中;它不是直接访问宿主机的 127.0.0.11,因为每个 namespace 有独立的 loopback 和 socket 空间。

embedded DNS 通常负责两类请求:

  1. 用户自定义 Docker 网络内的容器名、服务名和网络别名;
  2. 将外部域名请求转发给 Docker 从宿主机配置推导出的上游 DNS。

因此,下面两个测试必须分开:

# 服务发现
docker exec api getent hosts db

# 外部 DNS
docker exec api getent hosts example.com

getent hosts 使用系统的 NSS 配置,比只测试 nslookup 更接近某些应用的真实解析路径。若镜像没有 getent,可以使用临时诊断容器:

docker run --rm --network container:api nicolaka/netshoot dig db
docker run --rm --network container:api nicolaka/netshoot dig example.com

--network container:api 让临时容器共享 api 的 network namespace,因此它看到的 IP、路由和 DNS 路径与 api 相同;这比单独启动一个网络不同的诊断容器更准确。

2. Service Name、网络别名与 IP 的关系

在 Compose 中:

services:
  api:
    image: example/api
    depends_on:
      - db
    networks: [appnet]

  db:
    image: postgres:16
    networks: [appnet]

networks:
  appnet:

api 访问数据库时应使用:

主机名:db
端口:5432

depends_on 主要表达启动顺序或依赖关系,不能保证数据库已经完成初始化并开始监听。即使 DNS 已经解析成功,数据库仍可能返回连接拒绝。应用应实现重试或健康检查,而不是把 DNS 成功当成服务就绪。

Compose 中可以通过 aliases 增加网络别名:

services:
  db:
    image: postgres:16
    networks:
      appnet:
        aliases:
          - database

此时同一网络中的客户端可能解析 dbdatabase。别名只在声明它的网络范围内生效。

容器重建后,服务名仍可解析,但返回的 IP 可能变化。因此:

正确:连接 db:5432,并在连接失败时重新解析/重试
错误:启动时解析 db 得到 172.18.0.3,永久缓存该 IP

3. Search Domain 与 ndots

DNS 名称不是简单的字符串替换。/etc/resolv.conf 中可能出现:

search project_default
options ndots:0

假设应用查询 db,解析器可能根据 search domain 尝试:

db.project_default
db

具体查询顺序由解析器实现和 ndots 决定。ndots:5 的常见效果是:少于 5 个点的名字可能先按相对名称拼接搜索域,再尝试绝对名称;ndots:0 则更倾向于直接查询该名称。

这会导致两个看似矛盾的现象:

  • getent hosts db 成功;
  • 应用查询 db.example.com 失败,或查询次数明显增加。

如果名称本身带有结尾点:

db.

它表示绝对 DNS 名称,通常不会再追加 search domain。诊断时应记录应用实际查询的名称、查询类型和返回码,而不是只在 shell 中测试一个短名称。

4. 常见 DNS 故障路径

服务名无法解析

docker exec api getent hosts db

失败时依次检查:

docker inspect api
docker inspect db
docker network inspect appnet
docker exec api cat /etc/resolv.conf

重点确认:

  • 两个容器是否真的连接到同一个用户自定义网络;
  • 服务名是否是 Compose 服务名,而不是容器内部应用名;
  • 目标容器是否已启动并仍在该网络中;
  • 是否使用了错误的网络别名;
  • 是否查询了不属于该网络作用域的名称。

外部域名无法解析

先分别验证 IP 连通性和 DNS:

docker exec api ping -c 1 1.1.1.1
docker exec api getent hosts example.com

如果 IP 可达而域名失败,重点检查:

  • /etc/resolv.conf
  • Docker daemon 的 DNS 配置;
  • 宿主机上游 DNS;
  • UDP/TCP 53 是否被防火墙阻断;
  • DNS 响应是否超过 MTU 或被中间设备丢弃。

如果 IP 也不可达,则 DNS 可能只是第一个暴露问题的应用层现象,应该回到路由、转发、NAT 和 MTU 检查。

DNS 成功但连接失败

docker exec api getent hosts db
docker exec api nc -vz -w 2 db 5432

第一条成功、第二条失败,说明名称解析路径大体正常,应检查:

docker exec db ss -lntp
docker logs db
docker inspect db

尤其要确认服务是否只监听 127.0.0.1、端口是否实际为 5432、容器是否正在重启,以及目标服务是否因为认证或初始化失败而主动关闭连接。


六、MTU:为什么小包成功,大包或 HTTPS 却失败

1. MTU 的定义与路径条件

MTU(Maximum Transmission Unit)是某个网络接口一次能够承载的最大三层 IP 数据包大小。以太网常见 MTU 是 1500 字节,但这不是所有路径的保证。

对一条路径而言,路径 MTU 可近似表示为:

PMTU=min(MTU1,MTU2,,MTUn)PMTU = \min(MTU_1, MTU_2, \ldots, MTU_n)

其中每个 MTUiMTU_i 是路径上第 ii 个链路或隧道接口允许的最大 IP 包大小。

如果发送方构造的包大小 LL 满足:

LPMTUL \le PMTU

则该包可以在不分片的情况下通过。若:

L>PMTUL > PMTU

则可能出现三种结果:

  1. 中间路由器分片;
  2. 中间路由器返回 ICMP “Fragmentation Needed”;
  3. 数据包被静默丢弃。

IPv4 允许部分分片,IPv6 路由器不负责分片,过大的包通常依赖 ICMPv6 Packet Too Big。防火墙阻断这些 ICMP 消息,会破坏 Path MTU Discovery。

2. Docker 中 MTU 变小的典型原因

常见路径包括:

容器 eth0 → veth → bridge → 物理网卡

如果底层使用 VXLAN、云厂商 overlay、VPN、GRE、WireGuard 或其他隧道,隧道头部会消耗额外空间。例如底层链路仍是 1500,但隧道额外占用 OO 字节,则可用上层 MTU 大约为:

MTUinner1500OMTU_{\text{inner}} \le 1500 - O

不能仅凭“宿主机物理网卡是 1500”推断容器路径一定支持 1500。

查看各层:

ip link show eth0
ip link show docker0
ip link show ens33
docker exec web ip link show eth0

Docker daemon 的 bridge MTU 可通过 daemon 配置或启动参数设置,具体配置名和支持范围应以当前 Engine 文档为准。修改后通常需要重建相关网络,已有容器不会自动获得所有网络属性变化。

3. 用 DF 测试路径

IPv4 下可用:

docker exec web ping -M do -s 1472 -c 2 1.1.1.1

1472 + 20 字节 IPv4 头 + 8 字节 ICMP 头 = 1500。如果失败,再降低 payload:

docker exec web ping -M do -s 1400 -c 2 1.1.1.1

这不是对所有目标和所有协议的完整证明,因为:

  • 目标可能不响应 ICMP;
  • 中间设备可能过滤 ICMP;
  • IPv6 使用不同的测试参数;
  • TCP 的分段和 MSS 行为不同。

更直接的 TCP 诊断可以结合抓包观察是否出现 SYN 成功但后续大段数据重传。对 TCP 而言,MSS 通常近似为:

MSS=MTUIP headerTCP headerMSS = MTU - IP\ header - TCP\ header

在无 TCP option 的简化情况下,IPv4/TCP 为 MTU - 20 - 20;实际 TCP 时间戳等 option 会影响可用负载,但 MSS 通告机制通常由端点在握手时协商。

4. MTU 故障的典型症状

MTU 或 PMTUD 故障经常表现为:

  • ping 小包成功;
  • TCP 三次握手成功;
  • 下载小响应成功,大响应卡住;
  • TLS ClientHello 或服务器证书阶段重传;
  • 某些网站可访问,另一些网站失败;
  • UDP 应用完全失败;
  • 只有跨主机或经过 VPN 的容器失败。

此时不要先把问题归因于 DNS。DNS 查询本身可能只有几十到几百字节,而真正失败的是后续应用数据。

调整 MTU 的原则是让容器接口、Docker bridge、底层路径之间满足可承载条件,而不是随意把所有接口改成 1400。过小的 MTU 会增加包数量和协议开销;过大的 MTU 则会导致分片或黑洞。修改前应测量实际路径并验证所有相关网络,尤其是多网络容器。


七、抓包:必须在正确的 namespace 和接口上观察

1. tcpdump 看到的只是某个观察点

抓包不是“查看网络真相”,而是查看某个接口在某个 namespace、某个时间点看到的帧。一个数据包可能在不同位置具有不同的地址:

容器 eth0:
172.18.0.2:40000 → 203.0.113.20:443

宿主机物理网卡 ens33:
192.0.2.10:52000 → 203.0.113.20:443

前者是 NAT 前,后者是 NAT 后。若只在 ens33 上抓包,看不到容器私有地址并不表示容器没有发送。

容器内抓包:

docker exec web tcpdump -ni eth0 'host 203.0.113.20 and port 443'

如果镜像没有 tcpdump,使用共享 namespace 的诊断容器:

docker run --rm -it \
  --network container:web \
  nicolaka/netshoot \
  tcpdump -ni eth0 'port 443'

宿主机 bridge 上抓包:

sudo tcpdump -ni docker0 'host 172.18.0.2'

用户自定义网络则可能是:

sudo tcpdump -ni br-91d2c3 'host 172.18.0.2'

宿主机物理出口抓包:

sudo tcpdump -ni ens33 'host 203.0.113.20 and port 443'

接口名必须替换为实际名称:

ip -br link
docker network inspect appnet

2. 一次出站连接应看到什么

以容器访问外部 HTTPS 为例,在容器 eth0 上通常观察到:

SYN
SYN, ACK
ACK
TLS/HTTP 数据

若在容器 eth0 上连 SYN 都没有:

  • 应用可能没有真正发起连接;
  • DNS 解析后选择了其他地址族;
  • 路由选择失败;
  • socket 被本地策略阻止。

若容器上有 SYN,但 bridge 上没有:

  • veth、bridge、namespace 连接异常;
  • 抓错了 bridge;
  • 过滤条件写错;
  • 数据包在更早的 hook 被丢弃。

若 bridge 上有 SYN,但物理网卡上没有:

  • 宿主机路由、FORWARD 规则或防火墙丢弃;
  • NAT 前规则未匹配;
  • 物理出口并不是你猜的接口。

若物理网卡上发出 SYN,但没有 SYN-ACK:

  • 上游网络、目标服务、防火墙或回程路径有问题;
  • 也可能目标明确丢弃了该源地址。

若物理网卡上能看到回包,但容器内看不到:

  • conntrack/NAT 反向转换异常;
  • 宿主机转发规则丢弃;
  • 回包路由错误;
  • 抓包过滤条件忽略了 NAT 后地址。

3. 端口发布的抓包方法

启动测试容器:

docker run -d --name web -p 8080:80 nginx:alpine

在宿主机上观察入站:

sudo tcpdump -ni any 'tcp port 8080'

再从另一台机器访问:

curl -v http://HOST_IP:8080/

同时在 Docker bridge 上观察容器端口:

sudo tcpdump -ni docker0 'tcp port 80'

如果宿主机 8080 有 SYN,而 bridge 上没有转为容器 80 的流量,检查端口发布规则和防火墙;如果 bridge 上有 SYN,而容器内没有,检查目标容器和接口;如果容器内收到 SYN 但没有响应,检查 nginx 监听地址、容器内防火墙或应用状态。

tcpdump -i any 适合快速观察,但它的接口归属、重复显示和链路层信息不如在具体接口上抓包清晰。确认路径后应改用具体接口,并用时间、五元组和方向缩小过滤范围。


八、一个可重复的端到端诊断实验

下面的实验构造一个最小的服务发现和端口测试环境:

docker network create diagnet

docker run -d \
  --name diag-server \
  --network diagnet \
  nginx:alpine

docker run --rm -it \
  --name diag-client \
  --network diagnet \
  busybox sh

diag-client 内执行:

cat /etc/resolv.conf
ip addr
ip route
nslookup diag-server
wget -S -O - http://diag-server/

预期逻辑是:

  1. ip addr 显示客户端在 diagnet 子网中的地址;
  2. ip route 显示该子网直连路由和默认路由;
  3. nslookup diag-server 返回 diag-server 的容器 IP;
  4. wget 连接该 IP 的 80 端口并得到 nginx 响应。

然后在宿主机查看网络对象:

docker network inspect diagnet
ip -br addr

网络检查结果应能对应到:

  • 一个 Docker bridge;
  • 一个网关地址;
  • 两个容器端点;
  • 每个容器的 IPv4 地址。

清理实验:

docker rm -f diag-server
docker network rm diagnet

如果第 3 步失败,优先调查网络归属和 embedded DNS;如果第 3 步成功、第 4 步失败,优先检查 nginx 是否运行、端口监听和 bridge 连通性;如果容器内访问成功而宿主机访问发布端口失败,则是另一条端口发布路径,不能用容器间成功来证明发布配置正确。


九、把“连接失败”拆成可验证的层次

一个有效的诊断流程应从低层到高层逐步增加条件,而不是一次执行一个复杂应用命令。

第一步:确认容器状态和网络归属

docker ps
docker inspect -f '{{.State.Status}} {{.State.Running}}' api
docker inspect -f '{{json .NetworkSettings.Networks}}' api
docker network inspect appnet

确认容器没有持续重启,并且客户端、服务端都加入了预期网络。

第二步:确认接口和路由

docker exec api ip addr
docker exec api ip route
docker exec api ip neigh

判断:

  • 是否存在 eth0
  • 是否有 IP;
  • 是否存在目标网段的直连路由;
  • 是否存在默认路由;
  • 网关和目标邻居是否能解析到 MAC。

ip neigh 出现 INCOMPLETEFAILED,说明 ARP/邻居发现没有完成,但原因可能是 bridge、地址冲突、接口状态或目标端点问题。

第三步:把 DNS 与 IP 连接分开

docker exec api getent hosts db
docker exec api nc -vz -w 2 db 5432

也可以直接连接解析得到的 IP:

docker exec api nc -vz -w 2 172.18.0.3 5432

对比结果:

名称解析 直接 IP 更可能的问题
失败 未测试 DNS、网络归属、embedded DNS
成功 失败 端口监听、服务状态、转发/过滤
成功 成功 应用层协议、认证、TLS 或请求内容
短名称失败,FQDN 成功 成功 search domain、服务名或别名配置

第四步:确认服务监听地址

docker exec db ss -lntp

重点区别:

127.0.0.1:5432       只接受容器自身 loopback 连接
0.0.0.0:5432         接受 IPv4 各接口地址
172.18.0.3:5432      只绑定该具体 IPv4 地址
[::]:5432            取决于 IPv6 dual-stack 配置

ss 显示监听不等于应用一定健康;还需结合应用日志和协议级客户端测试。

第五步:确认防火墙、转发和 NAT

sysctl net.ipv4.ip_forward
sudo iptables -S FORWARD
sudo iptables -S DOCKER-USER
sudo iptables -t nat -S
sudo nft list ruleset

不要只看计数为零就断定规则未匹配。规则计数可能被重置、使用了另一后端、流量走了不同地址族,或连接已经存在而没有再次经过相同路径。最好在短时间窗口内执行测试并观察计数变化:

sudo iptables -t nat -L -n -v

第六步:在两个以上观察点抓包

最少选择:

容器 eth0 + 宿主机物理出口

对于端口发布,再增加:

宿主机 bridge

用 SYN、SYN-ACK、RST、重传和 ICMP 错误判断丢包位置。TCP RST 通常说明某个节点主动拒绝或连接状态不匹配;静默超时更像丢弃、路由错误或回程失败,但仍需以抓包为证据。

第七步:最后检查 MTU 和应用协议

当小包和握手成功、较大数据失败时,执行 DF 测试并观察:

docker exec api ping -M do -s 1400 -c 2 1.1.1.1

然后使用真实协议客户端验证。例如:

docker exec api curl -v --max-time 5 https://example.com/

curl 输出可以区分 DNS、TCP、TLS 和 HTTP 阶段,但它不能代替抓包;它只能说明应用观察到的最终结果。


十、典型失败场景与反例

场景一:把容器名当作任意网络中的全局 DNS 名称

错误假设:

只要容器名是 db,任何容器都能解析 db

实际情况是名称解析具有网络作用域。容器是否能解析某名称,取决于它们是否连接到同一个提供该记录的 Docker 网络,以及名称是否是容器名、服务名或别名。

反例:

docker run -d --name db postgres:16
docker run --rm --network appnet busybox nslookup db

即使宿主机上存在名为 db 的容器,appnet 中的客户端也不一定能解析它,因为 db 未加入 appnet

场景二:DNS 成功就认为数据库可用

getent hosts db

成功只能证明名称解析返回了地址。数据库可能:

  • 尚未完成初始化;
  • 只监听 loopback;
  • 端口配置不同;
  • 主动拒绝连接;
  • 要求 TLS 或认证;
  • 因连接数限制关闭新连接。

必须继续测试 TCP,再测试数据库协议。

场景三:宿主机能访问发布端口,所以容器一定能访问

宿主机访问:

curl http://127.0.0.1:8080

成功只验证了宿主机到发布端口的路径。它没有证明:

  • 外部网卡可以访问;
  • 容器间 DNS 正常;
  • 容器访问宿主机发布端口的 hairpin 路径正常;
  • IPv6 路径正常。

容器间通信优先使用共享 Docker 网络中的服务名和容器端口,并单独测试宿主机发布路径。

场景四:关闭防火墙后问题消失,于是认为 Docker 配置错误

关闭防火墙可能绕过:

  • FORWARD 策略;
  • DOCKER-USER 过滤规则;
  • nftables 与 iptables 规则冲突;
  • 云主机安全组之外的本机过滤。

但这只是证明“过滤路径参与了故障”,不是证明 Docker 自动规则错误。正确修复应根据方向、接口、源地址、目的端口和连接状态添加最小范围规则,然后恢复安全策略。

场景五:把 MTU 统一改成 1500

1500 是常见以太网值,不是路径保证。若底层存在隧道,bridge 仍设置为 1500 可能导致大包黑洞;如果没有隧道而盲目改成 1400,又会增加额外开销。MTU 调整必须以实际路径测试、抓包和 PMTUD 行为为依据。


十一、生产环境中的状态、并发和恢复风险

1. 容器 IP 不是稳定身份

容器重建、Compose 扩缩容和网络重新创建都可能改变 IP。服务发现应依赖网络范围内的服务名或别名,连接池需要处理连接失效和重新解析。

同时,DNS 记录变化并不保证所有客户端立即感知。应用可能缓存 DNS,glibc、语言运行时、连接池和代理各自有不同缓存策略。服务已重建但客户端仍连接旧 IP,是常见的“网络看起来不稳定”现象。

2. 规则和连接跟踪存在时间状态

防火墙规则改变后,已有连接可能继续使用旧的 conntrack/NAT 映射;新连接则走新规则。因此:

旧连接成功、新连接失败

不一定矛盾。反过来也可能出现规则已修复,但旧连接状态仍然异常。

重启 Docker、删除网络或清空 conntrack 都可能中断大量业务连接。运维操作应先记录:

docker network inspect appnet > appnet-before.json
sudo iptables-save > iptables-before.txt
sudo nft list ruleset > nft-before.txt

并在低风险窗口执行。恢复不仅是“让 ping 通”,还要验证 DNS、TCP、TLS、应用协议和外部访问路径。

3. 地址重叠会产生确定性的错误路由

如果 Docker 网络使用了与宿主机办公网、VPN 或云 VPC 重叠的网段,例如都使用 172.18.0.0/16,最长前缀匹配可能把本应发往外部网络的流量送入 Docker bridge。

检查:

ip route
docker network inspect appnet

形式上,若宿主机存在:

172.18.0.0/16 dev br-xxxx
172.18.0.0/16 dev tun0

路由优先级、添加顺序和策略路由会决定实际出口;这种配置本身就容易造成不可预测的冲突。修复应重新规划 Docker address pool 和 VPN/VPC 网段,而不是依靠临时静态路由掩盖问题。

4. Rootless 和特殊平台不能直接套用宿主机 bridge 结论

Rootless Docker 可能使用用户态网络实现,宿主机上不一定出现与 rootful Docker 相同的 docker0、iptables NAT 和 veth 结构。Docker Desktop 的容器网络又位于 Linux VM 内,用户在 macOS 或 Windows 宿主机上抓物理网卡,未必能直接看到容器内部流量。

因此诊断前应先确认:

docker info
docker context show
docker info --format '{{json .SecurityOptions}}'

文章中的 namespace、bridge、iptables/nftables 路径主要适用于 Linux rootful Docker Engine;其他模式应以实际网络后端和平台边界为准。


十二、用证据构造故障结论

网络故障诊断的目标不是收集最多命令输出,而是验证数据路径上的必要条件。例如,容器访问外部 HTTPS 至少需要:

C=NRFTAC = N \land R \land F \land T \land A

其中:

  • NN:名称解析成功,或调用方已经获得正确 IP;
  • RR:路由选择了有效下一跳;
  • FF:bridge、宿主机转发和防火墙允许数据包通过;
  • TT:TCP/UDP 传输层完成预期交互;
  • AA:应用协议、TLS、认证和服务状态正常。

如果调用方直接使用 IP,则 NN 不参与该次连接;如果访问同一 bridge 内的容器,可能不需要外部 NAT;如果使用 UDP,则不能用 TCP 三次握手的证据替代 UDP 验证。

一个严谨的结论应类似:

容器 api 的 DNS 查询 db 成功,db 解析为 172.18.0.3;
api eth0 上能看到 SYN,db eth0 上也收到 SYN;
db 仅监听 127.0.0.1:5432,没有监听 172.18.0.3:5432;
因此故障位于服务监听地址,而不是 Docker DNS 或 bridge。

而不是笼统地说:

Docker 网络坏了。

同样,若容器内 eth0 看到 SYN,宿主机物理网卡看到 NAT 后的 SYN,但回包只出现在物理网卡、未出现在容器 bridge,则应继续检查 conntrack、反向 NAT、FORWARD 规则和回程路由,而不能仅凭“外网已经收到请求”判断服务端有问题。

Docker 网络由 Linux namespace、veth、bridge、路由、过滤、conntrack、NAT 和 DNS 共同构成。把名称解析、二层连通、三层转发、端口发布、路径 MTU 和抓包观察点分开,才能把“连接失败”还原成一个可验证的故障路径:哪个 namespace 发出了什么、经过哪个接口、是否被转换、在哪个位置消失,以及回包是否沿着状态表返回。


系列导航与关联阅读

官方资料

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