feat(server): bound negotiated packet lengths

This commit is contained in:
Nils Andreas Svee committed 2026-08-12 22:25:14 +07:00
1 parent 964e7d6339
commit 0e7d363f96
7 files changed
+361 -53

No files matched your search

+32 -3
View File
@@ -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 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 executable: `irtt-proto` floors actual echo packet sizing at the required
protocol field block, so a negative length behaves as zero does. 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 ## Resource policy
Total session and resource state must stay bounded. A single unauthenticated Total session and resource state must stay bounded. A single unauthenticated
datagram creates a session and opens are never deduplicated, so an unbounded datagram creates a session and opens are never deduplicated, so neither an
table is a liability. Upstream's observed absence of session and per-peer bounds unbounded table nor a remotely chosen packet buffer is defensible. Upstream's
is explicitly **not** a compatibility target and must not be reproduced. 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 Keep fallible preparation before irreversible state mutation: a reply is
prepared before a session is inserted, so an internal failure cannot leave a prepared before a session is inserted, so an internal failure cannot leave a
+57 -4
View File
@@ -8,17 +8,36 @@
/// server is not expected to reproduce. /// server is not expected to reproduce.
pub const DEFAULT_MAX_SESSIONS: usize = 1024; 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). /// Configuration for a [`ServerCore`](crate::ServerCore).
/// ///
/// Fields are private and set through consuming builder methods, so later /// Fields are private and set through consuming builder methods, so later
/// slices can add configuration without breaking construction. Only the /// slices can add configuration without breaking construction. Only the
/// settings this OPEN/session slice actually uses exist yet; parameter /// settings this OPEN/session slice actually uses exist yet; the remaining
/// restriction knobs, lifetime policy and rate limits arrive with the slices /// parameter restriction knobs, lifetime policy and rate limits arrive with the
/// that implement them. /// slices that implement them.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServerConfig { pub struct ServerConfig {
hmac_key: Option<Vec<u8>>, hmac_key: Option<Vec<u8>>,
max_sessions: usize, max_sessions: usize,
max_packet_length: usize,
} }
impl ServerConfig { impl ServerConfig {
@@ -45,6 +64,32 @@ impl ServerConfig {
self 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. /// The configured HMAC key, if this server authenticates.
#[must_use] #[must_use]
pub fn hmac_key(&self) -> Option<&[u8]> { pub fn hmac_key(&self) -> Option<&[u8]> {
@@ -56,14 +101,22 @@ impl ServerConfig {
pub fn max_sessions(&self) -> usize { pub fn max_sessions(&self) -> usize {
self.max_sessions 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 { 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 { fn default() -> Self {
Self { Self {
hmac_key: None, hmac_key: None,
max_sessions: DEFAULT_MAX_SESSIONS, max_sessions: DEFAULT_MAX_SESSIONS,
max_packet_length: DEFAULT_MAX_PACKET_LENGTH,
} }
} }
} }
+36 -9
View File
@@ -1,7 +1,7 @@
use std::{collections::HashMap, net::SocketAddr}; use std::{collections::HashMap, net::SocketAddr};
use irtt_proto::{ 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, 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 // Not implemented in this slice. An echo must not advance receive
// state until the slice that implements it; emitting a placeholder // 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), DecodedRequestKind::Echo { .. } => Ok(None),
} }
} }
@@ -164,7 +167,7 @@ impl ServerCore {
let params = negotiate_params(decoded.params, &self.config); let params = negotiate_params(decoded.params, &self.config);
// Deliberately after negotiation, and before either open path: what has // Deliberately after negotiation, and before either open path: what has
// to be executable is the effective session, not the request. // to be executable is the effective session, not the request.
if !negotiated_params_are_admissible(&params) { if !negotiated_params_are_admissible(&params, &self.config) {
return Ok(None); 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 /// exists to tell a client what the session would be, so it must not report a
/// session that could not exist. /// session that could not exist.
/// ///
/// One rule so far: **selecting timestamps requires a specified clock.** A /// Two rules so far.
/// 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 /// **Selecting timestamps requires a specified clock.** A non-none `stamp_at`
/// session with a request the server cannot honor. /// 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 /// [`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 /// 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. /// `stamp_at` to none would silently drop the measurement it did ask for.
/// Neither is the server's to decide. /// 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 /// The check is deliberately *after* negotiation so that a later policy slice
/// restricting timestamps can restrict `stamp_at` to [`StampAt::None`], at /// restricting timestamps can restrict `stamp_at` to [`StampAt::None`], at
/// which point an absent clock is safe and this predicate holds without /// which point an absent clock is safe and this predicate holds without
/// changing. /// 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())
} }
+8 -6
View File
@@ -23,11 +23,13 @@
//! //!
//! # Resource policy //! # Resource policy
//! //!
//! Total session state is bounded (see [`DEFAULT_MAX_SESSIONS`]). A single //! Total session state is bounded (see [`DEFAULT_MAX_SESSIONS`]), and so is the
//! unauthenticated datagram creates a session and opens are never deduplicated, //! echo datagram a single session can negotiate (see
//! so an unbounded table is a liability rather than a compatibility feature. //! [`DEFAULT_MAX_PACKET_LENGTH`]). A single unauthenticated datagram creates a
//! Upstream's observed lack of session bounds is explicitly not a compatibility //! session and opens are never deduplicated, so neither an unbounded table nor a
//! target. //! 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 //! # Current scope
//! //!
@@ -47,6 +49,6 @@ mod token;
#[cfg(test)] #[cfg(test)]
mod tests; 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 core::ServerCore;
pub use error::ServerError; pub use error::ServerError;
+38 -18
View File
@@ -5,26 +5,46 @@ use crate::config::ServerConfig;
/// Produces the parameters the server returns in an open reply and will enforce /// Produces the parameters the server returns in an open reply and will enforce
/// for the session. /// for the session.
/// ///
/// This slice applies the minimum the specification requires: the requested /// Two restrictions apply so far.
/// 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.
/// ///
/// Nothing else is restricted yet. A server *may* reduce or replace requested /// **Protocol version is rewritten to [`PROTOCOL_VERSION`].** A server must
/// parameters, and later policy slices will add the maximum test duration /// return version 1 so that version-1 clients accept the session; any requested
/// clamp, minimum interval, idle-timeout interval cap, maximum packet length, /// value, including an absent, zero, negative or future one, is accepted and
/// timestamp allowance, DSCP policy and server-fill allow-list. This function /// answered with 1. Detecting a version mismatch is therefore client-side only.
/// is where they belong, which is why the configuration is threaded through
/// even though no knob reads it yet.
/// ///
/// Negotiation rewriting nothing but the version does not mean every open is /// **A positive Length above the configured maximum is reduced to it.** That
/// answered: the core validates what this function returns and silently /// bounds the echo datagram — and so the buffer — a remote peer can make this
/// discards an open whose effective parameters it could not execute. That is a /// server negotiate, which is why it happens here rather than at send time: the
/// separate step on purpose — this function decides what the session *would* /// value returned in the open reply has to be the one the server will actually
/// be, and the core decides whether that session can exist. /// honor. A negative Length is left exactly as requested and a zero stays zero;
pub(crate) fn negotiate_params(mut requested: Params, _config: &ServerConfig) -> Params { /// 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; 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 requested
} }
+45
View File
@@ -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"); 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] #[test]
fn a_no_test_open_is_still_answered_when_no_session_could_be_created() { 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 // No-test consumes no capacity, so it must keep working exactly where a
+145 -13
View File
@@ -2,9 +2,9 @@ use irtt_proto::{Clock, Params, ReceivedStats, ServerFill, StampAt};
use super::support::{ use super::support::{
client_params, core_with_tokens, expect_normal_open_reply, open_request, 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; const TOKEN_A: u64 = 0x7896_b6ab_8771_5213;
@@ -186,10 +186,13 @@ fn selecting_timestamps_without_a_clock_is_silently_refused() {
} }
#[test] #[test]
fn negotiation_restricts_nothing_but_the_protocol_version() { fn current_non_length_parameters_survive_negotiation_unchanged() {
// These are the values a later restriction-policy slice will deliberately // Duration, interval, statistics, timestamps, DSCP and server fill are the
// start changing. Pinning them now makes that a visible decision rather // values a later restriction-policy slice will deliberately start changing.
// than an accident. // 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 // This is about what *negotiation* rewrites, which is a separate question
// from whether the core will acknowledge the result: an open whose // 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, protocol_version: 2,
duration_ns: 3_600_000_000_000, duration_ns: 3_600_000_000_000,
interval_ns: 1, interval_ns: 1,
length: 65_535, length: 1472,
received_stats: ReceivedStats::Window, received_stats: ReceivedStats::Window,
stamp_at: StampAt::Midpoint, stamp_at: StampAt::Midpoint,
clock: Clock::Monotonic, 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] #[test]
fn negative_and_out_of_range_length_and_dscp_survive_negotiation() { fn negative_and_out_of_range_length_and_dscp_survive_negotiation() {
// Length and DSCP are accepted as decoded in this slice, including values a // These lengths are all at or below the default maximum, and DSCP is
// socket could never carry. Restricting a DSCP the server cannot apply is a // accepted as decoded in this slice, including values a socket could never
// later policy decision, not an admission rule. // 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 // 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 // stored session; the packet-length policy reduces oversized *positive*
// packet sizing in `irtt-proto` floors it at the required field block, so // lengths and touches nothing else. It does not need clamping here to be
// the session this creates can be run as negotiated. // 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)] { for (length, dscp) in [(-1, -1), (0, 0), (1472, 184), (-4096, 512)] {
let requested = Params { let requested = Params {
protocol_version: 1, protocol_version: 1,