From 25d5c1db1c3604b6694f3034d5453ac10e83617a Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 7 May 2026 17:15:08 +0700 Subject: [PATCH] feat(client): add receive metadata scaffold --- crates/irtt-client/src/client.rs | 13 +-- .../src/client/tests/probes_replies.rs | 5 ++ crates/irtt-client/src/event.rs | 19 ++++ crates/irtt-client/src/lib.rs | 1 + crates/irtt-client/src/metadata.rs | 90 +++++++++++++++++++ 5 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 crates/irtt-client/src/metadata.rs diff --git a/crates/irtt-client/src/client.rs b/crates/irtt-client/src/client.rs index c914ac9..23d7ec7 100644 --- a/crates/irtt-client/src/client.rs +++ b/crates/irtt-client/src/client.rs @@ -14,9 +14,10 @@ use crate::{ config::{ClientConfig, RecvBudget, RunMode, MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES}, error::ClientError, event::{ - ClientEvent, OneWayDelaySample, OpenOutcome, PacketMeta, ReceivedStatsSample, RttSample, - ServerTiming, SignedDuration, WarningKind, + ClientEvent, OneWayDelaySample, OpenOutcome, ReceivedStatsSample, RttSample, ServerTiming, + SignedDuration, WarningKind, }, + metadata::ReceiveMeta, probe::{CompletedSet, PendingMap, PendingProbe, TimedOutMap}, session::{validate_negotiated_params, ActiveSession, ClientPhase, NegotiatedParams}, socket::{connect_udp_socket, resolve_remote, validate_open_timeouts}, @@ -431,7 +432,7 @@ impl Client { one_way, received_stats, bytes: packet.len(), - packet_meta: PacketMeta::default(), + packet_meta: ReceiveMeta::default().into(), }); } else { events.push(ClientEvent::EchoReply { @@ -445,7 +446,7 @@ impl Client { one_way, received_stats, bytes: packet.len(), - packet_meta: PacketMeta::default(), + packet_meta: ReceiveMeta::default().into(), }); } Ok(events) @@ -478,7 +479,7 @@ impl Client { one_way, received_stats, bytes: packet.len(), - packet_meta: PacketMeta::default(), + packet_meta: ReceiveMeta::default().into(), }]) } else if session.highest_received_seq.is_some_and(|h| wire_seq < h) { // TODO: u32 ordering is acceptable for finite tests but continuous @@ -495,7 +496,7 @@ impl Client { one_way: None, received_stats: build_received_stats(&reply), bytes: packet.len(), - packet_meta: PacketMeta::default(), + packet_meta: ReceiveMeta::default().into(), }]) } else { Ok(vec![ClientEvent::Warning { diff --git a/crates/irtt-client/src/client/tests/probes_replies.rs b/crates/irtt-client/src/client/tests/probes_replies.rs index ef58c6c..ea1b7af 100644 --- a/crates/irtt-client/src/client/tests/probes_replies.rs +++ b/crates/irtt-client/src/client/tests/probes_replies.rs @@ -284,6 +284,7 @@ fn recv_once_decodes_echo_reply_and_emits_event() { received_stats, server_timing, bytes, + packet_meta, .. } => { assert_eq!(*seq, 0); @@ -298,6 +299,10 @@ fn recv_once_decodes_echo_reply_and_emits_event() { assert!(server_timing.is_some()); let st = server_timing.as_ref().unwrap(); assert!(st.processing.is_some()); + assert_eq!(packet_meta.traffic_class, None); + assert_eq!(packet_meta.dscp, None); + assert_eq!(packet_meta.ecn, None); + assert_eq!(packet_meta.kernel_rx_timestamp, None); } other => panic!("expected EchoReply, got {other:?}"), } diff --git a/crates/irtt-client/src/event.rs b/crates/irtt-client/src/event.rs index 8f43da3..4d361de 100644 --- a/crates/irtt-client/src/event.rs +++ b/crates/irtt-client/src/event.rs @@ -143,6 +143,10 @@ pub struct ReceivedStatsSample { pub window: Option, } +/// Receive-side packet metadata observed outside the IRTT wire protocol. +/// +/// `None` means the metadata was unavailable. `Some(0)` means the metadata was +/// observed and its value was zero. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct PacketMeta { pub traffic_class: Option, @@ -150,3 +154,18 @@ pub struct PacketMeta { pub ecn: Option, pub kernel_rx_timestamp: Option, } + +#[cfg(test)] +mod tests { + use super::PacketMeta; + + #[test] + fn packet_meta_default_is_unavailable() { + let meta = PacketMeta::default(); + + assert_eq!(meta.traffic_class, None); + assert_eq!(meta.dscp, None); + assert_eq!(meta.ecn, None); + assert_eq!(meta.kernel_rx_timestamp, None); + } +} diff --git a/crates/irtt-client/src/lib.rs b/crates/irtt-client/src/lib.rs index d1b44a6..98e6183 100644 --- a/crates/irtt-client/src/lib.rs +++ b/crates/irtt-client/src/lib.rs @@ -5,6 +5,7 @@ mod config; mod error; mod event; mod managed; +mod metadata; mod probe; mod session; mod socket; diff --git a/crates/irtt-client/src/metadata.rs b/crates/irtt-client/src/metadata.rs new file mode 100644 index 0000000..8d0d509 --- /dev/null +++ b/crates/irtt-client/src/metadata.rs @@ -0,0 +1,90 @@ +use std::time::SystemTime; + +use crate::event::PacketMeta; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) struct ReceiveMeta { + pub(crate) traffic_class: Option, + pub(crate) kernel_rx_timestamp: Option, +} + +impl From for PacketMeta { + fn from(meta: ReceiveMeta) -> Self { + Self { + traffic_class: meta.traffic_class, + dscp: meta.traffic_class.map(|traffic_class| traffic_class >> 2), + ecn: meta.traffic_class.map(|traffic_class| traffic_class & 0b11), + kernel_rx_timestamp: meta.kernel_rx_timestamp, + } + } +} + +#[cfg(test)] +mod tests { + use super::ReceiveMeta; + use crate::event::PacketMeta; + + #[test] + fn metadata_default_is_unavailable() { + let meta = ReceiveMeta::default(); + let packet_meta = PacketMeta::from(meta); + + assert_eq!(packet_meta.traffic_class, None); + assert_eq!(packet_meta.dscp, None); + assert_eq!(packet_meta.ecn, None); + assert_eq!(packet_meta.kernel_rx_timestamp, None); + } + + #[test] + fn metadata_observed_traffic_class_zero_preserves_observed_zero() { + let packet_meta = PacketMeta::from(ReceiveMeta { + traffic_class: Some(0), + kernel_rx_timestamp: None, + }); + + assert_eq!(packet_meta.traffic_class, Some(0)); + assert_eq!(packet_meta.dscp, Some(0)); + assert_eq!(packet_meta.ecn, Some(0)); + assert_eq!(packet_meta.kernel_rx_timestamp, None); + } + + #[test] + fn metadata_observed_traffic_class_derives_dscp_and_ecn() { + let packet_meta = PacketMeta::from(ReceiveMeta { + traffic_class: Some(184), + kernel_rx_timestamp: None, + }); + + assert_eq!(packet_meta.traffic_class, Some(184)); + assert_eq!(packet_meta.dscp, Some(46)); + assert_eq!(packet_meta.ecn, Some(0)); + assert_eq!(packet_meta.kernel_rx_timestamp, None); + + let packet_meta = PacketMeta::from(ReceiveMeta { + traffic_class: Some(186), + kernel_rx_timestamp: None, + }); + + assert_eq!(packet_meta.traffic_class, Some(186)); + assert_eq!(packet_meta.dscp, Some(46)); + assert_eq!(packet_meta.ecn, Some(2)); + assert_eq!(packet_meta.kernel_rx_timestamp, None); + } + + #[test] + fn metadata_mapping_preserves_none_vs_observed_zero() { + let unavailable = PacketMeta::from(ReceiveMeta::default()); + let observed_zero = PacketMeta::from(ReceiveMeta { + traffic_class: Some(0), + kernel_rx_timestamp: None, + }); + + assert_eq!(unavailable.traffic_class, None); + assert_eq!(unavailable.dscp, None); + assert_eq!(unavailable.ecn, None); + + assert_eq!(observed_zero.traffic_class, Some(0)); + assert_eq!(observed_zero.dscp, Some(0)); + assert_eq!(observed_zero.ecn, Some(0)); + } +}