跳到主要内容

Docker 拉取失败排查记录

创建日期:2026-09-16 | 最近更新:2026-09-16

环境:Ubuntu(kernel 6.8.0-138)· Docker 29.5.3 · mihomo-party(Clash Party) 主机:LAN 192.168.1.50,网关 192.168.1.1,ufw 运行中 排查日期:2026-09-15


一、现象

$ docker pull qdrant/qdrant:latest
Error response from daemon: failed to resolve reference "docker.io/qdrant/qdrant:latest":
failed to do request: Head "https://registry-1.docker.io/v2/qdrant/qdrant/manifests/latest":
dial tcp [2a03:2880:f107:83:face:b00c:0:25de]:443: i/o timeout

关键线索:报错里的地址 2a03:2880::/32Facebook 的 IPv6 段。Docker Hub 不可能解析到这里 —— 这是典型的 DNS 投毒,不是 IPv6 配置问题。


二、根因

三层问题叠加,缺一不可。前两层解决后症状会变化,很容易误判为"没修好"。

第 1 层:dockerd 不继承 shell 代理

dockerd 由 systemd 以 root 启动,systemd 给服务的是一份干净环境,不读 ~/.zshrc,也不继承终端里 export 的变量。所以 shell 里 curl 能走代理,docker pull 却不行。

这不是权限问题。 用 root 跑 docker pull 一样失败,因为 root 的 shell 环境同样不会传给 daemon。Mac 上能用是因为 Docker Desktop 是 GUI 程序,会主动读系统代理并注入到 VM 里的 daemon。

修复/etc/systemd/system/docker.service.d/http-proxy.conf

[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,::1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12"
sudo systemctl daemon-reload
sudo systemctl restart docker

两个坑:

  1. ALL_PROXY 无效 —— Go 的 http.ProxyFromEnvironment 只读 HTTP_PROXY / HTTPS_PROXY / NO_PROXYALL_PROXY 被直接忽略。
  2. 协议必须写 http://,不能写 socks:// —— socks:// 是 SOCKS4 语义,DNS 在本地解析,照样拿到投毒地址;http:// 走 HTTP CONNECT,把域名原样交给代理远端解析,才能绕过污染。非要用 SOCKS 得写 socks5h://(带 h = remote DNS)。

第 2 层:明文 UDP DNS 被投毒

DNS 服务器registry-1.docker.iowww.google.com
223.5.5.5(阿里,明文 UDP)31.13.83.2 ← Facebook 段31.13.92.37 ← Facebook 段
119.29.29.29(DNSPod,明文 UDP)199.96.58.177 ← Twitter 段185.45.5.35 ← 投毒
doh.pub(DoH / HTTPS)✅ 正确 AWS 地址✅ 正确
dns.alidns.com(DoH)❌ 无应答❌ 无应答

明文 53 端口的应答被中间人伪造。DoH 是干净的doh.pub 可用,dns.alidns.com 实测完全无应答。

投毒返回的假地址是随机的 —— 有时落在国内 IP 段,有时落在国外段。这一点是第 3 层的直接诱因。

第 3 层:GEOIP,CN 放大危害(真正的坑)

订阅的兜底规则是:

- GEOIP,CN,DIRECT # 命中即直连
- MATCH,悠兔 # 兜底走代理

因为投毒地址随机,路由结果也跟着随机:

投毒返回匹配规则结果
国内 IP(如 118.193.202.219GeoIP/cn✗ 直连垃圾地址 → 超时
国外 IP(如 199.96.61.1Match✅ 走代理,域名交给节点远端解析 → 成功

表现为「同一个 docker pull 三次里成功一次」,极难复现和定位。真实日志:

# 失败
[TCP] dial DIRECT (match GeoIP/cn) --> registry-1.docker.io:443
error: connect failed: dial tcp 118.193.202.219:443: i/o timeout

# 成功
[TCP] --> registry-1.docker.io:443 match Match using 悠兔[专线2.5x-香港1]

三、排查过程中被证伪的假设

记录下来,避免下次重走:

假设证伪方式
「IPv6 解析问题」是 DNS 投毒;IPv4 同样是假地址(118.193.202.219
「用户权限问题导致走不了系统代理」是 systemd 环境继承问题;root 同样失败,权限只影响「改配置要 sudo」
「切全局模式就能绕过」GLOBAL 组默认选中 DIRECT,等于没开代理 ② DNS 仍被投毒,daemon 依然解析出假地址
default-nameserver: tls://223.5.5.5 的 853 端口不通」实测 853 端口可达,且 mihomo 能正确解析自己的 DoH 服务器域名
/etc/hosts 能修好」无效。dockerd 走 HTTP CONNECT 时把域名交给代理,本地 hosts 根本不参与解析

四、最终解决方案

在 mihomo-party 的覆写(Override)里加域名规则,让 docker 流量按域名匹配,不再依赖解析结果:

配置位置:~/.config/mihomo-party/override/<id>.yaml

+rules:
- DOMAIN-SUFFIX,docker.io,悠兔
- DOMAIN-SUFFIX,docker.com,悠兔

三种写法只有 +rules: 是对的:

写法行为是否可用
rules:覆盖整份规则表(893 条全没)❌ 代理直接废掉
+rules:前置插入,优先级高于 GEOIP,CN
rules+:追加到末尾,排在 GEOIP,CN 之后❌ 等于没加

覆盖到的域名:

  • registry-1.docker.io —— manifest
  • auth.docker.io —— 拉取 token
  • production.cloudfront.docker.com —— layer blob(注意是 cloudfront 不是 cloudflare)

验证

运行时配置中规则位置正确(必须在 GEOIP,CN 之前):

697: - DOMAIN-SUFFIX,docker.io,悠兔
698: - DOMAIN-SUFFIX,docker.com,悠兔
894: - GEOIP,CN,DIRECT

日志应显示 DomainSuffix 匹配而非 GeoIP

[TCP] --> registry-1.docker.io:443 match DomainSuffix(docker.io) using 悠兔[专线2.5x-香港3]
[TCP] --> auth.docker.io:443 match DomainSuffix(docker.io) using 悠兔[专线2.5x-香港3]

五、遗留问题

DNS 污染本身没有解决。 域名规则只让 docker 免疫,其他被墙域名若没有显式规则,仍会受同样的随机性影响。

根治方案是让 mihomo 的解析走可信路径:

dns:
nameserver:
- "https://doh.pub/dns-query"
- "https://1.1.1.1/dns-query#悠兔" # 经代理解析,绕开污染
nameserver-policy:
"+.docker.io": ["https://1.1.1.1/dns-query#悠兔"]
"+.docker.com": ["https://1.1.1.1/dns-query#悠兔"]

六、可复用的排查手法

  1. 先看报错里的 IP 属于谁。 face:b00c 是 Facebook 的特征串,31.13.x / 69.63.x / 199.96.x / 104.244.42.x 也都是投毒常用的段。地址明显不对 → 直接怀疑 DNS 污染,别在应用层绕圈。

  2. 用 DoH 对照验证解析。 一条命令区分「真解析失败」和「被投毒」:

    curl -s -H 'accept: application/dns-json' \
    "https://doh.pub/dns-query?name=<域名>&type=A"
  3. 看代理内核的分流日志,别看应用报错。 应用层只能看到 EOF / timeout / TLS error,真正的路由决策在 mihomo 日志里(~/.config/mihomo-party/logs/core-*.log)。

  4. mihomo-party 的控制器在 Unix socket 上,不在 TCP 端口。 config.yamlexternal-controller 是空的,实际由内核参数指定:

    /opt/clash-party/resources/sidecar/mihomo -d .../work -ext-ctl-unix /tmp/mihomo-party-<uid>-<pid>.sock
    SOCK=$(ls /tmp/mihomo-party-*.sock | head -1)
    curl -s --unix-socket "$SOCK" http://localhost/configs # 看模式
    curl -s --unix-socket "$SOCK" http://localhost/proxies/GLOBAL # 看代理组选中项

    ::: warning

    zsh 不对未加引号的变量做分词,别把 --unix-socket 和 URL 塞进同一个变量,会报 option ... is unknown

    :::

  5. "时好时坏" 几乎总是随机性来源。 DNS 投毒返回随机假地址、负载均衡、多 DNS 竞速都会造成这种模式。定位到随机源,问题就解了。

  6. Docker 的端口发布绕过 ufw。 -p 会在 iptables 的 DOCKER 链插规则,优先级在 ufw 之前,ufw deny 无效。暴露服务的唯一可靠防线是应用自身的认证。


附:排查中用到的一次性操作

临时切换 mihomo 模式(测试用,非持久):

SOCK=$(ls /tmp/mihomo-party-*.sock | head -1)
curl -s -X PATCH --unix-socket "$SOCK" -H 'Content-Type: application/json' \
-d '{"mode":"global"}' http://localhost/configs

GLOBAL 组的选中节点(默认是 DIRECT,全局模式下等于没代理):

curl -s -X PUT --unix-socket "$SOCK" -H 'Content-Type: application/json' \
-d '{"name":"专线2.5x-香港1"}' http://localhost/proxies/GLOBAL