Skip to main content

INBOUND_QUEUE_DEPTH

Constant INBOUND_QUEUE_DEPTH 

Source
pub const INBOUND_QUEUE_DEPTH: usize = 64;
Expand description

Inbound datagrams buffered per session before packets start being dropped.

Bounded on purpose: UDP allows loss, whereas an unbounded queue would let a slow target turn into unbounded memory growth. The same queue serves both phases of a session — the one before the target socket exists, where it holds what RFC 9298 §5 calls optimistically sent packets, and the running one — so this constant is the whole per-session bound rather than one of two.

§What this costs at the configured limits

A session holds three buffers, and this constant sizes only the first of them.

The queue is depth x payload. Only the payload needs care: a QUIC DATAGRAM frame cannot be fragmented, so a datagram never exceeds the max_udp_payload_size this server advertises (quinn’s default, 1472 bytes) even though RFC 9298 §5 permits a 65527-byte UDP payload in principle. At INBOUND_QUEUE_DEPTH = 64 that is ~92 KiB.

The second is the buffer a running session reads its target socket into (crate::tunnel’s UDP path), one per session and sized for the largest UDP payload there can be — the whole 65527 bytes, ~64 KiB — because a packet that has arrived must fit somewhere before its length is known. Unlike the queue it exists only once the session loop is running, so a session refused before its socket is bound never allocates one.

The third is the crate::capsule::CapsuleDecoder on the request stream, which buffers a DATAGRAM capsule’s value until all of it has arrived — crate::capsule::MAX_DATAGRAM_CAPSULE_VALUE bounds it, and a peer that declares that much and stops one byte short holds it for the session’s idle timeout. Measured at ~78 KiB, since the BytesMut behind it doubles its way to 64 KiB rather than landing on it (tests/it_bounds.rs::a_session_holds_one_unfinished_capsule_and_no_more).

A session therefore costs ~236 KiB, and the worst case is that times max_targets_per_conn times max_connections. With the shipped defaults (max_targets_per_conn = 256, max_connections = 256) that is ~59 MiB per connection and ~14.7 GiB across a server saturated at both limits — an operator lowering either limit lowers it proportionally.

All three are released by dropping what owns them rather than by any explicit call, on whichever of the half-dozen paths a session ends by; tests/it_bounds.rs::a_connection_keeps_nothing_for_the_sessions_it_has_closed is where that composition is weighed rather than reasoned about.

Registering a session before its target socket exists does not raise that ceiling: the queue is the same size in both phases, sessions are still capped by the per-connection tunnel quota, and a full queue is already reachable on a running session whenever a client sends faster than the proxy forwards. What it changes is how long a full queue can sit undrained — no longer than name resolution takes, after which the session either starts draining or is refused and the queue is discarded with it.