QUICの話 (QUICプロトコルの簡単なまとめ)

[追記] QUIC, HTTP/3 関連記事

QUICの話

QUICのプロトコルについて、自分の理解を整理するためにその中身について簡単にまとめていきたい。

本当は体系的にちゃんとまとめていきたい。。。

(なお、draft16を元に書いている)

QUICの概要

QUICと今

プロトコルの中身に入る前に、簡単にQUICについて紹介をする。

QUICはもともとGoogleによって考案実装されたUDP上で動作するプロトコルであり、TCPのような信頼性を持ちTLSのような暗号化された通信路で効率的なHTTPのメッセージをやりとりする。すでにChromeGoogleのサービスで実装されている。そのデプロイについては以前このブログで触れた、GoogleのQUIC関係者によって書かれた論文が詳しい(こちらを参照)。

Googleでの実験をへて、2016年11月より正式にIETFでQUIC WGが結成され、QUICの標準化が進められている。

ここで注意すべきは、Googleが現在使っているQUICとIETFで標準化を行っているQUICはとこどころ異なっており互換性はない。区別して前者をgQUIC、後者をiQUICと呼び分ける。(参照: gQUICのiQUICへの移行について)

iQUICは実装はすでにいくつかあり、定期的に実装をもちよって相互接続性試験を行っており、その結果を仕様側にフィードバックし仕様の確度をあげている。今は「9th Implementation Draft」となっており、その相互接続性試験の結果はスプレッドシートにまとめられている(URL)。

なお、iQUICはトランスポートプロトコルであり、上位のプロトコルとしてまずHTTPを想定しているがHTTPのみには限定されない。
以後、特に断りがない場合、QUICと行った場合 iQUIC を指す。

背景とQUICの目的

Webの配信高速化のために、HTTP/1.1からHTTP/2への改定された。より高速化のために、トランスポートレイヤでの問題に焦点が当てられるようになりました。

例えば下記の点があげられる

  • TCPでは、パケットロスがあるとパケットロスが回復されるのをまってアプリケーションにデータが渡される。
    • HTTP/2では複数のリクエスト・レスポンスが一つのTCPコネクションに多重化されている。
    • HTTPレスポンスAとHTTPレスポンスBがあったとする、片方のデータのパケットロスが、もう片方の通信もブロックさせてしまう。
  • ハンドシェイクのラウンドトリップ回数 (TCP + TLS)
    • 通信を開始するにあたって、TCPTLSのコネクション確立にラウンドトリップタイムがかかる
  • クライアントのIP / ポート変更に伴うコネクションの切断
    • Wi-Fiからキャリア回線などにハンドオーバーした際、TCPコネクションが切れてしまう

そのような問題を解決するために、QUICでは下記の項目にフォーカスして標準化が進められました

  • コネクション確立と全体的な転送待ち時間を最小限に抑え、アプリケーションデータをより早いタイミングで転送可能とする
  • ヘッドラインブロッキング(HoLB)なしで多重化を提供する。パケットロスの影響を最小化する
  • エンドポイントのみが対応すればQUICが疎通するようにする(スイッチや中間装置の対応が不要)
  • マルチパスとFEC拡張の対応
  • デフォルトでTLS 1.3を使用して、常に安全なトランスポートを提供する。

フォーカス項目としては以上のとおりだが、QUICの機能は多く、上記にあがってないが、例えばECN(Explicit Congestion Notification)がサポートされていたりとたくさんの機能が組み込まれている。

ちなみに、現在はQUIC v1と呼ばれる機能の標準化を行っており、マルチパスやFECといった機能は先送りとなっている。しかし、そういった機能が将来的には対応できるよう拡張性をもっている。

QUICのオーバービュー

QUICのプロトコルスタックは以下の通りである
f:id:ASnoKaze:20181031000151p:plain
(引用: QUIC Tutorial)

左側のスタックが一般的に使用されているHTTP/2で、右側がQUICのスタックです。

QUICの構成としては

  • QUICで提供されるトランスポート上でHTTPのメッセージをやりとりする。
  • コネクション確立時にTLS1.3のメッセージをやり取りし、以後のメッセージを暗号化する鍵を取得する(0-RTT鍵, 1-RTT鍵)。
    • メッセージのAckやコネクションの切断メッセージといった制御メッセージも暗号化される。
  • UDP上で、TCPのような信頼性(パケロスの回復・パケットの順番の入れ替わりの補正)や輻輳制御フロー制御を提供するレイヤ。
    • パスの到達性や、アドレスの所持確認の機能や、反射攻撃を防ぐ工夫など
    • ストリームと呼ばれる仮想的な通信単位を持つ
    • 輻輳制御やパケロス検出アルゴリズム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パケットにタイプが有り、それぞれロングヘッダもしくはショートヘッダを持つ
    • ロングヘッダはコネクション確立時に利用され、ショートヘッダはそれ以降利用される
    • パケットタイプ毎にパケット番号を持ち、パケットを送るごとにインクリメントされる

f:id:ASnoKaze:20181031010348p:plain

ストリームとフレーム
  • フレーム
    • QUICパケットの中に含まれるメッセージ単位
    • 現在のQUICの仕様では21のフレームタイプが定義されており、フレームによって様々なメッセージを格納する。例えば次の通り
    • 暗号ハンドシェイクのデータ、アプリケーションプロトコルのデータ、Ackやクローズといった制御情報、フロー制御情報
    • パケットがロスした場合は、再送する必要のあるフレームのみ、新しいパケット番号を持つパケットで再送される
  • ストリーム
    • QUICはHTTP/2のように、コネクションの中にストリームと呼ばれる仮想的な通信単位を持つ
    • いくつかのフレームは、ストリームIDを持ちストリームと紐付けられる
    • 各ストリームはストリームIDを持つ
    • ストリームにはタイプが有り、そのストリームIDの下位2bitによって次の通り分かれる
      • 0x0:クライアントが作成した双方向ストリーム、0x1:サーバが作成した双方向ストリーム
      • 0x2:クライアントが作成した単方向ストリーム、0x3:サーバが作成した単方向ストリーム
    • 同じストリームに属するデータ内でのみ、パケットロスや並び替わりでブロックが発生する。他のストリームのデータはブロックしない。
    • (UDPなので、来たデータからユーザ空間で処理可能)

f:id:ASnoKaze:20181031005926p:plain
(STREAMフレームはHTTPなどのアプリケーションデータを運ぶ)

QUICのコネクション確立/切断

コネクションの確立

QUICのコネクションの確立は下記のとおり行われます。
f:id:ASnoKaze:20181031012530p:plain

このときにトランスポート用パラメータと、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: 拡張用のフレーム。トランスポートパラメータで合意が得られた場合、任意の拡張フレームを使用できる

アンプ攻撃対策

別途記事を書きました
QUICの仕様におけるアンプ攻撃対策

次書きたい話題

HTTP 418ステータスコードが予約される

「418 I’m a tea pot」としてよく知られる 418 ステータスコードについて、IETF HTTPbis WGで議論になっていました。

「418 I’m a tea pot」はジョークRFCである「RFC2324 Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0)」で定義されているステータスコードです。

ここで注意するのは、418はHTCPCPで定義されているステータスコードであり、HTTPで定義されているステータスコードではないという点です。

この議論については以前 Qiita で書いたとおりです。
qiita.com

HTTPで418ステータスコードを予約

以前このブログで書いたとおり、HTTPセマンティクスに関して仕様の再改定が行われております
asnokaze.hatenablog.com

その後、3つの仕様に分割され、HTTPのセマンティクス、HTTP/1.1のフォーマット、キャッシュに関する3つの文書に整理されています。

この改訂版HTTP仕様 (HTTPtre) のdraft-03が本日提出され、そのHTTP Semanticsでついに418が Unused として予約されました

418 (Unused)

418はすでにHTTP実装でイースターエッグとして実装されている可能性があるため、418を予約し、今後も仕様としては418は利用しないことになりました。

Auto Upgrade Mixed Contentとは

HTTPSのサイト内にHTTPで提供される画像やスクリプトがあると、「Mixed Content」の仕組みによりURLバーに黄色い警告が出たり、リソースがブロックされます。

もちろんスクリプトがブロックされればWebページを正しく表示できません、URLバーに表示されたシールドマークから通信を許可することもできますが、一般ユーザにとって分かりにくいUXとなっています。

そこでChromeでは、HTTPで読まれるリソースをブロックするのではなく、HTTPSに自動でアップグレードしてリソースを取得する「Autoupgrade Mixed Content」という仕組みが検討されています。

以前、GoogleのMike West氏から提案されていた「Mixed Content Level 2」と似たような仕組みです。
asnokaze.hatenablog.com

( Auto Upgrade Mixed Contentの資料には「THIS DOCUMENT IS PUBLIC」と書かれてるので、おそらく大丈夫)

実験

Chromeでは、6通りの方式を実験してどのようなアップグレード手法が有効か模索しようとしています。この実験を解析し、新しい機能を改めて提案するかが決定されます。

3種類のアップグレード方式と、それぞれについてHTTPSのロードに失敗した場合HTTPにフォールバックするかの2種類を試し、計6種類を実験するようです。

stableのユーザのうち1%で実験を行うと書かれています。この実験では、画像など今までブロックされていなかったリソースもフォールバックなしだと表示されなくなる場合がありそうです。

方式

Upgrade blockable mixed content to HTTPS

Scriptなどのblockable mixed contentをアップグレードします。すでにブロックされているリソースですので、HTTPSにアップグレードしてリソースが読み込めなくてもページを破損させることはありません。

Upgrade only optionally blockable mixed content to HTTPS

画像などのoptionally blockable mixedのみをアップグレードします。通常、表示できているリソースですので、HTTPSににアップグレードすることで読み込めなくなり、ページを破損させる可能性があります。

Upgrade all mixed content to HTTPS

すべてのmixed contentをアップグレードします。このケースでは、警告表示や、ユーザによるmixed contentの許可はなくなります。

サイトがupgrade-insecure-requests ヘッダを指定した場合と似た挙動となります。

フォールバック

HTTPSにアップグレードを試みて、リクエストが失敗したりタイムアウトした場合はHTTPにフォールバックします。このフォールバックは、表示速度が遅くなるとともに処理も複雑になることが懸念されています。

NTPを暗号化する Network Time Security for NTP の提案仕様

サーバが正しい時刻に設定されていることは重要であり、時刻同期に使用されるNTPにおいてもサーバの認証や改ざんを防ぎたいというモチベーションは理解できる。

IETFのNTP WGでは、まさにNTPを暗号的に保護するNetwork Time Security for the Network Time Protocol」という仕様策定が進められている。

NTPにはいくつかモードがあるが client-server モードを前提としている。

NTPは詳しくないがざっと見てみる

(正しくはNTPの暗号化ではなく、保護的な感じはある)

目的

Network Time Securityの目的は以下の通り

  • Identity: x.509 証明書で通信相手を確認する
  • Authentication: 時刻同期のパケットを認証し、改ざんされていないことを確認する
  • Confidentiality: 時刻情報に秘匿性はありませんが、NTP拡張フィールドの暗号化をサポートする
  • Replay prevention: 以前のパケットが再送されても検知できるようにする
  • Request-response consistency: クライアントの要求に対する応答で有ることを確かにする
  • Unlinkability: クライアントが別のネットワークに移動して通信を行ったとしても、経路上からは同一クライアントか識別できないようにする
  • Non-amplification: 増幅攻撃が行われないように、リクエストより大きなレスポンスを返さない
  • Scalability: サーバはユーザ固有の状態を保存せず、多くのクライアントにサービスを提供できるようにする

プロトコル概要

このNTSは2つの疎結合のサブプロトコルからなります

暗号化するための鍵を交換するNTS Key Establishment と、NTPv4を暗号化してやりとりするための拡張を定義したNTS Extensions for NTPv4です。

f:id:ASnoKaze:20181005023438p:plain

NTS Key Establishment (NTS-KE)

NTPv4のNTS拡張領域で使用するキーマテリアル(お馴染み RFC5705)を取得するメカニズムです。

TLSを使用して鍵を交換し、クライアントに最初のCookie、NTPサーバのIP等を提供します。また、暗号アルゴリズムなどいくつかのパラメータをネゴシエーションします。

NTS Extensions for NTPv4

NTPv4でNTS用の拡張領域を持ちます。この領域によって正しいサーバと通信できており、時刻情報が改ざんされていないことを確認できるようになります。

具体的には以前に取得したキーマテリアルを使用してNTPv4を暗号的に保護する複数の拡張領域が定義されます。

すべての状態はクライアント側に保持され、サーバはクライアントの状態を保持する必要がないため、スケーラビリティがあります。これらの拡張フィールドには、NTS-KEハンドシェイクから抽出されたキーマテリアルを使用して計算されたCookieと認証タグが含まれます。

NTPサーバはこのクッキーを使用してこのキーマテリアルをリカバリし、認証されたレスポンスを返します。 レスポンスには新しく暗号化されたCookieが含まれます。クライアントは次はこのCookieを使用します。こうすることで、返されたレスポンスは自身のリクエストがあってから生成されたレスポンスであることが確認できます。

実装

すでにいくつかの実装が出ている

その他

時刻同期プロトコルでサーバ認証を行う際、クライアントの時刻が同期されておらずサーバ証明書の検証が失敗される問題が有る。このドキュメントでも完璧な解決策は無いと書かれている。いくつかの緩和策は述べられていますが、やはり本質的な改善では無いようです。

JS Self-Profiling API とは

W3C Web Performance Workingの議事録に「JS Self-Profiling API」についての議論があったので簡単に眺めておく。

ミーティングの「発表スライドはこちら

JS Self-Profiling API

いわゆるRUM(Real user monitoring)などと同様、実際のユーザ側でJavaScriptのプロファイルを取得可能にするというのが「JS Self-Profiling API」のようだ。

f:id:ASnoKaze:20181004002206p:plain

ユーザにより端末やネットワーク環境が違うため、実際のユーザ側でJavaScriptのプロファイルを取りたい。timerを使うことで擬似的に測定はできるが、コード量やオーバヘッドが増える。この提案を行っている、Facebookの人らはJavaScriptとSharedArrayBuffersでサンプリングプロファイラを実装したが正確性とパフォーマンスの欠点があると述べています

といった背景から、このような提案が出ているようだ。

使い方

プロファイルを取るサンプルコードが出ている。
performance.profileでプロファイリングを開始して、stopして得られた結果を送信するような流れである。

const profiler = performance.profile({ categories: ['js'], sampleInterval: 10 });
for (let i = 0; i < 1000000; i++) {
     await doWork();
}
const trace = await profiler.stop();
sendTrace(trace);

profileデータ

得られたデータのフォーマットは、V8のTrace Event FormatGecko profile formatを参考に作られているようだ。

interface ProfilerTrace {
  readonly attribute FrozenArray<ProfilerFrame> frames;
  readonly attribute FrozenArray<ProfilerStack> stacks;
  readonly attribute FrozenArray<ProfilerSample> samples;

  [Default] object toJSON();
};

framesから実行状態を取れる模様

QUICの信頼性のないデータグラム拡張(MESSAGEフレーム/Datagramフレーム)

QUICはUDP上で暗号化された信頼性のあるデータ通信を提供するトランスポートプロトコルです。

現在IETFの「QUIC WG」で標準化が進んでおり、先行して実装されていたGoogle版QUICもIETF版QUICへの移行が進められています。

QUICのアプリケーションレイヤ

現在QUIC上のアプリケーションプロトコルとしては「HTTP over QUIC」にフォーカスして標準化が進められています。

一方で、WebRTC over QUIC (Quartc)、QUIC as a VPN (QBone)、DNS over QUICといったその他のアプリケーションプロトコルでもQUICを使用する考えを持ってる人もいます。

そこでそういったプロトコルのトランスポートとして使用できるように、QUIC上でロスしたパケットに含まれるデータを再送しないデータ通信を可能にする拡張仕様が提案されています。もちろん通信は暗号化されていますし、Ackにより相手が受信したかどうかは確認することが出います。

MESSAGEフレーム/Datagramフレーム

提案自体は同時期に個別に2つの仕様が出ています。

1つ目の「QUIC Messages」はGoogleのIan Swett氏提案しているMESSAGEフレームを定義し利用するもので、Google QUIC v45ですでに実装されています。

2つ目の「An Unreliable Datagram Extension to QUIC」はApple人らによって提案されているDATAGRAMフレームを定義利用するものです。著者はIETF102において、Ian Swett氏との議論に基づいてDraftを書いたと述べています。

これらはフレーム名を除いて同じもののようです。実際に、Ian Swett氏は共同で作業していく意思を示しています。

DATAGRAMフレーム

DATAGRAMフレームのフレームタイプは、0x1c又は0x1dであり最下位ビットが1の場合はLengthフィールドを持ちます。0の場合はパケットの最後まででデータであることを意味します。

DATAGRAMフレームのフォーマットは次のとおりです
f:id:ASnoKaze:20180922223804p:plain

STREAMフレームの場合はストリームIDによって一つのコネクション上に多重化されますが、DATAGRAMフレームの場合は多重化するのならQUICを利用するアプリケーションの責任によって実施されます。

DATAGRAMフレームのみのパケットにはAckする必要がありますが、ロス回復には使用されないためAckを遅延させてバッチ的に応答すべきです(SHOULD)。DATAGRAMフレームはコネクションレベルのフロー制御を受けます。また、輻輳制御やフロー制御によりブロックされた場合はそのままドロップすることもできます(MAY)

ブラウザからシリアルポートにアクセスするSerial API

blink-devメーリングリストに「Intent to Implement: Serial API」として「Serial API」の実装に着手する旨の投稿がされている。

この「Serial API」はW3CのWICGで議論がされており、ブラウザからシリアルポートにアクセス可能にする。3DプリンタやArdbinoなど、様々なデバイスと接続できるようになる。

パーミッションの要求については、すでに実装されている「Web Bluetooth」「WebUSB API」とおなじになるようだ。

sample

仕様では、Ardinoからデータを読み込む例が示されている

//Request the list of ports from the user
SerialPort.requestPorts().then(ports => {

  //Pick the first matching port
  var serial,
      kind = "Arduino",
      key = "manufacturer",
      //find the Arduinos!
      arduinos = ports.filter(port => port.get(key).search(kind) > -1);

  if (arduinos.length) {
     serial = new SerialPort(arduinos[0].path);
     serial.in.read(readData)
  }

  function readData(){
    while(let data = yield serial.read()) {
      console.log(data);
    }
  }
})
.catch(console.error);