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<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.
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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/<name>` → 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<HandlerRegistry>` 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<HandlerRegistry>` 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<HandlerRegistry>` 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user