docs(arch): break the AlknetClient circular hedge — TlsClientConfig not blocked on dial (ADR-087, resolves OQ-64)
OQ-64 and OQ-55 were linked in a circular dependency: the client-side TLS config was deferred behind the dial seam (OQ-55), but the dial needs the TLS config. No second transport can dial until it has a TLS config; the TLS config was deferred until a second transport dials. Schrödinger's code — required and not required until observed. ADR-087 breaks the circle by separating two concerns that were conflated as 'the same seam': 1. TlsClientConfig — rustls::ClientConfig + ADR-034 verifier selection + ADR-084 crypto provider. Transport-agnostic. All decisions made. Buildable today. A PREREQUISITE for any dial, not a consequence of it. 2. The dial (AlknetClient::dial()) — transport-specific connection establishment. Extracting a transport-polymorphic dial from one shape (QUIC) would bake QUIC in. Legitimate deferral (OQ-55, unchanged). The hub makes this non-optional: a hub dials out to workers it supervises and to other hubs (hub-as-client). The first hub deployment (web + native) dials workers over QUIC with the worker's fingerprint pinned. There is no 'later' for the TLS config — it is on the critical path for the first hub and for alknet-worker. Changes: - ADR-087: TlsClientConfig in alknet-tls, not blocked on OQ-55 - OQ-64: resolved (yes, alknet-tls provides TlsClientConfig) - OQ-55: amended — only the dial seam is deferred; TLS client config is explicitly NOT part of the deferral - TLS README: 'Server-only (for now)' section replaced with TlsClientConfig section; crate is no longer server-only - Hub README: dial/supervision section references TlsClientConfig for outbound connections
This commit is contained in:
1 parent
7d9b1ebad9
commit
77321a7e84
8 files changed
+502
-140
No files matched your search
@@ -19,41 +19,62 @@
|
||||
and is not the thing being deferred — the shared *dial* is. The deferral
|
||||
is on transport-polymorphism of the dial, not on client count or on the
|
||||
channels protocol's API.
|
||||
- **What is NOT deferred (amended by ADR-087)**: the client-side TLS
|
||||
config (`TlsClientConfig`) is **not** part of this deferral. OQ-64
|
||||
is resolved — `alknet-tls` provides `TlsClientConfig` now, unblocked
|
||||
by this OQ. The TLS config is a **prerequisite** for the dial, not a
|
||||
consequence of it; it is transport-agnostic (ADR-034 verifier
|
||||
selection + ADR-084 provider, both already decided). Each
|
||||
transport-specific dial helper builds its `TlsClientConfig` and
|
||||
passes it to its transport's connector. This OQ defers only the
|
||||
*transport-polymorphic dial extraction* — the shared `AlknetClient::dial()`
|
||||
that picks the transport and calls the right connector. When a second
|
||||
transport's dial exists, the dial seam is extractable; the TLS config
|
||||
is already shared by then.
|
||||
- **Resolution**: Not yet decidable. The shared substance across
|
||||
TLS-carrying transports is ADR-034's verifier-selection rule (PeerEntry
|
||||
presence → fingerprint pin : CA-verify / fail-closed) and the
|
||||
`rustls::ClientConfig` construction — ~20 lines, transport-agnostic in
|
||||
rule. But the *dial* is transport-specific, and we have one of ~5 shapes
|
||||
implemented (QUIC; the others being HTTP, TCP+TLS, WebTransport, raw
|
||||
TCP). Extracting a QUIC-shaped connector to core and naming it
|
||||
`AlknetClient` would bake QUIC in as *the* establishment shape — the
|
||||
same welding ADR-065 unwound on the server side, repeated on the client
|
||||
side. The dial is transport-polymorphic; the shared rule is narrow. Until
|
||||
a second transport's dial exists, the seam between "dial + TLS" (per-
|
||||
transport) and "spawn the dispatcher" (per-crate) is not extractable from
|
||||
two real shapes — it's guessable from one.
|
||||
`rustls::ClientConfig` construction — now centralized in
|
||||
`TlsClientConfig::new` (ADR-087, OQ-64 resolved). What remains
|
||||
transport-specific is the *dial* itself — `quinn::Endpoint::connect`,
|
||||
`TcpStream::connect` + `TlsConnector::connect`, iroh's
|
||||
`Endpoint::connect`. We have one of ~5 shapes implemented (QUIC; the
|
||||
others being HTTP, TCP+TLS, WebTransport, raw TCP). Extracting a
|
||||
QUIC-shaped connector to core and naming it `AlknetClient` would bake
|
||||
QUIC in as *the* establishment shape — the same welding ADR-065
|
||||
unwound on the server side, repeated on the client side. The dial is
|
||||
transport-polymorphic; the shared TLS config is narrow and now
|
||||
extracted (ADR-087). Until a second transport's dial exists, the seam
|
||||
between "dial" (per-transport) and "spawn the dispatcher" (per-crate)
|
||||
is not extractable from two real shapes — it's guessable from one.
|
||||
- **What does NOT block on this**: each crate building its own client
|
||||
standalone, and each transport-specific dial helper. `CallClient`'s
|
||||
transport-agnostic take-over (`spawn_dispatch`) and `ChannelClient`'s
|
||||
transport-agnostic take-over (`from_connection`, ADR-080) are decided;
|
||||
`connect_quic` dials QUIC and calls `from_connection`. The SSH crate's
|
||||
TCP client, the HTTP call client, a `connect_tcp_tls` / `connect_webtransport` helper — each builds its own dial standalone. Core
|
||||
already permits all of this — `Connection::from_stream` / `from_bidi`
|
||||
(ADR-065) handles the non-QUIC transport on the server side, and nothing
|
||||
prevents a client from constructing a `Connection` the same way after
|
||||
its own transport-specific dial. The friction is duplicated boilerplate
|
||||
(each dial helper rebuilds verifier selection), not a missing
|
||||
capability and not a QUIC-welded client API. The bidirectionality
|
||||
criterion (a crate needs a Client type when (a) the endpoint has
|
||||
protocol-level authority — e.g., channels' id allocation — or (b) the
|
||||
protocol needs a reliable establishment interface) is met by each crate
|
||||
independently; `AlknetClient` is the eventual *shared dial+TLS seam*,
|
||||
not a prerequisite for any single client to exist.
|
||||
- **Cross-references**: ADR-034 (verifier selection — currently in
|
||||
`CallClient`, would move to the extracted seam when this is resolved),
|
||||
ADR-065 (server-side transport generalization — the client-side analogue
|
||||
this OQ's deferral avoids preempting), ADR-070 (the `BidiStreamSource`
|
||||
extension point, which is the *Connection* opening and is orthogonal to
|
||||
the *client* establishment question), OQ-CH-14 in
|
||||
standalone with a shared `TlsClientConfig` (ADR-087), and each
|
||||
transport-specific dial helper. `CallClient`'s transport-agnostic
|
||||
take-over (`spawn_dispatch`) and `ChannelClient`'s transport-agnostic
|
||||
take-over (`from_connection`, ADR-080) are decided; `connect_quic`
|
||||
dials QUIC and calls `from_connection`. The SSH crate's TCP client,
|
||||
the HTTP call client, a `connect_tcp_tls` / `connect_webtransport`
|
||||
helper — each builds its `TlsClientConfig` (ADR-087) and its own dial
|
||||
standalone. Core already permits all of this —
|
||||
`Connection::from_stream` / `from_bidi` (ADR-065) handles the
|
||||
non-QUIC transport on the server side, and nothing prevents a client
|
||||
from constructing a `Connection` the same way after its own
|
||||
transport-specific dial. The friction is the duplicated dial
|
||||
boilerplate (each dial helper calls its transport's connector), not
|
||||
duplicated TLS config (that is shared via ADR-087). The
|
||||
bidirectionality criterion (a crate needs a Client type when (a) the
|
||||
endpoint has protocol-level authority — e.g., channels' id allocation
|
||||
— or (b) the protocol needs a reliable establishment interface) is
|
||||
met by each crate independently; `AlknetClient` is the eventual
|
||||
*shared dial seam*, not a prerequisite for any single client to
|
||||
exist.
|
||||
- **Cross-references**: ADR-034 (verifier selection — centralized in
|
||||
`TlsClientConfig::new` per ADR-087), ADR-087 (`TlsClientConfig` is
|
||||
not blocked on this OQ — the TLS config is extracted; only the dial
|
||||
remains deferred), ADR-065 (server-side transport generalization —
|
||||
the client-side analogue this OQ's deferral avoids preempting),
|
||||
ADR-070 (the `BidiStreamSource` extension point, which is the
|
||||
*Connection* opening and is orthogonal to the *client* establishment
|
||||
question), OQ-CH-14 in
|
||||
`docs/research/alknet-channels/phase-0-findings.md` (the research-scope
|
||||
question this core-scope OQ carries forward).
|
||||
@@ -1,55 +1,60 @@
|
||||
# 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
|
||||
for now" section flagged 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.
|
||||
provider — previously enforced by convention, not shared code).
|
||||
- **Status**: resolved
|
||||
- **Door type**: one-way (`TlsClientConfig` as the shared client-side
|
||||
TLS config in `alknet-tls` is structural — every outbound-dialing
|
||||
crate depends on it. Reversing would re-distribute verifier selection
|
||||
+ provider wiring across crates.)
|
||||
- **Priority**: high (upgraded from medium — the hub-as-client
|
||||
requirement makes this a prerequisite for the first hub deployment,
|
||||
not a future extraction)
|
||||
- **Resolution**: **Yes. `alknet-tls` provides `TlsClientConfig`.** It
|
||||
is not blocked on the dial-seam extraction (OQ-55).
|
||||
|
||||
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.
|
||||
The previous deferral linked OQ-64 and OQ-55 as "the same seam,"
|
||||
creating a circular dependency: the TLS config is deferred behind the
|
||||
dial, but the dial needs the TLS config. The circle is broken by
|
||||
separating the two concerns:
|
||||
|
||||
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)
|
||||
1. **`TlsClientConfig`** — the `rustls::ClientConfig` + ADR-034
|
||||
verifier selection + ADR-084 crypto provider. Transport-agnostic.
|
||||
All decisions are made. Buildable today. It is a **prerequisite**
|
||||
for any dial, not a consequence of it.
|
||||
2. **The dial** (`AlknetClient::dial()`) — transport-specific
|
||||
connection establishment. Extracting a transport-polymorphic dial
|
||||
from one shape (QUIC) would bake QUIC in. **Legitimate deferral
|
||||
(OQ-55, unchanged).**
|
||||
|
||||
`TlsClientConfig::new` takes a verifier context (the inputs to
|
||||
ADR-034's rule: `PeerEntry` presence, expected fingerprint, remote
|
||||
cert type) and returns a `rustls::ClientConfig`. The caller
|
||||
(transport-specific dial helper, `CallClient::connect_quic`, a future
|
||||
`connect_tcp_tls`) passes it to the transport's connector. Iroh is
|
||||
the exception — it has its own TLS and does not consume a
|
||||
`rustls::ClientConfig`; the iroh dial helper applies the same
|
||||
ADR-034 rule via iroh's `NodeId` verification API.
|
||||
|
||||
The hub makes this non-optional: a hub dials out to workers it
|
||||
supervises and to other hubs (hub-as-client). The hub's
|
||||
`dial_worker_connection` / `supervise_worker` need a
|
||||
`TlsClientConfig` for the outbound dial. The first hub deployment
|
||||
(web + native) dials workers over QUIC with the worker's fingerprint
|
||||
pinned. There is no "later" — it is on the critical path for the
|
||||
first hub and for `alknet-worker`.
|
||||
|
||||
See [ADR-087](../decisions/087-tlsclientconfig-not-blocked-on-dial.md)
|
||||
for the full decision, including the circular-hedge analysis, the
|
||||
hub-as-client requirement, and the iroh exception.
|
||||
- **Cross-references**: [ADR-087](../decisions/087-tlsclientconfig-not-blocked-on-dial.md)
|
||||
(the decision), [ADR-034](../decisions/034-outgoing-only-x509-and-three-peer-roles.md)
|
||||
§3 (verifier selection — the rule `TlsClientConfig::new`
|
||||
centralizes), [ADR-084](../decisions/084-aws-lc-rs-crypto-provider.md)
|
||||
(provider consistency — enforced by code, not convention, for the
|
||||
client side), [ADR-082](../decisions/082-alknet-tls-extraction.md)
|
||||
(`TlsServerConfig` — the server-side analogue), OQ-55 (the dial seam
|
||||
— remains deferred; this OQ's resolution does not affect it),
|
||||
OQ-63 (`TlsError` shape — now covers both server and client variants)
|
||||
Reference in new issue
Block a user