Docker Hub

Docker Hub 在国内用不了已经有一段时间了,由于我们在实验室自建了透明代理,所以大部分时候对我们而言其实没啥影响。对于不在内网环境下或偶尔帮助他人调试的时候,也还有国内镜像站凑合,但好景不长,今年 6 月开始国内 Docker Hub 镜像站相继停止服务,这下不得不想点办法了。

自建 Proxy

考虑到我在海外还有一台闲置的小鸡,每个月可用流量也不少,正好可以利用起来。 最简单的方法就是参照这篇文章利用 Nginx 做反向代理,从而实现加速的效果。事实上最开始我们也是这么操作的,令人惊喜的是,速度还不错,接近 9MB/s。

Nginx 直接反向代理 Docker Hub

可惜好景又不长,很快实验室的同学和我反馈镜像拉取异常,复现后发现面向auth.docker.io的请求受到了阻断,而在此之前,这个阻断是不存在的。对于 Docker Hub 来说,在拉取镜像前需要先进行鉴权(即使是公开镜像),既然无法鉴权,自然也就无法顺利下载镜像。

拉取镜像失败

这个auth.docker.io的域名是哪来的,可不可以替换?结合其他博主的抓包结果,还真有办法。但首先,我想先复现一遍抓包结果。

Linux 抓包的准备

怎么抓?Docker 跑在 Linux 上,那么最简单的办法就是通过给 Docker 配置代理,指向 Windows 下的抓包软件 Fiddler 监听的端口即可。

安装 Fiddler

Fiddler 有两个版本,经典 Classic 和新版 Everywhere,这里我们用 Everywhere。 这是一个付费软件,那怎么能顺利用上就需要自己想办法了,这里留一个传送门

Fiddler 使用

安装完成后,打开设置面板,这里有两步操作:

  • 安装并导出 CA 证书备用,用于解密 HTTPS 流量 导出证书

  • 允许接收局域网内的流量 允许接收局域网内的流量

需要注意的是,导出证书需要选择 PEM 格式(即纯文本),这是在 Linux 下的通用格式,这点我曾经在小例会上提过。

同样的 X.509 证书可能存在不同的编码格式,但他们可以相互转换:

  • PEM - Privacy Enhanced Mail
    • 文本类型 Base64 编码 BEGIN 开头 END 结尾
    • UNIX 常见
  • DER - Distinguished Encoding Rules
    • 二进制类型
    • Windows 常见

编码格式是局限的,但扩展名是丰富多彩的:

  • PEM 后缀:通常用于公钥
  • CRT 后缀:通常用于公钥
  • KEY 后缀:通常用于私钥
  • PIZZA 后缀:你高兴就好

在 UNIX 上通常使用 PEM 编码,即文本格式。但是具体证书的后缀用什么,其实开心就好,无所谓的。

在 Linux 安装 CA 证书

在安装 Fiddler 的 Windows 电脑上安装证书只能解密来自本机的流量,还需要在安装 Docker 的 Linux 上也安装证书。

我这里是 Ubuntu,具体的安装过程如下:

  • 将证书复制到/usr/local/share/ca-certificates目录下
  • 执行update-ca-certificates

搞定!记得重启一下 Docker,否则 Pull 的时候依旧会报证书错误。

如果提示证书加载失败,记得看看自己导入的证书文件能否使用文本编辑器打开,别不小心导成了 DER 格式的证书文件了,这是 Fiddler 的默认导出。

给 Docker 配置代理

Docker 的 Proxy 有两种,一种是给 Daemon(守护进程)配置,一种是给 CLI 配置。

  • 给守护进程配置的代理会用于 Dockerd 访问 Docker Hub,即拉取相关镜像:官方文档
  • 给 CLI 配置的代理会用于容器本身,即给容器套上代理:官方文档

显然,我们这里需要给守护进程配置代理。

按照官方文档的提示,我们创建/etc/docker/daemon.json

如果 Docker 使用的是 rootless 模式,这个路径的位置会有所不同,具体参见:Daemon | Docker Docs,但正常情况下,你应该和我一样。

需要注意的是,如果进行过配置镜像源等操作,这个文件你应该创建过,把代理相关配置加进去的时候,注意符合 JSON 格式。

除了使用daemon.json,还有其他载入代理信息的方法,但daemon.json是最被推荐的,如果你需要使用其他方法,记得去看文档。

接着我们填入代理相关信息:

{
  "proxies": {
    "http-proxy": "http://proxy.example.com:3128",
    "https-proxy": "https://proxy.example.com:3129",
    "no-proxy": "*.test.example.com,.example.org,127.0.0.0/8"
  }
}

需要注意的是,这里的http-proxyhttps-proxy都需要使用http://192.168.181.1:8866(根据自己的进行修改)即 HTTP 协议。

如何验证配置被正确加载了?输入docker info,如果找到自己配置的 Proxy 相关内容,就说明配置成功了。

顺便一提,如果你需要使用 SSH 隧道之类的操作来给 Docker 提供一个通畅的网络环境,也需要通过这种方式来进行。

开始抓包

随意 Pull 一个镜像,如docker pull mysql,如果一切顺利,你就能在 Fiddler 的界面看到相关请求了。

Fiddler

果然遇事不决先抓包,一抓,万事万物都清晰了。

首先,Docker 会访问https://registry-1.docker.io/v2/并得到一个 401 状态码,在www-authenticate头中包含了无法访问的 URL:https://auth.docker.io/token,在正常情况下,Docker 会接着访问https://auth.docker.io/token并得到相关 Token,最终拿着 Token 去访问镜像仓库https://registry-1.docker.io/v2/。只是这还没结束,请求最终会被 307 重定向到https://production.cloudflare.docker.com,这才是最终产生流量的地方。

Docker Pull

那让我们再看看如果使用最开始反向代理的方式会发生什么呢?

Docker Pull By Proxy

可以看到,一切几乎照旧,唯一不同的是镜像的获取不是通过 307 将客户端重定向到 CloudFlare 的服务,而是直接通过代理下载,这也是之前通过之前博主对 Nginx 的配置实现的。

那么现在需要解决的问题就很简单了,在原来反向代理的基础上加上对auth部分的代理就好了,同时替换第一次 401 中无法访问的 URL 为代理后的地址即可。

对策

参考这篇文章这篇文章,给出我的 Nginx 配置文件:

server {
    listen       443 ssl;
    server_name  docker.cqut.cc;

    #access_log  /var/log/nginx/host.access.log  main;
    ssl_certificate /ssl/cqut.cc.crt;
    ssl_certificate_key /ssl/cqut.cc.key;


    location / {
        proxy_pass https://registry-1.docker.io;
        proxy_set_header Host registry-1.docker.io;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forward-Proto $scheme;
        # 替换认证Header
        more_set_headers 'www-authenticate: Bearer realm="https://docker.cqut.cc/auth",service="registry.docker.io"';
        # 关闭缓存
        proxy_buffering off;
        proxy_set_header Authorization $http_authorization;
        proxy_pass_header  Authorization;
        proxy_intercept_errors on;
        recursive_error_pages on;
        error_page 301 302 307 = @handle_redirect;
    }

    location /auth {
        proxy_pass https://auth.docker.io/token;
        proxy_set_header Host auth.docker.io;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forward-Proto $scheme;
    }

    location @handle_redirect {
        resolver 1.1.1.1;
        set $saved_redirect_location '$upstream_http_location';
        proxy_pass $saved_redirect_location;
    }
}

需要注意的是,more_set_headers指令用到了headers-more-nginx-module模块,这部分默认是没有被编译进 Nginx 的,也没有被放到 Nginx 官方源中。需要自行编译或看看自己操作系统的发行版是否提供这个包,包名为:libnginx-mod-http-headers-more-filter或被包含在nginx-extra中,如果 Nginx 版本太新导致无法被依赖或者找不到这个包,就需要自行编译了。

自行编译可以使用 Nginx 最新的dynamic module,动态模块技术,相对方便,增减模块无需重新编译整个 Nginx。只需要下载 Nginx 对应版本源码和headers-more-nginx-module源码,将编译好的 so 文件放到 Nginx 的 moudles 文件夹中,并在 Nginx 配置中增加这一部分就可以了,具体可以参考官方仓库

搞定!再抓个包看看效果。

Success

舒服了。

使用

  • 官方镜像:docker pull docker.cqut.cc/library/mysql
  • 普通镜像:docker pull docker.cqut.cc/xhofe/alist

参考