--- status: accepted last_updated: 2026-09-10 --- # ADR-004: Complete the accessors — `for_tcp_tls()`, `rustls_config()`, and borrow-vs-consume ## Status Accepted (2026-09-10) ## Context The alknet spec (ADR-082's API table and the `crates/tls` README) pins four server-side accessors and three client-side accessors. The extracted code implements a subset and diverges in shape: | Accessor | Spec | Extracted code | |----------|------|----------------| | `TlsServerConfig::new` | `async`, `(&TlsIdentity, &[Vec])` | same | | `TlsServerConfig::for_quinn` | `&self` | `self` (consumes) | | `TlsServerConfig::for_tcp_tls` | `-> TlsAcceptor`, infallible | **missing** — callers wrap `tokio_rustls::TlsAcceptor::from(Arc::new(cfg.rustls_config.clone()))` themselves | | `TlsServerConfig::rustls_config` | `&self -> &ServerConfig` | **missing** — the field is `pub(crate)` | | `TlsClientConfig::new` | sync, `(&ConnectionCredentials, &[u8])` | same | | `TlsClientConfig::for_quinn` | `self` (consumes) | `self` | | `TlsClientConfig::into_rustls_config` | `self` (consumes) | same | The missing server accessors force the assembly layer to reach into crate internals (`pub(crate)` field access is impossible for external consumers) or re-derive the acceptor wrap. The `self`-consuming server accessor prevents the literal ADR-082 story — one `TlsServerConfig` feeding both a QUIC endpoint and a TCP+TLS acceptor — without contortions. ## Decision The public API surface is the spec's surface, with borrow-vs-consume decided per accessor: ```rust impl TlsServerConfig { pub async fn new(identity: &TlsIdentity, alpns: &[Vec]) -> Result; /// `&self` — the inner rustls config is Clone (Arc-shared /// resolvers); one TlsServerConfig can feed a noq endpoint AND a /// TCP+TLS acceptor without contortions. #[cfg(feature = "noq")] pub fn for_noq(&self) -> Result; /// Infallible — `TlsAcceptor::from(Arc)` cannot /// fail. Feature-gated on `tcp`. #[cfg(feature = "tcp")] pub fn for_tcp_tls(&self) -> tokio_rustls::TlsAcceptor; /// Borrow the inner config for transport wrappers the crate does /// not cover. pub fn rustls_config(&self) -> &rustls::ServerConfig; } impl TlsClientConfig { pub fn new(credentials: &ConnectionCredentials, alpn: &[u8]) -> Result; /// Consumes — one dial = one config build; a `TlsClientConfig` is /// not reused across dials. #[cfg(feature = "noq")] pub fn for_noq(self) -> Result; /// Consumes — the TCP+TLS dial wraps the returned config in a /// `TlsConnector` itself. pub fn into_rustls_config(self) -> rustls::ClientConfig; } ``` Rationale per accessor: - **Server accessors take `&self`.** The ADR-082 story is "one identity, N transports": the assembly layer builds one `TlsServerConfig` per endpoint type and hands it to every transport that endpoint serves. `&self` plus the Clone inner config makes the multi-transport sharing direct; the extracted `self`-consuming shape is a vestige of the quinn-only extraction era (the TCP path was re-derivable only because the field happened to be `pub(crate)` in the same workspace). - **Client accessors consume `self`.** A `TlsClientConfig` is built per dial (`dial_quic`, `dial_tcp_tls` build fresh configs per ADR-089's pattern); nothing reuses it. Consuming makes `into_rustls_config` zero-cost (no clone behind the scenes) and keeps the API honest about reuse. - **`for_tcp_tls()` is adopted** (Phase 0 gap #2). It is one line, infallible, and the spec is unambiguous; leaving it out would keep the de facto "callers wrap the acceptor" shape and force the rewrite to duplicate it. - **`rustls_config()` is adopted** for any transport wrapper beyond `for_noq` / `for_tcp_tls` (iroh does not need it — key-not-config — but the escape hatch costs nothing and stays true to the spec). - **`new` stays `async fn`** for API uniformity with the ACME path (which spawns the state-machine task); the non-ACME paths have no await point (sync file I/O) — this is the spec's recorded posture, and the uniform signature is worth more than the await-free purity. ## Consequences **Positive:** - The assembly layer builds configs and transports without reaching into crate internals or hand-rolling acceptor wraps. - The API freeze matches the spec the rewrite was specced against — zero translation for the rewrite's consumers. - The multi-transport story (one config → noq + TCP+TLS) is directly expressible. **Negative:** - `&self` server accessors require the inner rustls config to stay `Clone`-able — already true and load-bearing (Arc-shared resolvers); a future config shape that is not Clone would break the accessor contract (acceptable: Clone is structural to the sharing story). ## References - alknet ADR-082 API table and the `crates/tls` README §Architecture — the spec surface this ADR adopts - `docs/research/phase-0.md` §Gaps #2, OQ-TLS-03/OQ-TLS-04 — the gap and the accessor-shape question - ADR-002 — `TlsError` (the `for_noq` failure variant) - ADR-003 — the `noq` feature