QUICをゆっくり解説(2):ネゴせよ

2021年07月16日 金曜日


【この記事を書いた人】
山本 和彦

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

「QUICをゆっくり解説(2):ネゴせよ」のイメージ

QUICへの誘導

前回のブログで、ブラウザが HTTP/3 (HTTP over QUIC) に対応したサーバにアクセスしたときに、最初は HTTP/2 を使い、2回目の通信からHTTP/3を使うようになると説明しました。今回は、この過程でクライアントとサーバが何を折衝しているか、以下の順で解説します。

  • TLSのバージョン
  • HTTPのバージョン
  • HTTP/2からHTTP/3への誘導
  • QUICのバージョン

TLSのバージョン

あるURLで指定されたサーバにクライアントがアクセスすることを考えます。URLは、httpsで始まっていたとしましょう。つまり、TLSの中でHTTPが使われます。現在推奨されているTLSのバージョンは、1.2と1.3です。どうやって、どちらに決めるのでしょうか?

TLS 1.2以前では、ClientHello メッセージの client_version フィールドで、クライアントが利用できる最高のバージョンを送ります。サーバは、指定されたバージョン以下で、サーバが利用できる最高のバージョンを ServerHelloserver_version フィールドに指定して返します。以下の図では、クライアントが「1.2まで」と提案し、サーバが1.1を選んでいます。

TLS 1.3では ClientHelloclient_version にも、 ServerHelloserver_version にも 1.2 を指定することで下位互換性を保ちます。TLS のバージョンをみて振る舞いを変える中間装置があった場合でも、TLS 1.3 は TLS 1.2 のフリをしているので、通信ができる可能性が高く、実際のフィールドテストでもそれを支持する結果が出ています。

実際のバージョン指定には、supported_versions 拡張が利用されます。クライアントは ClientHellosupported_versions にバージョンを列挙し、サーバは選んだバージョンを ServerHellosupported_versions に指定して返します。

TLS 1.3 クライアントと TLS 1.2 サーバが通信した場合、TLS 1.2 サーバは supported_versions を理解できないので無視し、単に server_version に 1.2 を指定して応答します。

TLS 1.3 クライアントと TLS 1.3 サーバが通信した場合、client_versionserver_version は 1.2 ですが、サーバは ServerHellosupported_versions 拡張に 1.3 を指定して返します。

HTTPのバージョン

現在、主に使われているHTTPのバージョンは、HTTP/1.1とHTTP/2です。URLを見ても、TLSの中で使うべきHTTPのバージョンは分かりません。どうやって、どちらに決めるのでしょうか?

TLSの中で利用されるアプリケーションプロトコルは、ALPN(Application-Layer Protocol Negotiation)という拡張で折衝します。ALPNも、クライアントが提案し、サーバが選択します。TLS 1.2と1.3共に、クライアントからのALPN拡張は ClientHello に含まれます。

サーバの選択は、TLS 1.2だと ServerHello のALPN拡張に入ります。これは暗号化されていません。下の図では、クラアントが ALPN で HTTP/2 (“h2”) と HTTP/1.1 (“http/1.1”) を提案し、サーバは HTTP/1.1 を選択しています。

TLS 1.3だと、サーバの選択は、暗号化されている EncryptedExtensions に入ります。TLS 1.3を利用すると、サーバが選択したアプリケーションプロトコルが中継装置には理解できないので、その分プライバシー性が高いと言えます。下の図では、クラアントが ALPN で HTTP/2 (“h2”) と HTTP/1.1 (“http/1.1”) を提案し、サーバは HTTP/2 を選択しています。

このように、TLSのバージョンもALPNも、クライアントが提案し、サーバが選択するという方針をとっています。

HTTP/2からHTTP/3への誘導

前回のブログで述べたように、HTTP/2からHTTP/3へ誘導する場合は、Alt-Svc 応答ヘッダを利用します。

HTTP/1.1は、テキストベースのプロトコルです。一方で、HTTP/2やHTTP/3では、バイナリベースのプロトコルです。ただし、HTTP/2やHTTP/3では、HTTP/1.1のテキストで表現されるヘッダの意味を継承しています。そこで、HTTPのやりとりを理解するときは、バージョンによらず、これまで慣れ親しんだテキストのヘッダで考えて構いません。

以下は、HTTP/3(“h3”)がUDPの443ポートで利用できることを示す例です。

Alt-Svc: h3=":443"

IANAでは、UDPのHTTPSとして443ポートが予約されています。ですので、HTTP/3を正式に提供する場合は、このポートを使うことになるでしょう。

HTTP/3の実験をする場合、特権でないUDPポートを利用したくなるかもしれません。事実、QUICの実装者の間では、4433というUDPポートが好まれて使用されていました。この場合の、Alt-Svc は以下のようになります。

Alt-Svc: h3=":4433"

この設定では、Firefoxでは2回目の通信からHTTP/3を使うようになりますが、残念ながらChromeではうまくいきません。なぜなら Chromeでは、「Alt-Svc は特権ポートの壁を越えられない」というポリシーを実装しているからです。誘導元のポートと誘導先のポートは、両方とも非特権ポートか、あるいは両方とも特権ポートである必要があります。ですので、実験のときも素直にUDPの 443 ポートを利用することをお勧めします。

QUICのバージョン

現時点で、QUICバージョンには、「草稿29」と「バージョン1」が使われています。草稿29は徐々に使われなくなっていき、その内バージョン1だけなるでしょう。QUICには、バージョンを折衝する方式が組み込まれています。

クライアントは、使いたいバージョンを Initial パケットに指定して、サーバに送ります。サーバがこのバージョンを利用したくない場合、他に利用できるバージョンの候補を Version Negotiation パケットに入れて送り返します。指定されたバージョンをクライアントが利用できる場合は、そのバージョンで Initial パケットをもう一度送ります。

以下の図では、クライアントがバージョン1を指定したものの、サーバが草稿29を指示したため、クライアントは草稿29で接続をやり直しています。

このようにQUICのバージョンの交渉は、サーバが提案し、クライアントが選ぶ方式になっています。前回、QUICはユーザ空間で実装されているので進化の速さが期待できると述べました。新しいバージョンが策定された場合、このバージョンの交渉方式を利用して、新しいバージョンに移行していきます。その移行がスムーズであるためには、よくテストしておかなければなりません。

現在は草稿29とバージョン1の2つが混在するため、バージョン交渉のテストは比較的簡単にできます。しかし、バージョン1だけになった場合、どのようにしてテストするのでしょうか? QUICの策定課程で、草稿のバージョンが1つしかなかったときがあったはずです。その際、実装者はどのようにしてバージョン交渉をテストしたのでしょうか?

その答えは、グリーシング(greasing)です。グリーシングとは、デタラメな値を指定した場合、プログラムがエラーを起こさずに、無視するかテストすることです。QUICのバージョン交渉のテストでは、まずクライアントがデタラメなバージョンを指定し、サーバがエラーを起こさずに、正しいバージョンの候補を返してくるかを検証します。QUICの実装者にはこのテスト方法が共有されており、異なる実装間の相互接続性テストではバージョン交渉がよくテストされています。

まとめ

今回は、主にプロトコルのバージョンをどうやって選択するのかについて説明しました。実際の通信では、他にも様々なパラメータが折衝されています。

山本 和彦

2021年07月16日 金曜日

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

Related
関連記事