From 0e7d363f967b6d528f6b055a09f40a16f791153f Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Wed, 12 Aug 2026 22:25:14 +0700 Subject: [PATCH] feat(server): bound negotiated packet lengths --- crates/irtt-server/AGENTS.md | 35 +++++- crates/irtt-server/src/config.rs | 61 ++++++++- crates/irtt-server/src/core.rs | 45 +++++-- crates/irtt-server/src/lib.rs | 14 ++- crates/irtt-server/src/negotiate.rs | 56 ++++++--- crates/irtt-server/src/tests/no_test.rs | 45 +++++++ crates/irtt-server/src/tests/open.rs | 158 ++++++++++++++++++++++-- 7 files changed, 361 insertions(+), 53 deletions(-) diff --git a/crates/irtt-server/AGENTS.md b/crates/irtt-server/AGENTS.md index e4d0dc0..871b4b8 100644 --- a/crates/irtt-server/AGENTS.md +++ b/crates/irtt-server/AGENTS.md @@ -83,13 +83,42 @@ effective parameters must be safe for the server to run. the open reply and stored verbatim in the session. It needs no clamping to be executable: `irtt-proto` floors actual echo packet sizing at the required protocol field block, so a negative length behaves as zero does. +- A session's echo datagram must fit `ServerConfig::max_packet_length`, whose + default is 65,507 bytes. That is `irtt-rs` resource policy, not an + interoperability requirement: it is the largest IPv4 UDP payload, a + conservative cross-family ceiling that bounds one session's echo buffer to + roughly 64 KiB and that no normal client exceeds. It is **not** an MTU, not an + upstream default, and says nothing about what a path will carry. Upstream's + unlimited default is not a compatibility target. +- A requested positive `Length` above the configured maximum is reduced to it + during negotiation, so the reply is honest about what the server will emit. + Zero and negative values are left exactly as requested. +- Capping the parameter is not sufficient. After negotiation, the mandatory echo + field block must itself fit the configured maximum or the open is silently + refused — statistics and timestamp fields, and authentication's 16 bytes, can + exceed a small maximum on their own. Ask `irtt_proto::echo_packet_len` with + this server's actual HMAC mode rather than growing a second packet-size rule + here; an unrepresentable length from it is a request rejection, not a + `ServerError`. +- Zero is a valid maximum and is not a synonym for unlimited: it refuses every + session, and no-test opens with it, since both are validated against the same + effective session. +- The echo slice must reuse this same configured maximum to admit inbound echo + request datagrams by their received length, before any receive state is + mutated. ## Resource policy Total session and resource state must stay bounded. A single unauthenticated -datagram creates a session and opens are never deduplicated, so an unbounded -table is a liability. Upstream's observed absence of session and per-peer bounds -is explicitly **not** a compatibility target and must not be reproduced. +datagram creates a session and opens are never deduplicated, so neither an +unbounded table nor a remotely chosen packet buffer is defensible. Upstream's +observed absence of session and per-peer bounds, and its unlimited default +packet length, are explicitly **not** compatibility targets and must not be +reproduced. + +Configured bounds are trusted local policy: an operator may deliberately set an +absurd `max_sessions` or `max_packet_length`, and that is their choice. The +defaults are what must stay bounded, and no bound gets an unlimited setting. Keep fallible preparation before irreversible state mutation: a reply is prepared before a session is inserted, so an internal failure cannot leave a diff --git a/crates/irtt-server/src/config.rs b/crates/irtt-server/src/config.rs index 591a78a..50b689e 100644 --- a/crates/irtt-server/src/config.rs +++ b/crates/irtt-server/src/config.rs @@ -8,17 +8,36 @@ /// server is not expected to reproduce. pub const DEFAULT_MAX_SESSIONS: usize = 1024; +/// Default bound on the executable echo datagram size of one session, in bytes. +/// +/// This is `irtt-rs` resource policy, not an interoperability requirement and +/// not a protocol constant. The server specification records upstream defaulting +/// to *unlimited*, which leaves a remotely negotiated allocation unbounded; that +/// is explicitly not a compatibility target. +/// +/// 65,507 is the largest payload an IPv4 UDP datagram can carry, so it is the +/// conservative ceiling above which no negotiated length could be emitted as one +/// datagram on any address family. It bounds a session's echo buffer to roughly +/// 64 KiB, and it is already the maximum a normal `irtt-rs` client will ask for, +/// so ordinary traffic never meets it. +/// +/// It is **not** an MTU and promises nothing about what a path will carry. Path +/// and interface policy is a separate, later concern; this value exists to keep +/// allocation bounded, not to predict fragmentation. +pub const DEFAULT_MAX_PACKET_LENGTH: usize = 65_507; + /// Configuration for a [`ServerCore`](crate::ServerCore). /// /// Fields are private and set through consuming builder methods, so later /// slices can add configuration without breaking construction. Only the -/// settings this OPEN/session slice actually uses exist yet; parameter -/// restriction knobs, lifetime policy and rate limits arrive with the slices -/// that implement them. +/// settings this OPEN/session slice actually uses exist yet; the remaining +/// parameter restriction knobs, lifetime policy and rate limits arrive with the +/// slices that implement them. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ServerConfig { hmac_key: Option>, max_sessions: usize, + max_packet_length: usize, } impl ServerConfig { @@ -45,6 +64,32 @@ impl ServerConfig { self } + /// Sets the maximum echo datagram size a session may negotiate, in bytes. + /// + /// This governs the *executable* echo datagram of a session, not the size of + /// the open exchange: a requested positive Length above this value is + /// reduced to it during negotiation, and an open whose mandatory echo field + /// block would not fit is silently refused, since the server could not emit + /// a compliant reply for it. Authentication counts toward that field block. + /// The echo slice will additionally use this limit to admit inbound echo + /// request datagrams by their received length. + /// + /// The value is trusted local configuration, like + /// [`with_max_sessions`](Self::with_max_sessions): an operator may + /// deliberately raise it far past what any path carries, and that is their + /// resource choice to make. What must stay bounded is the *default*, which + /// is why there is deliberately no unlimited setting. + /// + /// Zero is accepted and is not a synonym for unlimited. No session can + /// describe an echo packet smaller than its mandatory field block, so a zero + /// maximum silently refuses every session-creating open — and every no-test + /// open too, since both are validated against the same effective session. + #[must_use] + pub fn with_max_packet_length(mut self, max_packet_length: usize) -> Self { + self.max_packet_length = max_packet_length; + self + } + /// The configured HMAC key, if this server authenticates. #[must_use] pub fn hmac_key(&self) -> Option<&[u8]> { @@ -56,14 +101,22 @@ impl ServerConfig { pub fn max_sessions(&self) -> usize { self.max_sessions } + + /// The maximum echo datagram size a session may negotiate, in bytes. + #[must_use] + pub fn max_packet_length(&self) -> usize { + self.max_packet_length + } } impl Default for ServerConfig { - /// An unauthenticated server bounded to [`DEFAULT_MAX_SESSIONS`] sessions. + /// An unauthenticated server bounded to [`DEFAULT_MAX_SESSIONS`] sessions, + /// each negotiating at most [`DEFAULT_MAX_PACKET_LENGTH`] bytes per echo. fn default() -> Self { Self { hmac_key: None, max_sessions: DEFAULT_MAX_SESSIONS, + max_packet_length: DEFAULT_MAX_PACKET_LENGTH, } } } diff --git a/crates/irtt-server/src/core.rs b/crates/irtt-server/src/core.rs index 0e28c6b..593b761 100644 --- a/crates/irtt-server/src/core.rs +++ b/crates/irtt-server/src/core.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, net::SocketAddr}; use irtt_proto::{ - decode_request, encode_open_reply, verify_packet_hmac, Clock, DecodedParams, + decode_request, echo_packet_len, encode_open_reply, verify_packet_hmac, Clock, DecodedParams, DecodedRequestKind, OpenReply, Params, StampAt, FLAG_CLOSE, FLAG_OPEN, FLAG_REPLY, }; @@ -141,7 +141,10 @@ impl ServerCore { } // Not implemented in this slice. An echo must not advance receive // state until the slice that implements it; emitting a placeholder - // reply now would be worse than silence. + // reply now would be worse than silence. That slice must admit an + // echo by its received datagram length against + // `max_packet_length` before it touches any receive state; adding + // that branch now would only be dead code guarding nothing. DecodedRequestKind::Echo { .. } => Ok(None), } } @@ -164,7 +167,7 @@ impl ServerCore { let params = negotiate_params(decoded.params, &self.config); // Deliberately after negotiation, and before either open path: what has // to be executable is the effective session, not the request. - if !negotiated_params_are_admissible(¶ms) { + if !negotiated_params_are_admissible(¶ms, &self.config) { return Ok(None); } @@ -324,10 +327,12 @@ fn open_params_are_admissible(decoded: &DecodedParams) -> bool { /// exists to tell a client what the session would be, so it must not report a /// session that could not exist. /// -/// One rule so far: **selecting timestamps requires a specified clock.** A -/// non-none `stamp_at` with [`Clock::Unspecified`] asks for timestamp fields -/// from no clock at all, which lays out no timestamp field and leaves the -/// session with a request the server cannot honor. +/// Two rules so far. +/// +/// **Selecting timestamps requires a specified clock.** A non-none `stamp_at` +/// with [`Clock::Unspecified`] asks for timestamp fields from no clock at all, +/// which lays out no timestamp field and leaves the session with a request the +/// server cannot honor. /// /// [`Clock::Unspecified`] means the Clock tag was absent — an explicit zero is /// already out of range for the decoder — so this cannot be reached by a @@ -337,10 +342,32 @@ fn open_params_are_admissible(decoded: &DecodedParams) -> bool { /// `stamp_at` to none would silently drop the measurement it did ask for. /// Neither is the server's to decide. /// +/// **The session's echo datagram must fit +/// [`max_packet_length`](ServerConfig::max_packet_length).** Negotiation already +/// reduced an oversized Length, but that only bounds what the *parameter* asks +/// for: the mandatory field block grows with the received statistics, the +/// timestamps and — by 16 bytes — authentication, and can exceed the configured +/// maximum on its own. A session whose smallest compliant reply is already too +/// large is one the server could never answer an echo for, so it is refused at +/// open rather than acknowledged and then found unserviceable. +/// +/// The size comes from [`echo_packet_len`], the one packet-size authority, so +/// this cannot drift from what the echo slice will actually encode. Its +/// unrepresentable-length error is a rejection here, not a [`ServerError`]: a +/// pathological requested length is remote input, not an internal failure. The +/// default policy makes it unreachable for an acknowledged session anyway, since +/// negotiation has already reduced the length to a representable maximum. +/// /// The check is deliberately *after* negotiation so that a later policy slice /// restricting timestamps can restrict `stamp_at` to [`StampAt::None`], at /// which point an absent clock is safe and this predicate holds without /// changing. -fn negotiated_params_are_admissible(params: &Params) -> bool { - params.stamp_at == StampAt::None || params.clock != Clock::Unspecified +/// +/// [`ServerError`]: crate::ServerError +fn negotiated_params_are_admissible(params: &Params, config: &ServerConfig) -> bool { + if params.stamp_at != StampAt::None && params.clock == Clock::Unspecified { + return false; + } + echo_packet_len(config.hmac_key().is_some(), params) + .is_ok_and(|len| len <= config.max_packet_length()) } diff --git a/crates/irtt-server/src/lib.rs b/crates/irtt-server/src/lib.rs index c5b2e00..bb551c6 100644 --- a/crates/irtt-server/src/lib.rs +++ b/crates/irtt-server/src/lib.rs @@ -23,11 +23,13 @@ //! //! # Resource policy //! -//! Total session state is bounded (see [`DEFAULT_MAX_SESSIONS`]). A single -//! unauthenticated datagram creates a session and opens are never deduplicated, -//! so an unbounded table is a liability rather than a compatibility feature. -//! Upstream's observed lack of session bounds is explicitly not a compatibility -//! target. +//! Total session state is bounded (see [`DEFAULT_MAX_SESSIONS`]), and so is the +//! echo datagram a single session can negotiate (see +//! [`DEFAULT_MAX_PACKET_LENGTH`]). A single unauthenticated datagram creates a +//! session and opens are never deduplicated, so neither an unbounded table nor a +//! remotely chosen buffer size is a compatibility feature. Upstream's observed +//! lack of session bounds, and its unlimited default packet length, are +//! explicitly not compatibility targets. //! //! # Current scope //! @@ -47,6 +49,6 @@ mod token; #[cfg(test)] mod tests; -pub use config::{ServerConfig, DEFAULT_MAX_SESSIONS}; +pub use config::{ServerConfig, DEFAULT_MAX_PACKET_LENGTH, DEFAULT_MAX_SESSIONS}; pub use core::ServerCore; pub use error::ServerError; diff --git a/crates/irtt-server/src/negotiate.rs b/crates/irtt-server/src/negotiate.rs index 486dfe6..21283b5 100644 --- a/crates/irtt-server/src/negotiate.rs +++ b/crates/irtt-server/src/negotiate.rs @@ -5,26 +5,46 @@ use crate::config::ServerConfig; /// Produces the parameters the server returns in an open reply and will enforce /// for the session. /// -/// This slice applies the minimum the specification requires: the requested -/// known parameters, with the protocol version rewritten to -/// [`PROTOCOL_VERSION`]. A server must return version 1 so that version-1 -/// clients accept the session; any requested value, including an absent, -/// zero, negative or future one, is accepted and answered with 1. Detecting a -/// version mismatch is therefore client-side only. +/// Two restrictions apply so far. /// -/// Nothing else is restricted yet. A server *may* reduce or replace requested -/// parameters, and later policy slices will add the maximum test duration -/// clamp, minimum interval, idle-timeout interval cap, maximum packet length, -/// timestamp allowance, DSCP policy and server-fill allow-list. This function -/// is where they belong, which is why the configuration is threaded through -/// even though no knob reads it yet. +/// **Protocol version is rewritten to [`PROTOCOL_VERSION`].** A server must +/// return version 1 so that version-1 clients accept the session; any requested +/// value, including an absent, zero, negative or future one, is accepted and +/// answered with 1. Detecting a version mismatch is therefore client-side only. /// -/// Negotiation rewriting nothing but the version does not mean every open is -/// answered: the core validates what this function returns and silently -/// discards an open whose effective parameters it could not execute. That is a -/// separate step on purpose — this function decides what the session *would* -/// be, and the core decides whether that session can exist. -pub(crate) fn negotiate_params(mut requested: Params, _config: &ServerConfig) -> Params { +/// **A positive Length above the configured maximum is reduced to it.** That +/// bounds the echo datagram — and so the buffer — a remote peer can make this +/// server negotiate, which is why it happens here rather than at send time: the +/// value returned in the open reply has to be the one the server will actually +/// honor. A negative Length is left exactly as requested and a zero stays zero; +/// neither asks for space beyond the mandatory field block, which `irtt-proto` +/// floors echo sizing at, so there is nothing to reduce and rewriting them would +/// answer with a session the client did not ask for. +/// +/// Those two are the whole of the current policy. A server *may* reduce or +/// replace requested parameters, and later policy slices will add the maximum +/// test duration clamp, +/// minimum interval, idle-timeout interval cap, timestamp allowance, DSCP policy +/// and server-fill allow-list. +/// +/// Negotiation is not by itself an acknowledgement: the core validates what this +/// function returns and silently discards an open whose effective parameters it +/// could not execute. That is a separate step on purpose — this function decides +/// what the session *would* be, and the core decides whether that session can +/// exist. Capping Length here is not enough on its own, because the mandatory +/// echo field block can exceed the configured maximum by itself. +pub(crate) fn negotiate_params(mut requested: Params, config: &ServerConfig) -> Params { requested.protocol_version = PROTOCOL_VERSION; + + // A configured maximum wider than `i64` cannot restrict anything: the wire + // field cannot carry more than `i64::MAX` in the first place. Saturating + // there is therefore exact, not an approximation — unlike converting the + // requested length the other way, which would turn a value this platform + // cannot represent into a plausible buffer size. + let max_length = i64::try_from(config.max_packet_length()).unwrap_or(i64::MAX); + if requested.length > max_length { + requested.length = max_length; + } + requested } diff --git a/crates/irtt-server/src/tests/no_test.rs b/crates/irtt-server/src/tests/no_test.rs index ea89ca2..06bd90d 100644 --- a/crates/irtt-server/src/tests/no_test.rs +++ b/crates/irtt-server/src/tests/no_test.rs @@ -98,6 +98,51 @@ fn a_no_test_open_selecting_timestamps_without_a_clock_is_silently_refused() { assert_eq!(tokens.remaining(), 1, "no token may be drawn"); } +#[test] +fn a_no_test_open_reports_the_reduced_packet_length() { + // No-test answers what the session *would* be, so it has to report the same + // reduced length a session-creating open would enforce — while still + // creating nothing and drawing no token. + let tokens = ScriptedTokens::new([TOKEN_A]); + let mut core = core_with_tokens( + ServerConfig::default().with_max_packet_length(64), + tokens.clone(), + ); + let requested = Params { + length: 1000, + ..client_params() + }; + + let packet = core + .handle_datagram(peer(), &no_test_request(&requested, None)) + .unwrap() + .expect("a reducible length must still be answered"); + + assert_eq!(expect_no_test_reply(&packet, None).params.length, 64); + assert_eq!(core.session_count(), 0); + assert_eq!(tokens.remaining(), 1, "no-test must not draw a token"); +} + +#[test] +fn a_no_test_open_is_refused_when_its_echo_field_block_exceeds_the_maximum() { + // A maximum below the mandatory echo block describes a session that could + // not exist at all, so no-test must not report one. Both open forms run the + // same effective-parameter check before no-test branches off. + let tokens = ScriptedTokens::new([TOKEN_A]); + let mut core = core_with_tokens( + ServerConfig::default().with_max_packet_length(0), + tokens.clone(), + ); + + assert_eq!( + core.handle_datagram(peer(), &no_test_request(&client_params(), None)) + .expect("an unservable packet size is not a server error"), + None + ); + assert_eq!(core.session_count(), 0); + assert_eq!(tokens.remaining(), 1, "no token may be drawn"); +} + #[test] fn a_no_test_open_is_still_answered_when_no_session_could_be_created() { // No-test consumes no capacity, so it must keep working exactly where a diff --git a/crates/irtt-server/src/tests/open.rs b/crates/irtt-server/src/tests/open.rs index 9a13509..6bcd4eb 100644 --- a/crates/irtt-server/src/tests/open.rs +++ b/crates/irtt-server/src/tests/open.rs @@ -2,9 +2,9 @@ use irtt_proto::{Clock, Params, ReceivedStats, ServerFill, StampAt}; use super::support::{ client_params, core_with_tokens, expect_normal_open_reply, open_request, - open_request_with_raw_params, param_int, peer, ScriptedTokens, + open_request_with_raw_params, param_int, peer, ScriptedTokens, KEY, }; -use crate::ServerConfig; +use crate::{ServerConfig, DEFAULT_MAX_PACKET_LENGTH}; const TOKEN_A: u64 = 0x7896_b6ab_8771_5213; @@ -186,10 +186,13 @@ fn selecting_timestamps_without_a_clock_is_silently_refused() { } #[test] -fn negotiation_restricts_nothing_but_the_protocol_version() { - // These are the values a later restriction-policy slice will deliberately - // start changing. Pinning them now makes that a visible decision rather - // than an accident. +fn current_non_length_parameters_survive_negotiation_unchanged() { + // Duration, interval, statistics, timestamps, DSCP and server fill are the + // values a later restriction-policy slice will deliberately start changing. + // Pinning them now makes that a visible decision rather than an accident. + // Version and an oversized packet length are already restricted, and have + // their own tests; the length here is deliberately under the default + // maximum so that this test says nothing about the packet-length policy. // // This is about what *negotiation* rewrites, which is a separate question // from whether the core will acknowledge the result: an open whose @@ -199,7 +202,7 @@ fn negotiation_restricts_nothing_but_the_protocol_version() { protocol_version: 2, duration_ns: 3_600_000_000_000, interval_ns: 1, - length: 65_535, + length: 1472, received_stats: ReceivedStats::Window, stamp_at: StampAt::Midpoint, clock: Clock::Monotonic, @@ -225,16 +228,145 @@ fn negotiation_restricts_nothing_but_the_protocol_version() { ); } +#[test] +fn a_positive_length_above_the_configured_maximum_is_reduced_to_it() { + // The reduced value is what the client is told and what the session holds, + // because it is what the server will actually emit. Both have to agree: an + // upstream client treats a reply shorter than the negotiated length as + // fatal, so an honest reduction at open is the only serviceable answer. + let tokens = ScriptedTokens::new([TOKEN_A]); + let mut core = core_with_tokens( + ServerConfig::default().with_max_packet_length(64), + tokens.clone(), + ); + let requested = Params { + length: 1000, + ..client_params() + }; + + let packet = core + .handle_datagram(peer(), &open_request(&requested, None)) + .unwrap() + .expect("a reducible length must still be answered"); + + let reply = expect_normal_open_reply(&packet, None); + assert_eq!(reply.params.length, 64); + assert_eq!(core.session_count(), 1); + assert_eq!(tokens.remaining(), 0, "one token was consumed"); + let session = core.session(TOKEN_A).expect("the session must be live"); + assert_eq!( + session.params().length, + 64, + "the session must enforce the reduced length" + ); +} + +#[test] +fn the_default_maximum_reduces_an_absurd_requested_length() { + // The largest length the wire can carry is representable as a `usize` on a + // 64-bit target, so packet sizing would happily have turned it into an + // allocation; the configured maximum is what stops it. Restriction happens + // during negotiation, before any sizing, so this holds on a 32-bit target + // too — the request never reaches a conversion that could fail. + let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A])); + let requested = Params { + length: i64::MAX, + ..client_params() + }; + + let packet = core + .handle_datagram(peer(), &open_request(&requested, None)) + .unwrap() + .expect("an absurd length is reduced, not refused"); + + let reply = expect_normal_open_reply(&packet, None); + assert_eq!(reply.params.length, DEFAULT_MAX_PACKET_LENGTH as i64); + let session = core.session(TOKEN_A).expect("the session must be live"); + assert_eq!(session.params().length, DEFAULT_MAX_PACKET_LENGTH as i64); +} + +#[test] +fn an_open_whose_echo_field_block_exceeds_the_maximum_is_silently_refused() { + // Length itself satisfies the policy — zero asks for nothing beyond the + // mandatory fields — but the received-statistics fields alone make the + // smallest compliant echo reply larger than 20 bytes. Capping the Length + // parameter is therefore not enough on its own: this session could never be + // answered, so it is never acknowledged. + let requested = Params { + protocol_version: 1, + length: 0, + received_stats: ReceivedStats::Both, + stamp_at: StampAt::None, + clock: Clock::Unspecified, + ..Params::default() + }; + let tokens = ScriptedTokens::new([TOKEN_A]); + let mut core = core_with_tokens( + ServerConfig::default().with_max_packet_length(20), + tokens.clone(), + ); + + assert_eq!( + core.handle_datagram(peer(), &open_request(&requested, None)) + .expect("an unservable packet size is not a server error"), + None + ); + assert_eq!(core.session_count(), 0); + assert_eq!(tokens.remaining(), 1, "no token may be drawn"); +} + +#[test] +fn authentication_counts_toward_the_executable_echo_size() { + // An authentication field adds 16 bytes to the mandatory echo block, so a + // maximum between the two floors admits the same minimal session on an + // unauthenticated server and refuses it on an authenticated one. The sizes + // themselves come from `irtt_proto`; what this pins is that the server asks + // about its own authentication mode rather than assuming the smaller floor. + let minimal = Params { + protocol_version: 1, + ..Params::default() + }; + + let tokens = ScriptedTokens::new([TOKEN_A]); + let mut authenticated = core_with_tokens( + ServerConfig::default() + .with_hmac_key(KEY) + .with_max_packet_length(31), + tokens.clone(), + ); + assert_eq!( + authenticated + .handle_datagram(peer(), &open_request(&minimal, Some(KEY))) + .expect("an unservable packet size is not a server error"), + None + ); + assert_eq!(authenticated.session_count(), 0); + assert_eq!(tokens.remaining(), 1, "no token may be drawn"); + + let mut unauthenticated = core_with_tokens( + ServerConfig::default().with_max_packet_length(31), + ScriptedTokens::new([TOKEN_A]), + ); + let packet = unauthenticated + .handle_datagram(peer(), &open_request(&minimal, None)) + .unwrap() + .expect("the same session fits without an authentication field"); + expect_normal_open_reply(&packet, None); + assert_eq!(unauthenticated.session_count(), 1); +} + #[test] fn negative_and_out_of_range_length_and_dscp_survive_negotiation() { - // Length and DSCP are accepted as decoded in this slice, including values a - // socket could never carry. Restricting a DSCP the server cannot apply is a - // later policy decision, not an admission rule. + // These lengths are all at or below the default maximum, and DSCP is + // accepted as decoded in this slice, including values a socket could never + // carry. Restricting a DSCP the server cannot apply is a later policy + // decision, not an admission rule. // // A negative length in particular stays negative in both the reply and the - // stored session. It does not need clamping here to be executable: echo - // packet sizing in `irtt-proto` floors it at the required field block, so - // the session this creates can be run as negotiated. + // stored session; the packet-length policy reduces oversized *positive* + // lengths and touches nothing else. It does not need clamping here to be + // executable: echo packet sizing in `irtt-proto` floors it at the required + // field block, so the session this creates can be run as negotiated. for (length, dscp) in [(-1, -1), (0, 0), (1472, 184), (-4096, 512)] { let requested = Params { protocol_version: 1, -- 2.54.0