HTTP/3 対応の nginx を作る

# 矛を作らば盾も作らん

そんな格言はないですが、矛となるクライアント([HTTP/3 対応 curl](https://qiita.com/keys/items/611d949ca26d6ca848c2))をせっかく作ったので、盾となるサーバも作ります。

……といっても、[やっぱり](https://qiita.com/keys/items/611d949ca26d6ca848c2#といっても)このページをなぞっているだけですが
https://github.com/cloudflare/quiche/tree/master/extras/nginx 

Cloudflare が作った QUIC の実装である [quiche](https://github.com/cloudflare/quiche) を使います。これを nginx で使えるようにしたパッチも Cloudflare が用意しています。

# ※ Docker 使えます

その前に。

Docker でお手軽に試せます。
https://hub.docker.com/r/keioni/nginx-http3


# 導入

quiche は Rust でできているので、Rust が入っていないなら [rustup](https://rustup.rs/) を使って入れます。

他にも、各々の環境にあわせて build に必要なものを入れます。私の場合はこんなパッケージを入れました。

```
yum install git tar gcc gcc-c++ make cmake3 patch pcre-devel zlib-devel
```

cmake3 以上が必要なのですが、cmake3 パッケージを入れるとバイナリが `cmake3` だったので cmake にシンボリックリンクを張ります。

```
ln -s /usr/bin/cmake3 /usr/bin/cmake
```

# build

## nginx + quiche patch

Cloudflare が用意しているパッチは nginx 1.16.x 系のみの対応です。

```
curl -O https://nginx.org/download/nginx-1.16.1.tar.gz
tar xzvf nginx-1.16.1.tar.gz
```

quiche を git から clone してきて、nginx 用のパッチを当てます。

```
git clone --recursive https://github.com/cloudflare/quiche
cd nginx-*
patch -p01 < ../quiche/extras/nginx/nginx-1.16.patch
```

## build

今回も HTTP/3 対応 curl を作ったときと同じように `/opt/http3` 以下に入れるようにしました。

```
./configure \
    --prefix=/opt/http3/nginx \
    --build="quiche-$(git --git-dir=../quiche/.git rev-parse --short HEAD)" \
    --with-http_ssl_module \
    --with-http_v2_module \
    --with-http_v3_module \
    --with-openssl=../quiche/deps/boringssl \
    --with-quiche=../quiche
```

```
make
make install
```

## 確認

うまくできているか確認します。

```
/opt/http3/nginx/sbin/nginx -V
```

```
nginx version: nginx/1.16.1 (quiche-918c13b)
built by gcc 7.3.1 20180712 (Red Hat 7.3.1-9) (GCC)
built with OpenSSL 1.1.0 (compatible; BoringSSL) (running with BoringSSL)
TLS SNI support enabled
configure arguments: --prefix=/opt/http3/nginx --build=quiche-918c13b --with-http_ssl_module --with-http_v2_module --with-http_v3_module --with-openssl=../quiche/deps/boringssl --with-quiche=../quiche
```

# 実行

## 設定ファイルの準備

証明書と秘密鍵を用意して、以下のように設定ファイルを作ります。

```nginx:conf/nginx.conf
events {
    worker_connections  1024;
}

http {
    server {
        # Enable QUIC and HTTP/3.
        listen 443 quic reuseport;
        listen [::]:443 quic reuseport;

        # Enable HTTP/2 (optional).
        listen 443 ssl http2;

        location / {
            root   html;
            index  index.html index.htm;
        }

        ssl_certificate      cert.crt;
        ssl_certificate_key  cert.key;

        # Enable all TLS versions (TLSv1.3 is required for QUIC).
        ssl_protocols TLSv1.2 TLSv1.3;

        # Request buffering in not currently supported for HTTP/3.
        proxy_request_buffering off;

        # Add Alt-Svc header to negotiate HTTP/3.
        add_header alt-svc 'h3-29=":443"; ma=86400';
    }
}
```

## 起動

```
/opt/http3/nginx/sbin/nginx
```

## 動作確認

### socket

```
netstat -an --udp | grep 443
```

443/udp が表示されているなら、うまくいっています。

```
udp        0      0 0.0.0.0:443             0.0.0.0:*
udp6       0      0 :::443                  :::*
```

### curl

[以前の記事](https://qiita.com/keys/items/611d949ca26d6ca848c2)で作った HTTP/3 対応の `curl` でアクセスしてみます。

```
./curl -v --http3 https://localhost/
*   Trying 127.0.0.1:443...
* Connect socket 5 over QUIC to 127.0.0.1:443
* QUIC handshake is completed
* Connected to localhost () port 443 (#0)
* Using HTTP/3 Stream ID: 0 (easy handle 0x137c9e0)
> GET / HTTP/3
> Host: localhost
> user-agent: curl/7.71.1
> accept: */*
>
* ngh3_stream_recv returns 0 bytes and EAGAIN
< HTTP/3 200
< server: nginx/1.16.1
< date: Tue, 21 Jul 2020 02:40:17 GMT
< content-type: text/html
< content-length: 612
< last-modified: Tue, 21 Jul 2020 02:09:03 GMT
< etag: "5f164e3f-264"
< alt-svc: h3-29=":443"; ma=86400
< accept-ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
* Connection #0 to host localhost left intact
```

ちゃんと HTTP/3 でアクセスできました。いえい。

# 正式なWebサーバとの同居

すでに別のWebサーバアプリでサービスを提供している場合も、HTTP/3 (QUIC) が使うプロトコル・ポートは 443/udp なので競合せず、一緒に動作させることができます。

## 設定

設定ファイルをこんな感じにします。

```nginx:conf/nginx_h3_only.conf

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    server {
        # Enable QUIC and HTTP/3.
        listen 443 quic reuseport;
        listen [::]:443 quic reuseport;

        location / {
            root   html;
            index  index.html index.htm;
        }

        ssl_certificate      cert.crt;
        ssl_certificate_key  cert.key;

        # Enable all TLS versions (TLSv1.3 is required for QUIC).
        ssl_protocols TLSv1.2 TLSv1.3;

        # Request buffering in not currently supported for HTTP/3.
        proxy_request_buffering off;

        # Add Alt-Svc header to negotiate HTTP/3.
        add_header alt-svc 'h3-29=":443"; ma=86400';

        # 他のHTTPSサーバと同居させるときに重要
        add_header last-modified '';
    }
}
```

元の設定ファイルとの違いは、以下の2つです:

* `listen 443 ssl http2;` を取り除く<br>
   これを残したままだと 443/tcp を待ち受けるようになるので、元のWebサーバと競合してしまいます。
* `add_header last-modified '';` を追加<br>
  すでにファイルがある場合、キャッシュの問題が発生しがちなので `Last-Modified` ヘッダを付けないようにします。このように設定しても `ETag` があるので、キャッシュはきちんと行われます。

## 動作確認

再起動したら netstat で確認します。

```
sudo netstat -an -p --udp --tcp | grep 443
```

以下のように `tcp` が、元々使っているWebサーバになっていて[^webserver]、`udp` が `nginx` になっていれば大丈夫です。

[^webserver]: ここでは `httpd` なので分かりやすいですが、`nginx` を使っている場合はPIDの違いで判別してください

```
tcp6       0      0 :::443                  :::*                    LISTEN      28911/httpd
udp        0      0 0.0.0.0:443             0.0.0.0:*                           9182/nginx: master
udp6       0      0 :::443                  :::*                                9182/nginx: master
```

# さいごに

~~HTTP/3 で使うプロトコルの QUIC は、2020年7月27日時点では正式版が出ていません。しかしそう遠くないうちに正式版が出るはずです。~~

~~正式版が出る前に、ちょっと試してみるのもいいのでは、と思います。~~

→ 2021年5月31日に [RFC9000](https://datatracker.ietf.org/doc/html/rfc9000) として標準化されました。(2021年8月13日追記)