819 lines
29 KiB
Rust
819 lines
29 KiB
Rust
//! Normal echo processing.
|
|
//!
|
|
//! An echo request that authenticates, fits the configured maximum, names a
|
|
//! live session and arrives from that session's endpoint is answered from the
|
|
//! session's negotiated parameters. Everything else is silence that leaves the
|
|
//! receive state alone, which these tests prove from the wire — the count and
|
|
//! window in the *next* accepted reply are what show that a rejected datagram
|
|
//! changed nothing.
|
|
//!
|
|
//! The count and window vectors come from the clean
|
|
//! `test-vectors/SERVER_BEHAVIORAL_VECTORS.md` Section 1 and are hard-coded
|
|
//! here rather than recomputed, so that replacing the observed semantics with a
|
|
//! conventional selective-acknowledgement bitmap fails these tests.
|
|
|
|
use std::net::{SocketAddr, SocketAddrV6};
|
|
|
|
use irtt_proto::{
|
|
echo_packet_len, verify_packet_hmac, Clock, Params, ReceivedStats, StampAt, TimestampFields,
|
|
};
|
|
|
|
use super::support::{
|
|
core_for, core_with_sources, core_with_tokens, echo_params, echo_request, expect_echo_reply,
|
|
open_negotiated, other_peer, peer, sample, unthrottled, ScriptedClock, ScriptedTokens, KEY,
|
|
OTHER_KEY,
|
|
};
|
|
use crate::{clock::ClockSample, ServerConfig, TimestampAllowance};
|
|
|
|
const TOKEN_A: u64 = 0x0102_0304_0506_0708;
|
|
const UNKNOWN_TOKEN: u64 = 0x2122_2324_2526_2728;
|
|
|
|
/// The lifecycle instant an open consumes before any echo is sent.
|
|
///
|
|
/// The core reads the clock once per authenticated request, to judge idle expiry
|
|
/// and to start the new session's deadlines. It is earlier than every echo
|
|
/// sample below, as a real monotonic reading would be, and no reply ever carries
|
|
/// it: an open reply has no timestamp fields.
|
|
fn open_sample() -> ClockSample {
|
|
sample(900, 9_000)
|
|
}
|
|
|
|
/// Params selecting no optional field at all, so the echo layout is the
|
|
/// mandatory field block and nothing else.
|
|
fn bare_params() -> Params {
|
|
echo_params(
|
|
ReceivedStats::None,
|
|
StampAt::None,
|
|
Clock::Unspecified,
|
|
/* length */ 0,
|
|
)
|
|
}
|
|
|
|
/// Params reporting both statistics, which is what most vectors are stated in.
|
|
fn counting_params(length: i64) -> Params {
|
|
echo_params(
|
|
ReceivedStats::Both,
|
|
StampAt::None,
|
|
Clock::Unspecified,
|
|
length,
|
|
)
|
|
}
|
|
|
|
/// Opens one session and returns the core, the token and the negotiated params.
|
|
///
|
|
/// Rate limiting is disabled: these tests drive a whole vector of echoes at once
|
|
/// against the production clock, and what they are about is the reply, not the
|
|
/// allowance. `rate` covers the limiter.
|
|
fn session_with(params: &Params) -> (crate::ServerCore, u64, Params) {
|
|
let mut core = core_with_tokens(unthrottled(), ScriptedTokens::new([TOKEN_A]));
|
|
let (token, negotiated) = open_negotiated(&mut core, peer(), params, None);
|
|
(core, token, negotiated)
|
|
}
|
|
|
|
#[test]
|
|
fn a_valid_echo_is_answered_from_the_negotiated_params() {
|
|
let (mut core, token, negotiated) = session_with(&bare_params());
|
|
|
|
let packet = core
|
|
.handle_datagram(peer(), &echo_request(token, 42, &negotiated, &[], None))
|
|
.unwrap()
|
|
.expect("an admissible echo must be answered");
|
|
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
assert_eq!(reply.token, token, "the session token is copied through");
|
|
assert_eq!(
|
|
reply.sequence, 42,
|
|
"the sequence number is copied unchanged"
|
|
);
|
|
assert_eq!(reply.recv_count, None, "no statistics were negotiated");
|
|
assert_eq!(reply.recv_window, None);
|
|
assert_eq!(
|
|
reply.timestamps,
|
|
TimestampFields::default(),
|
|
"no timestamps were negotiated, so none are emitted"
|
|
);
|
|
assert!(reply.payload.is_empty());
|
|
assert_eq!(
|
|
packet.bytes().len(),
|
|
echo_packet_len(false, &negotiated).unwrap()
|
|
);
|
|
assert_eq!(
|
|
packet.bytes().len(),
|
|
16,
|
|
"magic, flags, token and sequence only"
|
|
);
|
|
|
|
assert_eq!(
|
|
core.session_count(),
|
|
1,
|
|
"an echo neither creates nor releases a session"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_reply_is_sized_by_the_session_not_by_the_request() {
|
|
// A request may be shorter or longer than the negotiated length, and its
|
|
// bytes past the sequence number are opaque either way.
|
|
let (mut core, token, negotiated) = session_with(&counting_params(64));
|
|
assert_eq!(negotiated.length, 64);
|
|
|
|
let short = Params {
|
|
length: 0,
|
|
..negotiated.clone()
|
|
};
|
|
// Requesting no statistics moves this request's payload region up to where
|
|
// the *session's* reply keeps its count and window, so the opaque bytes
|
|
// land squarely on fields a server must not read from a request.
|
|
let long = Params {
|
|
length: 100,
|
|
received_stats: ReceivedStats::None,
|
|
..negotiated.clone()
|
|
};
|
|
|
|
for (sequence, name, sizing, payload, expected) in [
|
|
(
|
|
0,
|
|
"a request shorter than the negotiated length",
|
|
&short,
|
|
&[][..],
|
|
(1, 0x1),
|
|
),
|
|
(
|
|
1,
|
|
"a request longer than the negotiated length, with opaque bytes",
|
|
&long,
|
|
&[0xa5; 40][..],
|
|
(2, 0x3),
|
|
),
|
|
] {
|
|
let request = echo_request(token, sequence, sizing, payload, None);
|
|
assert_ne!(request.len(), 64, "{name} must not be the reply's length");
|
|
|
|
let packet = core
|
|
.handle_datagram(peer(), &request)
|
|
.unwrap()
|
|
.unwrap_or_else(|| panic!("{name} must be answered"));
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
|
|
assert_eq!(
|
|
packet.bytes().len(),
|
|
64,
|
|
"{name}: the reply is the negotiated length"
|
|
);
|
|
assert_eq!(reply.sequence, sequence);
|
|
assert_eq!(
|
|
(reply.recv_count, reply.recv_window),
|
|
(Some(expected.0), Some(expected.1)),
|
|
"{name}: the opaque tail must not influence the reply"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_echo_over_the_configured_maximum_is_dropped_before_anything_advances() {
|
|
let config = ServerConfig::default().with_max_packet_length(64);
|
|
let mut core = core_with_tokens(config, ScriptedTokens::new([TOKEN_A]));
|
|
let (token, negotiated) = open_negotiated(&mut core, peer(), &counting_params(64), None);
|
|
assert_eq!(negotiated.length, 64);
|
|
|
|
let at_maximum = echo_request(token, 0, &negotiated, &[], None);
|
|
assert_eq!(at_maximum.len(), 64);
|
|
let over_maximum = echo_request(
|
|
token,
|
|
1,
|
|
&Params {
|
|
length: 65,
|
|
..negotiated.clone()
|
|
},
|
|
&[],
|
|
None,
|
|
);
|
|
assert_eq!(over_maximum.len(), 65);
|
|
|
|
// The comparison is strict, so exactly the maximum is served.
|
|
let packet = core
|
|
.handle_datagram(peer(), &at_maximum)
|
|
.unwrap()
|
|
.expect("a request of exactly the maximum must be answered");
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
assert_eq!((reply.recv_count, reply.recv_window), (Some(1), Some(0x1)));
|
|
|
|
assert_eq!(
|
|
core.handle_datagram(peer(), &over_maximum).unwrap(),
|
|
None,
|
|
"one byte over the maximum must be answered with silence"
|
|
);
|
|
|
|
// The next admissible request is what proves the drop advanced nothing: had
|
|
// the oversized one counted, this would report 3 rather than 2.
|
|
let packet = core
|
|
.handle_datagram(peer(), &echo_request(token, 1, &negotiated, &[], None))
|
|
.unwrap()
|
|
.expect("an admissible echo after a dropped one must be answered");
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
assert_eq!((reply.recv_count, reply.recv_window), (Some(2), Some(0x3)));
|
|
}
|
|
|
|
#[test]
|
|
fn an_echo_that_owns_no_session_here_is_answered_with_silence() {
|
|
let (mut core, token, negotiated) = session_with(&counting_params(0));
|
|
|
|
for (name, endpoint, request_token) in [
|
|
("an unknown token", peer(), UNKNOWN_TOKEN),
|
|
("a zero token", peer(), 0),
|
|
(
|
|
"a valid token from another source port",
|
|
other_peer(),
|
|
token,
|
|
),
|
|
] {
|
|
let request = echo_request(request_token, 0, &negotiated, &[], None);
|
|
assert_eq!(
|
|
core.handle_datagram(endpoint, &request).unwrap(),
|
|
None,
|
|
"{name} must be answered with silence"
|
|
);
|
|
}
|
|
|
|
// Count 1 and window 0x1 are the first-request values, so none of the three
|
|
// rejected datagrams touched this session's receive state.
|
|
let packet = core
|
|
.handle_datagram(peer(), &echo_request(token, 0, &negotiated, &[], None))
|
|
.unwrap()
|
|
.expect("the session's own echo must still be answered");
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
assert_eq!((reply.recv_count, reply.recv_window), (Some(1), Some(0x1)));
|
|
assert_eq!(core.session_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn an_echo_resolves_ipv6_endpoints_by_the_same_identity_rule_as_a_close() {
|
|
// The comparator itself is pinned by the close tests; this is the echo path
|
|
// reaching the same one rather than growing a second rule.
|
|
let global = "2001:db8::1".parse().unwrap();
|
|
let link_local = "fe80::1".parse().unwrap();
|
|
let unlabeled = SocketAddr::V6(SocketAddrV6::new(global, 2112, 0, 0));
|
|
let labeled = SocketAddr::V6(SocketAddrV6::new(global, 2112, 0x000f_1234, 0));
|
|
let zone_three = SocketAddr::V6(SocketAddrV6::new(link_local, 2112, 0, 3));
|
|
let zone_four = SocketAddr::V6(SocketAddrV6::new(link_local, 2112, 0, 4));
|
|
|
|
for (bound, sender, answered) in [
|
|
// Flow information is routing metadata that identifies no endpoint.
|
|
(unlabeled, labeled, true),
|
|
(labeled, unlabeled, true),
|
|
// The zone is identity by `irtt-rs` policy, not by observed behavior.
|
|
(zone_three, zone_four, false),
|
|
] {
|
|
let mut core = core_with_tokens(ServerConfig::default(), ScriptedTokens::new([TOKEN_A]));
|
|
let (token, negotiated) = open_negotiated(&mut core, bound, &counting_params(0), None);
|
|
|
|
let reply = core
|
|
.handle_datagram(sender, &echo_request(token, 0, &negotiated, &[], None))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
reply.is_some(),
|
|
answered,
|
|
"an echo from {sender} on a session opened from {bound}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn received_count_and_window_follow_the_observed_vectors() {
|
|
// Every row is `(request sequence, reply count, reply window)` exactly as
|
|
// measured in Section 1 of the behavioral vectors.
|
|
for (name, vectors) in [
|
|
(
|
|
"1.1 sequential, no loss",
|
|
&[
|
|
(0, 1, 0x1),
|
|
(1, 2, 0x3),
|
|
(2, 3, 0x7),
|
|
(3, 4, 0xf),
|
|
(4, 5, 0x1f),
|
|
(5, 6, 0x3f),
|
|
][..],
|
|
),
|
|
(
|
|
"1.2 one gap",
|
|
&[(0, 1, 0x1), (1, 2, 0x3), (3, 3, 0xd), (4, 4, 0x1b)][..],
|
|
),
|
|
(
|
|
"1.3 multiple gaps",
|
|
&[(0, 1, 0x1), (3, 2, 0x9), (7, 3, 0x91), (8, 4, 0x123)][..],
|
|
),
|
|
(
|
|
// Filling a gap late discards the history rather than setting the
|
|
// bit the gap left clear.
|
|
"1.4 later gap fill",
|
|
&[(0, 1, 0x1), (1, 2, 0x3), (5, 3, 0x31), (3, 4, 0x1)][..],
|
|
),
|
|
(
|
|
"1.5 duplicate sequence number",
|
|
&[(0, 1, 0x1), (1, 2, 0x3), (1, 3, 0x3), (2, 4, 0x7)][..],
|
|
),
|
|
(
|
|
// Row 5 rebuilds from the reset: bit 1 stays clear even though
|
|
// sequence 2 was received before the reordering.
|
|
"1.6 reordering",
|
|
&[
|
|
(0, 1, 0x1),
|
|
(1, 2, 0x3),
|
|
(2, 3, 0x7),
|
|
(1, 4, 0x1),
|
|
(3, 5, 0x5),
|
|
][..],
|
|
),
|
|
(
|
|
"1.7 strictly descending sequence numbers",
|
|
&[
|
|
(5, 1, 0x1),
|
|
(4, 2, 0x1),
|
|
(3, 3, 0x1),
|
|
(2, 4, 0x1),
|
|
(1, 5, 0x1),
|
|
(0, 6, 0x1),
|
|
][..],
|
|
),
|
|
(
|
|
"1.8 a gap of exactly 63",
|
|
&[(0, 1, 0x1), (63, 2, 0x8000_0000_0000_0001)][..],
|
|
),
|
|
("1.8 a gap of 64", &[(0, 1, 0x1), (64, 2, 0x1)][..]),
|
|
("1.8 a gap of 65", &[(0, 1, 0x1), (65, 2, 0x1)][..]),
|
|
("1.8 a gap of 1000", &[(0, 1, 0x1), (1000, 2, 0x1)][..]),
|
|
(
|
|
"1.9 a non-zero starting sequence number",
|
|
&[(100, 1, 0x1), (101, 2, 0x3), (102, 3, 0x7)][..],
|
|
),
|
|
(
|
|
"1.10 32-bit sequence wraparound",
|
|
&[
|
|
(0xffff_fffd, 1, 0x1),
|
|
(0xffff_fffe, 2, 0x3),
|
|
(0xffff_ffff, 3, 0x7),
|
|
(0, 4, 0xf),
|
|
(1, 5, 0x1f),
|
|
][..],
|
|
),
|
|
] {
|
|
let (mut core, token, negotiated) = session_with(&counting_params(0));
|
|
|
|
for &(sequence, count, window) in vectors {
|
|
let packet = core
|
|
.handle_datagram(
|
|
peer(),
|
|
&echo_request(token, sequence, &negotiated, &[], None),
|
|
)
|
|
.unwrap()
|
|
.unwrap_or_else(|| panic!("{name}: sequence {sequence} must be answered"));
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
|
|
assert_eq!(
|
|
reply.recv_count,
|
|
Some(count),
|
|
"{name}: count after sequence {sequence}"
|
|
);
|
|
assert_eq!(
|
|
reply.recv_window,
|
|
Some(window),
|
|
"{name}: window after sequence {sequence}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn only_the_negotiated_received_statistics_are_reported() {
|
|
// Field presence only; the transition itself is pinned by the vectors
|
|
// above, so two in-order requests are enough here.
|
|
for (stats, count, window) in [
|
|
(ReceivedStats::None, None, None),
|
|
(ReceivedStats::Count, Some(2), None),
|
|
(ReceivedStats::Window, None, Some(0x3)),
|
|
(ReceivedStats::Both, Some(2), Some(0x3)),
|
|
] {
|
|
let params = echo_params(stats, StampAt::None, Clock::Unspecified, 0);
|
|
let (mut core, token, negotiated) = session_with(¶ms);
|
|
|
|
let mut reply = None;
|
|
for sequence in 0..2 {
|
|
let packet = core
|
|
.handle_datagram(
|
|
peer(),
|
|
&echo_request(token, sequence, &negotiated, &[], None),
|
|
)
|
|
.unwrap()
|
|
.unwrap_or_else(|| panic!("{stats:?}: sequence {sequence} must be answered"));
|
|
reply = Some(expect_echo_reply(&packet, &negotiated, None));
|
|
}
|
|
|
|
let reply = reply.expect("two requests were sent");
|
|
assert_eq!(reply.recv_count, count, "count presence for {stats:?}");
|
|
assert_eq!(reply.recv_window, window, "window presence for {stats:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_authenticated_session_answers_only_authenticated_echoes() {
|
|
let mut core = core_for(Some(KEY), ScriptedTokens::new([TOKEN_A]));
|
|
let (token, negotiated) = open_negotiated(&mut core, peer(), &counting_params(0), Some(KEY));
|
|
|
|
let packet = core
|
|
.handle_datagram(peer(), &echo_request(token, 3, &negotiated, &[], Some(KEY)))
|
|
.unwrap()
|
|
.expect("a correctly authenticated echo must be answered");
|
|
assert!(
|
|
verify_packet_hmac(KEY, packet.bytes()).is_ok(),
|
|
"the reply must carry a MAC that verifies with the server's key"
|
|
);
|
|
// `expect_echo_reply` pins the HMAC flag and verifies the MAC again while
|
|
// decoding, so a reply that authenticated but omitted the flag would fail
|
|
// here — the failure mode that silently breaks upstream clients.
|
|
let reply = expect_echo_reply(&packet, &negotiated, Some(KEY));
|
|
assert_eq!(reply.token, token);
|
|
assert_eq!(reply.sequence, 3);
|
|
assert_eq!((reply.recv_count, reply.recv_window), (Some(1), Some(0x1)));
|
|
|
|
let mut corrupted = echo_request(token, 4, &negotiated, &[], Some(KEY));
|
|
corrupted[5] ^= 0x80;
|
|
for (name, packet) in [
|
|
(
|
|
"an echo with no MAC field at all",
|
|
echo_request(token, 4, &negotiated, &[], None),
|
|
),
|
|
(
|
|
"an echo signed with a different key",
|
|
echo_request(token, 4, &negotiated, &[], Some(OTHER_KEY)),
|
|
),
|
|
("an echo with one bit flipped in the MAC", corrupted),
|
|
] {
|
|
assert_eq!(
|
|
core.handle_datagram(peer(), &packet).unwrap(),
|
|
None,
|
|
"{name} must be answered with silence"
|
|
);
|
|
}
|
|
|
|
let packet = core
|
|
.handle_datagram(peer(), &echo_request(token, 4, &negotiated, &[], Some(KEY)))
|
|
.unwrap()
|
|
.expect("an authenticated echo after failed ones must be answered");
|
|
let reply = expect_echo_reply(&packet, &negotiated, Some(KEY));
|
|
assert_eq!(
|
|
(reply.recv_count, reply.recv_window),
|
|
(Some(2), Some(0x3)),
|
|
"none of the packets that failed authentication may be counted"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn only_the_negotiated_timestamps_are_emitted() {
|
|
let received = sample(1_000, 10_000);
|
|
let sent = sample(1_200, 10_300);
|
|
|
|
for (stamp_at, clock, expected) in [
|
|
(
|
|
StampAt::Send,
|
|
Clock::Wall,
|
|
TimestampFields {
|
|
send_wall: Some(1_200),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Send,
|
|
Clock::Monotonic,
|
|
TimestampFields {
|
|
send_mono: Some(10_300),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Send,
|
|
Clock::Both,
|
|
TimestampFields {
|
|
send_wall: Some(1_200),
|
|
send_mono: Some(10_300),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Receive,
|
|
Clock::Wall,
|
|
TimestampFields {
|
|
recv_wall: Some(1_000),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Receive,
|
|
Clock::Monotonic,
|
|
TimestampFields {
|
|
recv_mono: Some(10_000),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Receive,
|
|
Clock::Both,
|
|
TimestampFields {
|
|
recv_wall: Some(1_000),
|
|
recv_mono: Some(10_000),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Both,
|
|
Clock::Wall,
|
|
TimestampFields {
|
|
recv_wall: Some(1_000),
|
|
send_wall: Some(1_200),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Both,
|
|
Clock::Monotonic,
|
|
TimestampFields {
|
|
recv_mono: Some(10_000),
|
|
send_mono: Some(10_300),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Both,
|
|
Clock::Both,
|
|
TimestampFields {
|
|
recv_wall: Some(1_000),
|
|
recv_mono: Some(10_000),
|
|
send_wall: Some(1_200),
|
|
send_mono: Some(10_300),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Midpoint,
|
|
Clock::Wall,
|
|
TimestampFields {
|
|
midpoint_wall: Some(1_100),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Midpoint,
|
|
Clock::Monotonic,
|
|
TimestampFields {
|
|
midpoint_mono: Some(10_150),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
StampAt::Midpoint,
|
|
Clock::Both,
|
|
TimestampFields {
|
|
midpoint_wall: Some(1_100),
|
|
midpoint_mono: Some(10_150),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
] {
|
|
let clock_source = ScriptedClock::new([open_sample(), received, sent]);
|
|
let mut core = core_with_sources(
|
|
ServerConfig::default(),
|
|
ScriptedTokens::new([TOKEN_A]),
|
|
clock_source.clone(),
|
|
);
|
|
let params = echo_params(ReceivedStats::None, stamp_at, clock, 0);
|
|
let (token, negotiated) = open_negotiated(&mut core, peer(), ¶ms, None);
|
|
assert_eq!(
|
|
clock_source.remaining(),
|
|
2,
|
|
"an open consumes exactly its own lifecycle sample"
|
|
);
|
|
|
|
let packet = core
|
|
.handle_datagram(peer(), &echo_request(token, 0, &negotiated, &[], None))
|
|
.unwrap()
|
|
.unwrap_or_else(|| panic!("{stamp_at:?} on {clock:?} must be answered"));
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
|
|
// Equality over the whole struct, so an unselected field appearing is a
|
|
// failure as much as a selected one carrying the wrong instant.
|
|
assert_eq!(reply.timestamps, expected, "{stamp_at:?} on {clock:?}");
|
|
assert_eq!(
|
|
packet.bytes().len(),
|
|
echo_packet_len(false, &negotiated).unwrap(),
|
|
"{stamp_at:?} on {clock:?} must be the negotiated layout's length"
|
|
);
|
|
assert_eq!(
|
|
clock_source.remaining(),
|
|
0,
|
|
"{stamp_at:?} on {clock:?}: one receive and one send sample"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_backward_wall_step_never_reports_receive_after_send() {
|
|
// A reply's receive instant must not be later than its send instant, and
|
|
// the wall clock can be stepped backwards between the two readings by NTP,
|
|
// a hypervisor or an administrator. The pair settles on the later reading,
|
|
// and only the wall domain moves: the monotonic one was already ordered.
|
|
let mut core = core_with_sources(
|
|
ServerConfig::default(),
|
|
ScriptedTokens::new([TOKEN_A]),
|
|
ScriptedClock::new([open_sample(), sample(1_000, 10_000), sample(990, 10_300)]),
|
|
);
|
|
let params = echo_params(ReceivedStats::None, StampAt::Both, Clock::Both, 0);
|
|
let (token, negotiated) = open_negotiated(&mut core, peer(), ¶ms, None);
|
|
|
|
let packet = core
|
|
.handle_datagram(peer(), &echo_request(token, 0, &negotiated, &[], None))
|
|
.unwrap()
|
|
.expect("a backward wall step must not stop the echo being answered");
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
|
|
assert_eq!(
|
|
reply.timestamps,
|
|
TimestampFields {
|
|
recv_wall: Some(990),
|
|
recv_mono: Some(10_000),
|
|
send_wall: Some(990),
|
|
send_mono: Some(10_300),
|
|
..TimestampFields::default()
|
|
},
|
|
"the receive wall value is held back to the send reading, and only it"
|
|
);
|
|
assert!(reply.timestamps.recv_wall <= reply.timestamps.send_wall);
|
|
assert!(reply.timestamps.recv_mono <= reply.timestamps.send_mono);
|
|
|
|
// A midpoint drawn from the same stepped pair is ordered for the same
|
|
// reason: it is the mean of two values that are no longer inverted.
|
|
let mut core = core_with_sources(
|
|
ServerConfig::default(),
|
|
ScriptedTokens::new([TOKEN_A]),
|
|
ScriptedClock::new([open_sample(), sample(1_000, 10_000), sample(990, 10_300)]),
|
|
);
|
|
let params = echo_params(ReceivedStats::None, StampAt::Midpoint, Clock::Wall, 0);
|
|
let (token, negotiated) = open_negotiated(&mut core, peer(), ¶ms, None);
|
|
|
|
let packet = core
|
|
.handle_datagram(peer(), &echo_request(token, 0, &negotiated, &[], None))
|
|
.unwrap()
|
|
.expect("a backward wall step must not stop the echo being answered");
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
|
|
assert_eq!(
|
|
reply.timestamps,
|
|
TimestampFields {
|
|
midpoint_wall: Some(990),
|
|
..TimestampFields::default()
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_single_clock_midpoint_emits_exactly_one_field() {
|
|
// A guard against reproducing upstream 0.9.1's dual-field midpoint, which
|
|
// emits both midpoint fields whatever the negotiated clock. `irtt-proto`
|
|
// still decodes that form from a peer; this is about what this server
|
|
// sends.
|
|
for (clock, expected) in [
|
|
(
|
|
Clock::Wall,
|
|
TimestampFields {
|
|
midpoint_wall: Some(1_100),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
(
|
|
Clock::Monotonic,
|
|
TimestampFields {
|
|
midpoint_mono: Some(10_150),
|
|
..TimestampFields::default()
|
|
},
|
|
),
|
|
] {
|
|
let mut core = core_with_sources(
|
|
ServerConfig::default(),
|
|
ScriptedTokens::new([TOKEN_A]),
|
|
ScriptedClock::new([open_sample(), sample(1_000, 10_000), sample(1_200, 10_300)]),
|
|
);
|
|
let params = echo_params(ReceivedStats::None, StampAt::Midpoint, clock, 0);
|
|
let (token, negotiated) = open_negotiated(&mut core, peer(), ¶ms, None);
|
|
|
|
let packet = core
|
|
.handle_datagram(peer(), &echo_request(token, 0, &negotiated, &[], None))
|
|
.unwrap()
|
|
.unwrap_or_else(|| panic!("a midpoint echo on {clock:?} must be answered"));
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
|
|
assert_eq!(
|
|
packet.bytes().len(),
|
|
echo_packet_len(false, &negotiated).unwrap(),
|
|
"{clock:?}: the negotiated length, not the compatibility one"
|
|
);
|
|
assert_eq!(
|
|
packet.bytes().len(),
|
|
24,
|
|
"{clock:?}: one 8-byte midpoint field, not the upstream two"
|
|
);
|
|
assert_eq!(reply.timestamps, expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_reply_payload_is_server_generated_and_never_reflects_the_request() {
|
|
// The inbound tail is opaque, and ServerFill describes the *server's* bytes:
|
|
// a request's payload is neither echoed, mixed in, nor used to seed
|
|
// anything. This session expressed no fill preference, so the region
|
|
// carries the server's default fill; `fill` covers the modes themselves.
|
|
let params = echo_params(ReceivedStats::None, StampAt::None, Clock::Unspecified, 64);
|
|
let (mut core, token, negotiated) = session_with(¶ms);
|
|
|
|
let request = echo_request(token, 0, &negotiated, &[0xa5; 48], None);
|
|
assert!(
|
|
request[16..].iter().all(|byte| *byte == 0xa5),
|
|
"the request must carry non-zero payload bytes to reflect"
|
|
);
|
|
|
|
let packet = core
|
|
.handle_datagram(peer(), &request)
|
|
.unwrap()
|
|
.expect("an admissible echo must be answered");
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
|
|
assert_eq!(packet.bytes().len(), 64);
|
|
assert_eq!(reply.payload.len(), 48);
|
|
assert!(
|
|
!reply.payload.contains(&0xa5),
|
|
"no request byte may appear in the reply payload"
|
|
);
|
|
assert!(
|
|
reply
|
|
.payload
|
|
.chunks(4)
|
|
.all(|chunk| chunk == &b"irtt"[..chunk.len()]),
|
|
"the region carries the server's default fill"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_restricted_session_emits_the_restricted_timestamps_and_not_the_requested_ones() {
|
|
// Timestamp generation follows the session's *negotiated* placement, which
|
|
// is the point of restricting it during negotiation: a client asking for
|
|
// both instants from both clocks gets one midpoint field per clock under a
|
|
// single-timestamp allowance, and nothing at all under none — with the reply
|
|
// laid out and sized from the restriction either way, not from the request.
|
|
for (allowance, expected_stamp_at, expected, expected_len) in [
|
|
(
|
|
TimestampAllowance::Single,
|
|
StampAt::Midpoint,
|
|
TimestampFields {
|
|
midpoint_wall: Some(1_100),
|
|
midpoint_mono: Some(10_150),
|
|
..TimestampFields::default()
|
|
},
|
|
32,
|
|
),
|
|
(
|
|
TimestampAllowance::None,
|
|
StampAt::None,
|
|
TimestampFields::default(),
|
|
16,
|
|
),
|
|
] {
|
|
let mut core = core_with_sources(
|
|
unthrottled().with_timestamp_allowance(allowance),
|
|
ScriptedTokens::new([TOKEN_A]),
|
|
ScriptedClock::new([open_sample(), sample(1_000, 10_000), sample(1_200, 10_300)]),
|
|
);
|
|
let requested = echo_params(ReceivedStats::None, StampAt::Both, Clock::Both, 0);
|
|
let (token, negotiated) = open_negotiated(&mut core, peer(), &requested, None);
|
|
assert_eq!(negotiated.stamp_at, expected_stamp_at, "{allowance:?}");
|
|
assert_eq!(negotiated.clock, Clock::Both, "{allowance:?}");
|
|
|
|
let packet = core
|
|
.handle_datagram(peer(), &echo_request(token, 0, &negotiated, &[], None))
|
|
.unwrap()
|
|
.unwrap_or_else(|| panic!("{allowance:?}: an admissible echo must be answered"));
|
|
let reply = expect_echo_reply(&packet, &negotiated, None);
|
|
|
|
// Equality over the whole struct, so a leftover receive or send field is
|
|
// a failure as much as a missing midpoint one.
|
|
assert_eq!(reply.timestamps, expected, "{allowance:?}");
|
|
assert_eq!(
|
|
packet.bytes().len(),
|
|
echo_packet_len(false, &negotiated).unwrap(),
|
|
"{allowance:?}: the restricted layout's length"
|
|
);
|
|
assert_eq!(
|
|
packet.bytes().len(),
|
|
expected_len,
|
|
"{allowance:?}: no empty slot is kept for a restricted field"
|
|
);
|
|
}
|
|
}
|