fix(server): reject unusable timestamp sessions
This commit is contained in:
5 files changed
+169
-6
No files matched your search
@@ -1,4 +1,4 @@
|
||||
use irtt_proto::{verify_packet_hmac, Params};
|
||||
use irtt_proto::{verify_packet_hmac, Clock, Params, StampAt};
|
||||
|
||||
use super::support::{
|
||||
client_params, core_with_tokens, expect_no_test_reply, no_test_request, open_request,
|
||||
@@ -74,6 +74,30 @@ fn a_no_test_open_is_rejected_on_the_same_parameters_a_normal_open_is() {
|
||||
assert_eq!(core.session_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_no_test_open_selecting_timestamps_without_a_clock_is_silently_refused() {
|
||||
// No-test exists to tell a client what the session would be, so it must not
|
||||
// report a session that could not exist. The effective-parameter check runs
|
||||
// before the no-test path branches off, so both open forms refuse the same
|
||||
// incoherent timestamp request.
|
||||
let tokens = ScriptedTokens::new([TOKEN_A]);
|
||||
let mut core = core_with_tokens(ServerConfig::default(), tokens.clone());
|
||||
|
||||
let requested = Params {
|
||||
clock: Clock::Unspecified,
|
||||
stamp_at: StampAt::Both,
|
||||
..client_params()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
core.handle_datagram(peer(), &no_test_request(&requested, None))
|
||||
.expect("an unusable timestamp request 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
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user