Files
alktls/docs/architecture/decisions/004-accessor-surface.md
T
glm-5.3-flash d74a27f764 phase 1: architecture spec — overview, server/client, ADR-001..006
- ADR-001: inherit the alknet TLS design as the baseline; deviations
  recorded as alktls ADRs
- ADR-002: TlsError ships the ADR-088 six-variant shape from day one
  (typed #[from] sources; NoqWrap; no string catch-all)
- ADR-003: the QUIC feature is noq (iroh's extracted fork), pre-
  consumer rename; default = [] per the lean-crate convention
  (corrects the extracted code's default = ["quinn"])
- ADR-004: complete accessors — for_tcp_tls() adopted, rustls_config()
  adopted; server accessors borrow (&self), client accessors consume
- ADR-005: identity + credentials + fingerprint types move into
  alktls; auth layer stays out
- ADR-006: eight-module layout; seed tests + integration invariant
  pins (exact nine-scheme list, client enable_early_data)
- specs: overview (transport picture, terminology), server.md (ACME
  lifecycle, invariants), client.md (verifier selection matrix, root-
  store fallback); open-questions.md promotes OQ-TLS-01..08 (all
  resolved at entry)
- Cargo.toml: quinn feature -> noq (per ADR-003); AGENTS.md aligned

Architecture review pass done: 0 critical, 2 major (ADR-002 AcmeConfig
doc comment contradiction; ADR-003 unrecorded default deviation) and
8 minors all addressed; cross-references verified against alknet ADRs,
rustls/noq/iroh sources.

Verified: cargo test, test --all-features, clippy -D warnings,
fmt --check, doc --no-deps
2026-09-10 05:37:55 +00:00

5.2 KiB

status, last_updated
status last_updated
accepted 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<u8>]) 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:

impl TlsServerConfig {
    pub async fn new(identity: &TlsIdentity, alpns: &[Vec<u8>])
        -> Result<Self, TlsError>;

    /// `&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<noq::ServerConfig, TlsError>;

    /// Infallible — `TlsAcceptor::from(Arc<ServerConfig>)` 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<Self, TlsError>;

    /// Consumes — one dial = one config build; a `TlsClientConfig` is
    /// not reused across dials.
    #[cfg(feature = "noq")]
    pub fn for_noq(self) -> Result<noq::ClientConfig, TlsError>;

    /// 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