Explicit listeners previously used recv_from everywhere, which cannot retrieve
control messages. On Linux they now go through the same safe nixrecvmsg
machinery the wildcard path already used, parameterised by whether a destination
is required. The send side is untouched: an explicit listener still replies with
plain send_to.
Failure semantics
SO_TIMESTAMPNS setup is best-effort and never prevents server startup. It is
attempted once in Server::from_socket, for both listener kinds, and strictly after the mandatory wildcard destination setup, so a timestamp refusal can
neither hide nor stand in for a source-selection failure. No new ServerRuntimeError variant was added.
A missing or malformed timestamp falls back to metadata absence
(kernel_rx_timestamp: None). Conversion from TimeSpec is structural only —
negative seconds, negative nanoseconds, nanoseconds ≥ 1e9 and UNIX_EPOCH.checked_add overflow all yield None.
Timing metadata absence never invalidates a datagram. MSG_CTRUNC on an
explicit listener is now accepted with no timestamp, because the timestamp is
all that can have been lost; on a wildcard listener it remains a drop, because
the destination may have gone with it.
Wildcard destination metadata remains correctness-critical: missing or
unspecified destination still drops the datagram before the core.
Payload truncation policy is unchanged. MSG_TRUNC drops on every ancillary
path, and the conservative full-buffer rule in socket_io::receive still drops
an exactly-full datagram on every path.
Control buffer
CONTROL_LEN was a magic 128 justified by "leaves headroom". It is now derived
from the cmsg payload types themselves via nix's pub const fn cmsg_space::<T>():
IPv4 destination — in_pktinfo on Linux, in_addr on the BSDs;
IPv6 destination — in6_pktinfo;
timestamp — timespec on Linux, 0 elsewhere.
Both families are counted so one buffer type serves either, and a const _: () = { assert!(..) } block proves at compile time that each family's
destination message and the timestamp fit together. A future control message that
outgrows the buffer now fails the build rather than silently setting MSG_CTRUNC.
No per-datagram heap allocation was introduced: the buffer is still the same
stack-resident #[repr(align(8))] array, sized by a constant instead of a guess.
The cmsg_space! macro was deliberately not used — it allocates a Vec.
Behavior
NO protocol timestamp behavior changes in this PR.
ServerCore::handle_datagram receives the same peer and packet bytes as before
— the call is byte-for-byte identical in its arguments and appears in the diff
only as unchanged context. recv_wall / recv_mono / send_wall / send_mono
/ midpoint are unchanged, as are server-processing values, session lifecycle,
expiry, rate limiting and negotiation. No existing expected protocol value needed
updating.
core.rs, clock.rs, Cargo.toml and Cargo.lock are untouched. ClockSample
is unchanged, and no wall → monotonic mapping exists anywhere in the diff.
At the runtime boundary the field is explicitly destructured and deliberately
ignored, with a comment stating why, rather than hidden behind an #[allow].
Scope
This PR does not implement:
kernel timestamp use for upstream OWD;
ServerCore metadata plumbing (no new argument, method, setter, pending-state
field or ClockSource wrapper);
wall → monotonic mapping;
midpoint policy changes;
a server-side plausibility rule — PR #100's MAX_KERNEL_RX_LAG is deliberately not copied here, because that check belongs with measurement acceptance, not
with capture;
macOS / FreeBSD / Windows timestamp support;
TX timestamping.
A follow-up decides how the captured observation enters ServerCore, with this
plumbing landed and reviewable on its own.
Tests
Explicit Linux receive — loopback listener with timestamps enabled receives
through the new recvmsg path: payload, length, peer and ReplySource::Bound
all as before, plus the timestamp.
Wildcard destination + timestamp coexistence — the critical regression test.
A wildcard listener with both options enabled recovers its required ReplySource::V4and its timestamp from one datagram, with no MSG_CTRUNC
drop, proving both the parse-all-cmsgs change and the derived control capacity.
Optional timestamp semantics — a wildcard listener that never enabled
timestamps still recovers its destination and serves the request; an explicit
listener with no timestamp is served with None; pure-function coverage pins
that a missing destination stops only the listener that needed one, and that MSG_CTRUNC condemns a datagram only where it may have taken the destination.
Structural TimeSpec conversion — valid value, epoch, negative seconds,
negative nanoseconds, out-of-range nanoseconds, and the representable extremes
converting rather than panicking.
Existing drop policy — the PR #99 tests are unchanged and still pass. On
Linux an oversized datagram is now caught by MSG_TRUNC rather than only by
the full-buffer rule; the exactly-full datagram is still dropped by the
conservative rule on every path.
Note that the Linux-gated tests could not be executed on the authoring host
(macOS); they are verified to compile for Linux and rely on CI to run.
## Summary
- Linux server sockets now opportunistically request `SCM_TIMESTAMPNS`;
- explicit and wildcard Linux receive paths capture the kernel receive wall
timestamp as optional transport metadata;
- wildcard destination metadata and kernel timestamps are collected
independently, without relying on cmsg ordering;
- `ServerCore` does not consume the timestamp yet.
## Receive paths
| listener | path |
|---|---|
| Linux explicit | `recvmsg` + optional `SCM_TIMESTAMPNS`, `ReplySource::Bound` |
| Linux wildcard | `recvmsg` + **required** destination metadata + optional `SCM_TIMESTAMPNS` |
| non-Linux explicit | unchanged `recv_from` |
| macOS/FreeBSD wildcard | existing destination ancillary path, semantically unchanged |
Explicit listeners previously used `recv_from` everywhere, which cannot retrieve
control messages. On Linux they now go through the same safe `nix` `recvmsg`
machinery the wildcard path already used, parameterised by whether a destination
is required. The send side is untouched: an explicit listener still replies with
plain `send_to`.
## Failure semantics
- `SO_TIMESTAMPNS` setup is best-effort and never prevents server startup. It is
attempted once in `Server::from_socket`, for both listener kinds, and strictly
*after* the mandatory wildcard destination setup, so a timestamp refusal can
neither hide nor stand in for a source-selection failure. No new
`ServerRuntimeError` variant was added.
- A missing or malformed timestamp falls back to metadata absence
(`kernel_rx_timestamp: None`). Conversion from `TimeSpec` is structural only —
negative seconds, negative nanoseconds, nanoseconds ≥ 1e9 and
`UNIX_EPOCH.checked_add` overflow all yield `None`.
- Timing metadata absence never invalidates a datagram. `MSG_CTRUNC` on an
explicit listener is now accepted with no timestamp, because the timestamp is
all that can have been lost; on a wildcard listener it remains a drop, because
the destination may have gone with it.
- Wildcard destination metadata remains correctness-critical: missing or
unspecified destination still drops the datagram before the core.
- Payload truncation policy is unchanged. `MSG_TRUNC` drops on every ancillary
path, and the conservative full-buffer rule in `socket_io::receive` still drops
an exactly-full datagram on every path.
## Control buffer
`CONTROL_LEN` was a magic `128` justified by "leaves headroom". It is now derived
from the cmsg payload types themselves via `nix`'s `pub const fn cmsg_space::<T>()`:
- IPv4 destination — `in_pktinfo` on Linux, `in_addr` on the BSDs;
- IPv6 destination — `in6_pktinfo`;
- timestamp — `timespec` on Linux, `0` elsewhere.
Both families are counted so one buffer type serves either, and a
`const _: () = { assert!(..) }` block proves at compile time that each family's
destination message and the timestamp fit together. A future control message that
outgrows the buffer now fails the build rather than silently setting `MSG_CTRUNC`.
No per-datagram heap allocation was introduced: the buffer is still the same
stack-resident `#[repr(align(8))]` array, sized by a constant instead of a guess.
The `cmsg_space!` macro was deliberately *not* used — it allocates a `Vec`.
## Behavior
**NO protocol timestamp behavior changes in this PR.**
`ServerCore::handle_datagram` receives the same `peer` and packet bytes as before
— the call is byte-for-byte identical in its arguments and appears in the diff
only as unchanged context. `recv_wall` / `recv_mono` / `send_wall` / `send_mono`
/ midpoint are unchanged, as are server-processing values, session lifecycle,
expiry, rate limiting and negotiation. No existing expected protocol value needed
updating.
`core.rs`, `clock.rs`, `Cargo.toml` and `Cargo.lock` are untouched. `ClockSample`
is unchanged, and no wall → monotonic mapping exists anywhere in the diff.
At the runtime boundary the field is explicitly destructured and deliberately
ignored, with a comment stating why, rather than hidden behind an `#[allow]`.
## Scope
This PR does **not** implement:
- kernel timestamp use for upstream OWD;
- `ServerCore` metadata plumbing (no new argument, method, setter, pending-state
field or `ClockSource` wrapper);
- wall → monotonic mapping;
- midpoint policy changes;
- a server-side plausibility rule — PR #100's `MAX_KERNEL_RX_LAG` is deliberately
*not* copied here, because that check belongs with measurement acceptance, not
with capture;
- macOS / FreeBSD / Windows timestamp support;
- TX timestamping.
A follow-up decides how the captured observation enters `ServerCore`, with this
plumbing landed and reviewable on its own.
## Tests
- **Explicit Linux receive** — loopback listener with timestamps enabled receives
through the new `recvmsg` path: payload, length, peer and `ReplySource::Bound`
all as before, plus the timestamp.
- **Wildcard destination + timestamp coexistence** — the critical regression test.
A wildcard listener with both options enabled recovers its required
`ReplySource::V4` *and* its timestamp from one datagram, with no `MSG_CTRUNC`
drop, proving both the parse-all-cmsgs change and the derived control capacity.
- **Optional timestamp semantics** — a wildcard listener that never enabled
timestamps still recovers its destination and serves the request; an explicit
listener with no timestamp is served with `None`; pure-function coverage pins
that a missing destination stops only the listener that needed one, and that
`MSG_CTRUNC` condemns a datagram only where it may have taken the destination.
- **Structural `TimeSpec` conversion** — valid value, epoch, negative seconds,
negative nanoseconds, out-of-range nanoseconds, and the representable extremes
converting rather than panicking.
- **Existing drop policy** — the PR #99 tests are unchanged and still pass. On
Linux an oversized datagram is now caught by `MSG_TRUNC` rather than only by
the full-buffer rule; the exactly-full datagram is still dropped by the
conservative rule on every path.
Note that the Linux-gated tests could not be executed on the authoring host
(macOS); they are verified to compile for Linux and rely on CI to run.
## Verification
All run at `1b0fc2c`:
- `cargo fmt --all -- --check` ✅
- `cargo check --workspace --all-targets --all-features --locked` ✅
- `cargo test --workspace --all-targets --all-features --locked` ✅ (26 test
binaries, 0 failures)
- `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` ✅
- `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps --locked` ✅
- `cargo check -p irtt-cli --no-default-features --features {server,client,tui} --all-targets --locked` ✅
- `git diff --check` ✅
Cross-target, covering Linux, a BSD and Windows cfg structure:
- `cargo check -p irtt-server --all-targets --all-features --locked --target …` for
`x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-musl`, `x86_64-unknown-freebsd`,
`x86_64-pc-windows-gnu` ✅
- `cargo clippy … -D warnings --target …` for `x86_64-unknown-linux-gnu`,
`x86_64-unknown-freebsd`, `x86_64-pc-windows-gnu` ✅
No new dependency, no new `nix` feature, no `Cargo.lock` change, and
`#![forbid(unsafe_code)]` still holds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
SCM_TIMESTAMPNS;timestamp as optional transport metadata;
independently, without relying on cmsg ordering;
ServerCoredoes not consume the timestamp yet.Receive paths
recvmsg+ optionalSCM_TIMESTAMPNS,ReplySource::Boundrecvmsg+ required destination metadata + optionalSCM_TIMESTAMPNSrecv_fromExplicit listeners previously used
recv_fromeverywhere, which cannot retrievecontrol messages. On Linux they now go through the same safe
nixrecvmsgmachinery the wildcard path already used, parameterised by whether a destination
is required. The send side is untouched: an explicit listener still replies with
plain
send_to.Failure semantics
SO_TIMESTAMPNSsetup is best-effort and never prevents server startup. It isattempted once in
Server::from_socket, for both listener kinds, and strictlyafter the mandatory wildcard destination setup, so a timestamp refusal can
neither hide nor stand in for a source-selection failure. No new
ServerRuntimeErrorvariant was added.(
kernel_rx_timestamp: None). Conversion fromTimeSpecis structural only —negative seconds, negative nanoseconds, nanoseconds ≥ 1e9 and
UNIX_EPOCH.checked_addoverflow all yieldNone.MSG_CTRUNCon anexplicit listener is now accepted with no timestamp, because the timestamp is
all that can have been lost; on a wildcard listener it remains a drop, because
the destination may have gone with it.
unspecified destination still drops the datagram before the core.
MSG_TRUNCdrops on every ancillarypath, and the conservative full-buffer rule in
socket_io::receivestill dropsan exactly-full datagram on every path.
Control buffer
CONTROL_LENwas a magic128justified by "leaves headroom". It is now derivedfrom the cmsg payload types themselves via
nix'spub const fn cmsg_space::<T>():in_pktinfoon Linux,in_addron the BSDs;in6_pktinfo;timespecon Linux,0elsewhere.Both families are counted so one buffer type serves either, and a
const _: () = { assert!(..) }block proves at compile time that each family'sdestination message and the timestamp fit together. A future control message that
outgrows the buffer now fails the build rather than silently setting
MSG_CTRUNC.No per-datagram heap allocation was introduced: the buffer is still the same
stack-resident
#[repr(align(8))]array, sized by a constant instead of a guess.The
cmsg_space!macro was deliberately not used — it allocates aVec.Behavior
NO protocol timestamp behavior changes in this PR.
ServerCore::handle_datagramreceives the samepeerand packet bytes as before— the call is byte-for-byte identical in its arguments and appears in the diff
only as unchanged context.
recv_wall/recv_mono/send_wall/send_mono/ midpoint are unchanged, as are server-processing values, session lifecycle,
expiry, rate limiting and negotiation. No existing expected protocol value needed
updating.
core.rs,clock.rs,Cargo.tomlandCargo.lockare untouched.ClockSampleis unchanged, and no wall → monotonic mapping exists anywhere in the diff.
At the runtime boundary the field is explicitly destructured and deliberately
ignored, with a comment stating why, rather than hidden behind an
#[allow].Scope
This PR does not implement:
ServerCoremetadata plumbing (no new argument, method, setter, pending-statefield or
ClockSourcewrapper);MAX_KERNEL_RX_LAGis deliberatelynot copied here, because that check belongs with measurement acceptance, not
with capture;
A follow-up decides how the captured observation enters
ServerCore, with thisplumbing landed and reviewable on its own.
Tests
through the new
recvmsgpath: payload, length, peer andReplySource::Boundall as before, plus the timestamp.
A wildcard listener with both options enabled recovers its required
ReplySource::V4and its timestamp from one datagram, with noMSG_CTRUNCdrop, proving both the parse-all-cmsgs change and the derived control capacity.
timestamps still recovers its destination and serves the request; an explicit
listener with no timestamp is served with
None; pure-function coverage pinsthat a missing destination stops only the listener that needed one, and that
MSG_CTRUNCcondemns a datagram only where it may have taken the destination.TimeSpecconversion — valid value, epoch, negative seconds,negative nanoseconds, out-of-range nanoseconds, and the representable extremes
converting rather than panicking.
Linux an oversized datagram is now caught by
MSG_TRUNCrather than only bythe full-buffer rule; the exactly-full datagram is still dropped by the
conservative rule on every path.
Note that the Linux-gated tests could not be executed on the authoring host
(macOS); they are verified to compile for Linux and rely on CI to run.
Verification
All run at
1b0fc2c:cargo fmt --all -- --check✅cargo check --workspace --all-targets --all-features --locked✅cargo test --workspace --all-targets --all-features --locked✅ (26 testbinaries, 0 failures)
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings✅RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps --locked✅cargo check -p irtt-cli --no-default-features --features {server,client,tui} --all-targets --locked✅git diff --check✅Cross-target, covering Linux, a BSD and Windows cfg structure:
cargo check -p irtt-server --all-targets --all-features --locked --target …forx86_64-unknown-linux-gnu,aarch64-unknown-linux-musl,x86_64-unknown-freebsd,x86_64-pc-windows-gnu✅cargo clippy … -D warnings --target …forx86_64-unknown-linux-gnu,x86_64-unknown-freebsd,x86_64-pc-windows-gnu✅No new dependency, no new
nixfeature, noCargo.lockchange, and#![forbid(unsafe_code)]still holds.🤖 Generated with Claude Code