Skip to main content

netget/server/
accept_bounded.rs

1//! A connection cap for accept loops, and the refusal that says why.
2//!
3//! # The problem this closes
4//!
5//! Every TCP accept loop in this tree used to accept without limit. A peer that connects and
6//! says nothing holds a socket, a task and an [`AppState`](crate::state::app_state::AppState)
7//! connection entry for as long as it likes; a thousand of them is a free denial of service,
8//! pre-authentication, on a server that will happily accept a thousand more. Per-connection
9//! read timeouts bound how long *one* peer can stall, and are the other half of this — but a
10//! timeout alone still lets an attacker hold `timeout × rate` connections at once. Only a cap
11//! turns "bounded per connection" into "bounded".
12//!
13//! # Why it refuses out loud
14//!
15//! A cap that silently drops the socket is worse than no cap at all: the client cannot tell a
16//! full server from a crashed one, so it retries immediately and forever, and the operator
17//! sees an outage with no cause. So [`accept_bounded`] writes the caller's own bytes before
18//! closing, and each protocol supplies the phrasing its *own* clients already understand —
19//! MySQL error 1040, RESP `-ERR max number of clients reached`, HTTP `503`, and so on. The
20//! bytes live in each protocol's `mod.rs`, not here: a table of per-protocol literals in a
21//! shared module would be exactly the centralised per-protocol logic this codebase forbids.
22//! This module knows only "some bytes, or none".
23//!
24//! Where a protocol has no way to say it — the peer speaks first and every server message is a
25//! positive assertion — the right answer is an empty slice and a plain close, for the same
26//! reason twenty protocols are deliberately silent on an LLM failure: a fabricated reply is
27//! worse than silence. The refusal is still logged, and the log line is the diagnosis.
28//!
29//! # Usage
30//!
31//! ```ignore
32//! const MAX_CONNECTIONS: usize = accept_bounded::DEFAULT_MAX_CONNECTIONS;
33//! const BUSY: &[u8] = b"-ERR max number of clients reached\r\n";
34//!
35//! let limiter = ConnectionLimiter::new(MAX_CONNECTIONS);
36//! loop {
37//!     let (socket, peer, permit) =
38//!         match accept_bounded(&listener, &limiter, BUSY, "REDIS", Some(&status_tx)).await {
39//!             Ok(v) => v,
40//!             Err(e) => { /* the listener itself failed; break */ }
41//!         };
42//!     tokio::spawn(async move {
43//!         let _permit = permit; // held for the life of the connection
44//!         handle(socket).await;
45//!     });
46//! }
47//! ```
48//!
49//! [`accept_bounded`] never returns a refused connection: it loops internally, so the caller's
50//! loop only ever sees peers that were admitted. The [`ConnectionPermit`] it returns must be
51//! moved into the connection task — dropping it early releases the slot while the connection
52//! is still live, which silently un-caps the server.
53
54use crate::logging::emit::Log;
55use std::net::SocketAddr;
56use std::sync::Arc;
57use tokio::io::AsyncWriteExt;
58use tokio::net::{TcpListener, TcpStream};
59use tokio::sync::{mpsc, OwnedSemaphorePermit, Semaphore};
60
61/// Concurrent connections a server admits before it starts refusing.
62///
63/// 256, which is what `src/server/nfs/guard.rs` chose on its own reasoning and the only number
64/// in this tree that was ever argued for: each admitted connection costs a bounded amount of
65/// memory, so the cap is what multiplies that bound into a total. It is deliberately generous
66/// — far above anything a human operator or a test harness produces, and far below what an
67/// attacker needs for the socket exhaustion to matter. A protocol whose per-connection cost is
68/// unusually high (a large read buffer, a long-lived session) declares its own smaller value
69/// and says why in the comment; one that multiplexes everything over few connections may
70/// declare a smaller one still.
71///
72/// This is not a tuning knob exposed to the model or the operator: a bound decided by
73/// configuration is a bound an attacker can ask you to raise.
74pub const DEFAULT_MAX_CONNECTIONS: usize = 256;
75
76/// How long a refusal is given to reach the peer before the socket is dropped anyway.
77///
78/// A peer that has filled our send buffer and stopped reading must not be able to hold the
79/// *refusal* open either — that would turn the cap into the same resource hole it closes.
80const REFUSAL_WRITE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
81
82/// Tracks how many connections a server currently has admitted.
83///
84/// Cheap to clone; every clone refers to the same count.
85#[derive(Clone, Debug)]
86pub struct ConnectionLimiter {
87    slots: Arc<Semaphore>,
88    max: usize,
89}
90
91impl ConnectionLimiter {
92    /// A limiter admitting at most `max` connections at once.
93    ///
94    /// `max` of zero would refuse everything, which is never what a caller means, so it is
95    /// clamped to one.
96    pub fn new(max: usize) -> Self {
97        let max = max.max(1);
98        Self {
99            slots: Arc::new(Semaphore::new(max)),
100            max,
101        }
102    }
103
104    /// The cap this limiter was built with, for log lines and tests.
105    pub fn max(&self) -> usize {
106        self.max
107    }
108
109    /// Connections currently admitted.
110    pub fn in_use(&self) -> usize {
111        self.max - self.slots.available_permits()
112    }
113
114    /// Take a slot, or `None` if the server is at its cap.
115    ///
116    /// Never waits: queueing a peer that is over the cap is just a slower way of holding the
117    /// resource it was refused.
118    pub fn try_acquire(&self) -> Option<ConnectionPermit> {
119        Arc::clone(&self.slots)
120            .try_acquire_owned()
121            .ok()
122            .map(|permit| ConnectionPermit { _permit: permit })
123    }
124}
125
126/// One admitted connection's slot. Released when dropped, so it must live as long as the
127/// connection task does.
128#[derive(Debug)]
129pub struct ConnectionPermit {
130    _permit: OwnedSemaphorePermit,
131}
132
133/// Accept the next connection the server has room for, refusing the ones it does not.
134///
135/// Returns only admitted peers. A peer over the cap is answered with `refusal` (skipped when
136/// empty), logged at WARN with `decision=fail_closed_connection_cap`, closed, and never seen
137/// by the caller.
138///
139/// `Err` means the *listener* failed, which is the caller's signal to break its accept loop —
140/// exactly the errors `TcpListener::accept` already returns.
141pub async fn accept_bounded(
142    listener: &TcpListener,
143    limiter: &ConnectionLimiter,
144    refusal: &[u8],
145    protocol: &str,
146    status_tx: Option<&mpsc::UnboundedSender<String>>,
147) -> std::io::Result<(TcpStream, SocketAddr, ConnectionPermit)> {
148    loop {
149        let (socket, peer_addr) = listener.accept().await?;
150
151        match limiter.try_acquire() {
152            Some(permit) => return Ok((socket, peer_addr, permit)),
153            None => {
154                Log::new(status_tx).warn(format!(
155                    "{} connection from {} refused decision=fail_closed_connection_cap \
156                     (limit {} concurrent)",
157                    protocol,
158                    peer_addr,
159                    limiter.max()
160                ));
161                refuse(socket, refusal).await;
162            }
163        }
164    }
165}
166
167/// [`accept_bounded`] for a Unix domain socket listener (`socket_file`, `ssh_agent`).
168///
169/// A filesystem socket is exposed to every local process that can write the node, and a peer
170/// that connects and says nothing holds a task and an `AppState` entry exactly as a TCP peer
171/// does, so it needs the same cap. There is no peer address to log, so the refusal line names
172/// the listener's own path instead.
173#[cfg(unix)]
174pub async fn accept_bounded_unix(
175    listener: &tokio::net::UnixListener,
176    limiter: &ConnectionLimiter,
177    refusal: &[u8],
178    protocol: &str,
179    status_tx: Option<&mpsc::UnboundedSender<String>>,
180) -> std::io::Result<(tokio::net::UnixStream, ConnectionPermit)> {
181    loop {
182        let (socket, _) = listener.accept().await?;
183
184        match limiter.try_acquire() {
185            Some(permit) => return Ok((socket, permit)),
186            None => {
187                let path = listener
188                    .local_addr()
189                    .ok()
190                    .and_then(|a| a.as_pathname().map(|p| p.display().to_string()))
191                    .unwrap_or_else(|| "<unnamed>".to_string());
192                Log::new(status_tx).warn(format!(
193                    "{} connection on {} refused decision=fail_closed_connection_cap \
194                     (limit {} concurrent)",
195                    protocol,
196                    path,
197                    limiter.max()
198                ));
199                refuse(socket, refusal).await;
200            }
201        }
202    }
203}
204
205/// Write the protocol's "busy" bytes and close.
206///
207/// Both halves are best-effort and time-bounded: the peer is already being refused, so nothing
208/// here may block the accept loop. Bytes first, then an explicit shutdown so the peer reads the
209/// refusal followed by a clean EOF rather than a reset that discards it.
210async fn refuse<S: tokio::io::AsyncWrite + Unpin>(mut socket: S, refusal: &[u8]) {
211    let _ = tokio::time::timeout(REFUSAL_WRITE_TIMEOUT, async {
212        if !refusal.is_empty() {
213            let _ = socket.write_all(refusal).await;
214            let _ = socket.flush().await;
215        }
216        let _ = socket.shutdown().await;
217    })
218    .await;
219}
220
221/// Bound the upload tail discarded after an HTTP response has been flushed and the write
222/// side half-closed. Two MiB covers the one-MiB overrun exercised by real HTTP clients,
223/// including framing; two seconds also bounds a silent peer. Nothing is buffered or modelled.
224const RESPONSE_DRAIN_BYTES: usize = 2 * 1024 * 1024;
225const RESPONSE_DRAIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
226
227/// Discard an in-flight upload tail after flushing the response and half-closing writes.
228///
229/// Dropping a TCP socket while unread upload bytes remain can send RST and erase a 413
230/// before the client reads it. The absolute time and byte budgets let an ordinary upload
231/// finish without allowing an endless or silent peer to keep the connection task alive.
232/// This runs only after the HTTP connection completes, outside live handler work.
233pub(crate) async fn drain_after_response(reader: &mut (impl AsyncRead + Unpin)) {
234    use tokio::io::AsyncReadExt;
235
236    let deadline = tokio::time::Instant::now() + RESPONSE_DRAIN_TIMEOUT;
237    let mut remaining = RESPONSE_DRAIN_BYTES;
238    let mut scratch = [0u8; 8192];
239    while remaining > 0 {
240        let read_size = remaining.min(scratch.len());
241        match tokio::time::timeout_at(deadline, reader.read(&mut scratch[..read_size])).await {
242            Ok(Ok(n)) if n > 0 => remaining -= n,
243            _ => break,
244        }
245    }
246}
247
248// ---------------------------------------------------------------------------------------
249// Read deadlines where the protocol's loop is not ours
250// ---------------------------------------------------------------------------------------
251//
252// Most servers here own their read loop, and the right shape there is the one
253// `src/server/whois/mod.rs` uses: wrap the `read()` — and *only* the `read()` — in
254// `tokio::time::timeout`, so the model round-trip and a `manual` rule parking an event for a
255// human sit outside the deadline by construction. Two protocols hand their socket to a crate
256// that owns the loop instead (`msql-srv`, `pgwire`), and the two types below are how they get
257// the same guarantee.
258
259use std::future::Future;
260use std::pin::Pin;
261use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
262use std::task::{Context, Poll};
263use std::time::Duration;
264use tokio::io::{AsyncRead, ReadBuf};
265
266/// An [`AsyncRead`] that fails with [`std::io::ErrorKind::TimedOut`] when the peer sends
267/// nothing for `idle`.
268///
269/// This exists for a crate that takes a generic reader and runs the protocol loop itself, where
270/// there is no `read()` call of ours to wrap.
271///
272/// **Why it cannot evict a live-but-slow operation.** The deadline is armed lazily, when a read
273/// is polled and finds nothing, and disarmed the moment bytes arrive. While the crate is off
274/// doing something else with what it read — an LLM round-trip, a `manual` event parked for a
275/// human at the dashboard — this reader is not being polled at all, so no clock is running. The
276/// next poll after that work finishes arms a *fresh* deadline rather than inheriting a stale
277/// one. That lazy arming is the whole correctness argument: a version that reset the deadline
278/// only on a successful read would fire immediately after any pause longer than `idle`, which
279/// is exactly the live-transfer eviction this project already learned about from TFTP.
280pub struct IdleTimeoutReader<R> {
281    inner: R,
282    /// Bound until the peer's first byte. Kept separate because "has said nothing at all" and
283    /// "has gone quiet mid-session" deserve different answers — the whois pair, expressed for a
284    /// loop that is not ours.
285    first: Duration,
286    idle: Duration,
287    seen_bytes: bool,
288    /// Armed only while a read is outstanding with no bytes yet.
289    deadline: Option<Pin<Box<tokio::time::Sleep>>>,
290}
291
292impl<R> IdleTimeoutReader<R> {
293    /// One bound for every read.
294    pub fn new(inner: R, idle: Duration) -> Self {
295        Self::with_first(inner, idle, idle)
296    }
297
298    /// `first` until the peer's first byte arrives, `idle` for every read after that.
299    pub fn with_first(inner: R, first: Duration, idle: Duration) -> Self {
300        Self {
301            inner,
302            first,
303            idle,
304            seen_bytes: false,
305            deadline: None,
306        }
307    }
308}
309
310impl<R: AsyncRead + Unpin> AsyncRead for IdleTimeoutReader<R> {
311    fn poll_read(
312        self: Pin<&mut Self>,
313        cx: &mut Context<'_>,
314        buf: &mut ReadBuf<'_>,
315    ) -> Poll<std::io::Result<()>> {
316        let this = self.get_mut();
317        let before = buf.filled().len();
318        match Pin::new(&mut this.inner).poll_read(cx, buf) {
319            Poll::Ready(result) => {
320                // Bytes (or EOF, or an error): whatever happens next is not this reader's
321                // business, so disarm.
322                this.deadline = None;
323                if result.is_ok() && buf.filled().len() > before {
324                    this.seen_bytes = true;
325                }
326                Poll::Ready(result)
327            }
328            Poll::Pending => {
329                let bound = if this.seen_bytes {
330                    this.idle
331                } else {
332                    this.first
333                };
334                let deadline = this
335                    .deadline
336                    .get_or_insert_with(|| Box::pin(tokio::time::sleep(bound)));
337                match deadline.as_mut().poll(cx) {
338                    Poll::Ready(()) => {
339                        this.deadline = None;
340                        Poll::Ready(Err(std::io::Error::new(
341                            std::io::ErrorKind::TimedOut,
342                            "peer sent nothing before the idle deadline",
343                        )))
344                    }
345                    Poll::Pending => Poll::Pending,
346                }
347            }
348        }
349    }
350}
351
352/// Whether a connection whose loop belongs to a crate is idle, and since when.
353///
354/// [`IdleTimeoutReader`] is the better answer wherever the crate takes a generic reader.
355/// `pgwire` does not — `process_socket` takes a concrete `TcpStream` — so PostgreSQL bounds its
356/// idle time from the outside instead, with a watchdog reading this.
357///
358/// The `in_flight` counter is what keeps that watchdog honest. A connection parked on an LLM
359/// call, or on a `manual` rule waiting for a human, is *working*, not idle, and must never be
360/// closed from under itself; the handler raises the counter for the whole of that work, and
361/// [`watch_idle`] refuses to fire while it is non-zero. Wall-clock alone would have made the
362/// bound "longer than the longest park anyone configures", which is a guess rather than a rule.
363#[derive(Debug)]
364pub struct ConnectionActivity {
365    /// Milliseconds since this process's clock epoch at the last sign of life.
366    last: AtomicU64,
367    /// Operations currently being answered on this connection.
368    in_flight: AtomicUsize,
369}
370
371impl Default for ConnectionActivity {
372    fn default() -> Self {
373        Self::new()
374    }
375}
376
377impl ConnectionActivity {
378    pub fn new() -> Self {
379        Self {
380            last: AtomicU64::new(now_millis()),
381            in_flight: AtomicUsize::new(0),
382        }
383    }
384
385    /// Note that something crossed the connection.
386    pub fn touch(&self) {
387        self.last.store(now_millis(), Ordering::Relaxed);
388    }
389
390    /// Mark the start of work the peer is waiting on. Pair with [`Self::end_work`].
391    pub fn begin_work(&self) {
392        self.touch();
393        self.in_flight.fetch_add(1, Ordering::SeqCst);
394    }
395
396    /// Mark the end of that work, and treat its completion as activity.
397    pub fn end_work(&self) {
398        self.in_flight.fetch_sub(1, Ordering::SeqCst);
399        self.touch();
400    }
401
402    /// Mark the connection busy until the returned guard is dropped.
403    ///
404    /// Preferred over a bare [`Self::begin_work`]/[`Self::end_work`] pair anywhere the work has
405    /// more than one exit — a `?`, an early `return`, an unwinding panic. Leaking the mark
406    /// pins the connection "busy" forever and silently disables the idle bound, which is the
407    /// worst possible failure for a guard nobody is looking at.
408    pub fn busy(self: &Arc<Self>) -> BusyGuard {
409        self.begin_work();
410        BusyGuard(Arc::clone(self))
411    }
412
413    /// How long the connection has been doing nothing at all. `None` while work is in flight,
414    /// which is the whole point: busy is not idle.
415    pub fn idle_for(&self) -> Option<Duration> {
416        if self.in_flight.load(Ordering::SeqCst) > 0 {
417            return None;
418        }
419        let last = self.last.load(Ordering::Relaxed);
420        Some(Duration::from_millis(now_millis().saturating_sub(last)))
421    }
422}
423
424/// Holds a connection "busy" for its lifetime. See [`ConnectionActivity::busy`].
425pub struct BusyGuard(Arc<ConnectionActivity>);
426
427impl Drop for BusyGuard {
428    fn drop(&mut self) {
429        self.0.end_work();
430    }
431}
432
433/// Watch a [`ConnectionActivity`] and resolve once it has been idle for `idle`.
434///
435/// Returns as soon as the bound is exceeded; the caller decides what to do about it (PostgreSQL
436/// aborts the connection task). This polls rather than waking on an event because the thing
437/// being watched is the *absence* of events, and a coarse tick costs nothing when the bound is
438/// minutes.
439pub async fn watch_idle(activity: Arc<ConnectionActivity>, idle: Duration) {
440    let tick = (idle / 20).clamp(Duration::from_millis(250), Duration::from_secs(15));
441    loop {
442        tokio::time::sleep(tick).await;
443        if let Some(elapsed) = activity.idle_for() {
444            if elapsed >= idle {
445                return;
446            }
447        }
448    }
449}
450
451/// [`watch_idle`] for a session protocol whose client may legitimately say nothing for a long
452/// time: resolve once `activity` has been idle for `idle`, having called `probe` once at half
453/// of it.
454///
455/// This is the shape for WebSocket-carried servers (`websocket`, `webrtc`, `webrtc_signaling`):
456/// the probe queues a Ping, every RFC 6455 endpoint answers a Ping with a Pong by itself, and the
457/// caller touches `activity` on every inbound frame — so a live-but-silent client is never
458/// closed, and what reaches the bound is a peer that has stopped reading or vanished without a
459/// FIN. The bound is on liveness, not on conversation.
460///
461/// The probe fires once per silent stretch, not per tick: any activity starts a new stretch, and
462/// a busy connection (work in flight) is neither probed nor reported idle.
463pub async fn watch_idle_with_probe<F: FnMut()>(
464    activity: Arc<ConnectionActivity>,
465    idle: Duration,
466    mut probe: F,
467) {
468    let tick = (idle / 20).clamp(Duration::from_millis(100), Duration::from_secs(15));
469    let mut probed = false;
470    loop {
471        tokio::time::sleep(tick).await;
472        match activity.idle_for() {
473            Some(elapsed) if elapsed >= idle => return,
474            Some(elapsed) if elapsed >= idle / 2 => {
475                if !probed {
476                    probe();
477                    probed = true;
478                }
479            }
480            // Busy, or heard from recently: the next silent stretch gets its own probe.
481            _ => probed = false,
482        }
483    }
484}
485
486/// The payload of the keepalive Ping the WebSocket-carried servers send through
487/// [`watch_idle_with_probe`]. A Pong echoes it (RFC 6455 §5.5.3); it is not checked, because any
488/// frame is proof of life, but it makes the Ping identifiable in a capture.
489pub const KEEPALIVE_PING_PAYLOAD: &[u8] = b"netget-keepalive";
490
491/// Milliseconds on a monotonic clock. `utils::clock` rather than `std::time::Instant`, which
492/// panics on wasm32 (see the project CLAUDE.md).
493fn now_millis() -> u64 {
494    crate::utils::clock::Instant::now()
495        .duration_since(*PROCESS_START)
496        .as_millis() as u64
497}
498
499static PROCESS_START: std::sync::LazyLock<crate::utils::clock::Instant> =
500    std::sync::LazyLock::new(crate::utils::clock::Instant::now);
501
502#[cfg(test)]
503mod response_drain_tests {
504    use super::*;
505
506    #[tokio::test]
507    async fn response_drain_stops_at_the_byte_budget() {
508        let bytes = vec![0; RESPONSE_DRAIN_BYTES + 1];
509        let mut reader = bytes.as_slice();
510        drain_after_response(&mut reader).await;
511        assert_eq!(
512            reader.len(),
513            1,
514            "an endless uploader cannot keep the task alive"
515        );
516    }
517
518    #[tokio::test]
519    async fn response_drain_stops_even_when_the_peer_keeps_its_write_half_open() {
520        let (mut reader, _silent_peer) = tokio::io::duplex(1);
521        let started = tokio::time::Instant::now();
522        tokio::time::timeout(
523            RESPONSE_DRAIN_TIMEOUT + std::time::Duration::from_secs(5),
524            drain_after_response(&mut reader),
525        )
526        .await
527        .expect("a silent peer cannot keep the task alive");
528        assert!(started.elapsed() >= RESPONSE_DRAIN_TIMEOUT);
529    }
530}