Files
alknet/tasks/endpoint/dispatch.md
T
deepseek-v4-pro 5467c30892 feat(endpoint): add tests and mark tasks 1-7 as completed
- Add 7 registry tests (handler_registry_*) to registry.rs
- Add 4 dispatch tests (build_auth_context_*, dispatch_decision_logic_*) to dispatch.rs
- Add 6 endpoint tests to endpoint.rs:
  - debug_for_alknet_endpoint_is_implemented_without_panicking
  - endpoint_constructs_with_iroh_raw_key_identity (adapted for new API)
  - iroh_endpoint_runs_accept_loop_and_shutdown (adapted for new API)
  - with_iroh_sets_field (replaces has_iroh_identity_true_for_raw_key)
  - without_iroh_field_is_none (replaces has_iroh_identity_false_for_x509)
  - endpoint_works_without_iroh (replaces has_iroh_identity_false_when_no_identity)
- Add async-trait as dev-dependency
- All 17 tests pass with iroh feature
- All 8 tests pass without features
- Workspace tests all pass (no breakage)
- Mark tasks 1-7 as completed
2026-07-17 10:53:25 +00:00

163 lines
6.3 KiB
Markdown

---
id: endpoint/dispatch
name: Implement public dispatch, build_auth_context, and ACME guard
status: completed
depends_on: [endpoint/endpoint-core]
scope: narrow
risk: low
impact: component
level: 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::spawn`s 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:
```rust
/// 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`:
1. **Takes `Connection` not `quinn::Connection` / `iroh::Connection`**: The accept loop
converts the transport-specific connection to `alknet_core::Connection` before calling
`dispatch`. This is the same pattern the old code used (`Connection::from_quinn_with_alpn`,
`Connection::from_iroh`).
2. **Takes pre-extracted values**: `alpn`, `fingerprint`, `remote_addr` are passed in
rather than extracted inside `dispatch`. The extraction is transport-specific and
lives in the accept loop modules.
3. **No `EndpointError`**: Handler-not-found is swallowed (close + log). No error return.
4. **ACME guard is `#[cfg(feature = "acme")]`**: The `acme` feature is not on
`alknet-endpoint` itself (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:
```rust
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_addr`
- [ ] `dispatch()` performs ACME guard (`acme-tls/1` → close + return) when `acme` feature enabled
- [ ] `dispatch()` looks up handler by ALPN, closes connection + logs warning on miss
- [ ] `dispatch()` calls `build_auth_context` and `tokio::spawn`s the handler
- [ ] `build_auth_context()` resolves identity from fingerprint via `IdentityProvider`
- [ ] `build_auth_context()` returns `AuthContext` with all fields populated
- [ ] No `EndpointError` — handler-not-found is swallowed (close + log)
- [ ] All imports use `alknet_core::` (not `crate::`)
- [ ] Feature gates: `acme` guard is `#[cfg(feature = "acme")]`; rest is always available
- [ ] `cargo check -p alknet-endpoint` succeeds (all feature combos)
- [ ] `cargo clippy -p alknet-endpoint` succeeds with no warnings
- [ ] `cargo test -p alknet-core` still 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
> `dispatch` is 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 old `dispatch_quinn` and `dispatch_iroh` are merged into one
> transport-agnostic `dispatch`. The old code in `endpoint.rs` is NOT deleted.
## Summary
> To be filled on completion