- 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).
6.3 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| endpoint/dispatch | Implement public dispatch, build_auth_context, and ACME guard | pending |
|
narrow | low | component | implementation |
Description
Phase 2, Task 4 of the crate extraction. Implement the shared dispatch method,
build_auth_context, and the ACME acme-tls/1 guard in
crates/alknet-endpoint/src/dispatch.rs.
dispatch is the shared dispatch path for every transport — the endpoint's own accept
loops call it after transport-specific extraction (ALPN, fingerprint, remote address),
and external dispatch callers (SSH channels, future WebTransport streams) call it after
their own extraction. It is public and synchronous (non-async): performs the
ACME guard, handler lookup, build_auth_context, and tokio::spawns the handler.
Types to extract / build
From endpoint.rs lines 330-490:
| Type/Function | Lines | Destination |
|---|---|---|
dispatch_quinn() |
330-365 | Adapted into dispatch() (public, transport-agnostic) |
extract_quinn_alpn() |
368-378 | Stays in accept/quinn.rs (Task 5) |
extract_quinn_client_fingerprint() |
381-388 | Stays in accept/quinn.rs (Task 5) |
dispatch_iroh() |
440-466 | Adapted into dispatch() (public, transport-agnostic) |
extract_iroh_client_fingerprint() |
469-472 | Stays in accept/iroh.rs (Task 6) |
build_auth_context() |
475-490 | dispatch.rs |
dispatch (public)
The new dispatch is transport-agnostic — it receives already-extracted values instead
of extracting them from a transport-specific connection:
/// Dispatch an accepted connection to its `ProtocolHandler` by ALPN.
///
/// Synchronous (non-async): performs the ACME guard, handler lookup,
/// `build_auth_context`, and `tokio::spawn`s the handler. Returns
/// immediately after spawning.
///
/// Public for connection-internal multiplexing shapes (SSH channels,
/// future WebTransport streams) that the endpoint can't own.
pub fn dispatch(
&self,
connection: Connection,
alpn: Vec<u8>,
fingerprint: Option<String>,
remote_addr: Option<SocketAddr>,
) {
// ACME guard
#[cfg(feature = "acme")]
if alpn == b"acme-tls/1" {
debug!("acme-tls/1 challenge connection; closing");
connection.close(0u32.into(), b"acme done");
return;
}
let handler = match self.handlers.get(&alpn) {
Some(h) => h.clone(),
None => {
connection.close(0u32.into(), b"no handler");
warn!("dispatch: no handler for ALPN {:?}", String::from_utf8_lossy(&alpn));
return;
}
};
let auth = build_auth_context(&alpn, remote_addr, fingerprint, &self.identity_provider);
tokio::spawn(async move {
if let Err(e) = handler.handle(connection, &auth).await {
error!("handler returned error: {e}");
}
});
}
Key differences from the old dispatch_quinn / dispatch_iroh:
-
Takes
Connectionnotquinn::Connection/iroh::Connection: The accept loop converts the transport-specific connection toalknet_core::Connectionbefore callingdispatch. This is the same pattern the old code used (Connection::from_quinn_with_alpn,Connection::from_iroh). -
Takes pre-extracted values:
alpn,fingerprint,remote_addrare passed in rather than extracted insidedispatch. The extraction is transport-specific and lives in the accept loop modules. -
No
EndpointError: Handler-not-found is swallowed (close + log). No error return. -
ACME guard is
#[cfg(feature = "acme")]: Theacmefeature is not onalknet-endpointitself (the endpoint doesn't build ACME configs), but the guard is kept for forward-compatibility. If the assembly layer registers an ACME handler, the guard prevents it from being dispatched as a normal protocol handler.
build_auth_context
Extracted from endpoint.rs lines 475-490, with imports updated:
pub(crate) fn build_auth_context(
alpn: &[u8],
remote_addr: Option<SocketAddr>,
tls_client_fingerprint: Option<String>,
identity_provider: &Arc<dyn IdentityProvider>,
) -> AuthContext {
let identity = tls_client_fingerprint
.as_ref()
.and_then(|fp| identity_provider.resolve_from_fingerprint(fp));
AuthContext {
identity,
alpn: alpn.to_vec(),
remote_addr,
tls_client_fingerprint,
}
}
What stays in core
The old dispatch_quinn, dispatch_iroh, and build_auth_context in endpoint.rs are
not deleted — they stay as duplicates. The prune happens in Phase 4.
Acceptance Criteria
dispatch()is public, synchronous, takes&self,Connection,alpn,fingerprint,remote_addrdispatch()performs ACME guard (acme-tls/1→ close + return) whenacmefeature enableddispatch()looks up handler by ALPN, closes connection + logs warning on missdispatch()callsbuild_auth_contextandtokio::spawns the handlerbuild_auth_context()resolves identity from fingerprint viaIdentityProviderbuild_auth_context()returnsAuthContextwith all fields populated- No
EndpointError— handler-not-found is swallowed (close + log) - All imports use
alknet_core::(notcrate::) - Feature gates:
acmeguard is#[cfg(feature = "acme")]; rest is always available cargo check -p alknet-endpointsucceeds (all feature combos)cargo clippy -p alknet-endpointsucceeds with no warningscargo test -p alknet-corestill passes (old code untouched)
References
- docs/research/alknet-crate-extraction/findings.md — Phase 2, dispatch module
- docs/architecture/crates/endpoint/README.md — dispatch spec (lines 195-211)
- docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md — ADR-083
- crates/alknet-core/src/endpoint.rs — lines 330-490 (source code to extract/adapt)
Notes
dispatchis the shared dispatch path for all transports. It's public because connection-internal multiplexing shapes (SSH channels, future WT streams) need to call it after their own extraction. The accept loops (quinn, iroh, TCP+TLS) call it internally. The olddispatch_quinnanddispatch_irohare merged into one transport-agnosticdispatch. The old code inendpoint.rsis NOT deleted.
Summary
To be filled on completion