docs(arch): workspace scope correction (ADR-085) + tls spec review fixes
ADR-085 records the actual workspace 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). Consumer repos (docker, agent) are separate repos depending on the published core crates. The overview's crate graph had been describing the wrong scope since ADR-003 — a flat ~12-crate workspace including DNS/messaging/NAPI while omitting channels, hub, worker, and tls. This stale scope was a causal factor in the 'assembly layer' hedging pattern: when the overview implies everything lives in one repo but the architecture needs a hub/worker composition layer not in the graph, the gap gets filled with 'assembly layer' as an escape hatch. The overview is rewritten to match the real boundary. TLS spec review fixes (from architecture review): - C3: hub/worker/hub-worker terminology pointers (tls README + endpoint.md) - W1: server-only statement + OQ-64 (client-side TLS helper, deferred) - W2: ACME task lifecycle semantics (returns immediately, no first-cert await) - W3: remove stale EndpointError::TlsConfig variant - W4: update stale ALPN section for two-config hub - W5: add alknet-tls to hub dep graph (assembly-layer dep) - W6: trim inline rationale -> point to ADR-084 - W7: ADR-084 status dependency note New open questions: - OQ-62: ALPN list sharing for two-config hub (open, high) - OQ-63: TlsError shape (open, high) - OQ-64: client-side TLS helper (deferred, blocked on OQ-55)
This commit is contained in:
1 parent
34729c7846
commit
7610ec1f31
11 files changed
+761
-82
No files matched your search
@@ -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<u8>]` — the caller decides), ADR-083 (the assembly
|
||||
layer builds transports; ALPN-list construction is its job),
|
||||
OQ-64 (client-side TLS helper — related but orthogonal; this OQ is
|
||||
server-side advertisement)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user