test: protect durable behavior identified in review

This commit is contained in:
Nils Andreas Svee committed 2026-10-02 16:12:52 +07:00
1 parent e115b96f21
commit 83b3607228
7 files changed
+528 -6

No files matched your search

@@ -316,3 +316,42 @@ fn negotiated_timestamp_modes_emit_only_the_selected_fields_in_order() {
}
}
}
#[test]
fn duration_close_waits_for_rate_allowance_after_the_deadline() {
let refill = Duration::from_secs(4);
let maximum = Duration::from_millis(1);
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 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));
assert!(
echo(&mut server, &opened, 63).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);
}
+106
View File
@@ -155,3 +155,109 @@ async fn wildcard_construction_is_refused_without_reply_source_selection() {
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. A short queue delay selects kernel wall time; a delay beyond
// the documented one-second sanity bound falls back to userspace. 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, kernel_expected) in [
(StampAt::Both, Duration::from_millis(100), true),
(StampAt::Both, Duration::from_millis(1100), false),
(StampAt::Midpoint, Duration::from_millis(100), false),
] {
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: &params,
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 kernel_expected {
let receive = timestamps.recv_wall.unwrap();
assert!(
receive >= before_send && receive <= queued_by,
"receive wall must identify the kernel arrival before the queue delay"
);
let wall_elapsed = timestamps.send_wall.unwrap() - 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 {
let receive = timestamps.recv_wall.or(timestamps.midpoint_wall).unwrap();
assert!(
receive >= resumed_at,
"fallback and midpoint must use userspace time"
);
}
stop_tx.send(()).unwrap();
run.await.unwrap().unwrap();
}
}
+144 -1
View File
@@ -1,4 +1,7 @@
use std::{net::SocketAddr, time::Duration};
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};
@@ -116,3 +119,143 @@ fn zero_burst_and_zero_idle_timeout_silently_refuse_echoes() {
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: &params,
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: &params,
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);
}