HTTP/3と新しいプライオリティ制御方式について

目次

関連記事

HTTP/3については以前書いたとおりです
asnokaze.hatenablog.com

背景

HTTP/2から複数のHTTPリクエストが一つのコネクション上で並列的に送信されるようになりました。それにともない、クライアント側からリクエストの優先度(プライオリティ)をサーバに通知する仕組みが導入されました。

Webページをレンダリングを開始するのに必要なリソース(HTML, CSSなど)の優先度を高く設定したりできます。

現在標準化が進められているHTTP/3でも、最初はHTTP/2と同様、リクエストの依存関係(dependency)と重さ(weight)で優先度を管理する方式(ツリー方式)が検討されていました。

しかし、議論が進むに連れこのツリー方式の優先度制御は複雑であり、あまり実装されていないことがわかりました。(Interimの資料)

HTTP/3では新しい優先度制御方式も議論されましたが、一旦HTTP/3の仕様からは優先度制御の仕組みは外され、優先度制御はオプショナルな機能とする方向となりました。

実際、HTTP/3の仕様からはすでに優先度制御の機能は削除されています。
github.com

またHTTP/2でも優先度制御を行わないで、拡張仕様の優先度制御方式をネゴシエーションする仕組みを検討することになりました。

優先度制御の仕様は、別途デザインチームを結成しそこで議論されることになりました。

ここらへんの議論は、kazuhoさんの記事に書かれているとおりです。
blog.kazuhooku.com

プライオリティの仕様の方向性

HTTP WGのメーリングリストに投げられている通り(URL)、先日行われたQUIC WGの中間会議においてデザインチームのリーダであるIan Sweet氏より、Fastlyのkazuhoさんが提案している「Extensible Prioritization Scheme for HTTP」をベースとして進めていくことがアナウンスされています。

Extensible Prioritization Scheme for HTTP

この仕様では、プロトコルバージョンに依存しない、リクエストここ別に絶対値で示される優先度をもつエンドツーエンドの優先度制御方式を定義しています。

具体的には以下のものが定義されています

  • Priorityヘッダ(リクエスト・レスポンスで使用される)
  • 優先度。urgency (優先度の絶対値)、プログレッシブ
  • ネゴシエーションの仕組み
  • 優先度を更新するフレーム(HTTP/2, HTTP/3の拡張フレーム)
Priorityヘッダ

priorityは下記のような形式です。フォーマットは「Structured Headers」で定義されたDictionary形式です。

   priority = urgency=3, progressive=?1

このヘッダを用いてクライアントはこのHTTPリクエストの優先度をサーバに通知します。

一方で、サーバはレスポンスヘッダにpriorityを指定することで、そのレスポンスがどれくらいの優先度で返されていることを示せます。これによって特に、そのHTTPリクエスト・レスポンスを扱っているProxyに対して、優先度を知らせることができます。

urgency と progressive

priorityヘッダは、urgencyとprogressiveというパラメータを持ちます。

urgencyは-1から6までの値をとります。値が小さいほど優先度が高いです。サーバは優先度が高いものから送信していきます。

  • prerequisite (-1)
  • default (0)
  • supplementary (1~5)
  • background (6)

それぞれ意味次のとおりです。

prerequisite: urgencyがdefaultやprerequisiteのレスポンスを止めます。ブラウザのレンダリングをブロックするようなCSSのリクエストなどに使用されます。

default: 他のリクエストをブロックしません。例えば新しいページに遷移したときにしようされます。

supplementary: この複数要求されたリソースの名から、必須ではないりそーすにつけられます。1~5の値によって優先度がしめされます(クライアントはサーバが上書きして使えるように1,5は使うべきではありません)

background: 他のリクエストに影響が出ないように後回しにできるリクエストに使用される。

ネゴシエーションの仕組み

HTTP/2およびHTTP/2で使用する優先度制御方式をネゴシエーションする方法もこの仕様で定義されています。

SETTINGS_PRIORITIESパラメータを用いてネゴシエーションします。値は次のとおりです

  • 1. H2_TREE: HTTP/2の優先度ツリー方式 (HTTP/3では使用できない)
  • 2. URGENCY: この仕様

また、urgencyとは別にprogressiveを指定できます。progressiveはそのリソースが逐次処理可能なリソースか示します(例えばprogressive jpegなど)。progressiveブーリアンであり0か1をとり、同じurgencyでprogressiveなレスポンスがあった場合は帯域を等分して使います。

優先度の更新

通信中に優先度を更新する場合もあります(例えば、ブラウザのタブを切り替えたり)。そのときに使用する拡張フレームも定義されています。

HTTP/2とHTTP/3両方にPRIORITY_UPDATEフレームを新しく定義しますが、それぞれフォーマットは異なります。HTTP/2ではストリームIDを指定します。HTTP/3ではストリームIDもしくはプッシュIDをしてします。新しい優先度はASCIIでヘッダと同じように指定します。

H2の場合
f:id:ASnoKaze:20191107015351p:plain

H3の場合
f:id:ASnoKaze:20191107015417p:plain

キャッシュ情報を示す、Cache-Status レスポンスヘッダ

リクエストに対してx-cacheレスポンスヘッダでキャッシュにhitしたか示すCDNベンダーもあります。

しかし、このx-cacheヘッダは各ベンダー独自のもので標準化されていませんでした。

そこでCache-Statusというヘッダをちゃんと定義する「The Cache-Status HTTP Response Header」という仕様がFastlyのMark Nottingham氏から出ています。

以前まではcacheヘッダという名前でしたが、draft 01ではCache-Statusヘッダと改称されました。それにあわせてヘッダ値もリファクタされています。

前の仕様は以前書いた通りです。
asnokaze.hatenablog.com

Cache-Statusヘッダ

Cache-Statusは以下のようになります。「Structured Headers for HTTP」の定義に従ってリスト構造を持ちます。

Cache-Status: ExampleCache; fwd=res-stale; fwd-res=notmod

まず、このCache-Statusヘッダをつけたサーバの識別子(CDN名)が付きます。そのあとに、キャッシュに関する情報がつきます。(複数CDNを経由する場合は、そのつど追記されていきます。)

続くパラメータは以下の通りです。

fwd パラメータ

fwdパラメータは、リクエストがフォワードされた理由を示します。

  • none: リクエストはフォワードされてません (キャッシュにhitし、それが使用されました)
  • bypass: リクエストなんの処理もしないように設定されています
  • uri-miss: リクエストのURIにマッチするキャッシュがありません
  • vary-miss: リクエストのURIにマッチしましたが、ヘッダに基づいてマッチするキャッシュがありませんでした
  • miss: キャッシュにhitしませんでした (uri-missとvary-missを区別なく使用できる)
  • res-stale: キャッシュにマッチするものがありましたが、すでに有効ではなくなっていました
  • req-stale: キャッシュはありましたが、リクエストのCache-Controlリクエストディレクティブに基づいて、使用できませんでした
fwd-res

fwd-resパラメータは、リクエストを転送して受け取ったレスポンスの結果を示します。

  • full: 完全なレスポンス (304, 206以外)
  • partial: 206 Partial Response
  • notmod: 304 Not Modified
fwd-stored

fwd-storedパラメータは、リクエストをフォワードした際にそのレスポンスを保存したかを示します。

res-fresh

res-freshパラメータはレスポンスの残りのFreshness(有効時間)を秒で示します。

Cache-Status: ExampleCache; res-fresh=376
cache-fresh

cache-freshパラメータは、持ってたキャッシュを返した際に、その残りのFreshness(有効時間)を秒で示します。

collapse-hit

キャッシュサーバが同じHTTPリクエストを同時に複数受信した場合は、そのうち1つだけを転送し、受け取ったレスポンスを複数リクエストに対して返す実装もあります。

collapse-hitは、ほかのHTTPリクエストと束ねられたかをしめします。

collapse-wait

collapse-waitパラメータは、ほかのHTTPリクエストに束ねられた場合の待ち時間をミリ秒で示します

Cache-Status: ExampleCache; fwd=uri-miss;
              collapse-hit=?0; collapse-wait=240

複数のCDNを利用する場合

先述の通り、複数のCDNを通る場合はそのつど追記されます。

Cache-Status: "CDN Company Here"; res-fresh=545,
              OriginCache; cache-fresh=1100; collapse-hit=?1

「Binary Structured HTTP Headers」について

Fastlyの「Binary Structured HTTP Headers」という提案仕様がでています。

目次

概要

HTTPヘッダはテキストで表現されます。特に値に関して数値、日付やブーリアン値もテキストで表現されます。また、Accept-Encodingのようにリスト構造を持つヘッダ値もあります。

本来バイナリで表現できるデータ形式や構造をテキストで表現するのは、データ量が増えるほかパースコストがかかります。

そこで、Structured Headersの定義に基づいて、HTTPヘッダ値を直接バイナリで表現できるようにするのが「Binary Structured HTTP Headers」です。この仕様では、バイナリのヘッダを送るBINHEADERSフレームと、HPACKに追加でBinary Literal Representationを定義します。

エンドポイントがこの仕様に対応していることを示すSETTINGS_BINARY_STRUCTURED_HEADERSパラメータも定義されます。

Structured Headersについては以前書いたとおり(少々内容は古くなっており、データ型の種類が異なっています)
asnokaze.hatenablog.com

BINHEADERSフレーム

BINHEADERSフレームは、次の一点を除きHEADERSフレームと同じフィールドを持ちます。HEADERSフレームとの違いは、ヘッダ圧縮(HPACK)でテキストを表現するのに使用されるString Literal Representationの代わりにBinary Literal Representationを使用することです。

ただし、Binary Literal Representationはヘッダ値でのみ使用されます。

Binary Literal Representation

Binary Literal Representationはデータ構造が示す部分と、値のデータ型を示す部分からなります。まずは、データ構造を示す部分から説明してきます。(ヘッダ構造を示すtypeと、データ型を示すtypeはそれぞれ別の種類であることに注意してください)

Binary Literal Representationは以下のフィールドを持ちます
f:id:ASnoKaze:20191102224210p:plain

ヘッダの構造を表す4つのTypeがあります

  • Lists (type=0x1)
  • Dictionaries (type=0x2)
  • Items (type=0x3)
  • String Literals (type=0x4)

(Inner Lists, Parametersは説明を省略)

Lists

Listsとは下記のような ’,’ で区切られているようなヘッダ値です。Binary Literal RepresentationではInner ListsやItemを複数書くことで表現されます。

Example-StrListHeader: "foo", "bar", "It was the best of times."
Dictionaries

Dictionariesとは下記のような、key=valueの構造を複数持つヘッダ値です。

   Example-DictHeader: en="Applepie", da=*w4ZibGV0w6ZydGU=*

Binary Literal Representationでは下記のように、Keyの長さ、Keyの文字列、値(Binary Structure Types)からなります。
f:id:ASnoKaze:20191102225721p:plain

Items

整数、実数、base64値、ブーリアンなどを示す単体の値です。それぞれごとにdata typeを持つので、それを見ることでどのデータ型なのか識別できます。

f:id:ASnoKaze:20191102230315p:plain

String Literals

これはバイナリではなく、単純な文字列表現です。バイナリ表現が使用できない場合に使用されます。HPACKで定義されるString Literal Representationsと同じものです。

Item Payload Types

具体的なItemのバイナリ表現方法です。

Itemには下記の種類があります。それぞれごとにバイナリフォーマットが定義されています。

  • Integers (type=0x3)
  • Floats (type=0x4)
  • Strings (type=0x5)
  • Tokens (type=0x6)
  • Byte Sequences (type=0x7)
  • Booleans (type=0x8)

(説明を省略したInner Lists, Parametersがそれぞれtype=1, 2です)

例えばInteger(整数)は下記のバイナリフォーマとを持ちます。
Sは正負、Xはパディング、その後に長さと値が入ります。
f:id:ASnoKaze:20191102232742p:plain

Booleansであれば以下のように短く表現できます。
f:id:ASnoKaze:20191102231737p:plain

Aliased Fields

この仕様ではいくつかのヘッダにおいて、Binary Structured HTTP Headersを使えるようにエイリアスというものを定義しています。

Dataヘッダは以下のとおりです。そのままではBinary表現は使用できません

Date: Sun, 06 Nov 1994 08:49:37 GMT

DataヘッダのエイリアスであるSH-Dateでは、Unixタイムを使用して以下のようにIntegerのバイナリ表現を使用できます。

SH-Date: 784072177

どのようなエイリアスが定義されているかは仕様を御覧ください。

NginxでHTTP/3が動いた (Cloudflareパッチ)

NginxをHTTP/3対応させるパッチがCloudflareから提供されました (CloudflareのHTTP/3ライブラリ Quicheを利用しています。現状ではHTTP/3ドラフト23版の対応になります)
github.com

基本的に、書いてるとおりにやればビルドできるのですが、無事HTTP/3しゃべるところまで確認できました

ビルド

rustインストールしておく

$ curl https://sh.rustup.rs -sSf | sh
$ source $HOME/.cargo/env

書いてあるとおり

$ curl -O https://nginx.org/download/nginx-1.16.1.tar.gz
$ tar xzvf nginx-1.16.1.tar.gz

$ git clone --recursive https://github.com/cloudflare/quiche

$ cd nginx-1.16.1
$ patch -p01 < ../quiche/extras/nginx/nginx-1.16.patch

$ ./configure                              \ # ./configureで実行した
       --prefix=$PWD                           \
       --with-http_ssl_module                  \
       --with-http_v2_module                   \
       --with-http_v3_module                   \
       --with-openssl=../quiche/deps/boringssl \
       --with-quiche=../quiche
$ make

nginx.conf の編集

nginx-1.16.1$ vim ./conf/nginx.conf  #下記を追記

    server {
        listen       4433 quic;
        server_name  localhost;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

        ssl_certificate      /path/to/server.crt;
        ssl_certificate_key  /path/to/server.key;

        location / {
            root   html;
            index  index.html index.htm;
        }
    }

起動

nginx-1.16.1$ sudo ./objs/nginx 

接続してみる

quicheのサンプルクライアントで接続する (draft-23バージョンでネゴシエーションします)

quiche$ cargo build --examples
quiche$ target/debug/examples/http3-client https://127.0.0.1:4433/index.html --no-verify   |head -n3
<!DOCTYPE html>
<html>
<head>

ちゃんとHTTP/3でアクセスログにも着てる

nginx-1.16.1$ tail ./logs/access.log 
127.0.0.1 - - [16/Oct/2019:18:21:43 +0900] "GET /index.html HTTP/3" 200 612 "-" "quiche"
127.0.0.1 - - [16/Oct/2019:18:21:45 +0900] "GET /index.html HTTP/3" 200 612 "-" "quiche"
127.0.0.1 - - [16/Oct/2019:18:22:19 +0900] "GET /index.html HTTP/3" 200 612 "-" "quiche"

パケットキャプチャもこの通り
f:id:ASnoKaze:20191016190623p:plain

WebTransport over QUIC について

目次

blink-devメーリングリストで、「Intent to Implement: QuicTransport」として、ChromeでのWebTransport over QUICの実装の機運が高まっている。

というか、GoogleのQUIC, HTTP/3実装ライブラリであるQUICHEにはすでにQuicTransportに関するコードが既にコミットされ始めている(URL)

WebTransportは、トランスポートプロトコルとしてHTTP/3やQUICを利用する双方向のメッセージプロトコルです。APIに関してはW3C側で、プロトコルとしてはIETFで議論が始まっています。仕様は以下の通り

来月行われるIETF106では、WebTransport WGを結成するためのBoFが提案されている。そのなかでも「Chromium is currently implementing a client Google QUIC library will support both client and server」と、クライアント・サーバともに実装している旨が書かれている。

仕様としては使用できるトランスポーはいくつかあるが、まずはover QUICから取り掛かるようだ

WebTransport over QUIC

一部本記事と重複するが、WebTransportの概要については下記の記事を参照
asnokaze.hatenablog.com

Googleさんが、WebTransport over QUICをまず進めていくようなので、WebTransport over QUICについて簡単に書く。なお、draftは著者様のgithub上ではすでに加筆されており、次のdraft-01で入る予定の機能についても触れる。

WebTransport over QUICは、WebTransportの特性のうち下記をサポート

  • Stream independence: Always supported
  • Partial reliability: Always supported
  • Pooling support: Not supported
  • Connection mobility: Implementation-dependent

また、QuicTransportでの0-RTTサポートは、QUICおよびTLS 1.3と同様にオプションです。

Stream independence

QUICレイヤのストリーム(単方向・双方向)を利用して、ヘッドオブラインブロッキングのない通信(パケットロスが発生しても他のストリームをブロックしない)となる。TCPとは異なり、アプリケーションはOSレイヤでのロスしたパケットの回復を待つことはなく、パケットから処理が可能となる。

Partial reliability

また、DATAGRAMフレームを利用し、パケットロスしたデータを回復しないことも選択できる。
asnokaze.hatenablog.com

Pooling support

WebTransport over QUICでは一つのQUICコネクションを利用してしまうので、同じ相手と通信していてもQUICコネクションを一つに束ねることはできない。その観点では、輻輳制御コントローラを共有するWebTransport over HTTP/3の方がメリットがあります。

Connection mobility

QUICはコネクションマイグレーションという機能がありますが、実装依存です。

asnokaze.hatenablog.com

接続の開始

WebTransport over QUICでは、draft-00ではALPNとして"wq"を使っていましたが、draft-01からは、individual draft版を示す "wq-vvv-01"を使用します。クロスプロトコル攻撃もなくなります。

サーバからのTLS Finishedのメッセージが受信されると、WebTransport over QUICのコネクションが確立されます。

Client Indication

draft-01からClient Indicationという仕組みが導入される予定です。

これは、下記Issueでも述べられている通りに、どのWebページ(オリジン)によって開始されたWebTransport over QUICなのか識別できるようにするためのものです。
github.com

https://example.comからwq.example.com:4433に対して接続を開始した場合、接続を開始したオリジンとしてhttps://example.com:443がWebTransport over QUICサーバに通知されます。こすることで第三者のWebページにWebTransport over QUICサーバを勝手に利用されるということはなくなります。

Client Indication自体は、クライアントからサーバに対してメタ的な情報を通知する汎用的な仕組みです。クライアントは、コネクションが確立した直後に、ストリームID:2 (単方向ストリーム)で接続を開始したオリジンを通知するClient Indicationメッセージを投げます。サーバはこのメッセージが来るまで待ち状態となります(正確にはストリームID:2がクローズするまで待ち状態)。

Client Indicationメッセージは下記のフォーマットになります。可変長Valueを持つ、Key-Value型のメッセージ形式です。
f:id:ASnoKaze:20191010013538p:plain

このうち、Keyとして0x0000を持つのがOrigin通知用のClient Indicationです。対応していないKey番号を持つClient Indicationは無視されます。

データのやり取り

コネクションが確立され、Client Indicationが完了したあと、各ストリーム上でクライアントとサーバでやりとりが開始されます。

Fallback

余談ですが、WebTransport over QUICのプロトコル自体にはコネクションの確立に失敗しても、WebTransport over TCPhのフォールバックの機能はありません。WebTransportのフレームワークが、WebSocketベースのポリフィルが提供されるらしいので、アプリケーションがそちらに切り替えを行う形になるのかとおもいます。

ChromeのHTTP/3(ドラフト版)対応

Chrome CanaryがHTTP/3のドラフト版に対応していたので、簡単に見てみる
(登場するバージョンについての説明は、後述する。)

動作確認

デフォルトでは喋ってくれない。chrome canaryの起動オプションに下記を追加する。

--enable-quic --quic-version=h3-23

(起動コマンドがわからない場合は、chrome://version/ を開くと確認できる)

HTTP/3 draft-23に対応しているページを開きます。
https:///quic.aiortc.org:4433

ページは正しく表示され、デベロッパーツール上はhttp/2+quic/99と表示されますが、HTTP/3 draft-23で通信は行われています。
f:id:ASnoKaze:20190921230316p:plain

Wireshark で、QUICトランスポートのバージョンを見てみるとちゃんとIETF QUICのバージョンを示す 0xFF00000017 (draft-23) で通信していることを確認できます。
f:id:ASnoKaze:20190921230528p:plain

バージョンについては、順に説明していく。

gQUIC, iQUICのバージョン

もともと、"QUIC"はGoogleが考案・実装していたプロトコルですが、標準化するに当たりIETFに持ち込まれました。そして、IETFではQUICはトランスポートプロトコルとして設計され、その上にHTTP/3がのっかる形となりました。(Google QUICはHTTPレイヤも含むプロトコルでした)

この過程で、もともとのGoogle QUICとIETF QUICは別物となり、それぞれ互換性の無いプロトコルとなりました。Google版のQUICをgQUIC, IETF版のQUICをiQUICと呼び分けることもあります。

もちろん、HTTP/3ではiQUICを前提にしているため、HTTP/3に対応するためにはiQUICに対応する必要があります。

Chromeでは標準仕様であるiQUICへの移行を段階的に進めており、HTTP/3 draft-23 が実験的に喋れる状況となっています。

識別子とalt-svc

iQUIC

HTTP/3では、サーバがHTTP/3に対応している旨、alt-svcレスポンスヘッダを用いて通知します。

下記例では、HTTP/3の仕様 draft-23バージョンを 4433ポートで提供していることを示しています。このレスポンスヘッダを受け取ったクライアントは、自身がそのバージョンに対応していれば、そこに繋がきに行きます。

alt-svc: h3-23=":4433"; ma=3600

IETF版、QUICではHTTP/3の識別子として、 h3- という形で通知を行います。もし、RFCとして正式なものが公開されれば、はなくなり、h3という識別子を用いることになります。
(ここでは、HTTP/3のバージョンにのみ言及しており、トランスポートとしてのQUICについては別途コネクション時にネゴシエーションされる点に注意。QUICトランスポートのハンドシェイクでは draft版は 0xff0000 を使用します。)

gQUIC

gQUICでも同様にalt-svcを使用しますが、バージョン体系が異なっています

Googleのサーバは下記のようなalt-svcを返します。gQUICは、HTTPレイヤも含むプロトコルですので識別子としてquicを使用しています。vという値を用いて、対応バージョンとして、46,43,39に対応していることを示しています。

alt-svc: quic=":443"; ma=2592000; v="46,43,39"

識別子も、バージョン体系もiQUICとgQUICは異なっています。

先述の通り、gQUICはiQUICへの移行を目指しております。GoogleのQUICライブラリに、その様子が伺えます
https://quiche.googlesource.com/quiche/+/refs/heads/master/quic/core/quic_versions.h

  QUIC_VERSION_46 = 46,  // Use IETF draft-17 header format with demultiplexing bit.
  QUIC_VERSION_47 = 47,  // Allow variable-length QUIC connection IDs.
  QUIC_VERSION_48 = 48,  // Use CRYPTO frames for the handshake.
  QUIC_VERSION_99 = 99,  // Dumping ground for IETF QUIC changes which are not yet ready for production.

gQUICでも暗号経路を確立するために暗号ハンドシェイクを行いますが、iQUICと同様のTLS1.3を用いたハンドシェイクの場合は、iQUICのバージョン同様 0xff0000を使うことがわかります(そうでない場合は、QUIC 99となる。)
https://quiche.googlesource.com/quiche.git/+/c8d9e40cd42e73b643e36300c807865b2ec8787d/quic/core/quic_versions.cc#115

    case QUIC_VERSION_99:
      if (parsed_version.handshake_protocol == PROTOCOL_TLS1_3) {
        return MakeVersionLabel(0xff, 0x00, 0x00, kQuicIetfDraftVersion);
      }
      return MakeVersionLabel(proto, '0', '9', '9');

ということで、実装的にはIETF QUIC版も対応していそうということがわかります。

QUICのコネクションマイグレーションについて

目次

概要

QUICはUDPを使っており、QUICレイヤにコネクションを管理するためのコネクションIDを持ちます。そのため、送信元IP・ポート番号、送信先IP・ポート番号が変わっても、コネクションが切断することなく通信を続けることができます。これをコネクションマイグレーションと呼びます(QUIC-TRANSPORTの仕様)。

もちろん、QUICをトランスポートプロトコルとして使用するHTTP/3でもこの恩恵に預かります。

GoogleではすでにQUICのコネクションマイグレーション機能を実験しており、ネットワーク起因のエラーの減少に一定の効果が得られているようです(IETF104での発表資料PDF)

コネクションのマイグレーションには主に2つあります

  • キャリア回線からWi-Fi回線への移行など、エンドポイントが意図して使用するネットワークを変更するケース
  • NATリバインディングなど、エンドポイントが意図せずポート番号などが変わるケース

前者は事前に移行先のネットワークが利用できることを検証してから切り替えますが、後者はそのようにはいきません。

いずれにせよ、このコネクションのマイグレーションは安全に行われる必要があります。通信を行っている二者は鍵を共有しており、コネクションのマイグレーションが起きたとしても、鍵を知らない第三者が通信に割り込んで解読するようなことはできません。

その他にも以下のような攻撃も対策されています。

  • アンプ攻撃
  • コネクションのトラッキング
  • 攻撃者自身を経由するようなPATHの構築

セキュリティ対策

アンプ攻撃

通信経路上の第三者がパケットの送信元IPを書き換え、エンドポイントの意図しないマイグレーションが発生したことを装います。そうすることで、送信元IPが書き換えられたパケットに対する応答として、そのIPアドレスにデータが送信されます。ここで、大量のデータが流れればアンプ攻撃となります。

QUICでは、マイグレーション時に関わらず、通信相手がそのIPアドレスの所持証明をできない場合は、送信できるデータ量が制限されます。コネクションのマイグレーション時も、後述するPath Validationを行い、そのIPアドレスが正しい通信相手であることを確認します。

コネクションのトラッキング

QUICでは、コネクションIDを識別子として用います。このコネクションIDはQUICパケットヘッダに平文で格納されます。

そのため経路上の第三者は、このコネクションIDを観測することができます。

もし、コネクションマイグレーション前後で同じコネクションIDを使用している場合は、通信者がどこからどこへ移動してるか観測者はトラッキングできます。会社のWi-Fi -> LTE ->自宅のWi-Fi というふうに、どういうふうに移動したかも追うことができます。

そのため、QUICでは事前に暗号化して送信されるNEW_CONNECTION_IDフレームにて、予備のコネクションIDを相手に通知しておきます。意図してコネクションのマイグレーションを行う場合は、この呼びのコネクションIDを用いることで第三者がコネクションマイグレーション前後の通信を関連付けられないようになっています。(お互い、新しい送信先コネクションIDを使用します)

もちろんNATリバインディングなど、意図せず起こるコネクションマイグレーション時は新しいコネクションIDを使用できませんが、時間をあけてから通信を行う場合は予防的に新しいコネクションIDを仕様することが推奨されています。

予備のコネクションIDを消費した場合は、NEW_CONNECTION_IDで相手から新しく予備のコネクションIDをもらえます。

攻撃者自身を経由するようなPATHの構築

パケットを観測できる通信経路外の攻撃者は、パケットをコピーし、本来のパケットが到着するよりも先に攻撃者のパケットを届けることで、攻撃者自身を経由する経路へとコネクションマイグレーションさせることができます。

この場合、攻撃者は届いたパケットを正しく二者に配送することでPath Validationは成功します。しかし、仮に成功したとしても、マイグレーション前の経路の方が高速であれば、もとの経路に戻すことも選択できるしょう。また、コピーされたパケットが重複して届くことに対して、ヒューリスティックな対応も考えられます。

コネクションマイグレーションの手順

QUICのハンドシェイクが終わったあとであれば、コネクションマイグレーションを行うことができます。ただし、トランスポートパラーメタとしてdisable_active_migrationが設定されている場合は除きます。

エンドポイントが意図して能動的に行うコネクションマイグレーションの場合で流れを説明をします(共通部分もあります)。

事前にコネクションマイグレーションをする前に、マイグレーション先のPATHで正しく通信できるか検証を行います。これをPath Validationと呼びます。

Path validation

8バイトのランダムな値を含むPATH_CHALLENGEフレームを送信します。このPATH_CHALLENGEフレームを受け取ったエンドポイントは、同じ値を持つPATH_RESPONSEフレームでお繰り返します。

f:id:ASnoKaze:20190916013416p:plain

このやり取りも、二者間で共有されている鍵を使っているため、第三者が正しく応答することはできません。

なお、PATH_CHALLENGEフレームとともにNEW_CONNECTION_IDフレームも送ることで、相手も必ず新しいPATHで新しいコネクションIDが使用できます。

もしタイムアウトした場合はPath validationは失敗となり、もともと使用していたPATHを使用します。

コネクションマイグレーションの開始

PATH_CHALLENGE、 PATH_RESPONSE、 NEW_CONNECTION_ID、 PADDINGフレームはプロービングレームと呼ばれ、実際の利用するコネクションのマイグレーションはまだ引き起こしません。

それ以外のフレーム(非プロービングフレーム)を新しいPATHで送信することで、コネクションのマイグレーションが行われ、新しいPATHでデータを送信し始めます。

コネクションマイグレーションへの対応

一方、通信相手の方は新しいアドレスから非プロービングフレームが届いたときに、相手がコネクションをマイグレーションしたことを知ります。

これを契機に、新しいPATHで応答を開始し、こちらからもPath Validationを行います。こちらでも相手がそのIPアドレスを所持している正しい通信相手であることを確認する必要があります。

f:id:ASnoKaze:20190916020450p:plain

NATリバインディングのように、エンドポイントが意図せずIPやポートが変わった場合は、事前の手続きはなくこの処理が行われます。

コネクションのマイグレーションが終わると、エンドポイントは古いPATHを破棄できます。

輻輳制御とECN

新しいPATHでは、RTTや帯域が異なるため、輻輳コントローラを初期値にセットしなければなりません。

また、ECNをサポートしているかもわからないため、その確認作業も再度行う必要があります。