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

2020/060/01 追記
nginx公式版が提供されました。こちらを御覧ください
asnokaze.hatenablog.com


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 について

2020/03/11 追記
関連記事を書きました


目次

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

URI Scheme

WebTransport over QUICでは、quic-transport用のURIが新しく定義されています。

通常のURL同様以下のような構造を持ちます。

quic-transport://host:port/path

クライアントは上記のURLに対して接続を開始します。host名はSNIとして送信され、pathは後述のClient Indicationを用いてサーバに伝えられます。

接続の開始

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で、valueとしてオリジンが入ります。Keyが0x0001のものはPathを送信するのに使用されます。

対応していないKey番号を持つClient Indicationは無視されます。

データのやり取り

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

Fallback

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

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

追記(2019/12/24)
最新版ですと、h3-23ではなくh3-24が必要ですー

「The latest Chrome Canary supports h3-24 instead of h3-23」
https://groups.google.com/a/chromium.org/forum/#!topic/proto-quic/trZsbXM_2CM


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をサポートしているかもわからないため、その確認作業も再度行う必要があります。

HTTPリクエストのレートリミットを示すRateLimitレスポンスヘッダの提案仕様

一定時間内でWeb APIを叩く回数などが制限されているサービス等は多々あります。制限に引っかかった場合は429 Too Many Requests レスポンスが返されます。

このとき、レスポンスヘッダやボディでリミットに関する情報が提供される場合がありますが、標準化された方法はありません。

そこで、先日提出された提案仕様「RateLimit Header Fields for HTTP」では、HTTPレスポンスでレートリミットに関する情報を通知するヘッダを定義します。

  • RateLimit-Limit: 単位時間内のリクエスト上限
  • RateLimit-Remaining: 現在の単位時間内の残りリクエスト回数
  • RateLimit-Reset: 上限がリセットされるまでの期間(秒)、もしくはタイムスタンプ

例1

今の上限は100で、残り20、リセットされるまで56秒です。

RateLimit-Limit: 100
RateLimit-Remaining: 20
RateLimit-Reset: 56
例2

RateLimit-Limitヘッダには","区切りで複数のウィンドを指定することができます。
以下の例では、1時間ごとに3600, 1日で5000リクエストの制限があることを示しています。

次のシチュエーションは次のとおりです。最初の14時間で4900リクエストを消費してしまったので、今の制限はトータル5000リクエストの制限状態で有ることを示しています。
そして、上限がリセットされるのは10時間後となっています。

RateLimit-Limit: 5000, 1000;window=3600, 5000;window=86400
RateLimit-Remaining: 100
RateLimit-Reset: 36000
例3

RateLimit-Resetは、IMF-fixdate形式でも指定できます

RateLimit-Reset: Tue, 15 Nov 1994 08:12:31 GMT

Mixed Content Level 2の仕様について

2年前ほど前に「Mixed Content Level 2の議論」に書いたとおり、Mixed Content Level 2についての議論は以前から行われていました。

2019/08/22の日付で、w3cリポジトリにEditor’s Draftとして「Mixed Content Level 2」のページが加わっております。
w3c.github.io

まだ、TODOのものもありますがアルゴリズム部分については書かれています。

Mixed Content Level 1では表示できていた Mixed Content な画像,音声,動画もブロックされる可能性があります。

Mixed Content Level 2

  • http://~なoptionally-blockable content(画像,音声,動画)は、自動でhttpsのURLとしてアクセスされます
  • httpsでアクセスに失敗した場合はブロックされます。

これによって、すべてのMixed Contentはブロックされることになり、CSPのblock-all-mixed-contentは廃止されます。

影響

chromeでは音声と動画を対象に、beta版の50%でこの機能を試しているようです。

httpsへの自動アップグレードにより、1%ほどのページ表示でリソースの読み込みの失敗を観測しているようです。

今後

今月開催されるTPAC 2019で議論される予定となっています
github.com

Double-keyed HTTP cache に関するメモ

201909027追記

Fetchの仕様にプルリクが出されています
HTTP cache partitioning by shivanigithub · Pull Request #943 · whatwg/fetch · GitHub


whatwgでfetchに関して「Double-keyed HTTP cache」という議論がされています。

github.com

ブラウザ側でも動きがあり、下記で議論がされています

背景

HTTPのキャッシュは、そのリソースがどのページ(ドメイン)で読み込まれたかに関わらずに共有で使用されます。しかし、そのキャッシュ状況をサイドチャネル攻撃で調べることによって、特定のリソースが別のページによってロードされているか確認することができます。

この方法を用いて、ユーザの検索履歴や連絡先情報などが取得可能であることを示した「Mass XS-Search using Cache Attack」という例もあります。

この問題への対策がDouble-keyed HTTP cacheです。

Double-keyed HTTP cache

Double-keyed HTTP cacheでは、ページを開いた際のアドレスバーに表示されているオリジンをキャッシュのキーとして使用します。

https:///a.example.comで読み込んだリソースのキャッシュは、https://b.example.comでは使用できなくなります。

これによって、そのリソースがキャッシュされているかはクロスドメインでは確認できなくなります。

Googleの調査ではChromeの開発版での統計データでは、キャッシュのヒット率が4%ほど低下し、キャッシュから読み込まれるデータ量は39.1%から37.8%に低下したと述べています。first contentful paintは大きく変わらなかったとしています。

また、クロスオリジンでキャッシュに読み込む機能のある、クロスオリジンのprefetch、HTTP/2サーバプッシュ(特にキャンセルによってキャッシュの有無が把握可能)については検討が必要そうです(関連資料)。

その他にもCDNへの影響など、議論は引き続き行われそうです。