429 lines
18 KiB
Rust
429 lines
18 KiB
Rust
use std::{
|
|
net::SocketAddr,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use crate::{session::NegotiatedParams, timing::ClientTimestamp};
|
|
|
|
/// Result of the IRTT open exchange.
|
|
///
|
|
/// The lower-level [`Client`](crate::Client) API returns this before any echo
|
|
/// probes are driven. Managed sessions publish the contained lifecycle event to
|
|
/// subscribers immediately after a successful open.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum OpenOutcome {
|
|
/// A normal probe session was opened.
|
|
Started {
|
|
/// Resolved remote socket address used by the UDP socket.
|
|
remote: SocketAddr,
|
|
/// Session token assigned by the server and used on echo/close packets.
|
|
token: u64,
|
|
/// Parameters returned by the server after negotiation.
|
|
negotiated: NegotiatedParams,
|
|
/// Lifecycle event corresponding to this outcome.
|
|
event: ClientEvent,
|
|
},
|
|
/// A no-test open exchange completed without starting a probe session.
|
|
NoTestCompleted {
|
|
/// Resolved remote socket address used by the UDP socket.
|
|
remote: SocketAddr,
|
|
/// Parameters returned by the server after negotiation.
|
|
negotiated: NegotiatedParams,
|
|
/// Lifecycle event corresponding to this outcome.
|
|
event: ClientEvent,
|
|
},
|
|
}
|
|
|
|
/// Event emitted by an IRTT client session.
|
|
///
|
|
/// Events form the public lifecycle and measurement stream for callers. UIs
|
|
/// usually display lifecycle, warning, and per-packet events directly; summary
|
|
/// reporting should aggregate `EchoSent`, `EchoReply`, `EchoLoss`,
|
|
/// `LateReply`, and `DuplicateReply` events or pass the stream to
|
|
/// `irtt-stats`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum ClientEvent {
|
|
/// A normal session has opened and is ready to send echo probes.
|
|
SessionStarted {
|
|
/// Resolved remote socket address.
|
|
remote: SocketAddr,
|
|
/// Session token assigned by the server.
|
|
token: u64,
|
|
/// Negotiated protocol parameters for this session.
|
|
negotiated: NegotiatedParams,
|
|
/// Client timestamp when the open reply was accepted.
|
|
at: ClientTimestamp,
|
|
},
|
|
/// A negotiation-only no-test exchange completed.
|
|
///
|
|
/// No `EchoSent`, `EchoReply`, `EchoLoss`, or `SessionClosed` events are
|
|
/// expected for this open outcome.
|
|
NoTestCompleted {
|
|
/// Resolved remote socket address.
|
|
remote: SocketAddr,
|
|
/// Negotiated protocol parameters returned by the server.
|
|
negotiated: NegotiatedParams,
|
|
/// Client timestamp when the open reply was accepted.
|
|
at: ClientTimestamp,
|
|
},
|
|
/// The session closed locally or through an authenticated peer close.
|
|
///
|
|
/// For a peer close-flagged echo reply, the reply's measurement or
|
|
/// classification event is emitted immediately before this event.
|
|
/// Managed sessions disconnect event subscribers after the worker exits,
|
|
/// while leaving already queued events available to drain.
|
|
SessionClosed {
|
|
/// Resolved remote socket address.
|
|
remote: SocketAddr,
|
|
/// Session token that was closed.
|
|
token: u64,
|
|
/// Client timestamp when the close request was sent.
|
|
at: ClientTimestamp,
|
|
},
|
|
|
|
/// An echo request was sent.
|
|
///
|
|
/// This records local scheduling and send-call timing. It does not imply a
|
|
/// reply will arrive; callers should pair it with `EchoReply`, `EchoLoss`,
|
|
/// `LateReply`, or `DuplicateReply` by sequence number when displaying
|
|
/// packet lifecycle state.
|
|
EchoSent {
|
|
/// Wire sequence number sent in the echo request.
|
|
seq: u32,
|
|
/// Resolved remote socket address.
|
|
remote: SocketAddr,
|
|
/// Intended send deadline, supplied by the managed runner.
|
|
/// Low-level adapters leave this absent.
|
|
scheduled_at: Option<Instant>,
|
|
/// Paired client wall/monotonic timestamp captured immediately after
|
|
/// the successful UDP send completed. This is the RTT and userspace
|
|
/// upstream one-way-delay fallback endpoint. The operational probe
|
|
/// timeout deadline is computed from an earlier, private, pre-send
|
|
/// timestamp instead — see the client crate's `AGENTS.md`.
|
|
sent_at: ClientTimestamp,
|
|
/// Number of bytes passed to the UDP socket.
|
|
bytes: usize,
|
|
/// Elapsed time spent in the socket send call, excluding protocol and
|
|
/// scheduler commit work.
|
|
send_call: Duration,
|
|
/// Absolute difference between the scheduled send time and the
|
|
/// post-send `sent_at` timestamp above; absent for low-level sends.
|
|
timer_error: Option<Duration>,
|
|
},
|
|
|
|
/// First in-window reply for a pending echo request.
|
|
///
|
|
/// This is the primary successful measurement event. RTT and optional
|
|
/// server timing/statistics fields should generally be aggregated from this
|
|
/// variant.
|
|
EchoReply {
|
|
/// Wire sequence number from the echo reply.
|
|
seq: u32,
|
|
/// Resolved remote socket address.
|
|
remote: SocketAddr,
|
|
/// Client timestamp recorded immediately after the matching request
|
|
/// finished sending (see `ClientEvent::EchoSent::sent_at`).
|
|
sent_at: ClientTimestamp,
|
|
/// Client timestamp recorded when the reply was received.
|
|
received_at: ClientTimestamp,
|
|
/// Round-trip timing sample for this reply.
|
|
rtt: RttSample,
|
|
/// Optional server-reported timestamps and processing time.
|
|
server_timing: Option<ServerTiming>,
|
|
/// Optional one-way delay values derived from client/server wall clocks.
|
|
one_way: Option<OneWayDelaySample>,
|
|
/// Optional server-reported received-statistics sample.
|
|
received_stats: Option<ReceivedStatsSample>,
|
|
/// UDP datagram size received from the socket.
|
|
bytes: usize,
|
|
/// Receive-side metadata observed outside the IRTT wire payload.
|
|
packet_meta: PacketMeta,
|
|
},
|
|
|
|
/// A sent echo request exceeded the local probe timeout before a reply was
|
|
/// matched.
|
|
///
|
|
/// A later datagram for the same sequence may still arrive and will be
|
|
/// reported as [`ClientEvent::LateReply`] rather than changing this event.
|
|
EchoLoss {
|
|
/// Wire sequence number that timed out.
|
|
seq: u32,
|
|
/// Client timestamp recorded immediately after the request finished
|
|
/// sending (see `ClientEvent::EchoSent::sent_at`). The timeout
|
|
/// deadline below is anchored to an earlier, private, pre-send
|
|
/// timestamp, not to this one.
|
|
sent_at: ClientTimestamp,
|
|
/// Monotonic deadline at which the probe was declared lost.
|
|
timeout_at: std::time::Instant,
|
|
},
|
|
|
|
/// Additional reply for a sequence that was already completed.
|
|
///
|
|
/// Duplicates are not primary RTT samples. They are useful for diagnostics
|
|
/// and packet counters, but should not be aggregated as successful first
|
|
/// replies.
|
|
DuplicateReply {
|
|
/// Wire sequence number from the duplicate reply.
|
|
seq: u32,
|
|
/// Resolved remote socket address.
|
|
remote: SocketAddr,
|
|
/// Client timestamp recorded when the duplicate was received.
|
|
received_at: ClientTimestamp,
|
|
/// UDP datagram size received from the socket.
|
|
bytes: usize,
|
|
},
|
|
|
|
/// Reply for a sequence that was no longer pending.
|
|
///
|
|
/// This commonly means the probe had already been reported as lost, or the
|
|
/// reply arrived out of the client's current tracking window. When the
|
|
/// original send metadata is still retained, RTT and one-way timing are
|
|
/// included; otherwise those fields are `None`.
|
|
LateReply {
|
|
/// Wire sequence number from the late reply.
|
|
seq: u32,
|
|
/// Highest sequence number accepted as an in-window first reply so far.
|
|
highest_seen: u32,
|
|
/// Resolved remote socket address.
|
|
remote: SocketAddr,
|
|
/// Original post-send client timestamp, when still retained (see
|
|
/// `ClientEvent::EchoSent::sent_at`).
|
|
sent_at: Option<ClientTimestamp>,
|
|
/// Client timestamp recorded when the late reply was received.
|
|
received_at: ClientTimestamp,
|
|
/// RTT sample computed when the original send timestamp is available.
|
|
rtt: Option<RttSample>,
|
|
/// Optional server-reported timestamps and processing time.
|
|
server_timing: Option<ServerTiming>,
|
|
/// Optional one-way delay values derived from client/server wall clocks.
|
|
one_way: Option<OneWayDelaySample>,
|
|
/// Optional server-reported received-statistics sample.
|
|
received_stats: Option<ReceivedStatsSample>,
|
|
/// UDP datagram size received from the socket.
|
|
bytes: usize,
|
|
/// Receive-side metadata observed outside the IRTT wire payload.
|
|
packet_meta: PacketMeta,
|
|
},
|
|
|
|
/// Non-fatal condition observed while receiving or classifying packets.
|
|
///
|
|
/// Warning events are diagnostic. They do not close the session by
|
|
/// themselves and are not measurement samples.
|
|
Warning {
|
|
/// Machine-readable warning category.
|
|
kind: WarningKind,
|
|
/// Human-readable diagnostic text.
|
|
message: String,
|
|
/// Client timestamp when the warning was emitted.
|
|
at: ClientTimestamp,
|
|
},
|
|
}
|
|
|
|
/// Category for a non-fatal [`ClientEvent::Warning`].
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[non_exhaustive]
|
|
pub enum WarningKind {
|
|
/// A received datagram could not be decoded as a relevant IRTT packet.
|
|
MalformedOrUnrelatedPacket,
|
|
/// A decoded packet used a token that does not match the open session.
|
|
WrongToken,
|
|
/// A decoded reply could not be matched to tracked probe state.
|
|
UntrackedReply,
|
|
}
|
|
|
|
/// Round-trip timing for one echo reply.
|
|
///
|
|
/// `raw` is the client-observed elapsed time from sending the echo request to
|
|
/// receiving the reply and is therefore non-negative. When the server reports
|
|
/// enough monotonic timing to compute processing time, `adjusted` subtracts
|
|
/// that server processing from `raw`. `effective` is the value used by reporting
|
|
/// and statistics: adjusted when available, otherwise raw converted to a signed
|
|
/// value.
|
|
///
|
|
/// Adjusted and effective RTT values are signed. They may be negative if the
|
|
/// reported server processing time is greater than the raw client-observed RTT.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct RttSample {
|
|
/// Client-observed RTT from send to receive.
|
|
pub raw: Duration,
|
|
/// Signed RTT adjusted for server processing when server processing is available.
|
|
pub adjusted: Option<SignedDuration>,
|
|
/// Signed effective RTT used by stats and CLI reporting.
|
|
///
|
|
/// This is `adjusted` when server processing is available, otherwise `raw`
|
|
/// converted to a signed duration.
|
|
pub effective: SignedDuration,
|
|
}
|
|
|
|
/// Signed nanosecond-backed duration used for derived timing values.
|
|
///
|
|
/// This type preserves negative values that can arise after subtracting server
|
|
/// processing time or comparing wall-clock timestamps from different hosts. It
|
|
/// should not be used to represent raw elapsed wall-clock or monotonic time,
|
|
/// which is non-negative and represented by [`Duration`].
|
|
///
|
|
/// # Example
|
|
///
|
|
/// Signed timing values retain a negative result instead of clamping clock
|
|
/// skew or server processing adjustments to zero:
|
|
///
|
|
/// ```
|
|
/// use irtt_client::SignedDuration;
|
|
///
|
|
/// let adjusted_rtt = SignedDuration::from_nanos(-250_000);
|
|
/// assert!(adjusted_rtt.is_negative());
|
|
/// assert_eq!(adjusted_rtt.as_micros(), -250);
|
|
/// ```
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct SignedDuration {
|
|
ns: i128,
|
|
}
|
|
|
|
impl SignedDuration {
|
|
/// Construct a signed duration from nanoseconds.
|
|
pub const fn from_nanos(ns: i128) -> Self {
|
|
Self { ns }
|
|
}
|
|
|
|
/// Convert a non-negative [`Duration`] into a signed duration.
|
|
///
|
|
/// Durations larger than `i128::MAX` nanoseconds saturate to `i128::MAX`.
|
|
pub fn from_duration(duration: Duration) -> Self {
|
|
Self {
|
|
ns: i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX),
|
|
}
|
|
}
|
|
|
|
/// Return the duration in nanoseconds.
|
|
pub const fn as_nanos(self) -> i128 {
|
|
self.ns
|
|
}
|
|
|
|
/// Return the duration in whole microseconds, truncated toward zero.
|
|
pub const fn as_micros(self) -> i128 {
|
|
self.ns / 1_000
|
|
}
|
|
|
|
/// Return the duration in whole milliseconds, truncated toward zero.
|
|
pub const fn as_millis(self) -> i128 {
|
|
self.ns / 1_000_000
|
|
}
|
|
|
|
/// Return whether this duration is less than zero.
|
|
pub const fn is_negative(self) -> bool {
|
|
self.ns < 0
|
|
}
|
|
}
|
|
|
|
impl From<Duration> for SignedDuration {
|
|
fn from(value: Duration) -> Self {
|
|
Self::from_duration(value)
|
|
}
|
|
}
|
|
|
|
/// Server-reported timing fields decoded from an echo reply.
|
|
///
|
|
/// These values are optional because the negotiated timestamp mode, clock mode,
|
|
/// server implementation, or packet contents may omit individual fields.
|
|
/// Timestamp values are nanoseconds in the server's reported wall-clock or
|
|
/// monotonic clock domain. `None` means unavailable; `Some(0)` means the server
|
|
/// reported a zero timestamp.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ServerTiming {
|
|
/// Server wall-clock receive timestamp, in nanoseconds.
|
|
pub receive_wall_ns: Option<i64>,
|
|
/// Server monotonic receive timestamp, in nanoseconds.
|
|
pub receive_mono_ns: Option<i64>,
|
|
/// Server wall-clock send timestamp, in nanoseconds.
|
|
pub send_wall_ns: Option<i64>,
|
|
/// Server monotonic send timestamp, in nanoseconds.
|
|
pub send_mono_ns: Option<i64>,
|
|
/// Server wall-clock midpoint timestamp, in nanoseconds.
|
|
pub midpoint_wall_ns: Option<i64>,
|
|
/// Server monotonic midpoint timestamp, in nanoseconds.
|
|
pub midpoint_mono_ns: Option<i64>,
|
|
/// Server processing time computed from monotonic receive/send timestamps.
|
|
///
|
|
/// `None` means the required monotonic timestamps were unavailable or
|
|
/// unusable. `Some(Duration::ZERO)` means the computed processing interval
|
|
/// was available and zero.
|
|
pub processing: Option<Duration>,
|
|
}
|
|
|
|
/// Directional one-way delay samples computed from wall-clock timestamps.
|
|
///
|
|
/// Each directional field is an `Option<SignedDuration>`. `None` means the
|
|
/// required client or server wall-clock timestamp was unavailable, or the
|
|
/// timestamp arithmetic was out of range. `Some(value)` means the delay was
|
|
/// computed from available timestamps.
|
|
///
|
|
/// Negative computed values are preserved as `Some` instead of being clamped to
|
|
/// zero or dropped. A negative one-way delay usually indicates wall-clock skew
|
|
/// between the client and server.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct OneWayDelaySample {
|
|
/// Client-to-server delay.
|
|
///
|
|
/// This is computed from the client send wall time and the server receive
|
|
/// wall time when both are available. The client send endpoint is the
|
|
/// correlated Linux kernel TX timestamp for this send when one is
|
|
/// available and plausible for this send, and the userspace post-send wall
|
|
/// time otherwise. The server receive endpoint is whatever wall time the
|
|
/// peer reported; against this crate's own Tokio server runtime on Linux
|
|
/// (the `irtt-server` applet), that is its kernel receive timestamp when
|
|
/// one is available and plausible, and its userspace receive wall time
|
|
/// otherwise. Against another IRTT-compatible peer, or an application
|
|
/// driving `irtt_server::ServerCore::handle_datagram` directly, the
|
|
/// client has no way to know which clock source produced it — the public
|
|
/// `ServerCore` API never supplies a kernel timestamp of its own accord.
|
|
/// `RttSample::raw` is unaffected and remains a userspace
|
|
/// monotonic measurement, but `adjusted` and `effective` can still shift:
|
|
/// when the session negotiated wall-clock timing rather than monotonic,
|
|
/// server processing time is derived from this same server receive
|
|
/// endpoint.
|
|
pub client_to_server: Option<SignedDuration>,
|
|
/// Server-to-client delay.
|
|
///
|
|
/// This is computed from the server send wall time and the client receive
|
|
/// wall time when both are available. The client receive endpoint is the
|
|
/// kernel receive timestamp reported with the datagram when one is
|
|
/// available and plausible for this receive, and the userspace receive wall
|
|
/// time otherwise. RTT is unaffected and remains a userspace monotonic
|
|
/// measurement.
|
|
pub server_to_client: Option<SignedDuration>,
|
|
}
|
|
|
|
/// Server-reported receive counters decoded from an echo reply.
|
|
///
|
|
/// These fields are optional because the negotiated received-statistics mode or
|
|
/// server implementation may omit them. `None` means unavailable; `Some(0)`
|
|
/// means the server reported the value and it was zero.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ReceivedStatsSample {
|
|
/// Server-reported count of received packets, when available.
|
|
pub count: Option<u32>,
|
|
/// Raw server-reported receive window bitmap, when available.
|
|
///
|
|
/// The bitmap is preserved exactly as it appeared on the wire; the client
|
|
/// does not interpret its bits.
|
|
pub window: Option<u64>,
|
|
}
|
|
|
|
/// 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<u8>,
|
|
pub dscp: Option<u8>,
|
|
pub ecn: Option<u8>,
|
|
/// Kernel software receive timestamp reported with the datagram, where the
|
|
/// platform provides one.
|
|
///
|
|
/// This is the raw observed value, reported whether or not it was accepted
|
|
/// as the downstream one-way delay endpoint. It is a kernel software
|
|
/// timestamp, not a NIC hardware ingress time.
|
|
pub kernel_rx_timestamp: Option<std::time::SystemTime>,
|
|
}
|