HTTP/3 DATAGRAMの優先度制御の提案仕様

HTTP/3 DATAGRAMの優先度制御方式を定義した「HTTP Datagram Prioritization」が提出されました。

これについて簡単に眺めていきます。

目次

背景: HTTP/3 DATAGRAMについて

下記の記事で書いた通り、HTTP/3の拡張仕様でパケロス時に再送を必要としないアプリケーションデータの送受信ができるようになっています。
asnokaze.hatenablog.com

特に、WebSocketの次世代版ともいわれるWebTransportや、HTTP/3コネクションをトンネルさせるMASQUEといった仕様で使われるものです。

このDATAGRAM拡張仕様では、優先度制御については具体的な方法を指定していません。

(なおDATAGRAMはHTTP/3のみに限定されず、HTTP/2でも使用されます)

補足: HTTP/3の優先度

HTTPでは、一つのコネクション上で複数のリクエストを並列的に送ります。そのときに、例えばWebページのレンダリングに必要なファイルの優先度をあげることで、結果としてWebページの表示を早くします。

通常のHTTPメッセージの送受信の優先度は、クライアントからサーバに対してレスポンスの優先度を通知します。

HTTPリクエストでpriorityヘッダで、u(urgency)で優先順位と、i(incremental)で並列処理出来るか示します。

   :method = GET
   :scheme = https
   :authority = example.net
   :path = /menu.png
   priority = u=5, i

また、通信中の優先度の変更はPRIORITY_UPDATEフレームを利用します。

この方法はHTTP/3以外でも利用できます。詳しくは以前書いた通り。
asnokaze.hatenablog.com

このHTTPの優先度制御では、DATAGRAM拡張の優先度制御については具体的な方法を指定していません。

DATAGRAM の優先度

本題です。「HTTP Datagram Prioritization」ではDATAGRAM通信の優先度制御方法を定義します。

HTTP/3のDATAGRAMは必ず関連付けられるHTTPリクエストがあります。そのリクエストで、通常のHTTPメッセージの優先度制御と同じように、DATAGRAMの優先度を制御します。意図的にシンプルな設計としているようです。

priorityヘッダでduを付けることで、DATAGRAMの優先度を指定できます。

   :method = GET
   :scheme = https
   :authority = example.net
   :path = /style.css
   priority = u=0, du=2

優先度の高いものから送信し、同じ優先度のものはラウンドロビンされます。

一つのHTTPリクエストに対して複数のDATAGRAMフローを指定できるのですが、各コンテキスト毎に優先度制御方式は指定しません。

また、優先度の更新は通常のHTTP優先度制御と同様、PRIORITY_UPDATEフレームで行います。

IETF 111の発表スライド

発表予定でしたが、時間が足りなかった模様。
内容はまとまってるので、おすすめ。
https://datatracker.ietf.org/meeting/111/materials/slides-111-masque-http-dgram-priorities-01

感想

WebTransportのServer initiatedなストリームとか、サーバ開始のDATAGRAMフレーム優先度制御とか、色々ユースケースに合わせて、柔軟性(複雑性)が上がっていきそうな気もする。

IETF 111で議論ができなかったため、MASQUE WGメーリングリストで引き続き議論。

TLS Encrypted ClientHello(ECH) を BoringSSLで試してみる

TLS Encrypted ClientHello(ECH) を BoringSSLで試してみます。

なお、現時点ではBoringSSLはdraft 10版の ECH 対応です。

TLS Encrypted ClientHello(ECH) について

TLSの通信のうち、クライアントからサーバに送信されるSNI(Server Name Indication)は、通信先のホスト名が記載されます。

これは平文で送信されるため、通信の観測者はどのドメインと通信しているか分かります。これを秘匿するために Encrypted SNI (ESNI)という仕様が議論されています。

今は、TLS Encrypted ClientHello(ECH)という形で標準化が進められており、Firefoxなどが既に対応しています。

例えばこのECHを使うと、クライアントは「public.example.com」と「private.example.org」をサーバに送りますが、private.example.orgの情報は暗号化されて送れます。CDNやマネージドロードバランサが対応し、外に見えるSNIはCloudflareやクラウドベンダーのドメインとなれば、見えるSNIから暗号化されたSNIを観測者が推測するのも難しくなるでしょう。
f:id:ASnoKaze:20210723222220p:plain

古いですがもう少し詳しくは昔書いた通り。
asnokaze.hatenablog.com

実装を触ってみる

すでに幾つかの実装があり、相互接続性のテストを行っております。
https://github.com/tlswg/draft-ietf-tls-esni/wiki/Implementations

今回はBoringSSLで通信の様子を見ていきます。

ビルドする

説明に基づいてビルドします
https://boringssl.googlesource.com/boringssl/+/HEAD/BUILDING.md

ECH設定の生成

generate-ech で ech_config_listを生成します。今回は public-nameとしてpublic.example.comを指定します。

$ ./build/tool/bssl generate-ech  -out-ech-config-list ech_config_list.data \
   -out-ech-config ech_config.data \
   -out-private-key ech.key \
   -public-name public.example.com \
   -config-id 0

この ech_configにはHpkePublicKeyやpublic_nameが格納されます。

一般的に、クライアントはこのech_configはDNS HTTPS レコードを介して取得します。(DNSの通信はDoHなどで暗号化しておけば、通信の観測者はどのドメインHTTPSレコードを取得している事もわかりません)

asnokaze.hatenablog.com

通信してみる

サーバを起動しておく

$ ./build/tool/bssl server -accept 4430 \
    -ech-key ech.key \ 
    -ech-config ech_config.data 

クライアント側でつなぐ

 $ ./build/tool/bssl client -connect localhost:4430 \
    -ech-config-list ech_config_list.data \ 
    -server-name private.example.org

Connecting to 127.0.0.1:4430
Connected.
  Version: TLSv1.3
  Resumed session: no
  Cipher: TLS_AES_128_GCM_SHA256
  ECDHE curve: X25519
  Signature algorithm: ecdsa_secp256r1_sha256
  Secure renegotiation: yes
  Extended master secret: yes
  Next protocol negotiated: 
  ALPN protocol: 
  OCSP staple: no
  SCT list: no
  Early data: no
  Encrypted ClientHello: yes
  Cert subject: C = US, O = BoringSSL
  Cert issuer: C = US, O = BoringSSL

サーバ側の出力

$ ./build/tool/bssl server -accept 4430 \
    -ech-key ech.key \
    -ech-config ech_config.data 

Connected.
  Version: TLSv1.3
  Resumed session: no
  Cipher: TLS_AES_128_GCM_SHA256
  ECDHE curve: X25519
  Secure renegotiation: yes
  Extended master secret: yes
  Next protocol negotiated: 
  ALPN protocol: 
  Client sent SNI: private.example.org
  Early data: no
  Encrypted ClientHello: yes

サーバ側からするとEncrypted ClientHelloがyesになっている。また、クライアントが送った本来のSNI private.example.org が認識できています。

また、crypto.cloudflare.com がECHに対応していますが、HTTPS DNSレコードからECH Config listを取得すれば、同様にEncrypted ClientHelloが通信できました。

パケットキャプチャしてみる

上記で行った通信をパケットキャプチャしてみます。

経路上の観測者としては、ESNIは「public.example.com」しかみえません。但し、暗号化されている Enclipted Clienthelloが格納されている encrypted_client_hello 拡張 (65034 == 0xFE0A (ECH Draft-10)) がついている事が分かります。

f:id:ASnoKaze:20210723224028p:plain

QUICやHTTP/3で利用を避けるべき送信元ポートの議論

[追記] yuyarin さんが詳しい考察を書いて頂きました!

とても詳しく書かれているので参考になります!ありがとうございます
yuyarin.hatenablog.com


IETFにおいて、QUICやHTTP/3の実装が利用を避けるべき送信元ポートについて議論になりました (URL)

具体的には、下記の送信元ポートは利用を避けるべきと述べられている。

  • port 53 - DNS
  • port 123 - NTP
  • port 1900 - SSDP
  • port 5353 - mDNS
  • port 11211 - memcached

これらのポートはUDPのリフレクション攻撃 (アンプ攻撃) に利用されるポートです。リフレクション攻撃のターゲットとなるサーバ側としては、これらのポートが送信元ポートに設定されたトラフィックを大量に受け付けることになります。

f:id:ASnoKaze:20210719234157p:plain

そのため、一部のインフラ環境ではこれらの送信元ポートをブロックするように設定されているかもしれません。

QUICやHTTP/3もUDPを使用します。運悪く送信元ポートとしてこれらのポートを使ってしまうと、通信がブロックされてしまうかもしれません。その結果としてタイムアウトまで待ちを発生させたり、HTTP/2へのフォールバックを引き起こしたりします。

利用されうる送信元ポート (エフェメラルポート)

正規利用のユーザは送信元ポートとしてこれらの値を使いうるのでしょうか。

一般的にアプリケーションは送信元ポートとして、エフェメラルポートを使用します。どうやら、IANAで"DYNAMIC AND/OR PRIVATE PORTS"として定義される49152~65535がエフェメラルポートとして使用されるようです。(Linuxでは32768~60999らしい)

エフェメラルポートとしてもっと小さい番号を設定している環境もあるにはあるようです。しかし、先に上げた避けるべきポートが使われる可能性は低いでしょう。

ただ、NATやCGNATでは1024~65535範囲が使われる可能性が示唆されています (RFC4787)

IETFの動向

QUICやHTTP/3では、これらの送信元ポートの利用を避けるようにという文言が加えられそうです。
github.com

また、避けるべきポート番号をRFCに直書きするのではなく、IANAで管理できないか意見が募られています。
mailarchive.ietf.org

まだ議論が行われている最中であり、今後何かしら進展があるものと思われる。


感想

(実際これらのポートが使われるケースどれくらいあるんだろう)

HTTP/3からのダウングレード攻撃を防ぐIncompatibleProtocol拡張の仕様

Mozilla のMartin Thomson氏が「Secure Negotiation of Incompatible Protocols in TLS」という仕様を提案しているので軽く目を通す。

これは、HTTP/3とHTTS/2ように、バージョンによって下位層に使うプロトコルが異なる場合でも、ダウングレード攻撃を防ぐことを目的としている。そのために、TLSに新しくIncompatibleProtocol拡張を定義する。

HTTPを例に説明をするが、ほかのアプリケーションプロトコルについても適応できる。
(なお、HTTPの場合はAlt-Svcの仕組みがあるため、その点で幾つかの攻撃は防ぐことが出来る)

背景

クライアントとサーバが通信するプロトコルのバージョンを下げさせる、ダウングレード攻撃という攻撃があります。

攻撃の方法としては、通信をブロックし低いバージョンのプロトコルを試行させたり、DNS SVCBレコードを介して高いバージョンのプロトコルをサポートしている事を隠蔽する方法が考えられる。

TLSとQUICのALPN

QUICやTLSでは、ハンドシェイク中にALPNを通して使用するアプリケーションプロトコル及びそのバージョンをネゴシエーションします。このALPNはTLSのハンドシェイクの一部として保護されるため、クライアントとサーバが確実にアプリケーションプロトコルに合意できます。

このALPNではそのコネクションで使用できるアプリケーションプロトコルを提示します。HTTPのように、バージョンによって下位層に使うプロトコルが異なる場合では、下記のようなALPNをクライアントから提示し、サーバが選択することになります。

  • TLSのALPN: h2, http/1.1
  • QUICのALPN: h3

TLSのハンドシェイクを行うクライアントとサーバは、お互いに本当はh3に対応しているということがハンドシェイク中には知ることが出来ません。

IncompatibleProtocol拡張

TLSの拡張として、IncompatibleProtocol拡張を定義します。

  • クライアントからは、IncompatibleProtocolで空を指定します
  • サーバは、空のIncompatibleProtocolが送られてきた場合、このコネクションでは非互換なアプリケーションプロトコルをクライアントに通知します


HTTP/3に対応しているサーバに対して、HTTP/2 + TLSで接続を開始した際の例は次のとおりです。
f:id:ASnoKaze:20210719013125p:plain

こうすることで、非互換なアプリケーションプロトコルも含めサーバが対応しているアプリケーションプロトコルのバージョンを知ることが出来ます。

その後は具体的なアプリケーションプロトコルの動作に依存するかと思います。

FacebookのQUICを活用したライブ動画用プロトコルRUSHについて

Facebookの方々から「RUSH - Reliable (unreliable) streaming protocol」というプロトコル仕様が、IETFに提出されています。

簡単に眺めていきます。

Facebookとライブ動画

このRUSHはRTMP代替として、クライアント(配信者)のアプリからライブ動画を取り込むためのプロトコルのようです。Facebookではすでに使っているようです。

2018年頃のFacebookのエンジニアブログを見ると、Facebook Liveではクライアント(配信者)からの動画の取り込みに RTMPSを使っていることが紹介されています。おそらく、ココの部分を代替するのでしょう。視聴者へはCDNを通してMPEG-DASHで配信されるようです。
f:id:ASnoKaze:20210715012930j:plain

また、IETFの投稿をみると、Facebookではアプリ及びインフラでこのプロトコルを使用していると述べています (URL)。

IETFでも、Twitchの方やGoogleの方もこの仕様に興味をしています。これから議論が進んでいくところでしょう。

今回は、そんなRUSHについて簡単に仕様を眺めていきます。


クライアントとの通信においてQUICを使うメリットは大きそうです。単純に、トランスポートプロトコルとしてQUICを使うメリットについては、以前書いた記事などを参考ください。その他、QUICのストリーム管理なども以前の記事に解説を譲ります。

RUSH

RUSH - Reliable (unreliable) streaming protocol」は、ビデオとオーディそれぞれ複数のトラックをQUIC上で送信可能にするプロトコルです。

配信保証に関して制御する仕組みがあります。ライブ動画ですので、遅れてきたデータがもう不要というケースがあります。具体的には、あるビデオフレームが一定時間内に受信できなかった場合は、そのデータの再送を中止する仕組みがあります。

RUSHではこの仕組を実現するために、ビデオフレーム及びオーディオフレーム毎にQUIC双方向ストリームを利用する形になっています。QUICはトランスポートプロトコルとして、ストリーム毎にパケロスやパケットの順番の入れ替わりが考慮されます。ストリームを越えてはパケロスの影響を受けず、届いたパケットから処理を進めることが出来ます。

RUSHではビデオフレームやオーディオフレーム毎に1つストリームを使用するため、遅れてきた不要なビデオフレームやオーディオフレームは再送を中断を実現できます。その時あ、そのストリームをクローズすることになります (なので、片方向ではなく双方向ストリームを使う)。

(なお、上記のような複数QUICストリームを用いるモードをMulti Stream Mode、単一のQUICストリームを用いて全部そこでデータ送信するNormal modeが利用できます。Normal modeでは全てのメディアデータは再送されます)

ビデオフレーム及びオーディオフレーム

ビデオフレームとオーディオフレームどのような形式で送信されるか、送信メッセージ形式を確認すると分かりやすいです。

これらは、QUICストリーム上で送信されます。

ビデオフレーム

ビデオデータは下記のVideo frameというメッセージ形式で送信されます。

Video frameのフォーマットは下記のとおりです。
f:id:ASnoKaze:20210715020522p:plain

Codecや、トラックを一意に識別するためのTrack IDが含まれています。現在のところCodecはH264, H265, VP8, VP9が使えるようです。

また、IDフィールドは同一トラック内で1ずつ単調増加する識別子です。RUSHでは、1つのトラックデータを複数QUICストリームでやりとりするため、トラックデータを再構築するために使用されます。

オーディオフレーム

オーディオフレームは下記のAudio frameというメッセージ形式で送信されます。

f:id:ASnoKaze:20210715021453p:plain

ビデオフレームと同様、トラックIDやIDでトラックデータが管理されます。

Codeとしては、AAC, OPUSが利用できるようです。

その他の制御メッセージ

ビデオデータ、オーディオデータの他にも通信制御用のメッセージが定義されています。

詳細は割愛しますが下記のとおりです。

  • Connect frame
  • Connect Ack frame
  • End of Video frame
  • Error frame
  • GOAWAY frame

おわりに

まだ議論は始まったばかりです。まだまだ、大きく変わることもあるでしょう。

注目を集めているプロトコルですので、これから盛り上がっていくことがたのしみです。

以下雑感

マルチトラックでフレーム毎日ストリーム消化するようだけど、優先度制御したいばあいどうするんだろうなあ。トラックIDで制御したそう。パッとDATAGRAM使いたい気持ちもあるが、再送不要のフィードバックどうするかだけ再設計かな。

トラッキングに利用できない3rdパーティクッキー「CHIPS」の仕組み (partitioned属性)

3rd パーティクッキーの廃止計画が世間を賑わせています。一方で、3rdパーティクッキーには、トラッキング目的でないユースケースもあります。それらのために、3rdパーティクッキーの制限を緩和する「Cookies Having Independent Partitioned State (CHIPS)」という仕組みが検討されています。

この仕組みに則っていれば、制限のもと3rdパーティクッキーを使えるようになります。
もちろん、これはトラッキングが出来ないようになっています。

仕様: https://github.com/WICG/CHIPS
Chromeの実装計画: 「Intent to Prototype: Cookies Having Independent Partitioned State (CHIPS)

ユースケースについては、仕様の中で述べられていますのでそちらを参照ください。

CHIPS以前の3rdパーティクッキー

CHIPSがどのようにCookiを制限するか説明する前に、今の3rdパーティクッキーについて軽く説明をします。

次の例では

f:id:ASnoKaze:20210704155533p:plain

クッキーは払い出したドメイン(ホスト名)をキーにブラウザに保存されます。そのため、どのサイトにサブリソースと埋め込まれても共通してCookieが共有されます。ですので、ユーザのトラッキングに使用できました。

CHIPSの緩和

Cookies Having Independent Partitioned State (CHIPS)」では、払い出したドメインだけでなく、トップレベルのサイトもキーにして保存領域が分割されます。

先の例と同じように、https://a.examplehttps://b.examplehttps://cookie.example を埋め込んでいます。https://cookie.exampleから払い出されたCookieは異なる領域に保存されます。そのため、クロスサイトでのトラッキングには使う事はできません。

f:id:ASnoKaze:20210704160655p:plain

CHIPSのオプトイン

CHIPSで定義されるように、Cookieを動作させるために、set-cookieする際にpartitioned属性を付与する必要があります。

Set-Cookie: __Host-SID=31d4d96e407aad42; SameSite=None; Secure; HttpOnly; Path=/; Partitioned;

その他にも以下の制約がある

  • Cookie Prefixesで定義される __Hostプレフィックスがついている必要がある (Domain属性を上書き設定不可になる)
  • SameSite属性はNoneが指定される必要がある
  • Secure属性がついている必要がある
  • HttpOnly属性がついている必要がある

将来的には、3rdパーティリクエストするさいPartitioned属性のついたCookieのみが送信され、Partitionedの無いCookieは送信されないようになるものと思われます。

標準化に関して

現在、W3CのWICGを中心に議論が進められています。IETFのHTTP WGにもドキュメントの状況は共有されていますが(URL)、具体的な提案仕様はまだ提出されていません。

タイムゾーンを含むタイムスタンプ文字列表現の標準化

Date and Time on the Internet: Timestamps with additional information」という提案仕様がIETFで提出されているので簡単に紹介する。

この仕様では、下記のようなタイムスタンプの文字列フォーマットの定義を行う

1996-12-19T16:39:57-08:00[America/Los_Angeles][u-ca=hebrew]

背景 TC39 Temporal

Temporalという時間を扱う新しいAPIが、TC39でStage 3となっている。
tc39.es

このAPIでは、タイムゾーンを含む文字列を生成できる。

const zonedDateTime = Temporal.ZonedDateTime.from({
  timeZone: 'America/Los_Angeles',
  year: 1995,
  month: 12,
  day: 7,
  hour: 3,
  minute: 24,
  second: 30,
  millisecond: 0,
  microsecond: 3,
  nanosecond: 500
}); // => 1995-12-07T03:24:30.0000035-08:00[America/Los_Angeles]

この文字列表現はTemporalの仕様では次のように示している。

f:id:ASnoKaze:20210620000835p:plain

このように、時刻のフォーマットしては RFC3339 を利用しているが、タイムゾーンはTime Zone Extensionとして定義している。

この新しいTime Zone ExtensionをちゃんとRFCとして定義しようというのが「Date and Time on the Internet: Timestamps with additional information」である。IETFへの提案は、Temporalの標準化を行っているUjjwal Sharma氏によって行われている。 (IETF110の発表スライドpdf)。

提案仕様

Date and Time on the Internet: Timestamps with additional information」の提案仕様では、RFC3339 の定義に加えtime-zone, suffix-tag を付加できるようになっている。

フォーマット定義
time-zone-char = ALPHA / "." / "_"
time-zone-part = time-zone-char *13(time-zone-char / DIGIT / "-" / "+") ; but not "." or ".."
time-zone-name = time-zone-part *("/" time-zone-part)
time-zone      = "[" time-zone-name "]"

namespace      = 1*alphanum
namespace-key  = 1*alphanum
suffix-key     = namespace ["-" namespace-key]

suffix-value   = 1*alphanum
suffix-values  = suffix-value *("-" suffix-value)
suffix-tag     = "[" suffix-key "=" suffix-values "]"
suffix         = [time-zone] *suffix-tag

date-time-ext  = date-time suffix

実際に使う例

1996-12-19T16:39:57-08:00[America/Los_Angeles][u-ca=hebrew]

任意のサフィックスタグもつけられる

1996-12-19T16:39:57-08:00[x-foo=bar][x-baz=bat]