docs(arch): tighten tls/endpoint/client specs — remove connect/connect_quic, move CallCredentials to core, shed alknet-call TLS deps
Review of the three new crates (alknet-tls, alknet-endpoint, alknet-client) + revised core found compile-blocking inconsistencies, stale claims, and dep-graph contradictions. All resolved: Critical: - C1: CallClient::connect / ChannelClient::connect_quic REMOVED (not delegated) — keeping them as thin wrappers over AlknetClient::dial_quic would make protocol crates depend on alknet-client, contradicting the dep graph. Callers compose dial + take-over (2 lines). - C2: alknet-client feature gates now pull alknet-core/quinn + alknet-core/iroh (for Connection::from_quinn_with_alpn / from_iroh). - C3: rustls-native-certs + webpki-roots added to alknet-tls deps (always-present, not feature-gated — CA-verify path is transport-agnostic). Warning: - W1: CallCredentials/RemoteIdentity moved to alknet-core (from alknet-call) — the dial must not depend on the call protocol; not a two-way-door, it determines the dep graph. - W2: webpki-roots fallback implemented in spec (ADR-088 §5 added) — the code claimed a fallback that never existed; now the store is never empty, NoRootAnchors unreachable, containerized deployments work. - W3: EndpointError removed entirely (BindFailed + HandlerNotFound both vestigial after ADR-083); shutdown() is now infallible. - W4: FingerprintPinVerifier moved to alknet-tls (from alknet-call) — alknet-call sheds quinn/rustls/rustls-pemfile/rustls-native-certs entirely; CallClient becomes a pure protocol crate. Plus: ClientError removed (only produced by removed connect); S1 (CallCredentials → ClientVerifierContext mapping + auth_token stripped at TLS boundary documented); amendment notes on ADR-017, ADR-069, ADR-080, ADR-082, ADR-087, ADR-090; overview crate graph + README index updated. 29 files, consistency-reviewed.
This commit is contained in:
1 parent
34e3be2801
commit
bf6ce957c0
29 files changed
+524
-267
No files matched your search
@@ -152,7 +152,16 @@ This reframes the connectivity model. The quinn and iroh paths are not distingui
|
||||
|
||||
### Error taxonomy
|
||||
|
||||
> **`EndpointError` is removed** per ADR-083 (Amendment 2026-07-15 +
|
||||
> the `EndpointError`-removal amendment). `BindFailed` is vestigial
|
||||
> (the endpoint takes pre-bound transports); `TlsConfig` is removed
|
||||
> (the endpoint takes no TLS config); `HandlerNotFound` is swallowed
|
||||
> by `dispatch` (close + log, not an error). `shutdown()` is
|
||||
> infallible. The sketch below is the historical shape; it does not
|
||||
> survive into `alknet-endpoint`. `HandlerError` is unchanged.
|
||||
|
||||
```rust
|
||||
// HISTORICAL — removed per ADR-083. See the note above.
|
||||
pub enum EndpointError {
|
||||
BindFailed(io::Error),
|
||||
TlsConfig(io::Error),
|
||||
@@ -167,8 +176,11 @@ pub enum HandlerError {
|
||||
}
|
||||
```
|
||||
|
||||
- `EndpointError`: Problems starting or running the endpoint. Fatal — the endpoint cannot accept connections.
|
||||
- `HandlerError`: Problems within a handler's `handle()` method. Non-fatal — the connection is closed, but the endpoint keeps running.
|
||||
- ~~`EndpointError`~~: **removed** (ADR-083). The endpoint takes
|
||||
pre-built transports and swallows no-handler matches; `shutdown()` is
|
||||
infallible.
|
||||
- `HandlerError`: Problems within a handler's `handle()` method.
|
||||
Non-fatal — the connection is closed, but the endpoint keeps running.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (amended 2026-06-26 and 2026-07-13 — see "Amendments" below)
|
||||
Accepted (amended 2026-06-26, 2026-07-13, and 2026-07-16 — see "Amendments" below; the 2026-07-16 amendment per ADR-089 §5 removes `CallClient::connect`)
|
||||
|
||||
## Context
|
||||
|
||||
@@ -468,17 +468,21 @@ existing code (which already has the right structure —
|
||||
shared dispatch loop, returns a live `CallConnection`. Mirrors the
|
||||
server-side `CallAdapter::handle(Connection)` and
|
||||
`ChannelClient::from_connection` (ADR-080).
|
||||
- **`CallClient::connect(addr, credentials)`** — a QUIC convenience
|
||||
constructor: dial QUIC (feature-gated on `quinn`), then
|
||||
`spawn_dispatch`. Additive and two-way-door. `connect_tcp_tls`,
|
||||
`connect_webtransport`, etc. join it as transports are added,
|
||||
without touching the `spawn_dispatch` contract.
|
||||
- **`CallClient::connect(addr, credentials)`** — ~~a QUIC convenience
|
||||
constructor~~ **REMOVED per ADR-089 §5 (2026-07-16)**. The dial is
|
||||
centralized in `AlknetClient` (`alknet-client`); `connect` is
|
||||
deleted, not retained as a two-way-door convenience, to avoid
|
||||
`alknet-call` depending on `alknet-client` and to let `alknet-call`
|
||||
shed its TLS/transport deps. Callers compose
|
||||
`AlknetClient::dial_quic(...).await?` +
|
||||
`CallClient::new(...).spawn_dispatch(conn)`.
|
||||
|
||||
The door-type classification is unchanged: `spawn_dispatch` is one-way
|
||||
(the handler-facing surface), `connect` is two-way (additive
|
||||
convenience). The `AlknetClient` extraction (OQ-55 — the shared
|
||||
dial+TLS seam) remains deferred; what is deferred is the shared *dial*,
|
||||
not a QUIC-welded client API. `spawn_dispatch` is decided now.
|
||||
The door-type classification is updated: `spawn_dispatch` is one-way
|
||||
(the handler-facing surface); ~~`connect` is two-way (additive
|
||||
convenience)~~ `connect` is **removed** (ADR-089 §5). The
|
||||
`AlknetClient` extraction (OQ-55) is **resolved** by ADR-089 — the
|
||||
shared dial seam is `alknet-client`; `spawn_dispatch` is the
|
||||
protocol-crate take-over that consumes the dial's `Connection`.
|
||||
|
||||
See [client-and-adapters.md](../crates/call/client-and-adapters.md)
|
||||
§"CallClient" for the reframed operational spec.
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
Proposed (amended 2026-07-16 by ADR-089 §5: `CallClient::connect()` is
|
||||
removed; `from_call` is now called by the assembly layer after
|
||||
`AlknetClient::dial_*` + `CallClient::new(...).spawn_dispatch(conn)`,
|
||||
not after `connect()`. The manual-free-function decision stands; the
|
||||
`connect()` references in the body are the pre-ADR-089 shape.)
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -2,7 +2,23 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (amended 2026-07-12 — see "Amendment: transport-agnostic API" below)
|
||||
Accepted (amended 2026-07-12 — see "Amendment: transport-agnostic API"
|
||||
below; amended 2026-07-16 — `connect_quic` removed per ADR-089 §5, see
|
||||
"Amendment: `connect_quic` removed" below)
|
||||
|
||||
## Amendment: `connect_quic` removed (2026-07-16, per ADR-089 §5)
|
||||
|
||||
The `connect_quic(addr, credentials)` convenience constructor is
|
||||
**removed**. Keeping it as a thin wrapper over
|
||||
`AlknetClient::dial_quic` would make `alknet-channels-call` depend on
|
||||
`alknet-client`, contradicting the dep graph (the protocol crates are
|
||||
parallel to the dial, not downstream of it). Callers compose
|
||||
`AlknetClient::dial_quic(...).await?` + `ChannelClient::from_connection(conn).await?`.
|
||||
The `from_connection` primary constructor (the 2026-07-12 amendment
|
||||
below) is unchanged and remains the one-way-door surface. The
|
||||
`connect_quic` references in the body of this ADR are the historical
|
||||
shape; they do not survive into the implementation. See ADR-089 §5 for
|
||||
the full rationale and the breaking-change acknowledgment.
|
||||
|
||||
## Amendment: transport-agnostic API (2026-07-12)
|
||||
|
||||
|
||||
@@ -130,8 +130,8 @@ impl TlsServerConfig {
|
||||
| `TlsIdentity` enum | `alknet-core/config.rs` | Config type — `StaticConfig` holds it |
|
||||
| `Ed25519SecretKey` | `alknet-core/config.rs` | Config type — iroh reads it directly |
|
||||
| `AcmeDirectory` | `alknet-core/config.rs` | Config type |
|
||||
| `fingerprint.rs` | `alknet-core` | Shared by server (endpoint) and client (`alknet-call`'s `FingerprintPinVerifier`) — moving it would create a dep edge from `alknet-call` to `alknet-tls`. Production code uses `sha2` + manual DER only; `rustls` is test-only. See OQ-59. |
|
||||
| `AlknetEndpoint` | `alknet-core` | The endpoint struct stays; it takes no TLS config (see ADR-083) |
|
||||
| `fingerprint.rs` | `alknet-core` | Shared by server (endpoint) and client (`FingerprintPinVerifier`, now in `alknet-tls` per ADR-089 §5 — the original dep-edge concern from `alknet-call` is dissolved) — production code uses `sha2` + manual DER only; `rustls` is test-only. See OQ-59. |
|
||||
| `AlknetEndpoint` | `alknet-endpoint` | Extracted from `alknet-core` per ADR-083 Am. 2026-07-15; takes no TLS config (see ADR-083) |
|
||||
|
||||
### Feature gates
|
||||
|
||||
@@ -240,9 +240,10 @@ that compiles and passes type-checks but silently changes TLS behavior:
|
||||
[ADR-083](083-endpoint-as-accept-loop-runner.md)). This is expected —
|
||||
it is the point of the extraction.
|
||||
- `alknet-core` may keep a narrow `rustls` dep if `fingerprint.rs` stays
|
||||
(OQ-59). If `fingerprint.rs` moves to `alknet-tls`, `alknet-call`'s
|
||||
client-side `FingerprintPinVerifier` gains a dep on `alknet-tls`. The
|
||||
trade-off is documented in OQ-59.
|
||||
(OQ-59). (Resolved by ADR-089 §5: `FingerprintPinVerifier` moved to
|
||||
`alknet-tls`, so the `alknet-call` → `alknet-tls` dep-edge concern that
|
||||
drove this trade-off is moot — `alknet-call` shed its `rustls` dep
|
||||
entirely.)
|
||||
- A new crate in the dep graph. Small, focused, one clear
|
||||
responsibility.
|
||||
|
||||
@@ -278,8 +279,10 @@ details that can change without breaking the contract.
|
||||
- ADR-080 — `ChannelClient::from_connection` (transport-agnostic
|
||||
client; the pattern this ADR mirrors on the TLS side)
|
||||
- OQ-59 — resolved: `fingerprint.rs` stays in `alknet-core` (the
|
||||
client-side `FingerprintPinVerifier` in `alknet-call` must not depend
|
||||
on `alknet-tls`; the `rustls` dep in core is test-only).
|
||||
client-side `FingerprintPinVerifier` is now in `alknet-tls` per
|
||||
ADR-089 §5, so the `alknet-call` → `alknet-tls` dep-edge concern that
|
||||
drove this resolution is dissolved; the `rustls` dep in core is
|
||||
test-only).
|
||||
- `crates/alknet-core/src/endpoint.rs` — the code being extracted
|
||||
- `crates/alknet-core/src/config.rs` — `TlsIdentity`, `Ed25519SecretKey`
|
||||
(staying in core)
|
||||
|
||||
@@ -223,9 +223,12 @@ impl AlknetEndpoint {
|
||||
pub async fn run(self: Arc<Self>);
|
||||
|
||||
/// Signal all owned accept loops to stop, drain in-flight handlers
|
||||
/// for `drain_timeout`, then close. One owner, one shutdown — no
|
||||
/// external loop coordination needed.
|
||||
pub async fn shutdown(&self) -> Result<(), EndpointError>;
|
||||
/// for `drain_timeout`, then close. Infallible — the endpoint owns
|
||||
/// all its accept loops (quinn, iroh, TCP+TLS) and takes pre-built
|
||||
/// pre-bound transports, so there is no bind failure path and no
|
||||
/// external loop coordination. No-handler matches are swallowed by
|
||||
/// `dispatch` (close + log). One owner, one shutdown.
|
||||
pub async fn shutdown(&self);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -386,13 +389,13 @@ 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") |
|
||||
| `AlknetEndpoint`, `HandlerRegistry`, all dispatch/loop/extraction code | `alknet-endpoint` (this ADR, §"Amendment 2026-07-15") |
|
||||
| `EndpointError` | **removed** — `BindFailed` and `HandlerNotFound` are both vestigial (endpoint takes pre-bound transports; `dispatch` swallows no-handler matches); `shutdown()` is infallible |
|
||||
|
||||
### 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`)
|
||||
@@ -418,8 +421,8 @@ The endpoint is extracted from `alknet-core` into a new crate
|
||||
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`.
|
||||
`HandlerRegistry` move from `alknet-core` to `alknet-endpoint`
|
||||
(`EndpointError` is removed — see above).
|
||||
|
||||
#### Why extract (the dependency data)
|
||||
|
||||
@@ -530,7 +533,7 @@ refactor.
|
||||
| `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` |
|
||||
| `endpoint.rs` (`AlknetEndpoint`, `HandlerRegistry`) | ~1600 | **no** — moves to `alknet-endpoint` (`EndpointError` is removed, not moved) |
|
||||
|
||||
Core loses ~1600 LOC (the endpoint) and 5 heavy deps (`quinn`, `iroh`,
|
||||
`rcgen`, `rustls-pemfile`, `rustls-acme`). The remaining ~3200 LOC is
|
||||
|
||||
@@ -123,22 +123,30 @@ sketched lightly here; the full variant-granularity of `TlsError` is
|
||||
OQ-63 (the next session).
|
||||
|
||||
This is **not** the dial. `TlsClientConfig` produces a
|
||||
`rustls::ClientConfig`; the caller (the transport-specific dial helper,
|
||||
or `CallClient::connect_quic`, or a future `connect_tcp_tls`) passes
|
||||
it to the transport's connector. The config is transport-agnostic; the
|
||||
dial is not.
|
||||
`rustls::ClientConfig`; the caller (the transport-specific dial helper
|
||||
— now `AlknetClient::dial_quic` / `dial_tcp_tls` per ADR-089; the
|
||||
per-protocol `CallClient::connect` / `ChannelClient::connect_quic`
|
||||
convenience constructors are removed per ADR-089 §5) passes it to the
|
||||
transport's connector. The config is transport-agnostic; the dial is
|
||||
not.
|
||||
|
||||
### 2. The dial seam (OQ-55) is unaffected
|
||||
### 2. The dial seam (OQ-55) — subsequently resolved by ADR-089
|
||||
|
||||
> **Update (2026-07-16):** OQ-55 is now resolved by ADR-089. The text
|
||||
> below is the original (pre-ADR-089) framing, preserved for context.
|
||||
> `AlknetClient` is the extracted dial seam; the per-protocol
|
||||
> convenience constructors are removed, not retained as wrappers.
|
||||
|
||||
OQ-55 (the `AlknetClient` transport-polymorphic dial extraction)
|
||||
remains deferred. The deferral is about the *dial* —
|
||||
transport-specific connection establishment — not about the TLS config.
|
||||
With `TlsClientConfig` in `alknet-tls`, each transport-specific dial
|
||||
helper (`CallClient::connect_quic`, a future `connect_tcp_tls`, a
|
||||
future `connect_iroh`) builds its `TlsClientConfig` and passes it to
|
||||
its transport's connector. The friction (each dial helper calls
|
||||
`TlsClientConfig::new` + its transport's connect) is real but narrow —
|
||||
it's duplicated `TlsClientConfig::new` calls, not duplicated verifier
|
||||
~~remains deferred~~ **is resolved by ADR-089**. The deferral was about
|
||||
the *dial* — transport-specific connection establishment — not about
|
||||
the TLS config. With `TlsClientConfig` in `alknet-tls`, each
|
||||
transport-specific dial helper (now `AlknetClient::dial_quic` /
|
||||
`dial_tcp_tls` / `dial_iroh`, ADR-089) builds its `TlsClientConfig`
|
||||
and passes it to its transport's connector. The friction (each dial
|
||||
helper calls `TlsClientConfig::new` + its transport's connect) is real
|
||||
but narrow — it's duplicated `TlsClientConfig::new` calls, not
|
||||
duplicated verifier
|
||||
selection logic. When a second transport's dial exists, the dial
|
||||
seam is extractable (OQ-55 unblocked); the TLS config is already
|
||||
shared by then.
|
||||
@@ -207,12 +215,12 @@ the same way.
|
||||
establishing that `Connection`, not about the take-over.
|
||||
- **`FingerprintPinVerifier`** — the existing verifier in
|
||||
`alknet-call` is the current implementation of ADR-034's
|
||||
fingerprint-pin path. `TlsClientConfig::new` centralizes the
|
||||
verifier construction (including the CA-verify and fail-closed
|
||||
paths that `FingerprintPinVerifier` does not cover). The
|
||||
`FingerprintPinVerifier` type may move to `alknet-tls` or stay in
|
||||
`alknet-call` and be constructed by `TlsClientConfig::new` — an
|
||||
implementation detail, not an architecture decision.
|
||||
fingerprint-pin path. It **moves to `alknet-tls`** (amended by
|
||||
ADR-089 §5): with `CallClient::connect` removed, it has no remaining
|
||||
home in `alknet-call`, and it is a TLS concern (implements
|
||||
`rustls::client::danger::ServerCertVerifier`). `TlsClientConfig::new`
|
||||
constructs it. Moving it lets `alknet-call` shed its direct `rustls`
|
||||
dep entirely — `CallClient` becomes a pure protocol crate.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ The decision is grounded in two sources, not memory:
|
||||
| `FingerprintPinVerifier::new(...)` | **infallible** | stores the fingerprint + supported algorithms; the known-peer path |
|
||||
| `WebPkiServerVerifier::builder_with_provider(...).build()` | **`VerifierBuilderError`** | `rustls::webpki::VerifierBuilderError` — `NoRootAnchors`, `InvalidCrl`; the unknown-X.509-remote path |
|
||||
| `rustls::RootCertStore::add(cert)` | `rustls::Error` | adding a native root cert (maps `webpki::Error` → `InvalidCertificate(...)`) |
|
||||
| `rustls-native-certs::load_native_certs()` errors | logged, not returned | the current client code logs and continues; an empty store falls back to built-in webpki-roots |
|
||||
| `rustls-native-certs::load_native_certs()` errors | logged, not returned | native-certs load errors are logged and the load continues; the built-in `webpki-roots` are merged into the store when the platform store is empty (see §5), so the store is never empty and `NoRootAnchors` is unreachable in practice |
|
||||
| Client-auth cert resolver — RawKey | **infallible** | builds `CertifiedKey` in-memory |
|
||||
| Client-auth cert resolver — X.509 | `io::Error` (load) + `rustls::Error` (`CertifiedKey::from_der`) | cert/key file load + key parse |
|
||||
| "ACME TLS identity is server-only; cannot be used for client auth" | `io::Error`-shape | the `TlsIdentity::Acme` as client-identity guard |
|
||||
@@ -287,7 +287,37 @@ The enum is `#[non_exhaustive]` so adding variants (e.g., a future
|
||||
`TcpWrap` if `for_tcp_tls` ever gains a failure path — it does not today)
|
||||
is not a breaking change.
|
||||
|
||||
### 5. ACME — errors are stream events, not `TlsError` variants
|
||||
### 5. Root store fallback — `webpki-roots` when the platform store is empty
|
||||
|
||||
The unknown-X.509-remote CA-verification path in `TlsClientConfig::new`
|
||||
loads the platform's native root certs (`rustls-native-certs`). The
|
||||
current code's comment claims a fallback to built-in `webpki-roots` when
|
||||
the platform store is empty, but the code does no such thing — an empty
|
||||
store produces `NoRootAnchors` from `WebPkiServerVerifier::build()`,
|
||||
surfacing as `TlsError::VerifierBuild`. This is a latent bug for
|
||||
containerized deployments (no system CA bundle) dialing public X.509
|
||||
endpoints — a real deployment shape.
|
||||
|
||||
The extraction to `alknet-tls` is the moment to make the claim true.
|
||||
`TlsClientConfig::new`'s CA-verify path merges the built-in
|
||||
`webpki-roots` into the `RootCertStore` when the platform store comes
|
||||
back empty (or unconditionally — the built-in roots are a superset of
|
||||
trust, and native certs are mostly the same CAs). `webpki-roots` is an
|
||||
always-present dep in `alknet-tls` (not feature-gated — the CA-verify
|
||||
path is transport-agnostic). Native-certs *load* errors are still
|
||||
logged, not returned (preserved behavior); the fallback guarantees the
|
||||
store is non-empty regardless, so `NoRootAnchors` is unreachable in
|
||||
practice. The `VerifierBuild` variant remains in `TlsError` for the
|
||||
`InvalidCrl` case and forward-compatibility, but the empty-store case
|
||||
is eliminated.
|
||||
|
||||
This is a behavior change from the current code (which silently fails on
|
||||
empty stores), but it is the behavior the docs already claimed — making
|
||||
the claim true, not preserving a latent bug. See ADR-089 §5 for the
|
||||
broader principle: preserve behavior only when it makes sense; a claim
|
||||
without code is a bug, not a contract.
|
||||
|
||||
### 6. ACME — errors are stream events, not `TlsError` variants
|
||||
|
||||
`AcmeConfig::new()` and `AcmeConfig::state()` are infallible (confirmed
|
||||
in the rustls-acme 0.12.1 source). `TlsServerConfig::new`'s ACME branch
|
||||
@@ -310,7 +340,7 @@ the caller (rather than log them), that is a new ADR — it changes the
|
||||
`TlsServerConfig` API (the ACME handle would need an error channel) and
|
||||
is out of scope for the error-shape decision.
|
||||
|
||||
### 6. The fail-closed distinction
|
||||
### 7. The fail-closed distinction
|
||||
|
||||
OQ-63's framing listed "unknown-remote fail-closed (not an error to
|
||||
return — it's a `Result::Err` the caller gets for trying to connect to
|
||||
@@ -334,9 +364,10 @@ distinction:
|
||||
`TlsError` is the **config-construction** error type. Handshake-time
|
||||
errors are the transport's error type. This keeps `TlsError` scoped to
|
||||
what `new` and `for_quinn` can actually fail on, and avoids pretending
|
||||
handshake outcomes are config-construction errors. A future ADR that
|
||||
introduces a dial helper (the OQ-55 dial seam) would define how
|
||||
handshake errors are surfaced then; that is not this ADR.
|
||||
handshake outcomes are config-construction errors. The dial seam
|
||||
(ADR-089, `AlknetClient`) surfaces handshake errors as
|
||||
`ClientDialError::Handshake(String)` — see the client spec's
|
||||
`ClientDialError` section.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -216,18 +216,68 @@ type — a native client can reach a native endpoint over QUIC, TCP+TLS
|
||||
choice is the caller's, driven by network conditions and the remote
|
||||
endpoint's reachability.
|
||||
|
||||
### 5. `CallClient::connect` / `ChannelClient::connect_quic` delegate
|
||||
### 5. `CallClient::connect` / `ChannelClient::connect_quic` are removed
|
||||
|
||||
The existing QUIC convenience constructors on `CallClient` and
|
||||
`ChannelClient` (`connect` / `connect_quic`) become thin wrappers over
|
||||
`AlknetClient::dial_quic`. They build an ephemeral `AlknetClient` (or
|
||||
accept one), dial QUIC, and call `spawn_dispatch` / `from_connection`.
|
||||
The one-way-door surface is the `AlknetClient` dial + take-over pattern;
|
||||
the per-protocol convenience constructors are two-way-door sugar over
|
||||
it. This does not break the existing APIs — they remain for the "I just
|
||||
want QUIC, no `AlknetClient` wiring" case. A caller that needs
|
||||
transport selection (QUIC with TCP+TLS fallback) uses `AlknetClient`
|
||||
directly.
|
||||
`ChannelClient` (`connect` / `connect_quic`) are **removed**, not
|
||||
delegated. Keeping them as thin wrappers over `AlknetClient::dial_quic`
|
||||
would make `alknet-call` / `alknet-channels-call` depend on
|
||||
`alknet-client` — contradicting the dep graph (§1: the protocol crates
|
||||
are parallel to the dial, not downstream of it) and re-coupling every
|
||||
`CallClient` user to `quinn` + `rustls` + the TLS verifier machinery,
|
||||
the exact welding the extraction undoes. Duplicating the dial inline in
|
||||
each convenience constructor would preserve the dep graph but defeats
|
||||
the point of centralizing the dial.
|
||||
|
||||
The dial is a distinct concern from the protocol take-over.
|
||||
`AlknetClient` is the single home for the dial; `CallClient` /
|
||||
`ChannelClient` are the single home for the take-over. A caller that
|
||||
wants the old one-liner shape composes two lines:
|
||||
`client.dial_quic(...).await?` then
|
||||
`CallClient::new(...).spawn_dispatch(conn)` (or
|
||||
`ChannelClient::from_connection(conn).await?`). The one-way-door
|
||||
surface is the `AlknetClient` dial + take-over pattern; the
|
||||
per-protocol convenience constructors are gone, not retained as
|
||||
two-way-door sugar.
|
||||
|
||||
This is a breaking change to `CallClient` / `ChannelClient`'s public
|
||||
APIs. It is expected — the develop branch is a total rewrite addressing
|
||||
issues not feasible to fix inline against `main`; there are no external
|
||||
consumers to preserve compatibility for. The migration plan handles
|
||||
the call-site updates.
|
||||
|
||||
**Consequence: `CallCredentials` / `RemoteIdentity` move to
|
||||
`alknet-core`.** These types were in `alknet-call` because
|
||||
`CallClient::connect` consumed them. With `connect` removed, the dial
|
||||
(`AlknetClient`) is the consumer, and the dial must not depend on
|
||||
`alknet-call` (§1). Both the call and channels clients need them (the
|
||||
channels client takes `CallCredentials` for its own removed
|
||||
`connect_quic`, and the dial takes them for all three dials). They move
|
||||
to `alknet-core` — the shared-types crate, alongside `TlsIdentity` and
|
||||
`AuthToken` which already live there. This is the cleaner of the two
|
||||
options the original ADR-089 draft called a "two-way-door
|
||||
implementation detail"; it is not implementation detail — it determines
|
||||
the dep graph, and the dep graph requires it.
|
||||
|
||||
**Consequence: `FingerprintPinVerifier` moves to `alknet-tls`.** With
|
||||
`connect` removed and the verifier-selection logic centralized in
|
||||
`TlsClientConfig::new` (ADR-087), `FingerprintPinVerifier` has no
|
||||
remaining home in `alknet-call`. It is a TLS concern (it implements
|
||||
`rustls::client::danger::ServerCertVerifier`); moving it to
|
||||
`alknet-tls` lets `alknet-call` shed its direct `rustls`,
|
||||
`rustls-pemfile`, and `rustls-native-certs` deps entirely — `CallClient`
|
||||
becomes a pure protocol crate (`{registry, identity_provider}` +
|
||||
`spawn_dispatch`). See ADR-087 §5 (amended).
|
||||
|
||||
**Consequence: `ClientError` is removed.** The existing
|
||||
`ClientError { Transport, TlsSetup, ConnectionClosed }` was produced
|
||||
only by `connect` (`Transport` and `TlsSetup`) and by no current
|
||||
`spawn_dispatch` path (`ConnectionClosed` is a `FrameError`/`StreamError`
|
||||
variant internal to the dispatch loop, not a `CallClient` API error).
|
||||
With `connect` gone, `ClientError` has no producing call site. It is
|
||||
removed rather than left as a vestigial enum. If `spawn_dispatch` ever
|
||||
gains a failure path, a fresh error type is cleaner than retrofitting
|
||||
this one.
|
||||
|
||||
### 6. `alknet/register` is a dialable ALPN (entry point, wire protocol deferred)
|
||||
|
||||
@@ -292,10 +342,11 @@ several possible native clients sharing the same wire protocols.
|
||||
- **`CallClient::spawn_dispatch` / `ChannelClient::from_connection`**
|
||||
— the take-over APIs are unchanged. They consume the `Connection`
|
||||
the dial produces; they do not know `AlknetClient` produced it.
|
||||
- **`CallCredentials` / `RemoteIdentity`** — unchanged. The credential
|
||||
bundle `AlknetClient` takes is the existing type from `alknet-call`
|
||||
(or moved to `alknet-client` / `alknet-core` as a shared type — an
|
||||
implementation detail; the shape is decided).
|
||||
- **`CallCredentials` / `RemoteIdentity`** — **moved to `alknet-core`**
|
||||
(see §5). The shape is unchanged; the location changes from
|
||||
`alknet-call` to `alknet-core` so the dial does not depend on the
|
||||
call protocol. Both the call and channels clients consume them from
|
||||
core.
|
||||
- **The channels substrate (ADR-071)** — unchanged. The dial produces a
|
||||
`Connection`; the channels protocol runs on it.
|
||||
- **ADR-086 (endpoint types / entry points)** — the endpoint-type model
|
||||
@@ -318,9 +369,16 @@ several possible native clients sharing the same wire protocols.
|
||||
|
||||
- **OQ-55 is resolved.** The transport-polymorphic dial seam is
|
||||
extracted, for the native case. The duplicated dial boilerplate
|
||||
(each convenience constructor rebuilding `TlsClientConfig::new` +
|
||||
its transport's connector) is centralized in `AlknetClient`. The
|
||||
friction the deferral accepted is removed.
|
||||
(the removed convenience constructors each rebuilt
|
||||
`TlsClientConfig::new` + their transport's connector) is centralized
|
||||
in `AlknetClient`. The friction the deferral accepted is removed.
|
||||
- **`alknet-call` becomes a pure protocol crate.** With `connect`
|
||||
removed and `FingerprintPinVerifier` moved to `alknet-tls` (§5),
|
||||
`alknet-call` sheds its direct `quinn`, `rustls`, `rustls-pemfile`,
|
||||
and `rustls-native-certs` deps. `CallClient` is `{registry,
|
||||
identity_provider}` + `spawn_dispatch` — no TLS, no transport. Every
|
||||
handler crate that uses `CallClient` stops transitively linking the
|
||||
TLS/transport stack.
|
||||
- **The client-side shape is symmetric with the server side.** A
|
||||
reader who understands `AlknetEndpoint` (accept + dispatch) can
|
||||
understand `AlknetClient` (dial + produce `Connection`) by
|
||||
@@ -342,6 +400,15 @@ several possible native clients sharing the same wire protocols.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- **Breaking change: `CallClient::connect` / `ChannelClient::connect_quic`
|
||||
removed; `CallCredentials` / `RemoteIdentity` / `FingerprintPinVerifier`
|
||||
relocated; `ClientError` removed.** Call sites that used the
|
||||
convenience constructors must switch to `AlknetClient::dial_*` +
|
||||
`spawn_dispatch` / `from_connection`. Import paths for
|
||||
`CallCredentials` / `RemoteIdentity` change from `alknet_call` to
|
||||
`alknet_core`. This is expected — the develop branch is a total
|
||||
rewrite; there are no external consumers to preserve compatibility
|
||||
for. The migration plan handles the call-site + import updates.
|
||||
- **A new crate.** `alknet-client` is one more crate in the workspace.
|
||||
The cost is low (the dial is narrow), and the dependency profile
|
||||
rules out the alternatives, but it is a new entry in the crate
|
||||
|
||||
@@ -135,11 +135,13 @@ pub struct Socks5Credentials {
|
||||
}
|
||||
```
|
||||
|
||||
The config's crate location is an implementation detail (it can live in
|
||||
`alknet-client`, `alknet-core`, or `alknet-tls` alongside the other
|
||||
client config types), same as the `CallCredentials` location is an
|
||||
implementation detail per ADR-089 §5. The shape is decided; the crate
|
||||
home is two-way-door.
|
||||
The config's crate location is an implementation detail (it can live
|
||||
in `alknet-client`, `alknet-core`, or `alknet-tls` alongside the other
|
||||
client config types) — the shape is decided; the crate home is
|
||||
two-way-door. (Note: `CallCredentials`'s location is **not** a
|
||||
two-way-door — it is decided as `alknet-core` per ADR-089 §5, because it
|
||||
determines the dep graph; `Socks5ProxyConfig`'s location does not have
|
||||
that dep-graph consequence.)
|
||||
|
||||
### 2. `AlknetClient` holds the proxy; each dial applies it
|
||||
|
||||
|
||||
Reference in new issue
Block a user