Record the send timestamp immediately before the socket operation to include local enqueue overhead in RTT measurements, providing more conservative latency data for consumers.
29 KiB
IRTT-RS Rust Design Draft
Status
This document is a Rust-side architecture and implementation design brief for an independent IRTT-compatible client suite.
It is intended to be consumed by a clean implementation-planning or implementation agent together with:
IRTT_CLIENT_PROTOCOL_SPEC.mdBLACKBOX_VERIFICATION_REPORT.md- clean packet test vectors
- clean-room process notes
This document describes Rust architecture and product goals. It is not a protocol specification and does not copy or depend on upstream source structure.
Core Product Goal
Build a layered, low-memory, event-producing IRTT-compatible Rust client suite.
The project is not primarily an upstream CLI clone. The primary design goal is a reusable client engine suitable for:
sqm-autorate-rustcake-autorateintegration through a CLI- router-friendly continuous latency probing
- normal user-facing finite IRTT-style tests
- optional aggregate statistics and reports
The central architectural rule is:
irtt-client emits probe events.
irtt-stats aggregates probe events.
irtt-cli formats probe events and/or statistics.
Aggregate statistics must not be welded into the client engine.
License
Preferred project license:
Apache-2.0
Optional alternative if broader Rust ecosystem convention is desired:
MIT OR Apache-2.0
The implementation agent must not inspect upstream GPL source code, upstream tests, upstream comments, contaminated notes, or contaminated transcripts.
Workspace Layout
Use a Cargo workspace with four crates:
irtt-rs/
Cargo.toml
crates/
irtt-proto/
irtt-client/
irtt-stats/
irtt-cli/
docs/
IRTT_CLIENT_PROTOCOL_SPEC.md
BLACKBOX_VERIFICATION_REPORT.md
CLEANROOM.md
RUST_DESIGN.md
test-vectors/
Crate Responsibilities
irtt-proto
Pure protocol crate.
Responsibilities
irtt-proto owns only binary/protocol logic:
- packet flags
- magic/version constants
- varint/zigzag parameter encoding and decoding
- parameter model
- packet layout calculation
- open request/reply encoding and decoding
- echo request/reply encoding and decoding
- close request encoding
- HMAC-MD5 calculation and verification
- clean packet vector tests
Must Not Contain
irtt-proto must not contain:
- sockets
- threads
- channels
- timers
- CLI code
- statistics aggregation
- session runtime control
- event fanout
- long-term sample retention
- OS-specific network metadata handling
Important Boundary
ECN, DSCP, TOS, Traffic Class, and kernel receive timestamps are socket/IP metadata. They are not IRTT payload fields. irtt-proto must not model ECN as a protocol field.
irtt-client
Client engine crate.
Responsibilities
irtt-client owns:
- connected UDP socket setup
- socket configuration through
socket2 - session negotiation
- open/no-test/active/close lifecycle
- caller-paced manual client API
- optional managed threaded runner
- probe sending
- reply receiving
- packet validation
- timestamp capture
- RTT sample construction
- duplicate detection
- late detection
- loss timeout detection
- public event types
- optional public event fanout for managed mode
Must Not Contain
irtt-client must not contain:
- aggregate statistics
- rolling windows
- median/stddev/variance
- human final reports
- CLI formatting
- mandatory full sample history
- upstream-style result monoliths
Retention Boundary
irtt-client must not retain samples for reporting or aggregation.
It may retain bounded in-flight operational state required for:
- matching replies to sent probes
- computing RTT for completed probes
- detecting duplicates
- detecting late replies
- detecting loss after timeout
- managing sequence/session state
Historical retention, rolling windows, finite-run summaries, and aggregate statistics belong in irtt-stats or in the consuming application.
irtt-stats
Optional statistics crate.
Responsibilities
irtt-stats consumes ClientEvent values and provides:
- cumulative statistics
- rolling time-window statistics
- rolling sample-count statistics
- finite-run summaries
- RTT summaries
- raw RTT summaries
- adjusted RTT summaries
- IPDV summaries
- server-processing summaries
- loss summaries
- duplicate/late summaries
- min/max/mean/median/stddev/variance where supported
Must Not Contain
irtt-stats must not contain:
- sockets
- packet encoding
- packet decoding
- session negotiation
- thread/runtime ownership
- CLI parsing
It is a consumer of events, not a driver of the client.
irtt-cli
User-facing executable crate.
Long-Term Role
irtt-cli should become the full user-facing client over time.
It should eventually support practical feature completeness for client usage:
- finite tests
- continuous streaming
- human output
- machine output
- statistics
- HMAC
- DSCP / Traffic Class
- timestamp modes
- received stats modes
- server fill
- no-test/check mode
- optional JSON output
Stats Relationship
irtt-cli should consume irtt-stats by default in normal builds.
The stats dependency is feature-gated for binary size and resource usage, not because stats are second-class.
Default/full CLI builds should include stats.
Minimal/router builds may disable stats to produce a smaller streaming-only binary.
[features]
default = ["stats"]
stats = ["dep:irtt-stats"]
json = ["dep:serde_json", "irtt-client/serde", "irtt-stats/serde"]
tracing = ["irtt-client/tracing"]
ancillary = ["irtt-client/ancillary"]
interop = []
Minimal build example:
cargo build -p irtt-cli --no-default-features --release
Normal build example:
cargo build -p irtt-cli --release
Layering Rule
irtt-cli must compose irtt-client and irtt-stats. It must not reimplement protocol, session, or statistics logic internally.
Public Client Layers
irtt-client should provide two public client layers:
ClientManagedClient
Client
Client is the manual, caller-paced client.
It owns:
- connected UDP socket
- socket configuration
- local and remote addresses
- session state
- negotiated parameters
- connection token
- sequence state
- bounded pending-probe state
It does not own:
- threads
- public event hub
- statistics aggregation
- CLI formatting
- long-term sample history
The consumer controls pacing and polling.
Example Shape
pub struct Client {
socket: ClientSocket,
state: SessionState,
}
impl Client {
pub fn connect(config: ClientConfig) -> Result<Self, ClientError>;
pub fn open(&mut self, now: ClientTimestamp) -> Result<Vec<ClientEvent>, ClientError>;
pub fn next_send_deadline(&self) -> Option<Instant>;
pub fn send_probe(&mut self, now: ClientTimestamp) -> Result<Vec<ClientEvent>, ClientError>;
pub fn recv_once(&mut self) -> Result<Option<Vec<ClientEvent>>, ClientError>;
pub fn recv_available(&mut self, budget: RecvBudget) -> Result<Vec<ClientEvent>, ClientError>;
pub fn poll_timeouts(&mut self, now: ClientTimestamp) -> Result<Vec<ClientEvent>, ClientError>;
pub fn close(&mut self, now: ClientTimestamp) -> Result<Vec<ClientEvent>, ClientError>;
}
The exact API may be adjusted, but the core principle is stable:
Client owns socket/session.
Caller owns pacing.
Manual pacing is required so consumers such as sqm-autorate-rust can integrate probing into their own control loop.
ManagedClient
ManagedClient is a convenience runner built on the same socket/session primitives.
It owns:
- runtime threads
- cancellation
- pacing loop
- receive loop
- coordinator/correlator
- event fanout
It is intended for:
irtt-cli- simple applications
- integration tests
- users who do not need manual control
Example Shape
pub struct ManagedClientSession {
// owns worker threads and event hub
}
impl ManagedClientSession {
pub fn subscribe(&self, config: SubscriberConfig) -> EventSubscription;
pub fn stop(&self);
pub fn join(self) -> Result<SessionOutcome, ClientError>;
}
pub struct ManagedClient;
impl ManagedClient {
pub fn start(config: ClientConfig) -> Result<ManagedClientSession, ClientError>;
}
Socket Ownership
The IRTT session uses one connected UDP socket for open, echo, and close traffic.
This socket is part of the client/session abstraction and should be owned by irtt-client, not by downstream consumers.
Manual pacing does not imply manual socket ownership.
ClientSocket
Use an internal socket wrapper named ClientSocket.
Do not use names like IrtSocket.
struct ClientSocket {
inner: std::net::UdpSocket,
}
ClientSocket is responsible for:
- creating the socket
- binding local address
- configuring socket options
- connecting UDP socket
- sending packets
- receiving packets
- cloning handles if required by managed mode
- returning optional packet metadata
socket2
Use socket2 for socket creation and configuration.
The MVP implementation may create/configure with socket2::Socket, then convert into std::net::UdpSocket internally.
socket2 is required because the standard library does not expose all socket options needed for DSCP/TOS/Traffic Class/TTL style behavior.
One Socket, Multiple Handles
Managed mode may clone the underlying socket handle for sender/receiver threads.
This is still one connected UDP socket at the OS/session level.
Received Packet Metadata
ECN and DSCP are IP-header metadata. They are not part of the IRTT payload wire format.
The client socket abstraction should expose optional packet metadata:
pub struct PacketMeta {
pub traffic_class: Option<u8>,
pub dscp: Option<Dscp>,
pub ecn: Option<Ecn>,
pub kernel_rx_timestamp: Option<SystemTime>,
}
Important Modeling Rule
The implementation must distinguish:
- raw Traffic Class / TOS byte
- DSCP bits
- ECN bits
It must not collapse these into a single ambiguous ecn field.
MVP
MVP may use normal UdpSocket::recv / recv_from behavior internally.
In this mode:
traffic_class = None
dscp = None
ecn = None
kernel_rx_timestamp = None
Future ancillary Feature
A future ancillary feature should use platform control-message APIs, such as recvmsg, to populate:
- raw received Traffic Class / TOS byte
- derived DSCP
- derived ECN
- optional kernel receive timestamp
Normal protocol compatibility must not depend on this feature.
Upstream Server ECN Mode
Upstream server ECN mode should be treated as an advanced compatibility feature. It appears to require a control-message socket path rather than a new IRTT payload field.
Before claiming compatibility with this mode, add dedicated black-box tests using irtt server --ecn.
DSCP / Traffic Class / ECN API
The Rust API should expose real DSCP and ECN concepts while also permitting raw compatibility.
pub struct Dscp(u8); // valid 0..=63
pub struct Ecn(u8); // valid 0..=3
pub enum TrafficClassConfig {
Dscp(Dscp),
DscpEcn { dscp: Dscp, ecn: Ecn },
Raw(u8),
}
CLI should distinguish:
--dscp 46 # real DSCP 46, raw byte 0xb8
--traffic-class 0xb8 # raw byte compatibility
--ecn ect0 # optional/future explicit ECN helper
The protocol parameter ultimately negotiates/sends a raw Traffic Class / TOS byte. The public Rust API should avoid misleading users into thinking every value is a pure 6-bit DSCP codepoint.
Runtime Model
Core Decision
Use a synchronous/threaded core for the managed runner.
Do not make Tokio or any async runtime required by the core crates.
Async integration may be added later as a wrapper or feature.
Manual Client
Manual Client owns no threads.
The caller decides when to:
- open
- send probes
- poll receive traffic
- poll timeouts
- close
This is the preferred integration layer for sqm-autorate-rust if it wants direct control.
Managed Runtime Topology
After negotiation, ManagedClient should use three runtime roles:
- coordinator/control thread
- sender/pacer thread
- receiver thread
Sender/Pacer Thread
The sender/pacer thread is timing-sensitive.
It must not perform:
- statistics aggregation
- public event fanout
- CLI formatting
- subscriber delivery
- long-running coordination work
- blocking operations unrelated to sending
It should only:
- maintain absolute send schedule
- prepare outgoing packets before deadline where possible
- send packets
- timestamp immediately before send call
- report compact sent records to coordinator
Receiver Thread
The receiver thread should:
- block on receive
- timestamp immediately after receive returns, before expensive parsing/validation
- parse/validate reply enough to create a compact reply record
- report compact reply records to coordinator
Coordinator Thread
The coordinator should:
- correlate sent and received records
- compute RTT samples
- manage pending probes
- handle loss timeouts
- detect duplicates/late replies
- emit public events through the event hub
- handle graceful shutdown and close
Event Distribution
flume channels are single-consumer queues, not broadcast channels.
Therefore ManagedClient must not expose a single shared Receiver<ClientEvent> as the only event API.
Use flume internally, but provide a small fanout/broadcast layer for public subscribers.
Event Hub
pub struct EventHub {
// internal list of subscriber queues
}
pub struct EventSubscription {
receiver: flume::Receiver<ClientEvent>,
id: SubscriberId,
}
Consumers subscribe independently:
let stats_rx = session.subscribe(SubscriberConfig::stats());
let sqm_rx = session.subscribe(SubscriberConfig::latest_only());
let cli_rx = session.subscribe(SubscriberConfig::cli());
Each subscriber receives its own copy of each event.
Subscriber Configuration
pub struct SubscriberConfig {
pub capacity: usize,
pub overflow: SubscriberOverflow,
pub event_filter: EventFilter,
}
pub enum SubscriberOverflow {
DropNewest,
DropOldest,
Block,
Disconnect,
}
Suggested profiles:
live control / sqm:
DropOldest, small bounded queue, keep freshest data
stats:
Block or Disconnect, because dropped events corrupt statistics
debug logging:
DropNewest
The event hub should track dropped events per subscriber where practical.
Public Events
Events should be compact and cheap to clone.
Do not include large packet buffers in normal public events.
Core Events
pub enum ClientEvent {
SessionStarted {
session_id: SessionId,
remote: SocketAddr,
negotiated: NegotiatedParams,
},
EchoReply {
session_id: SessionId,
seq: u64,
wire_seq: u32,
remote: SocketAddr,
sent_at: ClientTimestamp,
received_at: ClientTimestamp,
rtt: RttSample,
one_way: Option<OneWayDelaySample>,
server_timing: Option<ServerTiming>,
received_stats: Option<ReceivedStatsSample>,
packet_meta: PacketMeta,
},
EchoLoss {
session_id: SessionId,
seq: u64,
wire_seq: u32,
kind: LossKind,
},
DuplicateReply {
session_id: SessionId,
seq: u64,
wire_seq: u32,
},
LateReply {
session_id: SessionId,
seq: u64,
wire_seq: u32,
rtt: Option<RttSample>,
},
Warning {
session_id: Option<SessionId>,
warning: ClientWarning,
},
SessionEnded {
session_id: SessionId,
reason: SessionEndReason,
},
FatalError {
session_id: Option<SessionId>,
error: ClientErrorKind,
},
}
EchoSent should not be emitted by default. It may be available as an optional debug event.
Sequence Numbers
The wire protocol sequence number is 32-bit.
Public events should expose a logical u64 sequence number and optionally also the wire u32 sequence.
struct ProbeSeq {
logical: u64,
wire: u32,
}
This keeps continuous mode sane even if wire sequence wraps in very long sessions.
Run Modes
pub enum RunMode {
NoTest,
Duration(Duration),
Count(u64),
Continuous,
}
NoTest
Open and immediately close/complete without echo probes.
Used for:
- CLI check mode
- negotiation smoke tests
- interop tests
Duration
Finite duration-limited test.
Finite duration has an exclusive end. Expected packet count for finite duration is:
ceil(duration / interval)
Count
Finite packet-count test.
Useful for deterministic tests and simple manual checks.
Continuous
Continuous mode is first-class.
It must:
- emit events until stopped
- avoid full sample retention
- avoid preallocating finite result vectors
- keep memory bounded
- send close packet on graceful stop where possible
If the server permits unbounded or sufficiently long sessions, use a single long-lived session.
If the server negotiates a finite duration or closes the session, behavior is controlled by ReconnectPolicy.
pub enum ReconnectPolicy {
Never,
OnServerClose { delay: Duration },
Always { delay: Duration, max_attempts: Option<u32> },
}
Suggested defaults:
library default:
ReconnectPolicy::Never
CLI continuous default:
ReconnectPolicy::OnServerClose { delay: 1s }
Negotiation Policy
Do not use a confusing strict/loose boolean as the primary API.
Use:
pub enum NegotiationPolicy {
AcceptServerRestrictions,
RequireExact,
RequireWithin(LocalRequirements),
}
Suggested defaults:
irtt-client default:
AcceptServerRestrictions
interop tests:
RequireExact where useful
sqm/cake usage:
RequireWithin(...) when the caller has hard operational limits
The server is authoritative unless the caller explicitly requires exact or bounded negotiated values.
Timing Policy
Send Timestamp
Capture send timestamp immediately before the send call. This ensures the
measured RTT includes local send/enqueue overhead, which is the more
conservative direction for latency-control consumers.
Receive Timestamp
Capture receive timestamp immediately after receive returns, before expensive validation/parsing.
If kernel receive timestamps are available through an optional ancillary feature, expose them separately in PacketMeta or timestamp metadata.
Timer Scheduling
Managed sender uses absolute scheduling:
scheduled_time(seq) = session_start + seq * interval
Do not schedule by repeatedly doing:
next = now + interval
because that accumulates drift.
Timer Modes
pub enum TimerMode {
Normal,
Precise { spin_threshold: Duration },
}
Default:
TimerMode::Normal
Router mode must not busy-spin unless explicitly requested.
RTT Model
Do not collapse RTT into a single ambiguous value.
Expose raw RTT, server processing time, and adjusted RTT separately.
pub struct RttSample {
pub raw: Duration,
pub adjusted: Option<Duration>,
pub server_processing: Option<Duration>,
}
impl RttSample {
pub fn effective(&self) -> Duration {
self.adjusted.unwrap_or(self.raw)
}
}
Rules
raw RTT:
client_receive_mono - client_send_mono
server processing:
server_send - server_receive, if both relevant server timestamps are available
adjusted RTT:
raw - server_processing, only if server_processing <= raw
if server_processing > raw:
adjusted = None
emit warning
effective RTT falls back to raw
Warning type:
pub enum ClientWarning {
ServerProcessingExceedsRawRtt {
seq: u64,
raw: Duration,
server_processing: Duration,
},
}
Consumers decide whether to use raw, adjusted, or effective RTT.
Suggested defaults:
sqm-autorate-rust:
use rtt.effective()
simple CLI/rtt-us format:
print rtt.effective()
machine CLI format:
include effective, raw, and server-processing fields
stats:
provide effective by default, but expose raw/adjusted variants where useful
Loss Model
irtt-client may emit loss events based on timeout/loss policy.
pub enum LossDetection {
Disabled,
Timeout(Duration),
MultipleOfInterval(u32),
}
Suggested default for continuous mode:
LossDetection::MultipleOfInterval(3)
Loss kind:
pub enum LossKind {
Unknown,
Upstream,
Downstream,
}
Direction classification should use received count/window when available. If insufficient data exists, use Unknown.
Validation Policy
pub enum ValidationPolicy {
Strict,
DropMalformed,
DropUnrelatedAbortMalformedSession,
}
Suggested default:
Drop unrelated garbage.
Abort or fatal-error on packets that appear to belong to the active session but violate core protocol invariants.
Examples:
wrong magic / unrelated packet:
drop
bad HMAC / bad token / impossible sequence / malformed active-session packet:
abort or emit FatalError depending on policy
Cancellation and Outcome
No signal handling in irtt-client.
irtt-cli handles Ctrl-C and calls stop() / join().
pub struct CancellationToken {
// Arc<AtomicBool> internally
}
pub struct SessionOutcome {
pub end_reason: SessionEndReason,
pub packets_sent: u64,
pub replies_received: u64,
pub duplicates: u64,
pub late: u64,
pub malformed: u64,
}
For manual Client, the caller owns cancellation by simply not calling further send/poll methods and then calling close/drop as appropriate.
CLI Output
CLI output must support router-friendly, awk-friendly, line-oriented formats.
The design is inspired by classic ping-style machine output:
- one result/event per line
- fixed positional fields
- configurable delimiter
- optional timestamp
- separate human and machine output
- no JSON parser required for router usage
Formats
Support:
human
machine
simple
rtt-us
jsonl # optional feature
Machine Format
Default delimiter: tab.
Reply line:
reply<TAB>timestamp<TAB>remote<TAB>session_id<TAB>seq<TAB>rtt_us<TAB>raw_rtt_us<TAB>server_processing_us<TAB>down_us<TAB>up_us<TAB>ecn<TAB>status
Loss line:
loss<TAB>timestamp<TAB>remote<TAB>session_id<TAB>seq<TAB>loss_kind
Duplicate line:
duplicate<TAB>timestamp<TAB>remote<TAB>session_id<TAB>seq
Late line:
late<TAB>timestamp<TAB>remote<TAB>session_id<TAB>seq<TAB>rtt_us
Session line:
session<TAB>timestamp<TAB>remote<TAB>session_id<TAB>state<TAB>reason
Example:
reply 1714320000.123456 1.2.3.4:2112 1 42 8341 8420 79 - - - ok
loss 1714320000.323456 1.2.3.4:2112 1 43 unknown
Field meanings:
rtt_us:
effective RTT = adjusted if valid, else raw
raw_rtt_us:
raw client-measured RTT
server_processing_us:
server processing time or "-"
down_us/up_us:
one-way estimates if available, otherwise "-"
ecn:
received ECN if available, otherwise "-"
Simple Format
timestamp<TAB>seq<TAB>result
Example:
1714320000.123456 42 8341
1714320000.323456 43 loss
1714320000.523456 44 8120
RTT-Only Format
8341
loss
8120
This is intended for extremely simple shell consumption.
Delimiter
Support:
--delimiter tab
--delimiter ','
--delimiter ';'
--delimiter '|'
Internally, join fields. Do not mutate format strings.
Timestamp Format
Default timestamp format:
unix_seconds.microseconds
Example:
1714320000.123456
Avoid nanosecond epoch integers in awk-oriented output.
Header
Default: no header.
Optional:
--header
Machine-readable output formats must be stable. Fields must not be reordered between releases. New fields should be added only in a new named/versioned format or at the end when explicitly documented.
Dependencies
irtt-proto
[dependencies]
thiserror = "2"
hmac = "0.12"
md-5 = "0.10"
subtle = "2"
fastrand = "2"
serde = { version = "1", features = ["derive"], optional = true }
irtt-client
[dependencies]
irtt-proto = { path = "../irtt-proto" }
thiserror = "2"
socket2 = "0.5"
flume = "0.11"
fastrand = "2"
serde = { version = "1", features = ["derive"], optional = true }
tracing = { version = "0.1", optional = true }
Future optional dependency for ancillary receive metadata:
rustix = { version = "...", optional = true, features = ["net"] }
Exact ancillary dependency can be chosen later.
irtt-stats
[dependencies]
irtt-client = { path = "../irtt-client" }
thiserror = "2"
serde = { version = "1", features = ["derive"], optional = true }
irtt-cli
[dependencies]
irtt-client = { path = "../irtt-client" }
irtt-stats = { path = "../irtt-stats", optional = true }
clap = { version = "4", features = ["derive"] }
thiserror = "2"
ctrlc = "3"
serde_json = { version = "1", optional = true }
Testing Strategy
Pure Tests
Normal cargo test should run:
- varint encode/decode tests
- parameter encode/decode tests
- packet layout calculation tests
- open request encode tests
- open reply decode tests
- echo request encode tests
- echo reply decode tests
- close request encode tests
- HMAC calculation and verification tests
- clean packet vector tests
- stats unit tests
- CLI formatter unit tests
Interop Tests
Interop tests require irtt in PATH.
Local default:
cargo test
Interop:
cargo test --workspace --features interop
CI should run interop tests and provide upstream irtt in PATH.
Local development should not require irtt unless the interop feature is explicitly enabled.
tshark
tshark is diagnostic/manual only.
Do not make tshark a normal test or CI dependency.
Black-Box ECN Tests
Before claiming support for upstream server ECN mode, run dedicated black-box tests for:
- IPv4 behavior
- IPv6 behavior
- server
--ecn - outgoing Traffic Class values with ECN bits set
- received Traffic Class metadata
- logged ECN values
- behavior with and without ancillary receive support
Implementation Milestones
Milestone 0 — Workspace Skeleton
- create Cargo workspace
- create crates
- add license files
- add clean-room docs
- add design docs
- ensure no upstream source is present
Milestone 1 — irtt-proto
- flags
- constants
- varint/zigzag
- params
- packet layout
- open request/reply
- echo request/reply
- close request
- HMAC
- packet vectors passing
Milestone 2 — irtt-client::Client No-Test / Negotiation
ClientSocketwithsocket2- connected UDP socket
- open request
- open reply parse
- no-test mode
- close behavior
- basic interop with upstream server
Milestone 3 — Manual Client Finite Probe Events
- caller-paced send_probe
- recv_once / recv_available
- timeout polling
- EchoReply events
- RTT sample model
- duplicate/late detection
- finite count/duration smoke test
Milestone 4 — Minimal irtt-cli Stream Mode
- use manual or managed layer as appropriate
- machine/simple/rtt-us output
- delimiter support
- timestamp support
- no stats required for minimal build
Milestone 5 — Managed Runner
- coordinator/control thread
- sender/pacer thread
- receiver thread
- EventHub fanout
- cancellation
- stop/join
- continuous mode
Milestone 6 — irtt-stats
- consumes
ClientEvent - cumulative stats
- rolling stats
- finite summary
- min/max/mean/median/stddev/variance
- loss/duplicate/late summaries
Milestone 7 — Full/default CLI Stats Integration
- default build enables stats
- finite human report
- optional stats lines in stream mode
- minimal
--no-default-featuresbuild still works without stats
Milestone 8 — Compatibility Features
- HMAC interop
- all timestamp modes
- all received stats modes
- server fill
- DSCP / Traffic Class
- TTL / hop limit where applicable
Milestone 9 — Advanced Networking
- ancillary receive metadata feature
- incoming Traffic Class / DSCP / ECN
- optional kernel RX timestamps
- upstream server
--ecncompatibility tests
Known Open / Verification Items
These are not blockers for the initial architecture, but should be tracked:
- exact behavior of upstream server ECN mode
- IPv4 vs IPv6 ECN metadata behavior
- kernel RX timestamp portability
- server close during active test
- rare case where server processing time exceeds raw RTT
- exact handling of very long or unbounded continuous sessions
- ancillary implementation strategy on Linux/OpenWrt/macOS
Design should be defensive around these items and expose data to consumers rather than hiding ambiguity.
Implementation Agent Rules
The implementation agent must follow these rules:
- Do not inspect upstream source code.
- Do not inspect upstream tests.
- Do not inspect contaminated notes or transcripts.
- Implement from the clean protocol spec, black-box report, packet vectors, and this Rust design brief.
- Do not build an upstream CLI clone first.
- Keep
irtt-clientevent-first and stats-free. - Keep stats in
irtt-stats. - Keep CLI formatting in
irtt-cli. - Keep ECN/DSCP receive metadata out of
irtt-proto. - Keep managed runtime optional; expose manual
Clientfor caller-paced integration.
Final architecture mantra:
irtt-proto: wire protocol
irtt-client::Client: socket + session, caller-paced
irtt-client::ManagedClient: threaded convenience runner
irtt-stats: event aggregation
irtt-cli: user-facing formatting and reports