QUIC及びHTTP/3対応のNGINXサーバーを構築

こんにちは。

Chromium系ブラウザではStable版でも`QUIC`及び`HTTP/3`通信が可能になりました。
新しい物好きとしては是非とも導入したいということで**NGINX**の**QUICプレビュー版**を用いて`QUIC`及び`HTTP/3`対応サーバーを構築します。
幸いにも[公式のREADME](https://quic.nginx.org/README)が丁寧なので、それをなぞれば簡単に導入できます。

なお、先達が[同様の記事](https://qiita.com/keys/items/bf492ef543fde3d8d822)を書かれていらっしゃいますが、今回は下記の通りNGINXが公式に発表したブランチを用いて構築します。

>このQUIC + HTTP/3の実装は完全に新しいものであり、quicheプロジェクトの一部としてCloudflareが提供するパッチとは関係ありません。

ソースからビルドしますが、目指すのは`apt install`した`Nginx`と同じ使用感です。

自分がハイハイをしていた頃、（上級者の方々には当たり前のことも）細かく書いている記事が有難いと感じたことが多々あったので、細かくコメントを書いていきたいと思います。

# 追記
記事を書いてからしばらくたち、HTTP/3の仕様もだいぶ定まってきました。
それに伴って`nginx-quic`にも変更があったので記事内容を更新しました。

1. プロトコルの変更
 1. `h3-29` => `h3`(2021/08/03)
 1. `We support IETF QUIC version 1.  Internet drafts are no longer supported.`(2022/02/12)
1. `configure.sh`内容変更
 1. `--with-http_quic_module`の削除(2022/02/12)
1. 設定ファイルの変更
 1. `$http3`変数の削除(2021/08/03)

# 環境

`AWS Lightsail`上の`Debian`で行っていきます。
最近初めて使いましたが、某*cherry blossoms*よりお手軽で便利だと感じました。

`Amazon Linux 2`でも行いましたので追記しました。

```
Linux ip-***-***-***-*** 4.19.0-14-cloud-amd64 #1 SMP Debian 4.19.171-2 (2021-01-30) x86_64 GNU/Linux
```

シェバングは適宜書き換えてください。

# 下準備

利用するプロトコルが違うので、きちんと設定すれば`QUIC版`と`Stable版`のデュアルスタック（？）的なことも可能ですが、その場合は`configure`をごにょごにょする必要があります。
なんてことはない作業ですので解説はしませんが、解説がないとできない人は素直に`sudo apt purge nginx nginx-full && sudo apt autoremove --purge`で`Stable版`をアンインストールしてからインストールするのがよいでしょう。
特に今回の設定では`apt`経由と同じディレクトリにインストールすることを目指すので削除してから臨むのがよいと思います。
`apt purge`すると従前の設定ファイルが消えるので保存しておきたい方はバックアップ（`sudo mv /etc/nginx /etc/nginx.bak`）をおすすめします。

必要なパッケージをインストールします。
`Ninja`を使用すると`Boringssl`のビルドを高速化できるらしいです。

```bash:必要なものをインストールするぞい！
# Debian系
sudo apt update && sudo apt upgrade
sudo apt install git mercurial cmake make golang libunwind-dev libpcre3-dev zlib1g-dev gcc g++

# Debian系(Ninjaを使用)
#sudo apt install git mercurial cmake ninja-build golang libunwind-dev libpcre3-dev zlib1g-dev gcc g++

# RedHat系(Ninjaを使用)
#sudo yum update
#sudo yum install git hg cmake3 ninja-build golang libunwind-devel pcre-devel zlib-devel gcc gcc-c++
```

# Boringssl

まずは`Google`さんが作った`OpenSSL`のフォークをインストールします。
適当なディレクトリ（今回は`/opt/nginx`とします）を用意して、`BoringSSL`をダウンロードします。
ダウンロードしたディレクトリにある[BUILDING.MD](https://boringssl.googlesource.com/boringssl/+/HEAD/BUILDING.md)を参考にビルドします。
`Ninja`を用いたインストールが速いらしく推奨されていますが、今回は`Make`でいきます。

```bash:Boringsslをインストールしていくぞい！
# ディレクトリを作る
sudo mkdir /opt/nginx
# 移動
cd /opt/nginx
# git clone
sudo git clone https://boringssl.googlesource.com/boringssl
# インストール
cd boringssl
sudo mkdir build
cd build

# [1]or[2]へ

# [1] makeでインストール
sudo cmake ..  # 何か不足していると言われればインストールする
sudo make  # ちょっと時間がかかります
# テスト
cd ..
go run util/all_tests.go # All tests passed!となればOK(時間がかかります)
cd ssl/test/runner
go test  # "PASS ok"と出ればOK(時間がかかります&メモリを食う)

# [2] Ninjaでインストール
#sudo cmake -GNinja ..  # Redhatでは"sudo cmake3 -GNinja .."
#sudo ninja  # Redhatでは"sudo ninja-build"
# テスト - Ninjaを使用した場合は二つのテストが一つのコマンドで完結します
#cd ..
#sudo ninja -C build run_tests  # Redhatでは"sudo ninja -C build run_tests"
```

~~本当はもう一種類テストがあるのですがどうやってもエラーなのと、放置しても進めるので今回はパスします。~~
下記備考の通り、インスタンスのメモリ不足が原因でした。

### 備考
インスタンスのスペックによってはコンパイル時に以下のようなエラーになることがあります。
`Debian`では問題なかったのですが、`Amazon linux 2`でハマりました。

```
g++: internal compiler error: Killed (program cc1plus)
Please submit a full bug report, with preprocessed source if appropriate.
See <http://bugzilla.redhat.com/bugzilla> for instructions.
```

このエラーはメモリ不足が原因です、依存関係ではありません…。
や、まぁ確かに`Killed`とは書かれていますが…。
初めてだったので依存関係と思い込んで解決まで苦労しました。（本当に苦労しました）

メモリ不足で失敗しているので、不要なプロセスを終了してから再挑戦するか、インスタンスのスペックを上げることを検討しましょう。
私は1GBに上げて解決しました。
また、コンパイル後のテストも二つとも成功しました。

# Nginx
では、いよいよ`エェンジンエッックス`をインストールしていきます。（名前かっこいいですよね）

```bash:まずはダウンロードするぞい！
cd /opt/nginx
# git clone的な
sudo hg clone -b quic https://hg.nginx.org/nginx-quic
```

次にオプションを設定していきますが、今回は**`apt`を用いてインストールした時と同じようになるよう**にインストールしたいと思います。
要するに`/etc/nginx`配下に設定ファイル諸々を、そして`/usr/sbin/`配下に実行ファイル、`/var/log/`配下にログファイル、etc...ということです。
ダイナミックモジュールは使いません。
`sudo nano /opt/nginx/configure.sh`で下記のファイルを作成します。

```bash:/opt/nginx/configure.sh
#!/usr/bin/bash

cd nginx-quic
./auto/configure \
    --with-debug \
    --with-http_v2_module \
    --with-http_v3_module \
    --with-stream \
    --with-stream_quic_module \
    --with-http_ssl_module \
    --with-cc-opt="-I../boringssl/include"   \
    --with-ld-opt="-L../boringssl/build/ssl  \
                   -L../boringssl/build/crypto" \
    --prefix=/etc/nginx \
    --sbin-path=/usr/sbin/nginx \
    --conf-path=/etc/nginx/nginx.conf \
    --error-log-path=/var/log/nginx/error.log \
    --http-log-path=/var/log/nginx/access.log \
    --pid-path=/run/nginx.pid \
    --lock-path=/run/nginx.lock \
    --http-client-body-temp-path=/var/cache/nginx/client_temp \
    --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
    --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
    --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
    --http-scgi-temp-path=/var/cache/nginx/scgi_temp \
    --user=www-data \
    --group=www-data
```

今回の設定で基本的なサーバー動作は可能ですが、モジュールは必要に応じて足し引きしてください。

**追記(8/3)**
PHPが503となる場合は`--with-stream`が指定されていない可能性があります。
当初、不要と思い省いていましたが、UNIXソケットへPHPを渡す際に必要になるようです。

ファイルを作成出来たらインストールを続行します。

```bash:インストールしていくぞい！
# 実行権付与
sudo chmod 744 configure.sh
# configure.sh実行
sudo -s ./configure.sh  # エラーなく進んでConfiguration summaryが出ればOK
cd nginx-quic
# インストール
sudo make  # ./objs配下にファイルができます
sudo make install  # ファイルを各所にコピーします（既に別バージョンがインストールされている場合は注意）
# 確認
sudo -s /usr/sbin/nginx -V  # インストールした内容できちんと表示されればOK
```

以上でインストール自体は完了です。

# 簡易設定
とりあえず動作確認だけできるように設定します。

`nginx.conf.default`で同じものが用意されていますので、インストール時に用意される設定ファイルはいったん削除して新しく作り直します。

1. `sudo rm -f /etc/nginx.conf`
2. `sudo nano /etc/nginx.conf`

```/etc/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
	worker_connections 1024;
}

http {
	server_tokens off;
	charset utf-8;

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

	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;

	server {
		listen 80;
		listen [::]:80;
		index index.html;
		root /var/www/html;
	}
}
```

`IPv6`推進の過激派なので最小設定といえど`IPv6`待ち受けは必須。
最後に諸々のディレクトリなどを用意します。

```bash:ディレクトリをつくるぞい！
sudo mkdir /var/cache/nginx
sudo -u www-data mkdir /var/www  # エラーが出たら下記「エラーがでたら」を参考にして解決して下さい
sudo mv /etc/nginx/html /var/www/
```

# systemd管理
言わずもがな、手動で起動するのは非現実的ですので設定していきます。
まずはユニットファイルを作成します。
これは公式ページにあるものを丸ぱくりです。

`sudo nano /lib/systemd/system/nginx.service`

```/lib/systemd/system/nginx.service
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
#PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target
```

ラズパイやさくらでは問題なかったのですが、`PIDFile`でなぜか怒られました。
`fork`した`PID`は`systemd`が自動で追跡できるので明示しなくても問題ないと判断しコメントアウトします。
それでは、ユニットファイルを読み込んで起動させていきます。

```bash:Nginxをsystemdおじさんに管理してもらうぞい！
sudo systemctl daemon-reload
sudo systemctl start nginx
```

エラーが出たら必要なファイルを用意するなど適宜対応してください。
問題なく起動できればブラウザででサーバーのIPアドレスを打ち込んでアクセスしてみましょう。
**`Welcome to nginx!`**というページが表示されれば成功です。

問題がなければ自動起動するようにします。

```bash:自動起動させるぞい！
sudo systemctl enable nginx
```

### エラーがでたら

下記のようなエラーに遭遇することがあります。

```
nginx[xxxx]: nginx: [emerg] getpwnam("www-data") failed in ...
```

これは、`www-data`というユーザーが存在しないと怒られているので以下のようにして追加します。
`www-data`というユーザーを追加して、ホームディレクトリを`/var/www`にして、ログインを禁止しています。
`passwd`でパスワードを設定しては**いけません**。

```
sudo useradd -d /var/www -s /usr/sbin/nologin www-data
```

# SSL証明書取得

これはちょっと違う話になるので[別記事（certbotの導入手順が新しくなっていた件）](https://qiita.com/genshibangou16/items/2eed0c0aa11093561b0a)で扱います。
既に発行済みの方はそちらを利用してください。

# 本番設定

皆様お好みの設定があると思うので、HTTP/3及びQUICを含めて取り敢えず動く為に必要な最低限の設定だけ書いておきます。

```/etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    ####
    # Basic settings
    ####

    server_tokens off;  # Hide server information
    charset utf-8;

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

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    ####
    # HTTP/3 and QUIC minimal settings
    ####

    ssl_protocols TLSv1.3;  # TLSv1.3 is required, but you can add TLSv1.2 for compatibility
    ssl_prefer_server_ciphers off;
    #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;  # Uncomment to disable unsafe ciphers if you use TLSv1.2

    # quic_retry on;  # Uncomment to enable address validation
    # ssl_early_data on;  # Uncomment to enable 0-RTT

    server {
        listen 443 ssl http2;  # Listen to legacy connections (HTTP/2)
        listen [::]:443 ssl http2;  # Same as above, for IPv6
        listen 443 http3 reuseport;  # Listen to cutting-edge connections!! (HTTP/3,QUIC)
        listen [::]:443 http3 reuseport;  # Same as above, for IPv6

        server_name example.com;

        index index.html index.php;
        root /var/www/example.com;

        ssl_certificate path2certs/example.com.crt;
        ssl_certificate_key path2certs/example.com.key;

        location / {
            try_files $uri $uri/ =404;
            #add_header Alt-Svc '$http3=":443"; ma=86400';  # Older version
            add_header Alt-Svc 'h3=":443"; h3-29=":443"; ma=86400';  # Tell browsers that HTTP/3 is available and its listening port
            add_header QUIC-Status $quic;  # Indicate using quic
        }
    }

    ####
    # Redirect HTTP to HTTPS
    ####

    server {
        listen 80;
        listen [::]:80;
        return 301 https://$host$request_uri;
    }
}
```

`Nginx`を触ったことがある人なら分かると思いますが、一応所々にコメントを付しておきましたので参考にしてください。
~~変数`$http3`は使用しているプロトコルが返りますので、現在であれば`h3-29`*(IETF QUIC)*となります。~~（追記参照）
もう一つ`$quic`という変数も用意されており、こちらは`QUIC`が利用されている際に文字列`quic`が返ります。

**追記(8/3)**
変数`$http3`は削除された模様です。
私見ですが、`HTTP/3`がIETFにより正式に策定されたため記述方法を変数化する必要がなくなったのかと思われます。
Googleの書き方を参考に最低限の記述量で済むように書き換えました。
最終的には`h3=":443"`だけで済むようになるのではないでしょうか。
ただ、現在では`h3-29`を含めないと読み込まれない事象も多々見受けられます。

要点は以下の二つです。

- `TLSv1.3`を有効にする
- `listen ... http3 reuseport`で待ち受ける
- `add_header Alt-Svc '$http3=":443"'`で伝える

~~サーバーはネコでブラウザはタチだけどお互い知らないので交渉前にサーバー側からネコって自己申告するという話です。~~
失礼。

# 確認

実際にどのプロトコルで通信しているかは、デベロッパーツールのネットワークタブで表のタイトル行を右クリックして`Protocol`にチェックを入れれば確認できます。
`http/1.1`はそのまま`HTTP/1.1`で、`h2`は`HTTP/2`、~~`h3-29`は`HTTP/3`~~です。

**追記(8/3)**
`HTTP/3`は`h3`となっています。

ファーストコンタクトには適用されず（ここで`Alt-svc`を受け取る）、二つ目以降のコネクションから~~`h3-29`~~`h3`が適用されます。

`Cache-Control: no-cache`のヘッダーが適用された場合などでキャッシュを読み込むと`h3-29`とはならないので、デベロッパーツールの`Disable cache`にチェックを入れてリロードしてみてください。
`キャッシュの削除とハード再読み込み`をすると`Alt-svc`の情報も消えてしまうので注意してください。

`add_header QUIC-Status $quic;`を設定した場合はレスポンスヘッダーから`QUIC`が使用されているかを確認できます。
`QUIC-Status: quic`が有れば有効、なければ無効です。

# アップデート

試験版なので数日ごとに更新がありますから、手軽にアップデートをできるようにします。

`sudo nano /opt/nginx/update.sh`で以下の内容のファイルを作成します。
`Debian系`で`Ninja`を使用しない前提で書いてあるので環境に合わせて書き直してください。

```bash:/opt/nginx/update.sh
#!/usr/bin/bash

git -C /opt/nginx/boringssl pull | grep -q 'Already up to date'
boringStatus=("${PIPESTATUS[@]}")

if [ ${boringStatus[0]} -eq 0 ]; then
    if [ ${boringStatus[1]} -gt 0 ]; then
        cd /opt/nginx/boringssl/build
        cmake ..
        cmakeStatus=$?
        make
        if [ $? -eq 0 -a $cmakeStatus -eq 0 ]; then
            echo 'BoringSSL update(s) installation has succeeded.'
            boringUpdated=0
        else
            echo 'BoringSSL update(s) installation has failed.(Installation failure)'
            boringUpdated=1
        fi
    else
        echo 'BoringSSL is already up to date.'
        boringUpdated=1
    fi
else
    echo 'BoringSSL update(s) installation has failed.(Sync failure)'
    boringUpdated=1
fi

hg --cwd /opt/nginx/nginx-quic pull -u | grep -q 'no changes found'
nginxStatus=("${PIPESTATUS[@]}")

if [ ${nginxStatus[0]} -eq 0 ]; then
    if [ ${nginxStatus[1]} -gt 0 -o $boringUpdated -eq 0 ]; then
        /opt/nginx/configure.sh
        cd /opt/nginx/nginx-quic
        make
        if [ $? -eq 0 ]; then
            systemctl stop nginx
            make install
            if [ $? -eq 0 ]; then
                systemctl start nginx
                echo 'NGINX update(s) installation has succeeded.'
                exit 0
            else
                echo 'NGINX update(s) installation has failed.(Installation failure)'
                exit 1
            fi
        else
            echo 'NGINX update(s) installation has failed.(Building failure)'
            exit 1
        fi
    else
        echo 'NGINX is already up to date.'
        exit 0
    fi
else
    echo 'NGINX update(s) installation has failed.(Sync failure)'
    exit 1
fi

exit 1
```

`sudo chmod a+x /opt/nginx/update.sh`で実行権を付与して、あとは手動で実行するなり、cronで定期実行するなりすればOKです。

`grep`で拾うこの方法はスマートじゃなくて嫌なのですが、もう少しスマートな方法があれば教えてください。

# 最後に

結構簡単に`HTTP/3`と`QUIC`に対応したサーバーが構築できます。
あまり体感するほどではないですが、気持ちはよいです。

`QUICプレビュー版`のバージョンは1.19なので`apt`からインストールできるようになるのは少し先の話かもしれません。
基本的には安定しているので、非商業用であれば一足先に導入してみるのもよいですね。

当方では`PHP`で時折`400 Bad request`が発生する問題がありますが、解決方法は未だ見つけられていません。
ご存じの方がいましたら教えてください。

**追記(8/3)**
アップデートで概ね解決しています。
未解決の方は、configure時に`--with-stream`と`--with-stream_quic_module`オプションが指定されているかを確認してください。

また、`OCSP stapling`には非対応です。

最後までお読みいただきありがとうございました。