Adds a proptest-based model test that drives generated sequences of
probe sends, valid/duplicate/late/unknown/wrong-token replies, bounded
and unbounded timeout polling, peer/local close, and u32 wire-sequence
wraparound through a real SessionMachine and an independent reference
model, asserting the observable state and every emitted event match
after each operation. This combinatorial interleaving of an open
session's operations was previously untested; existing tests are all
narrow single-scenario cases.
Add proptest-based property tests for irtt-proto's varint and Params
codecs, covering the full u64/i64 domains for varint/zigzag round trips
and generated valid Params for semantic round trips, presence
omission rules, repeated-tag last-wins behavior, and unknown-tag
tolerance.
Installing cargo-fuzz under a nightly directory override made rustix's
build script (a cargo-fuzz dependency) gate on `rustc_attrs`, which this
runner's nightly rejects as reserved-for-rustc. Install nightly without
overriding the directory default so `cargo install cargo-fuzz` still
builds under stable; only `fuzz build`/`fuzz run` opt into `+nightly`.
Add isolated cargo-fuzz/libFuzzer infrastructure for irtt-proto, fuzzing
decode_request, decode_open_reply, decode_echo_reply, Params::decode(_with_presence),
and the varint codec against arbitrary and structurally-derived wire input.
Each target's core invariant is that arbitrary bytes must never panic; a
small clean-room seed corpus (generated from irtt-proto's own encoders) and
a bounded nightly smoke CI workflow are included. The fuzz project is its
own isolated cargo workspace and does not affect the root workspace, its
lockfile, or any published crate's dependency graph.
cargo-deny's licenses check requires a license expression even on an
unpublished crate; add the workspace's Apache-2.0 license so the check
passes rather than treating irtt-integration-tests as unlicensed.
irtt-client and irtt-server mutually dev-depended on each other for
cross-crate integration tests. That cycle blocks `cargo publish` for
either crate whenever the other doesn't already exist on the registry
at the required version, which is exactly the situation for a crate's
first-ever release (irtt-server has never been published before).
Introduce a new `irtt-integration-tests` crate (publish = false, no
library target) to own tests whose actual subject is the interaction
between two or more irtt-* production crates:
crate-local behavior -> crate's own tests/
cross-crate/system behavior -> irtt-integration-tests
Move every test that imports both irtt_client and irtt_server there:
the client-side support harness (hmac, large_packets, protocol_options,
fill_negotiation) and the server-side real-socket tests that drive a
real AsyncClient against a real Server (async_client, negotiation_policy,
fill_wire_bytes, traffic_class, wildcard_source). server_set.rs stays in
irtt-server/tests since it only uses irtt_proto as test machinery to
drive irtt-server directly, which is a normal one-directional test
dependency, not a peer relationship.
irtt-server's dev-dependency on irtt-client is dropped entirely now
that nothing under crates/irtt-server/tests needs it, which fully
breaks the cycle from that side.
irtt-client keeps a one-directional dev-dependency on irtt-server: a
handful of white-box unit tests in src/async_client/tests.rs assert on
private AsyncClient internals (socket, remote, test_hooks) while also
needing a real in-tree server for blocking/async equivalence checks, so
they can't move to an external crate without losing that access. Their
InTreeServer helper is duplicated locally rather than shared, matching
the existing precedent of irtt-cli's own tests/support/in_tree_server.rs.
Each workspace crate now owns its own version instead of the previous
lockstep scheme; irtt-proto, irtt-client, irtt-server, irtt-stats, and
irtt-cli all become 0.5.0 as the first independently-versioned release.
Workspace dependency requirements are bumped to match.
Add a root CHANGELOG.md with a real per-crate 0.5.0 section drawn from
archaeology of every merge since v0.4.0, not just path-filtered commits.
Move cargo-dist to a crate-qualified `irtt-cli/vX.Y.Z` tag namespace so
only irtt-cli (the one dist-able application) triggers a GitHub Release;
library crate tags (irtt-proto/irtt-client/irtt-server/irtt-stats) exist
purely to pin the source for a crates.io release and no longer risk
tripping the release workflow. Regenerate the renamed
irtt-cli-release.yml via `dist generate` and update the few other files
that referenced the old release.yml path by name. Also fix a stale
`features = ["full"]` in dist-workspace.toml left over from a feature-flag
refactor that dropped the "full" feature entirely, which was silently
breaking every dist build.
Rescope semver.yml's tag trigger to the same five crate-qualified tags
so semver checks keep running under the new convention.
- Error-queue drain: an interrupted MSG_ERRQUEUE read no longer ends
the bounded drain early as if the queue were empty (which could
strand a still-queued TX timestamp past the point its probe is
looked up and removed). It now reports ErrorQueueRecord::Ignored,
consuming one attempt of the drain's existing per-call work budget
and retrying the same slot instead.
- Fake-server recv_from test helpers (client_multi_target.rs and
tests/support/mod.rs) now keep one absolute deadline across
interruptions, re-applying only the remaining time on each retry
and restoring the configured timeout afterward, mirroring the
production blocking-receive fix these tests exist to guard rather
than restarting the full timeout on every interruption.
Real signal delivery on Linux (observed via x86_64-musl workspace
testing) can interrupt blocking and nonblocking recv/recvmsg calls
with EINTR. Several production and test receive paths treated that as
a fatal error rather than a transient interruption:
- Client::open's blocking receive loop and Client::recv_once/
recv_available's blocking receive returned ClientError::Socket on
Interrupted instead of retrying within their existing timeout
budget.
- AsyncClient::poll_open and poll_recv's nonblocking recvmsg path
(Linux ancillary) failed the same way instead of retrying through
the existing readiness/work-budget machinery.
- The server's ancillary receive loop could terminate the listener on
an interrupted nonblocking recvmsg.
- The client's MSG_ERRQUEUE TX-timestamp drain turned an interrupted
read of optional metadata into a fatal ClientError::Socket.
- Fake-server test helpers panicked or misclassified EINTR as a
timeout instead of retrying it.
Interrupted is now retried transparently everywhere a receive can
observe it, while preserving each path's existing timing contract:
- Blocking Open reuses its already-absolute per-attempt deadline, so
interruptions resume within the same attempt without retransmitting
or extending the deadline.
- recv_once/recv_available compute one absolute deadline per logical
receive (when a timeout is configured) so repeated interruptions
cannot extend it past the configured timeout, and restore the
socket's configured read timeout afterward so a later call in the
same recv_available budget is unaffected.
- AsyncClient's poll_open already bounds its retry loop with
OPEN_POLL_WORK_BUDGET; poll_recv gains an equivalent
RECV_POLL_WORK_BUDGET, since unlike WouldBlock, Interrupted does not
clear Tokio's readiness state, so an unbounded retry loop was
otherwise possible under sustained signal delivery.
- The server's receive loop retries Interrupted the same way it
already retries WouldBlock, yielding to the runtime after a bounded
number of consecutive interruptions for the same reason.
- The error-queue drain treats an interrupted read as no record
obtained this drain (deferring to the next natural drain point)
rather than a socket error, matching its existing EWOULDBLOCK
handling.
Deterministic regression tests use a small test-only injection hook
(mirroring existing send-injection test hooks) rather than real signal
timing, covering: Open completing despite interruptions with no
retransmit, Open's deadline not extending under repeated
interruptions, recv_once classifying a reply normally despite
interruptions, recv_once's timeout not extending under repeated
interruptions, and the AsyncClient equivalents for open and recv.
No protocol, session, or timeout-classification behavior changes; no
new dependencies; send-side EINTR handling is unchanged and out of
scope.
timeout_at is pre-send anchored (F-04), so it can precede the same
probe's post-send sent_at when probe_timeout is small relative to the
send-call duration. Rolling-window eviction assumes non-decreasing
event timestamps, so clamp the internal StatsEvent::Loss windowing
value without changing the public EchoLoss.timeout_at field. Also
correct an AGENTS.md claim that sent_at feeds the downstream one-way
delay, which it does not.
The post-send sent_at change does shift the upstream IPDV component's
endpoint, since ReplySample copies sent_at into client_send_mono/wall
for send_ipdv_ns. Document the shift instead of claiming IPDV is
unaffected.
Moves the client userspace probe sent_at measurement to immediately
after a successful UDP send, improving raw RTT and the userspace
upstream-OWD fallback by excluding the socket send-call interval and
the small amount of local work preceding it.
All fallible probe-commit preflight (timeout deadline checked_add,
capacity/collision checks) still runs before the send, from a private
pre-send anchor (SessionMachine::finalize_probe_commit). The public
sent_at is now supplied afterward to an infallible
SessionMachine::commit_probe_sent, so DurationOverflow still guarantees
nothing was transmitted and a WouldBlock try_send still commits
nothing.
The operational probe timeout deadline stays anchored to the pre-send
timestamp on purpose, not to the new post-send sent_at, preserving
existing timeout/loss timing. Linux kernel TX_SOFTWARE plausibility
keeps its own pre-send wall-clock lower bound
(PendingProbe::tx_not_before_wall) instead of the post-send sent_at,
since a legitimate TX_SOFTWARE timestamp can be generated while the
send syscall is still in flight and would otherwise be wrongly
rejected.
Also flags (without touching production code) that irtt-stats'
ReplySample::from_reply_parts feeds the public sent_at directly into
IpdvSample.client_send_mono/client_send_wall_ns, so the upstream/send
IPDV component naturally shifts along with the sent_at semantics
change; per scope this is reported rather than compensated for.
Upstream one-way delay now prefers the correlated Linux TX_SOFTWARE wall
timestamp retained by PendingProbe when it is locally plausible (sent_at
<= kernel_tx <= received_at, all on the client's own wall clock), falling
back to the userspace sent_at.wall sample otherwise. RTT, downstream OWD,
IPDV, and timeout/scheduling semantics are unchanged.
On Linux, best-effort upgrade the socket's SO_TIMESTAMPING flags to
TX_SOFTWARE | OPT_ID | OPT_TSONLY after a successful Open and before the
first Echo probe. Promote the #103 error-queue foundation to production:
MSG_ERRQUEUE is drained opportunistically with a fixed per-call budget at
several natural choke points (after a probe send, before processing a
reply, before evaluating timeouts), and each automatic OPT_ID timestamp is
correlated directly with wire_seq — safe because Linux rolls back its
speculative sk_tskey increment on every send failure, wire_seq and the
kernel counter start together at every Open, and a socket can never be
reopened for a second session. The observed kernel TX wall timestamp is
retained on PendingProbe (surviving pending -> timed-out retention) as
dormant metadata only; RTT, OWD, and IPDV are unchanged and continue to use
the userspace send timestamp exclusively. Capability failure is fully
non-fatal and RX timestamping is unaffected either way. Non-Linux and
non-ancillary builds are unchanged.
The MSG_ERRQUEUE control buffer was a plain [u8; N], which only
guarantees byte alignment. recvmsg's cmsgs() walks cmsghdr references
out of that storage and requires cmsghdr alignment, so this mirrors
the parent module's #[repr(align(8))] ControlBuffer instead of relying
on incidental stack alignment.
Migrate the Linux client's receive kernel timing from SO_TIMESTAMPNS to
SO_TIMESTAMPING with RX_SOFTWARE + SOFTWARE, reported via
SCM_TIMESTAMPING instead of SCM_TIMESTAMPNS. Linux documents fallback
quirks when SO_TIMESTAMP[NS] and SO_TIMESTAMPING are both enabled on a
socket, so this establishes one unambiguous software-timestamp facility
before client TX timestamp work builds on the same API.
ReceiveMeta.kernel_rx_timestamp semantics, PacketMeta propagation, and
all PR #100 downstream OWD/RTT/IPDV behavior are unchanged. The receive
control buffer is now sized deliberately from cmsg_space::<T>() instead
of an unexplained fixed constant.
Also add safe, test-only Linux primitives that a follow-up change needs
for client TX timestamping: SO_TIMESTAMPING TX flag construction,
nonblocking MSG_ERRQUEUE parsing, and extended-error classification
that distinguishes a genuine send-timestamp completion from a malformed
timestamp record and from a real socket/network error. Production
sockets do not enable SOF_TIMESTAMPING_TX_SOFTWARE and nothing drains
the error queue in production; these primitives only run under
#[cfg(test)].
An echo reply's receive wall timestamp now prefers the kernel arrival
reading the Linux listener captured, where that reading is plausible for
the datagram it describes. That removes socket-queue and runtime wakeup
latency from the upstream one-way delay a client computes, and it is the
only thing the observation is allowed to change.
The runtime reaches the core through a crate-private entry point taking
the optional observation as an explicit per-datagram argument; the public
ServerCore::handle_datagram is unchanged in signature and in behavior, and
both run one implementation. There is no "next receive timestamp" side
channel and no clock wrapper.
ClockSample stays one paired userspace instant: the preferred wall value
is carried beside it on the classified echo, never inside it. Lifecycle,
rate and maximum-duration policy remain on received_at.mono_ns, midpoints
remain the mean of the paired userspace samples, and no monotonic instant
is estimated from a wall reading. A reading that does not convert, that is
later than the core's own sample, or that lags it by more than one second
falls back to the userspace wall; it is never a reason to drop a datagram.
Rolling snapshots are recomputed with `SampleMode::RunningOnly` regardless
of the configured sample mode, so the new `SampleMode::Exact` wording — that
every timing metric a `Snapshot` exposes reports a median — was not true of
`rolling_count()` and `rolling_time()`.
Scope the promise to the cumulative snapshot and say plainly on both rolling
accessors that they use running statistics only and report no median, since
a rolling window is a bounded recent view rather than the run's retained
history. A test pins both halves so the wording cannot quietly go stale.
Rolling retention behavior itself is unchanged.
`SampleMode::Exact` retained exact samples for eight of the eleven public
timing metrics. Send call, timer error, and server processing always built
their `TimeMetric` with retention disabled, so a finite run reported no
median for them even though they are the same public `TimeStats` type and
appear in the same summary table with a Median column.
Derive one `retain_exact` decision from the sample mode in `CoreStats::new`
and apply it to every timing metric, so exact mode means the same thing for
all of them. Running-only mode is unchanged and still retains none of them.
The retention estimator counted eight exact sample streams per measurable
reply. Rename it to the eleven exact samples an ordinary successful probe
can retain and document the wider optional-measurement upper bound, which
raises the finite estimate on this target from 668 to 764 bytes per probe.
A test counts the metrics a fully measured probe actually retains and
compares it against that constant, so retention and its estimate cannot
drift apart unnoticed.
No new median-capability type was needed: `TimeStats::median_ns` already
distinguishes a retained median from an absent one, and an optional
measurement the server never supplied still produces no samples at all.