diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 72ecddc..1d8cd46 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,12 +1,27 @@ --- status: draft -last_updated: 2026-07-14 +last_updated: 2026-07-15 --- # Alknet Architecture ## Current State +**Workspace scope corrected (ADR-085, 2026-07-15).** The overview's +crate graph had been describing the wrong scope since ADR-003 — a flat +~12-crate workspace including DNS, messaging, and NAPI, while omitting +channels, hub, worker, and tls. [ADR-085](decisions/085-workspace-scope-core-vs-consumer-repos.md) +records the actual scope: the mono-repo is the **core networking +toolkit** (substrate: core, tls, call, channels; deployment shapes: hub, +worker; foundational handlers: tty, http, ssh, tunnel, socks5, fs, sftp; +vault). Crates that build on top of a hub or worker (docker, agent) are +**consumer repos** — separate repos depending on the published core +crates. This corrects the root cause of the "assembly layer" hedging +pattern: the overview now reflects the real boundary, so the +"assembly layer" has a bounded home (hub/worker), not an escape hatch. +The [overview.md](overview.md) crate graph and ALPN registry are +rewritten to match. + **alknet-channels specs drafted.** The alknet-channels crate (multiplexing proxy — `ProtocolHandler` on `alknet/channels`, 9-byte chunk format, N channels over transport stream(s), channel 0 pre-negotiated as @@ -138,7 +153,7 @@ adapter location map is now consistent: all HTTP-backed adapters | Document | Status | Description | |----------|--------|-------------| -| [overview.md](overview.md) | draft | Workspace-level overview, crate graph, shared types, design principles | +| [overview.md](overview.md) | draft | Workspace-level overview, crate graph (core mono-repo scope per ADR-085), hub/worker model, shared types, design principles | | [open-questions.md](open-questions.md) | draft | OQ index — theme-grouped tables + Deferred/Blocked section; per-OQ files in [`questions/`](questions/) | | [crates/core/README.md](crates/core/README.md) | draft | alknet-core crate index | | [crates/core/core-types.md](crates/core/core-types.md) | draft | ProtocolHandler, HandlerError, Connection (`Box` — ADR-070), BidiStreamSource trait, BiStream, StreamError | @@ -269,10 +284,11 @@ adapter location map is now consistent: all HTTP-backed adapters | [082](decisions/082-alknet-tls-extraction.md) | alknet-tls Crate Extraction | Proposed (amended — endpoint signature superseded by ADR-083) | | [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 | ## Open Questions -Open questions are tracked in [open-questions.md](open-questions.md) — an index of theme-grouped tables (55 OQs across 17 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 (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). ## Document Lifecycle diff --git a/docs/architecture/crates/core/endpoint.md b/docs/architecture/crates/core/endpoint.md index 42cb66a..4c931e2 100644 --- a/docs/architecture/crates/core/endpoint.md +++ b/docs/architecture/crates/core/endpoint.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-07-14 +last_updated: 2026-07-15 --- # Endpoint @@ -46,6 +46,14 @@ A node can be reachable through different paths depending on its network context These are not interchangeable transports — they are **complementary connectivity modes**. A node behind NAT that also has a public IP can use both simultaneously. Both produce QUIC connections that dispatch through the same `HandlerRegistry` by ALPN string. +> **Terminology — hub, worker, hub-worker.** A *hub* accepts inbound +> connections from workers and browsers (see +> [`crates/hub/README.md`](../hub/README.md) for the hub-and-spoke +> topology). A *worker* dials out to a hub. A *hub-worker* does both. A +> *pure worker* has no inbound endpoints. These terms come from ADR-029 +> / ADR-034; "assembly layer" (ADR-014) is the deployment binary that +> wires crates — in practice, today, usually a hub or hub-worker. + ### TCP+TLS is a first-class owned transport TCP+TLS is a listener transport, same shape as quinn and iroh. The @@ -94,7 +102,19 @@ Registration is static at startup (see [OQ-04](../../open-questions.md)). The CL ### ALPN strings in TLS ServerConfig and iroh endpoint -The quinn endpoint's `rustls::ServerConfig` ALPN list is set from `registry.alpn_strings()` at construction time. The iroh endpoint's ALPN list is similarly derived. Both connection sources advertise the same set of ALPNs. +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. + +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. ## Accept Loops @@ -301,11 +321,18 @@ Fatal errors that prevent the endpoint from starting or continuing. ```rust pub enum EndpointError { BindFailed(io::Error), - TlsConfig(io::Error), HandlerNotFound(Vec), // ALPN string with no registered handler } ``` +After ADR-083, the endpoint takes no TLS config and constructs no +transports — TLS config errors now surface as `TlsError` in +`alknet-tls` / the assembly layer (see +[`crates/tls/README.md`](../tls/README.md) and OQ-62), not as +`EndpointError`. The `TlsConfig(io::Error)` variant that existed when +the endpoint built TLS internally is removed. `BindFailed` covers +listener bind failures (quinn, iroh, TCP+TLS `TcpListener::bind`). + ### HandlerError Non-fatal errors within a handler. See [core-types.md](core-types.md) for details. diff --git a/docs/architecture/crates/hub/README.md b/docs/architecture/crates/hub/README.md index a82da41..70d3bb8 100644 --- a/docs/architecture/crates/hub/README.md +++ b/docs/architecture/crates/hub/README.md @@ -564,7 +564,7 @@ fixed until the enrollment-token model is decided. ## Crate dependencies ``` -alknet-hub +alknet-hub (Hub struct deps) ├── alknet-channels-call (ChannelClient, ChannelsAdapter, ChannelManager, │ ChannelBidiStreamSource) ├── alknet-call (CallAdapter, Dispatcher, PeerCompositeEnv, @@ -574,6 +574,12 @@ alknet-hub │ HandlerRegistry, AuthContext) ├── tokio (spawn, time::sleep) └── tracing (logging) + + (assembly-layer deps — used by the hub's composition code, not by + the Hub struct itself) + ├── alknet-tls (TlsServerConfig — builds the raw-key + X.509/ACME + │ configs handed to the endpoint's transports) + └── alknet-core [tcp feature] (with_tcp_tls — the TCP+TLS accept loop) ``` `alknet-hub` depends on `alknet-channels-call`, `alknet-call`, and @@ -584,6 +590,15 @@ registration endpoint and browser HTTP access — the hub wires `HttpAdapter` into the same `HandlerRegistry` as `ChannelsAdapter`. +`alknet-tls` is an **assembly-layer** dependency, not a `Hub` struct +dependency: the `Hub` struct holds no `TlsServerConfig`, but the hub's +composition code (the assembly-layer wiring shown in "Assembly layer +integration" below) builds the `TlsServerConfig`s and hands them to the +endpoint's transports via `for_quinn()` / `for_tcp_tls()`. This +distinction matters: a consumer that uses `Hub` with externally-built +transports does not pull `alknet-tls` through the `Hub` type, only +through the assembly wiring. + ## Assembly layer integration A downstream hub (alkapi) uses `alknet-hub` like this: diff --git a/docs/architecture/crates/tls/README.md b/docs/architecture/crates/tls/README.md index 3a04116..7823d0f 100644 --- a/docs/architecture/crates/tls/README.md +++ b/docs/architecture/crates/tls/README.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-07-14 +last_updated: 2026-07-15 --- # alknet-tls @@ -150,6 +150,28 @@ The ALPN list is the set of ALPNs the deployment wants to advertise `acme-tls/1` ALPN is appended automatically (for the TLS-ALPN-01 challenge, ADR-027 §7). +### `async fn new` — lifecycle semantics + +`new` is `async` because the ACME path spawns a state-machine task +(`tokio::spawn`) before returning — the spawn itself is await-free, but +the function is async so the non-ACME paths share one signature. + +**ACME path**: `new` spawns the `AcmeState` task, wires its `resolver()` +into the `rustls::ServerConfig`, and returns **immediately** — it does +**not** await the first certificate. The returned `TlsServerConfig` is +usable for `for_quinn()` / `for_tcp_tls()` right away; the resolver may +return no cert until the first ACME order completes, causing TLS +handshakes to fail transiently during that window. This matches the +current code's behavior (`TlsSetup::new_acme` spawns and returns). + +**Non-ACME paths** (X509 / RawKey / SelfSigned): cert loading is +synchronous file I/O (`std::fs::read`) + in-memory construction; there +is no await point in the implementation. The `async` signature is for +API uniformity with the ACME path, not because the work is async. An +implementer who finds this objectionable may split a non-async +constructor — that is a two-way-door implementation detail, not an +architecture decision. + ### Behavior-preservation invariants The extraction must preserve these load-bearing TLS behaviors. They @@ -163,9 +185,10 @@ changes TLS behavior: RawKey, SelfSigned, ACME). Enables 0-RTT / early data. Omitting it disables 0-RTT, silently breaking clients that use it. - **`rustls::crypto::aws_lc_rs::default_provider()`** as the crypto - provider on all paths. Matches iroh's `tls-aws-lc-rs` feature. Do not - switch to `ring` or the process-default provider without an ADR — - different FIPS status, different platform support. + provider on all paths. Do not switch to `ring` or the process-default + provider without a new ADR — see + [ADR-084](../../decisions/084-aws-lc-rs-crypto-provider.md) for the + rationale (FIPS, platform matrix, iroh consistency). - **`AcceptAnyCertVerifier`'s `supported_verify_schemes()`** returns ED25519 + ECDSA P-256/P-384 + RSA PSS/PKCS1 (SHA256/384/512). This list determines which client cert signature algorithms the server @@ -264,6 +287,16 @@ endpoint struct and accept loops remain in core), `ed25519-dalek` production and `rustls::sign` in the test helper `build_ed25519_spki_der` — see OQ-59). +> **Terminology — hub, worker, hub-worker.** A *hub* is a node that +> accepts inbound connections from workers and browsers (the central +> node in a hub-and-spoke topology — see +> [`crates/hub/README.md`](../hub/README.md)). A *worker* is a node +> that dials out to a hub. A *hub-worker* is a node that does both +> (accepts inbound and dials out). A *pure worker* has no inbound +> endpoints. These terms come from the hub topology (ADR-029, ADR-034); +> "assembly layer" (ADR-014) is the deployment binary that wires crates +> — in practice, today, usually a hub or hub-worker. + ### What `AlknetEndpoint` does after the refactor `AlknetEndpoint::new()` currently builds `TlsSetup` internally. After @@ -334,6 +367,34 @@ behind a `tcp` feature, as an owned transport on `AlknetEndpoint` (via cert provider, not the accept loop. This keeps `alknet-tls` focused on TLS setup and cert sharing, not transport accept logic. +### Server-only (for now) — client-side config is a separate question + +`alknet-tls` as specified here is **server-side only**: +`TlsServerConfig` and its accessors (`for_quinn`, `for_tcp_tls`, +`rustls_config`) produce `rustls::ServerConfig`-shaped outputs for +inbound listeners. There is no `TlsClientConfig`, no `for_client()`, +no client-side cert/verifier helper in this crate. + +The client side — the `rustls::ClientConfig` construction used by +`CallClient` / `ChannelClient` for outbound dials, including +[ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md)'s +verifier-selection rule (fingerprint pin for known peers, CA-verify for +unknown X.509, fail-closed for unknown raw-key) — currently lives in +`alknet-call`'s `FingerprintPinVerifier`. ADR-084 requires the client +side to use the same `aws_lc_rs` provider; today that is enforced by +**convention** (two crates independently constructing +`aws_lc_rs::default_provider()`), not by shared code. + +Whether `alknet-tls` should grow a client-side helper (a +`TlsClientConfig` that centralizes verifier selection + provider +consistency, so `alknet-call` / a future `AlknetClient` don't each +rebuild it) is **deferred** — see OQ-64. The deferral blocks on the +`AlknetClient` dial-seam extraction (OQ-55): the client-side TLS helper +and the shared dial are the same seam, and extracting either without a +second transport's real client dial would bake QUIC in as the client +shape — the same welding ADR-065 unwound on the server side. The +provider-consistency convention (ADR-084) holds until then. + ## Crate dependencies (in the dep graph) ``` @@ -391,6 +452,18 @@ 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-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. +- **OQ-64** (deferred): Client-side TLS config helper. `alknet-tls` is + server-side only as specified; the client-side `ClientConfig` + + verifier selection lives in `alknet-call`. Blocked on the + `AlknetClient` dial-seam extraction (OQ-55) — extracting the client + helper without a second transport's dial would bake QUIC in as the + client shape. ## References diff --git a/docs/architecture/decisions/084-aws-lc-rs-crypto-provider.md b/docs/architecture/decisions/084-aws-lc-rs-crypto-provider.md index 522c02b..78158d2 100644 --- a/docs/architecture/decisions/084-aws-lc-rs-crypto-provider.md +++ b/docs/architecture/decisions/084-aws-lc-rs-crypto-provider.md @@ -2,7 +2,11 @@ ## Status -Accepted +Accepted (depends on ADR-082/083, which are still Proposed — this ADR +records a decision those ADRs list as an invariant. If ADR-082/083 are +revised in a way that changes the config-construction paths, the +provider decision here still stands; only the "where it is applied" +referent would update.) ## Context diff --git a/docs/architecture/decisions/085-workspace-scope-core-vs-consumer-repos.md b/docs/architecture/decisions/085-workspace-scope-core-vs-consumer-repos.md new file mode 100644 index 0000000..32588ab --- /dev/null +++ b/docs/architecture/decisions/085-workspace-scope-core-vs-consumer-repos.md @@ -0,0 +1,263 @@ +# ADR-085: Workspace Scope — Core vs. Consumer Repos + +## Status + +Accepted + +## Context + +The original crate decomposition (ADR-003, 2026-06) listed a flat +workspace of ~12 crates: `alknet-core`, `alknet-vault`, `alknet-ssh`, +`alknet-call`, `alknet-agent`, `alknet-git`, `alknet-sftp`, `alknet-msg`, +`alknet-http`, `alknet-dns`, `alknet-napi`, and a CLI binary. This was +written before two things were clear: + +1. **Channels** (ADR-071) — a multiplexing substrate that carries + `alknet/call` on channel 0 and any ALPN on data channels. Channels + did not exist when ADR-003 was written; it was created to solve a + pain point that surfaced writing the first consumers. Channels + changed the shape: a "hub" is a channels hub, a "worker" is a + channels worker, and most protocol handlers (TTY, tunnels, SFTP, + SOCKS5, git, SSH) ride inside channels as data-channel ALPNs, not as + top-level endpoint ALPNs. + +2. **The hub/worker topology** (ADR-029, ADR-034, ADR-079) — the + deployment shape where a central hub accepts worker connections, + relays channels between legs, and aggregates operations. This is + the "vpn-like without being a vpn" target. A hub is one who + initiates an `AlknetEndpoint` serving many transports; a worker + dials out to a hub. A hub-worker does both. + +Because ADR-003's flat list was never revised, the overview's crate +graph has been describing the wrong scope the entire time. It lists +crates that should be consumers in separate repos (`alknet-agent`, +`alknet-docker`) and omits crates that are core to the actual target +(`alknet-tls`, `alknet-channels`, `alknet-hub`, `alknet-worker`, +`alknet-tty`). It also lists crates that were never specced and are not +part of the current scope (`alknet-dns`, `alknet-msg`, `alknet-napi`). + +This stale scope is a causal factor in the "assembly layer" hedging +pattern. When the overview implies everything lives in one repo, but +the actual architecture requires a hub/worker composition layer that +isn't in the graph, agents fill the gap with "assembly layer" as an +escape hatch — a path-of-least-resistance solution to an impossible +bind. The fix is not to suppress the hedging; it's to make the scope +clear so the bind doesn't arise. + +### What "core" means now + +The alknet mono-repo is the **core networking toolkit** — the crates +that a hub, a worker, or a hub-worker are built from, plus the protocol +handlers that are foundational to the "p2p-capable vpn-like without +being a vpn" target. Crates that build *on top of* a hub or worker +(docker operations, agent, future applications) are consumers in +their own repos — they depend on `alknet-call` / `alknet-channels` / +`alknet-hub` / `alknet-worker`, not on `alknet-core` directly. + +The distinction is: +- **Core mono-repo**: the substrate (core, tls, call, channels), the + deployment shapes (hub, worker), and the foundational protocol + handlers that every hub/worker needs (tty, tunnels, fs, sftp, ssh, + http). Vault is core because it's foundational to ACL (key derivation, + identity). +- **Consumer repos**: crates that build on top of a hub or worker + (docker operations, agent, future applications). These are + independent repos that depend on the published core crates. + +### The foundational handlers + +The protocol handlers that are foundational to the vpn-like target +ride inside channels as data-channel ALPNs: + +| ALPN (inside channels) | Crate | Status | +|------------------------|-------|--------| +| `alknet/tty` | `alknet-tty` | specced (ADR-052–057), implemented | +| `alknet/tunnel` | (in `alknet-channels` or a sibling) | POC-validated, not yet specced — minimal (the channels POC covers this use case) | +| `alknet/socks5` | (TBD) | not yet discussed — SOCKS5 proxy over channels | +| `alknet/fs` | (TBD) | not yet specced — filesystem access over channels | +| `alknet/sftp` | (TBD) | not yet specced — SFTP protocol core over channels | + +Additionally, `alknet-http` (the HTTP edge case — registration endpoint, +browser access, MCP/OpenAPI adapters) and a future `alknet-ssh` (russh +server channels wrapper as an option for hubs, for git/sftp +compatibility) are core mono-repo concerns because they are part of the +hub's inbound surface. + +### What leaves the mono-repo + +| Crate | Destination | Why it's a consumer, not core | +|-------|-------------|-------------------------------| +| `alknet-docker` | own repo | Docker operations build on top of a hub/worker — a docker host is a worker, not a substrate concern. Depends on `alknet-call` + `alknet-tty`, not on core transport. | +| `alknet-agent` | own repo | The agent builds on `alknet-call` for tool dispatch — it's an application, not networking substrate. | + +These specs are kept in `docs/architecture/crates/docker/` for +reference (the work is not lost), but the crates move to their own repos +as consumers. The overview's crate graph no longer lists them as +mono-repo members. + +### What was never in scope (and is removed from the graph) + +`alknet-dns`, `alknet-msg`, `alknet-napi` were listed in ADR-003's flat +decomposition but were never specced, never implemented, and are not +part of the current target. They are removed from the overview's crate +graph. If a DNS or messaging handler becomes needed, it will be a +consumer repo (a handler that rides inside channels or registers on the +endpoint), not a mono-repo member. NAPI projection, if needed, lives +with whatever consumer needs the Node.js bridge. + +## Decision + +### The alknet mono-repo scope is the core networking toolkit + +The mono-repo contains the substrate, the deployment shapes, and the +foundational protocol handlers. Everything else is a consumer in its +own repo. + +``` +alknet mono-repo (the core networking toolkit) +│ +├── Substrate +│ ├── alknet-core (ProtocolHandler, endpoint, auth, config, Connection) +│ ├── alknet-tls (shared TLS config — ADR-082) +│ ├── alknet-call (call protocol on alknet/call) +│ └── alknet-channels (multiplexing substrate on alknet/channels — ADR-071) +│ ├── alknet-channels-core (pure multiplexer — ADR-081) +│ └── alknet-channels-call (channel 0 pre-negotiation — ADR-081) +│ +├── Deployment shapes +│ ├── alknet-hub (channels hub — accepts workers, relays, aggregates) +│ └── alknet-worker (channels worker — dials out to a hub) +│ +├── Foundational handlers (inside channels as data-channel ALPNs, or on the endpoint) +│ ├── alknet-tty (alknet/tty — specced, implemented) +│ ├── alknet-http (h2/http1.1 + WebSocket — the HTTP edge case) +│ ├── alknet-tty-local (PTY/pipe backend — sibling crate) +│ ├── 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-vault (standalone — foundational to ACL: key derivation, identity) +``` + +### Dependency rules + +- The substrate crates (`core`, `tls`, `call`, `channels`) depend on + each other in a clean DAG: `channels` → `call` → `core`; `tls` → + `core`. No cycles. +- `alknet-hub` and `alknet-worker` depend on the substrate (channels, + call, core) and on the handlers they wire. They are consumers of the + substrate, not part of it. +- Foundational handlers depend on `alknet-core` (for + `ProtocolHandler`, `Connection`) and/or `alknet-channels` (for + `ChannelBidiStreamSource`, `into_sub_streams`). No handler depends + on another handler — cross-handler communication goes through + `alknet/call` on channel 0. +- `alknet-vault` is standalone (zero alknet crate dependencies — ADR-018). + It is foundational to ACL: the hub/worker identity model + (`IdentityProvider`, `PeerEntry`, fingerprint resolution) derives + from vault-managed keys. Vault is accessed only at the assembly layer + (ADR-019); handlers receive derived credentials via capabilities + (ADR-014), never a vault reference. +- Consumer repos (docker, agent, future applications) depend on the + published core crates (`alknet-call`, `alknet-channels`, + `alknet-hub`, `alknet-worker`), not on `alknet-core` directly. + +### The hub/worker model + +A **hub** is a channels hub — it accepts inbound connections (over +quinn, iroh, TCP+TLS — ADR-083), runs `ChannelsAdapter` on +`alknet/channels`, relays data channels between legs (ADR-079), +aggregates workers' operations into a shared env, and serves the +discovery API. A hub may also serve HTTP (`h2`/`http/1.1` for +registration and browser access) and, optionally, an SSH server (russh +channels wrapper for git/sftp compatibility). + +A **worker** is a channels worker — it dials out to a hub via +`ChannelClient`, runs `from_call` to discover the hub's (and other +workers') operations, and exposes its own operations on channel 0. A +worker has no inbound endpoints unless it is also a hub (hub-worker). + +A **hub-worker** does both — accepts inbound and dials out. This is a +valid deployment shape; the topology is not strictly hierarchical. + +The bidirectionality of call and channels means both sides can be both +hub and worker within a connection. A hub (A) that uses a client to +connect to another hub (B) is, from B's perspective, a worker. This +does not require a separate "hub-as-client" abstraction — the +`ChannelClient` / `CallClient` take-over APIs (`from_connection`, +`spawn_dispatch`) are transport-agnostic and work regardless of +whether the dialer is a hub, a worker, or a hub-worker. + +### ADR-003 is amended + +ADR-003's flat crate table is superseded for scoping purposes. The +"one crate per protocol handler, core provides shared infra" principle +survives; the specific crate list does not. The crate list is now this +ADR's scope table. ADR-003's amendments (the `alknet-call` as +protocol-foundation clarification, the `alknet-tty` no-`alknet-call` +clarification, the irpc removal) survive — they are about dependency +edges, not about which crates are in the mono-repo. + +## Consequences + +**Positive:** +- The overview's crate graph will match reality for the first time. + Future sessions start with an accurate scope, not a stale flat list + that implies the wrong boundary. +- The "assembly layer" escape hatch has a bounded home: the hub and + worker crates. "Assembly layer" = the deployment binary (hub, worker, + or hub-worker), not a dump for unknowns. This is the fix for the + hedging pattern's root cause. +- Consumer repos (docker, agent) are unblocked — they can be developed + independently against the published core crates, without waiting for + the mono-repo to accommodate their concerns. +- The scope is narrow enough to finish. Six substrate + deployment + crates (core, tls, call, channels, hub, worker) plus the foundational + handlers (tty, http, ssh, tunnel, socks5, fs, sftp) plus vault is a + bounded surface. The previous scope (~12 flat crates including DNS, + messaging, NAPI) was never the target. +- The foundational handlers that ride inside channels (tunnel, socks5, + fs, sftp) are correctly scoped as channels data-channel ALPNs, not + top-level endpoint handlers. This is the "p2p-capable vpn-like" + shape — these services are available over channels, with the ACL and + bidirectionality that channels + call provide. + +**Negative:** +- `alknet-docker` and `alknet-agent` leave the mono-repo. Their specs + stay in `docs/architecture/crates/docker/` (and a future + `crates/agent/` if specced) for reference, but the crate code moves to + consumer repos. This is a repository boundary change, not a loss of + work — the specs and ADRs (058–063 for docker) remain valid as the + consumer's architecture. +- Several foundational handlers (ssh, tunnel, socks5, fs, sftp) are + named in the scope but not yet specced. The scope table makes this + visible — it is a backlog, not a hidden gap. +- `alknet-worker` has no spec yet. The worker pattern is described + inside the hub README (as the inverse of hub), but a dedicated + `crates/worker/README.md` or a combined hub/worker doc is needed. + +## Door type + +**One-way.** The repo boundary (core mono-repo vs. consumer repos) is +structural — once docker and agent are in their own repos with their +own release cycles, reversing means merging them back and breaking +downstream consumers that depend on the published crates. The scope +table (which crates are core) is one-way for the same reason: the +dep graph and the overview orient around it. + +## References + +- ADR-003: Crate Decomposition (amended — the flat crate list is + superseded; the decomposition principle survives) +- ADR-029: Peer-Graph Routing Model (the hub/worker topology) +- ADR-034: Three Peer Roles (hub = role-3, worker = role-1/2) +- ADR-071: alknet-channels Wire Format (the multiplexing substrate) +- ADR-079: Hub Relay (translate, not transparently forward) +- ADR-080: ChannelClient (the worker's dial path) +- ADR-081: channels sub-crate decomposition (channels-core / channels-call) +- ADR-082: alknet-tls extraction +- ADR-083: Endpoint as multi-transport accept-loop runner +- `docs/architecture/crates/hub/README.md` (the hub pattern — the + deployment shape this scope is built around) \ No newline at end of file diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 697fe17..c464e58 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-07-14 +last_updated: 2026-07-15 --- # Open Questions @@ -178,6 +178,14 @@ Door type is separate from whether a decision is made. A two-way door is a decis | [OQ-56](questions/056-full-channel-level-flow-control-windowing.md) | Full Channel-Level Flow-Control Windowing | deferred(scope) | two | low | | [OQ-57](questions/057-two-pump-helper-extraction.md) | Two-Pump Helper Extraction to alknet-core | deferred(scope) | two | low | +### alknet-tls + +| 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-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 | + ## Deferred / Blocked The safe-exit visibility surface. These questions are parked because the @@ -282,3 +290,19 @@ filtering the tables above. - **Priority**: low - **Full file**: [OQ-57](questions/057-two-pump-helper-extraction.md) +### OQ-64: Should `alknet-tls` Provide a Client-Side TLS Config Helper? + +- **Blocked on**: the `AlknetClient` dial-seam extraction (OQ-55). The + client-side TLS helper and the shared dial are the same seam — both + answer "how does an outbound connection build its + `rustls::ClientConfig` + select a verifier (ADR-034) + dial." The + blocking condition is the same as OQ-55: a second transport's real + client dial existing (TCP+TLS, SSH raw-TCP, HTTP-wrapped call), so the + transport-polymorphic client+TLS seam is extractable from two + *different* transport implementations, not one QUIC shape. Until then, + `alknet-tls` is server-side only; the client side lives in + `alknet-call`'s `FingerprintPinVerifier`, with provider consistency + (ADR-084) enforced by convention. +- **Priority**: medium +- **Full file**: [OQ-64](questions/064-client-side-tls-helper.md) + diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 97e2e44..15175a2 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -1,15 +1,35 @@ --- status: draft -last_updated: 2026-07-12 +last_updated: 2026-07-15 --- # Alknet Overview ## What Alknet Is -Alknet is a self-hostable networking toolkit built on QUIC+TLS with ALPN-based protocol dispatch. 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 — SSH, SFTP, Git, HTTP, DNS, messaging, RPC — is an ALPN on a shared endpoint. +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. -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. +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. + +### Scope: core mono-repo vs. consumer repos + +The mono-repo is the **core networking toolkit** — the substrate +(core, tls, call, channels), the deployment shapes (hub, worker), the +foundational protocol handlers (tty, http, ssh, tunnel, socks5, fs, +sftp), and vault (foundational to ACL). Crates that build *on top of* +a hub or worker (docker operations, agent, future applications) are +**consumer repos** — they depend on the published core crates, not on +`alknet-core` directly. See [ADR-085](decisions/085-workspace-scope-core-vs-consumer-repos.md) +for the full scope decision. ## Why ALPN Dispatch @@ -22,52 +42,80 @@ The previous architecture used a three-layer model (StreamInterface/MessageInter See [ADR-001](decisions/001-alpn-protocol-dispatch.md) for the full rationale. +## The Hub/Worker Model + +Alknet's deployment shape is hub-and-spoke. A **hub** is a channels +hub — it accepts inbound connections (over quinn, iroh, TCP+TLS), +runs `ChannelsAdapter` on `alknet/channels`, relays data channels +between legs (ADR-079), aggregates workers' operations, and serves +discovery. A **worker** is a channels worker — it dials out to a hub +via `ChannelClient`, discovers operations via `from_call`, and exposes +its own operations on channel 0. A **hub-worker** does both. + +The bidirectionality of call and channels means both sides can be both +hub and worker within a connection. A hub (A) that dials another hub +(B) is, from B's perspective, a worker. This does not require a +separate "hub-as-client" abstraction — `ChannelClient` / +`CallClient` take-over APIs (`from_connection`, `spawn_dispatch`) are +transport-agnostic and work regardless of whether the dialer is a hub, +a worker, or a hub-worker. + +See [ADR-029](decisions/029-peer-graph-routing-model.md), +[ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md), +[ADR-079](decisions/079-hub-relay-translate-not-forward.md), and +[crates/hub/README.md](crates/hub/README.md) for the full topology. + ## Crate Graph +The mono-repo contains the substrate, the deployment shapes, and the +foundational handlers. Consumer repos (docker, agent) are not in this +graph — they depend on the published core crates from their own repos +(ADR-085). + ``` -alknet-vault (standalone, no alknet-core dependency) +alknet-vault (standalone — foundational to ACL: key derivation, identity) │ -alknet-core -│ ├── ProtocolHandler trait -│ ├── ALPN router / endpoint -│ ├── BiStream trait, Connection type -│ ├── AuthContext, IdentityProvider -│ └── StaticConfig, DynamicConfig (ArcSwap) +├── Substrate +│ alknet-core ProtocolHandler, endpoint (multi-transport accept loop), +│ │ Connection, BidiStreamSource, AuthContext, IdentityProvider, +│ │ StaticConfig, DynamicConfig +│ ├── alknet-tls TlsServerConfig — shared TLS config across quinn + TCP+TLS + iroh (ADR-082) +│ ├── alknet-call CallAdapter on alknet/call, CallClient, OperationRegistry, adapters +│ └── alknet-channels +│ ├── alknet-channels-core pure multiplexer (wire format, demux/mux) — ADR-081 +│ └── alknet-channels-call channel 0 pre-negotiation + lifecycle ops — ADR-081 │ -├── alknet-ssh (depends on alknet-core, russh) -├── alknet-call (depends on alknet-core) -│ ├── CallAdapter (server: ProtocolHandler for alknet/call) -│ ├── Call client (send/receive over QUIC) -│ ├── OperationSpec, OperationRegistry, AccessControl -│ └── Adapter traits (from_*, to_*) +├── Deployment shapes +│ ├── alknet-hub channels hub — accepts workers, relays, aggregates (ADR-079) +│ └── alknet-worker channels worker — dials out to a hub [not yet specced] │ -├── alknet-agent (depends on alknet-call) -│ ├── LLM execution loop (forked aisdk, simplified) -│ ├── Tool dispatch via call protocol -│ └── Provider credentials via capabilities (no env vars, no vault on the wire) +├── 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-git (depends on alknet-core, gix) -├── alknet-sftp (depends on alknet-core, russh-sftp) -├── alknet-msg (depends on alknet-core) -├── alknet-http (depends on alknet-core, alknet-call, axum, reqwest, wtransport, rmcp) -├── alknet-dns (depends on alknet-core, hickory-proto) -│ -├── alknet-napi (depends on alknet-call, napi-rs) -│ └── Thin NAPI projection of call protocol client to Node.js -│ -└── alknet (CLI binary, depends on all handler crates + alknet-vault) +└── Consumer repos (separate repos, depend on the published core crates) + alknet-docker docker operations — a docker host is a worker + alknet-agent LLM agent — builds on alknet-call for tool dispatch ``` Dependency rules: -- No handler crate depends on another handler crate -- All handler crates depend on alknet-core -- alknet-vault has zero alknet crate dependencies -- alknet-agent depends on alknet-call (not alknet-core) — it uses the call protocol client for tool dispatch -- alknet-napi depends only on alknet-call — thin NAPI projection, no business logic -- alknet (CLI) is the only crate that depends on all handler crates and alknet-vault -- Rust is the canonical implementation language — TypeScript is a reference/browser adaptation, not a parallel implementation (see ADR-013) +- The substrate crates form a clean DAG: `channels` → `call` → `core`; `tls` → `core`. No cycles. +- `alknet-hub` and `alknet-worker` depend on the substrate (channels, call, core) and on the handlers they wire. They are consumers of the substrate, not part of it. +- No handler crate depends on another handler crate — cross-handler communication goes through `alknet/call` on channel 0. +- `alknet-call` is a protocol-foundation crate (ADR-003 Am. 1): `alknet-http` depends on it for `OperationSpec`/`Handler`/`OperationAdapter` types, not as a peer-handler dep. +- `alknet-vault` has zero alknet crate dependencies (ADR-018). It is foundational to ACL: the hub/worker identity model derives from vault-managed keys. Vault is accessed only at the assembly layer (ADR-019); handlers receive derived credentials via capabilities (ADR-014). +- Consumer repos (docker, agent) depend on the published core crates, not on `alknet-core` directly. +- Rust is the canonical implementation language (ADR-013). -See [ADR-003](decisions/003-crate-decomposition.md) for the full decomposition rationale. +See [ADR-003](decisions/003-crate-decomposition.md) (as amended by +[ADR-085](decisions/085-workspace-scope-core-vs-consumer-repos.md)) for +the decomposition rationale. ## ProtocolHandler Trait @@ -92,21 +140,56 @@ See [ADR-002](decisions/002-protocol-handler-trait.md) and [ADR-007](decisions/0 ## ALPN Registry +ALPNs are split into two layers: **endpoint ALPNs** (negotiated in the +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. + +### Endpoint ALPNs + | ALPN | Handler | Description | |------|---------|-------------| -| `alknet/ssh` | SshAdapter | SSH-2 handshake, channel multiplexing, SOCKS5, port forwarding | -| `alknet/call` | CallAdapter | JSON-RPC via hand-rolled EventEnvelope framing: operations, streaming, pub/sub | -| `alknet/git` | GitAdapter | Git smart protocol over QUIC (gix, pkt-line) | -| `alknet/sftp` | SftpAdapter | SFTP protocol (russh-sftp core) | -| `alknet/msg` | MessageAdapter | E2E encrypted messaging, mixnet | -| `alknet/http` | HttpAdapter | axum REST API, dashboard, MCP endpoint | -| `alknet/dns` | DnsAdapter | DNS over QUIC/TLS, pkrr service discovery | -| `h3` | HttpAdapter (HTTP/3 + WebTransport) | Browser-compatible WebTransport + HTTP/3 (first-class, ADR-038) | -| `h2` / `http/1.1` | HttpAdapter | Standard HTTP for browsers, curl | +| `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) | -> **Note**: `alknet/agent` is not in the ALPN registry. The agent service is a future consumer that builds on top of `alknet-call` (it depends on `alknet-call`, not `alknet-core` directly — see ADR-003). It uses the call protocol for tool dispatch and exposes agent operations (e.g., `/agent/chat`) as call-protocol operations in the `OperationRegistry`, not as a separate ALPN. The agent is a mental model that informed the core architecture (capabilities, scoped env, abort cascade) but is not specced yet — its design will change as it's built out against the implemented core crates. +### Channels data-channel ALPNs -> **Note**: `alknet/vault` is not in the ALPN registry. alknet-vault is a standalone local key vault with no alknet-core dependency and no remote dispatch capability (ADR-025). The CLI binary embeds it and accesses it at the assembly layer — unlocking the vault at startup, deriving and decrypting credentials, and injecting them into handler capabilities. The vault is not exposed over the call protocol. No vault operations are registered in the operation registry. See ADR-008, ADR-014, and ADR-025. +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. + +| ALPN | Handler | Status | +|------|---------|--------| +| `alknet/tty` | `TtyAdapter` | specced (ADR-052–057), implemented | +| `alknet/tunnel` | (tunnel handler) | POC-validated, minimal spec needed [not yet specced] | +| `alknet/socks5` | (SOCKS5 handler) | not yet specced | +| `alknet/fs` | (fs handler) | not yet specced | +| `alknet/sftp` | (sftp handler) | not yet specced | +| (future) | any ALPN a consumer registers | channels supports any ALPN — ADR-071 | + +### Notes + +> **`alknet/vault`** is not in the ALPN registry. alknet-vault is a +> standalone local key vault with no alknet-core dependency and no +> remote dispatch capability (ADR-025). The assembly layer (hub or +> worker binary) embeds it, unlocks it at startup, derives/decrypts +> credentials, and injects them into handler capabilities (ADR-014). +> The vault is foundational to ACL — the hub/worker identity model +> (`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). + +> **Consumer-repo ALPNs** (e.g., docker operations) are not listed +> here. A consumer that builds on top of a hub or worker registers its +> operations on the call protocol (channel 0), not as a separate ALPN. +> Docker, for example, registers its operations as call-protocol ops +> (ADR-058), not as `alknet/docker`. ## Authentication @@ -178,13 +261,21 @@ The following types live in alknet-core and are used across handler crates: Not all decisions carry the same reversal cost. One-way door decisions (BiStream type, crate independence, secret material flow) require ADRs and possibly POCs before commitment. Two-way door decisions (single vs multi-transport) can be decided during implementation — start simple, add complexity when needed. The static-vs-dynamic registration question is now resolved: the `HandlerRegistry` (ALPN-level) is static at startup (ADR-010, OQ-04), while the `OperationRegistry` (call-protocol-level) is layered — curated ops static, session/imported ops dynamic at their trust-boundary scopes (ADR-024). WASM compatibility is a design constraint within this framework, not a separate principle: decisions that would permanently close the WASM door require explicit justification. See [ADR-009](decisions/009-one-way-door-decision-framework.md). -### One ALPN, One Connection, One Handler +### One ALPN, One Connection, One Handler (endpoint layer) -Each ALPN gets its own QUIC connection. The handler owns the entire connection lifecycle. Handlers that need multiple streams (SSH, call) call `connection.accept_bi()` or `connection.open_bi()` as needed. There is no multiplexing layer between connections. +Each endpoint ALPN gets its own connection. The handler owns the +entire connection lifecycle. Handlers that need multiple streams (call, +channels) open/accept streams as needed. At the channels layer, the +model extends: one `alknet/channels` connection carries many +data-channel ALPNs, each dispatched via `channel/open` (ADR-073) — a +multiplexing power QUIC's per-connection ALPN doesn't provide natively. ### Handler Independence -No handler crate depends on another handler crate. Cross-handler communication goes through the call protocol (`alknet/call`) or through alknet-core's endpoint. The only crate that depends on all handlers is the CLI binary. +No handler crate depends on another handler crate. Cross-handler +communication goes through the call protocol (`alknet/call` on channel +0) or through the channels substrate. The assembly layer (hub or +worker binary) is the only place that depends on all handlers. ## Design Decisions @@ -194,7 +285,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/). |-----|----------|---------| | [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | Single endpoint, ALPN negotiation routes to handlers | | [002](decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | One trait replaces StreamInterface/MessageInterface | -| [003](decisions/003-crate-decomposition.md) | Crate Decomposition | One crate per protocol handler, core provides shared infra | +| [003](decisions/003-crate-decomposition.md) | Crate Decomposition | One crate per protocol handler, core provides shared infra (crate list superseded by [ADR-085](decisions/085-workspace-scope-core-vs-consumer-repos.md) — core mono-repo vs. consumer repos) | | [004](decisions/004-auth-as-shared-core.md) | Auth as Shared Core | IdentityProvider in core, handlers extract credentials | | [005](decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | ~~Accepted~~ → **Superseded** by [ADR-064](decisions/064-irpc-never-integrated-hand-rolled-framing.md) (irpc was never integrated) | | [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention and Connection Model | `alknet/` prefix, one ALPN per connection | @@ -219,6 +310,13 @@ All design decisions are documented as ADRs in [decisions/](decisions/). | [025](decisions/025-vault-local-only-dispatch.md) | Vault Local-Only Dispatch | Dropped irpc from vault; direct method calls; local-only by construction | | [026](decisions/026-vault-key-model-hd-derivation.md) | Vault Key Model — HD Derivation | HD derivation from BIP39 seed; `74'` coin type; SLIP-0010/Ed25519 default; AES-256-GCM for credentials | | [027](decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign — ACME + RawKey Decoupling | `TlsIdentity::Acme` variant + two-phase server config; `RawKey` uses `ed25519-dalek` (not `iroh::SecretKey`); `acme` feature gate | +| [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | Generic single-stream connections — unblocks TCP+TLS, SSH channels, WebTransport, wasm | +| [070](decisions/070-bidistreamsource-trait.md) | BidiStreamSource Trait | Open `Connection` for extension — downstream crates add connection shapes without editing core | +| [071](decisions/071-channels-wire-format.md) | alknet-channels Wire Format | 9-byte chunk header; N channels over one transport stream | +| [079](decisions/079-hub-relay-translate-not-forward.md) | Hub Relay | Translate `channel/open`, byte-forward data channels; the hub never runs protocol-specific handlers | +| [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) | ## Open Questions @@ -245,21 +343,19 @@ Open questions are tracked in [open-questions.md](open-questions.md). Key questi | Config reload fails | `ArcSwap` keeps the previous valid config. Error is logged. No service interruption | | BiStream read/write error | QUIC stream-level error. The handler detects this as an I/O error and returns from `handle()`. The connection itself may remain open for other streams — but since each handler owns a full `Connection` (one ALPN per connection, ADR-006), a stream error typically causes the handler to return, closing the connection | -## What Stays from the Previous Implementation +## Reference Implementation -The reference implementation at `/workspace/@alkdev/alknet-main/` contains working code that carries forward, adapted to the new model: +The reference implementation at `/workspace/@alkdev/alknet-main/` contains +working code that informed the new architecture. It is reference, not +constraint — understand what it did and why, then implement against +the new `ProtocolHandler` trait, ALPN router, and channels substrate. -| Module | Lines | Destination | Notes | -|--------|-------|-------------|-------| -| `src/auth/*` | ~1450 | alknet-core | Identity, IdentityProvider, keys — simplified per ADR-004 | -| `src/config/*` | ~950 | alknet-core | StaticConfig, DynamicConfig, ArcSwap — adapted for ALPN handler config | -| `src/transport/*` | ~1500 | alknet-core | Transport trait, TCP/TLS/iroh — becomes endpoint connection acceptors | -| `src/call/*` | ~1200 | alknet-call | EventEnvelope, registry, framing — becomes ProtocolHandler on alknet/call | -| `src/interface/ssh.rs` | 982 | alknet-ssh | SSH channel handling | -| `src/server/handler.rs` | 974 | alknet-ssh | SSH server handler | -| `src/server/channel_proxy.rs` | 555 | alknet-ssh | Channel proxy | -| `src/server/serve.rs` | 1526 | alknet-core (reference) | Accept loop pattern informs ALPN router, but gets rewritten | -| `src/client/*` | ~1900 | alknet-ssh | SOCKS5 client, connect logic | -| `src/socks5/*` | ~800 | alknet-ssh | SOCKS5 protocol | - -The old code is reference, not constraint. Understand what it did and why, then implement against the new ProtocolHandler trait and ALPN router. \ No newline at end of file +| Module | Destination | Notes | +|--------|-------------|-------| +| `src/auth/*` | alknet-core | Identity, IdentityProvider, keys — simplified per ADR-004 | +| `src/config/*` | alknet-core | StaticConfig, DynamicConfig, ArcSwap | +| `src/transport/*` | alknet-core + alknet-tls | Transport construction → alknet-tls (ADR-082); accept loops → alknet-core (ADR-083) | +| `src/call/*` | alknet-call | EventEnvelope, registry, framing — becomes `ProtocolHandler` on `alknet/call` | +| `src/server/serve.rs` | alknet-core (reference) | Accept loop pattern informs the ALPN router; rewritten as multi-transport accept-loop runner (ADR-083) | +| `src/interface/ssh.rs`, `src/server/*` | alknet-ssh [not yet specced] | SSH channel handling — future russh server channels wrapper for git/sftp compat | +| `src/socks5/*`, `src/client/*` | alknet-socks5 [not yet specced] | SOCKS5 protocol — future channels data-channel ALPN | \ No newline at end of file diff --git a/docs/architecture/questions/062-alpn-list-sharing-two-config-hub.md b/docs/architecture/questions/062-alpn-list-sharing-two-config-hub.md new file mode 100644 index 0000000..890f39d --- /dev/null +++ b/docs/architecture/questions/062-alpn-list-sharing-two-config-hub.md @@ -0,0 +1,54 @@ +# OQ-62: Does a Hub Pass the Same ALPN List to Both `TlsServerConfig`s? + +- **Origin**: `docs/architecture/crates/tls/README.md` (the + "What `AlknetEndpoint` does after the refactor" section describes a + hub holding two `TlsServerConfig`s — raw key + X.509/ACME — but does + not state whether each receives the same ALPN list or different + 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 +- **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* — + same-list vs split-list — sets the assembly-layer wiring shape that + downstream consumers copy) +- **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: + + **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. + + **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 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. + + 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]` — 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) \ No newline at end of file diff --git a/docs/architecture/questions/063-tlserror-shape.md b/docs/architecture/questions/063-tlserror-shape.md new file mode 100644 index 0000000..277ffca --- /dev/null +++ b/docs/architecture/questions/063-tlserror-shape.md @@ -0,0 +1,52 @@ +# OQ-63: `TlsError` Shape + +- **Origin**: `docs/architecture/crates/tls/README.md` + (`TlsError` is referenced as the `Result` error type in + `TlsServerConfig::new`, `for_quinn()`, and the crate's public + signatures, but is never sketched or defined); ADR-082 (same — + `TlsError` in signatures, no shape). +- **Status**: open +- **Door type**: one-way (the error type is the public API surface of + `alknet-tls`; changing it after consumers exist is a breaking change + to every assembly-layer call site) +- **Priority**: high (an implementer cannot write the crate without + deciding this; guessing produces divergent shapes — one thin + `rustls::Error` wrapper vs a 10-variant enum with per-path context) +- **Resolution**: Not yet decided. The shape needs to cover the failure + modes across all four identity paths: + + - **Cert/key loading** (`X509`: file read + PEM parse; + `SelfSigned`: rcgen generation) — currently `io::Error`-wrapped in + the core code. + - **rustls config construction** (`builder_with_provider` / + `with_safe_default_protocol_versions` / + `with_single_cert` / `with_cert_resolver`) — currently + `rustls::Error`-wrapped. + - **quinn wrap** (`QuicServerConfig::try_from(rustls::ServerConfig)`) + — the one path where `for_quinn()` can fail; currently + `io::Error::other(e)`-wrapped. + - **ACME** — `rustls-acme` has its own error types; the ACME task + runs in the background and surfaces errors via events (logged, not + returned from `new`), so `new`'s ACME path may only need to cover + "ACME feature not enabled but `TlsIdentity::Acme` configured" + (currently an `io::ErrorKind::Unsupported`). + + The open question is the granularity: a single `TlsError` enum with + variants per failure category (cert-load, rustls-build, quinn-wrap, + acme-disabled) vs a thin wrapper around `rustls::Error` / + `io::Error`. The single-enum shape gives callers matchable context + (the assembly layer can distinguish "cert file missing" from "quinn + rejected the rustls config"); the thin wrapper is less code but + loses the distinction. This is an API-surface decision — it needs an + ADR or at minimum a sketch in the TLS README before implementation. + + Subsidiary question: does `TlsError` live in `alknet-tls` (owned by + the crate that produces it) or is it re-exported from `alknet-core`? + Likely `alknet-tls` (it's the crate's own error), but worth + confirming so `alknet-core`'s `EndpointError` (which no longer has a + `TlsConfig` variant after ADR-083) doesn't need to know about it. +- **Cross-references**: ADR-082 (the extraction that introduces + `TlsError`), ADR-083 (the endpoint refactor that removes + `EndpointError::TlsConfig`, making `TlsError` the sole TLS error + surface), `crates/alknet-core/src/endpoint.rs` (the current + `io::Error`-wrapping pattern the new type replaces) \ No newline at end of file diff --git a/docs/architecture/questions/064-client-side-tls-helper.md b/docs/architecture/questions/064-client-side-tls-helper.md new file mode 100644 index 0000000..840ffcc --- /dev/null +++ b/docs/architecture/questions/064-client-side-tls-helper.md @@ -0,0 +1,55 @@ +# OQ-64: Should `alknet-tls` Provide a Client-Side TLS Config Helper? + +- **Origin**: `docs/architecture/crates/tls/README.md` (the "Server-only + for now" section flags this as deferred); ADR-084 (requires the + client-side `rustls::ClientConfig` to use the same `aws_lc_rs` + provider — currently enforced by convention, not shared code). +- **Status**: deferred(scope) +- **Door type**: two-way (adding a client helper to `alknet-tls` is + additive; the risk is not the addition but *extracting it + prematurely* and baking in a QUIC-shaped client — see Blocking on) +- **Priority**: medium +- **Blocked on**: the `AlknetClient` dial-seam extraction (OQ-55). The + client-side TLS helper and the shared dial are the same seam: both + answer "how does an outbound connection build its + `rustls::ClientConfig` + select a verifier (ADR-034) + dial the + transport." Extracting the TLS helper without a second transport's + real client dial existing would bake the QUIC client's shape into a + shared helper — the same welding ADR-065 unwound on the server side. + The blocking condition is the same as OQ-55: a second transport's + real dial (TCP+TLS, SSH raw-TCP, HTTP-wrapped call) existing, so the + transport-polymorphic client+TLS seam is extractable from two + *different* transport implementations. +- **Resolution**: Not yet decidable. `alknet-tls` is server-side only + as specified. The client side — `rustls::ClientConfig` construction + + ADR-034 verifier selection (fingerprint pin for known peers, CA-verify + for unknown X.509, fail-closed for unknown raw-key) — lives in + `alknet-call`'s `FingerprintPinVerifier` today. The + provider-consistency requirement (ADR-084: `aws_lc_rs` on all paths) + is enforced by convention (two crates independently constructing + `aws_lc_rs::default_provider()`) until this OQ is resolved. + + What does NOT block on this: each client (`CallClient`, + `ChannelClient`) building its own `ClientConfig` standalone with the + matching provider. The friction is duplicated boilerplate (each + client rebuilds verifier selection + provider wiring), not a missing + capability and not a QUIC-welded client API. The + transport-agnostic take-over (`CallClient::spawn_dispatch`, + `ChannelClient::from_connection` — ADR-080) is decided and is not + the thing being deferred; only the shared *client TLS config helper* + is. + + Note on the hub-as-client case: a hub (A) that dials another hub (B) + uses a client to do so — from B's perspective A is a worker. The + bidirectionality of the call and channels protocols means both sides + can be both hub and worker within a connection. This does not change + the blocking condition: the shared client TLS helper is still about + the *dial*, regardless of whether the dialer is a hub, a worker, or a + hub-worker. The hub-as-client case is a use case that the resolved + helper must cover, not a reason to resolve it now. +- **Cross-references**: OQ-55 (the `AlknetClient` dial-seam extraction + — this OQ's blocking condition), ADR-034 (verifier selection — the + rule the helper would centralize), ADR-084 (provider consistency — + the convention that holds until this is resolved), ADR-065 (the + server-side transport generalization this OQ's deferral avoids + preempting on the client side) \ No newline at end of file