docs(arch): endpoint types and entry points (ADR-086) — resolves OQ-62
Name the three-endpoint-type model (web/native/iroh) and the entry-point vs. endpoint ALPN distinction. This untangles the hub/endpoint/ALPN-config confusion that OQ-62 hinted at: - A hub composes a SUBSET of three endpoint types (web, native, iroh), each with its own identity model, auth model, and transport(s). A full hub runs all three; a minimal hub runs iroh alone (no public IP required). The first real use case is web + native. Corrects the hub README's 'must support TCP+TLS and QUIC' framing. - ALPNs split into entry points (accepted without identity — h2, http/1.1, future alknet/register) and endpoints (identity required before dispatch — alknet/channels, alknet/call, alknet/ssh). This resolves OQ-62: split ALPN lists by endpoint type (Option B), because each endpoint type serves a different client class with different negotiable ALPNs. The assembly-layer wiring pattern is now guessable. - Foundational handlers are two categories, not one: channels data-channel ALPNs (tunnel, socks5, fs, sftp — gated by channels, not in any TLS ALPN list) vs. SSH (an endpoint ALPN that wraps channels inside it, RFC 7250 keys, legacy compat, comes later). Files OQ-65 (WebSocket carrying channels — the browser story update) filed as a one-way-door question, open. ADR-048 is not superseded; OQ-65 may extend it. The web config advertises alknet/channels by default so the hub is ready if OQ-65 resolves to 'WebSocket carries channels.'
This commit is contained in:
1 parent
7610ec1f31
commit
7d9b1ebad9
9 files changed
+800
-129
No files matched your search
@@ -186,8 +186,8 @@ adapter location map is now consistent: all HTTP-backed adapters
|
|||||||
| [crates/vault/encryption.md](crates/vault/encryption.md) | stable | AES-256-GCM, EncryptedData, key versioning, salt (Phase B reserved) |
|
| [crates/vault/encryption.md](crates/vault/encryption.md) | stable | AES-256-GCM, EncryptedData, key versioning, salt (Phase B reserved) |
|
||||||
| [crates/vault/service.md](crates/vault/service.md) | stable | VaultServiceHandle lifecycle, direct dispatch, cache, error model |
|
| [crates/vault/service.md](crates/vault/service.md) | stable | VaultServiceHandle lifecycle, direct dispatch, cache, error model |
|
||||||
| [crates/vault/protocol.md](crates/vault/protocol.md) | stable | DerivedKey redaction, KeyType, serialization behavior |
|
| [crates/vault/protocol.md](crates/vault/protocol.md) | stable | DerivedKey redaction, KeyType, serialization behavior |
|
||||||
| [crates/hub/README.md](crates/hub/README.md) | draft | alknet-hub crate — multi-transport endpoint (TCP+TLS + QUIC), channels substrate (ADR-079 relay), worker registration flow (OQ-58), identity over transports, aggregated peer env, connection lifecycle, service discovery |
|
| [crates/hub/README.md](crates/hub/README.md) | draft | alknet-hub crate — composes a subset of three endpoint types (web/native/iroh — ADR-086), channels substrate (ADR-079 relay), worker registration flow (OQ-58), identity over transports, aggregated peer env, connection lifecycle, service discovery |
|
||||||
| [crates/tls/README.md](crates/tls/README.md) | draft | alknet-tls crate — shared TLS config (`TlsServerConfig`) extractable across quinn + TCP+TLS + iroh; one cert, one ACME state machine, N transports; fixes cert-reuse welding in `alknet-core/endpoint.rs` (ADR-082) |
|
| [crates/tls/README.md](crates/tls/README.md) | draft | alknet-tls crate — shared TLS config (`TlsServerConfig`) extractable across quinn + TCP+TLS + iroh; one cert, one ACME state machine, N transports; split ALPN lists per endpoint type (ADR-086, resolves OQ-62); fixes cert-reuse welding in `alknet-core/endpoint.rs` (ADR-082) |
|
||||||
| [crates/channels/README.md](crates/channels/README.md) | draft | alknet-channels crate — multiplexing proxy, 9-byte chunk format, N channels over one transport stream |
|
| [crates/channels/README.md](crates/channels/README.md) | draft | alknet-channels crate — multiplexing proxy, 9-byte chunk format, N channels over one transport stream |
|
||||||
| [crates/channels/overview.md](crates/channels/overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, transport agnosticism, WASM, relationship to existing crates |
|
| [crates/channels/overview.md](crates/channels/overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, transport agnosticism, WASM, relationship to existing crates |
|
||||||
| [crates/channels/channels-wire.md](crates/channels/channels-wire.md) | draft | 9-byte chunk format, stream types, sentinels, framing disambiguation, wire-level invariants (REQ-CH-01..05) |
|
| [crates/channels/channels-wire.md](crates/channels/channels-wire.md) | draft | 9-byte chunk format, stream types, sentinels, framing disambiguation, wire-level invariants (REQ-CH-01..05) |
|
||||||
@@ -285,10 +285,11 @@ adapter location map is now consistent: all HTTP-backed adapters
|
|||||||
| [083](decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as Multi-Transport Accept-Loop Runner with Public Dispatch | Proposed (revised — TCP+TLS is an owned transport, not external) |
|
| [083](decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as Multi-Transport Accept-Loop Runner with Public Dispatch | Proposed (revised — TCP+TLS is an owned transport, not external) |
|
||||||
| [084](decisions/084-aws-lc-rs-crypto-provider.md) | aws-lc-rs as the TLS Crypto Provider | Accepted |
|
| [084](decisions/084-aws-lc-rs-crypto-provider.md) | aws-lc-rs as the TLS Crypto Provider | Accepted |
|
||||||
| [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | Accepted |
|
| [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | Accepted |
|
||||||
|
| [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Accepted |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
Open questions are tracked in [open-questions.md](open-questions.md) — an index of theme-grouped tables (64 OQs across 18 themes) with a cross-theme [Deferred / Blocked](open-questions.md#deferred--blocked) section surfacing the safe-exit deferrals. Each OQ lives in its own file under [`questions/`](questions/) (`NNN-slug.md`, mirroring the ADR convention).
|
Open questions are tracked in [open-questions.md](open-questions.md) — an index of theme-grouped tables (65 OQs across 18 themes) with a cross-theme [Deferred / Blocked](open-questions.md#deferred--blocked) section surfacing the safe-exit deferrals. Each OQ lives in its own file under [`questions/`](questions/) (`NNN-slug.md`, mirroring the ADR convention).
|
||||||
|
|
||||||
## Document Lifecycle
|
## Document Lifecycle
|
||||||
|
|
||||||
|
|||||||
@@ -106,15 +106,25 @@ ALPN-list construction is the **assembly layer's** responsibility, not
|
|||||||
the endpoint's. After ADR-083, the endpoint takes no TLS config and
|
the endpoint's. After ADR-083, the endpoint takes no TLS config and
|
||||||
builds no transports — the assembly layer reads `registry.alpn_strings()`
|
builds no transports — the assembly layer reads `registry.alpn_strings()`
|
||||||
and passes the appropriate ALPN list to each `TlsServerConfig::new()`
|
and passes the appropriate ALPN list to each `TlsServerConfig::new()`
|
||||||
(see [`crates/tls/README.md`](../tls/README.md)). For a single-config
|
(see [`crates/tls/README.md`](../tls/README.md)).
|
||||||
deployment (one identity, one set of ALPNs), all transports advertise
|
|
||||||
the same set. For a two-config hub (raw key + X.509/ACME — see OQ-62),
|
The ALPN list is **split by endpoint type** (ADR-086 §3, resolving
|
||||||
the assembly layer may pass different lists to each config; that split
|
OQ-62): each `TlsServerConfig` advertises only the ALPNs its endpoint
|
||||||
is a hub-assembly-layer concern, not an endpoint concern.
|
type's client class can negotiate. The native config (raw key) gets
|
||||||
|
`alknet/channels`, `alknet/call`, `alknet/ssh` (future); the web config
|
||||||
|
(X.509/ACME) gets `h2`, `http/1.1`, `alknet/channels` (for
|
||||||
|
WebSocket-carrying-channels, OQ-65); the iroh builder gets
|
||||||
|
`alknet/channels`, `alknet/call`. The distinction is between
|
||||||
|
**entry points** (ALPNs accepted without identity — `h2`/`http/1.1`,
|
||||||
|
future `alknet/register`) and **endpoints** (ALPNs requiring identity —
|
||||||
|
`alknet/channels`, `alknet/call`, `alknet/ssh`). See
|
||||||
|
[ADR-086](../../decisions/086-endpoint-types-and-entry-points.md) for
|
||||||
|
the full model and ALPN-list table.
|
||||||
|
|
||||||
The iroh endpoint's ALPN list is set via `iroh::Endpoint::builder().alpns()`
|
The iroh endpoint's ALPN list is set via `iroh::Endpoint::builder().alpns()`
|
||||||
by the assembly layer at construction time, from the same
|
by the assembly layer at construction time, from the same
|
||||||
`registry.alpn_strings()` source.
|
`registry.alpn_strings()` source (filtered to the iroh endpoint type's
|
||||||
|
ALPNs).
|
||||||
|
|
||||||
## Accept Loops
|
## Accept Loops
|
||||||
|
|
||||||
@@ -370,6 +380,7 @@ Non-fatal errors within a handler. See [core-types.md](core-types.md) for detail
|
|||||||
| Stealth mode = HTTP handler on standard ALPNs | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Decoy via ALPN routing, not byte-peek |
|
| Stealth mode = HTTP handler on standard ALPNs | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Decoy via ALPN routing, not byte-peek |
|
||||||
| Network identity ≠ auth identity | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | TLS cert/NodeId = network, SSH key/token = auth |
|
| Network identity ≠ auth identity | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | TLS cert/NodeId = network, SSH key/token = auth |
|
||||||
| Handler panics isolated | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | tokio task isolation, connection closes |
|
| Handler panics isolated | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | tokio task isolation, connection closes |
|
||||||
|
| Endpoint types and entry points | [ADR-086](../../decisions/086-endpoint-types-and-entry-points.md) | Three endpoint types (web/native/iroh); entry-point vs. endpoint ALPN distinction; split ALPN lists per endpoint type |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-07-14
|
last_updated: 2026-07-15
|
||||||
---
|
---
|
||||||
|
|
||||||
# alknet-hub
|
# alknet-hub
|
||||||
|
|
||||||
The hub pattern as a reusable crate: multi-transport endpoint that
|
The hub pattern as a reusable crate: a multi-transport endpoint that
|
||||||
accepts workers and browsers over TCP+TLS and QUIC, relays channels
|
composes a subset of three endpoint types (web, native, iroh —
|
||||||
between legs (ADR-079), manages peer lifecycle, aggregates operations,
|
ADR-086), accepts workers and browsers over whichever transports the
|
||||||
and exposes service discovery. One channels connection per peer;
|
subset implies, relays channels between legs (ADR-079), manages peer
|
||||||
multiple endpoint types coexisting.
|
lifecycle, aggregates operations, and exposes service discovery. One
|
||||||
|
channels connection per peer; multiple endpoint types coexisting.
|
||||||
|
|
||||||
## What
|
## What
|
||||||
|
|
||||||
@@ -24,29 +25,77 @@ types — it wires the existing `ChannelsAdapter`, `ChannelClient`,
|
|||||||
`CallAdapter`, `PeerCompositeEnv`, `from_call`, and `Dispatcher` types
|
`CallAdapter`, `PeerCompositeEnv`, `from_call`, and `Dispatcher` types
|
||||||
into a coherent hub runtime.
|
into a coherent hub runtime.
|
||||||
|
|
||||||
|
### The hub composes endpoint types (ADR-086)
|
||||||
|
|
||||||
|
A hub composes a **subset** of three endpoint types, each an independent
|
||||||
|
listener with its own identity model, auth model, and transport(s). The
|
||||||
|
subset determines which transports the hub runs and which ALPNs each
|
||||||
|
`TlsServerConfig` advertises. See [ADR-086](../../decisions/086-endpoint-types-and-entry-points.md)
|
||||||
|
for the full model.
|
||||||
|
|
||||||
|
| Endpoint type | Identity | Auth model | Transport(s) | Client class |
|
||||||
|
|---------------|----------|------------|--------------|--------------|
|
||||||
|
| **web** | X.509 (ACME or manual) | token-based (Bearer) | TCP+TLS (HTTP, WebSocket), QUIC (WebTransport — deferred per ADR-044) | browsers, curl, registration, HTTP API consumers |
|
||||||
|
| **native** | RFC 7250 raw key (Ed25519) | key-based (fingerprint) | QUIC (primary), TCP+TLS (fallback when UDP blocked) | alknet-native clients, workers (fingerprint auth) |
|
||||||
|
| **iroh** | RFC 7250 raw key (NodeId) | key-based (fingerprint) | iroh (relay-assisted QUIC) | p2p peers, NAT'd nodes, minimal-hub deployments |
|
||||||
|
|
||||||
|
The hub shapes that make sense:
|
||||||
|
|
||||||
|
| Hub shape | Endpoint types | Public IP required? | Example |
|
||||||
|
|-----------|---------------|---------------------|---------|
|
||||||
|
| **full hub** | web + native + iroh | yes (web, native) | the general case — browsers, native clients, p2p |
|
||||||
|
| **web + native** | web + native | yes | the first real use case — public domain, native clients |
|
||||||
|
| **native + iroh** | native + iroh | yes (native only) | a hub without browser-facing services |
|
||||||
|
| **minimal hub** | iroh only | no | a p2p-only hub behind NAT, relay-assisted |
|
||||||
|
|
||||||
|
The first real use case is **web + native** (public domain with X.509
|
||||||
|
for browsers/registration + raw-key QUIC for native clients). Iroh is a
|
||||||
|
hard requirement for the project (the p2p, no-public-IP case) but is not
|
||||||
|
in the first deployed subset. All three are hard requirements for the
|
||||||
|
project as a whole — a full hub runs all three.
|
||||||
|
|
||||||
|
### Entry points vs. endpoints (ADR-086 §2)
|
||||||
|
|
||||||
|
Within each endpoint type, ALPNs fall into two categories:
|
||||||
|
|
||||||
|
- **Entry points** — connections accepted without an established peer
|
||||||
|
identity. Per-request auth may apply (registration token, Bearer
|
||||||
|
header, or `auth_token` on channel 0), but the connection itself is
|
||||||
|
not identity-gated at the TLS layer. Examples: `h2`/`http/1.1`
|
||||||
|
(HTTP registration, browser API, stealth decoy, WebSocket upgrade),
|
||||||
|
the future `alknet/register` ALPN (worker registration over
|
||||||
|
QUIC/TCP without HTTP). Entry points exist to bootstrap a peer
|
||||||
|
relationship or serve non-peer clients (browsers).
|
||||||
|
- **Endpoints** (narrow sense) — connections that require identity
|
||||||
|
resolution before the handler runs. No identity → rejected.
|
||||||
|
Examples: `alknet/channels`, `alknet/call` (top-level),
|
||||||
|
`alknet/ssh` (future).
|
||||||
|
|
||||||
|
The distinction determines which `TlsServerConfig` advertises which
|
||||||
|
ALPNs — see "ALPN lists" below. All ALPNs (entry-point and endpoint)
|
||||||
|
are registered on the same `HandlerRegistry`; the difference is which
|
||||||
|
listener accepted them and whether identity was required at the TLS
|
||||||
|
layer.
|
||||||
|
|
||||||
### The hub is multi-transport
|
### The hub is multi-transport
|
||||||
|
|
||||||
A hub **must** support TCP+TLS and QUIC endpoints simultaneously. This is
|
A hub runs whichever transports its endpoint-type subset implies. A
|
||||||
not a convenience — it is the structure of the primary use case. A hub
|
full hub runs TCP+TLS (web), QUIC (native), and iroh (iroh)
|
||||||
that provisions a worker (in a container, on vast.ai, on runpod) uses
|
simultaneously; a minimal hub runs iroh alone. The channels protocol is
|
||||||
TCP+TLS for registration and QUIC (or another transport) for the ongoing
|
transport-agnostic (ADR-071; ADR-065
|
||||||
channels session. The hub's HTTP endpoint (registration, browser access)
|
|
||||||
runs over TCP+TLS; the hub's channels endpoint runs over QUIC or
|
|
||||||
TCP+TLS depending on the worker's transport. These coexist on one hub.
|
|
||||||
|
|
||||||
The channels protocol is transport-agnostic (ADR-071; ADR-065
|
|
||||||
`Connection::from_stream`/`from_bidi`). The hub's accept and dial paths
|
`Connection::from_stream`/`from_bidi`). The hub's accept and dial paths
|
||||||
inherit that property — they take a `Connection`, not a `SocketAddr`
|
inherit that property — they take a `Connection`, not a `SocketAddr`
|
||||||
welded to a dial. See "Transport" below.
|
welded to a dial. See "Transport" below.
|
||||||
|
|
||||||
### What the hub provides
|
### What the hub provides
|
||||||
|
|
||||||
1. **Multi-transport endpoint** — accepts channels connections over
|
1. **Multi-transport endpoint** — composes a subset of three endpoint
|
||||||
QUIC and over TCP+TLS (both owned transports on `AlknetEndpoint` via
|
types (web, native, iroh — ADR-086), each wired as an owned transport
|
||||||
`with_quinn` / `with_tcp_tls`, per ADR-083). Both feed the same
|
on `AlknetEndpoint` via `with_quinn` / `with_iroh` / `with_tcp_tls`
|
||||||
dispatch path. Also serves HTTP (`h2`/`http/1.1`) over TCP+TLS for
|
(ADR-083). All feed the same dispatch path. A web+native hub runs
|
||||||
registration and browser access. All three coexist; the endpoint owns
|
TCP+TLS (web, X.509) + QUIC (native, raw key); a minimal hub runs
|
||||||
all accept loops and `shutdown()` stops them all.
|
iroh alone; a full hub runs all three. The endpoint owns all accept
|
||||||
|
loops and `shutdown()` stops them all.
|
||||||
|
|
||||||
2. **Peer lifecycle** — accept, dial, disconnect, reconnect with
|
2. **Peer lifecycle** — accept, dial, disconnect, reconnect with
|
||||||
backoff. Identity resolution via `IdentityProvider` (fingerprint or
|
backoff. Identity resolution via `IdentityProvider` (fingerprint or
|
||||||
@@ -79,12 +128,12 @@ welded to a dial. See "Transport" below.
|
|||||||
The hub pattern requires wiring that `alknet-channels` and
|
The hub pattern requires wiring that `alknet-channels` and
|
||||||
`alknet-call` do not provide out of the box:
|
`alknet-call` do not provide out of the box:
|
||||||
|
|
||||||
- The hub accepts channels connections over **multiple transports**
|
- The hub composes **multiple endpoint types** (ADR-086). The channels
|
||||||
simultaneously. The channels crate is transport-agnostic (ADR-071,
|
crate is transport-agnostic (ADR-071, ADR-065); the hub composes the
|
||||||
ADR-065); the hub composes the multi-transport endpoint
|
multi-transport endpoint (`AlknetEndpoint` with `with_quinn` /
|
||||||
(`AlknetEndpoint` with `with_quinn` / `with_tcp_tls`, ADR-083). The
|
`with_iroh` / `with_tcp_tls`, ADR-083) and filters ALPN lists per
|
||||||
accept loops themselves live in `alknet-core`; the hub provides the
|
endpoint type. The accept loops themselves live in `alknet-core`; the
|
||||||
handlers and wiring.
|
hub provides the handlers and wiring.
|
||||||
- The hub relays channels between legs (ADR-079) — terminating
|
- The hub relays channels between legs (ADR-079) — terminating
|
||||||
channel 0 on each leg, translating `channel/open`, byte-forwarding
|
channel 0 on each leg, translating `channel/open`, byte-forwarding
|
||||||
data channels. The channels crate is ALPN-blind and does not know it
|
data channels. The channels crate is ALPN-blind and does not know it
|
||||||
@@ -181,25 +230,44 @@ The `Hub` exposes builder methods for optional hooks
|
|||||||
### Transport
|
### Transport
|
||||||
|
|
||||||
The hub accepts and dials channels connections over any transport
|
The hub accepts and dials channels connections over any transport
|
||||||
the channels protocol supports (ADR-071). In practice, a hub runs
|
the channels protocol supports (ADR-071). A hub composes a subset of
|
||||||
multiple accept loops simultaneously — all owned by `AlknetEndpoint`
|
three endpoint types (ADR-086); the subset determines which transports
|
||||||
via builder methods (ADR-083):
|
run. All are owned by `AlknetEndpoint` via builder methods (ADR-083):
|
||||||
|
|
||||||
| Builder method | Transport | What it carries |
|
| Builder method | Endpoint type | Transport | What it carries |
|
||||||
|----------------|-----------|-----------------|
|
|----------------|---------------|-----------|-----------------|
|
||||||
| `with_quinn` | QUIC (quinn) | Channels connections from workers with QUIC reachability |
|
| `with_quinn` (raw-key config) | native | QUIC (quinn) | `alknet/channels`, `alknet/call`, `alknet/ssh` (future) — native clients with QUIC reachability |
|
||||||
| `with_iroh` | QUIC (iroh, relay-assisted) | Channels connections from workers behind NAT |
|
| `with_quinn` (X.509/ACME config) | web | QUIC (quinn) | `h2`, `http/1.1`, `h3` (WebTransport — deferred per ADR-044) — browsers over QUIC |
|
||||||
| `with_tcp_tls` | TCP + TLS (`h2`/`http/1.1`/`alknet/channels`) | HTTP registration endpoint, browser access, channels-over-TCP from workers |
|
| `with_iroh` | iroh | QUIC (iroh, relay-assisted) | `alknet/channels`, `alknet/call` — p2p peers, NAT'd nodes |
|
||||||
| (Future) WebTransport | WebTransport | Browser bidirectional path (deferred per ADR-044; WebSocket is the v1 browser path) |
|
| `with_tcp_tls` (X.509/ACME config) | web | TCP + TLS | `h2`/`http/1.1` (registration, browser), `alknet/channels` (WebSocket-carrying-channels — OQ-65), `acme-tls/1` (appended automatically) |
|
||||||
|
| `with_tcp_tls` (raw-key config) | native | TCP + TLS | `alknet/channels`, `alknet/call` — native clients using TCP+TLS when UDP is blocked |
|
||||||
|
| (Future) WebTransport | web | WebTransport | Browser bidirectional path (deferred per ADR-044; WebSocket is the v1 browser path) |
|
||||||
|
|
||||||
|
A single `AlknetEndpoint` may hold two quinn listeners (one per
|
||||||
|
`TlsServerConfig` — raw-key for native, X.509 for web), one iroh
|
||||||
|
endpoint, and one or two TCP+TLS listeners (X.509 for web, raw-key
|
||||||
|
for native fallback). The endpoint owns all of them; `shutdown()`
|
||||||
|
stops them all.
|
||||||
|
|
||||||
|
### ALPN lists (ADR-086 §3)
|
||||||
|
|
||||||
|
Each `TlsServerConfig` advertises only the ALPNs its endpoint type's
|
||||||
|
client class can negotiate. The assembly layer filters
|
||||||
|
`registry.alpn_strings()` by endpoint type — see
|
||||||
|
[ADR-086](../../decisions/086-endpoint-types-and-entry-points.md) §3
|
||||||
|
for the full table. The split makes the advertisement honest (no `h2`
|
||||||
|
on a raw-key listener that browsers cannot reach) and the
|
||||||
|
assembly-layer wiring pattern guessable (not a same-list-vs-split
|
||||||
|
guess).
|
||||||
|
|
||||||
All accept loops run inside `endpoint.run()` and feed the same dispatch
|
All accept loops run inside `endpoint.run()` and feed the same dispatch
|
||||||
path. The hub's `HttpAdapter` serves `h2`/`http/1.1` over the TCP+TLS
|
path. The hub's `HttpAdapter` serves `h2`/`http/1.1` over the web
|
||||||
path for registration and browser access; the `ChannelsAdapter`
|
endpoint's TCP+TLS path for registration and browser access; the
|
||||||
(registered on the hub's `HandlerRegistry`) serves `alknet/channels`
|
`ChannelsAdapter` (registered on the hub's `HandlerRegistry`) serves
|
||||||
over the QUIC path (and over TCP+TLS when a worker dials
|
`alknet/channels` over the native endpoint's QUIC path (and over
|
||||||
channels-over-TCP). Both ALPNs are registered on the same registry;
|
TCP+TLS when a worker dials channels-over-TCP). Both ALPNs are
|
||||||
the TLS handshake on each connection negotiates the ALPN and dispatches
|
registered on the same registry; the TLS handshake on each connection
|
||||||
to the right adapter.
|
negotiates the ALPN and dispatches to the right adapter.
|
||||||
|
|
||||||
The TCP+TLS accept loop constructs a `Connection` per accepted
|
The TCP+TLS accept loop constructs a `Connection` per accepted
|
||||||
`TlsStream<TcpStream>` via `Connection::from_bidi` (ADR-065) and hands
|
`TlsStream<TcpStream>` via `Connection::from_bidi` (ADR-065) and hands
|
||||||
@@ -384,11 +452,19 @@ authenticates with its TLS fingerprint. Both resolve to the same
|
|||||||
|
|
||||||
### Worker registration
|
### Worker registration
|
||||||
|
|
||||||
The registration flow is what makes TCP+TLS a hard requirement for
|
The registration flow is an **entry point** (ADR-086 §2) — a connection
|
||||||
the hub. A freshly-provisioned worker (container, vast.ai, runpod)
|
accepted without an established peer identity, authenticated per-request
|
||||||
does not yet have an established peer relationship with the hub — it
|
by the registration token. This is why the registration endpoint is an
|
||||||
has a one-time registration token supplied via the provisioning
|
HTTP route on `h2`/`http/1.1` (an entry-point ALPN), not a
|
||||||
config. The flow:
|
call-protocol operation: the worker has no `CallConnection` yet at
|
||||||
|
registration time. A future `alknet/register` ALPN would serve the same
|
||||||
|
entry-point role over QUIC/TCP without the HTTP layer (not yet specced).
|
||||||
|
|
||||||
|
The registration flow is what makes the web endpoint (TCP+TLS, X.509)
|
||||||
|
a hard requirement for a hub that provisions workers. A freshly-provisioned
|
||||||
|
worker (container, vast.ai, runpod) does not yet have an established
|
||||||
|
peer relationship with the hub — it has a one-time registration token
|
||||||
|
supplied via the provisioning config. The flow:
|
||||||
|
|
||||||
1. The hub provisions a worker instance (docker, vast.ai, runpod —
|
1. The hub provisions a worker instance (docker, vast.ai, runpod —
|
||||||
platform-specific) and supplies a registration token via
|
platform-specific) and supplies a registration token via
|
||||||
@@ -703,6 +779,7 @@ into `CallAdapter::with_aggregated_env`.
|
|||||||
| TCP+TLS as first-class owned transport | [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) | `with_tcp_tls(listener, acceptor)` — TCP+TLS is owned by the endpoint, not a sibling loop; supersedes ADR-010 Am. 1 |
|
| TCP+TLS as first-class owned transport | [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) | `with_tcp_tls(listener, acceptor)` — TCP+TLS is owned by the endpoint, not a sibling loop; supersedes ADR-010 Am. 1 |
|
||||||
| Channel 0 pre-negotiated | [ADR-072](../../decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 = `alknet/call`; the `CallAdapter` runs here |
|
| Channel 0 pre-negotiated | [ADR-072](../../decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 = `alknet/call`; the `CallAdapter` runs here |
|
||||||
| Channel lifecycle operations | [ADR-073](../../decisions/073-channel-lifecycle-operations.md) | `channel/open`/`close`/`control`/`resources/subscribe` — what the hub translates |
|
| Channel lifecycle operations | [ADR-073](../../decisions/073-channel-lifecycle-operations.md) | `channel/open`/`close`/`control`/`resources/subscribe` — what the hub translates |
|
||||||
|
| Endpoint types and entry points | [ADR-086](../../decisions/086-endpoint-types-and-entry-points.md) | Three endpoint types (web/native/iroh); entry-point vs. endpoint ALPN distinction; split ALPN lists per endpoint type |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
@@ -713,7 +790,16 @@ See [open-questions.md](../../open-questions.md) for full details.
|
|||||||
`register_worker` API. Decision-ready in shape (HTTP POST, token in,
|
`register_worker` API. Decision-ready in shape (HTTP POST, token in,
|
||||||
`PeerEntry` created, session credential returned); the exact token
|
`PeerEntry` created, session credential returned); the exact token
|
||||||
model (one-time vs. refresh, rotation) and endpoint path need a
|
model (one-time vs. refresh, rotation) and endpoint path need a
|
||||||
dedicated ADR before the hub crate stabilizes.
|
dedicated ADR before the hub crate stabilizes. The registration
|
||||||
|
endpoint is an entry point (ADR-086 §2) — a future `alknet/register`
|
||||||
|
ALPN would serve the same role over QUIC/TCP without HTTP.
|
||||||
|
- **OQ-65** (open): WebSocket carrying channels — whether the browser
|
||||||
|
path extends from call-protocol-only (ADR-048) to full channels
|
||||||
|
(the 9-byte chunk format over WebSocket binary frames). If chosen,
|
||||||
|
the browser is a first-class channels participant and the hub relay
|
||||||
|
works unchanged for browser legs. The web endpoint advertises
|
||||||
|
`alknet/channels` by default (ADR-086 §3 — the advertisement is
|
||||||
|
settled; OQ-65 governs whether the browser path uses it).
|
||||||
- **OQ-52** (open): `CallConnection::wait_for_close()` — the
|
- **OQ-52** (open): `CallConnection::wait_for_close()` — the
|
||||||
supervision loop needs a way to await connection close. The
|
supervision loop needs a way to await connection close. The
|
||||||
committed interim is polling `connection().accept_bi()` until
|
committed interim is polling `connection().accept_bi()` until
|
||||||
@@ -756,6 +842,7 @@ See [open-questions.md](../../open-questions.md) for full details.
|
|||||||
- ADR-080: ChannelClient (transport-agnostic `from_connection`)
|
- ADR-080: ChannelClient (transport-agnostic `from_connection`)
|
||||||
- ADR-082: alknet-tls extraction (`TlsServerConfig` — shared across quinn + TCP+TLS)
|
- ADR-082: alknet-tls extraction (`TlsServerConfig` — shared across quinn + TCP+TLS)
|
||||||
- ADR-083: Endpoint as multi-transport accept-loop runner (`with_tcp_tls` — TCP+TLS owned by the endpoint; the hub composes transports and handlers)
|
- ADR-083: Endpoint as multi-transport accept-loop runner (`with_tcp_tls` — TCP+TLS owned by the endpoint; the hub composes transports and handlers)
|
||||||
|
- ADR-086: Endpoint types and entry points (web/native/iroh; entry-point vs. endpoint; split ALPN lists per endpoint type)
|
||||||
- alkapi [hub.md](/workspace/@alkdev/alkapi/docs/architecture/hub.md) —
|
- alkapi [hub.md](/workspace/@alkdev/alkapi/docs/architecture/hub.md) —
|
||||||
the first hub consumer, the concrete use case that informed this
|
the first hub consumer, the concrete use case that informed this
|
||||||
crate
|
crate
|
||||||
|
|||||||
@@ -72,13 +72,30 @@ separate crate is the right shape (dependency isolation, ACME weight,
|
|||||||
quinn/iroh having their own TLS), is in
|
quinn/iroh having their own TLS), is in
|
||||||
[ADR-082](../../decisions/082-alknet-tls-extraction.md).
|
[ADR-082](../../decisions/082-alknet-tls-extraction.md).
|
||||||
|
|
||||||
### The three use cases
|
### The three endpoint types (ADR-086)
|
||||||
|
|
||||||
| Use case | Identity | Transports | Browsers? |
|
A hub composes a subset of three endpoint types, each with its own
|
||||||
|----------|----------|-----------|-----------|
|
identity model and transport(s). `alknet-tls` provides the
|
||||||
| P2P / native clients | RFC 7250 raw key (Ed25519) | QUIC + TCP (fallback when UDP blocked) | No (browsers can't do raw keys) |
|
`TlsServerConfig`s; the assembly layer builds one per endpoint type
|
||||||
| Domain-hosted / public service | X.509 (manual or ACME) | QUIC + TCP+TLS (same cert) | Yes (via HTTPS / WebSocket; WebTransport when revived) |
|
that uses a `rustls::ServerConfig` (iroh is the exception — it has its
|
||||||
| Development | Self-signed | Any | No (untrusted) |
|
own TLS).
|
||||||
|
|
||||||
|
| Endpoint type | Identity | `TlsServerConfig` | Transport(s) | Browsers? |
|
||||||
|
|---------------|----------|-------------------|--------------|-----------|
|
||||||
|
| **native** | RFC 7250 raw key (Ed25519) | raw-key config | QUIC (primary), TCP+TLS (fallback when UDP blocked) | No (browsers can't do raw keys) |
|
||||||
|
| **web** | X.509 (manual or ACME) | X.509/ACME config | TCP+TLS (HTTP, WebSocket), QUIC (WebTransport — deferred) | Yes (via HTTPS / WebSocket; WebTransport when revived) |
|
||||||
|
| **iroh** | RFC 7250 raw key (NodeId) | (no `TlsServerConfig` — iroh has its own TLS) | iroh (relay-assisted QUIC) | No |
|
||||||
|
| Development | Self-signed | self-signed config | Any | No (untrusted) |
|
||||||
|
|
||||||
|
The ALPN list each `TlsServerConfig` advertises is **split by endpoint
|
||||||
|
type** (ADR-086 §3, resolving OQ-62): the native config advertises the
|
||||||
|
native ALPNs (`alknet/channels`, `alknet/call`, `alknet/ssh` future);
|
||||||
|
the web config advertises the entry-point ALPNs (`h2`, `http/1.1`) +
|
||||||
|
`alknet/channels` (for WebSocket-carrying-channels, OQ-65) +
|
||||||
|
`acme-tls/1` (appended automatically). The assembly layer filters
|
||||||
|
`registry.alpn_strings()` per config. See
|
||||||
|
[ADR-086](../../decisions/086-endpoint-types-and-entry-points.md) for
|
||||||
|
the full ALPN-list table and the entry-point/endpoint distinction.
|
||||||
|
|
||||||
In all cases, TLS + ALPNs "just works" — the TLS handshake negotiates the
|
In all cases, TLS + ALPNs "just works" — the TLS handshake negotiates the
|
||||||
ALPN, the `HandlerRegistry` dispatches by ALPN, the transport is a
|
ALPN, the `HandlerRegistry` dispatches by ALPN, the transport is a
|
||||||
@@ -145,10 +162,14 @@ impl TlsServerConfig {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The ALPN list is the set of ALPNs the deployment wants to advertise
|
The ALPN list is the set of ALPNs the endpoint type advertises. For a
|
||||||
(`alknet/call`, `alknet/channels`, `h2`, `http/1.1`, etc.). For ACME, the
|
native config: `alknet/channels`, `alknet/call`, `alknet/ssh` (future).
|
||||||
`acme-tls/1` ALPN is appended automatically (for the TLS-ALPN-01
|
For a web config: `h2`, `http/1.1`, `alknet/channels` (for
|
||||||
challenge, ADR-027 §7).
|
WebSocket-carrying-channels, OQ-65). The list is **split by endpoint
|
||||||
|
type** (ADR-086 §3) — the assembly layer filters
|
||||||
|
`registry.alpn_strings()` per `TlsServerConfig`, not passes the same
|
||||||
|
list to both. For ACME, the `acme-tls/1` ALPN is appended
|
||||||
|
automatically (for the TLS-ALPN-01 challenge, ADR-027 §7).
|
||||||
|
|
||||||
### `async fn new` — lifecycle semantics
|
### `async fn new` — lifecycle semantics
|
||||||
|
|
||||||
@@ -433,6 +454,7 @@ All design decisions are documented as ADRs in
|
|||||||
| [082](../../decisions/082-alknet-tls-extraction.md) | alknet-tls crate extraction | Extract TLS setup from alknet-core/endpoint.rs; `TlsServerConfig` shareable across quinn + TCP+TLS + iroh; one ACME state machine |
|
| [082](../../decisions/082-alknet-tls-extraction.md) | alknet-tls crate extraction | Extract TLS setup from alknet-core/endpoint.rs; `TlsServerConfig` shareable across quinn + TCP+TLS + iroh; one ACME state machine |
|
||||||
| [083](../../decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as multi-transport accept-loop runner | `AlknetEndpoint` takes no TLS config; TCP+TLS is an owned transport (`with_tcp_tls`); `dispatch` public for SSH/WT; `acme-tls/1` guard moves to shared `dispatch` |
|
| [083](../../decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as multi-transport accept-loop runner | `AlknetEndpoint` takes no TLS config; TCP+TLS is an owned transport (`with_tcp_tls`); `dispatch` public for SSH/WT; `acme-tls/1` guard moves to shared `dispatch` |
|
||||||
| [084](../../decisions/084-aws-lc-rs-crypto-provider.md) | aws-lc-rs crypto provider | `rustls::crypto::aws_lc_rs::default_provider()` on all server + client config paths; matches iroh; FIPS-capable; do not switch to `ring` or process-default without a new ADR |
|
| [084](../../decisions/084-aws-lc-rs-crypto-provider.md) | aws-lc-rs crypto provider | `rustls::crypto::aws_lc_rs::default_provider()` on all server + client config paths; matches iroh; FIPS-capable; do not switch to `ring` or process-default without a new ADR |
|
||||||
|
| [086](../../decisions/086-endpoint-types-and-entry-points.md) | Endpoint types and entry points | Three endpoint types (web/native/iroh); split ALPN lists per endpoint type (resolves OQ-62); entry-point vs. endpoint ALPN distinction |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
@@ -452,9 +474,13 @@ See [open-questions.md](../../open-questions.md) for full details.
|
|||||||
- **OQ-61** (dissolved): Multi-owner shutdown coordination. The
|
- **OQ-61** (dissolved): Multi-owner shutdown coordination. The
|
||||||
problem does not arise — the endpoint owns all its accept loops
|
problem does not arise — the endpoint owns all its accept loops
|
||||||
(quinn, iroh, TCP+TLS); `shutdown()` stops them all. See ADR-083.
|
(quinn, iroh, TCP+TLS); `shutdown()` stops them all. See ADR-083.
|
||||||
- **OQ-62** (open): Does a hub pass the same ALPN list to both
|
- **OQ-62** (resolved): Does a hub pass the same ALPN list to both
|
||||||
`TlsServerConfig`s, or different (transport-appropriate) lists?
|
`TlsServerConfig`s? **Split list, by endpoint type** (ADR-086 §3).
|
||||||
Decision-needed before the hub's assembly code is written.
|
Each config advertises only the ALPNs its endpoint type's client
|
||||||
|
class can negotiate — native ALPNs on the raw-key config, entry-point
|
||||||
|
ALPNs + `alknet/channels` on the X.509/ACME config, native ALPNs on
|
||||||
|
the iroh builder. The assembly layer filters
|
||||||
|
`registry.alpn_strings()` per config.
|
||||||
- **OQ-63** (open): `TlsError` shape — the error type is referenced in
|
- **OQ-63** (open): `TlsError` shape — the error type is referenced in
|
||||||
public signatures but not sketched. Needs a variant-granularity
|
public signatures but not sketched. Needs a variant-granularity
|
||||||
decision (single enum vs thin wrapper) before implementation.
|
decision (single enum vs thin wrapper) before implementation.
|
||||||
@@ -478,6 +504,9 @@ See [open-questions.md](../../open-questions.md) for full details.
|
|||||||
- `docs/architecture/decisions/010-alpn-router-and-endpoint.md`
|
- `docs/architecture/decisions/010-alpn-router-and-endpoint.md`
|
||||||
Amendment 2 — TCP+TLS is a first-class owned transport
|
Amendment 2 — TCP+TLS is a first-class owned transport
|
||||||
(`with_tcp_tls`); supersedes Amendment 1's sibling-loop framing
|
(`with_tcp_tls`); supersedes Amendment 1's sibling-loop framing
|
||||||
|
- `docs/architecture/decisions/086-endpoint-types-and-entry-points.md`
|
||||||
|
— three endpoint types (web/native/iroh); split ALPN lists per
|
||||||
|
endpoint type (resolves OQ-62); entry-point vs. endpoint distinction
|
||||||
- `docs/architecture/crates/core/endpoint.md` — current endpoint design
|
- `docs/architecture/crates/core/endpoint.md` — current endpoint design
|
||||||
(TLS section will be amended to point to `alknet-tls`)
|
(TLS section will be amended to point to `alknet-tls`)
|
||||||
- `docs/architecture/crates/core/config.md` — `TlsIdentity`, `StaticConfig`
|
- `docs/architecture/crates/core/config.md` — `TlsIdentity`, `StaticConfig`
|
||||||
|
|||||||
@@ -0,0 +1,400 @@
|
|||||||
|
# ADR-086: Endpoint Types and Entry Points
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (resolves OQ-62)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
> **Terminology note.** "Endpoint" is used in three senses in the
|
||||||
|
> alknet docs, and this ADR uses all three: (1) **`AlknetEndpoint`** —
|
||||||
|
> the struct in `alknet-core` (ADR-010/083) that runs accept loops and
|
||||||
|
> dispatches by ALPN; (2) **endpoint type** — one of web, native, or
|
||||||
|
> iroh, a composition unit with its own identity/auth/transport
|
||||||
|
> (defined in §1 below); (3) **endpoint** in the narrow ALPN sense —
|
||||||
|
> an ALPN whose connections require identity resolution before
|
||||||
|
> dispatch, contrasted with an **entry point** (defined in §2 below).
|
||||||
|
> When the sense is ambiguous, this ADR uses "endpoint type" or
|
||||||
|
> "endpoint ALPN" to disambiguate from `AlknetEndpoint` the struct.
|
||||||
|
|
||||||
|
### The tangle
|
||||||
|
|
||||||
|
The hub/endpoint/TLS configuration has an unnamed distinction at its
|
||||||
|
core that, once named, dissolves several interlocking confusions:
|
||||||
|
|
||||||
|
1. **OQ-62** asked whether a hub passes the same ALPN list to both
|
||||||
|
`TlsServerConfig`s (raw-key + X.509/ACME) or different
|
||||||
|
transport-appropriate lists. The question could not be answered
|
||||||
|
cleanly because the docs treated all ALPNs as the same kind of thing.
|
||||||
|
There was no principled basis for *why* the lists would differ.
|
||||||
|
|
||||||
|
2. **The hub README** states a hub "**must** support TCP+TLS and QUIC
|
||||||
|
endpoints simultaneously," framing it as a hard requirement. This is
|
||||||
|
wrong as a general statement — a minimal hub can run on iroh alone
|
||||||
|
(no public IP/port required), and the first real use case hosts a
|
||||||
|
subset (web + native), not all three. The "must" framing obscures
|
||||||
|
that a hub composes a *subset* of endpoint types, and which subset
|
||||||
|
determines which ALPNs it advertises.
|
||||||
|
|
||||||
|
3. **The ALPN registry** lists `h2`/`http/1.1` alongside
|
||||||
|
`alknet/channels` and `alknet/call` with no distinction in kind.
|
||||||
|
But `h2`/`http/1.1` are structurally different: a connection
|
||||||
|
negotiating `h2` is accepted without an established peer identity
|
||||||
|
(the registration endpoint, browser API routes), while a connection
|
||||||
|
negotiating `alknet/channels` requires identity resolution (fingerprint
|
||||||
|
or bearer token on channel 0). Conflating these under one "ALPN" label
|
||||||
|
is why the ALPN-list-split question had no answer.
|
||||||
|
|
||||||
|
4. **The foundational handlers** were listed as one undifferentiated
|
||||||
|
backlog (ssh, tunnel, socks5, fs, sftp). In fact they fall into two
|
||||||
|
structurally different categories: channels data-channel ALPNs (tunnel,
|
||||||
|
socks5, fs, sftp — gated by channels, opened via `channel/open`, inherit
|
||||||
|
ACL + bidirectionality) vs. an endpoint ALPN that wraps channels inside
|
||||||
|
it (ssh — a legacy-client entry point that runs channels-over-SSH and
|
||||||
|
uses RFC 7250 keys). SSH is not in the same category as the rest.
|
||||||
|
|
||||||
|
### The insight: endpoint types and entry points
|
||||||
|
|
||||||
|
A hub serves different **client classes**, each with a different
|
||||||
|
identity model, auth model, and transport. These are **endpoint
|
||||||
|
types** — independent, composable listeners. A hub composes a subset;
|
||||||
|
the subset determines the ALPN lists.
|
||||||
|
|
||||||
|
Within each endpoint type, there are two kinds of ALPNs:
|
||||||
|
|
||||||
|
- **Entry points** — ALPNs whose connections are accepted without an
|
||||||
|
established peer identity. Per-request auth may apply (registration
|
||||||
|
token, Bearer for API routes), but the connection itself is not
|
||||||
|
identity-gated at the TLS layer. Examples: `h2`/`http/1.1` (HTTP
|
||||||
|
registration, browser API, stealth decoy), the future `alknet/register`
|
||||||
|
ALPN (worker registration over QUIC/TCP without HTTP). Entry points
|
||||||
|
exist to bootstrap a peer relationship or serve non-peer clients
|
||||||
|
(browsers).
|
||||||
|
|
||||||
|
- **Endpoints** (in the narrow sense) — ALPNs whose connections require
|
||||||
|
identity resolution before the handler runs. No identity → the
|
||||||
|
connection is rejected (or, for channels, identity is resolved on
|
||||||
|
channel 0 before dispatch). Examples: `alknet/channels`, `alknet/call`
|
||||||
|
(when used as a top-level ALPN rather than channel 0), `alknet/ssh`.
|
||||||
|
|
||||||
|
This distinction is not cosmetic. It is what makes the ALPN-list-split
|
||||||
|
question answerable: each endpoint type serves a client class, and the
|
||||||
|
ALPNs that client class can negotiate are the ones that endpoint type
|
||||||
|
advertises. A raw-key QUIC listener serving native clients advertises
|
||||||
|
the native ALPNs (`alknet/channels`, `alknet/call`); it does not
|
||||||
|
advertise `h2`/`http/1.1` because browsers cannot connect to a raw-key
|
||||||
|
listener and native clients do not use HTTP ALPNs. An X.509 TCP+TLS
|
||||||
|
listener serving browsers and registration advertises the entry-point
|
||||||
|
ALPNs plus `alknet/channels` (for the WebSocket-channels browser path,
|
||||||
|
per OQ-65).
|
||||||
|
|
||||||
|
### Why iroh is a separate endpoint type
|
||||||
|
|
||||||
|
Iroh is not "just another transport" alongside quinn and TCP+TLS. It is
|
||||||
|
a distinct endpoint type because it combines three things that neither
|
||||||
|
quinn nor TCP+TLS provide alone:
|
||||||
|
|
||||||
|
1. **No public IP/port required** — relay-assisted p2p. A minimal hub
|
||||||
|
with no public presence runs iroh alone.
|
||||||
|
2. **RFC 7250 raw keys built in** — iroh's `Endpoint` handles TLS
|
||||||
|
internally; it does not consume a `rustls::ServerConfig` (ADR-082
|
||||||
|
§"Iroh: shares the key, not the rustls config"). The ALPN list is
|
||||||
|
set via `iroh::Endpoint::builder().alpns()`, not via
|
||||||
|
`TlsServerConfig::new`.
|
||||||
|
3. **Key-based auth (NodeId)** — same fingerprint model as the native
|
||||||
|
quinn path, but the connectivity is p2p.
|
||||||
|
|
||||||
|
A hub that serves all three endpoint types (a "full hub") runs three
|
||||||
|
independent listeners. A hub that serves only iroh (a "minimal hub")
|
||||||
|
runs one. The composition is additive — each endpoint type is an
|
||||||
|
independent `with_*` on `AlknetEndpoint` (ADR-083).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. Three endpoint types
|
||||||
|
|
||||||
|
A hub composes a subset of three endpoint types. Each is an independent
|
||||||
|
listener with its own identity model, auth model, and transport(s).
|
||||||
|
|
||||||
|
| Endpoint type | Identity | Auth model | Transport(s) | Client class |
|
||||||
|
|---------------|----------|------------|--------------|--------------|
|
||||||
|
| **web** | X.509 (ACME or manual) | token-based (Bearer) | TCP+TLS (HTTP, WebSocket), QUIC (WebTransport — deferred per ADR-044) | browsers, curl, registration, HTTP API consumers |
|
||||||
|
| **native** | RFC 7250 raw key (Ed25519) | key-based (fingerprint) | QUIC (primary), TCP+TLS (fallback when UDP blocked) | alknet-native clients, workers (fingerprint auth) |
|
||||||
|
| **iroh** | RFC 7250 raw key (NodeId) | key-based (fingerprint) | iroh (relay-assisted QUIC) | p2p peers, NAT'd nodes, minimal-hub deployments |
|
||||||
|
|
||||||
|
A hub may run any subset. The subsets that make sense:
|
||||||
|
|
||||||
|
| Hub shape | Endpoint types | Public IP required? | Example |
|
||||||
|
|-----------|---------------|---------------------|---------|
|
||||||
|
| **full hub** | web + native + iroh | yes (web, native) | the general case — browsers, native clients, p2p |
|
||||||
|
| **web + native** | web + native | yes | the first real use case — public domain, native clients |
|
||||||
|
| **native + iroh** | native + iroh | yes (native only) | a hub without browser-facing services |
|
||||||
|
| **minimal hub** | iroh only | no | a p2p-only hub behind NAT, relay-assisted |
|
||||||
|
|
||||||
|
The first real use case is **web + native** (public domain with X.509
|
||||||
|
for browsers/registration + raw-key QUIC for native clients). Iroh is a
|
||||||
|
hard requirement for the project (the p2p, no-public-IP case) but is
|
||||||
|
not in the first deployed subset. All three are hard requirements for
|
||||||
|
the project as a whole — a full hub runs all three.
|
||||||
|
|
||||||
|
### 2. Entry points vs. endpoints (the ALPN-level distinction)
|
||||||
|
|
||||||
|
ALPNs fall into two categories:
|
||||||
|
|
||||||
|
**Entry points** — connections accepted without an established peer
|
||||||
|
identity. The TLS handshake succeeds without client identity; auth
|
||||||
|
happens per-request inside the handler (registration token, Bearer
|
||||||
|
header, or the call-protocol `auth_token` on channel 0 for a channels
|
||||||
|
connection that has not yet established identity). Entry points exist
|
||||||
|
to bootstrap a peer relationship (worker registration) or to serve
|
||||||
|
non-peer clients (browsers, curl).
|
||||||
|
|
||||||
|
| ALPN | Category | Handler | Purpose |
|
||||||
|
|------|----------|---------|---------|
|
||||||
|
| `h2` / `http/1.1` | entry point | `HttpAdapter` | HTTP registration, browser API routes, stealth decoy, WebSocket upgrade |
|
||||||
|
| `alknet/register` (future) | entry point | (registration handler) | Worker registration over QUIC/TCP without HTTP — a direct ALPN for enrollment, avoiding the HTTP layer. Not yet specced; tracked as a hub concern. |
|
||||||
|
|
||||||
|
**Endpoints** (narrow sense) — connections that require identity
|
||||||
|
resolution before the handler runs. The TLS handshake or the
|
||||||
|
call-protocol first frame must produce an identity (fingerprint or
|
||||||
|
token); no identity → rejected.
|
||||||
|
|
||||||
|
| ALPN | Category | Handler | Identity source |
|
||||||
|
|------|----------|---------|-----------------|
|
||||||
|
| `alknet/channels` | endpoint | `ChannelsAdapter` | Fingerprint (raw key / client cert) or bearer token on channel 0 (ADR-072) |
|
||||||
|
| `alknet/call` | endpoint | `CallAdapter` | Fingerprint or bearer token (first frame) — when used as a top-level ALPN; as channel 0 inside channels, identity is resolved before dispatch |
|
||||||
|
| `alknet/ssh` (future) | endpoint | (ssh handler) | RFC 7250 key fingerprint — SSH is a legacy-client entry point that wraps channels inside it (see §4 below) |
|
||||||
|
|
||||||
|
The distinction is structural, not cosmetic: it determines which
|
||||||
|
`TlsServerConfig` advertises which ALPNs (§3 below) and which
|
||||||
|
connections require identity at the TLS layer vs. per-request.
|
||||||
|
|
||||||
|
### 3. ALPN lists are split per endpoint type (resolves OQ-62)
|
||||||
|
|
||||||
|
**Option B — split list, transport-appropriate.** Each `TlsServerConfig`
|
||||||
|
advertises only the ALPNs its client class can negotiate. The assembly
|
||||||
|
layer filters `registry.alpn_strings()` by endpoint type.
|
||||||
|
|
||||||
|
| `TlsServerConfig` / iroh builder | Endpoint type | ALPNs advertised |
|
||||||
|
|----------------------------------|---------------|------------------|
|
||||||
|
| raw-key config (`for_quinn`) | native | `alknet/channels`, `alknet/call`, `alknet/ssh` (future) — the native ALPNs |
|
||||||
|
| raw-key config (`for_tcp_tls`) | native (TCP fallback) | `alknet/channels`, `alknet/call` — native clients using TCP+TLS when UDP is blocked |
|
||||||
|
| X.509/ACME config (`for_tcp_tls`) | web | `h2`, `http/1.1`, `alknet/channels` (for WebSocket-carrying-channels, per OQ-65), `acme-tls/1` (appended automatically by `TlsServerConfig::new`) |
|
||||||
|
| X.509/ACME config (`for_quinn`) | web (WebTransport — deferred) | `h2`, `http/1.1`, `h3` (when WebTransport revives per ADR-044) |
|
||||||
|
| iroh `Endpoint::builder().alpns()` | iroh | `alknet/channels`, `alknet/call` — iroh uses raw keys only; native ALPNs |
|
||||||
|
|
||||||
|
Rationale for split (not same-list):
|
||||||
|
|
||||||
|
- **No dead negotiation.** A raw-key QUIC listener does not advertise
|
||||||
|
`h2`/`http/1.1` — browsers cannot connect to a raw-key listener
|
||||||
|
(RFC 7250 unsupported), and native clients do not negotiate HTTP
|
||||||
|
ALPNs. Advertising them is harmless but misleading: it implies the
|
||||||
|
listener serves a client class it cannot.
|
||||||
|
- **No misleading advertisement.** An X.509 TCP+TLS listener does not
|
||||||
|
advertise `alknet/call` as a top-level ALPN unless the deployment
|
||||||
|
explicitly serves native call-over-TCP clients. The web endpoint
|
||||||
|
serves browsers and registration, not raw call-protocol clients. If
|
||||||
|
a deployment wants call-over-TCP on the web endpoint, it adds
|
||||||
|
`alknet/call` to the X.509 config's list — an explicit choice, not a
|
||||||
|
default.
|
||||||
|
- **The `alknet/channels` exception.** The web endpoint advertises
|
||||||
|
`alknet/channels` so that WebSocket-carrying-channels (OQ-65) works:
|
||||||
|
a browser opens a WebSocket (HTTP upgrade on `h2`/`http/1.1`), and
|
||||||
|
the WebSocket stream carries the channels protocol. The channels
|
||||||
|
ALPN is advertised on the X.509 config for this path, not for
|
||||||
|
native-channels-over-TCP (which is the native endpoint's concern).
|
||||||
|
- **`alknet/register` (future).** When the direct-registration ALPN
|
||||||
|
exists, it is an entry point advertised on both the native and web
|
||||||
|
configs (workers may register over either transport). It is not
|
||||||
|
advertised on iroh (iroh peers establish identity via NodeId, not
|
||||||
|
registration tokens). Revisiting the iroh exclusion requires a new
|
||||||
|
ADR.
|
||||||
|
|
||||||
|
The assembly layer builds the ALPN lists. The pattern (split by
|
||||||
|
endpoint type) is the one-way door — downstream consumers copy it.
|
||||||
|
The specific ALPNs in each list are two-way (additive — adding
|
||||||
|
`alknet/register` or `alknet/ssh` to a config's list is a config
|
||||||
|
change, not a structural one).
|
||||||
|
|
||||||
|
### 4. Foundational handlers — two categories
|
||||||
|
|
||||||
|
The foundational handlers are not one undifferentiated backlog. They
|
||||||
|
fall into two structurally different categories:
|
||||||
|
|
||||||
|
**Channels data-channel ALPNs** — gated by the channels substrate,
|
||||||
|
opened via `channel/open` on channel 0, inherit ACL + bidirectionality
|
||||||
|
from channels + call. These are NOT in any `TlsServerConfig`'s ALPN
|
||||||
|
list — they are negotiated inside channels, not at the TLS layer.
|
||||||
|
|
||||||
|
| ALPN (inside channels) | Crate | Status |
|
||||||
|
|------------------------|-------|--------|
|
||||||
|
| `alknet/tty` | `alknet-tty` | specced (ADR-052–057), implemented |
|
||||||
|
| `alknet/tunnel` | (in `alknet-channels` or sibling) | POC-validated, not yet specced |
|
||||||
|
| `alknet/socks5` | (TBD) | not yet specced — SOCKS5 proxy over channels |
|
||||||
|
| `alknet/fs` | (TBD) | not yet specced — filesystem access over channels |
|
||||||
|
| `alknet/sftp` | (TBD) | not yet specced — SFTP over channels |
|
||||||
|
|
||||||
|
**SSH — an endpoint ALPN that wraps channels.** SSH is structurally
|
||||||
|
different from the channels data-channel ALPNs. It is an endpoint ALPN
|
||||||
|
(negotiated at the TLS layer on the native config), and it runs
|
||||||
|
channels *inside* it (channels-over-SSH): the SSH server accepts a
|
||||||
|
connection, and each SSH channel becomes a channels data-channel ALPN.
|
||||||
|
SSH uses the same RFC 7250 keys as the native endpoint — it is a
|
||||||
|
legacy-client entry point for git/sftp compatibility, not a new
|
||||||
|
identity model. SSH is gated by channels (the channels run inside it)
|
||||||
|
but is itself an endpoint ALPN, not a data-channel ALPN.
|
||||||
|
|
||||||
|
| ALPN | Crate | Category | Status |
|
||||||
|
|------|-------|----------|--------|
|
||||||
|
| `alknet/ssh` | `alknet-ssh` | endpoint ALPN (wraps channels) | not yet specced — russh server channels wrapper for git/sftp compat; legacy clients; comes after tunnel/sftp/etc. |
|
||||||
|
|
||||||
|
SSH is advertised on the **native** config's ALPN list (raw-key QUIC
|
||||||
|
or TCP+TLS), not the web config. It is a legacy-native-client path,
|
||||||
|
not a browser path. It comes later in the roadmap — tunnels, sftp, and
|
||||||
|
other channels data-channel ALPNs are prioritized first because they
|
||||||
|
serve the primary use cases; SSH serves legacy compatibility.
|
||||||
|
|
||||||
|
### 5. A hub composes a subset; "must support TCP+TLS and QUIC" is corrected
|
||||||
|
|
||||||
|
The hub README's "a hub **must** support TCP+TLS and QUIC endpoints
|
||||||
|
simultaneously" is corrected. A hub composes a subset of endpoint
|
||||||
|
types. The subset determines which transports and ALPN lists the hub
|
||||||
|
uses. A minimal hub (iroh only) has no TCP+TLS and no quinn listener;
|
||||||
|
a web+native hub has both but no iroh; a full hub has all three.
|
||||||
|
|
||||||
|
The "must" applied to the first real use case (web + native), not to
|
||||||
|
all hubs. The correction is structural: the hub crate's composition API
|
||||||
|
takes a subset of endpoint types, not a fixed pair.
|
||||||
|
|
||||||
|
## What this does NOT change
|
||||||
|
|
||||||
|
- **`AlknetEndpoint` (ADR-083)** — the endpoint struct is unchanged.
|
||||||
|
It takes transports via `with_quinn` / `with_iroh` / `with_tcp_tls`
|
||||||
|
and runs their accept loops. The endpoint-types model is a
|
||||||
|
composition pattern at the assembly layer, not a new endpoint struct
|
||||||
|
field. The assembly layer builds the `TlsServerConfig`s and the
|
||||||
|
transports per endpoint type and hands them to the endpoint.
|
||||||
|
- **`TlsServerConfig` (ADR-082)** — the `new(identity, alpns)` signature
|
||||||
|
is unchanged. The caller (assembly layer) decides the ALPN list; this
|
||||||
|
ADR specifies *how* the caller decides — by endpoint type, not by
|
||||||
|
"same list or split" guesswork.
|
||||||
|
- **`HandlerRegistry` (ADR-010)** — all ALPNs (entry-point and endpoint)
|
||||||
|
are registered on the same registry. The distinction is in which
|
||||||
|
`TlsServerConfig` advertises them, not in which registry holds them.
|
||||||
|
A connection negotiating `h2` and a connection negotiating
|
||||||
|
`alknet/channels` both dispatch through the same `HandlerRegistry`;
|
||||||
|
the difference is which listener accepted them and whether identity
|
||||||
|
was required at the TLS layer.
|
||||||
|
- **The channels substrate (ADR-071)** — channels data-channel ALPNs
|
||||||
|
are unchanged. They are negotiated inside channels, not at the TLS
|
||||||
|
layer.
|
||||||
|
- **ADR-034 (three peer roles)** — the three peer roles (public X.509
|
||||||
|
endpoint, transport relay, hub/hosting node) are about *client-side
|
||||||
|
outbound* identity. This ADR is about *server-side inbound* endpoint
|
||||||
|
composition. They are orthogonal: a hub (role 3) composes endpoint
|
||||||
|
types for inbound; a client dialing a public X.509 endpoint (role 1)
|
||||||
|
is an outbound concern. The two decisions compose without conflict.
|
||||||
|
- **ADR-044 (WebSocket for browsers)** — the browser bidirectional path
|
||||||
|
uses WebSocket (unchanged). ADR-048 is **not superseded** by this
|
||||||
|
ADR; OQ-65 is a separate question that may extend ADR-048 (WebSocket
|
||||||
|
carrying channels, not just call). Whether WebSocket carries the call
|
||||||
|
protocol only (ADR-048) or carries channels (OQ-65) is a separate
|
||||||
|
question; this ADR's ALPN-list-split accounts for either outcome by
|
||||||
|
advertising `alknet/channels` on the web config by default (the
|
||||||
|
WebSocket-carrying-channels path needs it; the
|
||||||
|
WebSocket-carrying-call-only path does not, but the advertisement is
|
||||||
|
harmless if the hub also serves native channels-over-TCP on the web
|
||||||
|
config).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
|
||||||
|
- **OQ-62 is resolved.** The ALPN-list question has a principled
|
||||||
|
answer: split by endpoint type, because each endpoint type serves a
|
||||||
|
different client class with different negotiable ALPNs. The assembly
|
||||||
|
layer's wiring pattern is now guessable, not a fork in the docs.
|
||||||
|
- **The hub's composition is explicit.** A hub composes a subset of
|
||||||
|
endpoint types; the subset determines transports, identity models,
|
||||||
|
auth models, and ALPN lists. "Must support TCP+TLS and QUIC" is
|
||||||
|
corrected to "composes the subset its deployment needs." A minimal
|
||||||
|
hub (iroh only) is a first-class shape, not a degenerate case.
|
||||||
|
- **Entry points are named.** The structural difference between
|
||||||
|
`h2`/`http/1.1` (accepted without identity, per-request auth) and
|
||||||
|
`alknet/channels` (identity required) is explicit. This unblocks the
|
||||||
|
`alknet/register` ALPN design (worker registration over QUIC/TCP
|
||||||
|
without HTTP) — it is an entry point, not an endpoint, and its
|
||||||
|
semantics follow from the distinction.
|
||||||
|
- **SSH is correctly categorized.** SSH is an endpoint ALPN that wraps
|
||||||
|
channels, not a channels data-channel ALPN. It is advertised on the
|
||||||
|
native config, not the web config, and it uses RFC 7250 keys (same
|
||||||
|
as the native endpoint). The foundational handler backlog is no
|
||||||
|
longer an undifferentiated list.
|
||||||
|
- **The first real use case is clear.** Web + native (public domain
|
||||||
|
with X.509 + raw-key QUIC) is the first deployed subset. Iroh is a
|
||||||
|
hard requirement for the project but not the first deployed subset.
|
||||||
|
The roadmap is: web + native first, iroh, then foundational handlers
|
||||||
|
(tunnel, sftp, socks5, fs), then SSH (legacy compat, last).
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
|
||||||
|
- **The assembly layer has more logic.** Splitting ALPN lists by
|
||||||
|
endpoint type requires the assembly layer to filter
|
||||||
|
`registry.alpn_strings()` per `TlsServerConfig`. This is a small
|
||||||
|
amount of code (a filter per config) but is more than "pass the same
|
||||||
|
list to both." The pattern is documented here; downstream consumers
|
||||||
|
copy it.
|
||||||
|
- **`alknet/channels` appears on the web config.** This is correct
|
||||||
|
(WebSocket-carrying-channels needs it, per OQ-65) but means the web
|
||||||
|
config advertises an ALPN that is also on the native config. A
|
||||||
|
deployment that does not serve WebSocket-channels and does not serve
|
||||||
|
native-channels-over-TCP on the web endpoint can omit
|
||||||
|
`alknet/channels` from the web config. The default (include it) is
|
||||||
|
safer; the omission is an explicit assembly-layer choice.
|
||||||
|
- **The `alknet/register` ALPN is not yet specced.** This ADR names it
|
||||||
|
as a future entry point but does not design it. Worker registration
|
||||||
|
over HTTP (the current OQ-58 path) remains the first implementation;
|
||||||
|
`alknet/register` is a later simplification that removes the HTTP
|
||||||
|
dependency from the registration flow.
|
||||||
|
|
||||||
|
## Door type
|
||||||
|
|
||||||
|
**One-way.** The three-endpoint-type model, the entry-point/endpoint
|
||||||
|
distinction, and the split-by-endpoint-type ALPN list pattern are
|
||||||
|
structural. The assembly-layer wiring pattern is what downstream
|
||||||
|
consumers copy; reversing it (back to same-list, or back to
|
||||||
|
undifferentiated ALPNs) would break the composition model and re-introduce
|
||||||
|
the tangle this ADR resolves. The specific ALPNs in each list are
|
||||||
|
two-way (additive); the pattern (split by endpoint type) is one-way.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- OQ-62 (resolved by this ADR) — does a hub pass the same ALPN list to
|
||||||
|
both `TlsServerConfig`s?
|
||||||
|
- [ADR-082](082-alknet-tls-extraction.md) — `TlsServerConfig::new(identity, alpns)`;
|
||||||
|
the caller decides the ALPN list; this ADR specifies how
|
||||||
|
- [ADR-083](083-endpoint-as-accept-loop-runner.md) — `AlknetEndpoint` as
|
||||||
|
multi-transport accept-loop runner; the endpoint-types model is a
|
||||||
|
composition pattern on top of it
|
||||||
|
- [ADR-085](085-workspace-scope-core-vs-consumer-repos.md) — workspace
|
||||||
|
scope; the foundational handler categorization (§4) amends the scope
|
||||||
|
table's handler list
|
||||||
|
- [ADR-071](071-channels-wire-format.md) — channels data-channel ALPNs
|
||||||
|
are negotiated inside channels, not at the TLS layer
|
||||||
|
- [ADR-072](072-channel-0-pre-negotiated-call.md) — channel 0 identity
|
||||||
|
resolution (why `alknet/channels` is an endpoint, not an entry point)
|
||||||
|
- [ADR-044](044-defer-webtransport-browsers-use-websocket.md) —
|
||||||
|
WebSocket for browsers; WebTransport deferred
|
||||||
|
- [ADR-048](048-websocket-native-session-not-gateway.md) — WebSocket
|
||||||
|
carries the native call-protocol session (OQ-65 may extend this to
|
||||||
|
channels)
|
||||||
|
- [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) — three
|
||||||
|
peer roles (client-side outbound); orthogonal to this ADR's
|
||||||
|
server-side inbound endpoint composition
|
||||||
|
- [ADR-027](027-tls-identity-redesign-acme-rawkey-decoupling.md) —
|
||||||
|
`TlsIdentity` (RawKey / X509 / Acme); the identity models per
|
||||||
|
endpoint type
|
||||||
|
- OQ-58 — worker registration flow (the entry point that
|
||||||
|
`alknet/register` will eventually serve directly)
|
||||||
|
- OQ-65 — WebSocket carrying channels (the browser path that requires
|
||||||
|
`alknet/channels` on the web config)
|
||||||
@@ -170,6 +170,7 @@ Door type is separate from whether a decision is made. A two-way door is a decis
|
|||||||
| [OQ-53](questions/053-backoff-config-defaults.md) | BackoffConfig default policy | open | two | low |
|
| [OQ-53](questions/053-backoff-config-defaults.md) | BackoffConfig default policy | open | two | low |
|
||||||
| [OQ-54](questions/054-inbound-worker-hook-placement.md) | Inbound worker on_worker_connected hook placement | resolved | two | low |
|
| [OQ-54](questions/054-inbound-worker-hook-placement.md) | Inbound worker on_worker_connected hook placement | resolved | two | low |
|
||||||
| [OQ-58](questions/058-worker-registration-flow.md) | Worker Registration Flow | open | one | high |
|
| [OQ-58](questions/058-worker-registration-flow.md) | Worker Registration Flow | open | one | high |
|
||||||
|
| [OQ-65](questions/065-websocket-carrying-channels.md) | Should WebSocket Carry the Channels Protocol (Not Just the Call Protocol)? | open | one | med |
|
||||||
|
|
||||||
### alknet-channels
|
### alknet-channels
|
||||||
|
|
||||||
@@ -182,9 +183,10 @@ Door type is separate from whether a decision is made. A two-way door is a decis
|
|||||||
|
|
||||||
| OQ | Title | Status | Door | Pri |
|
| OQ | Title | Status | Door | Pri |
|
||||||
|----|-------|--------|------|-----|
|
|----|-------|--------|------|-----|
|
||||||
| [OQ-62](questions/062-alpn-list-sharing-two-config-hub.md) | Does a Hub Pass the Same ALPN List to Both `TlsServerConfig`s? | open | one | high |
|
| [OQ-62](questions/062-alpn-list-sharing-two-config-hub.md) | Does a Hub Pass the Same ALPN List to Both `TlsServerConfig`s? | resolved | one | high |
|
||||||
| [OQ-63](questions/063-tlserror-shape.md) | `TlsError` Shape | open | one | high |
|
| [OQ-63](questions/063-tlserror-shape.md) | `TlsError` Shape | open | one | high |
|
||||||
| [OQ-64](questions/064-client-side-tls-helper.md) | Should `alknet-tls` Provide a Client-Side TLS Config Helper? | deferred(scope) | two | med |
|
| [OQ-64](questions/064-client-side-tls-helper.md) | Should `alknet-tls` Provide a Client-Side TLS Config Helper? | deferred(scope) | two | med |
|
||||||
|
| [OQ-65](questions/065-websocket-carrying-channels.md) | Should WebSocket Carry the Channels Protocol (Not Just the Call Protocol)? | open | one | med |
|
||||||
|
|
||||||
## Deferred / Blocked
|
## Deferred / Blocked
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ last_updated: 2026-07-15
|
|||||||
Alknet is a **core networking toolkit** for building self-hostable,
|
Alknet is a **core networking toolkit** for building self-hostable,
|
||||||
p2p-capable, "vpn-like without being a vpn" systems. It is built on
|
p2p-capable, "vpn-like without being a vpn" systems. It is built on
|
||||||
QUIC+TLS with ALPN-based protocol dispatch, plus TCP+TLS for the
|
QUIC+TLS with ALPN-based protocol dispatch, plus TCP+TLS for the
|
||||||
hub/HTTP path. A single endpoint accepts connections on one port, and
|
web/browser path. A single endpoint accepts connections on one port
|
||||||
the ALPN string negotiated during the TLS handshake routes each
|
per transport, and the ALPN string negotiated during the TLS handshake
|
||||||
connection to the correct protocol handler. Every service — call,
|
routes each connection to the correct protocol handler. Every service —
|
||||||
channels, HTTP, TTY, tunnels, SFTP — is an ALPN on a shared endpoint
|
call, channels, HTTP, TTY, tunnels, SFTP — is an ALPN on a shared
|
||||||
or a data-channel ALPN inside channels.
|
endpoint or a data-channel ALPN inside channels.
|
||||||
|
|
||||||
This is the core insight: **a service IS an ALPN.** One endpoint, one
|
This is the core insight: **a service IS an ALPN.** One endpoint, one
|
||||||
port, many protocols — dispatched by the TLS handshake, not by
|
port per transport, many protocols — dispatched by the TLS handshake,
|
||||||
application-level peeking or separate listeners.
|
not by application-level peeking or separate listeners.
|
||||||
|
|
||||||
### Scope: core mono-repo vs. consumer repos
|
### Scope: core mono-repo vs. consumer repos
|
||||||
|
|
||||||
@@ -31,6 +31,24 @@ a hub or worker (docker operations, agent, future applications) are
|
|||||||
`alknet-core` directly. See [ADR-085](decisions/085-workspace-scope-core-vs-consumer-repos.md)
|
`alknet-core` directly. See [ADR-085](decisions/085-workspace-scope-core-vs-consumer-repos.md)
|
||||||
for the full scope decision.
|
for the full scope decision.
|
||||||
|
|
||||||
|
### Endpoint types and entry points (ADR-086)
|
||||||
|
|
||||||
|
A hub composes a subset of three **endpoint types**, each an independent
|
||||||
|
listener with its own identity model, auth model, and transport(s):
|
||||||
|
|
||||||
|
| Endpoint type | Identity | Auth model | Transport(s) | Client class |
|
||||||
|
|---------------|----------|------------|--------------|--------------|
|
||||||
|
| **web** | X.509 (ACME or manual) | token-based (Bearer) | TCP+TLS (HTTP, WebSocket), QUIC (WebTransport — deferred) | browsers, curl, registration |
|
||||||
|
| **native** | RFC 7250 raw key (Ed25519) | key-based (fingerprint) | QUIC (primary), TCP+TLS (fallback) | alknet-native clients, workers |
|
||||||
|
| **iroh** | RFC 7250 raw key (NodeId) | key-based (fingerprint) | iroh (relay-assisted QUIC) | p2p peers, NAT'd nodes |
|
||||||
|
|
||||||
|
A full hub runs all three; a minimal hub runs iroh alone (no public IP
|
||||||
|
required). The first real use case is web + native. See
|
||||||
|
[ADR-086](decisions/086-endpoint-types-and-entry-points.md) for the
|
||||||
|
full model, including the entry-point vs. endpoint ALPN distinction
|
||||||
|
(entry points are accepted without identity; endpoints require
|
||||||
|
identity resolution) and the split-by-endpoint-type ALPN list pattern.
|
||||||
|
|
||||||
## Why ALPN Dispatch
|
## Why ALPN Dispatch
|
||||||
|
|
||||||
The previous architecture used a three-layer model (StreamInterface/MessageInterface, ListenerConfig, OperationEnv) that required separate listener types, application-level protocol detection via byte-peeking, and complex dispatch paths. ALPN negotiation eliminates all of this:
|
The previous architecture used a three-layer model (StreamInterface/MessageInterface, ListenerConfig, OperationEnv) that required separate listener types, application-level protocol detection via byte-peeking, and complex dispatch paths. ALPN negotiation eliminates all of this:
|
||||||
@@ -92,12 +110,12 @@ alknet-vault (standalone — foundational to ACL: key derivation, identity)
|
|||||||
├── Foundational handlers (inside channels as data-channel ALPNs, or on the endpoint)
|
├── Foundational handlers (inside channels as data-channel ALPNs, or on the endpoint)
|
||||||
│ ├── alknet-tty alknet/tty — specced (ADR-052–057), implemented
|
│ ├── alknet-tty alknet/tty — specced (ADR-052–057), implemented
|
||||||
│ ├── alknet-tty-local PTY/pipe backend — sibling crate (ADR-054)
|
│ ├── alknet-tty-local PTY/pipe backend — sibling crate (ADR-054)
|
||||||
│ ├── alknet-http h2/http1.1 + WebSocket — the HTTP edge case (registration, browser, MCP)
|
│ ├── alknet-http h2/http1.1 + WebSocket — the web endpoint edge case (registration, browser, MCP)
|
||||||
│ ├── alknet-ssh russh server channels wrapper — for git/sftp compat [not yet specced]
|
│ ├── alknet-ssh russh server — endpoint ALPN wrapping channels (channels-over-SSH); RFC 7250 keys; legacy compat [not yet specced]
|
||||||
│ ├── alknet-tunnel alknet/tunnel — POC-validated, minimal spec needed [not yet specced]
|
│ ├── alknet-tunnel alknet/tunnel — channels data-channel ALPN; POC-validated, minimal spec needed [not yet specced]
|
||||||
│ ├── alknet-socks5 SOCKS5 proxy over channels [not yet specced]
|
│ ├── alknet-socks5 SOCKS5 proxy — channels data-channel ALPN [not yet specced]
|
||||||
│ ├── alknet-fs filesystem access over channels [not yet specced]
|
│ ├── alknet-fs filesystem access — channels data-channel ALPN [not yet specced]
|
||||||
│ └── alknet-sftp SFTP over channels [not yet specced]
|
│ └── alknet-sftp SFTP — channels data-channel ALPN [not yet specced]
|
||||||
│
|
│
|
||||||
└── Consumer repos (separate repos, depend on the published core crates)
|
└── Consumer repos (separate repos, depend on the published core crates)
|
||||||
alknet-docker docker operations — a docker host is a worker
|
alknet-docker docker operations — a docker host is a worker
|
||||||
@@ -145,19 +163,38 @@ TLS handshake, dispatched by the endpoint) and **channels data-channel
|
|||||||
ALPNs** (negotiated via `channel/open` inside a channels connection,
|
ALPNs** (negotiated via `channel/open` inside a channels connection,
|
||||||
dispatched by the channels substrate). See ADR-071 and ADR-073.
|
dispatched by the channels substrate). See ADR-071 and ADR-073.
|
||||||
|
|
||||||
|
Within the endpoint ALPNs, there is a further distinction (ADR-086 §2):
|
||||||
|
**entry points** (connections accepted without an established peer
|
||||||
|
identity; per-request auth inside the handler) vs. **endpoints** in the
|
||||||
|
narrow sense (connections that require identity resolution before the
|
||||||
|
handler runs). This distinction determines which `TlsServerConfig`
|
||||||
|
advertises which ALPNs — each endpoint type (web, native, iroh)
|
||||||
|
advertises only the ALPNs its client class can negotiate (ADR-086 §3).
|
||||||
|
|
||||||
### Endpoint ALPNs
|
### Endpoint ALPNs
|
||||||
|
|
||||||
| ALPN | Handler | Description |
|
#### Entry points (no identity required at the TLS layer)
|
||||||
|------|---------|-------------|
|
|
||||||
| `alknet/call` | `CallAdapter` | Call protocol: operations, streaming, pub/sub (hand-rolled EventEnvelope — ADR-064) |
|
| ALPN | Handler | Endpoint type | Description |
|
||||||
| `alknet/channels` | `ChannelsAdapter` | Multiplexing substrate: N channels over one transport stream (ADR-071); channel 0 = `alknet/call` (ADR-072) |
|
|------|---------|---------------|-------------|
|
||||||
| `h2` / `http/1.1` | `HttpAdapter` | Standard HTTP for browsers, curl, registration endpoint (WebSocket for bidirectional — ADR-048) |
|
| `h2` / `http/1.1` | `HttpAdapter` | web | HTTP registration, browser API routes, stealth decoy, WebSocket upgrade (ADR-048) |
|
||||||
|
| `alknet/register` (future) | (registration handler) | native, web | Worker registration over QUIC/TCP without HTTP — a direct ALPN for enrollment. Not yet specced. |
|
||||||
|
|
||||||
|
#### Endpoints (identity required before dispatch)
|
||||||
|
|
||||||
|
| ALPN | Handler | Endpoint type | Description |
|
||||||
|
|------|---------|---------------|-------------|
|
||||||
|
| `alknet/channels` | `ChannelsAdapter` | native, web (for WS-channels), iroh | Multiplexing substrate: N channels over one transport stream (ADR-071); channel 0 = `alknet/call` (ADR-072). Identity resolved on channel 0 before dispatch. |
|
||||||
|
| `alknet/call` | `CallAdapter` | native, iroh | Call protocol: operations, streaming, pub/sub (hand-rolled EventEnvelope — ADR-064). When used as a top-level ALPN; as channel 0 inside channels, identity is resolved before dispatch. |
|
||||||
|
| `alknet/ssh` (future) | (ssh handler) | native | SSH server wrapping channels (channels-over-SSH); RFC 7250 keys; legacy-client entry point for git/sftp compat. Not yet specced. |
|
||||||
|
|
||||||
### Channels data-channel ALPNs
|
### Channels data-channel ALPNs
|
||||||
|
|
||||||
These ride inside a `alknet/channels` connection as data channels,
|
These ride inside a `alknet/channels` connection as data channels,
|
||||||
opened via `channel/open` (ADR-073). They get the ACL and
|
opened via `channel/open` (ADR-073). They get the ACL and
|
||||||
bidirectionality of channels + call for free.
|
bidirectionality of channels + call for free. They are NOT in any
|
||||||
|
`TlsServerConfig`'s ALPN list — they are negotiated inside channels,
|
||||||
|
not at the TLS layer.
|
||||||
|
|
||||||
| ALPN | Handler | Status |
|
| ALPN | Handler | Status |
|
||||||
|------|---------|--------|
|
|------|---------|--------|
|
||||||
@@ -168,6 +205,19 @@ bidirectionality of channels + call for free.
|
|||||||
| `alknet/sftp` | (sftp handler) | not yet specced |
|
| `alknet/sftp` | (sftp handler) | not yet specced |
|
||||||
| (future) | any ALPN a consumer registers | channels supports any ALPN — ADR-071 |
|
| (future) | any ALPN a consumer registers | channels supports any ALPN — ADR-071 |
|
||||||
|
|
||||||
|
### SSH — an endpoint ALPN that wraps channels (ADR-086 §4)
|
||||||
|
|
||||||
|
SSH is structurally different from the channels data-channel ALPNs
|
||||||
|
above. It is an **endpoint ALPN** (negotiated at the TLS layer on the
|
||||||
|
native config), and it runs channels *inside* it
|
||||||
|
(channels-over-SSH): the SSH server accepts a connection, and each SSH
|
||||||
|
channel becomes a channels data-channel ALPN. SSH uses the same RFC
|
||||||
|
7250 keys as the native endpoint — it is a legacy-client entry point
|
||||||
|
for git/sftp compatibility, not a new identity model. SSH is gated by
|
||||||
|
channels (the channels run inside it) but is itself an endpoint ALPN,
|
||||||
|
not a data-channel ALPN. It comes later in the roadmap — tunnels,
|
||||||
|
sftp, and other data-channel ALPNs are prioritized first.
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
> **`alknet/vault`** is not in the ALPN registry. alknet-vault is a
|
> **`alknet/vault`** is not in the ALPN registry. alknet-vault is a
|
||||||
@@ -179,11 +229,13 @@ bidirectionality of channels + call for free.
|
|||||||
> (`IdentityProvider`, `PeerEntry`, fingerprint resolution) derives
|
> (`IdentityProvider`, `PeerEntry`, fingerprint resolution) derives
|
||||||
> from vault-managed keys. See ADR-008, ADR-014, ADR-018, ADR-019.
|
> from vault-managed keys. See ADR-008, ADR-014, ADR-018, ADR-019.
|
||||||
|
|
||||||
> **`alknet/http`** is the HTTP edge case. It is an endpoint ALPN
|
> **`alknet/http`** is the web endpoint edge case. It is an
|
||||||
> (`h2`/`http/1.1`), not a channels data-channel ALPN — it wraps the
|
> entry-point ALPN (`h2`/`http/1.1`), not a channels data-channel ALPN
|
||||||
> call protocol for browser/curl access (registration, MCP/OpenAPI
|
> — it wraps the call protocol for browser/curl access (registration,
|
||||||
> adapters, WebSocket bidirectional path). See
|
> MCP/OpenAPI adapters, WebSocket bidirectional path). It is advertised
|
||||||
> [crates/http/README.md](crates/http/README.md).
|
> on the web endpoint's `TlsServerConfig` (X.509/ACME), not the native
|
||||||
|
> config. See [crates/http/README.md](crates/http/README.md) and
|
||||||
|
> ADR-086 §2 (entry points vs. endpoints).
|
||||||
|
|
||||||
> **Consumer-repo ALPNs** (e.g., docker operations) are not listed
|
> **Consumer-repo ALPNs** (e.g., docker operations) are not listed
|
||||||
> here. A consumer that builds on top of a hub or worker registers its
|
> here. A consumer that builds on top of a hub or worker registers its
|
||||||
@@ -317,6 +369,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
|||||||
| [082](decisions/082-alknet-tls-extraction.md) | alknet-tls Crate Extraction | Shared `TlsServerConfig` across quinn + TCP+TLS + iroh; one ACME state machine |
|
| [082](decisions/082-alknet-tls-extraction.md) | alknet-tls Crate Extraction | Shared `TlsServerConfig` across quinn + TCP+TLS + iroh; one ACME state machine |
|
||||||
| [083](decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as Multi-Transport Accept-Loop Runner | Endpoint takes no TLS config; TCP+TLS is an owned transport; public `dispatch` for SSH/WT |
|
| [083](decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as Multi-Transport Accept-Loop Runner | Endpoint takes no TLS config; TCP+TLS is an owned transport; public `dispatch` for SSH/WT |
|
||||||
| [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | Core mono-repo (substrate + deployment shapes + foundational handlers + vault) vs. consumer repos (docker, agent) |
|
| [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | Core mono-repo (substrate + deployment shapes + foundational handlers + vault) vs. consumer repos (docker, agent) |
|
||||||
|
| [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Three endpoint types (web/native/iroh); entry-point vs. endpoint ALPN distinction; split ALPN lists per endpoint type (resolves OQ-62) |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
lists); `docs/architecture/crates/core/endpoint.md` (the ALPN section
|
lists); `docs/architecture/crates/core/endpoint.md` (the ALPN section
|
||||||
previously stated "both connection sources advertise the same set of
|
previously stated "both connection sources advertise the same set of
|
||||||
ALPNs," which is stale under the two-config hub model).
|
ALPNs," which is stale under the two-config hub model).
|
||||||
- **Status**: open
|
- **Status**: resolved
|
||||||
- **Door type**: one-way (the ALPN list each `TlsServerConfig` advertises
|
- **Door type**: one-way (the ALPN list each `TlsServerConfig` advertises
|
||||||
is baked into the `rustls::ServerConfig` at construction; changing it
|
is baked into the `rustls::ServerConfig` at construction; changing it
|
||||||
after the hub is deployed is a config+restart, but the *pattern* —
|
after the hub is deployed is a config+restart, but the *pattern* —
|
||||||
@@ -16,39 +16,51 @@
|
|||||||
- **Priority**: high (the hub is the first two-config consumer; its
|
- **Priority**: high (the hub is the first two-config consumer; its
|
||||||
wiring sets the pattern, and an implementer cannot write the hub's
|
wiring sets the pattern, and an implementer cannot write the hub's
|
||||||
assembly code without this decided)
|
assembly code without this decided)
|
||||||
- **Resolution**: Not yet decided. The two plausible options:
|
- **Resolution**: **Split list, by endpoint type.** Each
|
||||||
|
`TlsServerConfig` advertises only the ALPNs its endpoint type's
|
||||||
|
client class can negotiate. The hub composes a subset of three
|
||||||
|
endpoint types (web, native, iroh), each with its own identity model,
|
||||||
|
auth model, and transport(s). The assembly layer filters
|
||||||
|
`registry.alpn_strings()` per `TlsServerConfig` by endpoint type:
|
||||||
|
|
||||||
**Option A — same list (union) to both configs.** Both
|
- **raw-key config (native endpoint)**: `alknet/channels`,
|
||||||
`TlsServerConfig`s receive `registry.alpn_strings()` verbatim. The
|
`alknet/call`, `alknet/ssh` (future) — the native ALPNs. No
|
||||||
raw-key QUIC listener advertises `h2`/`http/1.1` (browsers can't
|
`h2`/`http/1.1` (browsers cannot connect to a raw-key listener;
|
||||||
connect to a raw-key listener anyway, so the advertisement is
|
native clients do not negotiate HTTP ALPNs).
|
||||||
harmless dead negotiation). The X.509 TCP+TLS listener advertises
|
- **X.509/ACME config (web endpoint)**: `h2`, `http/1.1`,
|
||||||
`alknet/call` (a native client connecting over TCP+TLS with an X.509
|
`alknet/channels` (for WebSocket-carrying-channels, per OQ-65),
|
||||||
client cert can use it). Simplest wiring; no split logic; every
|
`acme-tls/1` (appended automatically). No `alknet/call` as a
|
||||||
transport can serve every ALPN.
|
top-level ALPN unless the deployment explicitly serves
|
||||||
|
call-over-TCP on the web endpoint.
|
||||||
|
- **iroh builder (iroh endpoint)**: `alknet/channels`,
|
||||||
|
`alknet/call` — iroh uses raw keys only; native ALPNs.
|
||||||
|
|
||||||
**Option B — split list, transport-appropriate.** The raw-key config
|
The rationale: each endpoint type serves a different client class.
|
||||||
gets the native ALPNs (`alknet/call`, `alknet/channels`,
|
Advertising ALPNs the client class cannot negotiate (e.g., `h2` on a
|
||||||
`alknet/tty`); the X.509/ACME config gets the union including
|
raw-key listener) is harmless but misleading — it implies the
|
||||||
`h2`/`http/1.1` (browser ALPNs that only make sense over TCP+TLS with
|
listener serves a client class it cannot. The split makes the
|
||||||
a domain cert). The assembly layer filters `registry.alpn_strings()`
|
advertisement honest and the assembly-layer wiring pattern guessable.
|
||||||
by which transports can serve each ALPN. More logic; cleaner
|
|
||||||
advertisement (no browser ALPNs on a raw-key listener).
|
|
||||||
|
|
||||||
The question is whether the "harmless dead negotiation" in Option A
|
The naming that makes this answerable is the **entry-point vs.
|
||||||
is acceptable or whether the cleaner advertisement in Option B is
|
endpoint** distinction (ADR-086 §2): entry-point ALPNs (`h2`,
|
||||||
worth the split logic. This needs a decision before the hub's
|
`http/1.1`, future `alknet/register`) are accepted without
|
||||||
assembly code is written — it is not guessable from the existing
|
established peer identity (per-request auth); endpoint ALPNs
|
||||||
specs, and guessing produces a wiring shape that downstream consumers
|
(`alknet/channels`, `alknet/call`, `alknet/ssh`) require identity
|
||||||
copy.
|
resolution. The web config advertises entry-point ALPNs (for
|
||||||
|
registration, browsers) + `alknet/channels` (for
|
||||||
|
WebSocket-channels); the native config advertises endpoint ALPNs
|
||||||
|
(for native clients); iroh advertises endpoint ALPNs (for p2p
|
||||||
|
peers).
|
||||||
|
|
||||||
Note: this is distinct from the *iroh* path. Iroh takes its ALPN list
|
See [ADR-086](../decisions/086-endpoint-types-and-entry-points.md)
|
||||||
from `iroh::Endpoint::builder().alpns()` at construction, set by the
|
for the full decision, including the three-endpoint-type model, the
|
||||||
assembly layer from `registry.alpn_strings()`. Iroh uses raw keys
|
hub-shape table (full / web+native / native+iroh / minimal), and the
|
||||||
only, so it gets the native ALPN set regardless of which option is
|
foundational-handler categorization (channels data-channel ALPNs vs.
|
||||||
chosen for the quinn/TCP+TLS pair.
|
SSH as an endpoint ALPN that wraps channels).
|
||||||
- **Cross-references**: ADR-082 (`TlsServerConfig::new` takes
|
- **Cross-references**: [ADR-086](../decisions/086-endpoint-types-and-entry-points.md)
|
||||||
`alpns: &[Vec<u8>]` — the caller decides), ADR-083 (the assembly
|
(the decision), ADR-082 (`TlsServerConfig::new` takes
|
||||||
layer builds transports; ALPN-list construction is its job),
|
`alpns: &[Vec<u8>]` — the caller decides; this OQ specified how),
|
||||||
OQ-64 (client-side TLS helper — related but orthogonal; this OQ is
|
ADR-083 (the assembly layer builds transports; ALPN-list construction
|
||||||
server-side advertisement)
|
is its job), OQ-65 (WebSocket carrying channels — why the web config
|
||||||
|
advertises `alknet/channels`), OQ-64 (client-side TLS helper —
|
||||||
|
related but orthogonal; this OQ is server-side advertisement)
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# OQ-65: Should WebSocket Carry the Channels Protocol (Not Just the Call Protocol)?
|
||||||
|
|
||||||
|
- **Origin**: `docs/architecture/crates/http/websocket.md` (ADR-048
|
||||||
|
specifies WebSocket carries the native call-protocol session — the
|
||||||
|
`EventEnvelope` wire format over a WebSocket text/binary frame
|
||||||
|
stream); `docs/architecture/decisions/086-endpoint-types-and-entry-points.md`
|
||||||
|
§3 (the web config advertises `alknet/channels` for the
|
||||||
|
WebSocket-carrying-channels path — a path ADR-048 does not specify).
|
||||||
|
- **Status**: open
|
||||||
|
- **Door type**: one-way (this supersedes or extends ADR-048's
|
||||||
|
"WebSocket carries the native call-protocol session" decision. If
|
||||||
|
WebSocket carries channels, a browser opens one WebSocket and gets
|
||||||
|
the full channels substrate — `channel/open`, data-channel ALPNs,
|
||||||
|
the relay path through the hub — instead of only the call-protocol
|
||||||
|
session. This is a browser-wire-format one-way door: once browsers
|
||||||
|
depend on the channels-over-WebSocket framing, changing it is a
|
||||||
|
breaking change for every browser client.)
|
||||||
|
- **Priority**: medium (the browser story works without this —
|
||||||
|
ADR-048's call-protocol-only WebSocket is functional — but
|
||||||
|
channels-over-WebSocket makes the browser a first-class channels
|
||||||
|
participant, which simplifies the hub relay and the browser's access
|
||||||
|
to data-channel ALPNs like TTY and tunnels. The question is whether
|
||||||
|
the simplification is worth the one-way-door commitment now, or
|
||||||
|
whether the call-protocol-only path suffices until a concrete
|
||||||
|
browser-needs-a-data-channel use case arrives.)
|
||||||
|
- **Resolution**: Not yet decided. The two options:
|
||||||
|
|
||||||
|
**Option A — WebSocket carries call only (ADR-048 unchanged).** The
|
||||||
|
browser opens a WebSocket and gets a call-protocol session
|
||||||
|
(`EventEnvelope` over WebSocket frames). Data-channel ALPNs (TTY,
|
||||||
|
tunnels) are not accessible from the browser — the browser can invoke
|
||||||
|
call operations but cannot open a `alknet/tty` channel. A browser
|
||||||
|
needing a TTY would use a separate WebSocket per session, each
|
||||||
|
carrying the TTY wire format directly (not via `channel/open`).
|
||||||
|
Simpler; the browser is a call-protocol client, not a channels
|
||||||
|
client.
|
||||||
|
|
||||||
|
**Option B — WebSocket carries channels (extends/supersedes
|
||||||
|
ADR-048).** The browser opens a WebSocket and gets a channels
|
||||||
|
connection — the 9-byte chunk format (ADR-071) over WebSocket binary
|
||||||
|
frames, with channel 0 as `alknet/call` and data channels opened via
|
||||||
|
`channel/open`. The browser is a full channels participant: it can
|
||||||
|
open TTY channels, tunnels, etc. through the same WebSocket. The
|
||||||
|
hub relay (ADR-079) works unchanged — the browser leg is a channels
|
||||||
|
connection, same as a native leg. This is the "browser as
|
||||||
|
channels client" path.
|
||||||
|
|
||||||
|
The trade-off: Option B commits to the channels-over-WebSocket
|
||||||
|
framing as a browser wire format (one-way door), but makes the
|
||||||
|
browser a first-class channels participant and simplifies the hub
|
||||||
|
(one relay model, not two). Option A keeps the browser simpler but
|
||||||
|
means the browser cannot use data-channel ALPNs over the same
|
||||||
|
connection — each data-channel ALPN needs its own WebSocket and its
|
||||||
|
own browser-side implementation.
|
||||||
|
|
||||||
|
This question is decision-ready when the first browser-needs-a-
|
||||||
|
data-channel use case arrives (e.g., a browser-based terminal that
|
||||||
|
needs `alknet/tty` over the hub). Until then, ADR-048's
|
||||||
|
call-protocol-only path is the implemented browser path, and the
|
||||||
|
web config advertises `alknet/channels` by default (per ADR-086 §3,
|
||||||
|
so the hub is ready for Option B if chosen).
|
||||||
|
|
||||||
|
Note: this is distinct from the WebTransport path (deferred per
|
||||||
|
ADR-044). WebTransport, when revived, would carry channels natively
|
||||||
|
(a WT bidi stream = a channels connection). WebSocket-carrying-
|
||||||
|
channels is the WebSocket equivalent — same substrate, different
|
||||||
|
transport. If Option B is chosen, the WebSocket-channels framing
|
||||||
|
and the WebTransport-channels framing share the channels wire
|
||||||
|
format (ADR-071); only the transport differs.
|
||||||
|
- **Cross-references**: ADR-048 (WebSocket carries the native
|
||||||
|
call-protocol session — the current decision this OQ may supersede
|
||||||
|
or extend), ADR-071 (channels wire format — the 9-byte chunk format
|
||||||
|
that would ride over WebSocket binary frames), ADR-079 (hub relay —
|
||||||
|
unchanged if the browser is a channels client), ADR-086 §3 (the web
|
||||||
|
config advertises `alknet/channels` for this path), ADR-044
|
||||||
|
(WebTransport deferred; WebSocket is the v1 browser path)
|
||||||
Reference in new issue
Block a user