Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 34s
Rust CI / cargo clippy (push) Successful in 35s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m14s
Rust CI / cargo doc (push) Successful in 23s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 29s
Supply chain / cargo-deny advisories (push) Successful in 9s
Supply chain / cargo-deny bans licenses sources (push) Successful in 5s
Supply chain / actionlint (push) Successful in 3s
Netem Interop / netem interop (push) Successful in 1m11s
378 lines
13 KiB
Rust
378 lines
13 KiB
Rust
//! Durable server contracts observed through encoded requests and public replies.
|
|
use std::{
|
|
net::SocketAddr,
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use irtt_proto::{
|
|
decode_echo_reply, decode_open_reply, encode_request, Clock, EchoReply, OpenReply, Params,
|
|
ReceivedStats, RequestToEncode, StampAt, FLAG_CLOSE,
|
|
};
|
|
use irtt_server::{ServerConfig, ServerCore, TimestampAllowance};
|
|
|
|
fn peer() -> SocketAddr {
|
|
"127.0.0.1:40000".parse().unwrap()
|
|
}
|
|
fn open_packet(params: &Params, no_test: bool, key: Option<&[u8]>) -> Vec<u8> {
|
|
encode_request(RequestToEncode::Open { params, no_test }, key).unwrap()
|
|
}
|
|
fn open(server: &mut ServerCore, params: &Params) -> OpenReply {
|
|
let reply = server
|
|
.handle_datagram(peer(), &open_packet(params, false, None))
|
|
.unwrap()
|
|
.unwrap();
|
|
decode_open_reply(reply.bytes(), None).unwrap()
|
|
}
|
|
fn echo(server: &mut ServerCore, opened: &OpenReply, sequence: u32) -> Option<EchoReply> {
|
|
let request = encode_request(
|
|
RequestToEncode::Echo {
|
|
token: opened.token,
|
|
sequence,
|
|
params: &opened.params,
|
|
payload: &[],
|
|
},
|
|
None,
|
|
)
|
|
.unwrap();
|
|
server
|
|
.handle_datagram(peer(), &request)
|
|
.unwrap()
|
|
.map(|reply| decode_echo_reply(reply.bytes(), &opened.params, None).unwrap())
|
|
}
|
|
fn unthrottled() -> ServerConfig {
|
|
ServerConfig::default().with_min_send_interval(Duration::ZERO)
|
|
}
|
|
|
|
#[test]
|
|
fn no_test_authenticates_negotiates_and_uses_no_session_capacity() {
|
|
let key = b"no-test-contract";
|
|
let mut server = ServerCore::new(
|
|
ServerConfig::default()
|
|
.with_hmac_key(key)
|
|
.with_max_sessions(0)
|
|
.with_max_packet_length(96)
|
|
.with_min_send_interval(Duration::from_millis(50))
|
|
.with_timestamp_allowance(TimestampAllowance::Single)
|
|
.with_dscp_allowed(false),
|
|
);
|
|
let params = Params {
|
|
protocol_version: 99,
|
|
length: 128,
|
|
dscp: 185,
|
|
stamp_at: StampAt::Both,
|
|
clock: Clock::Both,
|
|
..Params::default()
|
|
};
|
|
let request = open_packet(¶ms, true, Some(key));
|
|
assert!(server
|
|
.handle_datagram(peer(), &open_packet(¶ms, true, None))
|
|
.unwrap()
|
|
.is_none());
|
|
let mut bad_mac = request.clone();
|
|
bad_mac[4] ^= 1;
|
|
assert!(server.handle_datagram(peer(), &bad_mac).unwrap().is_none());
|
|
let reply = server.handle_datagram(peer(), &request).unwrap().unwrap();
|
|
assert_eq!(reply.traffic_class(), 0);
|
|
let negotiated = decode_open_reply(reply.bytes(), Some(key)).unwrap();
|
|
assert_eq!(negotiated.token, 0);
|
|
assert_eq!(negotiated.params.protocol_version, 1);
|
|
assert_ne!(negotiated.flags & FLAG_CLOSE, 0);
|
|
assert_eq!(negotiated.params.length, 96);
|
|
assert_eq!(negotiated.params.interval_ns, 50_000_000);
|
|
assert_eq!(negotiated.params.stamp_at, StampAt::Midpoint);
|
|
assert_eq!(negotiated.params.clock, Clock::Both);
|
|
assert_eq!(negotiated.params.dscp, 0);
|
|
assert_eq!(server.session_count(), 0);
|
|
assert!(server
|
|
.handle_datagram(peer(), &open_packet(¶ms, false, Some(key)))
|
|
.unwrap()
|
|
.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn no_test_refuses_unexecutable_layout_but_validates_after_restriction() {
|
|
let params = Params {
|
|
stamp_at: StampAt::Both,
|
|
..Params::with_protocol_defaults()
|
|
};
|
|
let mut server = ServerCore::new(unthrottled());
|
|
assert!(server
|
|
.handle_datagram(peer(), &open_packet(¶ms, true, None))
|
|
.unwrap()
|
|
.is_none());
|
|
let mut restricted =
|
|
ServerCore::new(unthrottled().with_timestamp_allowance(TimestampAllowance::None));
|
|
let reply = restricted
|
|
.handle_datagram(peer(), &open_packet(¶ms, true, None))
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
decode_open_reply(reply.bytes(), None)
|
|
.unwrap()
|
|
.params
|
|
.stamp_at,
|
|
StampAt::None
|
|
);
|
|
let rich = Params {
|
|
received_stats: ReceivedStats::Both,
|
|
stamp_at: StampAt::Both,
|
|
clock: Clock::Both,
|
|
..Params::with_protocol_defaults()
|
|
};
|
|
let mut too_small = ServerCore::new(unthrottled().with_max_packet_length(32));
|
|
assert!(too_small
|
|
.handle_datagram(peer(), &open_packet(&rich, true, None))
|
|
.unwrap()
|
|
.is_none());
|
|
assert_eq!(too_small.session_count(), 0);
|
|
let invalid = Params {
|
|
interval_ns: -1,
|
|
..Params::with_protocol_defaults()
|
|
};
|
|
assert!(server
|
|
.handle_datagram(peer(), &open_packet(&invalid, true, None))
|
|
.unwrap()
|
|
.is_none());
|
|
assert_eq!(server.session_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn receive_statistics_follow_verified_gap_duplicate_reordering_and_wrap_vectors() {
|
|
// SERVER_BEHAVIORAL_VECTORS.md Sections 1.2, 1.5, 1.6, 1.8 and 1.10.
|
|
let vectors: &[&[(u32, u64)]] = &[
|
|
&[(0, 1), (1, 3), (3, 13), (4, 27)],
|
|
&[(0, 1), (1, 3), (1, 3), (2, 7)],
|
|
&[(0, 1), (1, 3), (2, 7), (1, 1), (3, 5)],
|
|
&[(0, 1), (63, 0x8000_0000_0000_0001)],
|
|
&[(0, 1), (64, 1)],
|
|
&[
|
|
(u32::MAX - 2, 1),
|
|
(u32::MAX - 1, 3),
|
|
(u32::MAX, 7),
|
|
(0, 15),
|
|
(1, 31),
|
|
],
|
|
];
|
|
for vector in vectors {
|
|
let mut server = ServerCore::new(unthrottled());
|
|
let opened = open(
|
|
&mut server,
|
|
&Params {
|
|
received_stats: ReceivedStats::Both,
|
|
..Params::with_protocol_defaults()
|
|
},
|
|
);
|
|
for (index, &(sequence, window)) in vector.iter().enumerate() {
|
|
let reply = echo(&mut server, &opened, sequence).unwrap();
|
|
assert_eq!(reply.recv_count, Some(index as u32 + 1));
|
|
assert_eq!(
|
|
reply.recv_window,
|
|
Some(window),
|
|
"sequence {sequence} in {vector:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_spent_nonzero_burst_refills_without_counting_dropped_echoes() {
|
|
let refill = Duration::from_millis(100);
|
|
let mut server = ServerCore::new(
|
|
ServerConfig::default()
|
|
.with_min_send_interval(refill)
|
|
.with_burst_allowance(1)
|
|
.with_idle_timeout(Duration::MAX),
|
|
);
|
|
let opened = open(
|
|
&mut server,
|
|
&Params {
|
|
received_stats: ReceivedStats::Both,
|
|
..Params::with_protocol_defaults()
|
|
},
|
|
);
|
|
let mut count = echo(&mut server, &opened, 0).unwrap().recv_count.unwrap();
|
|
// Observe actual refusal rather than assuming two calls fit a timing window.
|
|
let deadline = Instant::now() + Duration::from_secs(2);
|
|
while let Some(reply) = echo(&mut server, &opened, 0) {
|
|
count = reply.recv_count.unwrap();
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"could not observe a spent burst within two seconds"
|
|
);
|
|
}
|
|
thread::sleep(refill); // Oversleep only replenishes more; idle expiry is disabled in practice.
|
|
let reply = echo(&mut server, &opened, 1).unwrap();
|
|
assert_eq!(reply.recv_count, Some(count + 1));
|
|
assert_eq!(reply.recv_window, Some(3));
|
|
}
|
|
|
|
#[test]
|
|
fn a_nonzero_idle_deadline_reclaims_an_unused_session_before_another_open() {
|
|
let idle = Duration::from_millis(10);
|
|
let mut server = ServerCore::new(unthrottled().with_max_sessions(1).with_idle_timeout(idle));
|
|
let params = Params::with_protocol_defaults();
|
|
let old = open(&mut server, ¶ms);
|
|
thread::sleep(idle); // Any oversleep still requires silent expiry and capacity recovery.
|
|
assert!(echo(&mut server, &old, 0).is_none());
|
|
assert_eq!(server.session_count(), 0);
|
|
open(&mut server, ¶ms);
|
|
assert_eq!(server.session_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn maximum_duration_starts_at_the_first_served_echo_and_closes_with_its_reply() {
|
|
let maximum = Duration::from_millis(1);
|
|
let mut server = ServerCore::new(
|
|
unthrottled()
|
|
.with_idle_timeout(Duration::MAX)
|
|
.with_max_test_duration(maximum),
|
|
);
|
|
let opened = open(
|
|
&mut server,
|
|
&Params {
|
|
received_stats: ReceivedStats::Both,
|
|
stamp_at: StampAt::Both,
|
|
clock: Clock::Both,
|
|
dscp: 185,
|
|
..Params::with_protocol_defaults()
|
|
},
|
|
);
|
|
assert_eq!(opened.params.duration_ns, 1_000_000);
|
|
let elapsed_deadline = maximum + Duration::from_secs(2);
|
|
thread::sleep(elapsed_deadline);
|
|
let first = echo(&mut server, &opened, 0).unwrap();
|
|
assert_eq!(
|
|
first.flags & FLAG_CLOSE,
|
|
0,
|
|
"OPEN must not start the maximum-duration clock"
|
|
);
|
|
thread::sleep(elapsed_deadline); // Oversleep still requires the next served echo to close.
|
|
let request = encode_request(
|
|
RequestToEncode::Echo {
|
|
token: opened.token,
|
|
sequence: 1,
|
|
params: &opened.params,
|
|
payload: &[],
|
|
},
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let reply = server.handle_datagram(peer(), &request).unwrap().unwrap();
|
|
assert_eq!(reply.traffic_class(), 185);
|
|
let closed = decode_echo_reply(reply.bytes(), &opened.params, None).unwrap();
|
|
assert_ne!(closed.flags & FLAG_CLOSE, 0);
|
|
assert_eq!(closed.sequence, 1);
|
|
assert_eq!(closed.recv_count, Some(2));
|
|
assert_eq!(closed.recv_window, Some(3));
|
|
assert!(closed.timestamps.recv_wall.is_some());
|
|
assert!(closed.timestamps.send_mono.is_some());
|
|
assert_eq!(server.session_count(), 0);
|
|
assert!(echo(&mut server, &opened, 2).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn negotiated_timestamp_modes_emit_only_the_selected_fields_in_order() {
|
|
for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] {
|
|
for placement in [
|
|
StampAt::None,
|
|
StampAt::Receive,
|
|
StampAt::Send,
|
|
StampAt::Both,
|
|
StampAt::Midpoint,
|
|
] {
|
|
let mut server = ServerCore::new(unthrottled());
|
|
let opened = open(
|
|
&mut server,
|
|
&Params {
|
|
stamp_at: placement,
|
|
clock,
|
|
..Params::with_protocol_defaults()
|
|
},
|
|
);
|
|
let t = echo(&mut server, &opened, 0).unwrap().timestamps;
|
|
let wall = clock != Clock::Monotonic;
|
|
let mono = clock != Clock::Wall;
|
|
let recv = matches!(placement, StampAt::Receive | StampAt::Both);
|
|
let send = matches!(placement, StampAt::Send | StampAt::Both);
|
|
let midpoint = placement == StampAt::Midpoint;
|
|
assert_eq!(t.recv_wall.is_some(), wall && recv);
|
|
assert_eq!(t.recv_mono.is_some(), mono && recv);
|
|
assert_eq!(t.send_wall.is_some(), wall && send);
|
|
assert_eq!(t.send_mono.is_some(), mono && send);
|
|
assert_eq!(t.midpoint_wall.is_some(), wall && midpoint);
|
|
assert_eq!(t.midpoint_mono.is_some(), mono && midpoint);
|
|
if let (Some(recv), Some(send)) = (t.recv_wall, t.send_wall) {
|
|
assert!(recv <= send);
|
|
}
|
|
if let (Some(recv), Some(send)) = (t.recv_mono, t.send_mono) {
|
|
assert!(0 <= recv && recv <= send);
|
|
let next = echo(&mut server, &opened, 1).unwrap().timestamps;
|
|
assert!(
|
|
next.recv_mono.unwrap() >= send,
|
|
"monotonic origin must remain stable"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn duration_close_waits_for_rate_allowance_after_the_deadline() {
|
|
let refill = Duration::from_secs(4);
|
|
let maximum = Duration::from_millis(1);
|
|
for _ in 0..3 {
|
|
let mut server = ServerCore::new(
|
|
ServerConfig::default()
|
|
.with_min_send_interval(refill)
|
|
.with_burst_allowance(1)
|
|
.with_idle_timeout(Duration::MAX)
|
|
.with_max_test_duration(maximum),
|
|
);
|
|
let started = Instant::now();
|
|
let opened = open(
|
|
&mut server,
|
|
&Params {
|
|
received_stats: ReceivedStats::Both,
|
|
..Params::with_protocol_defaults()
|
|
},
|
|
);
|
|
let first = echo(&mut server, &opened, 0).unwrap();
|
|
assert_eq!(first.flags & FLAG_CLOSE, 0);
|
|
thread::sleep(maximum + Duration::from_secs(2));
|
|
let refused = echo(&mut server, &opened, 63);
|
|
// Include open/setup and the request itself in the elapsed bound. If the
|
|
// scheduler carried us past refill, the reply may legitimately close.
|
|
if started.elapsed() >= refill {
|
|
continue;
|
|
}
|
|
assert!(
|
|
refused.is_none(),
|
|
"exhausted allowance must suppress the deadline-crossing reply"
|
|
);
|
|
assert_eq!(
|
|
server.session_count(),
|
|
1,
|
|
"rate refusal must not release the session"
|
|
);
|
|
thread::sleep(refill);
|
|
let closed = echo(&mut server, &opened, 1).unwrap();
|
|
assert_ne!(closed.flags & FLAG_CLOSE, 0);
|
|
assert_eq!(closed.sequence, 1);
|
|
assert_eq!(closed.recv_count, Some(2));
|
|
assert_eq!(closed.recv_window, Some(3));
|
|
assert_eq!(server.session_count(), 0);
|
|
return;
|
|
}
|
|
panic!("scheduler crossed the refill boundary in all three fresh sessions");
|
|
}
|
|
|
|
#[test]
|
|
fn default_server_resource_limits_remain_bounded() {
|
|
let config = ServerConfig::default();
|
|
// Upper resource budgets, not an exact default or a comparison against
|
|
// the same constants used by the implementation. Smaller bounds are fine.
|
|
assert!((1..=1024).contains(&config.max_sessions()));
|
|
assert!((1..=65_507).contains(&config.max_packet_length()));
|
|
}
|