WebTransport over QUICのサンプルサーバを試してみる

"WebTransport over QUIC"の標準化及び、Chromiumでの実装が進んでいます。

今回はクライントを書いて、Chromiumに同梱されているquic_transport_simple_serverと通信してみました。環境は Ubuntu 18.04です。

WebTransport over QUICについては以前書いたブログを参照してください
asnokaze.hatenablog.com

quic_transport_simple_server のビルド

Chromiumのビルド手順通り勧めます (Google社員向けはスルーしましょう)
https://chromium.googlesource.com/chromium/src/+/master/docs/linux/build_instructions.md

  • Install
  • Get the code
    • Install additional build dependencies
    • Run the hooks
  • Setting up the build

フェッチに時間がかかります。
最後にビルドする際に、chromeのビルドではなく、サンプルサーバのみをビルドします

autoninja -C out/Default chrome #この部分を
ninja -C out/Default quic_transport_simple_server #こうする

証明書・鍵の作成

サーバを起動するのにPKCS#8形式の鍵が必要です

openssl req -new -key server.key > server.csr
openssl req -new -key server.key > server.csr
openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt
openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in server.key -out server.p8

起動

ビルドができていれば、実行ファイルがありますので実行します

~/chromium/src/out/Default$ ./quic_transport_simple_server \
    --accepted_origins="" \
    --key_file="/path_to/server.p8" \
    --certificate_file="/path_to//server.crt" \
    --allow_unknown_root_cert --port="10000" 
  • --accepted_origins: client indicationで受け付けるオリジン名(空を指定)
  • --key_file: 鍵
  • --certificate_file: 証明書
  • --port: ポート

クライアントサイド

WebTransport over QUICでは、QUIC DATAGRAMフレームのサポートが必須です。実際にはトランスポートパラメータでmax_datagram_frame_sizeを指定する必要があります。

今回はDATAGRAMフレームをサポートしてる、PythonのQUICライブラリ「aioquic」を利用します。
(exampleとして、DATAGRAM検証用プロトコル"siduck"実装が含まれてるのも選んだ理由です)

インストール方法は「aioquicのgithubリポジトリ」を参照。

./examples/siduck_client.py を雑にいじって、WebTransport over QUIC クライアントを作成します。
ソースはこちら https://gist.github.com/flano-yuki/3ab550393138066b19d2ff4d44e0cb47

~/work/aioquic$ python3 examples/quic_transport.py  localhost 10000 -k -q out -l ./ssllog
2020-01-13 21:04:51,881 INFO quic [229cca53a4662860] ALPN negotiated protocol wq-vvv-01
2020-01-13 21:04:51,882 INFO client Sent Client Indication
2020-01-13 21:04:51,883 INFO client Say Hello
2020-01-13 21:04:51,885 INFO client Echo is received: 
2020-01-13 21:04:51,885 INFO client b'hello world'

(-k はデバックログをファイルに出力、-lは復号用の鍵ファイルをファイルに出力)

quic_transport_simple_serverにデータを送って、Echoを受信することに成功しました。

修正ポイント

ポイントは

  • ALPNとして"wq-vvv-01"を指定する
  • Client Indicationを送信する(OriginとPATHをそれぞれ指定)
    • PATHとして/echoを指定すると、サーバはechoモードで動作します (ソース)
        b  = b'\x00\x00' + b'\x00\x17' + b'https://example.com:443'
        b += b'\x00\x01' + b'\x00\x05' + b'/echo'
        self._quic.send_stream_data(2, b, True)
  • クライアントからデータが送れるストリームでデータを送り、Echoされたデータを受信する
  • (いくつか試したが、DATAGARAMフレームにはEchoを返してくれない...? ソース的には未実装?)

終わりに

とりあえず、通信できるところまでうまく行ったので、ちゃんとクライアントを実装したい。quic-go もDATAGARAMフレーム実装が進んでるので、そっちでも試したい (サーバ・クライアント両方)

ダウンロードもMixed Contentsの対象となる話し

Chrome 81のデスクトップ版から、ダウンロードもMixed Contentsでブロック対象になる話があります。
これにより、https:// のページから http://でリンクされたファイルをダウロードしようとするとブラウザによってブロックされます。
chromestatus.com

詳細は「Downloads as Active Mixed Content」に書かれていますが、その他のMixed Contents同様何も表示することなくブロックされます (デベロッパーツールには表示される)。

その挙動について簡単に確認してみます。
(本日時点のChrome Canary 81.0.4023.0 で動作確認するため、挙動は変更される可能性があります)

動作確認

https://asnokaze.com/download.html にダウンロード用リンクがあります。

zip

ドキュメントによるとファイルは拡張子で判断し、実行可能ファイルなどリスクの高いものをブロックすると書いてあります。

f:id:ASnoKaze:20200111175533p:plain

https://なページから、http://なリンクをクリックしてzipファイルをダウンロードしようとするとブロックされます。見た目上は何も起こりません。
同様に、右クリックにて"名前をつけてリンク先を保存"しようとしても何も起こりません。

デベロッパーツールには以下のように、ブロックした旨ログが出ます。

Mixed Content: The site at 'https://asnokaze.com/' was loaded over a secure connection, but the file at 'http://asnokaze.com/test.zip' was redirected through an insecure connection. This file should be served over HTTPS.

もちろん、Chrome 80ですとちゃんとダウンロードできます。

画像

aリンクにdownload属性を付与した場合の動作も確認します。

<a href="http://asnokaze.com/test.png" download> http://asnokaze.com/test.png</a><br>

Mixed Contentsの場合は、新しいタブが開きダウンロードは開始されません。
もちろん、https://のリンクの場合はダウンロードが開始されます。

また、imgタグで表示されているものに関しては、右クリックでの「画像を保存」はMixed Contentsでも通常通り行えます。

HTTPメッセージに署名をするSignatureヘッダの標準化

HTTPメッセージに署名をするSignatureヘッダを定義する「Signing HTTP Messages」という仕様が提案されています。

HTTPメッセージへの署名は、様々なところで行われていますが、それぞれ独自の方式で行われています。例えば、AWS APIを叩く際に利用する「Signature Version 4 Signing」や、OAuth2.0 DPoPなどでHTTPメッセージへの署名が行われています。

その他にも、WebPackagingで使用される「Signed HTTP Exchanges」といったところでも署名が行われています。

一方でTLSではだめなのかという話もありますが、TLSを用いることでHTTPメッセージの機密性および完全性は保護されますが、TLS終端プロキシなどを経由するとそれ移行の部分で通信の完全性は担保されません。クライアントとサーバ間でHTTPメッセージの完全性を保つために、HTTPメッセージに署名する方法が必要です。

HTTPはProxyなどを通ると、viaヘッダやX-Forwarded-Forヘッダが足されたり、順序が入れわかったり、同名のヘッダが結合されたりされます。そのような場合でもちゃんと署名および検証をできるようにする標準的な署名方式を定義するのが「Signing HTTP Messages」です。

もともとは、Mark Cavage氏によって2013年頃より標準化が進められていましたが、昨年12月にその仕様を基に改めてAnnabelle Backman氏やJustin Richer氏のもと再出発となっています。同名の仕様ですので注意が必要です。背景については、[IETF106の資料]および、[MLの議論]をご覧ください

Signing HTTP Messages

Signing HTTP Messages」の仕様は主に3つの部分に別れます

  • HTTPメッセージの正規化
  • HTTPメッセージの署名および検証
  • 署名のメタデータを付与する方法

今回は正規化方法を説明し、残りふたつは合わせて説明します。

HTTPメッセージの正規化

Proxyによって、HTTPヘッダは順番が入れ替わったり、同名のヘッダが結合されたりします。

その他にも実装によって、大文字小文字の変換、HTTP/1.1とHTTP/2での変換、obs-fold(行頭空白を用いた改行)の追加・削除、などさまざまな変形が行われる可能性があります。

そのため、正規化処理を行います。

例として、obs-foldや同名のヘッダを持つ下記のHTTPレスポンスは

   HTTP/1.1 200 OK
   Server: www.example.com
   Date: Tue, 07 Jun 2014 20:51:35 GMT
   X-OWS-Header:   Leading and trailing whitespace.
   X-Obs-Fold-Header: Obsolete
       line folding.
   X-Empty-Header:
   Cache-Control: max-age=60
   Cache-Control:    must-revalidate

次のようにソート、正規化されます。
f:id:ASnoKaze:20200107005553p:plain

また、HTTPリクエストの場合はメソッドやパスも署名対象であり、リクエストターゲットも正規化されます。その例は次のとおりです。
f:id:ASnoKaze:20200107005836p:plain

HTTPボディはどうするのかというと、digestヘッダを用いてボディのダイジェストをHTTPヘッダに組み入れることでボディも合わせて署名対象に入れることができます。

Signatureヘッダ

Signatureヘッダ

   Signature: keyId="test-key-b", algorithm="rsa-sha256",
       created=1402170695, expires=1402170995,
       headers="(request-target) (created) host date cache-control
           x-emptyheader x-example",
       signature="T1l3tWH2cSP31nfuvc3nVaHQ6IAu9YLEXg2pCeEOJETXnlWbgKtBTa
           XV6LNQWtf4O42V2DZwDZbmVZ8xW3TFW80RrfrY0+fyjD4OLN7/zV6L6d2v7uB
           puWZ8QzKuHYFaRNVXgFBXN3VJnsIOUjv20pqZMKO3phLCKX2/zQzJLCBQvF/5
           UKtnJiMp1ACNhG8LF0Q0FPWfe86YZBBxqrQr5WfjMu0LOO52ZAxi9KTWSlceJ
           2U361gDb7S5Deub8MaDrjUEpluphQeo8xyvHBoNXsqeax/WaHyRYOgaW6krxE
           GVaBQAfA2czYZhEA05Tb38ahq/gwDQ1bagd9rGnCHtAg=="

それぞれのパラメータの意味は次のとおりです

  • keyId: 検証で用いる鍵を示す識別子
  • algorithm: 署名アルゴリズム。デフォルトは”hs2019” (RSASSA-PSS and SHA-512)、 "rsa-sha256" が指定できる
  • created: 署名の作成時刻
  • expires: 失効時刻
  • headers: 署名の対象となるメタデータ及びヘッダを指定する
  • signature: 署名値

署名の対象はheadersで指定された、正規化されたメタデータ及びヘッダ値を改行(\n)で連結したものです。
例えばこんな感じのものが署名アルゴリズムのインプットになります

(request-target): get /foo
(created): 1402170695
host: example.org
date: Tue, 07 Jun 2014 20:51:35 GMT
cache-control: max-age=60, must-revalidate
x-emptyheader:
x-example: Example header with some whitespace.

検証する側は同様に、HTTPメッセージの正規化を行い署名値を計算することによって検証を行います。

2019年 asnokaze 振り返り

今年も大晦日になりました。例年通り振り返ります。

今年も全52記事と、たくさん書いた。一方でイベント類やアウトプットは少なかった気がする。来年頑張りたい。

また今年特に印象深いのが、「ブログ、読んでますよー」とお声がけいただく機会が何度かあった。本当に励みになるので感謝しかない。

イベント類

登壇や関わったイベントは多くありませんでした。

HTTP/3やQUIC、WebTransportなどでお話できることもあるとおもいますので、話者をお探しでしたらお声がけいただければお手伝いできればなーと思う所存です。(自分より適任者は多そうですが...)

一方で、少人数のクローズドなディスカッションを定期的に開催したりと、ちょっとは活動した。

アウトプット

t2q2t (記事参照)を書いたりしたが、手を動かす系は相変わらず弱かった。

プロトコルのIssue立てたりして直してもらったりしたのは良かった。

記事

HTTP/3やQUIC関連の仕様をたくさん書いた。

記事のカテゴリ分布はこんな感じ(1記事に複数カテゴリつくので、合計は記事数と一致しない)
f:id:ASnoKaze:20191231220903p:plain

書いた順

HTTP/3をUDPロードバランサで分散するときの問題点 (AWS NLBで試してみた)

目次

はじめに

HTTP/3はQUICというトランスポートプロトコルを利用しています。QUICはUDPを利用していますが、QUIC自体はステートフルなプロトコルです。

ステートフルなQUICを、QUICを解釈しないUDPロードバランサでバランシングしようとするにはいくつかの注意問題点があります。今回は簡単に説明し、NLBでも実験をしてみました。


QUICの用語などは以前書いた記事を参照
asnokaze.hatenablog.com

UDPロードバランサ

QUICはステートフルですので、おなじQUICコネクションのUDPパケットは同じサーバに割り振ってやる必要があります

ロードバランサの分散アルゴリズムにはいくつかありますが、下記をとりあげます

  • ラウンドロビン: 来たパケットを順々に各サーバに振り分けます
  • ハッシュ: 送信元IP, 送信元ポート, 送信先IP, 送信先ポート などをもとにハッシュを計算し、振り分け先を決定します

ラウンドロビン方式

ステートレスなUDPパケットを順々に各サーバに振り分けます。
QUICコネクションを処理するサーバが変わってしまうため、ハンドシェイクすら成功しないでしょう。

ハッシュ方式

送信元IP, 送信元ポート が一緒であれば、同じサーバに振り分けられるため、一見うまくいくように見えます。

しかし、いくつかの注意点があります

ハッシュの再計算

1つめはハッシュの再計算が起こる点です。
UDPロードバランサは、特定のタイミングでハッシュの再計算が発生しえます。例えば、振り分け先のサーバが増減した際や、設定を更新した際に振り分け表がクリアされるロードバランサもあります(*)

ハッシュの計算が変わった場合、既存の通信の振り分け先が変わってしまいます。運良く再計算する前と同じサーバに振り分けられればいいですが、そうでなければStateless resetとなります(QUICパケットは暗号化する必要があり、鍵を失った際の切断方法)。実際は振り分け先の台数に依存しますが、現在扱っている接続のうち多くの接続が切断されることになります。

f:id:ASnoKaze:20191230200921p:plain

コネクションマイグレーションが出来ない

2つめはコネクションマイグレーションが出来ない点です。QUICのコネクションマイグレーションは以前書いたとおりです。
asnokaze.hatenablog.com

QUICでは送信元IPや送信元ポートが変わっても通信をそのまま継続できます。そのため、クライアントはそれらが変わったとしてもそのまま通信を継続しようとします。しかし、送信元IPや送信元ポートが変わるとUDPロードバランサによって振り分けられるサーバが変わるため、コネクションを維持できずStateless resetを行うことになります。

クライアントが開始するコネクションマイグレーションはPath validationの失敗するだけで通信は維持できますが、NATリバインディングなどの中間装置によって送信元ポートが変わる場合は先述の通りStateless resetとなります。(追記) NATの変換テーブルから消えてる場合ですので、通信がない時間がある場合に起こります。再接続で問題ないケースが多いでしょう。PINGフレームを送ることで回避もできそうです。

その他の懸念事項

その他にもいくつかの懸念事項があります

例えばUDPロードバランサは、コネクションクローズタイミングが分かりません。QUICはコネクションをクローズしたつもりはないのに、UDPロードバランサが割当表からエントリを先に削除してしまうと、別のサーバに割り振られてしまいます。これはPINGフレームを定期的に送ることで回避できます。

NLBを用いたハッシュの再計算の実験

AWS NLBでUDPロードバランサはハッシュを用いて振り分けを行っています(Docs)、ハッシュの再計算が発生するか確認します。

今回は簡易的に、HTTP/3ではなく単純にUDPを使用して実験を行います。

  • client: UDPで指定されたNLBに定期的にメッセージを送ります。(今回は送信元ポートを5つ用意し、並列にメッセージを投げる)
  • server: UDPでリッスンし、UDPパケットが届くと、自身のホスト名を返します。

(コード: https://gist.github.com/flano-yuki/4e70d4d38da87018d4ed8c035d27b765


今回はEIP付きのNLB(リスナープロトコルUDP)で、5つのインスタンスをターゲットグループに登録します (手抜きで、全部unhealtyで均等に処理させる)

  • ip-10-xxx-yyy-232
  • ip-10-xxx-yyy-181
  • ip-10-xxx-yyy-225
  • ip-10-xxx-yyy-74
  • ip-10-xxx-yyy-119

clientを実行すると、5つの送信元ポートでメッセージを送信し受信するのを、数秒おきに行います。5つの送信元ポートは同じ順序で使うため、縦に同じホストが表示されます。

しかし、インスタンスの増減を行ったタイミングで、ハッシュ計算が代わり振り分け先ホストが変わるのが確認できます。

$ cat ./client.rb 
ip-10-xxx-yyy-225 ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-181 
ip-10-xxx-yyy-225 ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-181 
ip-10-xxx-yyy-225 ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-181 
ip-10-xxx-yyy-225 ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-181 
ip-10-xxx-yyy-225 ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-181 
(timeout)               (timeout)               (timeout)              ip-10-xxx-yyy-74  (timeout)          
ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-74 ip-10-xxx-yyy-74 
ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-74 ip-10-xxx-yyy-74 

ですので、HTTP/3 (QUIC)をNLBで負荷分散するのは適切とは言えません。

QUICの負荷分散について

QUICのロードバランシングについてはIETFでも議論が行われています。

基本的にはQUICを解釈してロードバランスを行います。
QUICのコネクションIDにサーバの識別子を暗号化して入れておき、それを見て適切なサーバに振り分けるなどの方法が考えられています。

その他にも、間違ったサーバに振り分けられた際に、自分宛てでないパケットを適切なサーバに転送するなども考えられますが、構成が複雑になってしまいそうですね。

ということで、通常はHTTP/3を終端しバックエンドにはHTTP/1.1で振り分けとかを行う構成が普通かなとは思います。(ゆくゆくはマネージドロードバランサも対応してくるでしょう)

QUIC-LBの提案仕様や、コネクションID-awareなNATに関するドキュメントを読むとより深く理解が出来ると思います(読めてない)


(*): 一般的なUDPロードバランサすべてに当てはまるわけではないので、表現を変更しました。
以前「UDPロードバランサは、特定のタイミングでハッシュの再計算を行います。例えば、振り分け先のサーバが増減した際や、設定を更新した際にハッシュの再計算が起こります。」
以後「UDPロードバランサは、特定のタイミングでハッシュの再計算が発生するものもあります。例えば、振り分け先のサーバが増減した際や、設定を更新した際に振り分け表がクリアされるロードバランサもあります(*)」

Crash/Deprecation/Intervention Reporting について

目次

ブラウザがWebページを表示する際に発生したエラーを、任意のエンドポイントにレポートさせる「Reporting API」という仕様があります。今回紹介する機能もchromeでいくつか実際に使用できます。

例えば、以前紹介した Deprecation Reporting もこの仕様によって定義されていました。
asnokaze.hatenablog.com

他にもTLS証明書エラーや名前解決エラーといった、ネットワークエラーを通知するNetwork Error Loggingといった仕様もReporting APIを利用しています。
asnokaze.hatenablog.com

Crash/Deprecation/Intervention Reporting

今までCrash/Deprecation/Interventionという3つのエラータイプが、「Reporting API」のドキュメントで定義されていましたが、議論の結果それぞれ独立したドキュメントに分離されることになりました。

背景は以下を参照
discourse.wicg.io

分離されたドキュメントはそれぞれ下記のとおりです。

レスポンスヘッダで下記のように設定することで、そのドメインでエラーなどが発生した場合に、指定したエンドポイントにレポートが送信されます。

Report-To: { "group": "default",
             "max_age": 10886400,
             "endpoints": [
               { "url": "https://example.com/reports", "priority": 1 },
               { "url": "https://backup.com/reports", "priority": 2 }
             ] }

前述のブログでも紹介しましたが、改めてそれぞれ簡単に紹介ようかとおもいます。

Crash Reporting

サイトを閲覧したブラウザがクラッシュした際に、サイト所有者に通知する機能。

下記のデータがPOSTされます

[{
  "type": "crash",
  "age": 42,
  "url": "https://example.com/",
  "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0",
  "body": {
    "reason": "oom"
  }
}]

reasonは現状下記の2つが定義されています

  • oom: メモリ不足
  • unresponsive: 反応がない

Deprecation Reporting

サイトを閲覧したブラウザがdeprecatedな機能(JavaScript API)を利用していた場合に、サイト所有者に通知する機能。

例えば、「Application Cache API」や「window.webkitStorageInfo」といったものをChromeで使ってるとDeprecation Reportingが飛びます

下記のデータがPOSTされます

    [{
      "type": "deprecation",
      "age": 32,
      "url": "https://example.com/",
      "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0",
      "body": {
        "id": "websql",
        "anticipatedRemoval": "2020-01-01",
        "message": "WebSQL is deprecated and will be removed in Chrome 97 around January 2020",
        "sourceFile": "https://example.com/index.js",
        "lineNumber": 1234,
        "columnNumber": 42
      }
    }]
}

Intervention Reporting

サイトを閲覧したブラウザがJavaScriptを実行した際など、セキュリティなどの理由によって動作が変更されたりブロックされた場合に、サイト所有者に通知する機能。

下記のデータがPOSTされます

    [{
      "type": "intervention",
      "age": 27,
      "url": "https://example.com/",
      "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0",
      "body": {
        "id": "audio-no-gesture",
        "message": "A request to play audio was blocked because it was not triggered by user activation (such as a click).",
        "sourceFile": "https://example.com/index.js",
        "lineNumber": 1234,
        "columnNumber": 42
      }
    }]
}

CookieのPriority属性の仕様

あまり知られていないがCookieにはPriority属性がある。

Chromeでは2013年に実装が入っている(該当コミット)、また提案仕様「A Retention Priority Attribute for HTTP Cookies」も2016年に提出されているが標準化には至ってない。

ここらへんの議論は当時の"メーリングリスト"を参照してください。

そんなPriority属性は、長年動きが無かったが、最近ChromeデベロッパーツールでPriority属性を表示するようになった( Chrome Canary 81)。
(JavaScriptからPriority属性を指定して、Cookieを設定した例)
f:id:ASnoKaze:20191219011406p:plain

超簡単にだが仕様を紹介する。

Priority Attribute

CookieのPriorityは保持優先度です。ブラウザが保存容量(ドメイン毎)の都合でCookieを消す場合はPriorityの低いCookieから消していきます。

値としては下記の3つです。それ以外の値が指定された場合はMediumとして扱われます。

  • High
  • Medium
  • Low

消す際は、LowとMediumごとにLデータ量・Mデータ量ふるい方から消されていきます。細かいところは実装依存になりそうです。

Priorityが指定された場合の、具体的な削除順番は仕様中の例を参照
https://tools.ietf.org/html/draft-west-cookie-priority-00#section-3.1