Skip to main content

irtt_server/
config.rs

1use std::time::Duration;
2
3/// Default bound on the number of simultaneously live sessions.
4///
5/// This is `irtt-rs` resource policy, not an interoperability requirement. The
6/// protocol has no rejection response, so a server refuses by staying silent
7/// and the client sees an open timeout it already has to tolerate. Upstream was
8/// observed to impose no session bound at all; the server specification
9/// explicitly records that as an upstream policy choice which a compatible
10/// server is not expected to reproduce.
11pub const DEFAULT_MAX_SESSIONS: usize = 1024;
12
13/// Default bound on the executable echo datagram size of one session, in bytes.
14///
15/// This is `irtt-rs` resource policy, not an interoperability requirement and
16/// not a protocol constant. The server specification records upstream defaulting
17/// to *unlimited*, which leaves a remotely negotiated allocation unbounded; that
18/// is explicitly not a compatibility target.
19///
20/// 65,507 is the largest payload an IPv4 UDP datagram can carry, so it is the
21/// conservative ceiling above which no negotiated length could be emitted as one
22/// datagram on any address family. It bounds a session's echo buffer to roughly
23/// 64 KiB, and it is already the maximum a normal `irtt-rs` client will ask for,
24/// so ordinary traffic never meets it.
25///
26/// It is **not** an MTU and promises nothing about what a path will carry. Path
27/// and interface policy is a separate, later concern; this value exists to keep
28/// allocation bounded, not to predict fragmentation.
29pub const DEFAULT_MAX_PACKET_LENGTH: usize = 65_507;
30
31/// Default floor on the send interval a session may negotiate, and the cadence
32/// its reply allowance replenishes at.
33///
34/// This matches the reference server's default policy. It is policy on both
35/// sides: the server specification lists the minimum send interval among the
36/// reference server's freely changeable defaults, not among the values the wire
37/// format fixes.
38pub const DEFAULT_MIN_SEND_INTERVAL: Duration = Duration::from_millis(10);
39
40/// Default number of echo requests a session may have answered before its
41/// allowance has to replenish.
42///
43/// This matches the reference server's default policy, and like it, the
44/// allowance is per session: one session's burst is not another's.
45pub const DEFAULT_BURST_ALLOWANCE: u32 = 5;
46
47/// Default idle lifetime of a session.
48///
49/// A session that goes this long without an echo request is released. This
50/// matches the reference server's default of one minute, but the deadline it
51/// measures is deliberately not upstream's: `irtt-rs` expires immediately at the
52/// deadline, applies it to sessions that have never carried an echo request, and
53/// emits no final reply. See [`ServerConfig::with_idle_timeout`].
54pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
55
56/// How many timestamps a server is willing to provide.
57///
58/// This is **server policy**, deliberately a separate type from the wire
59/// [`StampAt`](irtt_proto::StampAt) a session requests and negotiates: one
60/// describes what a client asked for, the other what this server will hand out.
61/// Negotiation reduces the requested `StampAt` accordingly, and only it — the
62/// requested [`Clock`](irtt_proto::Clock) is never rewritten, because the clean
63/// evidence records a timestamp allowance restricting timestamp *placement*, not
64/// the clock domains it is read from.
65///
66/// See [`ServerConfig::with_timestamp_allowance`] for the mapping.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub enum TimestampAllowance {
69    /// No timestamps at all: every request is negotiated to
70    /// [`StampAt::None`](irtt_proto::StampAt::None).
71    None,
72    /// At most one timestamp *instant*. A request for both receive and send
73    /// becomes [`StampAt::Midpoint`](irtt_proto::StampAt::Midpoint); every
74    /// placement that already names a single instant is left alone.
75    ///
76    /// This restricts placement, not field count: the reply still carries one
77    /// field per negotiated clock domain, so a session on
78    /// [`Clock::Both`](irtt_proto::Clock::Both) receives both a wall and a
79    /// monotonic midpoint field.
80    Single,
81    /// Every requested placement is honored. This is the default, and the
82    /// behavior of a server that configures nothing.
83    #[default]
84    Dual,
85}
86
87/// Configuration for a [`ServerCore`](crate::ServerCore).
88///
89/// Fields are private and set through consuming builder methods, so later
90/// slices can add configuration without breaking construction.
91///
92/// **Server fill is deliberately not configurable.** The policy is fixed and
93/// safe: every valid descriptor is honored, one that cannot be parsed falls back
94/// to `pattern:69727474`, and a client expressing no preference is served that
95/// same default. There is no allow-list to configure, because refusing a valid
96/// mode would prevent nothing — the payload carries no protocol meaning, a
97/// descriptor is bounded to 32 wire bytes, a random fill is bounded by
98/// [`max_packet_length`](Self::max_packet_length) like every other reply, and
99/// `none` is zero-filled here rather than left as residue. An operator
100/// restriction can be added if a real use case turns up.
101///
102/// # Example
103///
104/// Configure a modest per-listener session budget and restrict timestamp
105/// placement while leaving DSCP marking disabled:
106///
107/// ```
108/// use std::time::Duration;
109///
110/// use irtt_server::{ServerConfig, TimestampAllowance};
111///
112/// let config = ServerConfig::default()
113///     .with_max_sessions(128)
114///     .with_idle_timeout(Duration::from_secs(30))
115///     .with_timestamp_allowance(TimestampAllowance::Single)
116///     .with_dscp_allowed(false);
117///
118/// assert_eq!(config.max_sessions(), 128);
119/// assert_eq!(config.idle_timeout(), Duration::from_secs(30));
120/// assert_eq!(config.timestamp_allowance(), TimestampAllowance::Single);
121/// assert!(!config.dscp_allowed());
122/// ```
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct ServerConfig {
125    hmac_key: Option<Vec<u8>>,
126    max_sessions: usize,
127    max_packet_length: usize,
128    min_send_interval: Duration,
129    burst_allowance: u32,
130    idle_timeout: Duration,
131    max_test_duration: Option<Duration>,
132    timestamp_allowance: TimestampAllowance,
133    dscp_allowed: bool,
134}
135
136impl ServerConfig {
137    /// Sets the HMAC key, enabling authentication for this server.
138    ///
139    /// Authentication is global: with a key configured, every request must
140    /// carry `FLAG_HMAC` and a valid MAC, and every reply is authenticated.
141    /// Without one, any request carrying `FLAG_HMAC` is dropped.
142    #[must_use]
143    pub fn with_hmac_key(mut self, key: impl Into<Vec<u8>>) -> Self {
144        self.hmac_key = Some(key.into());
145        self
146    }
147
148    /// Sets the maximum number of simultaneously live sessions.
149    ///
150    /// Once the table holds this many sessions, a session-creating open is
151    /// silently dropped; nothing is evicted. Zero refuses every
152    /// session-creating open while still answering no-test opens, which create
153    /// no state. There is deliberately no unbounded setting.
154    #[must_use]
155    pub fn with_max_sessions(mut self, max_sessions: usize) -> Self {
156        self.max_sessions = max_sessions;
157        self
158    }
159
160    /// Sets the maximum echo datagram size a session may negotiate, in bytes.
161    ///
162    /// This governs the *executable* echo datagram of a session, not the size of
163    /// the open exchange: a requested positive Length above this value is
164    /// reduced to it during negotiation, and an open whose mandatory echo field
165    /// block would not fit is silently refused, since the server could not emit
166    /// a compliant reply for it. Authentication counts toward that field block.
167    /// The same limit additionally admits inbound echo request datagrams by
168    /// their received length, before any receive state moves.
169    ///
170    /// The value is trusted local configuration, like
171    /// [`with_max_sessions`](Self::with_max_sessions): an operator may
172    /// deliberately raise it far past what any path carries, and that is their
173    /// resource choice to make. What must stay bounded is the *default*, which
174    /// is why there is deliberately no unlimited setting.
175    ///
176    /// Zero is accepted and is not a synonym for unlimited. No session can
177    /// describe an echo packet smaller than its mandatory field block, so a zero
178    /// maximum silently refuses every session-creating open — and every no-test
179    /// open too, since both are validated against the same effective session.
180    #[must_use]
181    pub fn with_max_packet_length(mut self, max_packet_length: usize) -> Self {
182        self.max_packet_length = max_packet_length;
183        self
184    }
185
186    /// Sets the floor on the send interval a session may negotiate, which is
187    /// also the cadence its reply allowance replenishes at.
188    ///
189    /// A requested interval below this is raised to it during negotiation, and
190    /// an absent Interval — which means the wire default of zero, not an
191    /// explicit zero, which is refused — is answered with it. The negotiated
192    /// value may still end up *below* this floor, because the idle-timeout cap
193    /// of [`with_idle_timeout`](Self::with_idle_timeout) is applied afterwards;
194    /// where that happens, the session's allowance replenishes at the shorter
195    /// negotiated interval instead, so this server never enforces a cadence
196    /// slower than the one it handed the client.
197    ///
198    /// [`Duration::ZERO`] disables both: no interval is raised, and an
199    /// otherwise admissible echo never waits for allowance. It does **not**
200    /// disable the burst allowance, which refuses everything when it is zero.
201    #[must_use]
202    pub fn with_min_send_interval(mut self, min_send_interval: Duration) -> Self {
203        self.min_send_interval = min_send_interval;
204        self
205    }
206
207    /// Sets how many echo requests a session may have answered before its
208    /// allowance has to replenish.
209    ///
210    /// The allowance is per session, starts full, is spent one unit per served
211    /// echo and refills at one unit per
212    /// [`min_send_interval`](Self::min_send_interval), never above this value.
213    /// An echo arriving with no allowance is dropped without a reply and
214    /// without advancing any reception statistic.
215    ///
216    /// Zero is accepted and means no allowance at all: every echo is
217    /// rate-limited, whatever the interval is set to. It is deliberately not a
218    /// synonym for unlimited.
219    #[must_use]
220    pub fn with_burst_allowance(mut self, burst_allowance: u32) -> Self {
221        self.burst_allowance = burst_allowance;
222        self
223    }
224
225    /// Sets how long a session may go without a served or rate-limited echo
226    /// request before it is released.
227    ///
228    /// The deadline runs from the open, not from the first echo, so a session
229    /// that never carries one still ages out. Release is silent — the protocol
230    /// defines no expiry notification — and immediate at the deadline.
231    ///
232    /// This additionally caps the negotiated send interval at a quarter of the
233    /// timeout, so a client sending at the interval it was given cannot idle
234    /// itself out.
235    ///
236    /// [`Duration::ZERO`] is accepted and is not a synonym for "never expire":
237    /// it releases a session the next time expiry is evaluated. There is
238    /// deliberately no unbounded setting.
239    #[must_use]
240    pub fn with_idle_timeout(mut self, idle_timeout: Duration) -> Self {
241        self.idle_timeout = idle_timeout;
242        self
243    }
244
245    /// Sets the maximum test duration a session may negotiate.
246    ///
247    /// A requested Duration above this is reduced to it, and a *continuous*
248    /// request — an absent Duration, meaning the wire default of zero — is
249    /// answered with it too, since restricting continuous mode to a finite test
250    /// is the whole point of configuring a maximum. An explicit zero Duration
251    /// is refused before negotiation and never reaches this.
252    ///
253    /// [`Duration::ZERO`] means *no maximum*, exactly as
254    /// [`without_max_test_duration`](Self::without_max_test_duration) does. A
255    /// finite maximum of zero could only be honored by negotiating a Duration of
256    /// zero, which on the wire means continuous — the opposite of a limit — so
257    /// rewriting a client's finite request into a continuous one is refused
258    /// rather than encoded.
259    #[must_use]
260    pub fn with_max_test_duration(mut self, max_test_duration: Duration) -> Self {
261        self.max_test_duration = (!max_test_duration.is_zero()).then_some(max_test_duration);
262        self
263    }
264
265    /// Removes any configured maximum test duration, which is the default.
266    ///
267    /// A session then negotiates the duration it asked for, including
268    /// continuous, and no server-initiated close is ever triggered by duration.
269    /// The session table bound and the idle timeout still apply, so this is not
270    /// an unbounded setting.
271    #[must_use]
272    pub fn without_max_test_duration(mut self) -> Self {
273        self.max_test_duration = None;
274        self
275    }
276
277    /// Sets how many timestamps this server is willing to provide.
278    ///
279    /// A requested placement above the allowance is reduced during negotiation,
280    /// so the open reply is honest about what the session's echo replies will
281    /// carry:
282    ///
283    /// | Requested | [`Dual`] | [`Single`] | [`None`] |
284    /// |-----------|----------|------------|----------|
285    /// | `None` | `None` | `None` | `None` |
286    /// | `Send` | `Send` | `Send` | `None` |
287    /// | `Receive` | `Receive` | `Receive` | `None` |
288    /// | `Both` | `Both` | **`Midpoint`** | `None` |
289    /// | `Midpoint` | `Midpoint` | `Midpoint` | `None` |
290    ///
291    /// The one substitution is the observed one: under a single-timestamp
292    /// allowance a request for both instants is answered with the *midpoint*,
293    /// not with whichever of the two the server felt like keeping.
294    ///
295    /// The requested [`Clock`](irtt_proto::Clock) is left exactly as it was in
296    /// every case. The allowance restricts which *instants* are reported, and
297    /// each reported instant still carries one field per negotiated clock domain
298    /// — [`Single`] on [`Clock::Both`](irtt_proto::Clock::Both) reports a wall
299    /// and a monotonic midpoint. Only [`None`] removes timestamp fields
300    /// outright, and it does so because the echo layout carries none once the
301    /// placement is [`StampAt::None`](irtt_proto::StampAt::None).
302    ///
303    /// Restriction runs *before* the effective-session check, which is what
304    /// makes a [`None`] allowance accept a request that selected timestamps
305    /// without sending a Clock: such a session is refused as requested, because
306    /// timestamps from no clock cannot be produced, but under this policy its
307    /// effective placement is none and the absent clock no longer matters.
308    ///
309    /// [`Dual`]: TimestampAllowance::Dual
310    /// [`Single`]: TimestampAllowance::Single
311    /// [`None`]: TimestampAllowance::None
312    #[must_use]
313    pub fn with_timestamp_allowance(mut self, timestamp_allowance: TimestampAllowance) -> Self {
314        self.timestamp_allowance = timestamp_allowance;
315        self
316    }
317
318    /// Sets whether this server provides the traffic-class marking a session
319    /// requests.
320    ///
321    /// `true`, the default, negotiates the requested DSCP parameter unchanged —
322    /// including a raw value outside `0..=255`, which stays in the negotiated
323    /// parameters and is transported unmarked.
324    ///
325    /// `false` negotiates it to **zero**, whatever was requested, so the reply
326    /// tells the client its replies will be unmarked and the session's stored
327    /// parameters say the same. The open is never refused over DSCP, and nothing
328    /// is clamped or wrapped: this is an operator policy about providing
329    /// traffic-class marking at all, not a judgement about a particular value.
330    ///
331    /// It is also not socket capability detection. What a given host can apply
332    /// to a socket is a transport concern, settled per send by the runtime.
333    #[must_use]
334    pub fn with_dscp_allowed(mut self, dscp_allowed: bool) -> Self {
335        self.dscp_allowed = dscp_allowed;
336        self
337    }
338
339    /// The configured HMAC key, if this server authenticates.
340    #[must_use]
341    pub fn hmac_key(&self) -> Option<&[u8]> {
342        self.hmac_key.as_deref()
343    }
344
345    /// The maximum number of simultaneously live sessions.
346    #[must_use]
347    pub fn max_sessions(&self) -> usize {
348        self.max_sessions
349    }
350
351    /// The maximum echo datagram size a session may negotiate, in bytes.
352    #[must_use]
353    pub fn max_packet_length(&self) -> usize {
354        self.max_packet_length
355    }
356
357    /// The floor on a negotiated send interval, and the allowance refill
358    /// cadence.
359    #[must_use]
360    pub fn min_send_interval(&self) -> Duration {
361        self.min_send_interval
362    }
363
364    /// How many echo requests a session may have answered before its allowance
365    /// has to replenish.
366    #[must_use]
367    pub fn burst_allowance(&self) -> u32 {
368        self.burst_allowance
369    }
370
371    /// How long a session may go without an echo request before it is released.
372    #[must_use]
373    pub fn idle_timeout(&self) -> Duration {
374        self.idle_timeout
375    }
376
377    /// The maximum test duration a session may negotiate, if one is configured.
378    ///
379    /// Never `Some(Duration::ZERO)`: a zero maximum is stored as `None`, since
380    /// a negotiated Duration of zero means continuous rather than instant.
381    #[must_use]
382    pub fn max_test_duration(&self) -> Option<Duration> {
383        self.max_test_duration
384    }
385
386    /// How many timestamps this server is willing to provide.
387    #[must_use]
388    pub fn timestamp_allowance(&self) -> TimestampAllowance {
389        self.timestamp_allowance
390    }
391
392    /// Whether a requested DSCP value is negotiated as asked, rather than to
393    /// zero.
394    #[must_use]
395    pub fn dscp_allowed(&self) -> bool {
396        self.dscp_allowed
397    }
398}
399
400impl Default for ServerConfig {
401    /// An unauthenticated server bounded to [`DEFAULT_MAX_SESSIONS`] sessions,
402    /// each negotiating at most [`DEFAULT_MAX_PACKET_LENGTH`] bytes per echo,
403    /// no faster than [`DEFAULT_MIN_SEND_INTERVAL`] with a burst of
404    /// [`DEFAULT_BURST_ALLOWANCE`], and released after
405    /// [`DEFAULT_IDLE_TIMEOUT`] without an echo request.
406    ///
407    /// No maximum test duration is configured, matching the reference server's
408    /// ordinary policy. A session is still bounded, by the table size and the
409    /// idle timeout.
410    ///
411    /// Both capability restrictions are off: every requested timestamp placement
412    /// is honored ([`TimestampAllowance::Dual`]) and a requested DSCP value is
413    /// negotiated as asked. They are opt-in, so an existing configuration
414    /// negotiates exactly what it did before they existed.
415    fn default() -> Self {
416        Self {
417            hmac_key: None,
418            max_sessions: DEFAULT_MAX_SESSIONS,
419            max_packet_length: DEFAULT_MAX_PACKET_LENGTH,
420            min_send_interval: DEFAULT_MIN_SEND_INTERVAL,
421            burst_allowance: DEFAULT_BURST_ALLOWANCE,
422            idle_timeout: DEFAULT_IDLE_TIMEOUT,
423            max_test_duration: None,
424            timestamp_allowance: TimestampAllowance::Dual,
425            dscp_allowed: true,
426        }
427    }
428}