HTTP/3でQUICv2を使うためのalt-svc拡張

An Alt-Svc Parameter for QUIC Versions」という提案仕様が提出されています。

仕様の内容自体は簡単だが、背景から軽く説明していきます。

背景: HTTP/3とalt-svcヘッダ

サーバがHTTP/3に対応しているかクライアントに通知する方法がalt-svcです。

HTTP/2の頃はTLSハンドシェイク中にHTTP/1.1を使うかHTTP/2を使うかネゴシエーションしていました。HTTP/3ではそもそもQUIC(UDP)で通信がはじまりますので、その前にサーバHTTP/3に対応しているか知る必要があります。

クライアントは一度HTTP/2で接続し、サーバ側からこのようなalt-svcヘッダを返すことでHTTP/3に対応している事を通知します。

alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

なお最近ではDNS HTTPS レコードを用いて、サーバのHTTP/3対応を知らせる方法も利用されています。
asnokaze.hatenablog.com

背景2: QUICv2とHTTP/3

RFC9000としてQUICが標準化されましたが、これはQUICバージョン1です。QUICの新しいバージョンであるQUICv2の標準化が進められています。

クライアントとサーバでどのバージョンのQUICで通信するかはQUICのバージョンネゴシエーションにより決定されます。互換性のあるバージョン同士では追加のRTTはありませんが、互換性のないバージョンを使う場合は追加のRTTが発生します。

ここでは詳しくは説明しませんが、気になる方は以前書いた記事を参照ください

asnokaze.hatenablog.com


「An Alt-Svc Parameter for QUIC Versions」

現状では、サーバがどのQUICバージョンに対応しているかクライアントは通信を行うまで分かりません。

現状だとクライアントはQUICv1を介してバージョンネゴシエーションを行うという戦略が妥当ですが、それだとサーバはいつまでもQUICv1をサポートしなければなりません。

そこで、サーバが対応しているQUICバージョンをalt-svcに記載するのがこの提案です。

例を見ると分かりやすいかと思います。

Alt-Svc: h3=":443"; quicv="1"
Alt-Svc: h3=":443"; quicv="709a50c4,1", h3=":1001"; quicv="709a50c4"

(ブログタイトルはQUICv2と書きましたが、将来のQUICバージョンで使えます。)

これで、クライアントはサーバが対応しているQUICバージョンを知ることが出来ます。

本提案によって、QUICv2以降のQUICv1と互換性がないQUICバージョンを利用する際も余計なネゴシエーションオーバヘッドが必要なくなります。

HTTPで再開可能なアップロードを可能にする提案仕様

HTTPでファイルをアップロードする時、ネットワークの寸断などによりアップロードが中途半端に終わってしまうことがあります。アップロードが途中まで成功したとしても、一度切断するとそこから再開する方法は標準化されていません。
(PUTリクエスト + Rangeヘッダによる再開をサポートしているシステムもありますが標準化されてはいません。 参考URL)

そこで、再開可能なアップロードの仕組みを定義した「Resumable Uploads Protocol」という仕様がIETFに提出されています。この仕様は、Transloadit Ltd, Apple Inc, Cloudflare などの方々の共著になっています。

Resumable Uploads Protocol の概要

Resumable Uploads Protocol」は、一度中断しても再開可能なアップロード方式を定義しています。

簡単な流れは次のとおりです

  • クライアントから、POSTやPUTでファイルのアップロードを開始する。この時、再開用トークンをupload-tokenヘッダで送る。
  • サーバは、Resumable Uploads Protocolに対応していれば、104 レスポンスを送る。
  • その後、アップロードが中断した場合は、クライアントは再開用のトークンでアップロードを再開する。

(100番台のレスポンスは、サーバからレスポンスを返しても通信は終わらない点に注意してください。通信を維持し、最終的に改めて正式なレスポンスコードを返します。)

Resumable Uploads Protocolの流れ

Resumable Uploads Protocolにおいて、アップロードの開始および再開の流れについて説明していきます
f:id:ASnoKaze:20220227230313p:plain

アップロードの開始

Resumable Uploads Protocolに対応したクライアントは、まずPOSTやPUTリクエストでファイルのアップロードを開始します。この時次のヘッダを付けます。

  • upload-token: 今回のファイルアップロードを識別するためのトークン。再開時にも利用する
  • upload-draft-interop-version: 準拠している仕様のバージョン。本仕様では1が指定される。
104レスポンス

HTTPの仕様として、100番台のレスポンスは、本来のレスポンスに先んじて返されます。その後正式なレスポンス (200~500番台)が返されます。ですので、100番台のレスポンスを返しても通信は終わりはしません。

Resumable Uploads Protocolでは、サーバがこの仕様に対応している場合は、104レスポンスを返します。104レスポンスは、ボディを受信仕切る前に返せるので、通信が途中で中断される前にクライアントに通知することが出来ます。

(なお、サーバがResumable Uploads Protocolに対応してない場合は、通常通りのPOSTリクエストとして処理されます)

通信の中断

ユーザのアクションや、ネットワークの切断、ソフトウェアの終了などの理由により、アップロードが中断される可能性があります。

アップロードの再開

ファイルのアップロードを再開するにあたって、サーバがどこまで受信したのかをまず確認します。upload-tokenのついたHEADリクエストを送り、サーバから upload-offset を取得します。これが、サーバが受信したデータです。

そのオフセットからファイルアップロードを再開します。POSTリクエストにはupload-tokenおよびupload-offsetを付け送信します。

その他

詳しく説明しませんが、仕様では次のケースについても書かれています

  • サーバ側で、受け取ったupload-tokenに紐づく途中ファイルがない場合の扱い
  • クライアント側から、途中ファイルを破棄するようにサーバに通知する方法
  • 予め長さが分かってないファイルのアップロード ( Upload-Incomplete )

Twitchの QUICを用いたライブストリーミングプロトコル Warp

Twitchは、HLSの代わりとなる、ライブ配信プロトコルWarpを開発している
(情報: MLへの投稿カンファレンスでの発表

このWarpと呼ばれるプロトコルの仕様「Warp - Segmented Live Video Transport」が、IETFに提出されています。

以前書いたFacebookのRushとは異なり、アップロードではなく配信がわのプロトコルのようです。面白そうなので、軽く目を通していこうかと思います

asnokaze.hatenablog.com

Warp

WarpはWebTransport上で実行されるようですが、今回提出された仕様では、QUICやWebTransportのコネクション確立には触れてません。

コネクションの確立後に、QUICの単方向ストリームを用いて、MP4(CMAF)を送信する方法について説明しています。

f:id:ASnoKaze:20220212003940p:plain

仕様の用語を用いると、通信はMedia producerからMedia consumerにセグメント化されたMP4が配信されます。

簡単にまとめると次の通り

  • QUIC単方向ストリームでセグメントを送信する
  • パケロス時の再送はすべてQUICレイヤに任せる (DATAGRAMフレーム使わない)
  • 各セグメントを異なるストリームを用いて、パケロス時のヘッドオブラインブロッキングを回避する
  • Media producerは各セグメントにPriorityを設定でき、高 priorityから優先して送信される (基本 最新のデータの方が優先度が高い。なので、パケロスの再送は相対的に優先度が低くなる)。オーディオストリームと、ビデオストリームで異なるPriorityの設定もできる。
  • Media consumerは、パケロスしたデータに対してどれくらい待つか自身が判断する (巻き戻し出来るようにデータは受け取って起きたいなど。) ただし、ストリームの並列数に上限があるので、優先度の低いものからMedia consumerによってクローズされる。

(疑問: リクエストURLは、WebTransport確立時に与えられているってことでいいのかな)

メッセージとセグメント

各ストリームではメッセージと一つ以上のセグメントを送ることが出来る。メッセージはセグメントのメタデータで、識別子やPriorityが含まれる。セグメントは、 初期化セグメント(ftyp, moovなど)とメディアセグメント(styp, styp, mdat)の2種類がある (CMAFの要件を満たす)。

メッセージはjson形式であり、仕様上はinit, media, priority という値が定義されている。なお拡張可能になっている。(今後形式は変わるかも)

以下に例を示すが、mediaとpriorityなどJSONは結合してもよい。

init: 後続に初期化セグメントが続くことを示すメッセージ。初期化セグメントのIDを持つ。1つずつ増加していく

{
  init: {
    id: int
  }
}

media: 後続にメディアセグメントが続くことを示すメッセージ。関連する初期化セグメントのIDとタイムスタンプを持つ。

{
  segment: {
    init: int,
    timestamp: int,
  }
}

priority: ストリームの優先度を示す。

{
  priority: {
    precedence: int,
  }
}

その他の補足

いくつかの疑問にメーリングリスト上で回答している
https://mailarchive.ietf.org/arch/msg/moq/0ZNlt5SvEzH3mroPOHjlAFpfHDI/

  • 『なぜWebRTCではないのか』
    • => 既存の仕組みよりもユーザエクスペリエンスが良くなかった
  • 『なぜDatagramを使わないのか』
    • => 輻輳制御, 再送制御, キャンセル, 多重化などを再実装することになるから
  • 『なぜHTTP上をつわかないのか』
    • =>Warpの利点である優先制御はHTTP/2やHTTP/3でも動くが、プッシュベースのプロトコルのほうが都合が良かった
  • 『なぜFragmented MP4を使うのか』
    • =>HLSやDASHが使用しており、配信プロトコルに依存せず使える。
  • 『なぜJSONを使うのか』
    • => 最初のDraftで厳密な定義を与えるのが面倒だったため

ChromeがQUICのInitialパケットに施すChaos Protection

先週、IIJ Technical WEEK 2021で山本 和彦さんが説明していた "Chrome Magic" について、知らなかったので自分用にメモ。

山本 和彦さんの発表資料はこちら。
www.iij.ad.jp

なお、QUICHEのコードではChaos Protectionと書かれているので、ここではChaos Protectionと呼ぶ。

前提: 硬直化(ossification)

ネットワークの中間装置が、パケットを観測し何かしらの処理をします。しかし、中間装置がパケットを読むときに、決め打ちで特定バイト目を取ってきて処理したりすると、将来のプロトコルが変更されたときに予期せぬ挙動をもたらすことがあります。

20017年頃、Googleの実験的プロトコルであったGoogle QUICではフラグの仕様を変えた際に、予期せず通信が阻害される場合があったと述べています。その他にも、TLS1.3の標準化においても、新しいバージョンのTLSの通信が阻害される事例が報告されました。

このようなことを、硬直化(ossification)と呼んでいます。

QUICのような最近のプロトコルでは、硬直化(ossification)を防ぐために工夫されています。たとえば、パケット暗号化を行い不用意に中間装置がパケットを処理できないようにしたり、Greaseという手法を用いて実装の予期せぬ挙動を検知するようにしています。

以前書いた記事もご参考に:
asnokaze.hatenablog.com

前提: QUICのInitialパケット

QUICはコネクションを確立する際に、クライアントからTLS Clienthelloメッセージを含む Initialパケットが送られます。このInitialパケットは仕様で定義された鍵を用いて暗号化されています。

この暗号化は、硬直化(ossification)を防ぐため行われていました。

もちろん、Initialパケットの暗号化は仕様で与えられていますので、だれでも復号することはできます(改ざんは出来ない)。冒頭紹介した発表で触れられていましたが、どうやら、SNIなどを取得するために、特定バイト列を読み込む中間装置がいるようです。

これでは、硬直化(ossification)の原因となりかねません。正しくパケットをパースするよう強制するために、導入されたのが Chaos Protectionです。

Chaos Protection

Chaos Protectionが行われた Initialパケットは次のような形になっています。ClientHelloのメッセージは、複数のCRYPTOフレームに分割されなおかつ順番もシャッフルされています。
f:id:ASnoKaze:20220124003604p:plain

一つのCRYPTOフレームは、ClientHelloのバイト列のうち指定されたOffsetからLength長のデータが格納されます。そのため、複数のCRYPTOフレームをどのような順番でInitialパケットに詰めても、それを正しく元のバイト列に直すことが出来ます。

CRYPTO Frame {
  Type (i) = 0x06,
  Offset (i),
  Length (i),
  Crypto Data (..),
}

このように正しくフレームをパースしないと、ClienthHelloのメッセージが読めなくなっています。

具体的に Chaos Protection で行われていることを、眺めていきます。

quic/core/quic_chaos_protector.cc - quiche - Git at Google

  SplitCryptoFrame();
  AddPingFrames();
  SpreadPadding();
  ReorderFrames();
  return BuildPacket(header, buffer);
  • SplitCryptoFrame: CRYPTOフレームをランダム個に適当に分割 (lengthもランダム)
  • AddPingFrames: PINGフレームをランダム個追加
  • SpreadPadding: 仕様で規定された長さまで複数*のPADDINGフレームで埋めます (*PADDINGフレームは1バイトですが、シャッフルのために長さを持つかのように扱ってる模様)
  • ReorderFrames: フレームをシャッフルする (すべてのフレームを、ランダムなフレームと位置をスワップする)

Initialパケット以外

Initialパケット以降は当事者間で共有された鍵で暗号化されるため、中間装置はパケットのほとんどの領域を読むことは出来ません。

ですので、Chaos Protectionを行う必要はありません。

HTTPの拡張CONNECTメソッド について復習する

HTTPにはCONNECTメソッドというものがあります、このメソッドを使ってProxyと接続しHTTPメッセージを中継してもらいます。

拡張CONNECTメソッドは、HTTPメッセージ以外のデータを中継してもらうのに使用します。特にHTTP/2やHTTP/3では特定のストリームを、データのトンネル用にするために、拡張CONNECTメソッドを使います。

例えば、WebTransport over HTTP/3なども拡張CONNECTメソッドを利用します。そのため、拡張CONNECTメソッドをサポートしてない単純なHTTP/3 ProxyとはWebTransport通信は行ったりは出来ません(Datagramらへんも絡む)。

今回は、その拡張CONNECTメソッド (Extended CONNECT Method) について見ていきます。

f:id:ASnoKaze:20220119010815p:plain

利用例

拡張CONNECTメソッドはもともと、WebSocket over HTTP/2の仕様で導入されました。

(HTTP/2でHTTPメッセージはストリームで管理するので、コネクションを切り替えるUpgradeは使えません。そこで拡張CONNECTを使うことになってます。標準化当時の議論 URL

その後、その他のプロトコルでも利用されるようになりました。例えば次のようなプロトコルです。

上記のように、HTTPリクエストを使ってサーバと通信したあとに、そのコネクション上でHTTPメッセージ以外のアプリケーションデータをやりとりできるようになります。もちろん、どのようなアプリケーションデータをやりとりするかはプロトコル次第ですが、HTTPレイヤではそのアプリケーションデータには関与しません (一般に、DATAフレームに格納され送信されます)。

UDP Proxying Support for HTTPは、Apple Private Relayでも利用されている技術であり、詳しくは以前の記事を参照ください
asnokaze.hatenablog.com

拡張CONNECTメソッド

拡張CONNECTメソッドはどのようなメッセージなのか具体例を以下に示します。これは、WebTransport over HTTP/3の接続リクエストの例です。

:method = CONNECT
:protocol = webtransport
:scheme = https
:path = /
:authority = server.example.com
origin: server.example.com

上記のように :protocol という疑似ヘッダを持つCONNECTメソッドが拡張CONNECTメソッドです。このリクエストに対して200番を返すことで該当のストリームは、:protocol疑似ヘッダで指定されたプロトコル用にデータをトンネルするようになります。ですので、レスポンスを返してもストリームはクローズしません。

先に上げたプロトコルでは、疑似ヘッダの値はこれらになります。

  • websocket
  • webtransport
  • connect-udp
  • connect-ip
疑似ヘッダ(pseudo-header)

疑似ヘッダについてはここで簡単に補足します。

HTTP/2やHTTP/3では、メソッド名やリクエストしたURLのスキームやパスは疑似ヘッダとして表現されます(これによりヘッダ圧縮が可能になります)。

疑似ヘッダはHTTP/2やHTTP/3の仕様で定義されていないものを送ってはなりません、受信した側はエラーとして扱う必要があります。ただし拡張CONNECTメソッドは拡張仕様であり、相手がこの拡張仕様に対応していれば :protocol疑似ヘッダを送信することが出来ます。

相手が :protocol 疑似ヘッダに対応しているかは、一般にSETTINGSパラメータで確認します。

SETTINGSパラメータ

HTTP/2やHTTP/3ではコネクションが確立したあとに、クライアントとサーバでSETTINGSパラメータを送り合い、通信に関するパラメータを交換します。その流れで、拡張CONNECTメソッドに対応しているかを確認します。

  • SETTINGS_ENABLE_CONNECT_PROTOCOL パラメータをセットして送ることで、相手に対応していることを通知します
  • もしくはWebTranportプロトコルの場合は、SETTINGS_ENABLE_WEBTRANSPORTパラメータを送ることで拡張CONNECTメソッドに対応していることを相手に通知します
その後

拡張CONNECTメソッドを用いてストリームをトンネル化したあと、どのようなデータをやりとりするかは、:protocol 疑似ヘッダで示されるプロトコルの仕様によります。

ブラウザでTCPを直接送受信できるDirect Sockets APIについて

ブラウザから直接TCPUDPで送受信する「Direct Sockets API」という仕組みが議論されています。

実験段階ですが、Chromeでは起動時にオプションを付けることでこの機能を有効にできます。今回はTCPの方で簡単に動作を見てみます。

Direct Sockets API

Direct Sockets APIは、TCPUDPで直接送受信可能にするAPIです。既存のアプリケーションプロトコル(SSHIRC)、P2Pのような機能を実現可能になります。

もちろんセキュリティ上の問題もあるので、Chromeでは現状デフォルトでは有効になっていない機能です。

セキュリティ面についてはだいぶGithubリポジトリで議論されておりますので目を通すと良いでしょう。ローカルネットワークへの通信やSame-Origin-Policy(CORS)回避の話が上がっていますが、今回は細かくは紹介しません。

その他、意見がある場合はコメントすることもできます。
github.com

試してみる

Chromeでは実験的に実装されています。おそらくまだ実装途中ですので、今後変わる可能性は高いです。

今回は、現在時点のChrome Canaryで動作確認しています。

クライアント側準備

起動時にオプションが必要です。
Chrome 99 では次の起動オプションをつけて、Chromeを起動します (現在の起動コマンドは、chrome:version から確認できます)

  • --enable-blink-features=DirectSockets
  • --enable-features=DirectSockets
  • --restricted-api-origins=https://asnokaze.com
サーバ側準備

有効にするページ(上記 --restricted-api-origins で指定したページ)で、次のレスポンスヘッダをつけます

  • Cross-Origin-Embedder-Policy: require-corp
  • Cross-Origin-Opener-Policy: same-origin
  • Permissions-Policy: direct-sockets=(self)

なお、TCPサーバ特定ポートで接続を受け付けるために、下記のコマンドを実行して123番ポートでリッスンしておきます。(well-knownポートが使用できるか確認するために、123番を使用)

$ while true; do ( echo "Hello from server" ) | sudo nc -l 123; done

つなぐ

Webページを開いたら、今回はデベロッパーツールで以下に示すスクリプトを実行します。このスクリプトでは

  • 指定されたサーバに、123番ポートで接続して、"Hello from chrome" と書き込みます
  • サーバから送信されたデータをデベロッパーツールに出力します

すると、ダイアログが出るので接続先を入力します。(現状は任意のサーバ・任意のポートが指定可能...)
f:id:ASnoKaze:20220104001004p:plain

const options = {
    remoteAddress: 'asnokaze.com',
    remotePort: 123
};
navigator.openTCPSocket(options).then(tcpSocket => {
	readableStream = tcpSocket.readable;
	writableStream = tcpSocket.writable;
	const defaultWriter = writableStream.getWriter();
	defaultWriter.ready.then(() => {
		return defaultWriter.write(str2ab("Hello from chrome"));
	})
	const defaultReader = readableStream.getReader()
	defaultReader.read().then( ({done, value}) => {
		console.log(ab2str(value));
	})
})

function str2ab(str) {
	var buf = new ArrayBuffer(str.length);
	var bufView = new Uint8Array(buf);
	for (var i=0, strLen=str.length; i < strLen; i++) {
		bufView[i] = str.charCodeAt(i);
	}
	return buf;
}

function ab2str(buf) {
	return String.fromCharCode.apply(null, new Uint8Array(buf));
}
サーバ側

クライアントからのメッセージが表示されました

$ while true; do ( echo "Hello from server" ) | sudo nc -l 123; done
Hello from chrome
クライアント側

サーバからのメッセージが表示されました
f:id:ASnoKaze:20220104001351p:plain

標準化に対する姿勢

この仕様は、かなり強力な機能であるため、標準化に対する姿勢はそれぞれです。

そもそもこの仕様はGoogleの方によって提案されています。しかし、Mozillaは反意を示しています(参考URL)。その他、Node.jsやDenoの領域でこの仕様に興味をもっている方も居ます。

なんにせよ、すぐに標準的な仕様になりそうというものではなさそうです。

Greasing the QUIC Bit の仕様について (RFC9287)

Greasing the QUIC Bit」という仕様が提案されています。

背景

仕様の解説の前に、QUIC BitとDemultiplexingの補足をします。

QUIC Bit

QUIC v1ではロングヘッダパケット、ショートヘッダパケット(1-RTTパケット)とも先頭から2bit目がFixed Bitとして "1" に固定されています。


UDP demultiplexing

たとえばWebRTCでは、一つのUDPポートで複数のプロトコルのパケットをやりとりします。RFC7983では、その多重化の方法を定義していますが、その仕様もQUICを考慮した形で改訂作業が進められています。それが「Multiplexing Scheme Updates for QUIC」です。

具体的には、下記のように各パケットの先頭1バイトによってパケットを識別します。

この識別に、QUIC Bitが利用できます。

硬直化

QUIC v1では、QUICパケットのヘッダもほとんどの領域が暗号化されていますが、QUIC Bitは暗号化されません。demultiplexingするために、QUIC Bitは復号前に利用されるからです。

このようにパケットの特定値が経路上で利用されていると、予期せぬ形で利用され、新しい拡張仕様や将来のQUICバージョンをデプロイするときに誤作動する要因となりえます。このような問題を硬直化とよんでおり、TLS1.3やQUICではすでに、そのような問題がすでに見つかっています。

そのため、一般的にGreasing という仕組みを導入し硬直化しづらくします。

Greasing the QUIC Bit

demultiplexingが必要ない通信では、クライアントとサーバ合意のもとQUIC Bitをランダムに設定しても通信上は問題有りません。それが「Greasing the QUIC Bit」です。

Greasing the QUIC Bit」では、grease_quic_bitトランスポートパラメータをemptyで送信します。

grease_quic_bitトランスポートパラメータを受信下側は、その相手に対してQUIC Bitをランダムに設定してもよくなります。

また将来的に、このbitをその他の用途に利用もすることもできます。