- endpoint/crate-init: initialize crate, Cargo.toml, module skeleton - endpoint/registry: extract HandlerRegistry (~50 lines) - endpoint/endpoint-core: AlknetEndpoint fresh build against ADR-083 shape - endpoint/dispatch: public dispatch, build_auth_context, ACME guard - endpoint/accept-quinn: quinn accept loop + extractors (extracted) - endpoint/accept-iroh: iroh accept loop + extractors (extracted) - endpoint/accept-tcp-tls: TCP+TLS accept loop (new code) - endpoint/tests: move + adapt 17 tests - endpoint/review-endpoint: review checkpoint 7 generations, 3 parallel tasks (accept loops), no cycles. Depends on tls/review-tls (Phase 1 complete).
8.2 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| endpoint/accept-quinn | Implement quinn accept loop and extractors in alknet-endpoint | pending |
|
narrow | low | component | implementation |
Description
Phase 2, Task 5 of the crate extraction. Implement the quinn accept loop and
transport-specific extractors in crates/alknet-endpoint/src/accept/quinn.rs.
Extract from crates/alknet-core/src/endpoint.rs lines 287-388:
run_quinn_accept_loop, extract_quinn_alpn, extract_quinn_client_fingerprint.
The old code stays in endpoint.rs (duplicated) — no breakage.
Types to extract
From endpoint.rs lines 287-388:
| Type/Function | Lines | Destination |
|---|---|---|
run_quinn_accept_loop() |
288-327 | accept/quinn.rs |
extract_quinn_alpn() |
368-378 | accept/quinn.rs |
extract_quinn_client_fingerprint() |
381-388 | accept/quinn.rs |
Adaptations
-
run_quinn_accept_loop→run_accept_loop: Rename torun_accept_loopsince it's already in thequinnmodule. Takesquinn::Endpoint,Arc<HandlerRegistry>,Arc<dyn IdentityProvider>,&mut watch::Receiver<bool>. -
Connection conversion: After the TLS handshake completes, convert the
quinn::Connectiontoalknet_core::ConnectionviaConnection::from_quinn_with_alpn(connection, alpn.clone())— same as the old code. -
Call
dispatchinstead ofdispatch_quinn: The accept loop extracts ALPN and fingerprint, then callsAlknetEndpoint::dispatch(connection, alpn, fingerprint, remote_addr). The old code calleddispatch_quinn(connection, &handlers, &identity_provider)which did the extraction internally. The new code extracts first, then dispatches. -
Imports: Update
crate::imports toalknet_core::andcrate::(forcrate::dispatch::build_auth_context— butbuild_auth_contextis called insidedispatchnow, not in the accept loop). -
Feature gates: All code in this module is gated on
#[cfg(feature = "quinn")].
Implementation sketch
// accept/quinn.rs
#[cfg(feature = "quinn")]
use std::net::SocketAddr;
#[cfg(feature = "quinn")]
use std::sync::Arc;
#[cfg(feature = "quinn")]
use tokio::sync::watch;
#[cfg(feature = "quinn")]
use tracing::{debug, warn};
#[cfg(feature = "quinn")]
use alknet_core::auth::IdentityProvider;
#[cfg(feature = "quinn")]
use alknet_core::types::Connection;
#[cfg(feature = "quinn")]
use crate::registry::HandlerRegistry;
#[cfg(feature = "quinn")]
pub(crate) async fn run_accept_loop(
quinn: quinn::Endpoint,
handlers: Arc<HandlerRegistry>,
identity_provider: Arc<dyn IdentityProvider>,
shutdown_rx: &mut watch::Receiver<bool>,
) {
loop {
tokio::select! {
_ = shutdown_rx.changed() => {
debug!("quinn accept loop: shutdown signaled");
break;
}
incoming = quinn.accept() => {
let Some(incoming) = incoming else {
debug!("quinn accept loop: endpoint closed");
break;
};
let connecting = match incoming.accept() {
Ok(c) => c,
Err(e) => {
warn!("quinn accept failed: {e}");
continue;
}
};
let handlers = handlers.clone();
let identity_provider = identity_provider.clone();
tokio::spawn(async move {
let connection = match connecting.await {
Ok(conn) => conn,
Err(e) => {
warn!("quinn TLS handshake failure: {e}");
return;
}
};
let alpn = extract_alpn(&connection);
let remote_addr = Some(connection.remote_address());
let fingerprint = extract_client_fingerprint(&connection);
let conn = Connection::from_quinn_with_alpn(connection, alpn.clone());
// dispatch is called by the endpoint — but the accept loop
// doesn't have a reference to the endpoint. Instead, the
// endpoint passes a dispatch function or the accept loop
// calls a free function.
//
// Design choice: the accept loop calls
// crate::dispatch::dispatch_connection(...) which takes
// the extracted values + handlers + identity_provider.
// This avoids coupling the accept loop to AlknetEndpoint.
crate::dispatch::dispatch_connection(
conn, alpn, fingerprint, remote_addr,
&handlers, &identity_provider,
);
});
}
}
}
}
#[cfg(feature = "quinn")]
pub(crate) fn extract_alpn(connection: &quinn::Connection) -> Vec<u8> {
use quinn::crypto::rustls::HandshakeData;
if let Some(data) = connection.handshake_data() {
if let Ok(hs) = data.downcast::<HandshakeData>() {
if let Some(protocol) = hs.protocol {
return protocol;
}
}
}
Vec::new()
}
#[cfg(feature = "quinn")]
pub(crate) fn extract_client_fingerprint(connection: &quinn::Connection) -> Option<String> {
let identity = connection.peer_identity()?;
let certs = identity
.downcast::<Vec<rustls::pki_types::CertificateDer>>()
.ok()?;
let leaf = certs.first()?;
alknet_core::fingerprint::fingerprint_from_cert_der(leaf.as_ref())
}
Design note: accept loop → dispatch coupling
The accept loop needs to call dispatch but doesn't have a reference to AlknetEndpoint.
Two approaches:
Option A (recommended): The accept loop calls a free function in crate::dispatch
that takes (Connection, alpn, fingerprint, remote_addr, &HandlerRegistry, &IdentityProvider).
This is the same pattern the old code used — dispatch_quinn was a free function that
took &HandlerRegistry and &IdentityProvider. The AlknetEndpoint::dispatch method
delegates to this same free function.
Option B: The accept loop receives a closure or Arc<AlknetEndpoint>. This couples
the accept loop to the endpoint struct, which is unnecessary — the accept loop only needs
the handler registry and identity provider.
Use Option A — it's the simplest, matches the old code's pattern, and keeps the accept loop decoupled from the endpoint struct.
What stays in core
The old run_quinn_accept_loop, extract_quinn_alpn, extract_quinn_client_fingerprint
in endpoint.rs are not deleted — they stay as duplicates. The prune happens in Phase 4.
Acceptance Criteria
accept/quinn.rscontainsrun_accept_loop,extract_alpn,extract_client_fingerprintrun_accept_loopspawns a task per accepted connection, handles shutdown signalextract_alpnextracts the negotiated ALPN from quinn handshake dataextract_client_fingerprintextracts the client cert fingerprint viaalknet_core::fingerprint- Accept loop calls
crate::dispatch::dispatch_connection()(free function) with extracted values - All code gated on
#[cfg(feature = "quinn")] - All imports use
alknet_core::(notcrate::from core) cargo check -p alknet-endpoint --features quinnsucceedscargo clippy -p alknet-endpoint --features quinnsucceeds with no warningscargo test -p alknet-corestill passes (old code untouched)
References
- docs/research/alknet-crate-extraction/findings.md — Phase 2, accept/quinn module
- docs/architecture/crates/endpoint/README.md — Accept loops (lines 177-193)
- crates/alknet-core/src/endpoint.rs — lines 287-388 (source code to extract)
Notes
This is a straightforward extraction — the quinn accept loop logic doesn't change. The main adaptation is calling
crate::dispatch::dispatch_connection()instead of the olddispatch_quinn(). The accept loop is a free function, not a method onAlknetEndpoint, to keep it decoupled. The old code inendpoint.rsis NOT deleted.
Summary
To be filled on completion