diff --git a/crates/irtt-app/tests/cli.rs b/crates/irtt-app/tests/cli.rs index cdebaa6..494f9be 100644 --- a/crates/irtt-app/tests/cli.rs +++ b/crates/irtt-app/tests/cli.rs @@ -144,6 +144,36 @@ fn default_client_output_emits_a_packet_summary() { } } +#[cfg(feature = "client")] +#[test] +fn target_authentication_inherits_overrides_or_disables_the_global_key() { + use clap::Parser; + use irtt_app::cmd::client::args::ClientArgs; + let args = ClientArgs::try_parse_from([ + "irtt-client", + "--hmac", + "global-test-key", + "inherit=127.0.0.1:2112", + "override=127.0.0.1:2113@hmac=target-test-key", + "disable=127.0.0.1:2114@hmac=", + ]) + .unwrap(); + let setup = args.prepare().unwrap(); + assert_eq!( + setup.managed_config().client.hmac_key.as_deref(), + Some(b"global-test-key".as_slice()) + ); + let targets = setup.managed_targets(); + let authentication: Vec<_> = targets + .iter() + .map(|target| target.auth.as_ref().map(|auth| auth.hmac_key.as_deref())) + .collect(); + assert_eq!( + authentication, + [None, Some(Some(b"target-test-key".as_slice())), Some(None)] + ); +} + // A real authenticated server detects broken client argument-to-config mapping. #[cfg(feature = "client")] #[test] diff --git a/crates/irtt-client/src/session/machine.rs b/crates/irtt-client/src/session/machine.rs index b54f931..3c74dd0 100644 --- a/crates/irtt-client/src/session/machine.rs +++ b/crates/irtt-client/src/session/machine.rs @@ -1314,6 +1314,78 @@ mod tests { } } + // Kernel completion order relative to timeout cannot be forced reliably + // through a real socket. Drive both transitions through production methods + // and observe the selected endpoint on the emitted measurable LateReply. + #[test] + fn late_replies_use_tx_timestamps_attached_before_or_after_timeout() { + for attach_before_timeout in [true, false] { + let mut machine = opened(); + let anchor = ClientTimestamp { + mono: Instant::now(), + wall: UNIX_EPOCH + Duration::from_secs(10), + }; + let sent_at = ClientTimestamp { + mono: anchor.mono + Duration::from_millis(1), + wall: anchor.wall + Duration::from_millis(1), + }; + let kernel_tx = anchor.wall + Duration::from_micros(500); + let probe = machine.prepare_probe().unwrap(); + let preflight = machine.preflight_probe_commit(&probe).unwrap(); + let commit = machine.finalize_probe_commit(preflight, anchor).unwrap(); + let sent = machine.commit_probe_sent(commit, sent_at, probe.bytes.len()); + if attach_before_timeout { + machine.record_kernel_tx_timestamp(sent.seq, kernel_tx); + } + let deadline = machine.next_probe_timeout_deadline().unwrap(); + assert!(matches!( + machine.poll_timeouts_at(deadline).unwrap()[0], + ClientEvent::EchoLoss { seq: 0, .. } + )); + if !attach_before_timeout { + machine.record_kernel_tx_timestamp(sent.seq, kernel_tx); + } + let events = machine + .process_echo_reply( + EchoReply { + flags: FLAG_REPLY, + token: 1, + sequence: sent.seq, + recv_count: None, + recv_window: None, + timestamps: TimestampFields { + recv_wall: Some(12_000_000_000), + ..TimestampFields::default() + }, + payload: vec![], + }, + probe.bytes.len(), + ClientTimestamp { + mono: deadline + Duration::from_millis(1), + wall: anchor.wall + Duration::from_secs(10), + }, + ReceiveMeta::default(), + ) + .unwrap(); + let ClientEvent::LateReply { + sent_at: Some(reported_send), + server_timing: Some(timing), + one_way: Some(one_way), + .. + } = &events[0] + else { + panic!("expected measurable LateReply: {events:?}"); + }; + assert_eq!(*reported_send, sent_at); + let selected = i128::from(timing.receive_wall_ns.unwrap()) + - one_way.client_to_server.unwrap().as_nanos(); + assert_eq!( + selected, 10_000_500_000, + "kernel TX endpoint must survive timeout" + ); + } + } + // A recoverable send failure that consumes an OPT_ID, followed by a // plausible wrong-ID completion, cannot be induced reliably with public // sockets. Exercise that failure boundary directly, without a socket hook diff --git a/crates/irtt-client/tests/client/probes_replies/timeouts.rs b/crates/irtt-client/tests/client/probes_replies/timeouts.rs index 6d9a7f3..7e1a88c 100644 --- a/crates/irtt-client/tests/client/probes_replies/timeouts.rs +++ b/crates/irtt-client/tests/client/probes_replies/timeouts.rs @@ -1,5 +1,45 @@ use super::*; +#[test] +fn probe_timeout_overflow_transmits_no_echo() { + let params = default_params(); + let server = start_fake_server(move |socket, tx| { + socket + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let (_, peer) = recv_request(&socket, &tx); + socket + .send_to( + &open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None), + peer, + ) + .unwrap(); + // CLOSE is a completion barrier: any incorrectly transmitted ECHO + // precedes it on this connected loopback socket. + let _ = recv_request(&socket, &tx); + }); + let config = ClientConfig { + probe_timeout: Duration::MAX, + ..default_test_config(server.addr) + }; + let mut client = Client::connect(config).unwrap(); + assert_open_started(client.open().unwrap()); + assert!(matches!( + client.send_probe(), + Err(ClientError::DurationOverflow) + )); + assert!(!client.has_pending_probes()); + client.close().unwrap(); + let packets: Vec<_> = server.rx.iter().collect(); + assert_eq!(packets.len(), 2); + assert_eq!( + packets[1][3], + flags::FLAG_CLOSE, + "overflow must not transmit an ECHO" + ); + server.join(); +} + #[test] fn poll_timeouts_emits_echo_loss() { let params = default_params(); diff --git a/crates/irtt-client/tests/linux_transport.rs b/crates/irtt-client/tests/linux_transport.rs index 2aa9699..bf9de55 100644 --- a/crates/irtt-client/tests/linux_transport.rs +++ b/crates/irtt-client/tests/linux_transport.rs @@ -132,9 +132,14 @@ fn assert_kernel_send_endpoint(event: &ClientEvent, before_send: SystemTime) { "TX timestamp was not observed; userspace fallback is insufficient" ); assert!(selected <= ns(received_at.wall)); - assert!( - packet_meta.kernel_rx_timestamp.is_some(), - "Linux RX timestamp required" + let selected_receive = + i128::from(timing.send_wall_ns.unwrap()) + one_way.server_to_client.unwrap().as_nanos(); + assert_eq!( + selected_receive, + ns(packet_meta + .kernel_rx_timestamp + .expect("Linux RX timestamp required")), + "downstream OWD must use the kernel receive endpoint" ); assert_eq!(rtt.raw, received_at.mono.duration_since(sent_at.mono)); }