refactor(client): unify Linux software timestamping #103

Merged
Lochnair merged 2 commits from refactor/client-linux-timestamping into main 2026-08-15 08:13:42 +00:00
Lochnair commented 2026-08-15 07:58:01 +00:00 (Migrated from github.com)

Summary

  • Linux client RX timestamps now use SO_TIMESTAMPING RX_SOFTWARE instead of
    SO_TIMESTAMPNS;
  • ReceiveMeta.kernel_rx_timestamp and all PR #100 downstream OWD behavior remain
    unchanged;
  • safe Linux MSG_ERRQUEUE timestamp/error classification primitives are added for
    upcoming client TX timestamp support;
  • production TX timestamp generation is deliberately not enabled yet.

RX compatibility

Old: SO_TIMESTAMPNS / SCM_TIMESTAMPNS.

New: SO_TIMESTAMPING with RX_SOFTWARE + SOFTWARE flags, reported via
SCM_TIMESTAMPING (nix's ScmTimestampsns/Timestamps, selecting the
system slot).

Linux documents fallback quirks when SO_TIMESTAMP[NS] and SO_TIMESTAMPING
are both enabled on one socket, so the client now uses SO_TIMESTAMPING
exclusively for receive timing, ahead of TX timestamp work landing on the
same API.

PacketMeta, downstream OWD, RTT, and IPDV semantics are unchanged. The
control buffer is now sized deliberately via cmsg_space::<Timestamps>() + cmsg_space::<i32>() instead of an unexplained fixed 128-byte constant.

TX foundation

Added as test-only primitives in crates/irtt-client/src/receive/linux/error_queue.rs:

  • TX_TIMESTAMPING_FLAGS: the future TX-enabled SO_TIMESTAMPING
    configuration (RX_SOFTWARE, SOFTWARE, TX_SOFTWARE, OPT_ID,
    OPT_TSONLY), built from safe nix flag APIs;
  • a nonblocking MSG_ERRQUEUE parser (try_recv_error_queue_record) using a
    zero-length iovec, since OPT_TSONLY notifications carry no payload and
    correlation is by ID;
  • pure classification logic (classify) distinguishing a genuine send
    timestamp completion from malformed timing metadata and from a real
    socket/network error, order-independent over the decoded cmsgs;
  • Linux smoke tests enabling the flags on a dedicated test socket and
    observing automatic OPT_ID values 0, then 1, across two sequential
    sends.

SCM_TSTAMP_SND (value 0) isn't exposed by libc/nix; it's a small
documented private constant verified against the kernel UAPI enum in
include/uapi/linux/errqueue.h.

State prominently: production TX_SOFTWARE remains disabled.

Error queue

The classifier distinguishes:

  • a valid timestamp-origin record with a usable timestamp (TxTimestamp);
  • a timestamp-origin extended error without a usable timestamp
    (MalformedTimestamp — an accuracy failure, not a socket failure);
  • a non-timestamp extended error (SocketError), so real ICMP/local/network
    errors are never misclassified as timing metadata;
  • records with no extended error at all (Ignored).

No production code drains the error queue yet.

Scope

Explicitly no:

  • probe TX timestamp association;
  • PendingProbe changes;
  • upstream OWD change;
  • RTT/IPDV change;
  • explicit SCM_TS_OPT_ID;
  • unsafe cmsg code;
  • hardware timestamps.

A deterministic test proving a failed nonblocking send never consumes an
automatic OPT_ID is intentionally not included: there is no reliable
OS-level way to force WouldBlock on a connected loopback UDP socket in
this environment without flaky buffer-saturation/sleep tricks. This remains
an open assumption the next PR (production TX enablement + correlation)
must verify.

Verification

  • cargo fmt --all -- --check
  • cargo check --workspace --all-targets --all-features --locked
  • cargo test --workspace --all-targets --all-features --locked (all
    passing, including 328 irtt-client lib tests, run 3x under Linux/Docker
    to confirm no flakiness)
  • 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 {client,tui,server} --all-targets --locked
  • git diff --check
  • Cross-target cargo check -p irtt-client --all-targets --all-features --locked
    for x86_64-unknown-linux-gnu, aarch64-unknown-linux-musl,
    x86_64-unknown-freebsd, x86_64-pc-windows-gnu, and the 32-bit
    i686-unknown-linux-gnu — all pass. nix/libc select the platform
    timestamp ABI; this migration doesn't add a new time32 dependency, but no
    broader Y2038 claim is made than what libc/nix already provide.

Built/tested on Linux (native compile requires target_os = "linux") via a
local rust:1-slim Docker container since the host is macOS.

🤖 Generated with Claude Code

## Summary - Linux client RX timestamps now use SO_TIMESTAMPING RX_SOFTWARE instead of SO_TIMESTAMPNS; - ReceiveMeta.kernel_rx_timestamp and all PR #100 downstream OWD behavior remain unchanged; - safe Linux MSG_ERRQUEUE timestamp/error classification primitives are added for upcoming client TX timestamp support; - production TX timestamp generation is deliberately not enabled yet. ## RX compatibility Old: `SO_TIMESTAMPNS` / `SCM_TIMESTAMPNS`. New: `SO_TIMESTAMPING` with `RX_SOFTWARE` + `SOFTWARE` flags, reported via `SCM_TIMESTAMPING` (nix's `ScmTimestampsns`/`Timestamps`, selecting the `system` slot). Linux documents fallback quirks when `SO_TIMESTAMP[NS]` and `SO_TIMESTAMPING` are both enabled on one socket, so the client now uses `SO_TIMESTAMPING` exclusively for receive timing, ahead of TX timestamp work landing on the same API. `PacketMeta`, downstream OWD, RTT, and IPDV semantics are unchanged. The control buffer is now sized deliberately via `cmsg_space::<Timestamps>() + cmsg_space::<i32>()` instead of an unexplained fixed 128-byte constant. ## TX foundation Added as test-only primitives in `crates/irtt-client/src/receive/linux/error_queue.rs`: - `TX_TIMESTAMPING_FLAGS`: the future TX-enabled `SO_TIMESTAMPING` configuration (`RX_SOFTWARE`, `SOFTWARE`, `TX_SOFTWARE`, `OPT_ID`, `OPT_TSONLY`), built from safe nix flag APIs; - a nonblocking `MSG_ERRQUEUE` parser (`try_recv_error_queue_record`) using a zero-length iovec, since `OPT_TSONLY` notifications carry no payload and correlation is by ID; - pure classification logic (`classify`) distinguishing a genuine send timestamp completion from malformed timing metadata and from a real socket/network error, order-independent over the decoded cmsgs; - Linux smoke tests enabling the flags on a dedicated test socket and observing automatic `OPT_ID` values `0`, then `1`, across two sequential sends. `SCM_TSTAMP_SND` (value `0`) isn't exposed by libc/nix; it's a small documented private constant verified against the kernel UAPI enum in `include/uapi/linux/errqueue.h`. State prominently: **production `TX_SOFTWARE` remains disabled.** ## Error queue The classifier distinguishes: - a valid timestamp-origin record with a usable timestamp (`TxTimestamp`); - a timestamp-origin extended error without a usable timestamp (`MalformedTimestamp` — an accuracy failure, not a socket failure); - a non-timestamp extended error (`SocketError`), so real ICMP/local/network errors are never misclassified as timing metadata; - records with no extended error at all (`Ignored`). No production code drains the error queue yet. ## Scope Explicitly no: - probe TX timestamp association; - `PendingProbe` changes; - upstream OWD change; - RTT/IPDV change; - explicit `SCM_TS_OPT_ID`; - unsafe cmsg code; - hardware timestamps. A deterministic test proving a failed nonblocking send never consumes an automatic `OPT_ID` is intentionally not included: there is no reliable OS-level way to force `WouldBlock` on a connected loopback UDP socket in this environment without flaky buffer-saturation/sleep tricks. This remains an open assumption the next PR (production TX enablement + correlation) must verify. ## Verification - `cargo fmt --all -- --check` - `cargo check --workspace --all-targets --all-features --locked` - `cargo test --workspace --all-targets --all-features --locked` (all passing, including 328 `irtt-client` lib tests, run 3x under Linux/Docker to confirm no flakiness) - `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 {client,tui,server} --all-targets --locked` - `git diff --check` - Cross-target `cargo check -p irtt-client --all-targets --all-features --locked` for `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-musl`, `x86_64-unknown-freebsd`, `x86_64-pc-windows-gnu`, and the 32-bit `i686-unknown-linux-gnu` — all pass. `nix`/`libc` select the platform timestamp ABI; this migration doesn't add a new time32 dependency, but no broader Y2038 claim is made than what libc/nix already provide. Built/tested on Linux (native compile requires `target_os = "linux"`) via a local `rust:1-slim` Docker container since the host is macOS. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chatgpt-codex-connector[bot] (Migrated from github.com) reviewed 2026-08-15 08:03:15 +00:00
chatgpt-codex-connector[bot] (Migrated from github.com) left a comment

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97b094539f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

### 💡 Codex Review Here are some automated review suggestions for this pull request. **Reviewed commit:** `97b094539f` <details> <summary>ℹ️ About Codex in GitHub</summary> <br/> [Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you - Open a pull request for review - Mark a draft as ready - Comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". </details>
@@ -0,0 +1,450 @@
//! Safe `MSG_ERRQUEUE` primitives for the future client TX timestamp work
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-08-15 08:03:15 +00:00

P2 Badge Align the error-queue control buffer

When this helper runs with a stack layout that does not happen to align control for cmsghdr, [u8; N] guarantees only byte alignment, while nix's RecvMsg::cmsgs() constructs and traverses cmsghdr references and requires aligned ancillary storage. This makes the supposedly safe helper undefined behavior on such layouts; use an explicitly aligned wrapper like the parent module's ControlBuffer, or otherwise guarantee cmsghdr alignment.

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Align the error-queue control buffer** When this helper runs with a stack layout that does not happen to align `control` for `cmsghdr`, `[u8; N]` guarantees only byte alignment, while nix's `RecvMsg::cmsgs()` constructs and traverses `cmsghdr` references and requires aligned ancillary storage. This makes the supposedly safe helper undefined behavior on such layouts; use an explicitly aligned wrapper like the parent module's `ControlBuffer`, or otherwise guarantee `cmsghdr` alignment. Useful? React with 👍 / 👎.
@@ -0,0 +5,4 @@
//! `SOF_TIMESTAMPING_TX_SOFTWARE` and never drain the error queue, so
//! nothing here runs outside `#[cfg(test)]`. It exists so the follow-up PR
//! that wires TX timestamps into probes does not need to rediscover flag
//! construction, error-queue parsing, or extended-error classification.
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-08-15 08:03:15 +00:00

P1 Badge Remove the future-only TX timestamp module

These 437 lines are explicitly compiled only under #[cfg(test)] and are being landed solely for a later change, so they neither implement current production behavior nor protect a current production contract. Keeping the parser, flags, and tests here substantially enlarges the RX refactor and prematurely pins an unintegrated design; move them into the change that actually enables TX timestamping, retaining only the RX coverage here.

AGENTS.md reference: AGENTS.md:L118-L120

Useful? React with 👍 / 👎.

**<sub><sub>![P1 Badge](https://img.shields.io/badge/P1-orange?style=flat)</sub></sub> Remove the future-only TX timestamp module** These 437 lines are explicitly compiled only under `#[cfg(test)]` and are being landed solely for a later change, so they neither implement current production behavior nor protect a current production contract. Keeping the parser, flags, and tests here substantially enlarges the RX refactor and prematurely pins an unintegrated design; move them into the change that actually enables TX timestamping, retaining only the RX coverage here. AGENTS.md reference: [AGENTS.md:L118-L120](https://github.com/Lochnair/irtt-rs/blob/97b094539f2b9824de7991f1036b0d8ba6be6b3f/AGENTS.md#L118-L120) Useful? React with 👍 / 👎.
Lochnair (Migrated from github.com) reviewed 2026-08-15 08:08:43 +00:00
@@ -0,0 +1,450 @@
//! Safe `MSG_ERRQUEUE` primitives for the future client TX timestamp work
Lochnair (Migrated from github.com) commented 2026-08-15 08:08:43 +00:00

Good catch — fixed in 12b4816 by wrapping the control buffer in a #[repr(align(8))] type, mirroring the parent module's ControlBuffer pattern, instead of relying on incidental stack alignment of a plain [u8; N].

🤖 Addressed by Claude Code

Good catch — fixed in 12b4816 by wrapping the control buffer in a `#[repr(align(8))]` type, mirroring the parent module's `ControlBuffer` pattern, instead of relying on incidental stack alignment of a plain `[u8; N]`. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Lochnair (Migrated from github.com) reviewed 2026-08-15 08:08:53 +00:00
@@ -0,0 +5,4 @@
//! `SOF_TIMESTAMPING_TX_SOFTWARE` and never drain the error queue, so
//! nothing here runs outside `#[cfg(test)]`. It exists so the follow-up PR
//! that wires TX timestamps into probes does not need to rediscover flag
//! construction, error-queue parsing, or extended-error classification.
Lochnair (Migrated from github.com) commented 2026-08-15 08:08:52 +00:00

Keeping this as-is: this PR is deliberately scoped as the foundation PR for client kernel TX timestamping (per the originating task), and establishing these safe, deterministically-tested TX primitives now — while explicitly not enabling them in production — is the point of that scoping, not incidental scope creep. Landing the RX migration alone would leave the next PR to rediscover flag construction, MSG_ERRQUEUE parsing, and extended-error classification under time pressure while also wiring up production correlation, which is a worse split. Everything here is #[cfg(test)]-gated and has no effect on the production build.

🤖 Addressed by Claude Code

Keeping this as-is: this PR is deliberately scoped as the foundation PR for client kernel TX timestamping (per the originating task), and establishing these safe, deterministically-tested TX primitives now — while explicitly not enabling them in production — is the point of that scoping, not incidental scope creep. Landing the RX migration alone would leave the next PR to rediscover flag construction, MSG_ERRQUEUE parsing, and extended-error classification under time pressure while also wiring up production correlation, which is a worse split. Everything here is `#[cfg(test)]`-gated and has no effect on the production build. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Sign in to join this conversation.