Files
yoi/docs/report/2026-08-03-lan-http-randomuuid-blocks-websocket.md
T

78 lines
4.2 KiB
Markdown

# LAN の HTTP アクセスで `crypto.randomUUID()` が WebSocket 接続開始を阻害する
## 概要
Workspace WebUI の Vite dev server を `--host` 付きで LAN に公開し、別端末から
`http://192.168.1.32:5173` にアクセスすると、Worker Console の protocol 状態が
`connecting` のまま進まなかった。
原因は WebSocket や Vite proxy ではなく、WebSocket multiplexing の相関 ID 生成に
`crypto.randomUUID()` を直接使っていることである。`http://localhost` はブラウザから
potentially trustworthy origin として扱われる一方、LAN IP 上の平文 HTTP は secure
context ではない。`crypto.randomUUID()` は secure context 限定なので、LAN アクセスでは
接続処理が WebSocket の生成前に例外終了する。
## 発生経路
Worker Console の `connectProtocolTransport` は、最初に `protocolState`
`"connecting"` に設定してから `WorkspaceMultiplexer.subscribe()` を呼ぶ。
- `web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte`
- `protocolState = "connecting"`
- 直後に `workspaceMultiplexer(...).subscribe(...)`
- `web/workspace/src/lib/workspace/multiplexer.ts`
- `subscribe()` の先頭で `const clientId = crypto.randomUUID()`
- その後の `#ensureConnected()` で初めて `new WebSocket(url)` を実行する
このため `crypto.randomUUID()` が利用できないブラウザでは、画面状態だけが
`connecting` に更新された後に同期例外が発生し、WebSocket request 自体が送信されない。
接続失敗を示す `closed` や diagnostic にも遷移しないため、見た目からは WebSocket の
接続待ちに見える。
同じ multiplexer では `crypto.randomUUID()` を次の3用途に使っている。
1. ブラウザ内で subscription を区別する `clientId`
2. `subscribe_events` request と response を対応付ける `request_id`
3. `unsubscribe_events``request_id`
いずれも暗号化や認証のためではなく、ローカル Map のキーまたはプロトコル上の相関 ID
である。secure-context-only API を要求する必要性はない。
## 切り分け結果
開発ホスト上から次の条件で `/api/w/<workspace-id>/protocol/ws` に WebSocket upgrade を
送ると、backend への直接接続と Vite の `5173` proxy 経由の両方で
`101 Switching Protocols` が返った。
- `Host: 192.168.1.32:5173`
- `Origin: http://192.168.1.32:5173`
したがって、現在の Vite 設定にある `proxy["/api"].ws = true` と、loopback に bind した
backend への proxy はこの現象の直接原因ではない。
## 改善案
- `WorkspaceMultiplexer` の client/request correlation ID は、module-local の単調増加
sequence から生成する。これらは1ページ・1接続内で重複しなければよく、暗号学的乱数や
永続的なglobal uniquenessは不要なので、`crypto``Math.random()`、時刻依存のfallbackを
使わない。
- `WorkspaceMultiplexer.subscribe()` の同期初期化失敗を Console の diagnostic/state に
反映し、初期値の `connecting` に留まらないようにする。
- LAN 上の平文 HTTP を開発時の対応経路とするなら、insecure context から subscription
初期化できることを regression test として固定する。
- 本番相当の LAN 公開では HTTPS を使う。Passkey/WebAuthn も secure context を要求し、
現在の開発設定は `rp_id = localhost` なので、LAN origin を正式対応する場合は auth の
origin/RP ID 設計も別途必要になる。
## 補足
`--host` は Vite の listener を LAN に bind するだけであり、配信 origin を secure
context に変えるものではない。localhost で正常に動くことだけでは、LAN IP の HTTP
アクセスでも同じブラウザ API が利用できることを証明できない。
## Resolution (2026-08-06)
`WorkspaceMultiplexer` の client/request ID をmodule-localの単調増加sequenceへ変更した。
これによりWebSocket接続開始経路からsecure-context限定の `crypto.randomUUID()` 依存を除去した。
IDはlocal correlationにのみ使われ、認証・認可・capability authorityには使われない。