The 'WebTransport proxy' concept was conflating two distinct things; this pass separates them: 1. In-process ALPN-stream-proxy (ADR-040, in alknet-http): the h3 handler hands a WebTransport stream to another ALPN handler (SshAdapter, GitAdapter, etc.) as a Connection, so a browser with a WASM parser can reach any ALPN service via WebTransport. Path-based routing (the CONNECT path declares the target: /alknet/ssh -> SshAdapter). HttpAdapter gains Arc<HandlerRegistry> for the lookup. The browser's WASM parser implements BiStream (ADR-007) over the WebTransport stream. SSH-over-WebTransport is HTTPS-shaped at the network layer (anti-censorship: the 'VPN-like without being a VPN' use case on a clean foundation). russh-sftp demonstrates WASM targeting is feasible; SSH is the next target. 2. Standalone relay service (OQ-38, future alknet-relay crate): a full relay - fork of iroh-relay - with WebTransport proxy fallback for NAT traversal. This is infrastructure, not a mode of the h3 handler. OQ-38 reframed to be the standalone-relay scope question (distinct from the in-process proxy now resolved by ADR-040). webtransport.md updated: three stream destinations (call protocol, ALPN-handler proxy, other sub-protocols) with path-based routing; new 'ALPN-stream-proxy' section covering the WASM client side, auth model (bearer token gates the session; protocol's own auth gates the protocol session), and the HandlerRegistry reference. README/overview ADR tables and OQ summaries updated for ADR-040.
14 KiB
ADR-040: WebTransport ALPN-Stream-Proxy
Status
Proposed
Context
alknet-http's h3 handler serves browsers over WebTransport. The
existing specs (webtransport.md,
ADR-038) describe two
stream destinations within a WebTransport session:
- Call-protocol
EventEnvelope→ the call protocol'sDispatcher - HTTP/3 requests → the axum
Router(ADR-036)
But there is a third, more important use case that the specs did not capture: a browser opening a WebTransport stream to speak a different ALPN protocol directly — SSH, git, SFTP — with a WASM parser on the browser side. This is the "VPN-like without being a VPN" use case the project was originally built for, now on a clean architectural foundation.
The use case
A browser connects to a hub over WebTransport (h3, X.509). It wants
to reach the hub's SSH service (or git, or SFTP). It cannot open a
quinn connection on ALPN alknet/ssh from the browser — browsers
don't speak raw QUIC with arbitrary ALPNs, they speak WebTransport. But
a WebTransport bidirectional stream is a QUIC bidirectional stream
(ADR-012), and the BiStream trait (AsyncRead + AsyncWrite + Send + Unpin, ADR-007) was designed so a browser can implement it over a
WebTransport stream. So the browser:
- Opens a WebTransport session to the hub.
- Creates a bidirectional stream.
- Runs a WASM parser for the target protocol (SSH, SFTP, etc.) that
reads/writes the WebTransport stream as a
BiStream.
The hub's h3 handler needs to hand that WebTransport stream to the
target ALPN handler (e.g., SshAdapter) as if it were a QUIC stream
arriving on that ALPN. The h3 handler becomes an ALPN-stream-proxy:
a browser-side gateway that gives browsers access to any ALPN handler
via WebTransport.
Why this matters
- SSH is hard to block. SSH can run over TLS, QUIC streams, or any stream. A browser running a WASM SSH client over WebTransport is indistinguishable from normal HTTPS traffic at the network layer (WebTransport is HTTP/3 over QUIC over UDP, the same as HTTP/3). This is the anti-censorship property: the protocol that governments most want to block (VPN-like connectivity) rides on the protocol they can't block (HTTPS/HTTP/3) without breaking the web.
- The browser is the universal client. With WASM parsers for the
target protocols, a browser becomes a full alknet client — SSH shell,
SFTP file browser, git client — without installing anything. The
h3handler's ALPN-stream-proxy is what makes this possible: it bridges the browser's WebTransport streams to the server's ALPN handlers. - WASM parsers are feasible.
russh-sftp's protocol parsing already targets WASM; there's no conceptual reason SSH itself can't. TheBiStreamtrait's design (ADR-007) preserves the WASM door specifically for this — a browser implementsBiStreamover a WebTransport stream, and the WASM parser speaks the protocol over it.
The structural question
The h3 handler proxying a WebTransport stream to another ALPN
handler requires the HttpAdapter to have access to the
HandlerRegistry (or a subset of it) — to look up the target ALPN
handler and hand the stream to its handle() method. The current
HttpAdapter (per http-server.md) has
Arc<dyn IdentityProvider> and Arc<OperationRegistry>, but not the
HandlerRegistry. This is a structural relationship the HTTP handler
didn't need before; the ALPN-stream-proxy requires it.
This is a one-way door: once browsers build WASM clients that reach SSH
(or git, or SFTP) via WebTransport, removing the proxy path breaks
them. The stream-routing contract (how the h3 handler decides which
ALPN handler a WebTransport stream targets) is the published interface
that WASM clients build against.
Decision
1. The h3 handler is an ALPN-stream-proxy for browser access to any ALPN handler
A WebTransport session opened by a browser can carry streams targeting
any ALPN handler, not just the call protocol. The h3 handler's
stream-routing within a WebTransport session has three destinations:
- Call-protocol
EventEnvelope→ the call protocol'sDispatcher(the existing path, webtransport.md). - ALPN-handler proxy → the
h3handler looks up the target ALPN handler in theHandlerRegistry, wraps the WebTransport stream as aConnection, and calls the handler'shandle()method — as if the stream had arrived on that ALPN. The browser's WASM parser speaks the target protocol directly over the stream. This is the ALPN-stream-proxy. - Other sub-protocols — sessions may carry other framing conventions; the session's purpose is declared at CONNECT time.
2. Stream routing: the session's CONNECT path declares the target
The h3 handler determines the target ALPN for a WebTransport session
at CONNECT time, from the session request's path/origin — not by
peeking the first application frame. A browser opens a WebTransport
session to:
https://hub.example.com/(or/alknet/call) → call-protocol session (destination 1).https://hub.example.com/alknet/ssh→ SSH-proxy session (destination 2); streams within this session are handed to theSshAdapter.https://hub.example.com/alknet/git→ git-proxy session; streams →GitAdapter.
The path is the routing key. The first-frame tag (EventEnvelope vs. raw SSH bytes) is a belt-and-suspenders check, not the routing mechanism — the session's CONNECT path already declared the target. This is the same principle as the HTTP/3-request-vs-WebTransport-session distinction (framing layer, not application bytes).
3. The HttpAdapter gains a HandlerRegistry reference
The HttpAdapter struct gains Arc<HandlerRegistry> (or an
equivalent mechanism for looking up ALPN handlers) so the h3 handler
can dispatch WebTransport streams to the target ALPN handler. This is
the structural change the ALPN-stream-proxy requires. The h2/
http/1.1 path does not use it (those handlers serve HTTP, not
ALPN-proxy streams); the HandlerRegistry reference is only used by
the h3 handler's WebTransport session routing.
pub struct HttpAdapter {
identity_provider: Arc<dyn IdentityProvider>,
registry: Arc<OperationRegistry>,
handlers: Arc<HandlerRegistry>, // NEW — for the h3 ALPN-stream-proxy
decoy: DecoyConfig,
}
The assembly layer constructs the HttpAdapter with the
HandlerRegistry it already builds for the endpoint — no new registry,
no new construction path. The HttpAdapter is registered in the same
HandlerRegistry it holds a reference to (a reference cycle that is
broken by the endpoint owning both, not by the handler owning itself).
4. The browser's WASM parser is the client-side implementation
The h3 handler's ALPN-stream-proxy hands a WebTransport stream to the
target ALPN handler as a Connection. The browser side runs a WASM
parser for the target protocol (SSH, SFTP, git) that reads/writes the
WebTransport stream as a BiStream. The BiStream trait (ADR-007) is
the contract: a browser implements BiStream over a WebTransport
stream, and the WASM parser speaks the protocol over it.
The WASM parsers are not part of alknet-http — they are separate
artifacts (the SSH WASM client, the SFTP WASM client, the git WASM
client) built against the BiStream contract and the target protocol's
wire format. alknet-http's job is the server-side proxy path; the
browser-side WASM is downstream. The russh-sftp protocol parsing
already targets WASM, demonstrating feasibility; SSH is the same
pattern.
5. Auth for proxied ALPN sessions
A browser opening a WebTransport session to /alknet/ssh authenticates
by bearer token on the WebTransport session request (the HTTP
Authorization header on the CONNECT request), resolved by the hub's
IdentityProvider::resolve_from_token — same as any other browser
connection (ADR-034 §4). The browser is not an alknet peer (no
PeerId). The target ALPN handler (SshAdapter) receives the
Connection and AuthContext from the h3 handler; the AuthContext
carries the bearer-token-resolved Identity. The SSH session then
proceeds with its own auth (the browser's WASM SSH client does SSH
key exchange over the WebTransport stream, same as a native SSH client
would over a QUIC stream).
The bearer token gates the WebTransport session (does the browser
have access to this hub at all?); the SSH protocol's own auth gates
the SSH session (does this SSH identity have access to this shell?).
Two layers, same as a native alknet/ssh connection.
Consequences
Positive:
- The browser is a universal alknet client. With WASM parsers for the
target protocols, a browser can SSH, SFTP, git, and call — all over
WebTransport, all through the
h3handler's ALPN-stream-proxy. No install, no native client, no VPN. - The anti-censorship property is real: SSH-over-WebTransport is HTTPS-shaped at the network layer. Blocking it requires blocking HTTP/3, which breaks the web. This is the "VPN-like without being a VPN" use case, now on a clean architectural foundation.
- The
BiStreamtrait (ADR-007) pays off. The WASM door it preserved is exactly what the browser-side WASM parsers use. The design decision to keepBiStreama trait (not a concrete quinn type) was made for this use case; this ADR is where it's exercised. - The
h3handler's stream-routing is path-based (the CONNECT path declares the target ALPN), not first-frame-peeking. This is the same principle as ALPN dispatch (ADR-001 — the TLS layer routes, no byte-peeking) applied to WebTransport sessions.
Negative:
- The
HttpAdaptergains aHandlerRegistryreference. This is a structural change to the handler's construction (the assembly layer passes the registry) and a reference cycle (the handler is registered in the registry it holds). The cycle is benign (the endpoint owns both; the handler doesn't look itself up), but it's a structural property worth noting. - The ALPN-stream-proxy path is only available over
h3(WebTransport), noth2/http/1.1. Browsers that don't support WebTransport cannot use it. This is inherent —h2/http/1.1don't have bidirectional streams that map toBiStream. The SSE projection (ADR-036) is theh2/http/1.1fallback for the call protocol; there is noh2/http/1.1fallback for ALPN-stream-proxy. - The WASM parsers (SSH, SFTP, git) are downstream artifacts not built
by
alknet-http. The server-side proxy path is in scope; the browser-side WASM is a separate build per protocol.russh-sftp's WASM targeting demonstrates feasibility; SSH is the next target.
Assumptions
-
The session's CONNECT path is the routing key. A browser opens a WebTransport session to
/alknet/sshto target the SSH handler. The path declares the target; the first-frame tag is a confirmation, not the routing mechanism. If a future use case requires path-independent routing (a session that multiplexes ALPNs by first-frame), the model needs extension. -
The target ALPN handler accepts a proxied
Connection. TheSshAdapter(orGitAdapter,SftpAdapter) receives aConnectionwrapped from a WebTransport stream and anAuthContextwith the bearer-token-resolvedIdentity. The handler'shandle()method works the same as on a native QUIC connection — theConnectionabstraction (ADR-007) is what makes this work. If a handler assumes itsConnectioncame from a specific QUIC source (quinn vs iroh vs WebTransport-proxied), it breaks the proxy. TheConnectiontype must remain source-agnostic. -
The WASM parsers are feasible for the target protocols.
russh-sftpdemonstrates WASM targeting for SFTP. SSH is the next target; the protocol parsing is stream-based and should target WASM. Git (gix) is a larger question (git's smart protocol is more complex). The assumption is that the protocols worth proxying (SSH, SFTP) have WASM-feasible parsers; if a protocol doesn't, its ALPN-stream-proxy path is not usable from a browser (but is still usable from a non-browser WebTransport client). -
The
HandlerRegistryreference is read-only for theh3handler. Theh3handler looks up ALPN handlers in the registry; it does not mutate the registry. TheHandlerRegistryis static at startup (ADR-010, OQ-04), so theh3handler's lookup is against an immutable registry — noArcSwap, no hot-reload concern.
References
- ADR-001 — ALPN dispatch (the principle the WebTransport path-based routing mirrors: the framing layer routes, no byte-peeking)
- ADR-007 —
BiStreamtrait (the contract the browser-side WASM parsers implement over WebTransport streams) - ADR-010 —
HandlerRegistry(the registry theh3handler looks up ALPN handlers in; static at startup) - ADR-012 — stream-agnostic correlation (a WebTransport stream is a QUIC bidirectional stream)
- ADR-027 —
browsers require X.509 (the
h3handler is domain-hosted) - ADR-034 §4 —
browsers are not alknet peers (bearer token, no
PeerId) - ADR-038 —
h3is first-class (this ADR adds the ALPN-stream-proxy as the third stream destination) crates/http/webtransport.md— the spec that implements this proxycrates/core/endpoint.md—HandlerRegistry(the registry theh3handler gains a reference to)