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をその他の用途に利用もすることもできます。

2021年 HTTPやQUICの最新動向振り返り

2021年について、プロトコル周りの動向を振り返っていきたいと思います。

今年は、個人的には次の2点がホットトピックと挙げられると思います。

  • QUICやHTTP/3を活用した応用系プロトコルの作業が進む
  • プライバシー系の取り組みが活発化

それでは、個別に補足していきます。(IETFの動向がメインです。なお、個人的にキャッチアップできてないトピックもあります...)

f:id:ASnoKaze:20211231030248p:plain

HTTP関連

まずは、HTTPです。HTTP/3の標準化が注目を浴びていますが、HTTP/1.1やHTTP/2なども改定作業が行われております。あわせて、HTTPセマンティクスは各バージョンから独立し、各バージョンから参照される形となりました。それぞれRFC出版の最終段階となっています。

f:id:ASnoKaze:20211230235428p:plain

書いた記事はここらへん

また、それ以外にもメソッド・ヘッダ・Cookieなど様々な仕様が提案されております

また、書いたのは一昨年ですが、次のトピックも標準化が進められています

その他書けていないトピックとしてRFC6265 HTTPのCookieも仕様の改定作業が進められています

QUIC

QUICは、RFC 9000として標準化が完了しました。いくつかの拡張仕様の議論や、実際のQUICの運用に関するトピックの議論が行われています。

個人的に注目しているところとして、Ossification(硬直化)対策として進められているQUIC v2や、Wi-FI+LTEといった複数通信を扱うMultipath QUICといったトピックがあります。
書いた記事はここらへん

その他記事には書けておりませんが、QUIC WGでは他にも次のような仕様の標準化が進んでいます

DATAGRAM拡張

QUICにはDatagram拡張という仕様の標準化が進められています。トランスポートプロトコルであるQUICはアプリケーションプロトコルのデータは再送されますが、再送を必要としない通信方法を規定しています。WebTransportやMASQUEなど多くのプロトコルで利用されます。

もともとはHTTP/3で利用のみとして標準化されていますが、TCPにより再送されてしまいますがHTTP/2, HTTP/1.1でも利用できる形での標準化が目指されています。また、DATAGRAM拡張自体を拡張出来るように現在専任デザインチームで議論されています。そのため、またその部分は変更されるものと思われます。

WebSocket, WebTransport

Webでのアプリケーションデータの双方向通信として、WebTransportというものが出てきました。これはHTTP/3を活用し再送が不要なアプリケーションデータの送信を可能にするものです。

今年は方針が固まった年であり、WebTransportは over HTTP/3でまず標準化することと、フォールバック先としてover HTTP/2の標準化を行うことが決まりました。

また、WebSocketではHTTP/3対応した仕様の標準化作業が進められています。

書いた記事はここらへん

WebTransport over HTTP/2は最初の仕様とは異なり、最新版ではlayered designという形で再設計されております。

また、ブラウザへのマルチキャスト配信についてW3CのMulticastCGで議論されておりますが、そこでもWebTransportを使用できないかという議論が進んでいます。

OHTTP, Masque

OHTTPもMasqueもそれぞれ全く違う技術ですが、合わせて紹介します

OHTTP(Oblivious HTTP)は、ユーザのIPを秘匿する通信方法の標準化を行っています。AppleのPrivate Relayなどの取り組みを行っている方々も興味をもっているようです。書いた記事はここらへん

Masqueは、HTTP/3上UDPパケットやIPパケットをトンネリングする仕組みを標準化しています。それぞれWGアイテムとして標準化が勧めラ得ています。書いた記事はここらへん

なお、CONNECTメソッドを使用していましたが、最新仕様ではWebSocket over HTTP/2と同様に拡張CONNECTメソッドを使う形で標準化されています。CONNECT-IPは複数の提案仕様があったのですが、合わせるような形で新しい仕様が出されています。

Media over QUIC

IETFでは、QUIC上でどのようにメディアデータを送信するかというトピックがホットになっています。すでに、FacebookやTwitchではそれぞれ独自の手法で実利用が始まっているようです。とはいえ、注目は浴びているものの、標準化のためにユースケース別に要件を整理している段階になります。

主なユースケースは次のとおりです。

  • 配信者からのアップロード
  • 視聴者のダウンロード
  • P2Pによる通信

FacebookはRUSHというプロトコルを、TwitchはWarpという仕組みを利用しているようです。Warpは提案仕様を提出する準備ができているようですが、まだ出てきておりません。その他SRT over QUICや、RTP over QUICの議論も行われています。

RUSHについては記事を書いております

XX over QUIC

QUICがRFCとなり、既存のアプリケーションプロトコルでも、QUICを利用することが考えられています。それぞれのワーキンググループにより標準化が進められています。

  • BGP over QUIC
  • DNS over QUIC
  • SSH over QUIC
  • SMB over QUIC
  • RTP over QUIC
  • (NTP over QUIC)

TLS

TLS関連も引き続き標準化が進められています。ココでは、個人的に注目しているトピックを触れます。

TLS1.3の仕様も改訂作業が進められています。機能上は大きな差はありません。

あと大きなところで言うと、Encrypt SNI(ESNI)とも呼ばれていましたが、ClientHelloを暗号するECHという仕様も標準化が進められています。以前記事に書いております。


その他にDTLS 1.3や、余分なデータ量を削ったコンパクトなcTLSの標準化も進めれています。また、TLSを安全に使うためのガイドラインRFC7525の改訂作業も進められています。

追えてないトピック

  • OAuth, Gnapまわり
  • Privacy Pass まわり
  • Privacy Interest Group まわり
  • 暗号系トピック

gRPC over HTTP/3のプロトコルと実装を眺める

gRPC over HTTP/3のプロポーザルと、実装が出てきています。

今回は、仕様を眺めつつ、Ubuntuで実装を動かすところまで試してみようと思います

プロトコル仕様

プロポーザルは、現在 "In review" の状態となっています。
github.com

HTTP/3の基本

HTTP/3はHTTP/2と機能上は大きな違いはありません。HTTPリクエストで通信が始まり、各HTTPリクエスト・レスポンスはQUICのストリームによって多重化されます。そのため、gRPC over HTTP/2にほぼマッピングされます。

HTTP/3では、トランスポートとしてQUICを使用します。これにより、ストリームが異なる場合は、パケットロスやパケットの順番が入れ替わったとしても、受信した後続のパケットを処理しすることができます
(TCPでは一般的にOSがそのパケットを回復するまで、後続のパケットを受信してても処理できません)

また、各コネクションはコネクションIDによって識別されるため、IPアドレスやポート番号が変わってもコネクションを維持することができます。
(サーバ間の通信が主なgRPCではそのメリットは少ないかもしれない)

一方で、QUICの通信は暗号化されます。暗号化しない手段は提供されていません。QUICは通信の中で、TLSハンドシェイク相当の処理を行うので、一般にはサーバ証明書が必要になります。

なお、1度通信した相手とは、0-RTTハンドシェイクを行うことで、コネクションの確立を早く行えます。

おまけ: QUIC及びHTTP/3の詳細

QUIC及びHTTP/3の詳細については、過去にガッツリ解説を書いたのでこちらを参照ください
asnokaze.hatenablog.com

gRPC over HTTP/3のプロトコル仕様

ここでは、gRPC over HTTP/2 との違うところ/同じところを簡単にかいつまんで紹介します。

  • ストリームID: HTTP/3ではQUICにより提供されるストリームを使用しますが、HTTP/2のストリームIDと同じように機能する
  • データフレーム: DATAフレームの使い方は、HTTP/3では変更されない
  • エラーコード: HTTP/3では、HTTP/2と異なるエラーコードをもちます。そのため、gRPCのエラーコードと改めてマッピングが行われます(仕様参照)
  • 接続の管理: GOAWAYフレームおよび、PINGフレームはHTTP/2のときと同様に使用できます。

そのため細かい点はありますが、HTTP/3とHTTP/2を変換するProxyを間に挟むだけで、プロトコル上は問題なく動作すると思われます。

また、HTTP/2はTCPであり、HTTP/3はQUIC(UDP)ですのでどちらで通信を行うのかうまく選択しなければなりません。その戦略には、一般的に2種類の方法があります。

  • クライアントがHTTP/3を使うように設定されている場合は、HTTP/3で最初に接続しに行く (Happy Eyeballsや、フォルバックは実装依存)
  • ブラウザが行うのと同じように、クライアントはまずHTTP/2で接続してから、alt-svcヘッダの情報をもとにサーバがHTTP/3に対応している事を知ります。その後、HTTP/3で通信を開始します。

実装

Ubuntu 20.04で、.Net core6 を[公式ドキュメント]にそってインストールします。

その後、下記のexampleを実行します。このGreeterは、nameをつけてSayHelloすると、「hello name」と返してくれます。
github.com

サーバ

.Net 6.0でビルドして、HTTPSと証明書の設定をし、ドキュメントに沿ってHTTP/3を有効化します。

要所としてはこんな感じで動きました。

webBuilder.ConfigureKestrel(options => {
	X509Certificate2 cert = new X509Certificate2("/path/to/server.pfx");
	options.ListenLocalhost(5001, o => {
		o.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
		o.UseHttps(cert);
	});
});

その後、ビルドして起動するだけです。

動作確認

今回は単純に、HTTP/3に対応したcurl で grpcなリクエストを投げ込んで動作確認しました

クライアントログ (出力はgrpc形式のバイナリがかえってきています)

$ hexdump -C ./req.bin 
00000000  00 00 00 00 0f 0a 0d 47  72 65 65 74 65 72 43 6c  |.......GreeterCl|
00000010  69 65 6e 74                                       |ient|
00000014

$ cat ./req.bin | curl -sS -X POST --data-binary @- https://localhost:5001/greet.Greeter/SayHello  -k -H"content-type:application/grpc"   --http3 -o - | hexdump -C
00000000  00 00 00 00 15 0a 13 48  65 6c 6c 6f 20 47 72 65  |.......Hello Gre|
00000010  65 74 65 72 43 6c 69 65  6e 74                    |eterClient|
0000001a

サーバ側ログ

:~/work/grpc-dotnet/examples/Greeter$ ./Server/bin/Debug/net6.0/Server
...

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/3 POST https://localhost:5001/greet.Greeter/SayHello application/grpc 20
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /greet.Greeter/SayHello'
info: Server.GreeterService[0]
      Sending hello to GreeterClient
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/3 POST https://localhost:5001/greet.Greeter/SayHello application/grpc 20 - 200 - application/grpc 6.2228ms

一旦、ちゃんと動いてそう。

最後に

他の言語でも繋いでみたいですね(冬休みの宿題)