如何对网站启动QUIC协议

最近在研究QUIC协议,如何让笔者的博客网站也支持QUIC呢?

背景

梳理下本网站的架构

1
client -> nginx(https) -> hexo server(127.0.0.1:4000)
  1. 负载均衡器: 使用nginx作为反向代理
  2. 博客服务: 使用hexo,并监听在127.0.0.1
  3. SSL证书: 使用acme.sh安装证书到对应的路径

实践

1. Nginx支持Quic

Nginx官方已在1.25.0以后支持Quic,需要手动编译和部署。
本物理机仍然是ubuntu16.04,综合考虑下,直接使用Docker的方式来运行,那么就需要考虑acme.sh如何将证书更新后reload docker中的nginx服务

acme.sh支持nginx docker

参考acme.sh的wiki部署相关acme.sh的docker和nginx docker

  • 启动nginx docker

    1
    docker run --rm -it -d  --label=sh.acme.autoload.domain=example.com   nginx:latest
  • 启动acme.sh docker

    1
    2
    3
    4
    5
    6
    docker run --rm  -itd  \
    -v "$(pwd)/out":/acme.sh \
    --net=host \
    --name=acme.sh \
    -v /var/run/docker.sock:/var/run/docker.sock \
    neilpang/acme.sh daemon
  • 申请证书

    1
    2
    3
    4
    docker  exec \
    -e CF_Email=xxx@exmaple.com \
    -e CF_Key=xxxxxxxxxx \
    acme.sh --issue -d example.com --dns dns_cf
  • 发布证书并reload nginx

    1
    2
    3
    4
    5
    6
    7
    8
    docker  exec \
    -e DEPLOY_DOCKER_CONTAINER_LABEL=sh.acme.autoload.domain=example.com \
    -e DEPLOY_DOCKER_CONTAINER_KEY_FILE=/etc/nginx/certs/example-com.key.pem \
    -e DEPLOY_DOCKER_CONTAINER_CERT_FILE="/etc/nginx/certs/example-com.one.cert.pem" \
    -e DEPLOY_DOCKER_CONTAINER_CA_FILE="/etc/nginx/certs/example-com.ca.pem" \
    -e DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/etc/nginx/certs/example-com.cert.pem" \
    -e DEPLOY_DOCKER_CONTAINER_RELOAD_CMD="service nginx force-reload" \
    acme.sh --deploy -d example.com --deploy-hook docker

    大家这里可以猜测下acme.sh的docker是如何将nginx docker中的服务reload的?

  • docker.sock是docker间通讯的关键

  • 通过DEPLOY_DOCKER_CONTAINER_LABEL找到对应的nginx docker

  • 发送DEPLOY_DOCKER_CONTAINER_RELOAD_CMD命令将nginx reload

  • 整体的配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    version: "2.0"
    services:
    nginx:
    image: nginx:1.25.2
    container_name: docker_nginx
    restart: unless-stopped
    labels:
    - "docker_nginx"
    volumes:
    - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    - ./nginx/conf.d:/etc/nginx/conf.d
    ports:
    - "80:80"
    - "443:443"
    - "443:443/udp"
    extra_hosts:
    - "host.docker.internal:xxx.xxx.xxx.xxx"

    acme-blogs:
    image: neilpang/acme.sh
    container_name: acme-blogs
    command: daemon
    volumes:
    - /data/acmeout:/acme.sh
    - /var/run/docker.sock:/var/run/docker.sock
    environment:
    - DEPLOY_DOCKER_CONTAINER_LABEL=docker_nginx
    - DEPLOY_DOCKER_CONTAINER_KEY_FILE="/etc/nginx/certs/example-com.key.pem"
    - DEPLOY_DOCKER_CONTAINER_CERT_FILE="/etc/nginx/certs/example-com.one.cert.pem"
    - DEPLOY_DOCKER_CONTAINER_CA_FILE="/etc/nginx/certs/example-com.ca.pem"
    - DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/etc/nginx/certs/example-com.cert.pem"
    - DEPLOY_DOCKER_CONTAINER_RELOAD_CMD="service nginx force-reload"

2. Nginx支持QUIC

参考nginx的文档配置server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
server {
server_name example.com;
listen 443 ssl;
listen 443 quic reuseport;
listen 80;

ssl_certificate /etc/nginx/certs/example-com.cert.pem;
ssl_certificate_key /etc/nginx/certs/example-com.key.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

root /usr/local/share/webroot/example.com;
# resolver 127.0.0.1;

location ~ ^/.well-known/(.*)$ {

}

location ~ ^/ind/.*$ {
}

location ~ ^/ok.html$ {
add_header Alt-Svc 'h3=":443"; ma=86400';

return 200 "ok";
}


location ~ ^/(.*)$ {
add_header Alt-Svc 'h3=":443"; ma=86400';

proxy_pass http://hexo-backend/$1;
}
}

3. Nginx访问宿主机的hexo服务

方法一

一般docker是使用bridge的方式来启动的,这时会在宿主机中启动一个docker0的网卡,将nginx中的upstream的server ip改为这里的172.17.0.1即可实现docker访问宿主机网络

1
2
3
4
5
6
7
8
$ ifconfig
docker0 Link encap:Ethernet HWaddr 02:42:fd:53:99:02
inet addr:172.17.0.1 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

nginx upstream配置如下:

1
2
3
upstream hexo-backend {
server 172.17.0.1:4000;
}

方法二

docker v20.10+提供了一种支持方案,可通过指向 host.docker.internal 来指向宿主机的 IP。参见文档:从容器连接到主机上的服务
配置 docker-compose.yaml

1
2
3
4
5
6
7
8
9
10
version: '2.0'

services:

nginx:
image: nginx:1.25.2
container_name: docker_nginx
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"

在nginx docker中执行相关语句后,应该是可以联通到宿主机。由于笔者的docker环境是v20.4,无法实验,暂无法确定本方法是否可行

1
$ curl http://host.docker.internal:4000

方法三

host方式启动nginx docker,这样nginx仍然在宿主机网络下

总结

笔者最后使用方法一来打通docker和宿主机的网络。至此,整体架构改为以下方式。

1
client -> nginx docker(443:443, 443:443/udp) -> hexo(172.17.0.1:4000)