Skip to content

feat: WebTransport(HTTP/3、UDP、BBR)で中継する。だめなら WebSocket に戻る / v0.8.0 - #56

Merged
5ym merged 4 commits into
mainfrom
feat/webtransport
Oct 12, 2026
Merged

5ym merged 4 commits into
mainfrom
feat/webtransport

Conversation

@5ym

@5ym 5ym commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

ブラウザと mado の間を、WebTransport(HTTP/3 over QUIC、UDP)でもつなげるようにする。輻輳制御は BBR。UDP が通らなければ、今の WebSocket に戻る。

サーバー

  • msquic(Microsoft.Native.Quic.MsQuic.Schannel 2.6.2)を P/Invoke で直接呼ぶ(server/Quic/)。HTTP/3・QPACK(動的表なし、Huffman)・WebTransport(draft-02 と draft-07 の設定)を自前で書いた
  • UDP 443(webtransport の設定。false で止める)。証明書は ACME のものを使い、更新したら作り直す
  • 認証: Origin が mado の origin と同じこと、/wt?token= のトークンを CONNECT で確かめ、RDCleanPath の proxy_auth も同じトークンで使い切る。WebTransport には cookie が付かないが、トークンは cookie で守られた /api/token でしか出さないので、/ws より弱くはならない
  • 中継は /ws と同じ(Relay.cs を IRelayChannel で WebSocket とバイト列の両方に)。受信が溜まったら止める
  • MSI: msquic.dll を mado.exe の隣に入れ、ファイアウォールで UDP 443 を開ける
  • msquic が読めない・待ち受けられないときは、ログに出して WebSocket だけで動く

ブラウザ

  • /api/session の wt があれば WebTransport を先に試し、1.5 秒で ready にならなければ WebSocket に移る。診断に「中継: WebTransport / WebSocket」

試験

  • .NET 84 件(varint、Huffman、QPACK、SETTINGS、CONNECT の確認、バイト列での中継)、web 279 件
  • コンテナで、Linux の mado + libmsquic + xrdp と headless Chromium 141: WebTransport で中継できる、UDP が届かなければ WebSocket に戻る、使ったトークン・違う origin・違う path は断る
  • Windows(Schannel・Native AOT・MSI)は CI とこの PC に入れて確かめる

🤖 Generated with Claude Code

https://claude.ai/code/session_01M2BadDnB3uH3FDGjaPMP4J

5ym and others added 3 commits October 12, 2026 18:15
msquic を P/Invoke で直接呼び、HTTP/3 と QPACK と WebTransport を自前で話す。
System.Net.Quic は QUIC の datagram を宣言できず、Kestrel の WebTransport は draft-02 で止まっている。
輻輳制御は BBR、datagram の受信を有効にする。

- MsQuic.cs: msquic の API 表と構造体、接続と stream の包み
- Http3.cs / Qpack.cs: 可変長整数、フレーム、SETTINGS、静的表と Huffman の QPACK
- WebTransport.cs: 拡張 CONNECT のセッション、0x41 / 0x54 の stream、datagram、capsule
- Program.cs: 証明書(ECDSA P-256、13 日)を作り、エコーのサーバーと試験のページを動かす
- headless.cjs: Playwright の Chromium で test.html を開く

Chromium 141 で双方向、単方向、datagram、セッションの close を確かめた。
Chrome の SETTINGS は draft-02(0x2b603742)だけだったので、draft-02 と draft-07 の両方を宣言する。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2BadDnB3uH3FDGjaPMP4J
msquic を P/Invoke で直接呼び、HTTP/3 と QPACK と WebTransport を自前で話す(server/Quic/)。
輻輳制御は BBR。/wt の最初の双方向 stream で、/ws と同じ RDCleanPath の中継をする。

- サーバー: UDP 443 で待ち受ける(設定 webtransport。tls が acme なら既定で使う)。証明書は ACME のものを
  PKCS#12 にして渡し、取り直したら次の接続から差し替える。msquic が読めなければ WebSocket だけで動く
- 認証: CONNECT の Origin と /wt?token= の使い捨てのトークンを見て、RDCleanPath の proxy_auth が
  同じトークンであることを確かめる(Cookie が付かないので)
- 中継: Relay を IRelayChannel で包み、WebSocket とバイトの流れの両方で動かす。
  受信は溜まりすぎたら msquic の受信を止め、送信は msquic がバッファを返すまで待つ
- msquic.dll(Microsoft.Native.Quic.MsQuic.Schannel 2.6.2)を exe の隣に置き、MSI に入れる。
  ファイアウォールに UDP 443 を足す
- クライアント: /api/session の wt があれば WebTransport を 1.5 秒試し、だめなら WebSocket。診断に道を出す
- 試験: varint、QPACK、Huffman、SETTINGS、CONNECT、トークン、バイトの流れでの中継。
  tools/wt-e2e で Linux の mado と xrdp を headless の Chromium でつなぐ
- tools/wt-poc は server/Quic に移したので消す

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2BadDnB3uH3FDGjaPMP4J
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2BadDnB3uH3FDGjaPMP4J
@claude

claude Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Claude finished @5ym's task in 42s —— View job


レビュー結果

WebTransport を足す設計は筋が通っていて、既存の /ws とも整合しています。ブラウザ側の WebSocket への戻りも、タイムアウトと後始末まで丁寧です。変更が大きい(約 3,500 行)ため、Quic/MsQuic.cs の P/Invoke とバッファ管理、WebTransport.cs の stream 処理は全行までは追えていません。主に認証まわり、Relay の分離、ブラウザ側を見ました。

指摘

1. 要確認: 認証前の接続が残り続ける(server/WebTransportRelay.cs:68)

  • 詳細はインラインコメントに書きました。
  • Relay.Run は RDCleanPath の要求が届くまで期限なしで待ちます。
  • KeepAlive が 15 秒なので、idle timeout 60 秒では切れません。
  • CONNECT のトークンは Check では使い切らないので、同じトークンで接続や stream を何本でも開けます。
  • 同時接続数や未認証接続の上限も見当たりません。UDP 443 を公開するので、/ws より DoS に弱くなりそうです。

2. 認証の強さの説明(WebTransportRelay.cs の冒頭コメント、Program.cs)

  • /ws では「トークンの sub と Cookie の sub が同じ」ことを見ています。WebTransport の側は Consume(auth, null) でこれを見ていません。
  • コメントの理屈(トークンを取れるのはログインしたページだけ)は理解できます。ただし、トークンが URL のクエリに入るので、ログに残る可能性があります。HTTP/3 の CONNECT なのでプロキシのログには出にくいはずです。ただし mado 自身が session.Path をログに出していないか確認してください。
  • 実害は小さいと思いますが、防御の層が一枚減っていることは README / docs に明記しておくとよいです。

3. 軽微: SetCertificate の _listener(WebTransportServer.cs:90-103)

  • _listener は _configLock の外で読み書きしています。
  • 呼び出しが証明書の更新ループ 1 本だけなら問題ありません。
  • Dispose(ApplicationStopping)と重なると、停止直後に listener が作り直される可能性があります。
  • 起こりにくいので、確認だけで構いません。

4. 軽微: StreamChannel.CloseAsync が stream を Dispose する(Relay.cs)

  • WebTransportRelay の finally でも bytes.DisposeAsync() を呼ぶので、二重の Dispose になります。
  • WebTransportByteStream が冪等なら問題ありません。冪等かどうか確認してください。

良い点

  • Relay を IRelayChannel で分けた形は小さく、/ws の挙動を変えていません。空のバイナリメッセージを「閉じた」と取り違えない処理もあります。
  • CONNECT で Origin とトークンを確かめ、使い切りは RDCleanPath の proxy_auth の側に置いています。CONNECT のトークンと proxy_auth が同じであることも見ています。
  • msquic が読めないときに WebSocket だけで動く形になっています。IsListening のときだけ wt を返す点も良いです。
  • ブラウザ側は、タイムアウトしたあとに ready になった WebTransport を閉じています。
  • テストは varint、Huffman、QPACK、CONNECT の確認、バイト列の中継、transport.test.ts とそろっています。
  • テストの不足として、指摘 1 の「要求が届かない接続」の期限と、トークンの使い回し(CONNECT が 2 回通ることの確認)のテストがあるとよいです。

確認できていないこと

  • ビルドとテストは実行していません。
  • Windows(Schannel・Native AOT・MSI)の動作も確認していません。
    | ブランチ: feat/webtransport

Comment thread server/WebTransportRelay.cs Outdated
log("wt open");
try
{
await Relay.Run(new StreamChannel(bytes), auth =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

要確認(堅牢性): ここから Relay.Run は RDCleanPath の要求が届くまで、時間制限なしに待ちます。WebTransport は KeepAliveIntervalMs = 15s なので、CONNECT を通ったあとに stream を開いて何も送らない接続は、idle timeout(60s)で切れず、ずっと残ります。

/ws では Cookie でログイン済みの相手に限られますが、こちらは CONNECT に有効なトークン(60 秒で切れるが、CONNECT では使い切らない)があれば、同じトークンで何本でも CONNECT と stream を開けます。

  • 要求が届くまでに数秒〜十数秒の期限を付ける(cts.CancelAfter を要求が揃ったら解除する形など)
  • または、CONNECT を通すときにトークンを使い切る

どちらかを検討してください。

レビューの指摘への対応。

- トークンは CONNECT で使う(2 度目の CONNECT は 401)。RDCleanPath の proxy_auth は、
  CONNECT で使ったトークンと同じ文字列だけを通す(ここではもう使わない)
- 1 つの QUIC 接続にセッションは 1 つ(2 つ目は 429)。中継は最初の双方向 stream だけ
- 時間切れ: 握手の後とセッションの後、10 秒のうちに正しい CONNECT が来なければ接続を閉じる。
  CONNECT の後 10 秒のうちに中継の stream が来なければセッションを閉じる。
  RDCleanPath の要求は 10 秒で揃わなければ 408 を返して閉じる(/ws も同じ)
- 上限: QUIC の接続は全部で 64、確かめる前は 16、同じ IP から確かめる前は 4(ConnectionLimits)
- WebTransportServer の SetCertificate と Dispose を重ねない
- WebTransportByteStream.DisposeAsync は何度呼んでもよい
- クライアント: WebSocket に移るときはトークンを取り直す(CONNECT の後で待ち時間が切れることがあるため)
- トークンの扱いを RelayTokens にまとめる
- 文書: WebTransport の認証(Origin と使い捨てのトークン)、URL のトークンはログに残らないこと

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2BadDnB3uH3FDGjaPMP4J
@claude

claude Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Claude finished @5ym's task in 18s —— View job


レビュー結果(最新コミット b687d42 後)

前回の指摘 1(認証前の接続が残る)と 2(トークンの使い回し)には、対応済みと読めました。

  • WebTransportRelay.cs: CONNECT のときに consume でトークンを使い切ります。同じトークンの 2 度目の CONNECT は 401 になります。
  • WebTransportRelay.cs: CONNECT の後、Relay.RequestTimeout のうちに stream が開かなければ Wait タイマーでセッションを閉じます。stream が開いたあとの期限は Relay.Run が同じ時間で見ます。
  • ConnectionLimits.cs: 全体、認証前、同じ IP ごとの上限を持つ券の仕組みが入りました。Authenticated と Release は冪等です。
  • WebTransportRelay.cs: Started を Volatile で扱い、2 本目の stream は打ち切ります。OnClosed の取り消しは Task.Run に逃がしてあり、Gate を持ったまま後始末が走ることはありません。
  • トークンが URL に入る点は、コメントに「mado はパスをログに書かない」と明記されました。

前回の指摘 3 と 4(SetCertificate の _listener の競合、CloseAsync の二重 Dispose)は、今回の差分では再確認していません。どちらも軽微で、前回書いた確認のままです。

要確認(任意)

  • ConnectionLimits の上限(total 64、preAuth 16、同一 IP 4)は、NAT の裏の複数人が同じ IP から使う場合に、認証前の枠がすぐ埋まる可能性があります。UDP の送信元 IP は偽装できるので、IP ごとの上限は完全な防御にはなりません。個人利用の中継としては妥当な値だと思います。
  • 期限切れと同時接続の上限を超えたときのテストが、tests/QuicTests.cs と tests/RelayTests.cs にあるかは見ていません。無ければ追加するとよいです。

確認できていないこと

  • ビルドとテストは実行していません。
  • Windows(Schannel・Native AOT・MSI)の動作は確認していません。
  • Quic/MsQuic.cs の P/Invoke とバッファ管理は、全行までは追えていません。

今回の差分に、新しい不具合やセキュリティ上の問題は見つかりませんでした。
| ブランチ: feat/webtransport

@5ym
5ym merged commit 20029a1 into main Oct 12, 2026
3 checks passed
@5ym
5ym deleted the feat/webtransport branch October 12, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant