From 398e3d512d72df18845ec8ad2b5a40a7d040bb59 Mon Sep 17 00:00:00 2001 From: "glm-5.2" Date: Mon, 29 Jun 2026 07:56:35 +0000 Subject: [PATCH] docs(http): add ADR-040 WebTransport ALPN-stream-proxy and reframe OQ-38 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 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. --- docs/architecture/README.md | 5 +- docs/architecture/crates/http/README.md | 10 +- docs/architecture/crates/http/overview.md | 1 + docs/architecture/crates/http/webtransport.md | 153 +++++++--- .../040-webtransport-alpn-stream-proxy.md | 280 ++++++++++++++++++ docs/architecture/open-questions.md | 55 ++-- 6 files changed, 444 insertions(+), 60 deletions(-) create mode 100644 docs/architecture/decisions/040-webtransport-alpn-stream-proxy.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 32dc167..23c0f38 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -18,7 +18,7 @@ The storage and auth strategy research (`docs/research/alknet-storage-strategy/f The alknet-call crate is **implemented and reviewed** — both the server-side core and the client/adapter surface (207 lib + 2 integration tests passing). The alknet-core and alknet-call crate specs are in draft; the alknet-vault crate specs are stable. -**alknet-http specs drafted.** The alknet-http crate (HTTP interface — `h2`/`http/1.1`/`h3` server + `from_openapi`/`to_openapi`/`from_mcp`/`to_mcp` adapters) now has architecture specs: [crates/http/](crates/http/) (overview, http-server, http-adapters, http-mcp, webtransport) and four new ADRs — [ADR-036](decisions/036-http-to-call-operation-mapping.md) (HTTP-to-call mapping), [ADR-037](decisions/037-mcp-stdio-transport-exclusion.md) (MCP stdio exclusion), [ADR-038](decisions/038-http3-and-webtransport-as-first-class.md) (HTTP/3 + WebTransport as first-class, correcting the Phase 0 deferral framing), [ADR-039](decisions/039-http-server-and-client-host-colocated.md) (HTTP server + client host colocated in one crate). ADR-003 Amendment 1 clarifies that `alknet-call` is a protocol-foundation crate (the `alknet-http` → `alknet-call` dependency edge). The specs are in draft; implementation has not started. Three open questions carried: OQ-38 (WebTransport relay-as-proxy scope), OQ-39 (`to_openapi` published-spec versioning), OQ-40 (reqwest client config). +**alknet-http specs drafted.** The alknet-http crate (HTTP interface — `h2`/`http/1.1`/`h3` server + `from_openapi`/`to_openapi`/`from_mcp`/`to_mcp` adapters) now has architecture specs: [crates/http/](crates/http/) (overview, http-server, http-adapters, http-mcp, webtransport) and five new ADRs — [ADR-036](decisions/036-http-to-call-operation-mapping.md) (HTTP-to-call mapping), [ADR-037](decisions/037-mcp-stdio-transport-exclusion.md) (MCP stdio exclusion), [ADR-038](decisions/038-http3-and-webtransport-as-first-class.md) (HTTP/3 + WebTransport as first-class, correcting the Phase 0 deferral framing), [ADR-039](decisions/039-http-server-and-client-host-colocated.md) (HTTP server + client host colocated in one crate), [ADR-040](decisions/040-webtransport-alpn-stream-proxy.md) (WebTransport ALPN-stream-proxy — browser → WebTransport stream → any ALPN handler via WASM parser; the "VPN-like without being a VPN" use case). ADR-003 Amendment 1 clarifies that `alknet-call` is a protocol-foundation crate (the `alknet-http` → `alknet-call` dependency edge). The specs are in draft; implementation has not started. Three open questions carried: OQ-38 (WebTransport standalone relay service scope — distinct from the in-process ALPN-stream-proxy resolved by ADR-040), OQ-39 (`to_openapi` published-spec versioning), OQ-40 (reqwest client config). **Next step**: The storage/repo-pattern ADRs (030–033) are accepted and amend the core and call specs. The next implementation phase is the ADR-029 migration (peer-keyed overlays, `PeerRef` routing, retire `remote_safe`/`trusted_peer`) with the ADR-030 `PeerEntry` change and the ADR-032 `forwarded_for` field folded in — the `OperationContext`, `from_call` handler, and `AuthPolicy` are all under edit, making this the cheapest window. After that: alknet-http implementation (specs drafted, ADRs 036–038 proposed), which consumes the `CredentialStore` trait and the `OperationAdapter` contract. The alknet-ssh crate (the other post-core crate, specced in parallel) proceeds independently — it depends on `alknet-core`, not `alknet-call`. @@ -92,6 +92,7 @@ The alknet-call crate is **implemented and reviewed** — both the server-side c | [037](decisions/037-mcp-stdio-transport-exclusion.md) | MCP Stdio Transport Exclusion | Proposed | | [038](decisions/038-http3-and-webtransport-as-first-class.md) | HTTP/3 and WebTransport as First-Class HTTP Transports | Proposed | | [039](decisions/039-http-server-and-client-host-colocated.md) | HTTP Server and Client Host Colocated in alknet-http | Proposed | +| [040](decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | Proposed | ## Open Questions @@ -139,7 +140,7 @@ See [open-questions.md](open-questions.md) for the full tracker. - **OQ-32**: Multi-hop federation — the one-hop model is the architectural commitment; multi-hop is a feature extension that doesn't break downstream - **OQ-36**: ~~Concrete persistence adapter shapes~~ — **resolved by ADR-035** (read-sync / write-async / honker-NOTIFY cache invalidation; `alknet-store-sqlite` crate; `IdentityStore` write trait; `CredentialStore::put`/`delete` async) - **OQ-37**: ~~X.509 outgoing-only case~~ — **resolved by ADR-034** (three remote roles named: public X.509 endpoint, transport relay, hub; `PeerEntry` asymmetry is correct; client-side verifier selection by `PeerEntry` presence) -- **OQ-38**: WebTransport relay-as-proxy scope — does the proxy live in `alknet-http` or a separate relay crate? (scope question, not deferral; ADR-038 brought h3 into scope) +- **OQ-38**: WebTransport standalone relay service scope — the standalone relay (future `alknet-relay`, fork of iroh-relay with WebTransport proxy fallback) is distinct from the in-process ALPN-stream-proxy (ADR-040); scope question, not deferral - **OQ-39**: `to_openapi` published-spec versioning — versioning strategy for generated OpenAPI specs (one-way after first publication) - **OQ-40**: reqwest client config and connection pooling — two-way-door config shape for the outbound HTTP client diff --git a/docs/architecture/crates/http/README.md b/docs/architecture/crates/http/README.md index 3377957..84b7012 100644 --- a/docs/architecture/crates/http/README.md +++ b/docs/architecture/crates/http/README.md @@ -40,6 +40,7 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters | [037](../../decisions/037-mcp-stdio-transport-exclusion.md) | MCP Stdio Transport Exclusion | Streamable HTTP only; stdio not built | | [038](../../decisions/038-http3-and-webtransport-as-first-class.md) | HTTP/3 and WebTransport as First-Class HTTP Transports | `h3` in scope, not deferred | | [039](../../decisions/039-http-server-and-client-host-colocated.md) | HTTP Server and Client Host Colocated in alknet-http | One crate for server + client host (shared HTTP deps, shared mapping) | +| [040](../../decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | Browser → WebTransport stream → any ALPN handler (SSH, git, SFTP) via WASM parser | ## Relevant Open Questions @@ -52,7 +53,7 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters | OQ-24 | Operation error schemas | resolved | `from_openapi`/`to_openapi` error fidelity | | OQ-26 | OperationAdapter error type | resolved | `AdapterError` variants reused by HTTP adapters | | OQ-37 | X.509 outgoing-only / three peer roles | resolved | Browsers are not peers; hub with mixed fingerprints | -| OQ-38 | WebTransport relay-as-proxy scope | open (scope, not deferral) | Does the proxy live in `alknet-http` or a separate relay crate? | +| OQ-38 | WebTransport standalone relay service scope | open (scope, not deferral) | The standalone relay (future `alknet-relay`, fork of iroh-relay) — distinct from the in-process ALPN-stream-proxy (ADR-040) | | OQ-39 | `to_openapi` published-spec versioning | open | Versioning strategy for generated OpenAPI specs | | OQ-40 | reqwest client config and connection pooling | open | Two-way-door: pooling/retry config shape | @@ -85,7 +86,12 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters 6. **HTTP/3 + WebTransport is a first-class transport, not a deferral.** The browser streaming path uses QUIC streams directly. See [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md). -7. **`h3` requires X.509.** Browsers don't support RFC 7250 raw keys +7. **The `h3` handler is an ALPN-stream-proxy for browsers.** A browser + with a WASM parser can reach any ALPN handler (SSH, git, SFTP) via + WebTransport — no install, no native client, no VPN. SSH-over- + WebTransport is HTTPS-shaped at the network layer (anti-censorship). + See [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md). +8. **`h3` requires X.509.** Browsers don't support RFC 7250 raw keys (ADR-027). A node serving WebTransport must have an X.509 identity. This is a browser limitation, not an alknet decision. diff --git a/docs/architecture/crates/http/overview.md b/docs/architecture/crates/http/overview.md index 9de05b5..6c19bf4 100644 --- a/docs/architecture/crates/http/overview.md +++ b/docs/architecture/crates/http/overview.md @@ -205,6 +205,7 @@ verified against this invariant. See ADR-014 and | MCP stdio transport exclusion | [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md) | Streamable HTTP only; stdio is not built (RCE vector) | | HTTP/3 + WebTransport first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | `h3` in scope, not deferred; browser streaming uses QUIC streams | | HTTP server + client host colocated | [ADR-039](../../decisions/039-http-server-and-client-host-colocated.md) | One crate for server + adapters (shared HTTP deps, shared mapping) | +| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | Browser → WebTransport stream → any ALPN handler (SSH, git, SFTP) via WASM parser | | `alknet-call` is protocol-foundation | [ADR-003](../../decisions/003-crate-decomposition.md) Am. 1 | `alknet-http` depends on `alknet-call` (types, not peer handler) | | Bearer auth via `resolve_from_token` | [ADR-004](../../decisions/004-auth-as-shared-core.md) | HTTP handler credential source + resolution (settled) | | Stealth mode = HTTP handler on standard ALPNs | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Decoy for unknown paths (settled) | diff --git a/docs/architecture/crates/http/webtransport.md b/docs/architecture/crates/http/webtransport.md index c5aa319..5e76640 100644 --- a/docs/architecture/crates/http/webtransport.md +++ b/docs/architecture/crates/http/webtransport.md @@ -7,9 +7,10 @@ last_updated: 2026-06-29 The `HttpAdapter` registration for the `h3` ALPN: HTTP/3 and WebTransport. This document covers the WebTransport session/stream -handling, the browser streaming path, and the relationship to the `h2`/ -`http/1.1` server. The `h3` support is a first-class transport, not a -deferral (ADR-038). +handling, the browser streaming path, the ALPN-stream-proxy (browser +access to any ALPN handler via WebTransport), and the relationship to +the `h2`/`http/1.1` server. The `h3` support is a first-class transport, +not a deferral (ADR-038). ## What @@ -25,11 +26,20 @@ enabled. It serves two things on a single `h3` connection: router. 2. **WebTransport sessions** — the browser streaming path. A WebTransport session is a long-lived connection over which the browser opens - bidirectional and unidirectional streams. A WebTransport stream that - targets the call protocol is handed to the call protocol's dispatch - loop directly — a WebTransport bidirectional stream is a QUIC - bidirectional stream, the same stream type the call protocol already - speaks (ADR-012). + bidirectional and unidirectional streams. Streams within a session + target one of three destinations (see [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md)): + - The call protocol (`EventEnvelope` → the call `Dispatcher`), + - An ALPN handler proxy (the stream is handed to another ALPN + handler like `SshAdapter` — the browser runs a WASM parser for the + target protocol), or + - Another sub-protocol (declared at CONNECT time). + +The ALPN-stream-proxy is what makes the browser a universal alknet +client: with a WASM parser for SSH (or SFTP, git), a browser can reach +any ALPN handler via WebTransport, no install, no native client, no +VPN. This is the "VPN-like without being a VPN" use case the project +was originally built for, now on a clean foundation. See +[ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md). ### Why h3 is a first-class transport @@ -80,31 +90,48 @@ HTTP/3 request and a WebTransport stream. ### WebTransport session and stream handling Once a WebTransport session is established (via extended CONNECT), the -browser creates bidirectional streams within it. The handler accepts -each stream (`session.accept_bi()`) and reads the first frame to -determine the sub-protocol: +browser creates bidirectional streams within it. The handler dispatches +each stream to one of three destinations, determined by the session's +CONNECT path (the routing key, declared at CONNECT time — not by peeking +the first application frame): -- **Call-protocol `EventEnvelope`** — the stream is a call-protocol - stream. The handler hands the `(SendStream, RecvStream)` pair to the - call protocol's `Dispatcher` (see [../call/call-protocol.md](../call/call-protocol.md) +- **`/` or `/alknet/call` → call-protocol session.** Each bidirectional + stream carries call-protocol `EventEnvelope` frames. The handler + hands the `(SendStream, RecvStream)` pair to the call protocol's + `Dispatcher` (see [../call/call-protocol.md](../call/call-protocol.md) for `EventEnvelope` and [../call/client-and-adapters.md](../call/client-and-adapters.md) §"Shared Dispatcher" for the `Dispatcher` — the same dispatch loop the `CallAdapter` uses for `alknet/call` connections, ADR-012, stream-agnostic correlation). The browser speaks the EventEnvelope wire format directly over the WebTransport stream. -- **Other sub-protocols** — a session may carry other framing - conventions (e.g., a future WT-native RPC framing). The session's - purpose is declared at CONNECT time (by path/origin), so the handler - knows which sub-protocol to expect; the first-frame tag is a - belt-and-suspenders disambiguator for sessions that multiplex - sub-protocols. For the call-protocol session, the first frame is an - `EventEnvelope` JSON object; the handler dispatches accordingly. +- **`/alknet/` → ALPN-handler proxy session.** Each bidirectional + stream is handed to the target ALPN handler (e.g., `SshAdapter` for + `/alknet/ssh`, `GitAdapter` for `/alknet/git`) as a `Connection` + wrapping the WebTransport stream. The browser runs a WASM parser for + the target protocol and speaks it directly over the stream. This is + the ALPN-stream-proxy — see + [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md). + The `h3` handler looks up the target ALPN handler in the + `HandlerRegistry` (`HttpAdapter` holds `Arc` for + this purpose), wraps the WebTransport stream as a `Connection`, and + calls `handler.handle(connection, &auth)`. The target handler runs + its normal protocol over the stream — SSH key exchange, git smart + protocol, SFTP — exactly as if the stream had arrived on that ALPN + via a native QUIC connection. +- **Other paths → other sub-protocols.** Sessions may carry other + framing conventions; the session's purpose is declared at CONNECT + time by path/origin. The first-frame tag is a belt-and-suspenders + confirmation for sessions that multiplex sub-protocols, not the + routing mechanism. The browser's `WebTransport` JS API is the client side of this: -`new WebTransport('https://hub.example.com')` → -`transport.createBidirectionalStream()` → write an `EventEnvelope` frame -→ read `call.responded` frames. No SSE translation, no HTTP framing — -the call protocol speaks directly over the WebTransport stream. +`new WebTransport('https://hub.example.com/alknet/ssh')` → +`transport.createBidirectionalStream()` → the browser's WASM SSH client +reads/writes the stream as a `BiStream` (ADR-007). No SSE translation, +no HTTP framing — the target protocol speaks directly over the +WebTransport stream. For the call-protocol session, the browser writes +`EventEnvelope` frames; for an SSH session, the browser runs the WASM +SSH parser. ### Subscription projection (native, not SSE) @@ -116,6 +143,54 @@ closes the stream with an error frame. This is the native streaming projection; SSE (ADR-036) is the projection for `h2`/`http/1.1` clients that don't speak WebTransport. +### ALPN-stream-proxy (ADR-040) + +The ALPN-stream-proxy is the `h3` handler's third stream destination and +the browser's gateway to every ALPN handler. A browser opens a +WebTransport session to `/alknet/ssh` (or `/alknet/git`, `/alknet/sftp`), +and the `h3` handler hands each bidirectional stream within that session +to the target ALPN handler as a `Connection`. The browser runs a WASM +parser for the target protocol and speaks it directly over the stream. + +**Why this matters:** SSH-over-WebTransport is HTTPS-shaped at the +network layer (WebTransport is HTTP/3 over QUIC over UDP, the same as +HTTP/3). Blocking it requires blocking HTTP/3, which breaks the web. +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 without breaking the web. This is the "VPN-like without +being a VPN" use case on a clean foundation. + +**The WASM client side:** the browser's WASM parser for the target +protocol (SSH, SFTP, git) reads/writes the WebTransport stream as a +`BiStream` (ADR-007). The `BiStream` trait (`AsyncRead + AsyncWrite + +Send + Unpin`) was designed for this — a browser implements it over a +WebTransport stream, and the WASM parser speaks the protocol over it. +The WASM parsers are downstream artifacts (the SSH WASM client, the +SFTP WASM client), not part of `alknet-http`; `russh-sftp`'s WASM +targeting demonstrates feasibility, SSH is the next target. + +**Auth for proxied ALPN sessions:** the browser 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 receives the `Connection` and +`AuthContext` from the `h3` handler; the `AuthContext` carries the +bearer-token-resolved `Identity`. The target protocol then runs its +own auth (the browser's WASM SSH client does SSH key exchange over the +WebTransport stream, same as a native SSH client over a QUIC stream). +Two layers: the bearer token gates the WebTransport session (does the +browser have access to this hub?); the protocol's own auth gates the +protocol session (does this SSH identity have access to this shell?). + +**The `HandlerRegistry` reference:** the `HttpAdapter` holds +`Arc` so the `h3` handler can look up the target ALPN +handler. The assembly layer constructs the `HttpAdapter` with the +`HandlerRegistry` it already builds for the endpoint — no new +registry, no new construction path. The `HandlerRegistry` is static at +startup (ADR-010), so the lookup is against an immutable registry. See +[ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md). + ### The TLS constraint (browsers require X.509) Browsers do not support RFC 7250 raw public keys (ADR-027, OQ-12). A @@ -184,11 +259,15 @@ as a first-class transport. non-browser-facing deployments don't compile it. - **Browsers are not alknet peers.** A browser over WebTransport authenticates by bearer token, gets no `PeerId` (ADR-034 §4). -- **WebTransport streams target the call protocol directly.** A - WebTransport bidirectional stream carrying an `EventEnvelope` is - handed to the call protocol's `Dispatcher` — no SSE translation, no - HTTP framing. The browser speaks the call protocol wire format - directly. +- **WebTransport streams target one of three destinations** (the + session's CONNECT path is the routing key): the call protocol + (`EventEnvelope` → `Dispatcher`), an ALPN handler proxy (→ + `HandlerRegistry` lookup → target handler's `handle()`), or another + sub-protocol. See ADR-040. +- **The ALPN-stream-proxy requires `Arc` on + `HttpAdapter`.** The `h3` handler looks up ALPN handlers in the + registry; the `h2`/`http/1.1` path does not use it. The registry is + static at startup (ADR-010). - **The HTTP/3 request path uses the same axum `Router` as `h2`/ `http/1.1`.** An HTTP/3 request is just another HTTP request from the router's perspective (ADR-036). @@ -201,9 +280,11 @@ as a first-class transport. | Decision | ADR | Summary | |----------|-----|---------| | `h3`/WebTransport is first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | In scope, not deferred; browser streaming uses QUIC streams | +| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | Browser → WebTransport stream → any ALPN handler (SSH, git, SFTP) via WASM parser | | Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3` needs X.509 (browser limitation) | | Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Bearer token, no `PeerId` | | WebTransport streams → call protocol directly | [ADR-012](../../decisions/012-call-protocol-stream-model.md) | Stream-agnostic; WebTransport stream = QUIC bidirectional stream | +| `BiStream` is a trait (WASM door) | [ADR-007](../../decisions/007-bistream-type-definition.md) | Browser implements `BiStream` over WebTransport stream; WASM parser speaks the protocol | | Stealth on h3 | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Unknown paths get the decoy | | HTTP path = operation path (for HTTP/3 requests) | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | Same axum `Router` as h2/http1.1 | @@ -211,13 +292,11 @@ as a first-class transport. See [open-questions.md](../../open-questions.md) for full details. -- **OQ-38** (open, scope): WebTransport relay-as-proxy — a proxy that - terminates the browser's WebTransport connection and forwards to a - P2P hub's Ed25519 endpoint (so the hub need not expose a public - X.509 cert). Recorded in ADR-034 §5. Does the proxy live in - `alknet-http` or a separate relay crate? This is a genuine scope - question (the proxy use case is not yet concrete enough to decide the - crate boundary), not a deferral. +- **OQ-38** (open, scope): WebTransport relay-as-proxy — the + *standalone relay service* (a future `alknet-relay` crate, fork of + iroh-relay with WebTransport-based proxy fallback). This is distinct + from the in-process ALPN-stream-proxy (ADR-040, in `alknet-http`). + See OQ-38 for the relay crate boundary question. ## References diff --git a/docs/architecture/decisions/040-webtransport-alpn-stream-proxy.md b/docs/architecture/decisions/040-webtransport-alpn-stream-proxy.md new file mode 100644 index 0000000..b1a8c24 --- /dev/null +++ b/docs/architecture/decisions/040-webtransport-alpn-stream-proxy.md @@ -0,0 +1,280 @@ +# ADR-040: WebTransport ALPN-Stream-Proxy + +## Status + +Proposed + +## Context + +`alknet-http`'s `h3` handler serves browsers over WebTransport. The +existing specs ([webtransport.md](../crates/http/webtransport.md), +[ADR-038](038-http3-and-webtransport-as-first-class.md)) describe two +stream destinations within a WebTransport session: + +1. Call-protocol `EventEnvelope` → the call protocol's `Dispatcher` +2. 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: + +1. Opens a WebTransport session to the hub. +2. Creates a bidirectional stream. +3. 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 + `h3` handler'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. + The `BiStream` trait's design (ADR-007) preserves the WASM door + specifically for this — a browser implements `BiStream` over 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](../crates/http/http-server.md)) has +`Arc` and `Arc`, 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: + +1. **Call-protocol `EventEnvelope`** → the call protocol's `Dispatcher` + (the existing path, [webtransport.md](../crates/http/webtransport.md)). +2. **ALPN-handler proxy** → the `h3` handler looks up the target ALPN + handler in the `HandlerRegistry`, wraps the WebTransport stream as a + `Connection`, and calls the handler's `handle()` 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. +3. **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 the + `SshAdapter`. +- `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` (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. + +```rust +pub struct HttpAdapter { + identity_provider: Arc, + registry: Arc, + handlers: Arc, // 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 `h3` handler'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 `BiStream` trait (ADR-007) pays off. The WASM door it preserved + is exactly what the browser-side WASM parsers use. The design + decision to keep `BiStream` a trait (not a concrete quinn type) was + made for this use case; this ADR is where it's exercised. +- The `h3` handler'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 `HttpAdapter` gains a `HandlerRegistry` reference. 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), + not `h2`/`http/1.1`. Browsers that don't support WebTransport cannot + use it. This is inherent — `h2`/`http/1.1` don't have bidirectional + streams that map to `BiStream`. The SSE projection (ADR-036) is the + `h2`/`http/1.1` fallback for the call protocol; there is no + `h2`/`http/1.1` fallback 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 + +1. **The session's CONNECT path is the routing key.** A browser opens + a WebTransport session to `/alknet/ssh` to 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. + +2. **The target ALPN handler accepts a proxied `Connection`.** The + `SshAdapter` (or `GitAdapter`, `SftpAdapter`) receives a + `Connection` wrapped from a WebTransport stream and an `AuthContext` + with the bearer-token-resolved `Identity`. The handler's `handle()` + method works the same as on a native QUIC connection — the + `Connection` abstraction (ADR-007) is what makes this work. If a + handler assumes its `Connection` came from a specific QUIC source + (quinn vs iroh vs WebTransport-proxied), it breaks the proxy. The + `Connection` type must remain source-agnostic. + +3. **The WASM parsers are feasible for the target protocols.** + `russh-sftp` demonstrates 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). + +4. **The `HandlerRegistry` reference is read-only for the `h3` handler.** + The `h3` handler looks up ALPN handlers in the registry; it does not + mutate the registry. The `HandlerRegistry` is static at startup + (ADR-010, OQ-04), so the `h3` handler's lookup is against an + immutable registry — no `ArcSwap`, no hot-reload concern. + +## References + +- [ADR-001](001-alpn-protocol-dispatch.md) — ALPN dispatch (the + principle the WebTransport path-based routing mirrors: the framing + layer routes, no byte-peeking) +- [ADR-007](007-bistream-type-definition.md) — `BiStream` trait (the + contract the browser-side WASM parsers implement over WebTransport + streams) +- [ADR-010](010-alpn-router-and-endpoint.md) — `HandlerRegistry` + (the registry the `h3` handler looks up ALPN handlers in; static at + startup) +- [ADR-012](012-call-protocol-stream-model.md) — stream-agnostic + correlation (a WebTransport stream is a QUIC bidirectional stream) +- [ADR-027](027-tls-identity-redesign-acme-rawkey-decoupling.md) — + browsers require X.509 (the `h3` handler is domain-hosted) +- [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) §4 — + browsers are not alknet peers (bearer token, no `PeerId`) +- [ADR-038](038-http3-and-webtransport-as-first-class.md) — `h3` is + first-class (this ADR adds the ALPN-stream-proxy as the third stream + destination) +- `crates/http/webtransport.md` — the spec that implements this proxy +- `crates/core/endpoint.md` — `HandlerRegistry` (the registry the + `h3` handler gains a reference to) \ No newline at end of file diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 2c16440..05f26ae 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -754,32 +754,49 @@ is a feature extension, not an unmade architecture decision. ## Theme: alknet-http -### OQ-38: WebTransport Relay-as-Proxy Scope +### OQ-38: WebTransport Standalone Relay Service Scope - **Origin**: [ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md) §5, [webtransport.md](crates/http/webtransport.md) - **Status**: open (scope, not deferral) - **Door type**: One-way (crate boundary), two-way (mechanism) - **Priority**: low -- **Resolution**: A WebTransport proxy that terminates the browser's - WebTransport connection and forwards encrypted traffic to a P2P hub's - Ed25519 endpoint (so the hub need not expose its own public X.509 - cert) is a real feature for the browser-to-P2P-peer case. ADR-034 §5 - recorded it in the h3/WebTransport bucket; ADR-038 brought h3/ - WebTransport into scope, so this OQ is now the remaining scope - question: does the proxy live in `alknet-http` (as a mode of the `h3` - handler) or in a separate relay crate? +- **Resolution**: There are two distinct "WebTransport proxy" concepts + that must not be conflated: - This is a genuine scope question, not a deferral. The proxy use case - is not yet concrete enough to decide the crate boundary — no - deployment has asked for it yet, and the design (transport-only - proxy, no auth-model change per ADR-034 §5) is clear but the home is - not. The decision is made when the browser-to-P2P-peer proxy use - case becomes concrete; until then it is tracked here, not deferred - with "v1/later" language. The proxy does not change the auth model - (bearer token + `PeerEntry.auth_token_hash`; proxy is transport- - only), so it does not block any other ADR. -- **Cross-references**: ADR-027, ADR-030, ADR-034, ADR-038, + 1. **In-process ALPN-stream-proxy (resolved, 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. This is resolved by + [ADR-040](decisions/040-webtransport-alpn-stream-proxy.md) and + lives in `alknet-http`'s `h3` handler. Not this OQ. + + 2. **Standalone relay service (this OQ).** A full relay — a fork of + `iroh-relay` — that provides NAT traversal infrastructure with + WebTransport-based proxy as a fallback alongside websocket. This + is a separate service, not a mode of the `h3` handler: it + terminates the browser's WebTransport connection and forwards + encrypted traffic to a P2P hub's Ed25519 endpoint (so the hub need + not expose its own public X.509 cert). ADR-034 §5 recorded it in + the h3/WebTransport bucket; ADR-038 brought h3/WebTransport into + scope; ADR-040 resolved the in-process proxy. This OQ is the + remaining scope question: does the standalone relay live in a + future `alknet-relay` crate (a fork of `iroh-relay` with + WebTransport proxy fallback) or is it out of scope for the + current alknet work? + + This is a genuine scope question, not a deferral. The relay use case + is not yet concrete enough to commit the crate boundary — no + deployment has asked for a standalone relay with WebTransport + fallback yet, and the design (transport-only proxy, no auth-model + change per ADR-034 §5) is clear but the home is not. The decision is + made when the browser-to-P2P-peer relay use case becomes concrete; + until then it is tracked here, not deferred with "v1/later" language. + The relay does not change the auth model (bearer token + + `PeerEntry.auth_token_hash`; relay is transport-only), so it does not + block any other ADR. +- **Cross-references**: ADR-027, ADR-030, ADR-034, ADR-038, ADR-040, [webtransport.md](crates/http/webtransport.md) ### OQ-39: `to_openapi` Published-Spec Versioning