Files
alknet/tasks/endpoint/accept-tcp-tls.md
T
deepseek-v4-pro 67d2f4affb feat(endpoint): decompose Phase 2 into 9 tasks for alknet-endpoint crate extraction
- 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).
2026-07-17 10:33:42 +00:00

170 lines
6.5 KiB
Markdown

---
id: endpoint/accept-tcp-tls
name: Implement TCP+TLS accept loop and extractors in alknet-endpoint (new code)
status: pending
depends_on: [endpoint/dispatch]
scope: narrow
risk: medium
impact: component
level: implementation
---
## Description
Phase 2, Task 7 of the crate extraction. Implement the TCP+TLS accept loop and
transport-specific extractors in `crates/alknet-endpoint/src/accept/tcp_tls.rs`.
**This is new code** — the current `endpoint.rs` does not have a TCP+TLS accept loop.
It must be written fresh, following the same pattern as the quinn and iroh accept loops
but adapted for TCP+TLS transport.
### Design
The TCP+TLS accept loop follows the same pattern as quinn and iroh:
1. `tcp_listener.accept()` → get a `TcpStream`
2. `tls_acceptor.accept(tcp_stream)` → TLS handshake → get a `TlsStream<TcpStream>`
3. Extract ALPN from the TLS session
4. Extract client fingerprint from the peer certificate chain
5. Convert to `Connection::from_bidi(tls_stream)` (the `TlsStream<TcpStream>` is
`AsyncRead + AsyncWrite`)
6. Call `crate::dispatch::dispatch_connection()`
### Implementation sketch
```rust
// accept/tcp_tls.rs
#[cfg(feature = "tcp")]
use std::net::SocketAddr;
#[cfg(feature = "tcp")]
use std::sync::Arc;
#[cfg(feature = "tcp")]
use tokio::sync::watch;
#[cfg(feature = "tcp")]
use tracing::{debug, warn};
#[cfg(feature = "tcp")]
use alknet_core::auth::IdentityProvider;
#[cfg(feature = "tcp")]
use alknet_core::types::Connection;
#[cfg(feature = "tcp")]
use crate::registry::HandlerRegistry;
#[cfg(feature = "tcp")]
pub(crate) async fn run_accept_loop(
listener: tokio::net::TcpListener,
acceptor: tokio_rustls::TlsAcceptor,
handlers: Arc<HandlerRegistry>,
identity_provider: Arc<dyn IdentityProvider>,
shutdown_rx: &mut watch::Receiver<bool>,
) {
loop {
tokio::select! {
_ = shutdown_rx.changed() => {
debug!("tcp+tls accept loop: shutdown signaled");
break;
}
result = listener.accept() => {
let (tcp_stream, remote_addr) = match result {
Ok(r) => r,
Err(e) => {
warn!("tcp+tls accept failed: {e}");
continue;
}
};
let acceptor = acceptor.clone();
let handlers = handlers.clone();
let identity_provider = identity_provider.clone();
tokio::spawn(async move {
let tls_stream = match acceptor.accept(tcp_stream).await {
Ok(s) => s,
Err(e) => {
warn!("tcp+tls TLS handshake failure: {e}");
return;
}
};
let (alpn, fingerprint) = extract_tls_session_info(&tls_stream);
let conn = Connection::from_bidi(tls_stream);
crate::dispatch::dispatch_connection(
conn, alpn, fingerprint, Some(remote_addr),
&handlers, &identity_provider,
);
});
}
}
}
}
#[cfg(feature = "tcp")]
fn extract_tls_session_info(
tls_stream: &tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
) -> (Vec<u8>, Option<String>) {
let (_, session) = tls_stream.get_ref();
let alpn = session.alpn_protocol().map(|a| a.to_vec()).unwrap_or_default();
let fingerprint = session
.peer_certificates()
.and_then(|certs| certs.first())
.and_then(|cert| alknet_core::fingerprint::fingerprint_from_cert_der(cert.as_ref()));
(alpn, fingerprint)
}
```
### Key design decisions
1. **`Connection::from_bidi`**: The `TlsStream<TcpStream>` implements `AsyncRead + AsyncWrite`,
so it can be passed directly to `Connection::from_bidi`. No `QuicStream` wrapper needed
(that's the Phase 6 fix for `alknet-http`).
2. **ALPN extraction**: `session.alpn_protocol()` returns the negotiated ALPN from the TLS
session. This is the standard rustls API.
3. **Fingerprint extraction**: `session.peer_certificates()` returns the peer's certificate
chain. The leaf cert's fingerprint is extracted via `alknet_core::fingerprint::fingerprint_from_cert_der`.
4. **`remote_addr`**: Available from `TcpListener::accept()` — passed to dispatch.
5. **Feature gate**: All code gated on `#[cfg(feature = "tcp")]`.
### What stays in core
There is no existing TCP+TLS accept loop in `endpoint.rs` — this is entirely new code.
No duplication, no prune needed.
## Acceptance Criteria
- [ ] `accept/tcp_tls.rs` contains `run_accept_loop`, `extract_tls_session_info`
- [ ] `run_accept_loop` accepts TCP connections, performs TLS handshake, spawns handler task
- [ ] `run_accept_loop` handles shutdown signal via `watch::Receiver`
- [ ] `extract_tls_session_info` extracts ALPN from TLS session
- [ ] `extract_tls_session_info` extracts client cert fingerprint via `alknet_core::fingerprint`
- [ ] Accept loop calls `crate::dispatch::dispatch_connection()` with extracted values
- [ ] `Connection::from_bidi(tls_stream)` used (no hand-rolled wrapper)
- [ ] `remote_addr` passed from `TcpListener::accept()`
- [ ] All code gated on `#[cfg(feature = "tcp")]`
- [ ] All imports use `alknet_core::` (not `crate::` from core)
- [ ] `cargo check -p alknet-endpoint --features tcp` succeeds
- [ ] `cargo clippy -p alknet-endpoint --features tcp` succeeds with no warnings
- [ ] `cargo test -p alknet-core` still passes (old code untouched)
## References
- docs/research/alknet-crate-extraction/findings.md — Phase 2, accept/tcp_tls module
- docs/architecture/crates/endpoint/README.md — Accept loops (lines 177-193), TcpTlsListener (lines 164-175)
- docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md — ADR-083
- docs/architecture/decisions/065-connection-from-stream-generic-single-stream.md — ADR-065 (Connection::from_bidi)
- crates/alknet-core/src/endpoint.rs — lines 287-388 (quinn accept loop, reference pattern)
## Notes
> This is the only genuinely new code in Phase 2 — the current `endpoint.rs` has no
> TCP+TLS accept loop. It follows the same pattern as the quinn and iroh accept loops
> but uses `TcpListener::accept()` + `TlsAcceptor::accept()` + `Connection::from_bidi`.
> The `TlsStream<TcpStream>` is already `AsyncRead + AsyncWrite` — no wrapper needed.
> Risk is medium because it's new code, but the pattern is well-established by the
> quinn and iroh loops.
## Summary
> To be filled on completion