nghttpx - HTTP/2 リバースプロキシー / L7 ロードバランサー

# nghttpx - HTTP/2 リバースプロキシー / L7 ロードバランサー

nghttpx は [nghttp2](https://nghttp2.org) プロジェクトで開発している HTTP/2 対応のリバースプロキシー / L7 ロードバランサーです.

この文書は v1.11.0 開発バージョンを元に執筆しました.

# リバースプロキシーとして使ってみよう

nghttpx のもっとも簡単な使い方はサーバー証明書を用意して:

```
$ nghttpx server.key server.crt
```

を実行することです.  server.key はサーバーの秘密鍵ファイル, server.crt は証明書のファイルです.  どちらも PEM 形式です.  この構成では nghttpx は全インターフェースのポート 3000 番でTLS 接続を受け付け, バックエンドサーバー 127.0.0.1:80 にフォワードします.  バックエンドの接続は TLS なしになります.  またはフォワードは HTTP リクエスト毎に行います.

nghttpx はすべての設定をコマンドラインオプションで指定できますが, 設定ファイルに書くこともできます.  シェルで特別に意味を持つ文字を入力することもあるので以下では設定ファイルを使うことにします.  以下のような設定ファイル nghttpx.conf を用意し:

```:nghttpx.conf
private-key-file=server.key
certificate-file=server.crt
```

nghttpx を以下のように実行します:

```
$ nghttpx --conf nghttpx.conf
```

デフォルトでは /etc/nghttpx/nghttpx.conf を見に行くようになっています.  設定ファイルの内容はコマンドラインオプションで上書き(一部のオプションは上書きではなく追加)できます.

対応プロトコルはどうなっているのでしょうか.  上記の場合, HTTP/2, HTTP/1.1 をフロントエンドでサポート[^1]しています.  ALPN または NPN でプロトコルのネゴシエーションをします.  バックエンドは HTTP/1.1 になります.  バックエンドで HTTP/2 を使う方法はすぐ後で書きます.

TLS に関しては nghttpx は充実しています[^4].  OCSPステープリング[^5], TLS セッションチケット秘密鍵の自動ローテーション, 複数 nghttpx インスタンス間の TLS セッションチケット秘密鍵および TLS セッションキャッシュの共有[^6]もサポートしています.

バックエンドの接続先を変更してみましょう.  この場合 `backend` オプションを使います.  127.0.0.1:8080 にフォワードするには以下のように設定ファイルに書きます:

```:nghttpx.conf
backend=127.0.0.1,8080
```

アドレスとポートの間は ":" ではなくて "," なので注意してください.

バックエンドで使うアプリケーションプロトコルはデフォルトでは HTTP/1.1 でした.  HTTP/2 を使う場合は `backend` オプションで:

```:nghttpx.conf
backend=127.0.0.1,8080;;proto=h2
```

とします.  ";" が二個ありますが誤植ではありません.  これについてはすぐ後で説明します.  この文書の執筆時点ではバックエンドのアプリケーションプロトコルは明示的に指定する必要があります.

nghttpx では `proto=h2` など `backend` オプション等で使用する追加設定をパラメーターと称しています.  `backend` オプションでパラメーターを使ってバックエンド接続の設定を変更することができます.  パラメーターは ";" で区切ります.

バックエンド接続を TLS で暗号化するには `tls` パラメーターを使います:

```:nghttpx.conf
backend=127.0.0.1,8080;;proto=h2;tls
```

UNIX ドメインソケットにも対応しています.  "unix:" に続けて接続先の UNIX ドメインソケットのパスを書きます:

```:nghttpx.conf
backend=unix:/tmp/h2o.sock;;proto=h2
```

慣れてきましたか?  では ";" が二個あった件についてお話しましょう.

nghttpx ではリクエストパスとホストでフォワード先を切り替えることができます.  `backend` オプションを複数回つかってフォワード先を指定します.  例えば以下の例をみてください:

```:nghttpx.conf
backend=127.0.0.1,6000;ws.example.com/ws/
backend=127.0.0.1,8080;/;proto=h2
```

上記の例では, ホストが ws.example.com でリクエストパスが /ws/ で始まる場合, 127.0.0.1:6000 へ HTTP/1.1 でフォワードする, それ以外は 127.0.0.1:8080 へ HTTP/2 でフォワードする, という設定になります.

フォワード先を切り替えるためのリクエストパスとホストの組をルーティングパターンと称しています.  書式について説明しましょう.  リクエストパスは必ず "/" で始まります.  "/" で始まらない場合, それはホスト名であると認識されます.  ホスト名だけ指定されていて, パスが指定されていない場合は, パスが "/" であるとみなされます.  ポート番号とキーワードの間にルーティングパターンを書き, ";" で区切るという書式になっています.  ルーティングパターンを省略すると "/" が指定されたことになり,  すべてのリクエストがマッチするようになります.  nghttpx ではこのようにすべてのリクエストがマッチするルーティングパターンを含めなければなりません.

ルーティングパターンのマッチングルールは Go の net/http パッケージの ServeMux によく似たものです[^7].  パスのマッチについては, 長いものが優先されます.  ホスト名がマッチするとパスのマッチよりも優先され, その中でパスのマッチで最も長いものが選ばれます.  "/" で終わる場合, 前方一致となります.  一つ例外があってリクエストパスに "/" を末尾に追加した時にマッチする場合も含むということです (e.g., ルーティングパターン "/ws/" は, リクエストパス "/ws" にもマッチする).  "/" で終わらないルーティングパターンは完全一致になります.

同じパターンを持つ `backend` を複数回つかってロードバランス先のバックエンドサーバーを追加できます:

```:nghttpx.conf
backend=127.0.0.1,8080;/;
backend=192.168.0.1,50051;/greeter/;proto=h2
backend=192.168.0.2,50051;/greeter/;proto=h2
backend=192.168.0.3,50051;/greeter/;proto=h2
```

上記の例では, ルーティングパターン "/greeter/" で 3 個のバックエンドサーバー 192.168.0.1, 192.168.0.2, 192.168.0.3 を追加しました[^2].

バックエンドの接続数は nghttpx が自動で調整します.  バックエンドの HTTP/2 接続を複数のクライアントで共有する場合もあります.

フロントエンド, バックエンドで共に HTTP/2 をサポートしているので例えば gRPC のような HTTP/2 ベースの RPC のリクエストを L7 でロードバランスすることができます.

`backend` オプションについて詳しく知りたい方は[マニュアル](https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-b)を参照してください.

ではフロントエンドの話をしましょう.  デフォルトでは全インターフェースでポート 3000 番を listen し TLS が必須なのでした.  listen するアドレスを変更するには `frontend` オプションを使います:

```:nghttpx.conf
frontend=*,80;no-tls
```

上記の例では全インターフェースでポート 80 番を listen し, TLS 無しの平文という設定内容になります.  "*" は全インターフェースを意味する記号です.  `0.0.0.0` のように IP アドレスを書くことももちろんできます.  `no-tls` はキーワードで, TLS 無しを意味します.

``frontend`` オプションを複数書いて listen するアドレスを増やすことができます:

```:nghttpx.conf
frontend=*,80;no-tls
frontend=*,443
```

上記の例では, 80 番ポートで平文通信, 443 番ポートで TLS 暗号化通信という設定になります.

UNIX ドメインソケットにも対応しています.  "unix:" に続けて UNIX ドメインソケットのパスを書きます:

```:nghttpx.conf
frontend=unix:/tmp/nghttpx.sock
```

`frontend` オプションについて詳しく知りたい方は[マニュアル](https://nghttp2.org/documentation/nghttpx.1.html#cmdoption-nghttpx-f)を参照してください.

ほとんどの Web サーバーによるリバースプロキシーではフロントエンドとバックエンドが関連付いて設定できる (あるいはそう強制される) ようになっています.  nghttpx はフロントエンドとバックエンドは分離されていてどのフロントエンドアドレスから入ってきても同じようにバックエンドにルーティングします.  これについては賛否両論あるでしょう.  後述する mruby スクリプトを使ってルーティングを制御することができます.

# HTTP/2 セキュアプロキシーとして使ってみよう

HTTP/2 セキュアプロキシーは, ブラウザーとフォワードプロキシーの間は原則的に 1 本の HTTP/2 TLS 接続になり, すべてのリクエストはこの接続を通してフォワードされます.  フォワードプロキシーはクライアントの要求に従い代理としてオリジンサーバーへリクエストをフォワードします.  https URI の場合は同じ接続でトンネリングされます.

バックエンドには Squid やフォワードプロキシーとして設定した Apache Traffic Server が必要です.  nghttpx は HTTP/2 と TLS の終端だけ行い, キャッシングなど残りの処理はすべてバックエンドに委譲します.

nghttpx を HTTP/2 セキュアプロキシーとして使うためには `http2-proxy` オプションを使います:

```:nghttpx.conf
http2-proxy=yes
```

`http2-proxy=yes` の場合, `backend` オプションのルーティングパターンはすべて無視されます.

Chromium と Mozilla Firefox が HTTP/2 セキュアプロキシーに対応しています.  両者ともに TLS 接続が必須になっています.

対応ブラウザで HTTP/2 セキュアプロキシーを使うには, 以下のような proxy.pac スクリプトを作成します:

```:proxy.pac
function FindProxyForURL(url, host) {
    return "HTTPS <SERVERADDR>:<PORT>";
}
```

`<SERVERADDR>` と `<PORT>` は nghttpx が listen しているアドレスとポート番号になります.  ブラウザは正しい SSL/TLS 証明書を要求します.  self-signed な証明書を使う場合はインポートするなりします.

Mozilla Firefox では Preference を開いて, Advanced, Network タブをクリックします.  Connection Settings ボタンをクリックして表示されるダイアログで "Automatic proxy configuration URL" を選択して proxy.pac へのパスを以下のように指定します:

```
file:///path/to/proxy.pac
```

Chromium では以下のようなコマンドラインを使います:

```
$ chromium --proxy-pac-url=file:///path/to/proxy.pac --use-npn
```

# mruby スクリプトによる拡張

nghttpx は mruby スクリプトで機能を拡張することができます[^3].  この文書の執筆時点では以下のようなことができます:

* リクエストヘッダー, レスポンスヘッダーの書き換え
* リクエストヘッダーを書き換えることによりバックエンドへのルーティングを制御
* バックエンドを使わずにレスポンスを返す

mruby スクリプトでは, 以下の 2 つのメソッドを持つオブジェクトを返すようにします:

* `on_req(ctx)`: クライアントからリクエストヘッダーを受信した時に実行される
* `on_resp(ctx)`: バックエンドからレスポンスヘッダーを受信した時に実行される

`ctx` はリクエストの情報が入ったオブジェクトです.  nghttpx は返されたオブジェクトのメソッドを上記に書いたイベントが発生すると呼び出します.

例として, すべてのリクエストパスの前に "/apps" というパスコンポーネントを挿入する mruby スクリプトは以下のようになります:

```:mod.rb
class App
  def on_req(env)
    env.req.path = "/apps#{env.req.path}"
  end
end

App.new
```

mod.rb を `mruby-file` オプションで指定します.

mruby スクリプトの詳しい使い方については[マニュアル](https://nghttp2.org/documentation/nghttpx.1.html#mruby-scripting)を参照してください.

# 参考文献

nghttpx について詳しく知りたい方には以下の文書をおすすめします.

* [nghttpx manual](https://nghttp2.org/documentation/nghttpx.1.html)
* [nghttpx - HTTP/2 proxy - HOW-TO] (https://nghttp2.org/documentation/nghttpx-howto.html)

# 脚注

[^1]: [spdylay](http://tatsuhiro-t.github.io/spdylay/) ライブラリと共にビルドするとフロントエンドで SPDY プロトコルもサポートします.
[^2]: この文書執筆時点では同じルーティングパターンを共有する場合, `proto` と `tls` キーワードの設定がバックエンドですべて同じである必要があります.  ルーティングパターンが違う場合はこの限りではありません.
[^3]: mruby を利用するためには, `--with-mruby` オプションを configure スクリプトに与えて nghttp2 をビルドします.  デフォルトでは有効になっていません.
[^4]: https://istlsfastyet.com/#server-performance
[^5]: https://nghttp2.org/documentation/nghttpx.1.html#ocsp-stapling
[^6]: https://nghttp2.org/documentation/nghttpx.1.html#tls-session-resumption
[^7]: https://golang.org/pkg/net/http/#ServeMux