From 86695946614b9ba1be0a30f15b201f883661e2b0 Mon Sep 17 00:00:00 2001 From: "glm-5.2" Date: Wed, 15 Jul 2026 13:19:30 +0000 Subject: [PATCH] docs(arch): extract AlknetEndpoint into alknet-endpoint (ADR-083 Am. 2026-07-15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amend ADR-083 with the crate-extraction decision: the endpoint moves from alknet-core into a new crate alknet-endpoint, mirroring the alknet-client extraction (ADR-089). The ADR's shape (new + builder methods + public dispatch + run/shutdown) is unchanged; only the location changes. The extraction is structural pruning, not an inline refactor. The endpoint is a leaf consumer of core's shared types (zero handler crates import it; 124 import sites for the other core modules). Extracting it lets core shed quinn/iroh/rcgen/rustls-acme — handler crates no longer transitively link those. A pure worker (client-only) does not pull alknet-endpoint at all. The dep graph is symmetric: alknet-core is the shared types crate; alknet-endpoint and alknet-client are the server-side and client-side establishment crates. New spec: crates/endpoint/README.md (the canonical endpoint spec). core/endpoint.md is deprecated to a stub. Cross-references updated across 8 docs (README, overview, tls, hub, client, core README, ADR-083, ADR-089 references). Architecture review passed (3 critical, 9 warnings — all addressed). --- docs/architecture/README.md | 7 +- docs/architecture/crates/client/README.md | 4 +- docs/architecture/crates/core/README.md | 26 +- docs/architecture/crates/core/endpoint.md | 429 ++---------------- docs/architecture/crates/endpoint/README.md | 384 ++++++++++++++++ docs/architecture/crates/hub/README.md | 7 +- docs/architecture/crates/tls/README.md | 50 +- .../083-endpoint-as-accept-loop-runner.md | 205 +++++++-- docs/architecture/overview.md | 12 +- 9 files changed, 667 insertions(+), 457 deletions(-) create mode 100644 docs/architecture/crates/endpoint/README.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index c9aa858..c3142b4 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -154,9 +154,9 @@ adapter location map is now consistent: all HTTP-backed adapters |----------|--------|-------------| | [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/README.md](crates/core/README.md) | draft | alknet-core crate index — shared types + auth + config (endpoint extracted to `alknet-endpoint` per ADR-083 Am. 2026-07-15) | | [crates/core/core-types.md](crates/core/core-types.md) | draft | ProtocolHandler, HandlerError, Connection (`Box` — ADR-070), BidiStreamSource trait, BiStream, StreamError | -| [crates/core/endpoint.md](crates/core/endpoint.md) | draft | ALPN router, HandlerRegistry, accept loop, shutdown | +| [crates/core/endpoint.md](crates/core/endpoint.md) | deprecated | Endpoint spec — **moved to `alknet-endpoint`** (ADR-083 Am. 2026-07-15); see [`crates/endpoint/README.md`](crates/endpoint/README.md) | | [crates/core/auth.md](crates/core/auth.md) | draft | AuthContext (incl. `anonymous` constructor), Identity, IdentityProvider, AuthToken, resolution flow | | [crates/core/config.md](crates/core/config.md) | draft | StaticConfig, DynamicConfig, ArcSwap, ConfigReloadHandle | | [crates/call/README.md](crates/call/README.md) | draft | alknet-call crate index | @@ -188,6 +188,7 @@ adapter location map is now consistent: all HTTP-backed adapters | [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) | reviewed | alknet-tls crate — shared TLS config (`TlsServerConfig` + `TlsClientConfig`) shared 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/client/README.md](crates/client/README.md) | draft | alknet-client crate — the native client dial seam (`AlknetClient`), client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); produces `Connection` for `CallClient`/`ChannelClient` take-over; `alknet/register` named (wire protocol deferred, OQ-66) | +| [crates/endpoint/README.md](crates/endpoint/README.md) | draft | alknet-endpoint crate — the server-side accept-loop runner (`AlknetEndpoint`), extracted from `alknet-core` (ADR-083 Am. 2026-07-15); takes pre-built transports via `with_quinn`/`with_iroh`/`with_tcp_tls`; public `dispatch` for SSH/WT; handler crates no longer transitively link quinn/iroh | | [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) | @@ -282,7 +283,7 @@ adapter location map is now consistent: all HTTP-backed adapters | [080](decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | Accepted | | [081](decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | Accepted | | [082](decisions/082-alknet-tls-extraction.md) | alknet-tls Crate Extraction | Accepted (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 | Accepted (revised — TCP+TLS is an owned transport, not external) | +| [083](decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as Multi-Transport Accept-Loop Runner with Public Dispatch | Accepted (revised — TCP+TLS is an owned transport, not external; amended 2026-07-15 — endpoint extracted from `alknet-core` into `alknet-endpoint`) | | [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 | diff --git a/docs/architecture/crates/client/README.md b/docs/architecture/crates/client/README.md index 3e16b83..ded0c4e 100644 --- a/docs/architecture/crates/client/README.md +++ b/docs/architecture/crates/client/README.md @@ -426,7 +426,7 @@ alknet-hub (uses AlknetClient for outbound worker dials) ├── alknet-client (the dial — the hub's dial_worker closure calls it) ├── alknet-channels-call (ChannelClient — the take-over) ├── alknet-call (CallAdapter, Dispatcher) -└── alknet-core (AlknetEndpoint) +└── alknet-endpoint (AlknetEndpoint) alknet-worker (uses AlknetClient to dial a hub) ├── alknet-client (the dial) @@ -533,7 +533,7 @@ See [open-questions.md](../../open-questions.md) for full details. `ChannelClient::from_connection` (the take-over the dial feeds) - [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md) — `CallClient::spawn_dispatch` (the take-over the dial feeds) -- [`crates/core/endpoint.md`](../core/endpoint.md) — `AlknetEndpoint` +- [`crates/endpoint/README.md`](../endpoint/README.md) — `AlknetEndpoint` (the server-side complement) - [`crates/tls/README.md`](../tls/README.md) — `TlsClientConfig` - [`crates/call/client-and-adapters.md`](../call/client-and-adapters.md) diff --git a/docs/architecture/crates/core/README.md b/docs/architecture/crates/core/README.md index fc756b6..c51a1ec 100644 --- a/docs/architecture/crates/core/README.md +++ b/docs/architecture/crates/core/README.md @@ -1,18 +1,27 @@ --- status: draft -last_updated: 2026-07-12 +last_updated: 2026-07-15 --- # alknet-core -Core library for ALPN-based protocol dispatch. Every handler crate depends on alknet-core. +Shared types, auth, config, and identity for ALPN-based protocol +dispatch. Every handler crate depends on `alknet-core` for +`ProtocolHandler`, `Connection`, `AuthContext`, `IdentityProvider`, and +config types. The endpoint (`AlknetEndpoint`, `HandlerRegistry`, +`EndpointError`) has been extracted to +[`alknet-endpoint`](../endpoint/README.md) (ADR-083 Amendment +2026-07-15); core no longer carries the accept-loop runner or its +transport deps (quinn, iroh, rcgen, rustls-acme). `Connection::from_quinn` +/ `from_iroh` stay in core's `types.rs` as shared constructors (gated +on core's `quinn` / `iroh` features). ## Documents | Document | Status | Description | |----------|--------|-------------| | [core-types.md](core-types.md) | draft | ProtocolHandler trait, HandlerError, Connection (`Box` — ADR-070), BidiStreamSource trait, BiStream, StreamError | -| [endpoint.md](endpoint.md) | draft | ALPN router, HandlerRegistry, accept loop, graceful shutdown | +| [endpoint.md](endpoint.md) | deprecated | Endpoint spec — **moved to [`alknet-endpoint`](../endpoint/README.md)** (ADR-083 Am. 2026-07-15); this file is a stub | | [auth.md](auth.md) | draft | AuthContext (incl. `anonymous` constructor), Identity, IdentityProvider, AuthToken, resolution flow, PeerEntry, CredentialStore | | [config.md](config.md) | draft | StaticConfig, DynamicConfig, ArcSwap, ConfigReloadHandle, AuthPolicy.peers | @@ -27,7 +36,7 @@ Core library for ALPN-based protocol dispatch. Every handler crate depends on al | [006](../../decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention | ALPN format, one-ALPN-per-connection | | [007](../../decisions/007-bistream-type-definition.md) | BiStream Type Definition | Connection, BiStream trait, SendStream, RecvStream | | [009](../../decisions/009-one-way-door-decision-framework.md) | One-Way Door Framework | Decision classification | -| [010](../../decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Endpoint, HandlerRegistry, accept loop | +| [010](../../decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | HandlerRegistry, accept loop — endpoint extracted to `alknet-endpoint` per ADR-083 Am. 2026-07-15 | | [011](../../decisions/011-authcontext-structure.md) | AuthContext Structure | AuthContext fields and resolution flow | | [015](../../decisions/015-privilege-model-and-authority-context.md) | Privilege Model and Authority Context | Per-request identity on OperationContext; admin scope for config reload | | [030](../../decisions/030-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | `authorized_fingerprints` → `peers: Vec`; `Identity.id` = `peer_id` (stable) | @@ -35,25 +44,26 @@ Core library for ALPN-based protocol dispatch. Every handler crate depends on al | [033](../../decisions/033-storage-boundary-and-repo-adapter-pattern.md) | Storage Boundary and Repo/Adapter Pattern | Core defines traits + in-memory defaults; persistence adapters are separate crates | | [065](../../decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` — Generic Single-Stream Connections | `from_stream`/`from_bidi` accept any `AsyncRead + AsyncWrite`; yield-once `accept_bi` contract; unblocks TCP+TLS, SSH channels, WebTransport, wasm | | [070](../../decisions/070-bidistreamsource-trait.md) | BidiStreamSource Trait — Open Connection for Extension | `Connection` holds `Box`; QUIC/iroh/stream wrap crate-private impls; `from_source` is the public constructor for downstream crates that implement the trait (channels, future transports) | +| [083](../../decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as accept-loop runner + crate extraction | The endpoint is extracted from core into `alknet-endpoint`; core loses `quinn`/`iroh`/`rcgen`/`rustls-acme` deps; `Connection::from_quinn`/`from_iroh` stay in core as shared constructors | ## Relevant Open Questions | OQ | Title | Status | Relevance | |----|-------|--------|-----------| -| OQ-04 | Dynamic handler registration | resolved (start static) | HandlerRegistry is immutable at startup | -| OQ-05 | Multi-connectivity endpoint | resolved (quinn + iroh) | AlknetEndpoint supports both, both feature-gated | +| OQ-04 | Dynamic handler registration | resolved (start static) | HandlerRegistry is immutable at startup (now in `alknet-endpoint`) | +| OQ-05 | Multi-connectivity endpoint | resolved (quinn + iroh) | AlknetEndpoint supports both, both feature-gated (now in `alknet-endpoint`) | | OQ-11 | Handler-level auth resolution observability | resolved | Handlers store resolved identity on Connection; two identity scopes (connection-level for observability, per-request for ACL) | | OQ-33 | PeerId — logical id vs crypto identity | resolved by ADR-030 | `PeerId` = `Identity.id` = `PeerEntry.peer_id` (stable across key rotation) | | OQ-34 | Persistent peer registry (storage boundary) | resolved by ADR-030+031+033 | Core defines repo traits + in-memory defaults; persistence adapters are separate crates | | OQ-35 | ~~API key asymmetry~~ | dissolved | `PeerEntry` supports multiple credential paths; `ApiKeyEntry` is for tokens that ARE the identity | | OQ-36 | Concrete persistence adapter shapes | resolved by ADR-035 | Read-sync / write-async split (`IdentityStore`); SQLite adapter caches in memory, honker NOTIFY for no-restart cache invalidation; `alknet-store-sqlite` crate | | OQ-37 | X.509 outgoing-only case | resolved by ADR-034 | Three remote roles (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry correct; client-side verifier by `PeerEntry` presence (CA vs fingerprint pin) | -| OQ-55 | AlknetClient / Client Establishment Extraction | resolved by ADR-089 | The native dial seam is extracted as `alknet-client` — the client-side analogue of `AlknetEndpoint`. Three dial methods (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key). The client take-over APIs (`CallClient::spawn_dispatch`, `ChannelClient::from_connection` — ADR-080) are transport-agnostic and decided; the shared dial is now extracted. | +| OQ-55 | AlknetClient / Client Establishment Extraction | resolved by ADR-089 | The native dial seam is extracted as `alknet-client` — the client-side analogue of `AlknetEndpoint` (now in `alknet-endpoint`). Three dial methods (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key). | ## Key Design Principles 1. **One trait, one dispatch point**: `ProtocolHandler` is the only abstraction handlers implement. No StreamInterface/MessageInterface split. -2. **ALPN does the routing**: The endpoint dispatches by ALPN string. No byte-peeking, no ListenerConfig enum. +2. **ALPN does the routing**: The endpoint (in `alknet-endpoint`) dispatches by ALPN string. No byte-peeking, no ListenerConfig enum. 3. **Handlers own their wire format**: Each handler manages its own protocol parsing. alknet-core provides the Connection, not the framing. 4. **Auth is hybrid**: The endpoint provides what it can (TLS-level auth). Handlers complete what they need. AuthContext may be partial. 5. **WASM door preserved**: BiStream is a trait, Connection is an opaque type. Core types don't assume tokio or quinn in public APIs. \ No newline at end of file diff --git a/docs/architecture/crates/core/endpoint.md b/docs/architecture/crates/core/endpoint.md index 3309897..c1e0ea5 100644 --- a/docs/architecture/crates/core/endpoint.md +++ b/docs/architecture/crates/core/endpoint.md @@ -1,393 +1,44 @@ --- -status: draft +status: deprecated last_updated: 2026-07-15 --- -# Endpoint - -ALPN router, handler registry, connection accept loops, multi-connectivity, and graceful shutdown. - -See [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) for the full rationale. - -## AlknetEndpoint - -The central runtime type. Manages one or more QUIC connection sources, each feeding into the same ALPN router. - -```rust -pub struct AlknetEndpoint { - // One or more connection sources — all optional, all can be active simultaneously - quinn: Option, // Public QUIC+TLS - iroh: Option, // P2P relay-assisted - #[cfg(feature = "tcp")] - tcp_tls: Option, // TCP+TLS (TcpListener + TlsAcceptor) - - handlers: Arc, - dynamic: Arc>, - identity_provider: Arc, - shutdown_tx: watch::Sender, - shutdown_rx: watch::Receiver, - drain_timeout: Duration, -} -``` - -See [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) for -the full design (the endpoint takes no `StaticConfig` or TLS config; -transports are built by the assembly layer and handed via -`with_quinn` / `with_iroh` / `with_tcp_tls`). - -### Why multiple connection sources? - -A node can be reachable through different paths depending on its network context: - -| Source | Requires | Identity source | Use case | -|--------|----------|-----------------|----------| -| `quinn::Endpoint` | Public IP, TLS cert | TLS cert (network), SSH key (auth) | VPS, replicators, service hosts | -| `iroh::Endpoint` | Relay access | NodeId (Ed25519) | Home servers, NAT, IoT | - -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 -endpoint owns it via `with_tcp_tls(listener, acceptor)` (behind a `tcp` -feature) and runs its accept loop inside `run()` — `tcp.accept()` → -`tls.accept()` → extract ALPN + fingerprint → `Connection::from_bidi` → -`dispatch`. No external sibling loop, no duplicated dispatch logic. - -This reverses ADR-010's original "TCP is not an endpoint struct concern." -The reason TCP was excluded — the endpoint built transports internally, -and TCP+TLS couldn't fit that shape — is gone (ADR-083: the endpoint no -longer builds transports; it runs accept loops on whatever it's given). -TCP+TLS fits the same listener shape as quinn and iroh. - -The `dispatch` method is public for transports the endpoint **can't -own** — SSH channels (one connection, many channels with different -ALPNs — a multiplexing shape, not a listener) and future WebTransport -streams (one QUIC connection, many WT streams). These are -connection-internal multiplexing, not listener transports. - -See [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) -for the full design. - -## HandlerRegistry - -Maps ALPN byte strings to `ProtocolHandler` instances. - -```rust -pub struct HandlerRegistry { - handlers: HashMap<&'static [u8], Arc>, -} - -impl HandlerRegistry { - pub fn new() -> Self; - pub fn register(&mut self, handler: Arc); - pub fn get(&self, alpn: &[u8]) -> Option<&Arc>; - pub fn alpn_strings(&self) -> Vec>; -} -``` - -- `register()`: Insert a handler. Panics if the ALPN is already registered. -- `get()`: Look up a handler by ALPN string. -- `alpn_strings()`: Return all registered ALPN strings. Used to build the TLS `ServerConfig` (for quinn) and the ALPN list (for iroh). - -Registration is static at startup (see [OQ-04](../../open-questions.md)). The CLI builds a `HandlerRegistry`, inserts all handlers, and passes it to `AlknetEndpoint::new()`. - -### ALPN strings in TLS ServerConfig and iroh endpoint - -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)). - -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 (filtered to the iroh endpoint type's -ALPNs). - -## Accept Loops - -Each active connection source runs its own accept loop. All loops dispatch through the same `HandlerRegistry`: - -### Quinn accept loop (public QUIC+TLS) - -``` -loop { - tokio::select! { - incoming = quinn_endpoint.accept() => { - let connection = incoming.await; // TLS handshake + ALPN negotiation - match connection { - Ok(conn) => dispatch(conn), - Err(e) => { /* log TLS handshake failure, continue */ } - } - } - _ = shutdown.changed() => break, - } -} -``` - -### iroh accept loop (P2P relay-assisted) - -iroh's `Endpoint` natively supports ALPN negotiation (step 4 of its connection establishment). The `iroh::Endpoint::set_alpns()` method configures which ALPNs the endpoint advertises — the same mechanism iroh's own `Router` uses internally with its `ProtocolMap`. - -We use `iroh::Endpoint` directly (not iroh's `Router`) because our `HandlerRegistry` is shared between quinn and iroh connection sources, and our `AuthContext` construction differs per source. Our accept loop replaces iroh's `Router` accept loop with our own dispatch: - -``` -loop { - tokio::select! { - incoming = iroh_endpoint.accept() => { - // incoming is an iroh::endpoint::Incoming - let accepting = incoming.accept(); // Accepting state - let alpn = accepting.alpn().await; // ALPN from TLS handshake - match alpn { - Ok(alpn) => dispatch(alpn, accepting), - Err(e) => { /* log handshake failure, continue */ } - } - } - _ = shutdown.changed() => break, - } -} -``` - -See iroh's `protocol.rs` (`/workspace/iroh/iroh/src/protocol.rs`) for the reference implementation of this pattern — `handle_connection()` reads the ALPN, looks up the handler in `ProtocolMap`, and calls `handler.accept(connection)`. Our dispatch is the same pattern with our `HandlerRegistry`. - -### Dispatch function (shared) - -The public `dispatch` method is the shared dispatch path for every -transport — the endpoint's own accept loops (quinn, iroh, TCP+TLS) call -it after transport-specific extraction, and external dispatch callers -(SSH channels, future WebTransport streams) call it after their own -extraction. - -``` -pub fn dispatch(&self, connection: Connection, alpn: Vec, - fingerprint: Option, remote_addr: Option) { - // ACME guard (transport-agnostic — ADR-083) - if alpn == b"acme-tls/1" { - connection.close(0, "acme done"); - return; - } - match handlers.get(&alpn) { - Some(handler) => { - let auth = build_auth_context(&alpn, remote_addr, fingerprint, &identity_provider); - tokio::spawn(async move { - if let Err(e) = handler.handle(connection, &auth).await { - // log error, connection closes - } - }); - } - None => { connection.close(0, "no handler"); /* log warning */ } - } -} -``` - -Synchronous (non-async): spawns the handler on its own task and returns -immediately. The caller's accept loop is not blocked. See -[ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) for the -full dispatch contract. - -### What the accept loops do NOT do - -- **No byte-peeking**: ALPN negotiation handles protocol detection. The old `stealth` module's `detect_protocol()` is unnecessary. -- **No per-handler accept loops**: The old `ListenerConfig` enum had Stream/Http/Dns variants with different accept paths. ALPN unifies this. -- **No SSH-specific logic**: The accept loop is ALPN-agnostic. It doesn't know or care what protocol the handler speaks. - -## Stealth Mode as ALPN Dispatch - -The reference implementation's "stealth mode" is SSH-over-TLS on port 443. The TLS cert is **camouflage**, not identity — it makes the port look like a web server to port scanners and DPI systems. Non-SSH traffic gets a fake nginx 404. - -In the ALPN model, this maps to: - -- The `alknet/http` handler is registered for standard HTTP ALPNs (`h2`, `http/1.1`) -- The HTTP handler can serve a decoy website or a fake 404 -- Real services use `alknet/ssh`, `alknet/call`, etc. -- Clients that don't offer alknet ALPNs get the HTTP handler — just like port scanners in stealth mode - -No byte-peeking, no `ProtocolDetection` enum. ALPN does the routing. - -## Network Identity vs Auth Identity - -A key distinction that the ALPN model makes explicit: - -| Layer | Purpose | Mechanism | -|-------|---------|-----------| -| **Network identity** | How a client finds and verifies the node | X.509 cert (domain) or RFC 7250 raw key (Ed25519) or iroh NodeId | -| **Auth identity** | Who the peer is and what they can do | SSH key, API token, certificate (handlers) | - -The TLS cert (or raw public key, or NodeId) is the node's network-facing identity. It's NOT the node's authentication identity. Auth happens inside the handler via `IdentityProvider`. - -This matches the reference implementation: the TLS cert encrypts and camouflages, but SSH key exchange handles the actual authentication. - -## RFC 7250: Raw Public Keys in TLS - -RFC 7250 raw public keys are the **default TLS identity mode** for most alknet nodes. They eliminate the need for domain names, CAs, and certificate renewal — the Ed25519 public key IS the node's identity. - -iroh uses this model with its `NodeId`. The implementation is ~100 lines (see `iroh/iroh/src/tls/resolver.rs`): take an Ed25519 key, wrap its SPKI public key as a `CertificateDer`, tell rustls `only_raw_public_keys() -> true`. No X.509, no CAs, no domain names, no cert renewal. - -Key implications: - -- **Default for alknet-native clients**: SSH, git, and alknet-native clients all work with raw Ed25519 keys out of the box. The same key type used for SSH auth can serve as the TLS identity. This is the most common deployment mode. -- **No domain required**: A node without a domain name uses raw public keys for the quinn path — key-based identity with direct QUIC over UDP. -- **Key = identity**: The Ed25519 public key IS the node's identity. No CA trust chain, no cert expiry. The key can be derived from alknet-vault. -- **X.509 is for domain-hosted services**: Domain-facing identity (replicators, public services, browsers) uses X.509 certs. This is a separate use case, not the default. -- **Browser limitation**: Browsers don't support RFC 7250. For browser/WebTransport clients, X.509 certs are needed. For all other clients, raw public keys work fine. - -The quinn and iroh paths share the same key-based identity model via RFC 7250. They're distinguished by **connection establishment** (direct UDP vs relay-assisted), not by identity: - -| Path | Connection establishment | Default identity | Alternative identity | -|------|------------------------|-----------------|---------------------| -| quinn | Direct UDP, public IP | RFC 7250 raw key (most nodes) | X.509 cert (domain-hosted, browsers) | -| iroh | Relay-assisted P2P | RFC 7250 raw key (NodeId) | N/A | - -## TLS Identity - -TLS identity in alknet has two distinct use cases, each with a different trust model and provisioning mechanism. See OQ-12 for the full rationale. - -### Use case 1: P2P / Key-based identity (default) - -Most alknet nodes use RFC 7250 raw Ed25519 public keys for TLS identity. No domain name, no CA, no certificate renewal. The Ed25519 public key IS the node's identity — the same key model as iroh's `NodeId`, but for direct QUIC connections. - -`TlsIdentity::RawKey` in `StaticConfig` configures this mode. The endpoint builds a `rustls::ServerConfig` with `only_raw_public_keys() -> true` and a `ResolvesServerCert` that generates the certificate on-the-fly from the key, exactly as iroh does (see `iroh/iroh/src/tls/resolver.rs`). - -This mode works natively with SSH auth (same key type) and git (SSH key-based auth). It is the default for alknet-native clients. **Browser/WebTransport clients do not support RFC 7250** — they require X.509 certificates. - -### Use case 2: Domain-hosted services - -Nodes that serve browser/WebTransport clients, or nodes with public domain names, use X.509 certificates. This has two sub-cases: - -- **Manual**: Provide cert/key file paths via `TlsIdentity::X509`. The endpoint loads them at startup and builds a standard `rustls::ServerConfig`. -- **ACME auto-provisioning**: Let's Encrypt via `rustls-acme`. `TlsIdentity::Acme { domains, cache_dir, directory, contact }` carries the static config; the endpoint constructs the `AcmeState` async state machine and `ResolvesServerCertAcme` at setup time (ADR-027). The `acme` feature gate keeps `rustls-acme` out of non-ACME builds. See [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) for the full design. - -`TlsIdentity::SelfSigned` is for development only — the endpoint generates a self-signed cert on startup. External clients will not trust it. - -### iroh endpoint identity - -The iroh endpoint does not need TLS certificate configuration — it uses `NodeId` (Ed25519) for identity, which is RFC 7250 raw key identity built into the iroh endpoint. - -### Identity model comparison - -| Path | Identity model | Client compatibility | Use case | -|------|---------------|---------------------|----------| -| quinn + `TlsIdentity::RawKey` | RFC 7250 Ed25519 raw key | alknet-native, SSH, git | Personal nodes, P2P, most deployments | -| quinn + `TlsIdentity::X509` | X.509 domain certificate (manual) | All clients including browsers | Relays, public services, WebTransport | -| quinn + `TlsIdentity::Acme` | X.509 via ACME auto-provisioning | All clients including browsers | Public relays, domain-hosted services | -| quinn + `TlsIdentity::SelfSigned` | X.509 self-signed cert | None (dev only) | Local development | -| iroh | NodeId (Ed25519, RFC 7250 built-in) | alknet-native, iroh clients | NAT traversal, home servers | - -Note: `TlsIdentity::RawKey` uses `Ed25519SecretKey` (alknet-core-owned, -backed by `ed25519-dalek`), not `iroh::SecretKey`. It is available in -quinn-only builds without the `iroh` feature. When the iroh transport is -also configured, `build_iroh_endpoint` converts the key to -`iroh::SecretKey::from_bytes` (ADR-027). The iroh dep is on `1.0` -(`default-features = false, features = ["tls-aws-lc-rs"]`, matching the -quinn path's aws-lc-rs crypto provider); migrated from `0.35` in commit -`acd049e` (2026-07-09) — 6 API surface edits, no architectural change. - -## Graceful Shutdown - -```rust -impl AlknetEndpoint { - pub fn shutdown_sender(&self) -> watch::Sender; - pub async fn shutdown(&self) -> Result<(), EndpointError>; -} -``` - -- `shutdown_sender()` returns a clone of the shutdown channel sender. Call `send(true)` to signal shutdown. The assembly layer uses this for any external dispatch callers (SSH, future WT); the endpoint's own loops are signaled internally. -- `shutdown()` signals all owned accept loops (quinn, iroh, TCP+TLS) to stop, waits for in-flight dispatched handlers with a drain timeout, then forcefully closes remaining connections. One owner, one shutdown — no external loop coordination (ADR-083). -- SIGTERM/SIGINT are wired to the shutdown channel by the CLI binary. - -The drain timeout is passed to `AlknetEndpoint::new()` directly (as -`drain_timeout: Duration`), not via `StaticConfig` — the endpoint no -longer takes `StaticConfig` (ADR-083). The assembly layer reads -`StaticConfig::drain_timeout` and passes it in. - -## Error Handling - -### EndpointError - -Fatal errors that prevent the endpoint from starting or continuing. - -```rust -pub enum EndpointError { - BindFailed(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. - -### Accept loop errors - -- **TLS handshake failure**: Log and continue. The client may have offered no compatible ALPN, or the cert may be untrusted. -- **Handler panic**: Caught by tokio's task isolation. The connection is dropped. Other connections continue. -- **Connection-level errors** (quinn/iroh `ConnectionError`): Log and continue. The accept loop keeps running. - -## Key Differences from Reference Implementation - -| Aspect | Reference (`alknet-main`) | New Model | -|--------|---------------------------|-----------| -| Transport | `TransportAcceptor` trait, `TransportKind` enum | `quinn::Endpoint` + `iroh::Endpoint`, ALPN dispatch | -| Listener config | `ListenerConfig` enum (Stream/Http/Dns) | Single `HandlerRegistry`, ALPN dispatch | -| Protocol detection | Byte-peeking (`stealth::detect_protocol`) | ALPN negotiation (TLS layer) | -| Stealth mode | SSH-over-TLS with byte-peek | HTTP handler on `h2`/`http/1.1` serves decoy | -| Accept loop | Per-transport, SSH-centric | Per-connection-source, ALPN-agnostic | -| Handler model | `ServerHandler` + `russh::server::Handler` | `ProtocolHandler::handle(Connection, &AuthContext)` | -| Config | `ServeOptions` builder | `StaticConfig` + `HandlerRegistry` + `AlknetEndpoint::new()` | -| iroh | Separate `IrohAcceptor` + `IrohTransport` | `Option` on `AlknetEndpoint` | -| Network vs auth identity | Conflated (TLS cert + SSH key both "auth") | Explicitly separated (TLS/NodeId = network, SSH key/token = auth) | - -## Design Decisions - -| Decision | ADR | Summary | -|----------|-----|---------| -| Multi-connectivity endpoint (quinn + iroh + TCP+TLS) | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md), [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) | All three optional, all feed same dispatch; endpoint owns all accept loops | -| Endpoint takes no TLS config; assembly layer builds transports | [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) | `new()` takes `drain_timeout` + builder methods, no `StaticConfig` or `Arc` | -| TCP+TLS is a first-class owned transport | [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) | `with_tcp_tls(listener, acceptor)` — reverses ADR-010's "TCP is not an endpoint struct concern" | -| Public `dispatch` for SSH/WT (multiplexing shapes) | [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) | `dispatch` is public for connection-internal multiplexing, not for listener transports | -| Static handler registration | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Two-way door, start static, add ArcSwap later | -| No byte-peeking, ALPN dispatch only | [ADR-001](../../decisions/001-alpn-protocol-dispatch.md) | TLS layer handles protocol detection | -| 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 - -See [open-questions.md](../../open-questions.md) for full details. - -- **OQ-04**: Resolved — HandlerRegistry is static at startup. -- **OQ-05**: Resolved — multi-connectivity endpoint with quinn + iroh, both feature-gated. -- **OQ-12**: Resolved — two distinct TLS identity use cases: RFC 7250 raw keys (default, P2P) and X.509 certs (domain-hosted, browsers). ACME auto-provisioning designed in [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md); RawKey decoupled from the `iroh` feature (available in quinn-only builds). -- **OQ-60**: Resolved — transport construction is inlined by the assembly layer; the TCP+TLS loop lives in `alknet-core` behind a `tcp` feature as an owned transport. See [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md). -- **OQ-61**: Dissolved — the multi-owner shutdown problem does not arise; the endpoint owns all its accept loops. See [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md). \ No newline at end of file +# Endpoint (moved to `alknet-endpoint`) + +> **This document is deprecated.** The `AlknetEndpoint`, +> `HandlerRegistry`, and `EndpointError` types have been extracted from +> `alknet-core` into a new crate `alknet-endpoint` (ADR-083 Amendment +> 2026-07-15). The canonical spec is now +> [`crates/endpoint/README.md`](../endpoint/README.md). +> +> The shared types the endpoint imports (`ProtocolHandler`, +> `Connection`, `AuthContext`, `IdentityProvider`, `DynamicConfig`) stay +> in `alknet-core` — see [`core-types.md`](core-types.md), +> [`auth.md`](auth.md), [`config.md`](config.md). + +## Historical summary + +The endpoint was originally in `alknet-core/endpoint.rs` as the central +runtime type — a multi-transport accept-loop runner that dispatches +incoming connections by ALPN (ADR-010, ADR-083). ADR-082 extracted the +TLS setup code to `alknet-tls`; ADR-083 restructured the endpoint to +take pre-built transports via `with_quinn` / `with_iroh` / +`with_tcp_tls` (no TLS config); ADR-083 Amendment 2026-07-15 extracted +the endpoint itself into `alknet-endpoint` so that handler crates no +longer transitively link quinn/iroh/rcgen via core. + +The endpoint's semantics — ALPN dispatch, `HandlerRegistry`, accept +loops, public `dispatch` for SSH/WT, graceful shutdown — are unchanged +by the extraction. See +[`crates/endpoint/README.md`](../endpoint/README.md) for the current +spec and [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) +for the full decision. + +## What stayed in `alknet-core` + +`Connection::from_quinn` / `from_iroh` stay in core's `types.rs` — they +are shared-type constructors used by both the endpoint's accept loop +(server) and `alknet-client`'s dial (client, ADR-089), gated on core's +`quinn` / `iroh` features. See +[ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) §"The +`quinn` feature split". \ No newline at end of file diff --git a/docs/architecture/crates/endpoint/README.md b/docs/architecture/crates/endpoint/README.md new file mode 100644 index 0000000..7c44cbf --- /dev/null +++ b/docs/architecture/crates/endpoint/README.md @@ -0,0 +1,384 @@ +--- +status: draft +last_updated: 2026-07-15 +--- + +# alknet-endpoint + +The server-side establishment crate — the multi-transport accept-loop +runner that dispatches incoming connections by ALPN. The server-side +analogue of `alknet-client` (ADR-089): `alknet-endpoint` accepts and +dispatches; `alknet-client` dials and produces a `Connection`. Both +depend on `alknet-core` for shared types; neither depends on the other. + +## What + +`AlknetEndpoint` is the central runtime type for any node that accepts +inbound connections. It takes pre-built transport endpoints (quinn, +iroh, TCP+TLS) via builder methods, runs their accept loops inside a +single `run()` method, and dispatches each accepted connection to the +registered `ProtocolHandler` by the ALPN the TLS handshake negotiated. +It does not build transports and does not build TLS configs — the +assembly layer does both (transports from `alknet-tls`'s +`TlsServerConfig`, per ADR-082). + +`alknet-endpoint` is extracted from `alknet-core` (ADR-083 Amendment +2026-07-15). The extraction is structural pruning, not a refactor: the +endpoint is a leaf consumer of core's shared types (it imports `auth`, +`config`, `types`; nothing in core imports from it), depended on by a +different audience (the assembly layer) than the shared types (every +handler crate). No handler crate imports `AlknetEndpoint`, +`HandlerRegistry`, or `EndpointError` — they depend on `alknet-core` +for `ProtocolHandler`, `Connection`, `AuthContext`, and types only. + +## Why + +`alknet-core` was two things welded: shared types (depended on by every +handler crate) + the endpoint (depended on by zero handler crates). +Extracting the endpoint into `alknet-endpoint` lets core shed the heavy +transport deps (quinn, iroh, rcgen, rustls-acme) and become the +lightweight types+auth+config crate the handler crates actually want. +See [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) +§"Amendment 2026-07-15 — crate extraction" for the full rationale, +including the dependency data and the symmetry with `alknet-client`. + +## Architecture + +### `AlknetEndpoint` + +```rust +pub struct AlknetEndpoint { + quinn: Option, + iroh: Option, + #[cfg(feature = "tcp")] + tcp_tls: Option, // (TcpListener, TlsAcceptor) + handlers: Arc, + dynamic: Arc>, + identity_provider: Arc, + shutdown_tx: watch::Sender, + shutdown_rx: watch::Receiver, + drain_timeout: Duration, +} + +impl AlknetEndpoint { + pub fn new( + handlers: HandlerRegistry, + dynamic: Arc>, + identity_provider: Arc, + drain_timeout: Duration, + ) -> Self; + + pub fn with_quinn(mut self, endpoint: quinn::Endpoint) -> Self; + pub fn with_iroh(mut self, endpoint: iroh::Endpoint) -> Self; + + #[cfg(feature = "tcp")] + pub fn with_tcp_tls( + mut self, + listener: tokio::net::TcpListener, + acceptor: tokio_rustls::TlsAcceptor, + ) -> Self; + + pub fn shutdown_sender(&self) -> watch::Sender; + + pub fn dispatch( + &self, + connection: Connection, + alpn: Vec, + fingerprint: Option, + remote_addr: Option, + ); + + pub async fn run(self: Arc); + pub async fn shutdown(&self) -> Result<(), EndpointError>; +} +``` + +`new` takes **no `StaticConfig`** and **no TLS config** — the assembly +layer reads `StaticConfig` (in `alknet-core`), builds the transports +(via `alknet-tls`'s `TlsServerConfig` + the transport's own builder), +and hands them to the endpoint via `with_quinn` / `with_iroh` / +`with_tcp_tls`. The endpoint's job is to run accept loops and dispatch; +transport construction is not its concern. See +[ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) for +the full design. + +### `HandlerRegistry` + +Maps ALPN byte strings to `ProtocolHandler` instances. Registered +statically at startup by the assembly layer; the endpoint dispatches by +looking up the negotiated ALPN. + +```rust +pub struct HandlerRegistry { + handlers: HashMap<&'static [u8], Arc>, +} + +impl HandlerRegistry { + pub fn new() -> Self; + pub fn register(&mut self, handler: Arc); + pub fn get(&self, alpn: &[u8]) -> Option<&Arc>; + pub fn alpn_strings(&self) -> Vec>; +} +``` + +- `register()`: Insert a handler. Panics if the ALPN is already registered. +- `get()`: Look up a handler by ALPN string. +- `alpn_strings()`: Return all registered ALPN strings. Used by the + assembly layer to build the TLS `ServerConfig`'s ALPN list (via + `alknet-tls`, filtered by endpoint type per ADR-086). + +Registration is static at startup (ADR-010, OQ-04). The assembly layer +builds a `HandlerRegistry`, inserts all handlers, and passes it to +`AlknetEndpoint::new()`. + +### `EndpointError` + +The error type for `AlknetEndpoint::shutdown` and listener bind +failures. Lives in `alknet-endpoint` (moves with the endpoint from +core). + +```rust +pub enum EndpointError { + BindFailed(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 surface as `TlsError` in `alknet-tls` / +the assembly layer, 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`). + +### `TcpTlsListener` + +The type held by the endpoint's `tcp_tls` field — a tuple of the TCP +listener and the TLS acceptor: + +```rust +type TcpTlsListener = (tokio::net::TcpListener, tokio_rustls::TlsAcceptor); +``` + +The endpoint owns both halves: `tcp.accept()` produces a `TcpStream`, +`tls.accept()` wraps it, then ALPN + fingerprint extraction → +`Connection::from_bidi` → `dispatch`. Feature-gated on `tcp`. + +### Accept loops + +Each active transport runs its own accept loop inside `run()`: + +- **Quinn** — `quinn.accept()` → TLS handshake → extract ALPN + + fingerprint → `Connection::from_quinn_with_alpn` → `dispatch`. +- **Iroh** — `iroh.accept()` → `accepting.alpn().await` → extract + fingerprint → `Connection::from_iroh` → `dispatch`. +- **TCP+TLS** (behind `tcp` feature) — `tcp.accept()` → + `tls.accept()` → extract ALPN + fingerprint → + `Connection::from_bidi` → `dispatch`. + +All three feed the same `dispatch` method. The transport-specific +extraction (ALPN, fingerprint, remote address) is private to the +endpoint; `dispatch` receives the extracted values. See +[ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) +§"Accept Loops" for the loop pseudocode. + +### `dispatch` (public) + +The shared dispatch path for every transport — the endpoint's own +accept loops call it after transport-specific extraction, and external +dispatch callers (SSH channels, future WebTransport streams) call it +after their own extraction. Synchronous (non-async): performs the +ACME guard, handler lookup, `build_auth_context`, and `tokio::spawn`s +the handler. Returns immediately after spawning. + +`dispatch` is public for **connection-internal multiplexing shapes** +that the endpoint can't own (SSH channels: one connection, many +channels with different ALPNs; future WT streams: one QUIC connection, +many WT streams). Listener transports (quinn, iroh, TCP+TLS) are owned +by the endpoint and call `dispatch` internally; they are not external +dispatch callers. See +[ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) +§"`dispatch` is public — for genuinely external shapes". + +### Shutdown + +`shutdown()` signals all owned accept loops (quinn, iroh, TCP+TLS) to +stop, waits for in-flight dispatched handlers with a drain timeout, +then forcefully closes remaining connections. One owner, one shutdown +— no external loop coordination. SIGTERM/SIGINT are wired to the +shutdown channel by the assembly layer (the deployment binary). + +### What `alknet-endpoint` does NOT do + +- **No transport construction.** The endpoint takes pre-built + transports via builder methods. The assembly layer builds them + (`TlsServerConfig::for_quinn()` → `quinn::Endpoint::server()`, etc.). +- **No TLS config.** The endpoint does not depend on `alknet-tls`. TLS + configs are built by the assembly layer; the endpoint receives the + resulting transport endpoints. +- **No protocol logic.** The endpoint dispatches by ALPN; the handler + runs the protocol. The endpoint does not know what `alknet/call` or + `alknet/channels` means — it looks up the ALPN in the registry and + spawns the handler. + +### Feature gates + +```toml +[features] +default = [] +quinn = ["dep:quinn", "alknet-core/quinn"] # with_quinn — quinn accept loop +iroh = ["dep:iroh", "alknet-core/iroh"] # with_iroh — iroh accept loop +tcp = ["dep:tokio-rustls"] # with_tcp_tls — TCP+TLS accept loop +``` + +The `quinn`/`iroh` features pull the corresponding features on +`alknet-core` (for `Connection::from_quinn` / `from_iroh` — the +constructors stay in core; see ADR-083 §"The `quinn` feature split"). A +deployment enables the features for the transports it runs. A +pure-QUIC node enables `quinn` + `iroh`; a hub serving HTTPS enables +`quinn` + `tcp`; a hub-worker enables all three. + +### Dependencies + +``` +alknet-endpoint +├── alknet-core (Connection, ProtocolHandler, AuthContext, +│ IdentityProvider, DynamicConfig) +├── quinn (optional — quinn accept loop) +├── iroh (optional — iroh accept loop) +├── tokio-rustls (optional — TCP+TLS accept loop, tcp feature) +├── tokio (spawn, watch, TcpListener) +├── arc-swap (DynamicConfig) +└── tracing (logging) +``` + +`alknet-endpoint` depends on `alknet-core` (for `Connection`, +`ProtocolHandler`, `AuthContext`, `IdentityProvider`, `DynamicConfig`). +`HandlerRegistry` and `EndpointError` live in `alknet-endpoint` (they +move with the endpoint from core). The endpoint does **not** depend on +`alknet-tls` — it takes pre-built transports, so TLS config +construction stays at the assembly layer. + +### Crate dependencies (in the dep graph) + +``` +alknet-endpoint +└── alknet-core (Connection, ProtocolHandler, AuthContext, IdentityProvider, DynamicConfig) + +alknet-hub (uses AlknetEndpoint for inbound) +├── alknet-endpoint (the accept-loop runner) +├── alknet-client (the dial — for outbound worker dials, ADR-089) +├── alknet-channels-call (ChannelsAdapter — registered on the HandlerRegistry) +├── alknet-call (CallAdapter, Dispatcher) +├── alknet-http (HttpAdapter) +└── alknet-core (shared types) + +alknet-worker (uses AlknetEndpoint if it accepts inbound) +├── alknet-endpoint (if the worker accepts inbound — a hub-worker) +├── alknet-client (the dial — to reach the hub, ADR-089) +└── alknet-core (shared types) +``` + +`alknet-call`, `alknet-http`, `alknet-tty`, and other handler crates do +**not** depend on `alknet-endpoint`. They depend on `alknet-core` for +`ProtocolHandler` and `Connection`; the endpoint dispatches to them via +the trait, without a dependency edge. This is the dep-weight win: a +handler crate no longer transitively links quinn, iroh, rcgen, or +rustls-acme. + +## Assembly layer integration + +A downstream hub uses `alknet-endpoint` like this: + +```rust +// 1. Build the HandlerRegistry — register all handlers by ALPN. +let mut registry = HandlerRegistry::new(); +registry.register(Arc::new(channels_adapter)); // alknet/channels +registry.register(Arc::new(http_adapter)); // h2, http/1.1 + +// 2. Build the TlsServerConfig(s) via alknet-tls (assembly layer). +let raw_key_tls = TlsServerConfig::new(&raw_key_identity, &native_alpns).await?; +let x509_tls = TlsServerConfig::new(&x509_identity, &web_alpns).await?; + +// 3. Build the transport endpoints from the TLS configs. +let quinn_endpoint = raw_key_tls.for_quinn()?.into_endpoint(listen_addr)?; +let tcp_listener = TcpListener::bind(web_addr).await?; +let tls_acceptor = x509_tls.for_tcp_tls(); + +// 4. Construct the endpoint with all owned transports. +let endpoint = Arc::new( + AlknetEndpoint::new(registry, dynamic, identity_provider, drain_timeout) + .with_quinn(quinn_endpoint) + .with_tcp_tls(tcp_listener, tls_acceptor), +); + +// 5. Run — all accept loops run inside run(), shutdown() stops them all. +endpoint.clone().run().await; +``` + +The endpoint takes the pre-built transports; the assembly layer built +them from `alknet-tls`'s `TlsServerConfig`s. The endpoint does not see +`alknet-tls` — it sees `quinn::Endpoint` and `TlsAcceptor`. + +## What `alknet-core` looks like after the extraction + +Core loses the endpoint module (~1600 LOC) and 5 heavy deps (`quinn`, +`iroh`, `rcgen`, `rustls-pemfile`, `rustls-acme`). The remaining surface +is the lightweight types+auth+config+ownership+store+fingerprint crate. +See [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) +§"Amendment 2026-07-15 — crate extraction" §"What `alknet-core` looks +like after" for the module-level table and the `quinn` feature split +(`Connection::from_quinn` stays in core; the accept loop moves here). + +## Design Decisions + +All design decisions are documented as ADRs in +[decisions/](../../decisions/). + +| ADR | Decision | Summary | +|-----|----------|---------| +| [083](../../decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as multi-transport accept-loop runner + crate extraction | `AlknetEndpoint` takes no TLS config; `with_quinn`/`with_iroh`/`with_tcp_tls` builder methods; public `dispatch` for SSH/WT; extracted from `alknet-core` into `alknet-endpoint` (Amendment 2026-07-15) | +| [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 | +| [010](../../decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | `HandlerRegistry`, accept loop, static registration (amended by ADR-083) | + +## Open Questions + +See [open-questions.md](../../open-questions.md) for full details. + +- **OQ-60** (resolved): Where does transport construction live? The + TCP+TLS accept loop lives in `alknet-endpoint` behind a `tcp` feature + as an owned transport. Builder functions are inlined by the assembly + layer. See [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md). +- **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. + +## References + +- [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) — + the decision this spec implements (including the Amendment + 2026-07-15 crate extraction) +- [ADR-082](../../decisions/082-alknet-tls-extraction.md) — + `TlsServerConfig` (the TLS config the assembly layer builds; the + endpoint does not see it) +- [ADR-086](../../decisions/086-endpoint-types-and-entry-points.md) — + endpoint types (web/native/iroh); entry-point vs. endpoint ALPN +- [ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) — + `alknet-client` (the client-side analogue — symmetric extraction) +- [ADR-065](../../decisions/065-connection-from-stream-generic-single-stream.md) + — `Connection::from_stream` / `from_bidi` (the TCP+TLS path) +- [ADR-070](../../decisions/070-bidistreamsource-trait.md) — + `BidiStreamSource` (the `Connection` extension point) +- [`crates/core/endpoint.md`](../core/endpoint.md) — the endpoint + design (will be updated to reflect the extraction; the endpoint + semantics stay, the location moves) +- [`crates/core/core-types.md`](../core/core-types.md) — + `ProtocolHandler`, `Connection`, `AuthContext` (the shared types the + endpoint imports from core) +- [`crates/client/README.md`](../client/README.md) — `alknet-client` + (the client-side complement) +- [`crates/tls/README.md`](../tls/README.md) — `TlsServerConfig` (the + assembly-layer TLS config that produces the transports the endpoint + takes) +- [`crates/hub/README.md`](../hub/README.md) — the hub (the first + multi-transport consumer of the endpoint) \ No newline at end of file diff --git a/docs/architecture/crates/hub/README.md b/docs/architecture/crates/hub/README.md index e65f000..f229ec2 100644 --- a/docs/architecture/crates/hub/README.md +++ b/docs/architecture/crates/hub/README.md @@ -657,15 +657,18 @@ alknet-hub (Hub struct deps) │ from_call, FromCallConfig, AdapterError, ClientError) ├── alknet-http (HttpAdapter — for the registration endpoint and browser access) ├── alknet-core (IdentityProvider, Connection, OperationRegistry, -│ HandlerRegistry, AuthContext) +│ AuthContext) ├── tokio (spawn, time::sleep) └── tracing (logging) (assembly-layer deps — used by the hub's composition code, not by the Hub struct itself) + ├── alknet-endpoint (AlknetEndpoint with quinn + iroh + tcp features, + │ │ HandlerRegistry — the accept-loop runner) + ├── alknet-client (AlknetClient — outbound worker dials, ADR-089) ├── 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-core [quinn/iroh features] (Connection::from_quinn/from_iroh) ``` `alknet-hub` depends on `alknet-channels-call`, `alknet-call`, and diff --git a/docs/architecture/crates/tls/README.md b/docs/architecture/crates/tls/README.md index 10a028d..df42d59 100644 --- a/docs/architecture/crates/tls/README.md +++ b/docs/architecture/crates/tls/README.md @@ -364,20 +364,25 @@ endpoint section below ("What `AlknetEndpoint` does after the refactor") describes the **post-refactor target**, not the current source. The current `crates/alknet-core/src/endpoint.rs` is the **extraction source** — `AlknetEndpoint::new(static_config, ...)` builds TLS -internally, the shape ADR-083 replaces. The extraction and refactor -are **sequenced**, not simultaneous: +internally, the shape ADR-083 replaces. The endpoint is extracted into +a new crate `alknet-endpoint` (ADR-083 Amendment 2026-07-15) as part of +this work. The extraction and refactor are **sequenced**, not +simultaneous: 1. **`alknet-tls` first** — build the crate in isolation. `TlsServerConfig` and `TlsClientConfig` are unit-testable against `TlsIdentity` without touching the endpoint. This is the greenfield step. -2. **Endpoint refactor second** — `endpoint.rs` is refactored to the - ADR-083 shape (`new(handlers, dynamic, identity_provider, drain_timeout)` - + `with_quinn` / `with_iroh` / `with_tcp_tls`), importing from - `alknet-tls` instead of building TLS internally. The call sites move - to the assembly layer here. +2. **`alknet-endpoint` second** — build the new endpoint crate fresh + against the ADR-083 shape (`new(handlers, dynamic, + identity_provider, drain_timeout)` + `with_quinn` / `with_iroh` / + `with_tcp_tls`), importing `Connection`/`ProtocolHandler`/`AuthContext` + from `alknet-core` and taking pre-built transports (no TLS config — + the assembly layer builds those via `alknet-tls`). The old + `crates/alknet-core/src/endpoint.rs` is deleted. 3. **Assembly layer last** — the deployment binary (hub/worker) builds the `TlsServerConfig`(s) and `TlsClientConfig`(s), the transports, and - hands them to `AlknetEndpoint` via the builder methods. + hands them to `AlknetEndpoint` (in `alknet-endpoint`) via the builder + methods. A compilable intermediate state exists after step 1: `alknet-tls` built and tested standalone, with `endpoint.rs` still in its old shape. The @@ -385,11 +390,12 @@ call sites for `TlsServerConfig` / `TlsClientConfig` do not exist until step 2/3 — an implementer testing step 1 writes tests against the TLS types directly, not against a wired-up endpoint. -### What `AlknetEndpoint` does after the refactor +### What `AlknetEndpoint` (in `alknet-endpoint`) does after the refactor `AlknetEndpoint::new()` currently builds `TlsSetup` internally. After the refactor (see [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md)), -the endpoint takes **no TLS config at all** — it is a multi-transport +the endpoint (extracted into `alknet-endpoint` per ADR-083 Amendment +2026-07-15) takes **no TLS config at all** — it is a multi-transport accept-loop runner. TCP+TLS is an owned transport (via `with_tcp_tls`), not an external loop: @@ -449,11 +455,12 @@ shutdown is single-owner — the endpoint owns all its accept loops `alknet-tls` provides `for_tcp_tls() -> TlsAcceptor`. The actual TCP accept loop (`TcpListener::accept` → `TlsAcceptor::accept` → -`Connection::from_bidi` → `endpoint.dispatch()`) lives in `alknet-core` -behind a `tcp` feature, as an owned transport on `AlknetEndpoint` (via -`with_tcp_tls(listener, acceptor)` — see ADR-083). `alknet-tls` is the -cert provider, not the accept loop. This keeps `alknet-tls` focused on -TLS setup and cert sharing, not transport accept logic. +`Connection::from_bidi` → `endpoint.dispatch()`) lives in +`alknet-endpoint` behind a `tcp` feature, as an owned transport on +`AlknetEndpoint` (via `with_tcp_tls(listener, acceptor)` — see ADR-083, +Amendment 2026-07-15). `alknet-tls` is the cert provider, not the +accept loop. This keeps `alknet-tls` focused on TLS setup and cert +sharing, not transport accept logic. ### Client-side — `TlsClientConfig` (ADR-087) @@ -654,10 +661,12 @@ alknet-call (client-side verifier — unchanged) alknet-hub (multi-transport endpoint) ├── alknet-tls (TlsServerConfig — shared across quinn + TCP) +├── alknet-endpoint (AlknetEndpoint with quinn + iroh + tcp features, HandlerRegistry) +├── alknet-client (AlknetClient — outbound worker dials, ADR-089) ├── alknet-channels-call (ChannelClient) ├── alknet-call (CallAdapter, Dispatcher) ├── alknet-http (HttpAdapter) -├── alknet-core (AlknetEndpoint with quinn + iroh + tcp features, HandlerRegistry, Connection) +├── alknet-core (Connection, ProtocolHandler, AuthContext, IdentityProvider) ``` `alknet-tls` depends on `alknet-core` only. No handler crate depends on @@ -694,8 +703,8 @@ See [open-questions.md](../../open-questions.md) for full details. `rustls::sign` usage is a test helper. `alknet-tls` re-exports the fingerprint functions for convenience. - **OQ-60** (resolved): Where does transport construction live? The - TCP+TLS accept loop lives in `alknet-core` behind a `tcp` feature as - an owned endpoint transport (`with_tcp_tls`). Builder functions are + TCP+TLS accept loop lives in `alknet-endpoint` behind a `tcp` feature + as an owned endpoint transport (`with_tcp_tls`). Builder functions are inlined by the assembly layer. See ADR-083. - **OQ-61** (dissolved): Multi-owner shutdown coordination. The problem does not arise — the endpoint owns all its accept loops @@ -774,8 +783,9 @@ named by ADR-089; its wire protocol is deferred (OQ-66). - `docs/architecture/decisions/087-tlsclientconfig-not-blocked-on-dial.md` — `TlsClientConfig` (client-side); not blocked on the dial seam; breaks the circular hedge; hub-as-client requirement -- `docs/architecture/crates/core/endpoint.md` — current endpoint design - (TLS section will be amended to point to `alknet-tls`) +- `docs/architecture/crates/endpoint/README.md` — `AlknetEndpoint` + (the endpoint spec; TLS config is built by `alknet-tls`, not the + endpoint — per ADR-083) - `docs/architecture/crates/core/config.md` — `TlsIdentity`, `StaticConfig` - `crates/alknet-core/src/endpoint.rs` — the server-side code being extracted (`build_rustls_server_config`, `TlsSetup`, `RawKeyCertResolver`, diff --git a/docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md b/docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md index 72c450e..cc35472 100644 --- a/docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md +++ b/docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md @@ -10,6 +10,12 @@ WebTransport streams. See §"TCP+TLS is a first-class owned transport". All TLS-scope OQs resolved; advanced to Accepted ahead of the task-decomposition session.) +Accepted (amended 2026-07-15: the endpoint is extracted from +`alknet-core` into a new crate `alknet-endpoint`. The ADR's shape +(`new` + builder methods + public `dispatch` + `run` / `shutdown`) is +unchanged; the *location* changes. See §"Amendment 2026-07-15 — crate +extraction".) + ## Context `AlknetEndpoint` (ADR-010) conflates two concerns: @@ -136,9 +142,9 @@ that's primarily the hub. A pure worker (no inbound endpoints) has a trivial assembly layer; a hub-worker combines both. Putting hub-specific composition in the hub crate is appropriate; putting transport loops that any node might need in the hub crate is not. The -TCP+TLS loop belongs in `alknet-core` (behind a `tcp` feature), where -any node that wants it can enable the feature and call `with_tcp_tls` — -no hub dependency. +TCP+TLS loop belongs in `alknet-endpoint` (behind a `tcp` feature), +where any node that wants it can enable the feature and call +`with_tcp_tls` — no hub dependency. ## Decision @@ -251,11 +257,11 @@ iroh loops. It is not an external sibling. This means: accepts inbound TCP+TLS enables the `tcp` feature and calls `with_tcp_tls(listener, acceptor)`. No hub dependency. The loop isn't duplicated per binary. -- **The TCP+TLS loop's home is `alknet-core`** (behind a `tcp` feature), - not the hub crate or the assembly layer. Core already has `quinn` and - `iroh` as feature-gated transport deps; adding `tcp` (pulling - `tokio-rustls`) is the same pattern. A deployment that doesn't use - TCP+TLS doesn't enable the feature. +- **The TCP+TLS loop's home is `alknet-endpoint`** (behind a `tcp` + feature), not the hub crate or the assembly layer. The endpoint + crate has `quinn` and `iroh` as feature-gated transport deps; adding + `tcp` (pulling `tokio-rustls`) is the same pattern. A deployment that + doesn't use TCP+TLS doesn't enable the feature. ### `dispatch` is public — for genuinely external shapes @@ -328,7 +334,7 @@ The `tcp` feature pulls `tokio-rustls`. A deployment enables the features for the transports it runs. A pure-QUIC node enables `quinn` + `iroh`; a hub serving HTTPS enables `quinn` + `tcp`; a hub-worker enables all three. This matches the existing pattern (`quinn` and -`iroh` are already feature-gated transport deps on `alknet-core`). +`iroh` are already feature-gated transport deps on `alknet-endpoint`). ### `StaticConfig` stays in core; the endpoint drops it @@ -380,11 +386,14 @@ live in the hub crate, not a generic transport crate. | `load_cert_chain()` / `load_private_key()` | `alknet-tls` (ADR-082) | | `build_quinn_server_config_from_rustls()` | `alknet-tls` (`for_quinn()`, ADR-082) | | `build_iroh_endpoint()` | Assembly layer (inlined; 15 lines of iroh API calls) | +| `AlknetEndpoint`, `HandlerRegistry`, `EndpointError`, all dispatch/loop/extraction code | `alknet-endpoint` (this ADR, §"Amendment 2026-07-15") | -### What stays in `alknet-core/endpoint.rs` +### What stays in `alknet-endpoint` (the new crate) - `AlknetEndpoint` struct (multi-transport accept-loop runner + public `dispatch`) - `HandlerRegistry` +- `EndpointError` +- `TcpTlsListener` (the `(TcpListener, TlsAcceptor)` tuple for the TCP+TLS field) - `dispatch` (public — ACME guard, handler lookup, `build_auth_context`, spawn) - `dispatch_quinn` / `dispatch_iroh` / `dispatch_tcp_tls` (private — transport-specific extraction, then call `dispatch`) - `run_quinn_accept_loop` / `run_iroh_accept_loop` / `run_tcp_tls_accept_loop` @@ -402,6 +411,132 @@ to the endpoint via `with_quinn` / `with_iroh` / `with_tcp_tls`. ADR-082 should reference this ADR for the endpoint signature and focus on what `alknet-tls` provides (`TlsServerConfig` and its accessors). +### Amendment 2026-07-15 — crate extraction (`alknet-endpoint`) + +The endpoint is extracted from `alknet-core` into a new crate +`alknet-endpoint`. The ADR's shape — `new(handlers, dynamic, +identity_provider, drain_timeout)` + `with_quinn` / `with_iroh` / +`with_tcp_tls` + public `dispatch` + `run` / `shutdown` — is **unchanged** +by this amendment. Only the *location* changes: `endpoint.rs`, +`HandlerRegistry`, and `EndpointError` move from `alknet-core` to +`alknet-endpoint`. + +#### Why extract (the dependency data) + +`alknet-core` is currently two things welded together with different +audiences: + +| Concern | Modules | Depended on by | +|---------|---------|----------------| +| **Shared types + auth + config** | `types.rs`, `auth.rs`, `config.rs`, `fingerprint.rs`, `ownership.rs`, `store.rs` | every handler crate (call, http, tty, tty-local) — 124 import sites | +| **Endpoint (accept-loop runner)** | `endpoint.rs` (1606 LOC) | **zero handler crates** — only the assembly layer (hub/worker) | + +The handler crates import `alknet_core::types` (57×), `alknet_core::auth` +(41×), `alknet_core::config` (13×), `alknet_core::ownership` (7×), +`alknet_core::fingerprint` (6×). They import `alknet_core::endpoint` +**zero times**. `endpoint.rs` is a one-way leaf consumer of the shared +types (it imports `auth`, `config`, `types`; nothing in core imports +*from* it). This is the textbook shape for an extraction: a leaf +consumer depended on by a different audience than the shared types. + +#### The dependency-weight win + +`alknet-core`'s `Cargo.toml` currently carries `quinn`, `iroh`, +`rustls-pemfile`, `rcgen`, `rustls-acme`, `rustls` — heavy transport/TLS +deps — because `endpoint.rs` uses them. Every handler crate transitively +pulls these via `alknet-core`, even though none of them use the +endpoint. `alknet-tty` is a pure wire-format handler — it has no business +linking quinn. + +After extraction: + +- **`alknet-core`** loses `quinn`, `iroh`, `rcgen`, `rustls-pemfile`, + `rustls-acme` from its `[dependencies]` (the TLS-setup deps already + move to `alknet-tls` per ADR-082; the transport deps move with the + endpoint). It keeps `rustls`/`rustls-pki-types` (narrow — for + `fingerprint.rs` types, per OQ-59), `ed25519-dalek`, `sha2`, `tokio`, + `serde`, `arc-swap`. It becomes a **lightweight types+auth+config + crate** — what the handler crates actually want. +- **`alknet-endpoint`** gains `quinn`, `iroh` (the accept-loop + transports). It depends on `alknet-core` for `Connection`, + `ProtocolHandler`, `AuthContext`, `IdentityProvider`, `DynamicConfig`. + It does **not** depend on `alknet-tls` — the endpoint takes pre-built + transports (per this ADR), so TLS config construction stays in + `alknet-tls` at the assembly layer. + +A handler crate (call, http, tty) no longer transitively links quinn, +iroh, rcgen, or rustls-acme. A pure worker (client-only, no inbound) +depends on `alknet-client` + handler crates + `alknet-core` and does +**not** pull `alknet-endpoint` at all. + +#### The `quinn` feature split + +`alknet-core`'s `quinn` feature currently gates two unrelated things: +(1) `endpoint.rs`'s quinn accept loop and (2) `types.rs`'s +`Connection::from_quinn` / `QuinnBidiStreamSource` constructor. After +extraction: + +- **`Connection::from_quinn` stays in `alknet-core`** (in `types.rs`). + It's a shared-type constructor used by both the endpoint's accept + loop (server) and `alknet-client`'s `dial_quic` (client, ADR-089) and + `CallClient` tests. The `quinn` feature on `alknet-core` gates this + constructor — "does `Connection` support quinn-constructed + connections" — not "does core run a quinn accept loop." Same for + `from_iroh` and the `iroh` feature. +- **The quinn accept loop moves to `alknet-endpoint`**, which has its + own `quinn` feature for the accept loop. The heavy quinn accept-loop + code is in `alknet-endpoint`, not core. + +#### Symmetry with `alknet-client` (ADR-089) + +The extraction mirrors the client-side extraction (ADR-089): + +| Server side | Client side | Audience | +|-------------|-------------|----------| +| `alknet-endpoint` (this ADR) | `alknet-client` (ADR-089) | the assembly layer (hub, worker, hub-worker) | +| `alknet-core` (shared types) | `alknet-core` (shared types) | every handler crate | + +A **hub** depends on `alknet-endpoint` + `alknet-client` + handler +crates + `alknet-core` (transitively). A **pure worker** depends on +`alknet-client` + handler crates + `alknet-core` — no `alknet-endpoint`. +A **handler crate** depends on `alknet-core` only — neither endpoint +nor client. + +#### Implementation: build new, delete old + +The endpoint's `endpoint.rs` is 1606 lines of `#[cfg(feature = "quinn")]` +/ `#[cfg(feature = "iroh")]` / `#[cfg(feature = "acme")]` conditionals — +the TLS-setup code that ADR-082 moves to `alknet-tls`, the +`StaticConfig`-taking constructor that this ADR replaces, the +`acme-tls/1` guard that this ADR moves to shared `dispatch`. Building +`alknet-endpoint` fresh against this ADR's shape — `new()` + builder +methods + public `dispatch` + `run` / `shutdown`, importing +`Connection`/`ProtocolHandler`/`AuthContext` from `alknet-core`, no TLS +setup (that's `alknet-tls`'s job now) — is cleaner than editing the +1606-line conditional file in place. The old `endpoint.rs` becomes a +deletion, not a refactor. This is pruning (cutting the endpoint + the +TLS deps that already move to `alknet-tls`), not a massive inline +refactor. + +#### What `alknet-core` looks like after + +`alknet-core` after the extraction + the ADR-082 TLS pruning: + +| Module | LOC | Stays? | +|--------|-----|--------| +| `types.rs` (`Connection`, `ProtocolHandler`, `BiStream`, `BidiStreamSource`, `StreamError`, `Capabilities`) | ~1100 | yes — shared by every crate | +| `auth.rs` (`AuthContext`, `Identity`, `IdentityProvider`, `IdentityStore`, `AuthToken`) | ~640 | yes — shared by every crate | +| `config.rs` (`StaticConfig`, `DynamicConfig`, `TlsIdentity`, `Ed25519SecretKey`) | ~710 | yes — shared config types | +| `fingerprint.rs` | ~260 | yes — shared by server + client (OQ-59) | +| `ownership.rs` (`OwnershipStore`, `OwnershipProvider`) | ~280 | yes — shared | +| `store.rs` (`CredentialStore`, `EncryptedData`) | ~200 | yes — shared | +| `endpoint.rs` (`AlknetEndpoint`, `HandlerRegistry`, `EndpointError`) | ~1600 | **no** — moves to `alknet-endpoint` | + +Core loses ~1600 LOC (the endpoint) and 5 heavy deps (`quinn`, `iroh`, +`rcgen`, `rustls-pemfile`, `rustls-acme`). The remaining ~3200 LOC is +the lightweight types+auth+config+ownership+store+fingerprint surface +that every handler crate depends on. + ## Consequences **Positive:** @@ -430,23 +565,30 @@ should reference this ADR for the endpoint signature and focus on what - `dispatch` stays public for genuinely external shapes (SSH channels, future WT streams) — transports the endpoint can't own because they're connection-internal multiplexing, not listener-based. +- **The endpoint is extracted into `alknet-endpoint`** (Amendment + 2026-07-15). Handler crates no longer transitively link quinn, iroh, + rcgen, or rustls-acme. A pure worker (client-only) does not pull the + endpoint at all. The dep graph is symmetric with `alknet-client` + (ADR-089): `alknet-core` is the shared types crate; `alknet-endpoint` + and `alknet-client` are the server-side and client-side establishment + crates that depend on it. **Negative:** - `AlknetEndpoint::new` signature changes (breaking). Pre-1.0, in-repo consumers only — the assembly layer and tests must update. Expected; this is the point of the refactor. -- `alknet-core` gains a `tcp` feature (pulls `tokio-rustls`). This is - the same pattern as the existing `quinn` and `iroh` features — a - transport that the endpoint can own. A deployment that doesn't use - TCP+TLS doesn't enable it. `alknet-core` already depends on `rustls` - (for `fingerprint.rs` types, per OQ-59); `tokio-rustls` is the - acceptor wrapper over `rustls::ServerConfig`, not a separate TLS - stack. +- `alknet-endpoint` gains a `tcp` feature (pulls `tokio-rustls`). This is + the same pattern as the `quinn` and `iroh` features — a transport that + the endpoint can own. A deployment that doesn't use TCP+TLS doesn't + enable it. `alknet-core` keeps a narrow `rustls` dep (for + `fingerprint.rs` types, per OQ-59); `tokio-rustls` is the acceptor + wrapper over `rustls::ServerConfig`, not a separate TLS stack, and + lives in `alknet-endpoint`, not core. - `build_iroh_endpoint` leaves core (inlined by the assembly layer). This is a one-way dep-graph change: the binary that uses iroh depends - on `iroh` directly, not via `alknet-core`. This is correct — the - binary *is* the thing that knows which transports it wants. The 15 - lines are pure iroh API calls; no shared logic is lost. + on `iroh` directly, not via `alknet-core` or `alknet-endpoint`. This + is correct — the binary *is* the thing that knows which transports it + wants. The 15 lines are pure iroh API calls; no shared logic is lost. - This ADR revises ADR-010's "TCP is not an endpoint struct concern" more deeply than the original ADR-083 draft. The reason TCP was excluded (the endpoint built transports internally, TCP+TLS couldn't @@ -456,12 +598,14 @@ should reference this ADR for the endpoint signature and focus on what ## Door type **One-way.** The endpoint's `new` signature, the public `dispatch` -contract, the "endpoint owns dispatch, not construction" boundary, and -the "endpoint owns TCP+TLS as a first-class transport" decision are -structural. Reversing would mean re-welding transport construction to -the endpoint, re-privatizing `dispatch`, and pushing TCP+TLS back outside -— breaking every multi-transport consumer (hub, hub-worker, future SSH, -future WT). +contract, the "endpoint owns dispatch, not construction" boundary, the +"endpoint owns TCP+TLS as a first-class transport" decision, and the +extraction of the endpoint into `alknet-endpoint` (a separate crate +from `alknet-core`) are structural. Reversing would mean re-welding +transport construction to the endpoint, re-privatizing `dispatch`, +pushing TCP+TLS back outside, and re-merging the endpoint into core — +breaking every multi-transport consumer (hub, hub-worker, future SSH, +future WT) and re-coupling every handler crate to quinn/iroh/rcgen. The `dispatch` signature (`connection, alpn, fingerprint, remote_addr`) is one-way — changing it after consumers exist is a rewrite. The @@ -492,9 +636,14 @@ tasks, the TCP+TLS loop's internal structure) is two-way. client pattern this ADR mirrors on the server side) - `docs/research/alknet-endpoint-refactor/findings.md` — the analysis that surfaced the conflation and the two-config hub case -- `crates/alknet-core/src/endpoint.rs` — the code being refactored +- `docs/architecture/crates/endpoint/README.md` — the canonical spec for + `alknet-endpoint` (this ADR's amendment is the decision; that spec is + the description) +- `crates/alknet-core/src/endpoint.rs` — the code being extracted into + `alknet-endpoint` (the old module becomes a deletion after the + extraction) - OQ-60: resolved — transport construction is inlined by the assembly layer (the deployment binary); the TCP+TLS loop lives in - `alknet-core` behind a `tcp` feature as an owned transport + `alknet-endpoint` behind a `tcp` feature as an owned transport - OQ-61: dissolved — the multi-owner shutdown problem does not arise; the endpoint owns all its accept loops (quinn, iroh, TCP+TLS) \ No newline at end of file diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 9fc98d4..282f889 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -94,15 +94,17 @@ graph — they depend on the published core crates from their own repos alknet-vault (standalone — foundational to ACL: key derivation, identity) │ ├── Substrate -│ alknet-core ProtocolHandler, endpoint (multi-transport accept loop), -│ │ Connection, BidiStreamSource, AuthContext, IdentityProvider, -│ │ StaticConfig, DynamicConfig +│ alknet-core ProtocolHandler, Connection, BidiStreamSource, AuthContext, +│ │ IdentityProvider, StaticConfig, DynamicConfig, fingerprint +│ │ (endpoint extracted to alknet-endpoint; core is now lightweight +│ │ types+auth+config — no quinn/iroh/rcgen deps) │ ├── alknet-tls TlsServerConfig + TlsClientConfig — shared TLS config across quinn + TCP+TLS + iroh (ADR-082/087) │ ├── 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-client AlknetClient — native client dial seam (QUIC + TCP+TLS + iroh); produces Connection for CallClient/ChannelClient take-over (ADR-089) +│ ├── alknet-client AlknetClient — native client dial seam (QUIC + TCP+TLS + iroh); produces Connection for CallClient/ChannelClient take-over (ADR-089) +│ └── alknet-endpoint AlknetEndpoint — multi-transport accept-loop runner, extracted from core (ADR-083 Am. 2026-07-15); takes pre-built transports; public dispatch for SSH/WT │ ├── Deployment shapes │ ├── alknet-hub channels hub — accepts workers, relays, aggregates (ADR-079); dials workers via AlknetClient @@ -368,7 +370,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/). | [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 | +| [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; endpoint extracted from `alknet-core` into `alknet-endpoint` (Am. 2026-07-15) | | [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) | | [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | `alknet-tls` provides client-side TLS config; not deferred behind OQ-55; breaks the circular hedge; hub-as-client is a first-class use case |