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