tests: rework the actually useful tests to live in the integration crate
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
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
This commit is contained in:
101 files changed
+3636
-15159
No files matched your search
@@ -0,0 +1,377 @@
|
||||
//! 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()));
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
use std::{net::Ipv4Addr, time::Duration};
|
||||
|
||||
use irtt_proto::{
|
||||
decode_echo_reply, decode_open_reply, encode_request, Clock, Params, ReceivedStats,
|
||||
RequestToEncode, StampAt,
|
||||
};
|
||||
use tokio::{net::UdpSocket, sync::oneshot, time::timeout};
|
||||
|
||||
use irtt_server::{Server, ServerConfig, ServerRuntimeError};
|
||||
use std::{io, net::SocketAddr};
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn bind_reports_local_address_and_shutdown_stops_run() {
|
||||
let requested = SocketAddr::from((Ipv4Addr::LOCALHOST, 0));
|
||||
let mut server = Server::bind(requested, ServerConfig::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let bound = server.local_addr().unwrap();
|
||||
assert_eq!(bound.ip(), requested.ip());
|
||||
assert_ne!(bound.port(), 0);
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
shutdown_tx.send(()).unwrap();
|
||||
server
|
||||
.run(async {
|
||||
let _ = shutdown_rx.await;
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn ipv4_open_echo_close_crosses_a_real_udp_socket() {
|
||||
exercise_udp_path(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn ipv6_open_echo_close_when_loopback_is_available() {
|
||||
let addr = "[::1]:0".parse().unwrap();
|
||||
let server = match Server::bind(addr, unthrottled()).await {
|
||||
Ok(server) => server,
|
||||
Err(ServerRuntimeError::Bind { source, .. })
|
||||
if matches!(
|
||||
source.kind(),
|
||||
io::ErrorKind::AddrNotAvailable | io::ErrorKind::Unsupported
|
||||
) =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
Err(error) => panic!("unexpected IPv6 loopback bind failure: {error}"),
|
||||
};
|
||||
exercise_bound_server(server).await;
|
||||
}
|
||||
|
||||
async fn exercise_udp_path(addr: SocketAddr) {
|
||||
let server = Server::bind(addr, unthrottled()).await.unwrap();
|
||||
exercise_bound_server(server).await;
|
||||
}
|
||||
|
||||
async fn exercise_bound_server(mut server: Server) {
|
||||
let server_addr = server.local_addr().unwrap();
|
||||
let client_bind = match server_addr {
|
||||
SocketAddr::V4(_) => "127.0.0.1:0",
|
||||
SocketAddr::V6(_) => "[::1]:0",
|
||||
};
|
||||
let client = UdpSocket::bind(client_bind).await.unwrap();
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
let server_task = tokio::spawn(async move {
|
||||
server
|
||||
.run(async {
|
||||
let _ = shutdown_rx.await;
|
||||
})
|
||||
.await
|
||||
});
|
||||
|
||||
let requested = Params {
|
||||
protocol_version: 1,
|
||||
duration_ns: 1_000_000_000,
|
||||
interval_ns: 100_000_000,
|
||||
received_stats: ReceivedStats::Both,
|
||||
stamp_at: StampAt::Both,
|
||||
clock: Clock::Both,
|
||||
..Params::default()
|
||||
};
|
||||
let open = encode_request(
|
||||
RequestToEncode::Open {
|
||||
params: &requested,
|
||||
no_test: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
client.send_to(&open, server_addr).await.unwrap();
|
||||
|
||||
let mut buffer = vec![0; 65_536];
|
||||
let (len, reply_source) = timeout(Duration::from_secs(1), client.recv_from(&mut buffer))
|
||||
.await
|
||||
.expect("OPEN reply timeout")
|
||||
.unwrap();
|
||||
assert_eq!(reply_source, server_addr);
|
||||
let open_reply = decode_open_reply(&buffer[..len], None).unwrap();
|
||||
assert_ne!(open_reply.token, 0);
|
||||
|
||||
let echo = encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: open_reply.token,
|
||||
sequence: 0,
|
||||
params: &open_reply.params,
|
||||
payload: &[],
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
client.send_to(&echo, server_addr).await.unwrap();
|
||||
let (len, reply_source) = timeout(Duration::from_secs(1), client.recv_from(&mut buffer))
|
||||
.await
|
||||
.expect("ECHO reply timeout")
|
||||
.unwrap();
|
||||
assert_eq!(reply_source, server_addr);
|
||||
let echo_reply = decode_echo_reply(&buffer[..len], &open_reply.params, None).unwrap();
|
||||
assert_eq!(echo_reply.token, open_reply.token);
|
||||
assert_eq!(echo_reply.sequence, 0);
|
||||
assert_eq!(echo_reply.recv_count, Some(1));
|
||||
assert_eq!(echo_reply.recv_window, Some(1));
|
||||
|
||||
let close = encode_request(
|
||||
RequestToEncode::Close {
|
||||
token: open_reply.token,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
client.send_to(&close, server_addr).await.unwrap();
|
||||
client.send_to(&echo, server_addr).await.unwrap();
|
||||
assert!(
|
||||
timeout(Duration::from_millis(100), client.recv_from(&mut buffer))
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
shutdown_tx.send(()).unwrap();
|
||||
server_task.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
fn unthrottled() -> ServerConfig {
|
||||
ServerConfig::default().with_min_send_interval(Duration::ZERO)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn wildcard_construction_is_refused_without_reply_source_selection() {
|
||||
let addr = "0.0.0.0:0".parse().unwrap();
|
||||
assert!(matches!(
|
||||
Server::bind(addr, unthrottled()).await,
|
||||
Err(ServerRuntimeError::WildcardSourceSelectionUnsupported { .. })
|
||||
));
|
||||
}
|
||||
|
||||
// A current-thread runtime cannot receive while this test holds its thread.
|
||||
// Queue real packets before delaying the server: no synthetic ancillary data or
|
||||
// transport hooks. Observed queue timing determines whether kernel wall time
|
||||
// is plausible under the one-second bound, including scheduler overshoot. Monotonic and
|
||||
// midpoint timestamps must always remain at the later userspace observation.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn queued_echoes_select_kernel_receive_wall_only_when_plausible() {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let ns = || {
|
||||
i64::try_from(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos(),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
for (placement, delay) in [
|
||||
(StampAt::Both, Duration::from_millis(100)),
|
||||
(StampAt::Both, Duration::from_millis(1100)),
|
||||
(StampAt::Midpoint, Duration::from_millis(100)),
|
||||
] {
|
||||
let mut server = Server::bind("127.0.0.1:0".parse().unwrap(), unthrottled())
|
||||
.await
|
||||
.unwrap();
|
||||
let addr = server.local_addr().unwrap();
|
||||
let client = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
client.connect(addr).await.unwrap();
|
||||
let (stop_tx, stop_rx) = oneshot::channel();
|
||||
let run = tokio::spawn(async move {
|
||||
server
|
||||
.run(async {
|
||||
let _ = stop_rx.await;
|
||||
})
|
||||
.await
|
||||
});
|
||||
let params = Params {
|
||||
stamp_at: placement,
|
||||
clock: Clock::Both,
|
||||
received_stats: ReceivedStats::Both,
|
||||
..Params::with_protocol_defaults()
|
||||
};
|
||||
let open = encode_request(
|
||||
RequestToEncode::Open {
|
||||
params: ¶ms,
|
||||
no_test: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
client.send(&open).await.unwrap();
|
||||
let mut buffer = [0; 2048];
|
||||
let len = timeout(Duration::from_secs(2), client.recv(&mut buffer))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let opened = decode_open_reply(&buffer[..len], None).unwrap();
|
||||
let echo = encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: opened.token,
|
||||
sequence: 0,
|
||||
params: &opened.params,
|
||||
payload: &[],
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
client.writable().await.unwrap();
|
||||
let before_send = ns();
|
||||
client.try_send(&echo).unwrap(); // No await from submission through resume marker.
|
||||
let queued_by = ns();
|
||||
std::thread::sleep(delay);
|
||||
let resumed_at = ns();
|
||||
let len = timeout(Duration::from_secs(2), client.recv(&mut buffer))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let reply = decode_echo_reply(&buffer[..len], &opened.params, None).unwrap();
|
||||
assert_eq!(reply.recv_count, Some(1));
|
||||
assert_eq!(reply.recv_window, Some(1));
|
||||
let timestamps = reply.timestamps;
|
||||
if placement == StampAt::Both {
|
||||
let receive = timestamps.recv_wall.unwrap();
|
||||
let send = timestamps.send_wall.unwrap();
|
||||
let kernel_endpoint = (before_send..=queued_by).contains(&receive);
|
||||
let userspace_endpoint = (resumed_at..=send).contains(&receive);
|
||||
// Arrival lies in [before_send, queued_by], and core observation
|
||||
// lies in [resumed_at, send]. Only demand one selection where these
|
||||
// observed bounds put the whole lag interval on one side of 1 s.
|
||||
// Scheduler overshoot can legitimately change the expected path.
|
||||
if send - before_send <= 1_000_000_000 {
|
||||
assert!(
|
||||
kernel_endpoint,
|
||||
"plausible arrival must select kernel wall time"
|
||||
);
|
||||
} else if resumed_at - queued_by > 1_000_000_000 {
|
||||
assert!(
|
||||
userspace_endpoint,
|
||||
"stale arrival must fall back to userspace"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
kernel_endpoint || userspace_endpoint,
|
||||
"boundary observation must identify a real endpoint"
|
||||
);
|
||||
}
|
||||
if kernel_endpoint {
|
||||
let wall_elapsed = send - receive;
|
||||
let mono_elapsed = timestamps.send_mono.unwrap() - timestamps.recv_mono.unwrap();
|
||||
assert!(
|
||||
wall_elapsed - mono_elapsed >= 50_000_000,
|
||||
"queue delay must not move the monotonic endpoint"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
assert!(
|
||||
timestamps.midpoint_wall.unwrap() >= resumed_at,
|
||||
"midpoint must use userspace time"
|
||||
);
|
||||
}
|
||||
stop_tx.send(()).unwrap();
|
||||
run.await.unwrap().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -465,3 +465,11 @@ where
|
||||
.await
|
||||
.expect("the test exceeded its bounded runtime");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn a_set_serving_nothing_is_refused() {
|
||||
assert!(matches!(
|
||||
ServerSet::bind([], ServerConfig::default()).await,
|
||||
Err(ServerSetError::NoListeners)
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
use std::{
|
||||
net::{SocketAddr, SocketAddrV6},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use irtt_proto::{decode_echo_reply, decode_open_reply, encode_request, Params, RequestToEncode};
|
||||
use irtt_server::{ServerConfig, ServerCore};
|
||||
|
||||
// Admission, authentication, endpoint ownership and capacity are checked through
|
||||
// the public packet interface, with actual OS-generated session tokens.
|
||||
#[test]
|
||||
fn authenticated_sessions_are_bounded_and_owned_by_the_opening_endpoint() {
|
||||
let key = b"session-behavior";
|
||||
let peer: SocketAddr = "127.0.0.1:40000".parse().unwrap();
|
||||
let other: SocketAddr = "127.0.0.1:40001".parse().unwrap();
|
||||
let mut server = ServerCore::new(
|
||||
ServerConfig::default()
|
||||
.with_hmac_key(key)
|
||||
.with_max_sessions(1)
|
||||
.with_min_send_interval(Duration::ZERO),
|
||||
);
|
||||
let params = Params {
|
||||
received_stats: irtt_proto::ReceivedStats::Both,
|
||||
..Params::with_protocol_defaults()
|
||||
};
|
||||
let open = encode_request(
|
||||
RequestToEncode::Open {
|
||||
params: ¶ms,
|
||||
no_test: false,
|
||||
},
|
||||
Some(key),
|
||||
)
|
||||
.unwrap();
|
||||
let mut unkeyed = ServerCore::new(ServerConfig::default());
|
||||
assert!(unkeyed.handle_datagram(peer, &open).unwrap().is_none());
|
||||
assert_eq!(unkeyed.session_count(), 0);
|
||||
let unauthenticated = encode_request(
|
||||
RequestToEncode::Open {
|
||||
params: ¶ms,
|
||||
no_test: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
for packet in [
|
||||
&[][..],
|
||||
&[0; 16][..],
|
||||
&open[..open.len() - 1],
|
||||
&unauthenticated,
|
||||
] {
|
||||
assert!(server.handle_datagram(peer, packet).unwrap().is_none());
|
||||
assert_eq!(server.session_count(), 0);
|
||||
}
|
||||
let reply = server.handle_datagram(peer, &open).unwrap().unwrap();
|
||||
let opened = decode_open_reply(reply.bytes(), Some(key)).unwrap();
|
||||
assert_ne!(opened.token, 0);
|
||||
assert!(server.handle_datagram(other, &open).unwrap().is_none());
|
||||
assert_eq!(server.session_count(), 1);
|
||||
let echo = encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: opened.token,
|
||||
sequence: 0,
|
||||
params: &opened.params,
|
||||
payload: &[],
|
||||
},
|
||||
Some(key),
|
||||
)
|
||||
.unwrap();
|
||||
let close = encode_request(
|
||||
RequestToEncode::Close {
|
||||
token: opened.token,
|
||||
},
|
||||
Some(key),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(server.handle_datagram(other, &echo).unwrap().is_none());
|
||||
assert!(server.handle_datagram(other, &close).unwrap().is_none());
|
||||
let mut corrupted = echo.clone();
|
||||
corrupted[4] ^= 1;
|
||||
assert!(server.handle_datagram(peer, &corrupted).unwrap().is_none());
|
||||
let reply = server.handle_datagram(peer, &echo).unwrap().unwrap();
|
||||
let echoed = decode_echo_reply(reply.bytes(), &opened.params, Some(key)).unwrap();
|
||||
assert_eq!(echoed.sequence, 0);
|
||||
assert_eq!(echoed.recv_count, Some(1));
|
||||
assert!(server.handle_datagram(peer, &close).unwrap().is_none());
|
||||
assert_eq!(server.session_count(), 0);
|
||||
assert!(server.handle_datagram(peer, &echo).unwrap().is_none());
|
||||
assert!(server.handle_datagram(other, &open).unwrap().is_some());
|
||||
}
|
||||
|
||||
// Zero allowance and zero idle lifetime expose the boundary behavior directly,
|
||||
// without scripting clock reads or inspecting session/rate representations.
|
||||
#[test]
|
||||
fn zero_burst_and_zero_idle_timeout_silently_refuse_echoes() {
|
||||
let peer: SocketAddr = "127.0.0.1:40000".parse().unwrap();
|
||||
for config in [
|
||||
ServerConfig::default().with_burst_allowance(0),
|
||||
ServerConfig::default().with_idle_timeout(Duration::ZERO),
|
||||
] {
|
||||
let mut server = ServerCore::new(config);
|
||||
let params = Params::with_protocol_defaults();
|
||||
let open = encode_request(
|
||||
RequestToEncode::Open {
|
||||
params: ¶ms,
|
||||
no_test: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let reply = server.handle_datagram(peer, &open).unwrap().unwrap();
|
||||
let opened = decode_open_reply(reply.bytes(), None).unwrap();
|
||||
let echo = encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: opened.token,
|
||||
sequence: 0,
|
||||
params: &opened.params,
|
||||
payload: &[],
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(server.handle_datagram(peer, &echo).unwrap().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_flow_labels_share_a_session_but_zones_do_not() {
|
||||
let endpoint = |flow, scope| {
|
||||
SocketAddr::V6(SocketAddrV6::new(
|
||||
"fe80::1234".parse().unwrap(),
|
||||
40000,
|
||||
flow,
|
||||
scope,
|
||||
))
|
||||
};
|
||||
let mut server =
|
||||
ServerCore::new(ServerConfig::default().with_min_send_interval(Duration::ZERO));
|
||||
let params = Params {
|
||||
received_stats: irtt_proto::ReceivedStats::Both,
|
||||
..Params::with_protocol_defaults()
|
||||
};
|
||||
let packet = encode_request(
|
||||
RequestToEncode::Open {
|
||||
params: ¶ms,
|
||||
no_test: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let reply = server
|
||||
.handle_datagram(endpoint(1, 2), &packet)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let opened = decode_open_reply(reply.bytes(), None).unwrap();
|
||||
let echo = encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: opened.token,
|
||||
sequence: 0,
|
||||
params: &opened.params,
|
||||
payload: &[],
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let close = encode_request(
|
||||
RequestToEncode::Close {
|
||||
token: opened.token,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(server
|
||||
.handle_datagram(endpoint(1, 3), &echo)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(server
|
||||
.handle_datagram(endpoint(1, 3), &close)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert_eq!(server.session_count(), 1);
|
||||
let reply = server
|
||||
.handle_datagram(endpoint(99, 2), &echo)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
decode_echo_reply(reply.bytes(), &opened.params, None)
|
||||
.unwrap()
|
||||
.recv_count,
|
||||
Some(1)
|
||||
);
|
||||
assert!(server
|
||||
.handle_datagram(endpoint(99, 2), &close)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert_eq!(server.session_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_authenticated_echo_does_not_advance_session_state() {
|
||||
let key = b"oversized-echo-test";
|
||||
let peer = "127.0.0.1:40000".parse().unwrap();
|
||||
let mut server = ServerCore::new(
|
||||
ServerConfig::default()
|
||||
.with_hmac_key(key)
|
||||
.with_max_packet_length(96)
|
||||
.with_burst_allowance(1)
|
||||
.with_min_send_interval(Duration::from_secs(60))
|
||||
.with_max_test_duration(Duration::from_millis(1))
|
||||
.with_idle_timeout(Duration::MAX),
|
||||
);
|
||||
let params = Params {
|
||||
length: 96,
|
||||
received_stats: irtt_proto::ReceivedStats::Both,
|
||||
..Params::with_protocol_defaults()
|
||||
};
|
||||
let open = encode_request(
|
||||
RequestToEncode::Open {
|
||||
params: ¶ms,
|
||||
no_test: false,
|
||||
},
|
||||
Some(key),
|
||||
)
|
||||
.unwrap();
|
||||
let reply = server.handle_datagram(peer, &open).unwrap().unwrap();
|
||||
let opened = decode_open_reply(reply.bytes(), Some(key)).unwrap();
|
||||
let oversized_params = Params {
|
||||
length: 128,
|
||||
..opened.params.clone()
|
||||
};
|
||||
let oversized = encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: opened.token,
|
||||
sequence: 63,
|
||||
params: &oversized_params,
|
||||
payload: &[],
|
||||
},
|
||||
Some(key),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(oversized.len() > 96);
|
||||
assert!(server.handle_datagram(peer, &oversized).unwrap().is_none());
|
||||
// If the oversized request started maximum duration, the first valid
|
||||
// echo would now close. Oversleep preserves this distinction.
|
||||
std::thread::sleep(Duration::from_millis(2001));
|
||||
let valid = encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: opened.token,
|
||||
sequence: 0,
|
||||
params: &opened.params,
|
||||
payload: &[],
|
||||
},
|
||||
Some(key),
|
||||
)
|
||||
.unwrap();
|
||||
let reply = server
|
||||
.handle_datagram(peer, &valid)
|
||||
.unwrap()
|
||||
.expect("oversized request must not consume allowance");
|
||||
let reply = decode_echo_reply(reply.bytes(), &opened.params, Some(key)).unwrap();
|
||||
assert_eq!(reply.flags & irtt_proto::FLAG_CLOSE, 0);
|
||||
assert_eq!(reply.recv_count, Some(1));
|
||||
assert_eq!(reply.recv_window, Some(1));
|
||||
assert_eq!(server.session_count(), 1);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
//! Observe the emitted IP byte, independently of protocol negotiation.
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use std::{io::IoSliceMut, net::UdpSocket as StdSocket, os::fd::AsRawFd, time::Duration};
|
||||
|
||||
use irtt_proto::{
|
||||
decode_echo_reply, decode_open_reply, encode_request, OpenReply, Params, RequestToEncode,
|
||||
FLAG_CLOSE,
|
||||
};
|
||||
use irtt_server::{Server, ServerConfig};
|
||||
use nix::sys::socket::{recvmsg, ControlMessageOwned, MsgFlags};
|
||||
use tokio::{net::UdpSocket, sync::oneshot, time::timeout};
|
||||
|
||||
async fn exchange(client: &UdpSocket, request: &[u8], expected: u8) -> Vec<u8> {
|
||||
client.send(request).await.unwrap();
|
||||
loop {
|
||||
client.readable().await.unwrap();
|
||||
let packet = client.try_io(tokio::io::Interest::READABLE, || {
|
||||
let mut bytes = [0; 2048];
|
||||
let mut control = nix::cmsg_space!(u8);
|
||||
let mut iov = [IoSliceMut::new(&mut bytes)];
|
||||
let msg = recvmsg::<()>(
|
||||
client.as_raw_fd(),
|
||||
&mut iov,
|
||||
Some(&mut control),
|
||||
MsgFlags::MSG_DONTWAIT,
|
||||
)?;
|
||||
let size = msg.bytes;
|
||||
let class = msg.cmsgs().unwrap().find_map(|message| match message {
|
||||
ControlMessageOwned::Ipv4Tos(class) => Some(class),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(class, Some(expected));
|
||||
Ok(bytes[..size].to_vec())
|
||||
});
|
||||
match packet {
|
||||
Ok(packet) => return packet,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
|
||||
Err(error) => panic!("receive failed: {error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn echo(opened: &OpenReply, sequence: u32) -> Vec<u8> {
|
||||
encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: opened.token,
|
||||
sequence,
|
||||
params: &opened.params,
|
||||
payload: &[],
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn every_open_echo_and_server_close_applies_its_own_raw_traffic_class() {
|
||||
timeout(Duration::from_secs(8), async {
|
||||
let maximum = Duration::from_millis(1);
|
||||
let mut server = Server::bind(
|
||||
"127.0.0.1:0".parse().unwrap(),
|
||||
ServerConfig::default()
|
||||
.with_min_send_interval(Duration::ZERO)
|
||||
.with_idle_timeout(Duration::MAX)
|
||||
.with_max_test_duration(maximum),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let addr = server.local_addr().unwrap();
|
||||
let (shutdown, stopped) = oneshot::channel();
|
||||
let task = tokio::spawn(async move {
|
||||
server
|
||||
.run(async {
|
||||
let _ = stopped.await;
|
||||
})
|
||||
.await
|
||||
});
|
||||
let socket = StdSocket::bind("127.0.0.1:0").unwrap();
|
||||
socket2::SockRef::from(&socket)
|
||||
.set_recv_tos_v4(true)
|
||||
.unwrap();
|
||||
socket.set_nonblocking(true).unwrap();
|
||||
let client = UdpSocket::from_std(socket).unwrap();
|
||||
client.connect(addr).await.unwrap();
|
||||
let mut sessions = Vec::new();
|
||||
// Each new OPEN follows a prior ECHO. The first raw byte also includes
|
||||
// ECN bits: neither a codepoint shift nor masking would preserve it.
|
||||
for (requested, class) in [(185, 185), (32, 32), (0, 0), (256, 0), (-1, 0)] {
|
||||
let params = Params {
|
||||
dscp: requested,
|
||||
..Params::with_protocol_defaults()
|
||||
};
|
||||
let request = encode_request(
|
||||
RequestToEncode::Open {
|
||||
params: ¶ms,
|
||||
no_test: false,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let packet = exchange(&client, &request, 0).await;
|
||||
let opened = decode_open_reply(&packet, None).unwrap();
|
||||
assert_eq!(opened.params.dscp, requested);
|
||||
let packet = exchange(&client, &echo(&opened, 0), class).await;
|
||||
assert_eq!(
|
||||
decode_echo_reply(&packet, &opened.params, None)
|
||||
.unwrap()
|
||||
.flags
|
||||
& FLAG_CLOSE,
|
||||
0
|
||||
);
|
||||
sessions.push((opened, class));
|
||||
}
|
||||
// Every session has now served its first echo. Waiting past all duration
|
||||
// deadlines is safe under oversleep; idle expiry cannot intervene.
|
||||
tokio::time::sleep(maximum + Duration::from_secs(2)).await;
|
||||
for (opened, class) in sessions {
|
||||
let packet = exchange(&client, &echo(&opened, 1), class).await;
|
||||
assert_ne!(
|
||||
decode_echo_reply(&packet, &opened.params, None)
|
||||
.unwrap()
|
||||
.flags
|
||||
& FLAG_CLOSE,
|
||||
0
|
||||
);
|
||||
}
|
||||
shutdown.send(()).unwrap();
|
||||
task.await.unwrap().unwrap();
|
||||
})
|
||||
.await
|
||||
.expect("marked runtime exchanges did not complete");
|
||||
}
|
||||
Reference in new issue
Block a user