QUICの概要
QUICと今
プロトコルの中身に入る前に、簡単にQUICについて紹介をする。
QUICはもともとGoogleによって考案実装されたUDP上で動作するプロトコルであり、TCPのような信頼性を持ちTLSのような暗号化された通信路で効率的なHTTPのメッセージをやりとりする。すでにChromeやGoogleのサービスで実装されている。そのデプロイについては以前このブログで触れた、GoogleのQUIC関係者によって書かれた論文が詳しい(こちらを参照)。
Googleでの実験をへて、2016年11月より正式にIETFでQUIC WGが結成され、QUICの標準化が進められている。
ここで注意すべきは、Googleが現在使っているQUICとIETFで標準化を行っているQUICはとこどころ異なっており互換性はない。区別して前者をgQUIC、後者をiQUICと呼び分ける。
iQUICは実装はすでにいくつかあり、定期的に実装をもちよって相互接続性試験を行っており、その結果を仕様側にフィードバックし仕様の確度をあげている。今は「9th Implementation Draft」となっており、その相互接続性試験の結果はスプレッドシートにまとめられている(URL)。
なお、iQUICはトランスポートプロトコルであり、上位のプロトコルとしてまずHTTPを想定しているがHTTPのみには限定されない。
以後、特に断りがない場合、QUICと行った場合 iQUIC を指す。
背景とQUICの目的
Webの配信高速化のために、HTTP/1.1からHTTP/2への改定された。より高速化のために、トランスポートレイヤでの問題に焦点が当てられるようになりました。
例えば下記の点があげられる
- TCPでは、パケットロスがあるとパケットロスが回復されるのをまってアプリケーションにデータが渡される。
- ハンドシェイクのラウンドトリップ回数 (TCP + TLS)
- クライアントのIP / ポート変更に伴うコネクションの切断
そのような問題を解決するために、QUICでは下記の項目にフォーカスして標準化が進められました
- コネクション確立と全体的な転送待ち時間を最小限に抑え、アプリケーションデータをより早いタイミングで転送可能とする
- ヘッドラインブロッキング(HoLB)なしで多重化を提供する。パケットロスの影響を最小化する
- エンドポイントのみが対応すればQUICが疎通するようにする(スイッチや中間装置の対応が不要)
- マルチパスとFEC拡張の対応
- デフォルトでTLS 1.3を使用して、常に安全なトランスポートを提供する。
フォーカス項目としては以上のとおりだが、QUICの機能は多く、上記にあがってないが、例えばECN(Explicit Congestion Notification)がサポートされていたりとたくさんの機能が組み込まれている。
ちなみに、現在はQUIC v1と呼ばれる機能の標準化を行っており、マルチパスやFECといった機能は先送りとなっている。しかし、そういった機能が将来的には対応できるよう拡張性をもっている。
QUICのオーバービュー
QUICのプロトコルスタックは以下の通りである
(引用: QUIC Tutorial)
左側のスタックが一般的に使用されているHTTP/2で、右側がQUICのスタックです。
QUICの構成としては
- QUICで提供されるトランスポート上でHTTPのメッセージをやりとりする。
- コネクション確立時にTLS1.3のメッセージをやり取りし、以後のメッセージを暗号化する鍵を取得する(0-RTT鍵, 1-RTT鍵)。
- メッセージのAckやコネクションの切断メッセージといった制御メッセージも暗号化される。
- UDP上で、TCPのような信頼性(パケロスの回復・パケットの順番の入れ替わりの補正)や輻輳制御フロー制御を提供するレイヤ。
HTTP部分については、通常のHTTP/2のメッセージ形式を使うのではなくQUICで扱うための変更が入っている
- 通所のHTTP/2に似ており、HTTPメッセージをやりとりする
- ただし後述の、QUICで提供されるQUICにあわせてメッセージ形式の変更がされている
- ヘッドオブラインブロッキング避けるために、HPACKをQUIC用に改良したQPACKを用いる
- サーバプッシュやプライオリティの扱いが少し変更された
- QUICにあわせてエラーメッセージの追加など
提案仕様はレイヤごとに別れており、スタックと合わせてみるとわかりやすい
プロトコルの話
ここからQUICのプロトコルの中の話に入っていく
QUICの用語
QUICで用いる用語について簡単に紹介する。
エンドポイント
- クライアント: QUICコネクションを開始する側
- サーバ: QUICコネクションを受け付ける側
- エンドポイント: クライアントもしくはサーバ
- ピア: 反対のエンドポイント
コネクションとパケット
- QUICコネクション: UDP上のQUICレイヤのいわゆるコネクション。可変長整数のコネクションIDによって識別される
- Source Connection ID: 送信元のコネクションID (一度、コネクションが確立すると省略される)
- Destination Connection ID: 送信先のコネクションD
- QUICパケット
- ClientとServerによってやりとりされるデータの単位
- UDPパケットの中にQUICパケットが結合されて格納される場合がある(ただし個別のQUICパケットとして処理される)
- QUICパケットにタイプが有り、それぞれロングヘッダもしくはショートヘッダを持つ
- ロングヘッダはコネクション確立時に利用され、ショートヘッダはそれ以降利用される
- パケットタイプ毎にパケット番号を持ち、パケットを送るごとにインクリメントされる
ストリームとフレーム
- フレーム
- QUICパケットの中に含まれるメッセージ単位
- 現在のQUICの仕様では21のフレームタイプが定義されており、フレームによって様々なメッセージを格納する。例えば次の通り
- 暗号ハンドシェイクのデータ、アプリケーションプロトコルのデータ、Ackやクローズといった制御情報、フロー制御情報
- パケットがロスした場合は、再送する必要のあるフレームのみ、新しいパケット番号を持つパケットで再送される
- ストリーム
- QUICはHTTP/2のように、コネクションの中にストリームと呼ばれる仮想的な通信単位を持つ
- 各ストリームはストリームIDを持つ
- ストリームにはタイプが有り、そのストリームIDの下位2bitによって次の通り分かれる
- 0x0:クライアントが作成した双方向ストリーム、0x1:サーバが作成した双方向ストリーム
- 0x2:クライアントが作成した単方向ストリーム、0x3:サーバが作成した単方向ストリーム
- 同じストリームに属するデータ内でのみ、パケットロスや並び替わりでブロックが発生する。他のストリームのデータはブロックしない。
- (UDPなので、来たデータからユーザ空間で処理可能)
(STREAMフレームはHTTPなどのアプリケーションデータを運ぶ)
QUICのコネクション確立/切断
コネクションの確立
QUICのコネクションの確立は下記のとおり行われます。
このときにトランスポート用パラメータと、TLSハンドシェイクをあわせて行うため、RTTが小さくなっています
TLS1.3の鍵交換のメッセージはCRYPTOフレームに格納されますが、コネクションを確立するための最初のCRYPTOフレームはInitialパケットで送信されます。各パケットタイプ毎にAckを返し、続いてHandshakeパケットでCRYPTOフレームを送信しTLS1.3のハンドシェイクを完了します。こうして1-RTT鍵が取得できたら、1-RTT鍵で保護されたパケットでアプリケーションデータを送受信できるようになります。
明示的にクライアントのIPアドレス所持を確認する方法もありますが、このハンドシェイクを通してクライアントのIPアドレス所持を確認できます
初期ウィンドウサイズや、ストリームの上限といったトランスポートパラメータはQUIC TLSで定義されるquic_transport_parameters拡張に格納され、ハンドシェイク後にきちんと認証されます。
コネクションの切断
QUICのコネクションの切断は3種類ある
- アイドルタイムアウト
- 即時切断
- ステートレスリセット
アイドルタイムアウトはその名の通り、トランスポートパラメータでアドバタイズされたアイドルタイムアウト値を超えた場合はクローズされる。なお、タイムアウト直前に送られたパケットが届く可能性があるため、draining状態になってから規定時間待ってコネクションの状態を破棄する。
即時切断は、CONNECTION_CLOSEフレームもしくはAPPLICATION_CLOSEフレームを送信することでコネクションを即時にクローズすることができます。なお、アイドルタイムアウトと同様、パケットの順番などが入れ替わる可能性があるのでdraining状態をヘてコネクションの状態が破棄されます。
ステートレスリセットは、コネクションを切断するための最後の手段です。仮にサーバ側の再起動などにより鍵情報が失われ、サーバが届いたパケットを正しく解読できなくなった場合、CONNECTION_CLOSEといった切断用のメッセージも解釈できなくなってしまいます。そうするとクライアントはアイドルタイムアウトまで待たないとコネクションが切断できなくなってしまいます。
上記の問題を防ぐために、サーバ側から事前にトークンを暗号路で発行しておき、そのトークンをステートレスリセットパケットでサーバに送ることでトークンと紐づくコネクションを切断できるようにします。
一度使ったトークンは二度としようできません。
QUICのフレーム
フーレムの概要
- PADDING: 意味のないフレーム。パケットサイズを増加させたりするのに使用される。
- RST_STREAM: ストリームを終了する
- CONNECTION_CLOSE: コネクションをクローズする
- APPLICATION_CLOSE: アプリケーションレイヤの都合によってコネクションをクローズする
- MAX_DATA: フロー制御で使用する。ピアがコネクション全体で送れるデータ量を増加する
- MAX_STREAM_DATA: フロー制御で使用する。ピアが各ストリームで送れるデータ量を増加する
- MAX_STREAM_ID: ピアの使用できるストリームIDの上限を増やす
- PING: コネクションの生存性を確認する
- BLOCKED: コネクションレベルのフロー制御によってデータが送信できないことをピアに通知する
- STREAM_BLOCKED: ストリームレベルのフロー制御によってデータが送信できないことをピアに通知する
- STREAM_ID_BLOCKED: ストリームID上限のためストリームが開始できないことをピアに通知する
- NEW_CONNECTION_ID: コネクションマイグレーション用のコネクションIDを発行する
- RETIRE_CONNECTION_ID: 発行されたコネクションIDを破棄する
- STOP_SENDING: アプリケーションの都合で、ピアのそのストリーム上でのデータ送信をやめさせる
- ACK: Ack情報を通知する
- PATH_CHALLENGE: パスの疎通性を確認を行う
- PATH_RESPONSE: パスの疎通性の確認に対する応答
- NEW_TOKEN: クライアントが将来のInitaliパケットで使用するトークンを発行する
- STREAM: ストリーム上のデータを転送する
- CRYPTO: 暗号ハンドシェイク用のデータを転送する
- Extension: 拡張用のフレーム。トランスポートパラメータで合意が得られた場合、任意の拡張フレームを使用できる