WebSocket upgrade to an https upstream returns 501 #45

Closed
opened 2026-10-22 16:27:26 +00:00 by james.oates · 0 comments
Owner

Spec: obsidian/Personal/Projects/edge-router/specs/2026-10-22-websocket-to-https-upstream-design.md

A WebSocket upgrade to a service with target_scheme = "https" returns 501. The refusal is currently correct, not a bug: proxy_websocket opens a raw TcpStream and hands it to hyper, because after the 101 it is a byte tunnel with nothing HTTP-shaped left to route — so there is no TLS layer on that path, and connecting plaintext to a TLS port would be worse than failing.

The fix is a tokio_rustls client layer beneath the existing upgrade logic, which is already generic over the stream type.

The part worth care is not the TLS wrapping (a dozen lines) but that upstream::apply_ca_pinning — the single source of truth for what an https upstream trusts, deliberately shared by the proxy client and the health probe — takes a reqwest::ClientBuilder, while tokio_rustls needs a rustls::ClientConfig. Two independent CA-loading paths would drift, and the result would be the WebSocket route trusting public roots while the proxy route is pinned to an internal CA: a silent, partial downgrade of the same class as #38. So the decision gets factored out of the application, with both shapes as thin adapters over one function.

Blocks Roku's browser TLS on 2443.

Scheduled for the same release as the per-name ACME work (feat/per-name-acme-httpreq) by agreement, though it is an independent change.

Spec: `obsidian/Personal/Projects/edge-router/specs/2026-10-22-websocket-to-https-upstream-design.md` A WebSocket upgrade to a service with `target_scheme = "https"` returns 501. The refusal is currently correct, not a bug: `proxy_websocket` opens a raw `TcpStream` and hands it to hyper, because after the 101 it is a byte tunnel with nothing HTTP-shaped left to route — so there is no TLS layer on that path, and connecting plaintext to a TLS port would be worse than failing. The fix is a `tokio_rustls` client layer beneath the existing upgrade logic, which is already generic over the stream type. **The part worth care** is not the TLS wrapping (a dozen lines) but that `upstream::apply_ca_pinning` — the single source of truth for what an https upstream trusts, deliberately shared by the proxy client and the health probe — takes a `reqwest::ClientBuilder`, while `tokio_rustls` needs a `rustls::ClientConfig`. Two independent CA-loading paths would drift, and the result would be the WebSocket route trusting public roots while the proxy route is pinned to an internal CA: a silent, partial downgrade of the same class as #38. So the decision gets factored out of the application, with both shapes as thin adapters over one function. Blocks Roku's browser TLS on 2443. Scheduled for the same release as the per-name ACME work (`feat/per-name-acme-httpreq`) by agreement, though it is an independent change.
Sign in to join this conversation.
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
IsoHex/edge-router#45
No description provided.