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:
glm-5.2 committed 2026-07-15 05:19:11 +00:00
1 parent 7610ec1f31
commit 7d9b1ebad9
9 files changed
+800 -129

No files matched your search

+4 -3
View File
@@ -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/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/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/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/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; 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/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) |
@@ -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) |
| [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 |
| [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Accepted |
## 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
+17 -6
View File
@@ -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
builds no transports — the assembly layer reads `registry.alpn_strings()`
and passes the appropriate ALPN list to each `TlsServerConfig::new()`
(see [`crates/tls/README.md`](../tls/README.md)). For a single-config
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 assembly layer may pass different lists to each config; that split
is a hub-assembly-layer concern, not an endpoint concern.
(see [`crates/tls/README.md`](../tls/README.md)).
The ALPN list is **split by endpoint type** (ADR-086 §3, resolving
OQ-62): each `TlsServerConfig` advertises only the ALPNs its endpoint
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()`
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
@@ -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 |
| 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 |
| 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
+136 -49
View File
@@ -1,15 +1,16 @@
---
status: draft
last_updated: 2026-07-14
last_updated: 2026-07-15
---
# alknet-hub
The hub pattern as a reusable crate: multi-transport endpoint that
accepts workers and browsers over TCP+TLS and QUIC, relays channels
between legs (ADR-079), manages peer lifecycle, aggregates operations,
and exposes service discovery. One channels connection per peer;
multiple endpoint types coexisting.
The hub pattern as a reusable crate: a multi-transport endpoint that
composes a subset of three endpoint types (web, native, iroh —
ADR-086), accepts workers and browsers over whichever transports the
subset implies, relays channels between legs (ADR-079), manages peer
lifecycle, aggregates operations, and exposes service discovery. One
channels connection per peer; multiple endpoint types coexisting.
## What
@@ -24,29 +25,77 @@ types — it wires the existing `ChannelsAdapter`, `ChannelClient`,
`CallAdapter`, `PeerCompositeEnv`, `from_call`, and `Dispatcher` types
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
A hub **must** support TCP+TLS and QUIC endpoints simultaneously. This is
not a convenience — it is the structure of the primary use case. A hub
that provisions a worker (in a container, on vast.ai, on runpod) uses
TCP+TLS for registration and QUIC (or another transport) for the ongoing
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
A hub runs whichever transports its endpoint-type subset implies. A
full hub runs TCP+TLS (web), QUIC (native), and iroh (iroh)
simultaneously; a minimal hub runs iroh alone. The channels protocol is
transport-agnostic (ADR-071; ADR-065
`Connection::from_stream`/`from_bidi`). The hub's accept and dial paths
inherit that property — they take a `Connection`, not a `SocketAddr`
welded to a dial. See "Transport" below.
### What the hub provides
1. **Multi-transport endpoint** — accepts channels connections over
QUIC and over TCP+TLS (both owned transports on `AlknetEndpoint` via
`with_quinn` / `with_tcp_tls`, per ADR-083). Both feed the same
dispatch path. Also serves HTTP (`h2`/`http/1.1`) over TCP+TLS for
registration and browser access. All three coexist; the endpoint owns
all accept loops and `shutdown()` stops them all.
1. **Multi-transport endpoint** — composes a subset of three endpoint
types (web, native, iroh — ADR-086), each wired as an owned transport
on `AlknetEndpoint` via `with_quinn` / `with_iroh` / `with_tcp_tls`
(ADR-083). All feed the same dispatch path. A web+native hub runs
TCP+TLS (web, X.509) + QUIC (native, raw key); a minimal hub runs
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
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
`alknet-call` do not provide out of the box:
- The hub accepts channels connections over **multiple transports**
simultaneously. The channels crate is transport-agnostic (ADR-071,
ADR-065); the hub composes the multi-transport endpoint
(`AlknetEndpoint` with `with_quinn` / `with_tcp_tls`, ADR-083). The
accept loops themselves live in `alknet-core`; the hub provides the
handlers and wiring.
- The hub composes **multiple endpoint types** (ADR-086). The channels
crate is transport-agnostic (ADR-071, ADR-065); the hub composes the
multi-transport endpoint (`AlknetEndpoint` with `with_quinn` /
`with_iroh` / `with_tcp_tls`, ADR-083) and filters ALPN lists per
endpoint type. The accept loops themselves live in `alknet-core`; the
hub provides the handlers and wiring.
- The hub relays channels between legs (ADR-079) — terminating
channel 0 on each leg, translating `channel/open`, byte-forwarding
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
The hub accepts and dials channels connections over any transport
the channels protocol supports (ADR-071). In practice, a hub runs
multiple accept loops simultaneously — all owned by `AlknetEndpoint`
via builder methods (ADR-083):
the channels protocol supports (ADR-071). A hub composes a subset of
three endpoint types (ADR-086); the subset determines which transports
run. All are owned by `AlknetEndpoint` via builder methods (ADR-083):
| Builder method | Transport | What it carries |
|----------------|-----------|-----------------|
| `with_quinn` | QUIC (quinn) | Channels connections from workers with QUIC reachability |
| `with_iroh` | QUIC (iroh, relay-assisted) | Channels connections from workers behind NAT |
| `with_tcp_tls` | TCP + TLS (`h2`/`http/1.1`/`alknet/channels`) | HTTP registration endpoint, browser access, channels-over-TCP from workers |
| (Future) WebTransport | WebTransport | Browser bidirectional path (deferred per ADR-044; WebSocket is the v1 browser path) |
| Builder method | Endpoint type | Transport | What it carries |
|----------------|---------------|-----------|-----------------|
| `with_quinn` (raw-key config) | native | QUIC (quinn) | `alknet/channels`, `alknet/call`, `alknet/ssh` (future) — native clients with QUIC reachability |
| `with_quinn` (X.509/ACME config) | web | QUIC (quinn) | `h2`, `http/1.1`, `h3` (WebTransport — deferred per ADR-044) — browsers over QUIC |
| `with_iroh` | iroh | QUIC (iroh, relay-assisted) | `alknet/channels`, `alknet/call` — p2p peers, NAT'd nodes |
| `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
path. The hub's `HttpAdapter` serves `h2`/`http/1.1` over the TCP+TLS
path for registration and browser access; the `ChannelsAdapter`
(registered on the hub's `HandlerRegistry`) serves `alknet/channels`
over the QUIC path (and over TCP+TLS when a worker dials
channels-over-TCP). Both ALPNs are registered on the same registry;
the TLS handshake on each connection negotiates the ALPN and dispatches
to the right adapter.
path. The hub's `HttpAdapter` serves `h2`/`http/1.1` over the web
endpoint's TCP+TLS path for registration and browser access; the
`ChannelsAdapter` (registered on the hub's `HandlerRegistry`) serves
`alknet/channels` over the native endpoint's QUIC path (and over
TCP+TLS when a worker dials channels-over-TCP). Both ALPNs are
registered on the same registry; the TLS handshake on each connection
negotiates the ALPN and dispatches to the right adapter.
The TCP+TLS accept loop constructs a `Connection` per accepted
`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
The registration flow is what makes TCP+TLS a hard requirement for
the hub. 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:
The registration flow is an **entry point** (ADR-086 §2) — a connection
accepted without an established peer identity, authenticated per-request
by the registration token. This is why the registration endpoint is an
HTTP route on `h2`/`http/1.1` (an entry-point ALPN), not a
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 —
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 |
| 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 |
| 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
@@ -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,
`PeerEntry` created, session credential returned); the exact token
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
supervision loop needs a way to await connection close. The
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-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-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) —
the first hub consumer, the concrete use case that informed this
crate
+42 -13
View File
@@ -72,13 +72,30 @@ separate crate is the right shape (dependency isolation, ACME weight,
quinn/iroh having their own TLS), is in
[ADR-082](../../decisions/082-alknet-tls-extraction.md).
### The three use cases
### The three endpoint types (ADR-086)
| Use case | Identity | Transports | Browsers? |
|----------|----------|-----------|-----------|
| P2P / native clients | RFC 7250 raw key (Ed25519) | QUIC + TCP (fallback when UDP blocked) | No (browsers can't do raw keys) |
| Domain-hosted / public service | X.509 (manual or ACME) | QUIC + TCP+TLS (same cert) | Yes (via HTTPS / WebSocket; WebTransport when revived) |
| Development | Self-signed | Any | No (untrusted) |
A hub composes a subset of three endpoint types, each with its own
identity model and transport(s). `alknet-tls` provides the
`TlsServerConfig`s; the assembly layer builds one per endpoint type
that uses a `rustls::ServerConfig` (iroh is the exception — it has its
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
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
(`alknet/call`, `alknet/channels`, `h2`, `http/1.1`, etc.). For ACME, the
`acme-tls/1` ALPN is appended automatically (for the TLS-ALPN-01
challenge, ADR-027 §7).
The ALPN list is the set of ALPNs the endpoint type advertises. For a
native config: `alknet/channels`, `alknet/call`, `alknet/ssh` (future).
For a web config: `h2`, `http/1.1`, `alknet/channels` (for
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
@@ -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 |
| [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 |
| [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
@@ -452,9 +474,13 @@ See [open-questions.md](../../open-questions.md) for full details.
- **OQ-61** (dissolved): Multi-owner shutdown coordination. The
problem does not arise — the endpoint owns all its accept loops
(quinn, iroh, TCP+TLS); `shutdown()` stops them all. See ADR-083.
- **OQ-62** (open): Does a hub pass the same ALPN list to both
`TlsServerConfig`s, or different (transport-appropriate) lists?
Decision-needed before the hub's assembly code is written.
- **OQ-62** (resolved): Does a hub pass the same ALPN list to both
`TlsServerConfig`s? **Split list, by endpoint type** (ADR-086 §3).
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
public signatures but not sketched. Needs a variant-granularity
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`
Amendment 2 — TCP+TLS is a first-class owned transport
(`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
(TLS section will be amended to point to `alknet-tls`)
- `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)
+3 -1
View File
@@ -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-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-65](questions/065-websocket-carrying-channels.md) | Should WebSocket Carry the Channels Protocol (Not Just the Call Protocol)? | open | one | med |
### 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-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-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
+77 -24
View File
@@ -10,15 +10,15 @@ last_updated: 2026-07-15
Alknet is a **core networking toolkit** for building self-hostable,
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
hub/HTTP path. A single endpoint accepts connections on one port, and
the ALPN string negotiated during the TLS handshake routes each
connection to the correct protocol handler. Every service — call,
channels, HTTP, TTY, tunnels, SFTP — is an ALPN on a shared endpoint
or a data-channel ALPN inside channels.
web/browser path. A single endpoint accepts connections on one port
per transport, and the ALPN string negotiated during the TLS handshake
routes each connection to the correct protocol handler. Every service —
call, channels, HTTP, TTY, tunnels, SFTP — is an ALPN on a shared
endpoint or a data-channel ALPN inside channels.
This is the core insight: **a service IS an ALPN.** One endpoint, one
port, many protocols — dispatched by the TLS handshake, not by
application-level peeking or separate listeners.
port per transport, many protocols — dispatched by the TLS handshake,
not by application-level peeking or separate listeners.
### 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)
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
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)
│ ├── alknet-tty alknet/tty — specced (ADR-052–057), implemented
│ ├── alknet-tty-local PTY/pipe backend — sibling crate (ADR-054)
│ ├── alknet-http h2/http1.1 + WebSocket — the HTTP edge case (registration, browser, MCP)
│ ├── alknet-ssh russh server channels wrapper — for git/sftp compat [not yet specced]
│ ├── alknet-tunnel alknet/tunnel — POC-validated, minimal spec needed [not yet specced]
│ ├── alknet-socks5 SOCKS5 proxy over channels [not yet specced]
│ ├── alknet-fs filesystem access over channels [not yet specced]
│ └── alknet-sftp SFTP over channels [not yet specced]
│ ├── alknet-http h2/http1.1 + WebSocket — the web endpoint edge case (registration, browser, MCP)
│ ├── alknet-ssh russh server — endpoint ALPN wrapping channels (channels-over-SSH); RFC 7250 keys; legacy compat [not yet specced]
│ ├── alknet-tunnel alknet/tunnel — channels data-channel ALPN; POC-validated, minimal spec needed [not yet specced]
│ ├── alknet-socks5 SOCKS5 proxy — channels data-channel ALPN [not yet specced]
│ ├── alknet-fs filesystem access — channels data-channel ALPN [not yet specced]
│ └── alknet-sftp SFTP — channels data-channel ALPN [not yet specced]
│
└── Consumer repos (separate repos, depend on the published core crates)
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,
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
| ALPN | Handler | Description |
|------|---------|-------------|
| `alknet/call` | `CallAdapter` | Call protocol: operations, streaming, pub/sub (hand-rolled EventEnvelope — ADR-064) |
| `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) |
#### Entry points (no identity required at the TLS layer)
| ALPN | Handler | Endpoint type | Description |
|------|---------|---------------|-------------|
| `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
These ride inside a `alknet/channels` connection as data channels,
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 |
|------|---------|--------|
@@ -168,6 +205,19 @@ bidirectionality of channels + call for free.
| `alknet/sftp` | (sftp handler) | not yet specced |
| (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
> **`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
> 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
> (`h2`/`http/1.1`), not a channels data-channel ALPN — it wraps the
> call protocol for browser/curl access (registration, MCP/OpenAPI
> adapters, WebSocket bidirectional path). See
> [crates/http/README.md](crates/http/README.md).
> **`alknet/http`** is the web endpoint edge case. It is an
> entry-point ALPN (`h2`/`http/1.1`), not a channels data-channel ALPN
> — it wraps the call protocol for browser/curl access (registration,
> MCP/OpenAPI adapters, WebSocket bidirectional path). It is advertised
> 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
> 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 |
| [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) |
| [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
@@ -7,7 +7,7 @@
lists); `docs/architecture/crates/core/endpoint.md` (the ALPN section
previously stated "both connection sources advertise the same set of
ALPNs," which is stale under the two-config hub model).
- **Status**: open
- **Status**: resolved
- **Door type**: one-way (the ALPN list each `TlsServerConfig` advertises
is baked into the `rustls::ServerConfig` at construction; changing it
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
wiring sets the pattern, and an implementer cannot write the hub's
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
`TlsServerConfig`s receive `registry.alpn_strings()` verbatim. The
raw-key QUIC listener advertises `h2`/`http/1.1` (browsers can't
connect to a raw-key listener anyway, so the advertisement is
harmless dead negotiation). The X.509 TCP+TLS listener advertises
`alknet/call` (a native client connecting over TCP+TLS with an X.509
client cert can use it). Simplest wiring; no split logic; every
transport can serve every ALPN.
- **raw-key config (native endpoint)**: `alknet/channels`,
`alknet/call`, `alknet/ssh` (future) — the native ALPNs. No
`h2`/`http/1.1` (browsers cannot connect to a raw-key listener;
native clients do not negotiate HTTP ALPNs).
- **X.509/ACME config (web endpoint)**: `h2`, `http/1.1`,
`alknet/channels` (for WebSocket-carrying-channels, per OQ-65),
`acme-tls/1` (appended automatically). No `alknet/call` as a
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
gets the native ALPNs (`alknet/call`, `alknet/channels`,
`alknet/tty`); the X.509/ACME config gets the union including
`h2`/`http/1.1` (browser ALPNs that only make sense over TCP+TLS with
a domain cert). The assembly layer filters `registry.alpn_strings()`
by which transports can serve each ALPN. More logic; cleaner
advertisement (no browser ALPNs on a raw-key listener).
The rationale: each endpoint type serves a different client class.
Advertising ALPNs the client class cannot negotiate (e.g., `h2` on a
raw-key listener) is harmless but misleading — it implies the
listener serves a client class it cannot. The split makes the
advertisement honest and the assembly-layer wiring pattern guessable.
The question is whether the "harmless dead negotiation" in Option A
is acceptable or whether the cleaner advertisement in Option B is
worth the split logic. This needs a decision before the hub's
assembly code is written — it is not guessable from the existing
specs, and guessing produces a wiring shape that downstream consumers
copy.
The naming that makes this answerable is the **entry-point vs.
endpoint** distinction (ADR-086 §2): entry-point ALPNs (`h2`,
`http/1.1`, future `alknet/register`) are accepted without
established peer identity (per-request auth); endpoint ALPNs
(`alknet/channels`, `alknet/call`, `alknet/ssh`) require identity
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
from `iroh::Endpoint::builder().alpns()` at construction, set by the
assembly layer from `registry.alpn_strings()`. Iroh uses raw keys
only, so it gets the native ALPN set regardless of which option is
chosen for the quinn/TCP+TLS pair.
- **Cross-references**: ADR-082 (`TlsServerConfig::new` takes
`alpns: &[Vec<u8>]` — the caller decides), ADR-083 (the assembly
layer builds transports; ALPN-list construction is its job),
OQ-64 (client-side TLS helper — related but orthogonal; this OQ is
server-side advertisement)
See [ADR-086](../decisions/086-endpoint-types-and-entry-points.md)
for the full decision, including the three-endpoint-type model, the
hub-shape table (full / web+native / native+iroh / minimal), and the
foundational-handler categorization (channels data-channel ALPNs vs.
SSH as an endpoint ALPN that wraps channels).
- **Cross-references**: [ADR-086](../decisions/086-endpoint-types-and-entry-points.md)
(the decision), ADR-082 (`TlsServerConfig::new` takes
`alpns: &[Vec<u8>]` — the caller decides; this OQ specified how),
ADR-083 (the assembly layer builds transports; ALPN-list construction
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)