- 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.5 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| endpoint/accept-tcp-tls | Implement TCP+TLS accept loop and extractors in alknet-endpoint (new code) | pending |
|
narrow | medium | component | 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:
tcp_listener.accept()→ get aTcpStreamtls_acceptor.accept(tcp_stream)→ TLS handshake → get aTlsStream<TcpStream>- Extract ALPN from the TLS session
- Extract client fingerprint from the peer certificate chain
- Convert to
Connection::from_bidi(tls_stream)(theTlsStream<TcpStream>isAsyncRead + AsyncWrite) - Call
crate::dispatch::dispatch_connection()
Implementation sketch
// 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
-
Connection::from_bidi: TheTlsStream<TcpStream>implementsAsyncRead + AsyncWrite, so it can be passed directly toConnection::from_bidi. NoQuicStreamwrapper needed (that's the Phase 6 fix foralknet-http). -
ALPN extraction:
session.alpn_protocol()returns the negotiated ALPN from the TLS session. This is the standard rustls API. -
Fingerprint extraction:
session.peer_certificates()returns the peer's certificate chain. The leaf cert's fingerprint is extracted viaalknet_core::fingerprint::fingerprint_from_cert_der. -
remote_addr: Available fromTcpListener::accept()— passed to dispatch. -
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.rscontainsrun_accept_loop,extract_tls_session_inforun_accept_loopaccepts TCP connections, performs TLS handshake, spawns handler taskrun_accept_loophandles shutdown signal viawatch::Receiverextract_tls_session_infoextracts ALPN from TLS sessionextract_tls_session_infoextracts client cert fingerprint viaalknet_core::fingerprint- Accept loop calls
crate::dispatch::dispatch_connection()with extracted values Connection::from_bidi(tls_stream)used (no hand-rolled wrapper)remote_addrpassed fromTcpListener::accept()- All code gated on
#[cfg(feature = "tcp")] - All imports use
alknet_core::(notcrate::from core) cargo check -p alknet-endpoint --features tcpsucceedscargo clippy -p alknet-endpoint --features tcpsucceeds with no warningscargo test -p alknet-corestill 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.rshas no TCP+TLS accept loop. It follows the same pattern as the quinn and iroh accept loops but usesTcpListener::accept()+TlsAcceptor::accept()+Connection::from_bidi. TheTlsStream<TcpStream>is alreadyAsyncRead + 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