[メモ] HTTP/2 における connection reuse の挙動について調べた

### こんなことがあった

[環境]
a.example.com -> DNS A record: X.X.X.X (origin A)
b.example.com -> DNS A record: Y.Y.Y.Y (origin B)
※どちらも `*.example.com` のワイルドカード証明書を使っていて、接続はTLSとする

HTTP/2 が有効な上記のサービスにおいて、エンドユーザがlocal networkにProxyを持っている場合(社内プロキシなど)に b.example.com が a.example.com のコネクションを再利用し、`origin A`は当然 `b.example.com` のコンテンツを持っていないので意図しないレスポンスが返ってしまう、といったことが起きた。

それぞれIPアドレスは違うのに、どうしてだろう？

## RFC 7540
RFCには、TLSコネクションについては名前解決結果を問わないとされている。

https://tools.ietf.org/html/rfc7540#section-9.1.1

> Connections that are made to an origin server, either directly or
> through a tunnel created using the CONNECT method (Section 8.3), MAY
> be reused for requests with multiple different URI authority
> components.  A connection can be reused as long as the origin server
> is authoritative (Section 10.1).

https://tools.ietf.org/html/rfc7540#section-10.1

> HTTP/2 relies on the HTTP/1.1 definition of authority for determining
> whether a server is authoritative in providing a given response (see
> [RFC7230], Section 9.1).  This relies on local name resolution for
> the "http" URI scheme and the authenticated server identity for the
> "https" scheme (see [RFC2818], Section 3).

つまり、今回の例で考えると、まずユーザが`a.example.com`にアクセスし、その際に`*.example.com`のワイルドカード証明書を受け取ります。この証明書は当然 `b.example.com` に対しても有効であるので、RFC上はそれぞれのIPアドレスが違ったとしても厳密にチェックを行うことを必須としておらず、このワイルドカード証明書をもとに`a.example.com`へのアクセスで確立したコネクションを再利用し、`b.example.com`のリクエストを`origin A`へ要求しても問題はないことになっているのです。

知らなかった。。。

## ブラウザはきちんと制御してくれる

手元でも同様の環境を用意して試しました。ユーザが直接それぞれのoriginへ接続しにいく構成です。
結論から言うと、各種ブラウザは HTTP/2 + TLS の場合でも、それぞれのドメインの名前解決の結果が一致しない限りはコネクションの再利用を行わないように独自のロジックを実装しているようです。親切ですね。でもこれが余計に私を混乱させたのも事実。

※面白いトピックとしては、FirefoxにはoriginのDNSエントリが IPv4 + IPv6 を含む混在環境において(+ クライアントがIPv6アドレスを持っているる場合)、当初説明したような意図しないコネクション再利用を行う挙動もあったりする。(手元でも再現確認済)
https://bugzilla.mozilla.org/show_bug.cgi?id=1190136

## Proxyは実装次第

では今回の問題が起きた構成と同様に、ユーザがlocal Proxyを利用するケースを考えます。
この場合の挙動はProxyの実装次第なので、ブラウザ同様に振る舞われるケースもあれば、特にそこは考慮されずにコネクションが再利用される事もあるということです。意外な落とし穴ですよね。

※当初 HTTP/2 readyのlocal proxyツール([mitmproxy](https://mitmproxy.org/))を使って検証したが、残念ながら問題の事象は再現できなかった。

## Webサーバ側で不正なコネクション再利用を検知できないのか

### 421 - Misdirected Request

RFCでは今回のような場合を考慮し、Webサーバが HTTPステータスコード `421 - Misdirected Request` を返すことが認められています。
https://tools.ietf.org/html/rfc7540#section-9.1.2

これにより、コネクション再利用によってクライアントから要求されたコンテンツをサーバが応答不能である場合にサーバがクライアントへ `421` を返し、これを受け取ったクライアントはコネクションを新たに作成した上でもう一度適切なoriginへ要求をリトライする **かもしれない** ことになっています。そうです、421を受け取った際の処理はあくまでも MAY であって、リトライを行わずにエラーをそのまま表示する可能性もあります。
またリトライ処理を行ったとしてもその分のperformance影響が避けられません。

### ORIGIN Frame

421 には前述したようにいくつか問題があります。そこで期待されているのが、RFC 8336として今年正式に公開されたORIGIN Frameです。

https://tools.ietf.org/html/rfc8336

> +-------------------------------+-------------------------------+
> |         Origin-Entry (*)                                    ...
> +-------------------------------+-------------------------------+
> 
> An Origin-Entry is a length-delimited string:
> 
> +-------------------------------+-------------------------------+
> |         Origin-Len (16)       | ASCII-Origin?               ...
> +-------------------------------+-------------------------------+

この拡張仕様では、上記のようにoriginからレスポンスを返す際に明示的にコネクション再利用可能なoriginのリストをクライアントへ渡す事が可能になります。そしてこの`Origin-Entry`に**ワイルドカードを使うことはできません。**

これによってorigin側での制御が可能になり、意図しないコネクション再利用によるエラーを防ぐことができるようになります。

ただし注意したい点としては、このORIGIN Frameもコネクション再利用時にドメインの名前解決を必須としていません。`Origin-Entry`にoriginをいくつか列挙する場合は、念のため余計なものが含まれないように注意するべきですね。

https://tools.ietf.org/html/rfc8336#section-2.4

> Additionally, clients MAY avoid consulting DNS to establish the
> connection’s authority for new requests to origins in the Origin Set;
> however, those that do so face new risks, as explained in Section 4.

## まとめ
RFC読もう。

## 参考リンク
https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
https://medium.com/bbc-design-engineering/http-2-is-easy-just-turn-it-on-34baad2d1fb1
https://asnokaze.hatenablog.com/entry/2018/01/11/220418