Skip to main content

weida_core/
limits.rs

1//! Resource limits.
2//!
3//! Master doc §50: no internal queue may be unbounded, and every allocation
4//! that a remote peer can influence must have a defensive ceiling. Each field
5//! below names the remote-controlled quantity it bounds.
6//!
7//! **`Limits` is a per-connection profile.** Every field applies to one
8//! connection — which is what a runtime holding one profile per connection
9//! tier will need
10//! (`docs/decisions/0002-control-and-bulk-separation.md` §6.4). Numbers that
11//! belong to a runtime rather than to a connection — how many connections a
12//! binding accepts, how many one peer may hold, how deep an endpoint's queue
13//! is, how many resolved addresses a dial tries — live on `RuntimeConfig`
14//! instead, so that no profile can carry a value nothing reads.
15
16use std::time::Duration;
17
18/// Resource limits applied to one connection.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct Limits {
21    /// Largest frame header this side will accept, in bytes. Checked against
22    /// the preamble length *before* any allocation.
23    pub max_header_bytes: u64,
24    /// QUIC concurrent inbound unidirectional streams. Bounds the number of
25    /// live per-stream parse tasks for one-way transfers.
26    pub max_concurrent_uni_streams: u32,
27    /// QUIC concurrent inbound bidirectional streams; bounds live request
28    /// exchanges per connection and, with `max_header_bytes`, worst-case
29    /// header memory.
30    pub max_concurrent_bidi_streams: u32,
31    /// QUIC per-stream receive window in bytes. Bounds buffered payload for one
32    /// transfer and provides backpressure to the sender.
33    pub stream_receive_window: u64,
34    /// QUIC per-connection receive window in bytes.
35    pub connection_receive_window: u64,
36    /// QUIC keep-alive interval. Sent by the dialling side only, so a binding's
37    /// value is not used.
38    pub keep_alive: Duration,
39    /// QUIC idle timeout, applied in both directions.
40    pub idle_timeout: Duration,
41    /// How long to wait for the peer HELLO before closing the connection with
42    /// `NEGOTIATION_FAILED`, in milliseconds.
43    pub hello_timeout_ms: u64,
44    /// Subscription filters one peer connection may hold at once, summed over
45    /// every path. Exceeding it closes the connection with `LIMIT_EXCEEDED`:
46    /// SUBSCRIBE has no stream to answer with an ERROR frame.
47    pub max_subscriptions: usize,
48    /// Payload bytes a publisher may hold queued for one subscriber. A message
49    /// that does not fit is dropped for that subscriber and counted; the
50    /// publisher never blocks on a slow consumer.
51    pub subscriber_buffer_bytes: usize,
52    /// Producer scopes — paths and topics — a receiver tracks per connection
53    /// for gap detection or reassembly, when `PerProducer` ordering is
54    /// negotiated. The peer chooses the scope names, so the table needs a
55    /// ceiling; at the cap a new scope is simply not tracked, no gap is
56    /// reported for it and nothing is held back for it.
57    pub max_sequence_scopes: usize,
58    /// Transfers a receiver may hold back at once, summed over scopes, when
59    /// `PerProducer(reassemble)` ordering is negotiated. A held transfer is
60    /// an unread stream, so the bytes it pins are quinn's — up to
61    /// `stream_receive_window` for it and `connection_receive_window` for the
62    /// hold as a whole. At the cap the oldest held transfer is released out
63    /// of order with its gap reported, never held in a growing buffer.
64    pub max_reorder_hold: usize,
65    /// Live transfers on one **local** connection, where the OS connection
66    /// *is* the stream and there is no multiplexing
67    /// ([decisions/0010](../../../docs/decisions/0010-local-transport.md)
68    /// §4.2). Windows caps named-pipe instances at 1-255, which is the
69    /// tightest platform limit and therefore the one the default respects;
70    /// an `open` at the cap waits for a live transfer to end, exactly as a
71    /// QUIC `open` waits on the peer's stream budget, rather than refusing.
72    /// A peer configured with zero can therefore open nothing and waits
73    /// until its caller's deadline, exactly as one granted no QUIC streams.
74    pub max_local_streams: usize,
75    /// Connections a subscriber parks toward a peer it dialled, so that peer
76    /// can open a stream back
77    /// ([decisions/0012](../../../docs/decisions/0012-local-connection-grouping.md)
78    /// §4.4). An accepted socket cannot be dialled, so fan-out over a local
79    /// socket transport rides connections the subscriber opened and left
80    /// waiting; each is a file descriptor held for a copy that may never
81    /// come, and each counts against `max_local_streams` on both sides. A
82    /// publisher that finds none parked drops that copy and counts it, the
83    /// same answer fan-out already gives an exhausted subscriber budget. Zero
84    /// disables the pool, which makes a subscription over such a transport an
85    /// error at subscribe time rather than a silence.
86    pub max_parked_reverse: usize,
87
88    /// Identities a receiver remembers per connection for `Bounded`
89    /// deduplication. The window bounds how long an identity is kept, not
90    /// how many arrive within it, so the count needs its own ceiling; at the
91    /// cap the oldest entry is evicted, which costs suppression rather than
92    /// memory.
93    pub max_dedup_entries: usize,
94
95    /// Bytes of a peer's QUIC datagrams buffered unread per connection,
96    /// oldest dropped first. **Zero turns flows off**: the connection then
97    /// advertises neither capability code `1` nor `max_datagram_frame_size`,
98    /// buffers nothing and runs no datagram reader
99    /// ([decisions/0034](../../../docs/decisions/0034-late-is-lost.md) §4.5).
100    /// [`DEFAULT_DATAGRAM_RECEIVE_BYTES`] is the documented value for turning
101    /// them on.
102    pub datagram_receive_bytes: usize,
103    /// Bytes of datagrams queued for sending per connection on QUIC, and per
104    /// flow on a local transport; beyond it the oldest queued one is
105    /// discarded. Bounds what this side holds for a peer that reads slowly.
106    pub datagram_send_bytes: usize,
107    /// Whether this side lists capability code `2` on a QUIC connection, so
108    /// that when the peer lists it too each side sends the other its view of
109    /// the path every 2 s and keeps the peer's latest record, read as
110    /// `ConnectionStats::remote`
111    /// ([decisions/0036](../../../docs/decisions/0036-connection-statistics.md)
112    /// §4.5). Off by default; a local transport never lists it.
113    pub path_report: bool,
114    /// Inbound flows one connection may hold live. Each is a FLOW stream the
115    /// peer opened and a ring this side keeps; a FLOW beyond it is stopped
116    /// with `LIMIT_EXCEEDED`.
117    pub max_flows: usize,
118    /// Unread datagram bytes held per inbound flow; a consumer that falls
119    /// behind loses its oldest datagrams, counted per flow.
120    pub flow_queue_bytes: usize,
121    /// Bytes of datagrams held per connection for flow ids with no live flow
122    /// yet — a datagram can overtake its FLOW header — and the one place a
123    /// peer can make this side hold datagrams it never registered.
124    pub flow_early_bytes: usize,
125    /// How long a datagram waits in that ring for its FLOW header before it
126    /// is dropped and counted.
127    pub flow_early_hold: Duration,
128
129    /// The congestion controller of this profile's QUIC connections. Not a
130    /// bound, but a per-connection choice, which is why it lives here: a
131    /// bulk profile that shares a bottleneck with media may want a different
132    /// controller than a media profile
133    /// ([decisions/0034](../../../docs/decisions/0034-late-is-lost.md) §6).
134    pub congestion: Congestion,
135}
136
137/// A QUIC congestion controller, as `quinn` implements them.
138#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
139pub enum Congestion {
140    /// CUBIC (RFC 9438), `quinn`'s default: loss-based.
141    #[default]
142    Cubic,
143    /// NewReno (RFC 9002 §7): loss-based, the conservative baseline.
144    NewReno,
145    /// BBR: model-based, reacts to delay rather than waiting for loss.
146    /// `quinn` marks its implementation experimental.
147    Bbr,
148}
149
150/// The documented `Limits::datagram_receive_bytes` for a profile that
151/// enables datagram flows: 64 KiB, about fifty full datagrams.
152pub const DEFAULT_DATAGRAM_RECEIVE_BYTES: usize = 65_536;
153
154impl Limits {
155    /// Worst-case header memory a single hostile connection can pin, in bytes.
156    ///
157    /// Both stream budgets count: a peer may open its full uni *and* bidi
158    /// allowance, and every accepted stream starts with one header.
159    pub const fn worst_case_header_memory(&self) -> u64 {
160        self.max_header_bytes
161            * (self.max_concurrent_uni_streams as u64 + self.max_concurrent_bidi_streams as u64)
162    }
163}
164
165impl Default for Limits {
166    fn default() -> Self {
167        Limits {
168            max_header_bytes: 16 * 1024,
169            max_concurrent_uni_streams: 2048,
170            max_concurrent_bidi_streams: 1024,
171            stream_receive_window: 1024 * 1024,
172            connection_receive_window: 16 * 1024 * 1024,
173            keep_alive: Duration::from_secs(10),
174            idle_timeout: Duration::from_secs(30),
175            hello_timeout_ms: 10_000,
176            max_subscriptions: 256,
177            subscriber_buffer_bytes: 8 * 1024 * 1024,
178            max_sequence_scopes: 1024,
179            max_reorder_hold: 256,
180            max_local_streams: 255,
181            max_parked_reverse: 8,
182
183            max_dedup_entries: 4096,
184            datagram_receive_bytes: 0,
185            datagram_send_bytes: 65_536,
186            path_report: false,
187            max_flows: 64,
188            flow_queue_bytes: 16_384,
189            flow_early_bytes: 4_096,
190            flow_early_hold: Duration::from_secs(1),
191            congestion: Congestion::Cubic,
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn defaults_match_the_protocol_document() {
202        let l = Limits::default();
203        assert_eq!(l.max_header_bytes, 16384);
204        assert_eq!(l.max_concurrent_uni_streams, 2048);
205        assert_eq!(l.max_concurrent_bidi_streams, 1024);
206        assert_eq!(l.stream_receive_window, 1 << 20);
207        assert_eq!(l.connection_receive_window, 16 << 20);
208        assert_eq!(l.keep_alive, Duration::from_secs(10));
209        assert_eq!(l.idle_timeout, Duration::from_secs(30));
210        assert_eq!(l.hello_timeout_ms, 10_000);
211        assert_eq!(l.max_subscriptions, 256);
212        assert_eq!(l.subscriber_buffer_bytes, 8 << 20);
213        assert_eq!(l.max_sequence_scopes, 1024);
214        assert_eq!(l.max_reorder_hold, 256);
215        assert_eq!(l.max_local_streams, 255);
216        assert_eq!(l.max_parked_reverse, 8);
217        assert_eq!(l.max_dedup_entries, 4096);
218        assert_eq!(l.datagram_receive_bytes, 0);
219        assert_eq!(DEFAULT_DATAGRAM_RECEIVE_BYTES, 64 << 10);
220        assert_eq!(l.datagram_send_bytes, 64 << 10);
221        assert_eq!(l.max_flows, 64);
222        assert_eq!(l.flow_queue_bytes, 16 << 10);
223        assert_eq!(l.flow_early_bytes, 4 << 10);
224        assert_eq!(l.flow_early_hold, Duration::from_secs(1));
225        assert_eq!(l.congestion, Congestion::Cubic);
226    }
227
228    #[test]
229    fn worst_case_header_memory_is_48_mib() {
230        assert_eq!(Limits::default().worst_case_header_memory(), 48 << 20);
231    }
232}