feat(server): capture kernel receive timestamps #101

Merged
Lochnair merged 1 commits from feat/server-kernel-rx-capture into main 2026-08-15 06:18:55 +00:00
Lochnair commented 2026-08-15 06:14:07 +00:00 (Migrated from github.com)

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

## 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)
Sign in to join this conversation.