fix(server): reject unusable timestamp sessions

This commit is contained in:
Nils Andreas Svee committed 2026-08-12 17:21:23 +07:00
1 parent 68ed5c6e2e
commit ca2c3dcbd8
5 files changed
+169 -6

No files matched your search

+67 -1
View File
@@ -104,6 +104,10 @@ fn an_empty_parameter_payload_is_answered_with_only_a_version() {
..Params::default()
}
);
// An unspecified clock is not itself a defect. Only pairing it with a
// selected `stamp_at` is, so a session requesting no timestamps at all is
// fully valid with no Clock tag.
assert_eq!(reply.params.stamp_at, StampAt::None);
assert_eq!(reply.params.clock, Clock::Unspecified);
assert_eq!(core.session_count(), 1);
}
@@ -136,10 +140,61 @@ fn unknown_parameter_tags_are_ignored_and_not_reflected_in_the_reply() {
}
#[test]
fn this_slice_restricts_nothing_but_the_protocol_version() {
fn selecting_timestamps_without_a_clock_is_silently_refused() {
// Timestamps from no clock at all describe a session the server could not
// run: the negotiated layout carries no timestamp field to fill. The open
// is discarded rather than repaired — synthesizing a clock would answer
// with a session the client never asked for, and dropping `stamp_at` would
// silently discard the measurement it did ask for.
//
// Only an *absent* Clock tag reaches this: an explicit zero is already out
// of range for the decoder, and a conforming client always sends a clock
// when it selects timestamps.
for stamp_at in [
StampAt::Send,
StampAt::Receive,
StampAt::Both,
StampAt::Midpoint,
] {
let requested = Params {
clock: Clock::Unspecified,
stamp_at,
..client_params()
};
let tokens = ScriptedTokens::new([TOKEN_A]);
let mut core = core_with_tokens(ServerConfig::default(), tokens.clone());
let reply = core
.handle_datagram(peer(), &open_request(&requested, None))
.expect("an unusable timestamp request is not a server error");
assert!(
reply.is_none(),
"{stamp_at:?} without a clock must be silent"
);
assert_eq!(
core.session_count(),
0,
"{stamp_at:?} without a clock must create no session"
);
assert_eq!(
tokens.remaining(),
1,
"{stamp_at:?} without a clock must not consume a token"
);
}
}
#[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.
//
// This is about what *negotiation* rewrites, which is a separate question
// from whether the core will acknowledge the result: an open whose
// effective parameters are incoherent is still discarded, as
// `selecting_timestamps_without_a_clock_is_silently_refused` shows.
let unrestricted = Params {
protocol_version: 2,
duration_ns: 3_600_000_000_000,
@@ -175,6 +230,11 @@ 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.
//
// 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.
for (length, dscp) in [(-1, -1), (0, 0), (1472, 184), (-4096, 512)] {
let requested = Params {
protocol_version: 1,
@@ -193,5 +253,11 @@ fn negative_and_out_of_range_length_and_dscp_survive_negotiation() {
assert_eq!(reply.params.length, length);
assert_eq!(reply.params.dscp, dscp);
assert_eq!(core.session_count(), 1);
let session = core.session(TOKEN_A).expect("the session must be live");
assert_eq!(
session.params(),
&reply.params,
"the session must hold the negotiated values verbatim"
);
}
}