The TLS setup in alknet-core is welded to quinn: build_rustls_server_config
produces a rustls::ServerConfig, then build_quinn_server_config_from_rustls
consumes it into quinn::ServerConfig — the rustls config is moved, not
shareable. ACME is worse: the AcmeState task is spawned inside the quinn
endpoint, so a TCP+TLS listener would need a second ACME state machine
(two orders for the same domain, two cert caches, Let's Encrypt
rate-limit risk).
alknet-tls extracts the TLS setup into a shareable TlsServerConfig:
- TlsServerConfig::new(identity, alpns) builds the rustls::ServerConfig
once (including the ACME state machine if ACME)
- for_quinn() clones it for quinn (QuicServerConfig::try_from)
- for_tcp_tls() clones it for tokio-rustls (TlsAcceptor::from)
- rustls_config() borrows it for any other consumer
- One cert, one ACME state machine, N transports
TlsIdentity and Ed25519SecretKey stay in core (config types).
fingerprint.rs stays in core (shared by server endpoint + client
FingerprintPinVerifier in alknet-call — OQ-59 tracks whether it should
move; likely stays because moving would force alknet-call to depend on
alknet-tls, pulling TLS infra into client-only deployments).
iroh shares the key, not the rustls config — iroh has its own TLS built
into the Endpoint (takes iroh::SecretKey, not rustls::ServerConfig).
alknet-tls has no for_iroh() method; the assembly layer passes
Ed25519SecretKey to iroh directly.
Feature gates: quinn / tcp / acme as independent opt-ins. rustls always
present. tokio-rustls only when tcp. quinn only when quinn. rustls-acme
only when acme.
Files:
- crates/tls/README.md: crate spec (TlsServerConfig API, what moves/
stays, feature gates, deps, behavior-preservation invariants)
- decisions/082-alknet-tls-extraction.md: the ADR (Proposed)
- questions/059-fingerprint-module-location.md: OQ-59 (should
fingerprint.rs stay in core or move to alknet-tls? open, two-way,
likely stays)
- open-questions.md: OQ-59 added to alknet-core table
- README.md: tls doc table row + ADR-082 row added
Review (architecture-reviewer): 3 critical (missing
build_quinn_server_config_from_rustls in README table, missing futures
dep, two TlsServerConfig code blocks disagreed on feature gates), 7
warnings (behavior-preservation invariants: max_early_data_size,
aws_lc_rs provider, verifier scheme list, acme-tls/1 ALPN;
rustls-pki-types dep; fingerprint.rs rustls-usage imprecision; ADR
missing acme-tls/1), 4 suggestions. All criticals and warnings addressed.
Each open question lives in its own file under questions/,
named NNN-slug.md (mirroring the ADR convention). This file is the index:
theme-grouped tables for scannability, plus a cross-theme
Deferred / Blocked section that surfaces the
safe-exit deferrals with their blocking conditions inline — so "what's
currently parked and why" is answerable at a glance.
Status values:
open — Needs to be resolved now. Has a clear path to resolution.
resolved — Decided. The resolution is stated cleanly, without caveats about how it could be changed later.
deferred(scope) — Cannot be resolved yet. The information doesn't exist. Has a concrete blocking condition (e.g., "blocked on: alknet-agent crate spec"). Not a failure — scope management.
partially resolved — Some aspects decided, others deferred or open.
dissolved — The question was reframed out of existence (e.g., superseded by an ADR that retires the premise). Kept for reference.
Door type classifications follow ADR-009 — they describe reversal cost (how expensive it is to undo), not urgency:
One-way door: Reversal requires rewriting significant code or permanently closes a capability. Getting it wrong is expensive — requires ADR before implementation.
Two-way door: Reversal is cheap or additive. Getting it wrong is recoverable — decide, implement, revert if needed.
Door type is separate from whether a decision is made. A two-way door is a decision you make now and can revert later, not a decision to defer. See ADR-009 §"What this framework is NOT."
The safe-exit visibility surface. These questions are parked because the
information needed to resolve them does not exist yet — each has a concrete
blocking condition. They are not failures; they are scope management. See
ADR-009 §"Safe Exit: Deferred Decisions." This section exists so "what's
currently blocking the architect" is answerable at a glance, not by
filtering the tables above.
OQ-09: WASM Target Boundaries
Blocked on: A concrete server-side WASM use case, or a deliberate confirmation that WASM stays a client-side design constraint. Tracked as architecture/oq-09-wasm-server-use-case in tasks/architecture/.
Priority: low
Amendment (2026-07-09): The Connection door is now open via Connection::from_stream (ADR-065) — a Connection can be constructed from any wasm-compatible stream. What remains closed is the accept-loop runtime (tokio::spawn does not run on WASM; PendingRequestMap/CallAdapter use tokio channels). The blocking condition (a concrete server-side WASM use case) is unchanged.
OQ-10: Git Adapter Scope — Smart Protocol Only or Full Server?
Blocked on: Speccing the alknet-git crate — resolve this when that crate is specified, not deferred past it. Tracked as architecture/oq-10-git-adapter-spec in tasks/architecture/.
Blocked on: A handler that needs stream operators and finds the existing combinators (Box::pin(stream::iter(...)), async_stream::stream!, futures::stream) insufficient. The operators library is a convenience, not a prerequisite for any handler.
Blocked on: a concrete use case for network or volume management over the call protocol. Dev containers use the default bridge network; hosted services declare networks/volumes in docker compose.
Blocked on: v1 implementation — the create input JSON Schema is finalized when register_docker_ops is written and tested against bollard's Config struct. An architectural decision (ADR-060 §5), not a deferral past implementation.
Blocked on: a second transport's real dial existing (not just a
second QUIC dial). The dial is transport-specific (QUIC, HTTP, TCP+TLS,
WebTransport, raw TCP); we have one shape implemented (QUIC —
CallClient::connect and ChannelClient::connect_quic). Extracting a
QUIC-shaped connector now would bake QUIC in as the establishment
shape — the same welding ADR-065 unwound on the server side. The blocking
condition is met when a non-QUIC dial (SSH raw-TCP, HTTP-wrapped call,
TCP+TLS) exists, so the transport-polymorphic dial+TLS seam is
extractable from two different transport implementations. Note: the
client APIs are already transport-agnostic — CallClient::spawn_dispatch
and ChannelClient::from_connection (ADR-080) take a pre-established
Connection. What is deferred is the shared dial, not the client
protocol surface.
Blocked on: a real deployment observes head-of-line blocking on a
saturated channel where the bounded-buffer's stop-reading mitigation is
insufficient (e.g., a high-throughput file transfer over a tunnel that
saturates a channel and causes frequent demux stalls affecting other
channels). The intended use cases (TTY, SSH, tunnels) are not
high-throughput in the HOL-blocking sense; the trigger requires a
high-throughput use case.
Blocked on: a second two-pump handler existing (the tunnel handler is
the first; SSH direct-tcpip will be the second), so the shape
convergence is observable. Extracting the helper from one consumer would
bake in a shape that the second might not fit. The shutdown-on-completion
contract is decided (ADR-078); only the helper extraction is deferred.