Skip to main content

subc_daemon/
server.rs

1use std::{
2    error::Error,
3    fmt, io,
4    net::SocketAddr,
5    sync::Arc,
6    time::{Duration, Instant},
7};
8
9use subc_transport::{authenticate_server, AuthError, DAEMON_ID_LEN, WATCHDOG_CLIENT_ROLE};
10use tokio::{
11    io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter},
12    net::TcpListener,
13    sync::{mpsc, Semaphore},
14    task::{JoinHandle, JoinSet},
15    time::timeout,
16};
17use tracing::{debug, warn};
18
19use crate::{
20    forwarding::{CloseReason, ConnectionCloseReceiver},
21    observability::ConnectedClients,
22    read_frame,
23    router::{FrameSink, RouteCtx, Router},
24    write_frame, FrameIoError, RouterError,
25};
26
27pub const CONNECTION_EGRESS_BUFFER: usize = 64;
28/// A pending `route.open` owns one slot in the connection's shared egress queue
29/// until its module answers. Limit binds to one eighth of that queue so even a
30/// client spending its whole bind allowance leaves seven eighths available for
31/// data-plane responses and other control traffic.
32pub(crate) const MAX_PENDING_ROUTE_OPENS_PER_CONNECTION: usize = CONNECTION_EGRESS_BUFFER / 8;
33/// A reconnect herd may spread one target's opens over many client connections.
34/// Two safe per-connection bursts retain useful parallelism without restoring
35/// the hundreds-of-binds fanout that serial dispatch used to suppress.
36pub(crate) const MAX_PENDING_ROUTE_BINDS_PER_TARGET: usize =
37    MAX_PENDING_ROUTE_OPENS_PER_CONNECTION * 2;
38pub const DEFAULT_AUTH_DEADLINE: Duration = Duration::from_secs(2);
39// Sized for the restart-herd shape: after a daemon bounce, every live client
40// connection plus all supervised children re-dial within the same second
41// (~120+ observed on the 2026-07-14 fleet). Handshakes are cheap loopback
42// HMAC exchanges; the deadline, not the permit count, is the DoS bound.
43pub const DEFAULT_MAX_UNAUTHENTICATED_CONNECTIONS: usize = 256;
44const CLOSE_DRAIN_GRACE: Duration = Duration::from_secs(2);
45
46/// Authentication material and DoS bounds applied before a TCP connection may
47/// reach the frame router.
48#[derive(Clone)]
49pub struct ServerAuth {
50    key: Arc<[u8]>,
51    daemon_id: [u8; DAEMON_ID_LEN],
52    daemon_ver: Arc<str>,
53    deadline: Duration,
54    unauthenticated: Arc<Semaphore>,
55    connected_clients: ConnectedClients,
56}
57
58impl ServerAuth {
59    pub fn new(
60        key: Vec<u8>,
61        daemon_id: [u8; DAEMON_ID_LEN],
62        daemon_ver: impl Into<String>,
63    ) -> Self {
64        Self::with_limits(
65            key,
66            daemon_id,
67            daemon_ver,
68            DEFAULT_AUTH_DEADLINE,
69            DEFAULT_MAX_UNAUTHENTICATED_CONNECTIONS,
70        )
71    }
72
73    // Production limits are deliberately not config-routed: loosening pre-auth DoS posture changes attack surface.
74    pub fn with_limits(
75        key: Vec<u8>,
76        daemon_id: [u8; DAEMON_ID_LEN],
77        daemon_ver: impl Into<String>,
78        deadline: Duration,
79        max_unauthenticated: usize,
80    ) -> Self {
81        Self {
82            key: Arc::from(key),
83            daemon_id,
84            daemon_ver: Arc::from(daemon_ver.into()),
85            deadline,
86            unauthenticated: Arc::new(Semaphore::new(max_unauthenticated.max(1))),
87            connected_clients: ConnectedClients::new(),
88        }
89    }
90
91    pub fn with_connected_clients(mut self, connected_clients: ConnectedClients) -> Self {
92        self.connected_clients = connected_clients;
93        self
94    }
95}
96
97impl fmt::Debug for ServerAuth {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.debug_struct("ServerAuth")
100            .field("key", &"<redacted>")
101            .field("daemon_id", &self.daemon_id)
102            .field("daemon_ver", &self.daemon_ver)
103            .field("deadline", &self.deadline)
104            .finish_non_exhaustive()
105    }
106}
107
108/// Serve an already-bound TCP listener. Each accepted connection gets its own
109/// async task so concurrent clients do not block the accept loop.
110pub async fn serve_listener(
111    listener: TcpListener,
112    router: Arc<Router>,
113    auth: ServerAuth,
114) -> Result<(), ServerError> {
115    serve_listener_with_accept(listener.local_addr().ok(), router, auth, || {
116        listener.accept()
117    })
118    .await
119}
120
121async fn serve_listener_with_accept<A, F>(
122    local_addr: Option<SocketAddr>,
123    router: Arc<Router>,
124    auth: ServerAuth,
125    mut accept: A,
126) -> Result<(), ServerError>
127where
128    A: FnMut() -> F,
129    F: std::future::Future<Output = io::Result<(tokio::net::TcpStream, SocketAddr)>>,
130{
131    loop {
132        let (stream, peer_addr) = match accept().await {
133            Ok(accepted) => accepted,
134            Err(source) => {
135                let kind = source.kind();
136                // Aborted/reset connections and interrupted syscalls affect one accept only.
137                // File-descriptor or socket-buffer exhaustion needs a short backoff to avoid spinning;
138                // other errors may mean the listener itself is unusable.
139                let exhausted = accept_resource_exhausted(&source);
140                if matches!(
141                    kind,
142                    io::ErrorKind::ConnectionAborted
143                        | io::ErrorKind::ConnectionReset
144                        | io::ErrorKind::Interrupted
145                ) || exhausted
146                {
147                    warn!(?local_addr, error = %source, "temporary TCP accept failure");
148                    if exhausted {
149                        tokio::time::sleep(Duration::from_millis(75)).await;
150                    }
151                    continue;
152                }
153                return Err(ServerError::Accept { local_addr, source });
154            }
155        };
156        // Every route frame is a discrete message whose reply the peer is waiting
157        // for, so there is never a later write for Nagle to coalesce with -- it can
158        // only hold a frame back until an ACK arrives.
159        //
160        // MEASURED: no effect on this transport. A 50-sample-per-arm sweep from
161        // 1 to 32 KiB over the full client->daemon->module->client path showed a
162        // flat 0.28-0.38ms p50 with no step at any buffer boundary, because a
163        // loopback ACK returns in microseconds and never reaches the delayed-ACK
164        // timer that makes Nagle expensive on a real network. Kept anyway: it is
165        // one syscall at accept, it removes the mechanism rather than relying on
166        // loopback staying fast, and Windows loopback was not part of that sweep.
167        // Do not cite it as a latency fix -- the measurement says it is not one.
168        //
169        // Failure is not fatal: the connection works, and refusing to serve a
170        // client over a socket option would be worse than anything it saves.
171        if let Err(source) = stream.set_nodelay(true) {
172            warn!(?peer_addr, error = %source, "could not disable Nagle on accepted connection");
173        }
174        debug!(?peer_addr, ?local_addr, "accepted subc TCP connection");
175        let router = Arc::clone(&router);
176        let auth = auth.clone();
177        tokio::spawn(async move {
178            if let Err(err) = handle_connection(stream, router, auth).await {
179                if err.is_quiet_reject() {
180                    debug!(?peer_addr, error = %err, "subc TCP connection rejected before routing");
181                } else {
182                    warn!(?peer_addr, error = %err, "subc connection ended with error");
183                }
184            }
185        });
186    }
187}
188
189fn accept_resource_exhausted(error: &io::Error) -> bool {
190    // ENFILE/EMFILE/ENOBUFS by name rather than number: the numbers differ by
191    // platform (ENOBUFS is 55 on macOS and 105 on Linux, and each number means
192    // something unrelated on the other), so a literal list misclassifies.
193    // Winsock: WSAEMFILE and WSAENOBUFS.
194    #[cfg(unix)]
195    {
196        use rustix::io::Errno;
197        let exhausted = [Errno::NFILE, Errno::MFILE, Errno::NOBUFS];
198        error
199            .raw_os_error()
200            .is_some_and(|code| exhausted.iter().any(|e| e.raw_os_error() == code))
201    }
202    #[cfg(windows)]
203    {
204        matches!(error.raw_os_error(), Some(10024 | 10055))
205    }
206    #[cfg(not(any(unix, windows)))]
207    {
208        let _ = error;
209        false
210    }
211}
212
213/// Serve all already-bound loopback TCP listeners until one accept loop fails.
214pub async fn serve_listeners(
215    listeners: Vec<TcpListener>,
216    router: Arc<Router>,
217    auth: ServerAuth,
218) -> Result<(), ServerError> {
219    if listeners.is_empty() {
220        return Err(ServerError::NoListeners);
221    }
222
223    let (tx, mut rx) = mpsc::channel(listeners.len());
224    let mut accept_tasks = AbortTasksOnDrop::default();
225    for listener in listeners {
226        let router = Arc::clone(&router);
227        let auth = auth.clone();
228        let tx = tx.clone();
229        accept_tasks.push(tokio::spawn(async move {
230            let result = serve_listener(listener, router, auth).await;
231            let _ = tx.send(result).await;
232        }));
233    }
234    drop(tx);
235
236    rx.recv().await.unwrap_or(Ok(()))
237}
238
239#[derive(Default)]
240struct AbortTasksOnDrop {
241    handles: Vec<JoinHandle<()>>,
242}
243
244impl AbortTasksOnDrop {
245    fn push(&mut self, handle: JoinHandle<()>) {
246        self.handles.push(handle);
247    }
248}
249
250impl Drop for AbortTasksOnDrop {
251    fn drop(&mut self) {
252        for handle in &self.handles {
253            if !handle.is_finished() {
254                handle.abort();
255            }
256        }
257    }
258}
259
260#[derive(Debug)]
261enum ConnectionLoopExit {
262    PeerClosed,
263    CloseRequested(CloseReason),
264}
265
266/// Run the authenticated frame read -> route loop for one connection.
267///
268/// Every accepted TCP connection must complete the key-auth prelude before any
269/// envelope bytes are read by the router. Outbound frames flow through a bounded
270/// [`FrameSink`] drained by one writer task. Inbound dispatch remains serial for
271/// every frame except `route.open`; only that bind wait runs in a connection-owned
272/// task so it cannot hold unrelated frames behind a slow target module.
273pub async fn handle_connection<S>(
274    mut stream: S,
275    router: Arc<Router>,
276    auth: ServerAuth,
277) -> Result<(), ConnectionError>
278where
279    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
280{
281    // Over-cap connections WAIT for a handshake slot (bounded by the auth deadline)
282    // instead of being reset. A restart herd — every client and supervised child
283    // re-dialing the fresh daemon at once — otherwise loses supervised modules to
284    // the permit lottery: each reset burns a module restart-budget slot, and a
285    // module that treats auth failure as fatal can exhaust its budget into
286    // state=failed within the boot window (2026-07-14 aft outage). On loopback
287    // with the pre-auth HMAC deadline, a bounded queue is strictly safer than a
288    // reset.
289    //
290    // Pre-auth time is governed by TWO deliberately separate budgets: the queue
291    // wait below is bounded by `auth.deadline`, and `authenticate_server` then
292    // starts a FRESH `auth.deadline` for the handshake itself. Total pre-auth
293    // occupancy per connection is therefore up to 2x the configured deadline.
294    // This is intentional, not an accounting slip: charging queue time against
295    // the handshake budget would hand a herd-queued supervised module a
296    // near-zero handshake window under CPU saturation, recreating the fatal
297    // auth-failure -> restart-budget-burn path this queue exists to prevent.
298    // On a loopback-only, key-authenticated listener the doubled bound is a
299    // per-connection occupancy cost, not a meaningful DoS surface.
300    let permit =
301        match tokio::time::timeout(auth.deadline, auth.unauthenticated.clone().acquire_owned())
302            .await
303        {
304            Ok(Ok(permit)) => permit,
305            Ok(Err(_)) | Err(_) => {
306                let _ = stream.shutdown().await;
307                return Err(ConnectionError::UnauthenticatedCapacity);
308            }
309        };
310
311    let authenticated = authenticate_server(
312        &mut stream,
313        auth.key.as_ref(),
314        &auth.daemon_id,
315        auth.daemon_ver.as_ref(),
316        auth.deadline,
317    )
318    .await
319    .map_err(ConnectionError::Auth)?;
320    drop(permit);
321
322    let mut connection = router.begin_connection();
323    let connection_id = connection.id();
324    // `authenticated.role` is CLIENT-SUPPLIED and unverified -- the handshake
325    // proves possession of the connection key, nothing about who is calling. So
326    // this exclusion is safe ONLY while the role decides a reporting question and
327    // never an authorization one: the watchdog's own loopback probe would
328    // otherwise inflate the very count it exists to sanity-check.
329    //
330    // The consequence of it being unverified, stated so nobody has to rediscover
331    // it: ANY client holding the key can claim this role and omit itself from
332    // `connected_clients`. That is a gauge a caller can lie to, and it is
333    // acceptable because the gauge informs an operator rather than gating
334    // anything. IF A ROLE IS EVER USED TO DECIDE ADMISSION, CAPACITY, OR PRIVILEGE,
335    // this stops being safe and the role must be attested rather than declared --
336    // the daemon already has the mechanism for that in the spawn-nonce path used
337    // for module identity.
338    let _connected_client = (authenticated.role != WATCHDOG_CLIENT_ROLE)
339        .then(|| auth.connected_clients.open(connection_id));
340    let close_receiver = connection.take_close_receiver();
341    debug!(
342        connection_id = connection_id.get(),
343        "subc authenticated connection opened"
344    );
345
346    let (read_half, write_half) = tokio::io::split(stream);
347    // Authentication is complete before this buffer can read ahead. The connection loop
348    // retains a partial frame read across completed route.open tasks; dropping the read
349    // happens only when a frame finishes or the connection ends.
350    let mut read_half = BufReader::new(read_half);
351    let (tx, rx) = mpsc::channel::<crate::router::OutboundFrame>(CONNECTION_EGRESS_BUFFER);
352    let mut writer = tokio::spawn(drain_writer(write_half, rx));
353
354    let egress = FrameSink::new(tx);
355    let ctx = RouteCtx {
356        connection_id,
357        egress: egress.clone(),
358    };
359
360    let mut route_open_tasks = JoinSet::new();
361    let loop_result = connection_loop(
362        &mut read_half,
363        Arc::clone(&router),
364        ctx.clone(),
365        close_receiver,
366        &mut route_open_tasks,
367    )
368    .await;
369
370    // Every in-flight route.open must have FINISHED before `connection` drops,
371    // because that drop runs the forwarding cleanup for this id, and cleanup is
372    // also where the id's closing mark is lifted. Dropping a JoinSet only
373    // requests abort: a task already running on another worker keeps going
374    // until its next await, and could commit a route for this connection after
375    // cleanup has cleared it, with nothing left to refuse it and nothing to
376    // clean it up. shutdown() aborts and then waits.
377    route_open_tasks.shutdown().await;
378
379    drop(ctx);
380    drop(egress);
381    drop(connection);
382
383    let close_reason = match &loop_result {
384        Ok(ConnectionLoopExit::CloseRequested(reason)) => Some(reason.to_string()),
385        Ok(ConnectionLoopExit::PeerClosed) | Err(_) => None,
386    };
387    let writer_result = if close_reason.is_some() {
388        match timeout(CLOSE_DRAIN_GRACE, &mut writer).await {
389            Ok(result) => Some(result.map_err(ConnectionError::WriterTask)),
390            Err(_) => {
391                warn!(
392                    connection_id = connection_id.get(),
393                    grace = ?CLOSE_DRAIN_GRACE,
394                    "connection writer did not drain after close request; aborting writer task"
395                );
396                writer.abort();
397                let _ = writer.await;
398                None
399            }
400        }
401    } else {
402        Some(writer.await.map_err(ConnectionError::WriterTask))
403    };
404
405    let result = if let Some(reason) = close_reason.as_deref() {
406        match writer_result {
407            Some(Ok(Ok(()))) | None => Ok(()),
408            Some(Ok(Err(writer_err))) => {
409                debug!(
410                    connection_id = connection_id.get(),
411                    close_reason = reason,
412                    writer_error = %writer_err,
413                    "writer failed after requested connection close"
414                );
415                Ok(())
416            }
417            Some(Err(join_err)) => {
418                warn!(
419                    connection_id = connection_id.get(),
420                    close_reason = reason,
421                    join_error = %join_err,
422                    "writer task join failed after requested connection close"
423                );
424                Ok(())
425            }
426        }
427    } else {
428        let writer_result =
429            writer_result.expect("writer result is present without a close request");
430        match (loop_result, writer_result) {
431            (Err(loop_err), Ok(Ok(()))) => Err(loop_err),
432            (Err(loop_err), Ok(Err(writer_err))) => {
433                warn!(
434                    connection_id = connection_id.get(),
435                    writer_error = %writer_err,
436                    "writer failed while closing after connection error"
437                );
438                Err(loop_err)
439            }
440            (Err(loop_err), Err(join_err)) => {
441                warn!(
442                    connection_id = connection_id.get(),
443                    join_error = %join_err,
444                    "writer task join failed while closing after connection error"
445                );
446                Err(loop_err)
447            }
448            (Ok(ConnectionLoopExit::PeerClosed), Ok(Ok(()))) => Ok(()),
449            (Ok(ConnectionLoopExit::PeerClosed), Ok(Err(writer_err))) => {
450                Err(ConnectionError::FrameIo(writer_err))
451            }
452            (Ok(ConnectionLoopExit::PeerClosed), Err(join_err)) => Err(join_err),
453            (Ok(ConnectionLoopExit::CloseRequested(_)), _) => {
454                unreachable!("close requests are handled before normal writer result matching")
455            }
456        }
457    };
458
459    match &result {
460        Ok(()) => {
461            if let Some(reason) = close_reason.as_deref() {
462                debug!(
463                    connection_id = connection_id.get(),
464                    close_reason = reason,
465                    "subc connection closed by request"
466                );
467            } else {
468                debug!(
469                    connection_id = connection_id.get(),
470                    "subc connection closed"
471                );
472            }
473        }
474        Err(err) => debug!(
475            connection_id = connection_id.get(),
476            error = %err,
477            "subc connection exited with error"
478        ),
479    }
480
481    result
482}
483
484async fn connection_loop<R>(
485    read_half: &mut R,
486    router: Arc<Router>,
487    ctx: RouteCtx,
488    mut close_receiver: ConnectionCloseReceiver,
489    route_open_tasks: &mut JoinSet<Result<(), RouterError>>,
490) -> Result<ConnectionLoopExit, ConnectionError>
491where
492    R: AsyncRead + Unpin,
493{
494    loop {
495        while let Some(result) = route_open_tasks.try_join_next() {
496            finish_route_open_task(result)?;
497        }
498
499        // Keep the same read future when a route.open completes: read_frame owns
500        // partial header/body buffers that would be lost if that future were dropped.
501        let read = read_frame(&mut *read_half);
502        tokio::pin!(read);
503        let frame = loop {
504            tokio::select! {
505                close = &mut close_receiver => {
506                    return Ok(ConnectionLoopExit::CloseRequested(close_reason(close)));
507                }
508                result = route_open_tasks.join_next(), if !route_open_tasks.is_empty() => {
509                    finish_route_open_task(
510                        result.expect("a non-empty route.open JoinSet has a next task")
511                    )?;
512                }
513                result = &mut read => {
514                    break match result.map_err(ConnectionError::FrameIo)? {
515                        Some(frame) => frame,
516                        None => return Ok(ConnectionLoopExit::PeerClosed),
517                    };
518                }
519            }
520        };
521
522        if let Some(target_module_id) = router.route_open_target(&frame) {
523            // A task can finish while the reader is waiting for the next frame.
524            // Reap it before checking admission so completed work never occupies
525            // one of the deliberately scarce connection slots.
526            while let Some(result) = route_open_tasks.try_join_next() {
527                finish_route_open_task(result)?;
528            }
529
530            if route_open_tasks.len() >= MAX_PENDING_ROUTE_OPENS_PER_CONNECTION {
531                // Never wait for capacity here: doing so would recreate the same
532                // reader head-of-line stall with a smaller constant.
533                let refusal = router
534                    .route_open_capacity_refusal(
535                        &ctx,
536                        &frame,
537                        &target_module_id,
538                        MAX_PENDING_ROUTE_OPENS_PER_CONNECTION,
539                    )
540                    .map_err(ConnectionError::Router)?;
541                let send_result = tokio::select! {
542                    close = &mut close_receiver => {
543                        return Ok(ConnectionLoopExit::CloseRequested(close_reason(close)));
544                    }
545                    result = ctx.egress.send(refusal) => result,
546                };
547                send_result.map_err(ConnectionError::Router)?;
548                continue;
549            }
550
551            let task_router = Arc::clone(&router);
552            let task_ctx = ctx.clone();
553            // Start slow-dispatch timing before spawn so it covers the task's
554            // full scheduler and handler lifetime, as inline timing did.
555            let dispatch_started_at = Instant::now();
556            route_open_tasks.spawn(async move {
557                route_open_tail(task_router, task_ctx, frame, dispatch_started_at).await
558            });
559            continue;
560        }
561
562        // Every non-route.open frame keeps the original serial dispatch path,
563        // including read backpressure and close-cancellation coupling.
564        let route_result = tokio::select! {
565            close = &mut close_receiver => {
566                return Ok(ConnectionLoopExit::CloseRequested(close_reason(close)));
567            }
568            result = router.route_for_connection(&ctx, frame) => result,
569        };
570
571        if let Err(err) = route_result {
572            if let Some(error_frame) = err.to_error_frame() {
573                warn!(
574                    connection_id = ctx.connection_id.get(),
575                    error = %err,
576                    "routing failure recovered with ERROR frame"
577                );
578                let send_result = tokio::select! {
579                    close = &mut close_receiver => {
580                        return Ok(ConnectionLoopExit::CloseRequested(close_reason(close)));
581                    }
582                    result = ctx.egress.send(error_frame) => result,
583                };
584                send_result.map_err(ConnectionError::Router)?;
585            } else {
586                debug!(
587                    connection_id = ctx.connection_id.get(),
588                    error = %err,
589                    "fatal routing failure"
590                );
591                return Err(ConnectionError::Router(err));
592            }
593        }
594    }
595}
596
597async fn route_open_tail(
598    router: Arc<Router>,
599    ctx: RouteCtx,
600    frame: crate::Frame,
601    dispatch_started_at: Instant,
602) -> Result<(), RouterError> {
603    match router
604        .route_for_connection_started(&ctx, frame, Some(dispatch_started_at))
605        .await
606    {
607        Ok(()) => Ok(()),
608        Err(err) => {
609            let Some(error_frame) = err.to_error_frame() else {
610                return Err(err);
611            };
612            warn!(
613                connection_id = ctx.connection_id.get(),
614                error = %err,
615                "routing failure recovered with ERROR frame"
616            );
617            ctx.egress.send(error_frame).await
618        }
619    }
620}
621
622fn finish_route_open_task(
623    result: Result<Result<(), RouterError>, tokio::task::JoinError>,
624) -> Result<(), ConnectionError> {
625    match result {
626        Ok(Ok(())) => Ok(()),
627        Ok(Err(err)) => Err(ConnectionError::Router(err)),
628        Err(err) => Err(ConnectionError::Router(RouterError::backend(
629            0,
630            0,
631            format!("route.open task failed: {err}"),
632        ))),
633    }
634}
635
636fn close_reason(
637    result: Result<CloseReason, tokio::sync::oneshot::error::RecvError>,
638) -> CloseReason {
639    result.unwrap_or_else(|_| {
640        CloseReason::new(
641            "close_registry_dropped",
642            "connection close registration was dropped without a reason",
643        )
644    })
645}
646
647async fn drain_writer<W>(
648    write_half: W,
649    mut rx: mpsc::Receiver<crate::router::OutboundFrame>,
650) -> Result<(), FrameIoError>
651where
652    W: AsyncWrite + Unpin,
653{
654    let mut writer = BufWriter::new(write_half);
655    while let Some(outbound) = rx.recv().await {
656        write_outbound(&mut writer, outbound).await?;
657        while let Ok(outbound) = rx.try_recv() {
658            write_outbound(&mut writer, outbound).await?;
659        }
660        writer.flush().await.map_err(FrameIoError::Io)?;
661    }
662    writer.flush().await.map_err(FrameIoError::Io)?;
663    Ok(())
664}
665
666/// Reply-path half of slow-control diagnosis: a channel-0 reply that sat in
667/// the writer queue past the threshold is reported with its queue residency
668/// and its own write duration separated, because "writer task not scheduled"
669/// and "socket write blocked" are different defects and the sum hides which.
670/// Data-plane frames are exempt: their latency is the client's own flow
671/// control, and logging them would drown the control signal in bulk traffic.
672async fn write_outbound<W>(
673    writer: &mut BufWriter<W>,
674    outbound: crate::router::OutboundFrame,
675) -> Result<(), FrameIoError>
676where
677    W: AsyncWrite + Unpin,
678{
679    const SLOW_REPLY_QUEUE: Duration = Duration::from_millis(1000);
680    let queued = outbound.enqueued_at.elapsed();
681    let frame = outbound.frame;
682    if frame.header.channel == 0 && queued >= SLOW_REPLY_QUEUE {
683        let write_started = std::time::Instant::now();
684        let result = write_frame(writer, &frame).await;
685        tracing::warn!(
686            corr = frame.header.corr,
687            queued_ms = queued.as_millis() as u64,
688            write_ms = write_started.elapsed().as_millis() as u64,
689            "slow control reply write"
690        );
691        result?;
692    } else {
693        write_frame(writer, &frame).await?;
694    }
695    if let Some(flushed) = outbound.flushed {
696        writer.flush().await.map_err(FrameIoError::Io)?;
697        let _ = flushed.send(());
698    }
699    Ok(())
700}
701
702#[cfg(all(test, unix))]
703#[tokio::test]
704async fn shutdown_notice_ack_waits_for_socket_flush() {
705    let (socket, mut peer) = tokio::io::duplex(1);
706    let (tx, rx) = mpsc::channel(1);
707    let sink = FrameSink::new(tx);
708    let writer = tokio::spawn(drain_writer(socket, rx));
709    let frame = crate::Frame::build(
710        subc_protocol::FrameType::Push,
711        subc_protocol::Flags::new(false, subc_protocol::Priority::Interactive, false),
712        0,
713        0,
714        0,
715        b"notice".to_vec(),
716    )
717    .unwrap();
718    let send = sink.send_flushed(frame);
719    tokio::pin!(send);
720    assert!(
721        timeout(Duration::from_millis(20), &mut send).await.is_err(),
722        "queueing bytes is not a socket flush acknowledgement"
723    );
724    let (sent, received) = timeout(Duration::from_secs(1), async {
725        tokio::join!(&mut send, read_frame(&mut peer))
726    })
727    .await
728    .unwrap();
729    sent.unwrap();
730    assert_eq!(received.unwrap().unwrap().body, b"notice");
731    writer.abort();
732}
733
734#[derive(Debug)]
735pub enum ServerError {
736    NoListeners,
737    Accept {
738        local_addr: Option<SocketAddr>,
739        source: io::Error,
740    },
741}
742
743impl fmt::Display for ServerError {
744    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
745        match self {
746            Self::NoListeners => write!(f, "no TCP listeners were provided"),
747            Self::Accept { local_addr, source } => match local_addr {
748                Some(addr) => write!(f, "failed to accept TCP connection on {addr}: {source}"),
749                None => write!(f, "failed to accept TCP connection: {source}"),
750            },
751        }
752    }
753}
754
755impl Error for ServerError {
756    fn source(&self) -> Option<&(dyn Error + 'static)> {
757        match self {
758            Self::Accept { source, .. } => Some(source),
759            Self::NoListeners => None,
760        }
761    }
762}
763
764#[derive(Debug)]
765pub enum ConnectionError {
766    Auth(AuthError),
767    UnauthenticatedCapacity,
768    FrameIo(FrameIoError),
769    Router(RouterError),
770    WriterTask(tokio::task::JoinError),
771}
772
773impl ConnectionError {
774    fn is_quiet_reject(&self) -> bool {
775        matches!(self, Self::Auth(_) | Self::UnauthenticatedCapacity)
776    }
777}
778
779impl fmt::Display for ConnectionError {
780    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
781        match self {
782            Self::Auth(err) => write!(f, "connection auth failed: {err}"),
783            Self::UnauthenticatedCapacity => write!(
784                f,
785                "too many concurrent unauthenticated subc TCP connections"
786            ),
787            Self::FrameIo(err) => write!(f, "frame connection error: {err}"),
788            Self::Router(err) => write!(f, "router connection error: {err}"),
789            Self::WriterTask(err) => write!(f, "connection writer task failed: {err}"),
790        }
791    }
792}
793
794impl Error for ConnectionError {
795    fn source(&self) -> Option<&(dyn Error + 'static)> {
796        match self {
797            Self::Auth(err) => Some(err),
798            Self::FrameIo(err) => Some(err),
799            Self::Router(err) => Some(err),
800            Self::WriterTask(err) => Some(err),
801            Self::UnauthenticatedCapacity => None,
802        }
803    }
804}
805
806#[cfg(test)]
807mod tests {
808    use std::{
809        pin::Pin,
810        sync::atomic::{AtomicUsize, Ordering},
811        task::{Context, Poll},
812    };
813
814    use super::*;
815    use subc_protocol::{
816        DecodeError, ErrorBody, Flags, FrameType, Priority, HEADER_LEN, PROTOCOL_VERSION,
817    };
818    use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt, ReadBuf};
819
820    use subc_transport::{authenticate_client, ConnectionInfo, Endpoint, SCHEMA_VERSION};
821
822    use crate::{ControlHandler, EchoBackend, Frame, ReadStage, Registry};
823
824    const TEST_DEADLINE: Duration = Duration::from_secs(2);
825    const TEST_DAEMON_VER: &str = "test-subc-server";
826
827    /// Reply-path stamp, slow polarity: a channel-0 reply whose queue residency
828    /// exceeds the threshold must produce the slow-control-reply-write WARN with
829    /// queued_ms reflecting the residency. Uses a backdated stamp rather than a
830    /// real stall so the test is fast and deterministic.
831    #[tokio::test]
832    async fn stale_queued_control_reply_logs_slow_reply_write() {
833        let (logs, _guard) = crate::router::test_log::log_capture(tracing::Level::WARN);
834        let (tx, rx) = mpsc::channel::<crate::router::OutboundFrame>(4);
835        let reply = Frame::build_with_version(
836            PROTOCOL_VERSION,
837            FrameType::Error,
838            Flags::new(false, Priority::Interactive, false),
839            0,
840            0,
841            7,
842            serde_json::to_vec(&ErrorBody {
843                code: "test".into(),
844                message: "reply".into(),
845                detail: None,
846            })
847            .expect("body encodes"),
848        )
849        .expect("frame builds");
850        tx.send(crate::router::OutboundFrame {
851            frame: reply,
852            enqueued_at: std::time::Instant::now() - Duration::from_millis(1500),
853            flushed: None,
854        })
855        .await
856        .expect("queued");
857        drop(tx);
858        let (write_half, mut read_half) = duplex(64 * 1024);
859        drain_writer(write_half, rx).await.expect("writer drains");
860        let mut sink = Vec::new();
861        read_half.read_to_end(&mut sink).await.expect("read");
862        assert!(!sink.is_empty(), "frame reached the socket");
863        let captured = crate::router::test_log::captured_logs(&logs);
864        assert!(
865            captured.contains("slow control reply write") && captured.contains("corr=7"),
866            "expected slow reply WARN naming corr, got: {captured}"
867        );
868        let queued_ms: u64 = captured
869            .split("queued_ms=")
870            .nth(1)
871            .and_then(|s| s.split_whitespace().next())
872            .and_then(|s| s.parse().ok())
873            .expect("queued_ms present");
874        assert!(
875            queued_ms >= 1500,
876            "queued_ms reflects residency: {queued_ms}"
877        );
878    }
879
880    /// Fast polarity: a promptly-drained control reply and a stale DATA-PLANE
881    /// frame must both stay silent — the WARN is channel-0-only by design, and
882    /// a healthy queue must add zero log volume.
883    #[tokio::test]
884    async fn fresh_control_and_stale_data_frames_log_nothing() {
885        let (logs, _guard) = crate::router::test_log::log_capture(tracing::Level::WARN);
886        let (tx, rx) = mpsc::channel::<crate::router::OutboundFrame>(4);
887        let control = Frame::build_with_version(
888            PROTOCOL_VERSION,
889            FrameType::Error,
890            Flags::new(false, Priority::Interactive, false),
891            0,
892            0,
893            8,
894            serde_json::to_vec(&ErrorBody {
895                code: "test".into(),
896                message: "fresh".into(),
897                detail: None,
898            })
899            .expect("body encodes"),
900        )
901        .expect("frame builds");
902        tx.send(crate::router::OutboundFrame {
903            frame: control,
904            enqueued_at: std::time::Instant::now(),
905            flushed: None,
906        })
907        .await
908        .expect("queued");
909        let data = Frame::build_with_version(
910            PROTOCOL_VERSION,
911            FrameType::Error,
912            Flags::new(false, Priority::Interactive, false),
913            9,
914            1,
915            9,
916            serde_json::to_vec(&ErrorBody {
917                code: "test".into(),
918                message: "data".into(),
919                detail: None,
920            })
921            .expect("body encodes"),
922        )
923        .expect("frame builds");
924        tx.send(crate::router::OutboundFrame {
925            frame: data,
926            enqueued_at: std::time::Instant::now() - Duration::from_millis(5000),
927            flushed: None,
928        })
929        .await
930        .expect("queued");
931        drop(tx);
932        let (write_half, _read_half) = duplex(64 * 1024);
933        drain_writer(write_half, rx).await.expect("writer drains");
934        let captured = crate::router::test_log::captured_logs(&logs);
935        assert!(
936            !captured.contains("slow control reply write"),
937            "no WARN for fresh control or stale data frames, got: {captured}"
938        );
939    }
940
941    struct CountingReader {
942        bytes: Vec<u8>,
943        offset: usize,
944        first_read_end: Option<usize>,
945        reads: Arc<AtomicUsize>,
946    }
947
948    impl CountingReader {
949        fn new(bytes: Vec<u8>, first_read_end: Option<usize>) -> (Self, Arc<AtomicUsize>) {
950            let reads = Arc::new(AtomicUsize::new(0));
951            (
952                Self {
953                    bytes,
954                    offset: 0,
955                    first_read_end,
956                    reads: Arc::clone(&reads),
957                },
958                reads,
959            )
960        }
961    }
962
963    impl AsyncRead for CountingReader {
964        fn poll_read(
965            mut self: Pin<&mut Self>,
966            _cx: &mut Context<'_>,
967            buf: &mut ReadBuf<'_>,
968        ) -> Poll<io::Result<()>> {
969            self.reads.fetch_add(1, Ordering::Relaxed);
970            let available = self.bytes.len().saturating_sub(self.offset);
971            let first_read_remaining = self
972                .first_read_end
973                .filter(|end| self.offset < *end)
974                .map_or(available, |end| end - self.offset);
975            let count = available.min(first_read_remaining).min(buf.remaining());
976            let end = self.offset + count;
977            buf.put_slice(&self.bytes[self.offset..end]);
978            self.offset = end;
979            Poll::Ready(Ok(()))
980        }
981    }
982
983    fn encode_frames(frames: &[Frame]) -> Vec<u8> {
984        let mut bytes = Vec::new();
985        for frame in frames {
986            bytes.extend_from_slice(&frame.header.encode());
987            bytes.extend_from_slice(&frame.body);
988        }
989        bytes
990    }
991
992    async fn read_frames<R>(reader: &mut R, count: usize) -> Vec<Frame>
993    where
994        R: AsyncRead + Unpin,
995    {
996        let mut frames = Vec::with_capacity(count);
997        for _ in 0..count {
998            frames.push(read_frame(reader).await.unwrap().unwrap());
999        }
1000        frames
1001    }
1002
1003    fn request(channel: u16, corr: u64, body: &[u8]) -> Frame {
1004        Frame::build(
1005            FrameType::Request,
1006            Flags::new(true, Priority::Interactive, false),
1007            channel,
1008            0,
1009            corr,
1010            body.to_vec(),
1011        )
1012        .unwrap()
1013    }
1014
1015    fn echo_router() -> Arc<Router> {
1016        let mut router = Router::with_default_self_handler();
1017        router.register_backend(7, EchoBackend).unwrap();
1018        router.register_backend(9, EchoBackend).unwrap();
1019        Arc::new(router)
1020    }
1021
1022    fn test_auth() -> (ServerAuth, ConnectionInfo) {
1023        test_auth_with_limit(4)
1024    }
1025
1026    fn test_auth_with_limit(max_unauthenticated: usize) -> (ServerAuth, ConnectionInfo) {
1027        let key = vec![0x42; 32];
1028        let daemon_id = [0x24; 16];
1029        let conn = ConnectionInfo {
1030            schema: SCHEMA_VERSION,
1031            wire_version: None,
1032            endpoints: vec![Endpoint {
1033                host: "127.0.0.1".to_owned(),
1034                port: 1,
1035            }],
1036            key: key.clone(),
1037            daemon_id,
1038            pid: std::process::id(),
1039            daemon_ver: TEST_DAEMON_VER.to_owned(),
1040        };
1041        (
1042            ServerAuth::with_limits(
1043                key,
1044                daemon_id,
1045                TEST_DAEMON_VER,
1046                TEST_DEADLINE,
1047                max_unauthenticated,
1048            ),
1049            conn,
1050        )
1051    }
1052
1053    async fn authenticate<S>(stream: &mut S, conn: &ConnectionInfo)
1054    where
1055        S: AsyncRead + AsyncWrite + Unpin,
1056    {
1057        authenticate_client(stream, conn, TEST_DEADLINE)
1058            .await
1059            .expect("test client should authenticate")
1060    }
1061
1062    #[tokio::test]
1063    async fn buffered_frame_reader_coalesces_reads_and_preserves_short_reads() {
1064        let frames = vec![
1065            request(7, 1, b"first"),
1066            request(9, 2, b"second"),
1067            request(7, 3, b"third"),
1068            request(9, 4, b"fourth"),
1069        ];
1070        let bytes = encode_frames(&frames);
1071
1072        let (mut direct, direct_reads) = CountingReader::new(bytes.clone(), None);
1073        assert_eq!(read_frames(&mut direct, frames.len()).await, frames);
1074        assert_eq!(direct_reads.load(Ordering::Relaxed), frames.len() * 3);
1075
1076        let (buffered_source, buffered_reads) = CountingReader::new(bytes, None);
1077        let mut buffered = BufReader::new(buffered_source);
1078        assert_eq!(read_frames(&mut buffered, frames.len()).await, frames);
1079        assert_eq!(buffered_reads.load(Ordering::Relaxed), 1);
1080
1081        let split_frame = request(7, 5, b"split-body");
1082        let split_bytes = encode_frames(std::slice::from_ref(&split_frame));
1083        let (split_source, split_reads) = CountingReader::new(split_bytes, Some(10));
1084        let mut split_reader = BufReader::new(split_source);
1085        assert_eq!(
1086            read_frame(&mut split_reader).await.unwrap(),
1087            Some(split_frame)
1088        );
1089        assert_eq!(split_reads.load(Ordering::Relaxed), 2);
1090    }
1091
1092    #[tokio::test]
1093    async fn interleaved_channels_on_one_stream_demux_byte_identically_after_auth() {
1094        let (mut client, server_stream) = duplex(4096);
1095        let (auth, conn) = test_auth();
1096        let server = tokio::spawn(handle_connection(server_stream, echo_router(), auth));
1097        authenticate(&mut client, &conn).await;
1098        let frames = [
1099            request(7, 1, b"chan7-first\0opaque"),
1100            request(9, 2, b"chan9-middle-{json?}"),
1101            request(7, 3, b"chan7-second\xffbytes"),
1102        ];
1103
1104        for frame in &frames {
1105            crate::write_frame(&mut client, frame).await.unwrap();
1106        }
1107
1108        for expected in &frames {
1109            let response = crate::read_frame(&mut client).await.unwrap().unwrap();
1110            assert_eq!(response.header.ty, FrameType::Response);
1111            assert_eq!(response.header.channel, expected.header.channel);
1112            assert_eq!(response.header.corr, expected.header.corr);
1113            assert_eq!(response.body, expected.body);
1114        }
1115
1116        drop(client);
1117        server.await.unwrap().unwrap();
1118    }
1119
1120    #[tokio::test]
1121    async fn channel_zero_goes_to_subc_self_handler_after_auth() {
1122        let (mut client, server_stream) = duplex(512);
1123        let (auth, conn) = test_auth();
1124        let server = tokio::spawn(handle_connection(
1125            server_stream,
1126            Arc::new(Router::with_default_self_handler()),
1127            auth,
1128        ));
1129        authenticate(&mut client, &conn).await;
1130        let ping = Frame::build(
1131            FrameType::Ping,
1132            Flags::new(false, Priority::Passive, false),
1133            0,
1134            0,
1135            55,
1136            Vec::new(),
1137        )
1138        .unwrap();
1139
1140        crate::write_frame(&mut client, &ping).await.unwrap();
1141        let response = crate::read_frame(&mut client).await.unwrap().unwrap();
1142
1143        assert_eq!(response.header.ty, FrameType::Pong);
1144        assert_eq!(response.header.channel, 0);
1145        assert_eq!(response.header.corr, 55);
1146        assert!(response.body.is_empty());
1147
1148        drop(client);
1149        server.await.unwrap().unwrap();
1150    }
1151
1152    #[tokio::test]
1153    async fn unauthenticated_connection_is_rejected_before_routing() {
1154        let (mut client, server_stream) = duplex(512);
1155        let (auth, _conn) = test_auth();
1156        let registry = Arc::new(Registry::default());
1157        let router = Arc::new(Router::with_control_handler(Arc::new(ControlHandler::new(
1158            Arc::clone(&registry),
1159        ))));
1160        let server = tokio::spawn(handle_connection(server_stream, router, auth));
1161        let ping = Frame::build(
1162            FrameType::Ping,
1163            Flags::new(false, Priority::Passive, false),
1164            0,
1165            0,
1166            66,
1167            Vec::new(),
1168        )
1169        .unwrap();
1170
1171        crate::write_frame(&mut client, &ping).await.unwrap();
1172        if let Ok(Ok(Some(frame))) =
1173            tokio::time::timeout(Duration::from_millis(200), crate::read_frame(&mut client)).await
1174        {
1175            panic!("unauthenticated frame reached router: {frame:?}");
1176        }
1177
1178        let err = server.await.unwrap().unwrap_err();
1179        assert!(matches!(err, ConnectionError::Auth(_)));
1180        assert_eq!(registry.active_registration_count().unwrap(), 0);
1181    }
1182
1183    #[tokio::test]
1184    async fn over_cap_peer_queues_for_a_slot_and_authenticates_when_one_frees() {
1185        // Restart-herd contract: an over-cap pre-auth connection WAITS (bounded by
1186        // the auth deadline) instead of being reset. When the slot-holder finishes,
1187        // the queued peer must complete a normal handshake — the 2026-07-14 aft
1188        // outage was exactly a supervised child being reset out of this lottery.
1189        let (mut first_client, first_server_stream) = duplex(2048);
1190        let (mut second_client, second_server_stream) = duplex(2048);
1191        let (auth, conn) = test_auth_with_limit(1);
1192        let registry = Arc::new(Registry::default());
1193        let router = Arc::new(Router::with_control_handler(Arc::new(ControlHandler::new(
1194            Arc::clone(&registry),
1195        ))));
1196
1197        let first_server = tokio::spawn(handle_connection(
1198            first_server_stream,
1199            Arc::clone(&router),
1200            auth.clone(),
1201        ));
1202
1203        let second_server = tokio::spawn(handle_connection(
1204            second_server_stream,
1205            Arc::clone(&router),
1206            auth.clone(),
1207        ));
1208
1209        // The second peer must still be pending (not reset) while the slot is held.
1210        tokio::time::sleep(Duration::from_millis(50)).await;
1211        assert!(
1212            !second_server.is_finished(),
1213            "queued peer must not be reset"
1214        );
1215
1216        // First peer completes its handshake, freeing the slot; the queued second
1217        // peer then authenticates normally.
1218        authenticate(&mut first_client, &conn).await;
1219        authenticate(&mut second_client, &conn).await;
1220
1221        drop(first_client);
1222        drop(second_client);
1223        let _ = first_server.await;
1224        let _ = second_server.await;
1225        assert_eq!(registry.active_registration_count().unwrap(), 0);
1226    }
1227
1228    #[tokio::test]
1229    async fn over_cap_peer_is_rejected_when_no_slot_frees_within_deadline() {
1230        // The deadline stays the DoS bound: if no slot frees, the queued peer is
1231        // rejected at the deadline with a closed stream. The slot is held DIRECTLY
1232        // (not by another connection) so nothing can free it mid-test — a squatting
1233        // connection's own auth deadline would release the slot at exactly the
1234        // waiter's timeout, making the outcome racy.
1235        let (mut second_client, second_server_stream) = duplex(512);
1236        let (auth, conn) = test_auth_with_limit(1);
1237        let registry = Arc::new(Registry::default());
1238        let router = Arc::new(Router::with_control_handler(Arc::new(ControlHandler::new(
1239            Arc::clone(&registry),
1240        ))));
1241
1242        let held_slot = auth
1243            .unauthenticated
1244            .clone()
1245            .try_acquire_owned()
1246            .expect("sole pre-auth slot");
1247
1248        let second_server = tokio::spawn(handle_connection(
1249            second_server_stream,
1250            Arc::clone(&router),
1251            auth.clone(),
1252        ));
1253        let second_err = tokio::time::timeout(TEST_DEADLINE * 2, second_server)
1254            .await
1255            .expect("capacity reject should settle at the deadline")
1256            .expect("second connection task should not panic")
1257            .expect_err("queued peer must be rejected when no slot frees");
1258        drop(held_slot);
1259        assert!(matches!(
1260            second_err,
1261            ConnectionError::UnauthenticatedCapacity
1262        ));
1263        let mut closed = [0u8; 1];
1264        assert_eq!(
1265            second_client.read(&mut closed).await.unwrap(),
1266            0,
1267            "capacity-rejected peer should observe a closed stream"
1268        );
1269        assert_eq!(registry.active_registration_count().unwrap(), 0);
1270
1271        let (mut authed_client, authed_server_stream) = duplex(2048);
1272        let authed_server = tokio::spawn(handle_connection(
1273            authed_server_stream,
1274            Arc::clone(&router),
1275            auth,
1276        ));
1277        authenticate(&mut authed_client, &conn).await;
1278        let ping = Frame::build(
1279            FrameType::Ping,
1280            Flags::new(false, Priority::Passive, false),
1281            0,
1282            0,
1283            77,
1284            Vec::new(),
1285        )
1286        .unwrap();
1287        crate::write_frame(&mut authed_client, &ping).await.unwrap();
1288        let pong = crate::read_frame(&mut authed_client)
1289            .await
1290            .unwrap()
1291            .unwrap();
1292        assert_eq!(pong.header.ty, FrameType::Pong);
1293        assert_eq!(pong.header.channel, 0);
1294        assert_eq!(pong.header.corr, 77);
1295        assert_eq!(registry.active_registration_count().unwrap(), 0);
1296
1297        drop(authed_client);
1298        authed_server.await.unwrap().unwrap();
1299    }
1300
1301    #[tokio::test]
1302    async fn bind_ack_during_partial_frame_preserves_next_request() {
1303        use subc_control::ClientControlRequest;
1304        use subc_protocol::{
1305            manifest::{
1306                Concurrency, ExecutionMode, IdentityScope, ModuleManifest, ProviderRole, Tool,
1307            },
1308            session::{ModuleControlRequest, ModuleControlResponse},
1309            BindIdentity, ModuleHelloBody, RouteTarget,
1310        };
1311
1312        let mut configured_router = Router::with_default_self_handler();
1313        configured_router.register_backend(7, EchoBackend).unwrap();
1314        let router = Arc::new(configured_router);
1315        let (auth, conn) = test_auth();
1316        let (mut module, module_stream) = duplex(4096);
1317        let module_server = tokio::spawn(handle_connection(
1318            module_stream,
1319            Arc::clone(&router),
1320            auth.clone(),
1321        ));
1322        authenticate(&mut module, &conn).await;
1323        let manifest = ModuleManifest::builder("frame-test", "0.1.0")
1324            .protocol_ver(PROTOCOL_VERSION)
1325            .provides(vec![ProviderRole::ToolProvider {
1326                tools: vec![Tool {
1327                    name: "read".into(),
1328                    description: None,
1329                    execution_mode: ExecutionMode::Pure,
1330                    schema: serde_json::json!({"type": "object"}),
1331                }],
1332                identity_scope: vec![IdentityScope::Project, IdentityScope::Session],
1333                concurrency: Concurrency::ModuleManaged,
1334                emits_push: true,
1335                sub_supervises: true,
1336            }])
1337            .build();
1338        let hello = Frame::build(
1339            FrameType::Hello,
1340            Flags::new(false, Priority::Passive, false),
1341            0,
1342            0,
1343            1,
1344            serde_json::to_vec(&ModuleHelloBody {
1345                manifest,
1346                protocol_ver: PROTOCOL_VERSION,
1347                control_ops: None,
1348                launch_nonce: None,
1349            })
1350            .unwrap(),
1351        )
1352        .unwrap();
1353        crate::write_frame(&mut module, &hello).await.unwrap();
1354        assert_eq!(
1355            read_frame(&mut module).await.unwrap().unwrap().header.ty,
1356            FrameType::HelloAck
1357        );
1358
1359        let (mut client, client_stream) = duplex(4096);
1360        let client_server = tokio::spawn(handle_connection(client_stream, router, auth));
1361        authenticate(&mut client, &conn).await;
1362        let open = Frame::build(
1363            FrameType::Request,
1364            Flags::new(false, Priority::Passive, false),
1365            0,
1366            0,
1367            2,
1368            serde_json::to_vec(&ClientControlRequest::RouteOpen {
1369                target: RouteTarget::ToolProvider {
1370                    module_id: "frame-test".into(),
1371                },
1372                identity: BindIdentity::new(std::env::current_dir().unwrap(), "unit", "session"),
1373                consumer_identity: None,
1374                consumer_capabilities: None,
1375                admission_facts: None,
1376            })
1377            .unwrap(),
1378        )
1379        .unwrap();
1380        crate::write_frame(&mut client, &open).await.unwrap();
1381        let bind = timeout(TEST_DEADLINE, read_frame(&mut module))
1382            .await
1383            .unwrap()
1384            .unwrap()
1385            .unwrap();
1386        assert!(matches!(
1387            serde_json::from_slice::<ModuleControlRequest>(&bind.body).unwrap(),
1388            ModuleControlRequest::RouteBind { .. }
1389        ));
1390
1391        let ping = request(7, 3, b"partial-frame-body");
1392        client.write_all(&ping.header.encode()).await.unwrap();
1393        // Give the connection reader time to consume the header while the bind is pending.
1394        tokio::time::sleep(Duration::from_millis(30)).await;
1395        let ack = Frame::build(
1396            FrameType::Response,
1397            Flags::new(false, Priority::Passive, false),
1398            0,
1399            0,
1400            bind.header.corr,
1401            serde_json::to_vec(&ModuleControlResponse::RouteBindAck {}).unwrap(),
1402        )
1403        .unwrap();
1404        crate::write_frame(&mut module, &ack).await.unwrap();
1405        let opened = timeout(TEST_DEADLINE, read_frame(&mut client))
1406            .await
1407            .unwrap()
1408            .unwrap();
1409        if opened.is_none() {
1410            panic!("client closed: {:?}", client_server.await);
1411        }
1412        let opened = opened.unwrap();
1413        assert_eq!(opened.header.corr, 2);
1414        client.write_all(&ping.body).await.unwrap();
1415        let pong = timeout(TEST_DEADLINE, read_frame(&mut client))
1416            .await
1417            .expect("partial frame must reach the router after the bind ack")
1418            .expect("frame must decode")
1419            .expect("connection must remain open");
1420        assert_eq!(pong.header.ty, FrameType::Response);
1421        assert_eq!(pong.header.corr, 3);
1422        assert_eq!(pong.body, ping.body);
1423        drop(client);
1424        drop(module);
1425        client_server.await.unwrap().unwrap();
1426        let _ = module_server.await.unwrap();
1427    }
1428
1429    #[tokio::test]
1430    async fn aborted_accept_does_not_end_listener() {
1431        let listener = Arc::new(TcpListener::bind("127.0.0.1:0").await.unwrap());
1432        let addr = listener.local_addr().unwrap();
1433        let (auth, conn) = test_auth();
1434        let mut attempts = 0;
1435        let server = tokio::spawn(serve_listener_with_accept(
1436            Some(addr),
1437            echo_router(),
1438            auth,
1439            move || {
1440                attempts += 1;
1441                let result = if attempts == 1 {
1442                    Some(io::Error::from(io::ErrorKind::ConnectionAborted))
1443                } else {
1444                    None
1445                };
1446                let listener = Arc::clone(&listener);
1447                async move {
1448                    match result {
1449                        Some(err) => Err(err),
1450                        None => listener.accept().await,
1451                    }
1452                }
1453            },
1454        ));
1455        let mut client = tokio::net::TcpStream::connect(addr).await.unwrap();
1456        authenticate(&mut client, &conn).await;
1457        let ping = Frame::build(
1458            FrameType::Ping,
1459            Flags::new(false, Priority::Passive, false),
1460            0,
1461            0,
1462            77,
1463            Vec::new(),
1464        )
1465        .unwrap();
1466        crate::write_frame(&mut client, &ping).await.unwrap();
1467        let pong = timeout(TEST_DEADLINE, read_frame(&mut client))
1468            .await
1469            .unwrap()
1470            .unwrap()
1471            .unwrap();
1472        assert_eq!(pong.header.ty, FrameType::Pong);
1473        assert!(
1474            !server.is_finished(),
1475            "a temporary accept failure must not stop the listener"
1476        );
1477        server.abort();
1478    }
1479
1480    #[tokio::test]
1481    async fn exhausted_accept_backs_off_then_serves_connection() {
1482        let listener = Arc::new(TcpListener::bind("127.0.0.1:0").await.unwrap());
1483        let addr = listener.local_addr().unwrap();
1484        let (auth, conn) = test_auth();
1485        let mut attempts = 0;
1486        let server = tokio::spawn(serve_listener_with_accept(
1487            Some(addr),
1488            echo_router(),
1489            auth,
1490            move || {
1491                attempts += 1;
1492                // The platform's "too many open files" code: EMFILE on Unix,
1493                // WSAEMFILE on Windows. A bare 24 is ERROR_BAD_LENGTH on Windows.
1494                #[cfg(unix)]
1495                let emfile = rustix::io::Errno::MFILE.raw_os_error();
1496                #[cfg(windows)]
1497                let emfile = 10024;
1498                let error = (attempts == 1).then(|| io::Error::from_raw_os_error(emfile));
1499                let listener = Arc::clone(&listener);
1500                async move {
1501                    match error {
1502                        Some(err) => Err(err),
1503                        None => listener.accept().await,
1504                    }
1505                }
1506            },
1507        ));
1508        let started = Instant::now();
1509        let mut client = tokio::net::TcpStream::connect(addr).await.unwrap();
1510        authenticate(&mut client, &conn).await;
1511        assert!(
1512            started.elapsed() >= Duration::from_millis(50),
1513            "fd exhaustion must back off before retrying"
1514        );
1515        assert!(!server.is_finished());
1516        server.abort();
1517    }
1518
1519    #[tokio::test]
1520    async fn fatal_accept_error_still_stops_listener() {
1521        let (auth, _) = test_auth();
1522        let err = serve_listener_with_accept(None, echo_router(), auth, || async {
1523            Err(io::Error::from(io::ErrorKind::PermissionDenied))
1524        })
1525        .await
1526        .unwrap_err();
1527        assert!(
1528            matches!(err, ServerError::Accept { source, .. } if source.kind() == io::ErrorKind::PermissionDenied)
1529        );
1530    }
1531
1532    #[tokio::test]
1533    async fn serve_listeners_with_no_listeners_returns_typed_error() {
1534        let (auth, _conn) = test_auth();
1535        let err = serve_listeners(Vec::new(), echo_router(), auth)
1536            .await
1537            .expect_err("empty listener set must fail loudly");
1538        assert!(matches!(err, ServerError::NoListeners));
1539    }
1540
1541    #[tokio::test]
1542    async fn malformed_header_returns_typed_error_no_panic() {
1543        let (mut client, server_stream) = duplex(128);
1544        let (auth, conn) = test_auth();
1545        let server = tokio::spawn(handle_connection(server_stream, echo_router(), auth));
1546        authenticate(&mut client, &conn).await;
1547        let mut header = [0u8; HEADER_LEN];
1548        header[4] = PROTOCOL_VERSION;
1549        header[5] = 250;
1550
1551        client.write_all(&header).await.unwrap();
1552        drop(client);
1553
1554        let err = server.await.unwrap().unwrap_err();
1555        assert!(matches!(
1556            err,
1557            ConnectionError::FrameIo(FrameIoError::DecodeHeader(DecodeError::UnknownFrameType {
1558                byte: 250
1559            }))
1560        ));
1561    }
1562
1563    #[tokio::test]
1564    async fn truncated_body_returns_typed_error_no_panic() {
1565        let (mut client, server_stream) = duplex(128);
1566        let (auth, conn) = test_auth();
1567        let server = tokio::spawn(handle_connection(server_stream, echo_router(), auth));
1568        authenticate(&mut client, &conn).await;
1569        let frame = request(7, 8, b"abcd");
1570
1571        client.write_all(&frame.header.encode()).await.unwrap();
1572        client.write_all(b"ab").await.unwrap();
1573        drop(client);
1574
1575        let err = server.await.unwrap().unwrap_err();
1576        assert!(matches!(
1577            err,
1578            ConnectionError::FrameIo(FrameIoError::UnexpectedEof {
1579                stage: ReadStage::Body,
1580                expected: 4,
1581                actual: 2
1582            })
1583        ));
1584    }
1585
1586    #[tokio::test]
1587    async fn unknown_channel_is_returned_as_error_frame_and_connection_continues() {
1588        let (mut client, server_stream) = duplex(1024);
1589        let (auth, conn) = test_auth();
1590        let server = tokio::spawn(handle_connection(server_stream, echo_router(), auth));
1591        authenticate(&mut client, &conn).await;
1592        let unknown = request(42, 10, b"lost");
1593        let known = request(7, 11, b"still-routes");
1594
1595        crate::write_frame(&mut client, &unknown).await.unwrap();
1596        crate::write_frame(&mut client, &known).await.unwrap();
1597
1598        let error = crate::read_frame(&mut client).await.unwrap().unwrap();
1599        assert_eq!(error.header.ty, FrameType::Error);
1600        assert_eq!(error.header.channel, 42);
1601        assert_eq!(error.header.corr, 10);
1602        let error_body: ErrorBody = serde_json::from_slice(&error.body).unwrap();
1603        assert_eq!(error_body.code, "unknown_channel");
1604
1605        let response = crate::read_frame(&mut client).await.unwrap().unwrap();
1606        assert_eq!(response.header.ty, FrameType::Response);
1607        assert_eq!(response.header.channel, 7);
1608        assert_eq!(response.header.corr, 11);
1609        assert_eq!(response.body, b"still-routes");
1610
1611        drop(client);
1612        server.await.unwrap().unwrap();
1613    }
1614}