The endpoint refactor findings doc relegated TCP+TLS to a 'sibling accept loop' that duplicated the endpoint's dispatch logic at the assembly layer. That was ADR-010 Amendment 1's workaround for the endpoint being welded to quinn — not a deliberate design. The endpoint now exposes a public dispatch() method so every transport (quinn, iroh, TCP+TLS, future SSH, future WebTransport) calls the same dispatch path: handler lookup, build_auth_context, spawn. The transport-specific parts (ALPN extraction, fingerprint extraction, Connection construction, no-handler close) happen before dispatch is called. No duplicated logic at the assembly layer. Also: the acme-tls/1 guard is quinn-specific (ACME challenges arrive over QUIC); a TCP+TLS listener serving HTTPS does not advertise acme-tls/1. build_auth_context becomes a private helper called by dispatch, not a standalone function.
27 KiB
alknet-endpoint-refactor — Endpoint as Pure ALPN Dispatcher
Status: Findings complete. The endpoint refactor and the TLS
extraction are interlocked; this document captures the full picture so
neither gets done in isolation and drops half the context.
Date: 2026-07-13
Scope: AlknetEndpoint becomes a pure accept-loop runner (no
transport construction); alknet-tls extraction (TlsServerConfig)
provides the shared cert config the assembly layer needs to build
transports. Both changes are enabled by the pre-implementation window
— nobody is using this code yet, and the issues surfaced from trying to
write the first real consumers (hub, workers, HTTP).
TL;DR
-
AlknetEndpointconflates two concerns: transport construction (buildingquinn::Endpoint,iroh::Endpoint, wiring TLS) and dispatch (take a connection, extract ALPN, look up handler, buildAuthContext, spawn). The TLS extraction forces a separation because the TLS config has to be built once and shared across transports — the endpoint can't build it internally anymore. -
The endpoint becomes a pure accept-loop runner with a public dispatch method. It owns the transport endpoints it runs accept loops for (quinn, iroh), and it exposes
dispatch()for transport accept loops it doesn't own (TCP+TLS, future SSH, future WebTransport). Every transport calls the same dispatch path — no duplicatedbuild_auth_contextor handler-lookup logic at the assembly layer. TheStaticConfigparameter,tls_identityreading, and transport construction all move to the assembly layer. -
TCP+TLS is a first-class transport, not a sibling afterthought. ADR-010 Amendment 1 made TCP+TLS a "sibling" because the endpoint's dispatch logic was crate-private and couldn't be shared. With a public
dispatch()method, the TCP+TLS accept loop calls into the endpoint — same path as quinn and iroh. The hub needs TCP+TLS as a day-1 requirement (HTTPS for browsers, raw-key fallback for native clients when UDP is blocked); it is the most common transport after raw-key QUIC, not an edge case. -
A hub holds one or two
TlsServerConfigs, not one. A hub serving both browsers and native workers holds a raw-key config (QUIC + TCP+TLS fallback for native clients) and an X.509/ACME config (TCP+TLS for HTTPS, QUIC for WebTransport when it revives). The cert-reuse value is within each identity: one config per identity, shared across that identity's transports. Iroh and SSH share the key with the raw-key TLS path, not theTlsServerConfig. -
The TCP fallback for native clients doesn't need X.509. The raw-key
TlsServerConfigserves both QUIC and TCP+TLS. X.509 is only for the browser/HTTPS path. Raw-key and X.509 clients can connect to either server type — the server cert type and client cert type are independent in TLS.AcceptAnyCertVerifieralready accepts both. -
This is a one-way door (endpoint struct shape) done during the pre-implementation window. The cost of doing it now is the same refactor that has to happen anyway. The cost of not doing it is a hybrid model (quinn TLS injected, iroh key read internally, TCP+TLS outside with duplicated dispatch) that will confuse every future reader and block the hub crate (the first multi-transport consumer).
1. The transport landscape
1.1 The full transport matrix
| Transport | TLS identity | Serves | Who connects | Status |
|---|---|---|---|---|
| QUIC (raw key) | RFC 7250 Ed25519 | alknet/channels, alknet/call |
Native clients (workers, spokes) | Day 1 |
| TCP+TLS (raw key fallback) | RFC 7250 Ed25519 | Same as above | Native clients (UDP blocked) | Day 1 |
| TCP+TLS (X.509 / ACME) | X.509 | h2/http/1.1 (HTTPS) |
Browsers, HTTP clients | Day 1 |
| QUIC (X.509 / ACME) | X.509 | WebTransport | Browsers | Deferred (ADR-044; revives with channels) |
| iroh | Ed25519 (own TLS) | alknet/channels, alknet/call |
P2P native clients | Day 1 |
| TCP (SSH) | Ed25519 key (SSH) | alknet/ssh channels |
Legacy SSH clients | Future |
1.2 Two cert identities, shared within each
A hub serving both browsers and native workers holds two
TlsServerConfigs:
-
Raw key config — built from
TlsIdentity::RawKey(Ed25519SecretKey). Shared across:- QUIC listener (
for_quinn()) — native clients with UDP - TCP+TLS listener (
for_tcp_tls()) — native clients when UDP blocked - The same
Ed25519SecretKeyalso feeds iroh and (future) SSH
- QUIC listener (
-
X.509 config — built from
TlsIdentity::X509orTlsIdentity::Acme. Shared across:- TCP+TLS listener (
for_tcp_tls()) — HTTPS for browsers - QUIC listener (
for_quinn()) — WebTransport when it revives
- TCP+TLS listener (
The cert-reuse value is within each identity, not across them. A hub
doesn't share one cert for everything — it shares one cert per identity
across that identity's transports. This is why AlknetEndpoint::new
taking a single Arc<TlsServerConfig> (as ADR-082 currently specifies)
is wrong for the hub case: the hub has two configs. The endpoint
shouldn't take any TLS config — the assembly layer builds the transports
and hands the endpoint pre-built quinn/iroh endpoints.
1.3 Iroh and SSH share the key, not the TLS config
Iroh has its own TLS built into the Endpoint (RFC 7250 raw keys). It
does not consume a rustls::ServerConfig. SSH uses the Ed25519 key for
its own handshake. Both share the Ed25519SecretKey with the raw-key
TlsServerConfig, but neither receives a TlsServerConfig.
The assembly layer reads Ed25519SecretKey from StaticConfig and
hands it to:
TlsServerConfig::new(TlsIdentity::RawKey(key), ...)— for quinn/TCP+TLSiroh::SecretKey::from_bytes(key.as_bytes())— for iroh- (future) the SSH handler's key configuration
1.4 Raw-key / X.509 mixing in the TLS handshake
A raw-key client can connect to an X.509 server and vice versa. In
TLS, the server cert type and client cert type are independent. A
server presenting an X.509 cert can accept a client presenting a raw
key (RFC 7250), as long as both sides have RFC 7250 support in their
rustls config. AcceptAnyCertVerifier already accepts both, and its
supported_verify_schemes() includes ED25519.
The mixing cases:
- Raw-key client → X.509 server: client verifies server via CA
(ADR-034 public X.509 case) or fingerprint pin (known hub); server
extracts client's
ed25519:<hex>fingerprint. - X.509 client → raw-key server: client verifies server via
fingerprint pin (raw-key remotes are always known peers); server
extracts client's
SHA256:<hex>fingerprint.
This means the TCP fallback for native clients doesn't need X.509 — the raw-key config serves both QUIC and TCP+TLS. X.509 is only for the browser/HTTPS path.
1.5 What's required vs. what's built now
All transports in the matrix above are requirements — not "nice to
have." But we do not have to build all of them right now. The
abstractions (the endpoint refactor, the TlsServerConfig extraction,
Connection::from_stream/from_bidi) are what make the full matrix
tractable without blocking any of the future transports. The point is
to get the abstractions right so that each transport can be added
without changing the core.
By volume, the most common use case is raw-key QUIC (native P2P, workers). But we have to support the HTTP crate and X.509 from day one. The TCP fallback for native clients (raw key) is important too since UDP is blocked on some networks. A worker has to register with the hub via HTTP POST in many cases, then connect via QUIC if available and fall back to TCP if not.
2. The endpoint refactor
2.1 Current state: the endpoint conflates construction and orchestration
AlknetEndpoint::new currently does three things:
- Builds the quinn endpoint — reads
static_config.tls_identity, constructs TLS internally (TlsSetup::new,build_rustls_server_config), wraps it intoquinn::ServerConfig, bindsquinn::Endpoint::server(). - Builds the iroh endpoint — reads
static_config.tls_identityagain (for the Ed25519 key), callsbuild_iroh_endpoint(). - Runs accept loops for whichever of those it built, dispatching
by ALPN through the
HandlerRegistry.
TCP+TLS is not in this picture at all. ADR-010 Amendment 1 made it a
"sibling accept loop" that runs outside the endpoint — the assembly
layer builds it and dispatches through the same HandlerRegistry. But
the dispatch logic (build_auth_context, fingerprint extraction, the
acme-tls/1 guard, handler lookup + spawn) is crate-private
(dispatch_quinn / dispatch_iroh in endpoint.rs), so the
assembly layer can't call it. A TCP+TLS accept loop has to duplicate it
or call HandlerRegistry::get and build_auth_context directly (the
latter is also crate-private). The "sibling" framing was a workaround
for the endpoint being welded to quinn — not a deliberate design.
The result is a three-tier asymmetry: quinn is built and dispatched internally, iroh is built internally and dispatched internally, TCP+TLS is built outside and dispatched outside (with duplicated logic). The TLS extraction surfaces this because it pulls quinn's TLS construction out, leaving iroh's key reading and TCP+TLS's second-class status as visible inconsistencies.
2.2 The dispatch logic is the endpoint's real value
Stripped of transport construction, what the endpoint provides is
dispatch: take a connection, extract its ALPN, look up the handler,
build the AuthContext, spawn the handler. This is the same for every
transport. The transport-specific parts are:
- ALPN extraction — quinn:
handshake_data().protocol; iroh:connecting.alpn().await; TCP+TLS:tls_stream.alpn()(from thetokio-rustlshandshake result). - Fingerprint extraction — quinn:
peer_identity()→CertificateDer; iroh:remote_id()→ed25519:<hex>; TCP+TLS:tls_stream.peer_certificates()→CertificateDer. - Connection construction — quinn:
from_quinn_with_alpn; iroh:from_iroh; TCP+TLS:from_bidi(tls_stream, alpn, remote_addr). - No-handler close — quinn:
connection.close(); iroh:connection.close(); TCP+TLS: drop the stream.
Everything between those transport-specific bookends — the ACME ALPN
guard, the handler lookup, the build_auth_context call, the
tokio::spawn — is identical. If the endpoint exposes a public
dispatch method that takes the already-extracted ALPN, fingerprint,
and Connection, every transport's accept loop calls the same dispatch
path. No duplication.
2.3 The new endpoint shape
The endpoint is a pure accept-loop runner + a public dispatch method.
It owns the transport endpoints it runs accept loops for (quinn, iroh),
and it exposes dispatch for transport accept loops that it doesn't
own (TCP+TLS, future SSH, future WebTransport).
pub struct AlknetEndpoint {
quinn: Option<quinn::Endpoint>,
iroh: Option<iroh::Endpoint>,
handlers: Arc<HandlerRegistry>,
dynamic: Arc<ArcSwap<DynamicConfig>>,
identity_provider: Arc<dyn IdentityProvider>,
shutdown_tx: watch::Sender<bool>,
shutdown_rx: watch::Receiver<bool>,
drain_timeout: Duration,
}
impl AlknetEndpoint {
pub fn new(
handlers: HandlerRegistry,
dynamic: Arc<ArcSwap<DynamicConfig>>,
identity_provider: Arc<dyn IdentityProvider>,
drain_timeout: Duration,
) -> Self;
pub fn with_quinn(mut self, endpoint: quinn::Endpoint) -> Self;
pub fn with_iroh(mut self, endpoint: iroh::Endpoint) -> Self;
/// Dispatch a pre-established connection by ALPN. Called by the
/// endpoint's own accept loops (quinn, iroh) after transport-
/// specific extraction, and by external accept loops (TCP+TLS,
/// future SSH, future WebTransport) after their own extraction.
/// The caller supplies the ALPN, the fingerprint (if available),
/// the remote address (if available), and the `Connection`.
pub fn dispatch(
&self,
connection: Connection,
alpn: Vec<u8>,
fingerprint: Option<String>,
remote_addr: Option<SocketAddr>,
);
/// Run the endpoint's accept loops. Returns when shutdown is
/// signaled. The caller drives shutdown via `shutdown_sender()`.
pub async fn run(self: Arc<Self>);
}
The StaticConfig parameter disappears from new(). The assembly
layer reads StaticConfig, builds the transports, and hands the
quinn/iroh endpoints to AlknetEndpoint via builder methods.
build_iroh_endpoint and build_quinn_server_config_from_rustls move
out of core — the assembly layer (or alknet-tls as a convenience)
builds the transport endpoints.
TCP+TLS accept loops call endpoint.dispatch(connection, alpn, fingerprint, remote_addr) — the same dispatch path quinn and iroh
use. No duplicated build_auth_context or handler-lookup logic at the
assembly layer. The hub's TCP+TLS accept loop is still a sibling (it
owns its TcpListener and TlsAcceptor), but its dispatch is
first-class — it calls into the endpoint, not around it.
This resolves the ADR-010 Amendment 1 workaround. Amendment 1 made
TCP+TLS a sibling because the endpoint couldn't share its dispatch
logic. Now it can — dispatch is public. The sibling framing (the
accept loop lives outside the endpoint struct) survives; the
second-class dispatch (duplicated logic) doesn't.
2.4 What the assembly layer does
For a hub serving native clients (raw key) and browsers (X.509):
- Read
StaticConfig. - Build the raw-key
TlsServerConfigfromTlsIdentity::RawKey(key). - Build the X.509
TlsServerConfigfromTlsIdentity::Acme { ... }(orX509). - Build the quinn endpoint for native clients:
raw_key_tls.for_quinn()→quinn::Endpoint::server(). - Build the iroh endpoint:
iroh::Endpoint::builder().secret_key(...). - Construct
AlknetEndpoint::new(registry, ...)with.with_quinn()and.with_iroh(). - Build the TCP+TLS accept loop for native fallback:
raw_key_tls.for_tcp_tls()→TcpListener+TlsAcceptor→endpoint.dispatch(from_bidi(tls_stream, alpn, addr), alpn, fp, addr). - Build the TCP+TLS accept loop for HTTPS:
x509_tls.for_tcp_tls()→TcpListener+TlsAcceptor→endpoint.dispatch(...). - (Future) Build the quinn endpoint for WebTransport:
x509_tls.for_quinn()→quinn::Endpoint::server(). - Spawn the TCP+TLS accept loops (they call
endpoint.dispatch). - Spawn
endpoint.run()— runs the quinn/iroh accept loops.
For a bare-bones P2P node (no public IP, no ACME, no browsers): just
step 1, step 5, step 6 (with only .with_iroh()), step 11. No
TlsServerConfig needed at all.
2.5 What this resolves
- C5 (iroh path ownership): the endpoint doesn't build iroh; the assembly layer does. No ambiguity.
- TCP+TLS second-class dispatch:
dispatchis public; TCP+TLS accept loops call the same path as quinn/iroh. No duplicatedbuild_auth_contextor handler-lookup logic. ADR-010 Amendment 1's workaround is retired. - The single-
Arc<TlsServerConfig>problem: ADR-082 currently specifiesAlknetEndpoint::newtakingArc<TlsServerConfig>, but a hub has two configs (raw key + X.509). With the endpoint taking no TLS config, this disappears. - The
static_config.tls_identityreading: the endpoint no longer reads it. The assembly layer reads it and builds the transports. - The
acme_state_handleon the endpoint: the ACME task handle lives onTlsServerConfig(the assembly layer owns it), not on the endpoint.
3. Interaction with alknet-tls extraction (ADR-082)
3.1 The TLS extraction is the trigger, not the whole refactor
The TLS extraction (alknet-tls crate with TlsServerConfig) is what
surfaced the endpoint's conflation. But the endpoint refactor is a
separate, larger change that ADR-082 should reference but not contain.
3.2 ADR-082 changes
With the endpoint refactor, ADR-082's AlknetEndpoint::new signature
changes. Instead of:
pub async fn new(
static_config: &StaticConfig,
tls_config: Arc<TlsServerConfig>, // ← was the new param
handlers: HandlerRegistry,
dynamic: Arc<ArcSwap<DynamicConfig>>,
identity_provider: Arc<dyn IdentityProvider>,
) -> Result<Self, EndpointError>;
The endpoint takes no TLS config at all (see §2.3). ADR-082 should
reference the endpoint refactor ADR (to be written) for the new
signature, and focus on what alknet-tls provides:
TlsServerConfig and its accessors.
3.3 What moves out of alknet-core/endpoint.rs
| Code | Destination |
|---|---|
build_rustls_server_config() |
alknet-tls |
TlsSetup / ACME state machine |
alknet-tls (TlsServerConfig::new ACME path) |
RawKeyCertResolver |
alknet-tls |
Ed25519SigningKey |
alknet-tls |
AcceptAnyCertVerifier |
alknet-tls |
SelfSignedCert / generate_self_signed_cert() |
alknet-tls |
load_cert_chain() / load_private_key() |
alknet-tls |
build_quinn_server_config_from_rustls() |
alknet-tls (for_quinn()) |
build_iroh_endpoint() |
Assembly layer (or a helper; construction decision is assembly-layer) |
What stays in alknet-core/endpoint.rs:
AlknetEndpointstruct (accept-loop runner + publicdispatch)HandlerRegistrydispatch(public — takes ALPN, fingerprint,Connection; callsbuild_auth_context, looks up handler, spawns)dispatch_quinn/dispatch_iroh(private — do transport-specific extraction, then calldispatch)run_quinn_accept_loop/run_iroh_accept_loopextract_quinn_alpn/extract_quinn_client_fingerprint/extract_iroh_client_fingerprint- The
acme-tls/1early-return guard (insidedispatch_quinn; the guard is quinn-specific — ACME challenges arrive over QUIC. TCP+TLS has no ACME ALPN guard becauseacme-tls/1is only negotiated over QUIC for the TLS-ALPN-01 challenge. A TCP+TLS listener serving HTTPS does not advertiseacme-tls/1.)
build_auth_context becomes a private helper called by dispatch,
not a standalone function — it's the shared core of the dispatch path
and should not be called directly by the assembly layer. The assembly
layer calls dispatch, which calls build_auth_context internally.
What stays in alknet-core/config.rs:
TlsIdentityenum (config type)Ed25519SecretKey(config type)AcmeDirectory(config type)StaticConfig(config type)
What stays in alknet-core/fingerprint.rs (pending OQ-59):
fingerprint_from_cert_der,extract_ed25519_raw_key_from_spki
3.4 Ordering question: tls first or core refactor first?
This is deferred to the task-decomposition phase. Both are
interlocked: the TLS extraction removes code from endpoint.rs, and the
endpoint refactor changes what stays. The natural ordering might be:
- Endpoint refactor first — change
AlknetEndpointto a pure accept-loop runner, movebuild_iroh_endpointandbuild_quinn_server_config_from_rustlsout of core. At this point, the TLS setup code still exists in core but is called by the assembly layer instead of the endpoint. - TLS extraction second — move the TLS setup code into
alknet-tls, buildTlsServerConfig, replace the assembly layer's direct calls withTlsServerConfig::new/for_quinn()/for_tcp_tls().
But this is a task-decomposition decision, not an architecture
decision. The architecture is: both changes happen, the endpoint
becomes a pure accept-loop runner, and alknet-tls provides the
shareable cert config.
4. The remaining review items (from the tls spec review)
These are unaffected by the endpoint refactor. They're about the TLS crate's API surface and the OQ/ADR status.
4.1 C2: Define TlsError type
The TlsError type is used in every public signature but never
defined. Needs a variant list. The current code uses
EndpointError::TlsConfig(io::Error). The new type should cover:
- IO errors (file read failures for cert/key loading)
- rustls config errors (protocol version, cipher suite setup)
- Quinn config errors (
QuicServerConfig::try_fromfailure) - ACME feature-not-enabled (when
TlsIdentity::Acmeis configured without theacmefeature)
4.2 C3 + C6: ACME task lifecycle and cert renewal
The ACME state machine task is spawned by TlsServerConfig::new (ACME
path) and its JoinHandle is held on TlsServerConfig. The spec
currently says "dropping this handle does NOT stop the ACME state
machine" but never says what does stop it.
Cert renewal behavior (confirmed from the reverse-proxy reference):
| Identity | Renewal behavior | Drain needed? |
|---|---|---|
| ACME | Live — ResolvesServerCertAcme swaps cert in place; all listeners sharing the Arc<rustls::ServerConfig> pick up the renewed cert on the next TLS handshake |
No |
| X.509 (manual) | Loaded once at startup via with_single_cert; no live renewal |
Yes (restart to load new cert) |
| RawKey | No expiry — Ed25519 keys don't expire | N/A |
| SelfSigned | Dev only — restart to regenerate | N/A |
The ACME renewal mechanism: TlsServerConfig::new (ACME path) calls
state.resolver() to get an Arc<ResolvesServerCertAcme>, wires it
into the rustls::ServerConfig via with_cert_resolver(resolver), and
spawns the AcmeState event loop. When the event loop processes a
renewed cert (EventOk::DeployedNewCert), it updates the resolver's
internal cert in place. Since for_quinn() and for_tcp_tls() clone
the rustls::ServerConfig (which shares the Arc<ResolvesServerCertAcme>),
both the quinn endpoint and the TCP+TLS acceptor see the renewed cert
on the next handshake. No drain, no restart, no listener replacement.
The shutdown path: the ACME task's lifetime is the renewal lifetime.
Shutting it down stops renewal (but existing listeners keep serving
the last-known cert until it expires). Restarting the process
re-spawns the state machine, which deploys the cached cert
(DeployedCachedCert) and resumes renewal. The TlsServerConfig
needs a shutdown() method (or a Drop impl that aborts the handle)
so the assembly layer can cleanly stop the ACME task on process
shutdown.
4.3 C4: OQ-59 (fingerprint.rs location)
OQ-59 is open but the spec treats fingerprint.rs staying in core as
decided. The "Likely resolution" in OQ-59 is Option A (stay in core),
with solid rationale: alknet-call's client-side
FingerprintPinVerifier uses fingerprint functions and shouldn't
have to depend on alknet-tls (which would pull rustls + tokio +
optionally quinn/tokio-rustls/rustls-acme into every
client-only deployment). Recommendation: resolve OQ-59 to Option A
(stay in core) — it's a two-way door (moving a module later is a
refactor, not a wire-format change), and the rationale is clear.
4.4 W3: Feature-gate edge cases
TlsIdentity::Acmeconfigured without theacmefeature — should return a runtime error (matching the current code's behavior).default = []with no features —new()andrustls_config()work butfor_quinn/for_tcp_tlsdon't exist. This is a supported config for iroh-only deployments (noTlsServerConfigneeded at all — the assembly layer just uses theEd25519SecretKeydirectly).acmewithoutquinnortcp— ACME task spawns,rustls_config()available, but no transport accessor. Valid for a pure cert-provider use case (not currently expected, but not blocked).
4.5 W4: aws_lc_rs crypto provider ADR
The aws_lc_rs::default_provider() choice is a load-bearing decision
(FIPS status, platform support) with no ADR. Either add a clause to
ADR-082 (or ADR-027) recording the choice, or write a small standalone
ADR. The invariant "do not switch to ring or the process-default
provider without an ADR" is already in the spec; the decision record
should exist.
4.6 W5: ADR-082 status
ADR-082 is "Proposed." If implementation is about to begin (and the endpoint refactor is part of the same work), the ADR should move to "Accepted" — with OQ-59 noted as a non-blocking open question that can be resolved alongside implementation. "Proposed" for an actively-implemented extraction is a stale status.
5. The reverse-proxy as reference
The reverse-proxy (/workspace/@alkdev/reverse-proxy/) is a useful
reference for the TLS setup, with the caveat that it is X.509-only
(manual + ACME). The relevant code:
src/tls/acme.rs—AcmeTlsConfig::setup()constructs theAcmeStateand gets theresolver().spawn_acme_state()runs the event loop with atokio::select!against a shutdown signal (so the ACME task can be cleanly stopped, not just aborted).src/tls/acceptor.rs—TlsModeenum (ManualvsAcme) holds theArc<ServerConfig>and theArc<ResolvesServerCertAcme>.setup_tls()builds theTlsAcceptorfrom the server config.src/tls/config.rs—crypto_provider()constructs theaws_lc_rsprovider with restricted cipher suites. Manual cert loading viarustls-pemfile.
What the reverse-proxy does that alknet-tls should learn from:
- The ACME task is spawned with a shutdown signal (
tokio::select!againstshutdown_rx.changed()), not just a bareJoinHandle. This is cleaner than abort — the state machine gets to finish its current operation and exit gracefully. (Relevant to C3.) - The
TlsModeenum separates the static config from the runtime objects —Manual(Arc<ServerConfig>)vsAcme { default_config, resolver }. This mirrors theTlsServerConfig/TlsIdentitysplit. - The
ResolvesServerCertAcmeis the live renewal mechanism — theTlsAcceptorbuilt fromdefault_config(which holds anArcto the resolver) picks up renewed certs on the next handshake. No drain, no restart. (Relevant to C6.)
What the reverse-proxy doesn't cover (because it's X.509-only):
- RFC 7250 raw keys (the
RawKeyCertResolver/Ed25519SigningKeypath) - Multiple transport sharing (it's TCP-only; no quinn, no iroh)
- The endpoint-as-accept-loop-runner pattern (it has its own
serve_https_listenerthat's simpler thanAlknetEndpoint)
6. Open questions for the next session
-
C2 (
TlsErrorvariants): what error types shouldTlsErrorwrap? The currentEndpointError::TlsConfig(io::Error)is too narrow — it doesn't coverrustls::Erroror ACME-specific errors. Need a variant list. -
C3 (ACME shutdown): should
TlsServerConfighave ashutdown()method (clean, explicit) or aDropimpl (automatic, but surprising)? The reverse-proxy uses a shutdown signal (tokio::select!againstshutdown_rx.changed()) rather thanabort(). Should alknet-tls do the same? -
C4 (OQ-59): resolve to Option A (stay in core) or keep open? Recommendation: resolve to Option A.
-
Endpoint refactor ADR: draft ADR-083 (amending ADR-010) for the endpoint-as-accept-loop-runner shape, or fold it into ADR-082? Recommendation: standalone ADR — it changes ADR-010's endpoint design, which is a broader change than the TLS extraction.
-
Ordering: does the endpoint refactor land before the TLS extraction, or after? Deferred to task decomposition.