irtt-client 0.6.0

IRTT-compatible Rust client library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
use std::{
    net::SocketAddr,
    time::{Duration, Instant},
};

use crate::{session::NegotiationResult, timing::ClientTimestamp};

/// Successful negotiation with a live session ready for echo probes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionStarted {
    /// Resolved remote socket address.
    pub remote: SocketAddr,
    /// Session token used on Echo and Close packets.
    pub token: u64,
    pub negotiation: NegotiationResult,
    /// Client timestamp when the Open reply was accepted.
    pub at: ClientTimestamp,
}

/// Successful negotiation-only exchange with no live probe session or token.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoTestCompleted {
    pub remote: SocketAddr,
    pub negotiation: NegotiationResult,
    /// Client timestamp when the Open reply was accepted.
    pub at: ClientTimestamp,
}

/// Result of the IRTT Open exchange, before any echo probes are driven.
///
/// Managed sessions publish the same payload as a [`ClientEvent`] after Open.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OpenOutcome {
    Started(SessionStarted),
    NoTestCompleted(NoTestCompleted),
}

/// 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(SessionStarted),
    /// A no-test exchange completed without creating a live session.
    ///
    /// No `EchoSent`, `EchoReply`, `EchoLoss`, or `SessionClosed` events are
    /// expected for this outcome.
    NoTestCompleted(NoTestCompleted),
    /// 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>,
}