feat(server): apply reply traffic class #84

Merged
Lochnair merged 4 commits from feat/server-reply-traffic-class into main 2026-08-13 16:12:59 +00:00
Lochnair commented 2026-08-13 13:41:05 +00:00 (Migrated from github.com)

Summary

The server negotiated a DSCP parameter per session and then never told the socket about it. This slice closes that gap.

  • ServerCore::handle_datagram now returns an OutboundDatagram — the reply bytes plus the raw traffic class they must be sent with — so transport policy travels out of the core with the packet instead of the runtime rediscovering session state.
  • ECHO replies request the session's negotiated raw IPv4 TOS / IPv6 Traffic Class byte, verbatim.
  • OPEN and no-test replies always request class zero, even on a listener that has already sent a marked reply.
  • A maximum-duration server close is an ordinary echo reply with FLAG_CLOSE, and keeps its session's marking.
  • A negotiated integer outside 0..=255 is still negotiated, returned and stored exactly as requested, but is transported unmarked.
  • Adds Linux ancillary tests that read the traffic class the kernel reports for each reply, so the claim is about packets and not only about intent.
  • One of those drives three sessions through one listener and proves no marking leaks between them.

Architecture

  • Every reply carries exactly one explicit traffic-class byte; there is no Option<u8> and no "unset" state.
  • The runtime applies that class to the listener socket before every send_to, zero included. Skipping the call for an unmarked reply would send it under whichever marking the previous reply left behind, which is precisely the OPEN-reply case the clean evidence pins.
  • Correctness rests on the existing sequential send model: one task owns the socket and nothing else sends from it, including while a send is suspended. That is documented at Server, and adding concurrent sends would require per-packet control messages instead.
  • There is no restore-to-zero-after-send step and no cached current value — the next reply is authoritative, which keeps failure, shutdown and cancellation paths uninteresting.
  • Applying the class and sending are both per-packet transport steps: either failing drops that one reply and the listener keeps serving. A reply whose class could not be applied is never sent, because sending it would put it on the wire under the previous marking.
  • The option comes from socket2's safe API (set_tos_v4 / set_tclass_v6), selected by the listener's bound address family rather than the peer's. #![forbid(unsafe_code)] stands; there is no raw setsockopt and no sendmsg/cmsg machinery.

Compatibility / policy

Two different kinds of claim, kept apart deliberately:

  • Clean evidence, for values in 0..=255. The DSCP parameter is the raw TOS / Traffic Class byte, not a six-bit codepoint; an echo reply carries its session's value verbatim; marking follows the session, not the listener; open replies are unmarked; a client close draws no reply. Nothing here shifts or masks the byte, and the core test uses 0xbb specifically so a codepoint round-trip would lose its ECN bits and fail.
  • irtt-rs policy, for everything else. A value that cannot be a byte is transported as zero. The specification records the reference host's handling of negative and oversized values as platform-specific and explicitly not a compatibility requirement, so there is nothing to reproduce: -1 does not become 255 by wrapping and 256 does not become 0 by truncation — both become unmarked because unrepresentable values get the unmarked transport policy. Negotiation itself is unchanged: no new rejection, no clamping, and the parameter is still echoed and stored exactly as requested.

Scope exclusions

  • No pktinfo / wildcard destination metadata / explicit source selection.
  • No full ServerFill; echo payloads stay zero-filled.
  • No new CLI flags. The class is negotiated per session, so the irtt-server / irtt-rs server applets gain the behavior through the library; a server-side --dscp would mean something else entirely.
  • No upstream black-box rerun, and no changes to docs/protocol/**, docs/clean-room/** or docs/validation/UPSTREAM_CLIENT_INTEROP.md — that validation predates this work and says DSCP was outside its matrix.
  • No irtt-proto, irtt-client or irtt-cli production changes. The only cross-crate change is the irtt-client dev-dependency gaining the ancillary feature.

Verification

Host is macOS (aarch64); the Linux tests were run in a container on a real Linux kernel.

Workspace, on the host:

  • cargo fmt --all -- --check
  • cargo check --workspace --all-targets --all-features --locked
  • cargo test --workspace --all-targets --all-features --locked — all suites pass
  • cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
  • cargo clippy -p irtt-server --all-targets --all-features --locked --target x86_64-unknown-linux-gnu -- -D warnings

Linux (rust:1-slim, aarch64):

  • cargo test -p irtt-server -p irtt-client --all-features --locked — all pass, including the three new wire tests.
  • IPv4 marked echo: requested DSCP 46, observed traffic_class = Some(0xb8), dscp = Some(46), ecn = Some(0).
  • Cross-session: one listener, three sessions (EF 46 → 0xb8, CS1 8 → 0x20, unmarked 0 → 0x00), driven marked → other-marked → back to marked → unmarked. Each reply carried its own session's byte.
  • IPv6 [::1]: requested DSCP 46, observed traffic_class = Some(0xb8).
  • Mutation check: forcing the runtime to apply zero regardless made all three wire tests fail, including the IPv6 one — so none of them is silently skipping.

Dependencies: cargo tree -p irtt-server --edges normal shows socket2 0.6.5 as a normal dependency and no irtt-client, which stays dev-only.

Manual smoke: irtt-server --bind 127.0.0.1:22112 with irtt-rs client --dscp 46 --duration 1s --interval 200ms — 5/5 replies, no loss, server stayed up and shut down cleanly on SIGINT.

## Summary The server negotiated a DSCP parameter per session and then never told the socket about it. This slice closes that gap. - `ServerCore::handle_datagram` now returns an `OutboundDatagram` — the reply bytes plus the raw traffic class they must be sent with — so transport policy travels out of the core with the packet instead of the runtime rediscovering session state. - ECHO replies request the session's negotiated raw IPv4 TOS / IPv6 Traffic Class byte, verbatim. - OPEN and no-test replies always request class zero, even on a listener that has already sent a marked reply. - A maximum-duration server close is an ordinary echo reply with `FLAG_CLOSE`, and keeps its session's marking. - A negotiated integer outside `0..=255` is still negotiated, returned and stored exactly as requested, but is transported unmarked. - Adds Linux ancillary tests that read the traffic class the kernel reports for each reply, so the claim is about packets and not only about intent. - One of those drives three sessions through one listener and proves no marking leaks between them. ## Architecture - Every reply carries exactly one explicit traffic-class byte; there is no `Option<u8>` and no "unset" state. - The runtime applies that class to the listener socket before **every** `send_to`, zero included. Skipping the call for an unmarked reply would send it under whichever marking the previous reply left behind, which is precisely the OPEN-reply case the clean evidence pins. - Correctness rests on the existing sequential send model: one task owns the socket and nothing else sends from it, including while a send is suspended. That is documented at `Server`, and adding concurrent sends would require per-packet control messages instead. - There is no restore-to-zero-after-send step and no cached current value — the next reply is authoritative, which keeps failure, shutdown and cancellation paths uninteresting. - Applying the class and sending are both per-packet transport steps: either failing drops that one reply and the listener keeps serving. A reply whose class could not be applied is never sent, because sending it would put it on the wire under the previous marking. - The option comes from `socket2`'s safe API (`set_tos_v4` / `set_tclass_v6`), selected by the listener's bound address family rather than the peer's. `#![forbid(unsafe_code)]` stands; there is no raw `setsockopt` and no `sendmsg`/`cmsg` machinery. ## Compatibility / policy Two different kinds of claim, kept apart deliberately: - **Clean evidence, for values in `0..=255`.** The DSCP parameter is the raw TOS / Traffic Class byte, not a six-bit codepoint; an echo reply carries its session's value verbatim; marking follows the session, not the listener; open replies are unmarked; a client close draws no reply. Nothing here shifts or masks the byte, and the core test uses `0xbb` specifically so a codepoint round-trip would lose its ECN bits and fail. - **`irtt-rs` policy, for everything else.** A value that cannot be a byte is transported as zero. The specification records the reference host's handling of negative and oversized values as platform-specific and explicitly not a compatibility requirement, so there is nothing to reproduce: `-1` does not become `255` by wrapping and `256` does not become `0` by truncation — both become unmarked because unrepresentable values get the unmarked transport policy. Negotiation itself is unchanged: no new rejection, no clamping, and the parameter is still echoed and stored exactly as requested. ## Scope exclusions - No pktinfo / wildcard destination metadata / explicit source selection. - No full `ServerFill`; echo payloads stay zero-filled. - No new CLI flags. The class is negotiated per session, so the `irtt-server` / `irtt-rs server` applets gain the behavior through the library; a server-side `--dscp` would mean something else entirely. - No upstream black-box rerun, and no changes to `docs/protocol/**`, `docs/clean-room/**` or `docs/validation/UPSTREAM_CLIENT_INTEROP.md` — that validation predates this work and says DSCP was outside its matrix. - No `irtt-proto`, `irtt-client` or `irtt-cli` production changes. The only cross-crate change is the `irtt-client` dev-dependency gaining the `ancillary` feature. ## Verification Host is macOS (aarch64); the Linux tests were run in a container on a real Linux kernel. Workspace, on the host: - `cargo fmt --all -- --check` - `cargo check --workspace --all-targets --all-features --locked` - `cargo test --workspace --all-targets --all-features --locked` — all suites pass - `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` - `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps` - `cargo clippy -p irtt-server --all-targets --all-features --locked --target x86_64-unknown-linux-gnu -- -D warnings` Linux (`rust:1-slim`, aarch64): - `cargo test -p irtt-server -p irtt-client --all-features --locked` — all pass, including the three new wire tests. - IPv4 marked echo: requested DSCP 46, observed `traffic_class = Some(0xb8)`, `dscp = Some(46)`, `ecn = Some(0)`. - Cross-session: one listener, three sessions (EF 46 → `0xb8`, CS1 8 → `0x20`, unmarked 0 → `0x00`), driven marked → other-marked → back to marked → unmarked. Each reply carried its own session's byte. - IPv6 `[::1]`: requested DSCP 46, observed `traffic_class = Some(0xb8)`. - Mutation check: forcing the runtime to apply zero regardless made all three wire tests fail, including the IPv6 one — so none of them is silently skipping. Dependencies: `cargo tree -p irtt-server --edges normal` shows `socket2 0.6.5` as a normal dependency and no `irtt-client`, which stays dev-only. Manual smoke: `irtt-server --bind 127.0.0.1:22112` with `irtt-rs client --dscp 46 --duration 1s --interval 200ms` — 5/5 replies, no loss, server stayed up and shut down cleanly on SIGINT.
chatgpt-codex-connector[bot] (Migrated from github.com) reviewed 2026-08-13 13:47:21 +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: 5420387846

ℹ️ 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:** `5420387846` <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>
@@ -66,3 +125,2 @@
/// writability. Per-packet send failures and short sends drop only that
/// reply; receive failures and internal core failures terminate the loop
/// with an error.
/// reply, and so does a failure to apply a marking that reply needed —
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-08-13 13:47:21 +00:00

P1 Badge Keep zero-class replies working when IP_TOS is unavailable

On Windows versions where IP_TOS is unsupported, socket2::set_tos_v4 returns an error even for the zero class used by every OPEN reply. This branch then silently discards that reply, so an IPv4 server created with the normal Server::bind path cannot establish any session, including clients that request no DSCP; distinguish a fresh unmarked socket or otherwise handle the unsupported-zero case without dropping all traffic.

Useful? React with 👍 / 👎.

**<sub><sub>![P1 Badge](https://img.shields.io/badge/P1-orange?style=flat)</sub></sub> Keep zero-class replies working when IP_TOS is unavailable** On Windows versions where `IP_TOS` is unsupported, `socket2::set_tos_v4` returns an error even for the zero class used by every OPEN reply. This branch then silently discards that reply, so an IPv4 server created with the normal `Server::bind` path cannot establish any session, including clients that request no DSCP; distinguish a fresh unmarked socket or otherwise handle the unsupported-zero case without dropping all traffic. Useful? React with 👍 / 👎.
@@ -0,0 +1,121 @@
//! Applying a reply's traffic class to the listener socket.
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-08-13 13:47:21 +00:00

P2 Badge Support socket2's illumos traffic-class setters

On illumos, this cfg routes IPv4 through unsupported_traffic_class, while illumos is also absent from the IPv6 supported-target list, even though the pinned socket2 0.6.5 exposes both set_tos_v4 and set_tclass_v6 there. Consequently, a session negotiating any nonzero in-range DSCP gets an OPEN reply but has every ECHO reply discarded when applying the class returns Unsupported; include illumos in the socket2-supported paths so these sessions remain usable.

AGENTS.md reference: crates/irtt-server/AGENTS.md:L196-L202

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Support socket2's illumos traffic-class setters** On illumos, this cfg routes IPv4 through `unsupported_traffic_class`, while illumos is also absent from the IPv6 supported-target list, even though the pinned socket2 0.6.5 exposes both `set_tos_v4` and `set_tclass_v6` there. Consequently, a session negotiating any nonzero in-range DSCP gets an OPEN reply but has every ECHO reply discarded when applying the class returns `Unsupported`; include illumos in the socket2-supported paths so these sessions remain usable. AGENTS.md reference: [crates/irtt-server/AGENTS.md:L196-L202](https://github.com/Lochnair/irtt-rs/blob/5420387846c26c1f87129f35b0ccba2afb5dd72b/crates/irtt-server/AGENTS.md#L196-L202) Useful? React with 👍 / 👎.
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-08-13 13:47:21 +00:00

P2 Badge Do not pretend an unsupported socket was cleared

On targets routed through this fallback, a zero class returns success without changing the socket. That assumption fails for Server::from_socket, which explicitly accepts an already-prepared socket that may already carry a nonzero TOS/Traffic Class set by its creator; an OPEN or unmarked ECHO reply is then sent with that inherited marking instead of being dropped. Unless the option can actually be cleared, report Unsupported for zero as well so the runtime does not transmit a reply under the wrong class.

AGENTS.md reference: crates/irtt-server/AGENTS.md:L187-L191

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Do not pretend an unsupported socket was cleared** On targets routed through this fallback, a zero class returns success without changing the socket. That assumption fails for `Server::from_socket`, which explicitly accepts an already-prepared socket that may already carry a nonzero TOS/Traffic Class set by its creator; an OPEN or unmarked ECHO reply is then sent with that inherited marking instead of being dropped. Unless the option can actually be cleared, report `Unsupported` for zero as well so the runtime does not transmit a reply under the wrong class. AGENTS.md reference: [crates/irtt-server/AGENTS.md:L187-L191](https://github.com/Lochnair/irtt-rs/blob/5420387846c26c1f87129f35b0ccba2afb5dd72b/crates/irtt-server/AGENTS.md#L187-L191) Useful? React with 👍 / 👎.
Lochnair (Migrated from github.com) reviewed 2026-08-13 15:27:26 +00:00
@@ -0,0 +1,121 @@
//! Applying a reply's traffic class to the listener socket.
Lochnair (Migrated from github.com) commented 2026-08-13 15:27:26 +00:00

Fixed in 1830429: checked socket2 0.6.5 itself — set_tos_v4 excludes only fuchsia/redox/solaris/haiku/wasi and set_tclass_v6 lists illumos, so both target lists now mirror those gates exactly instead of an older matrix copied from irtt-client; illumos gets the real setters on both paths.

🤖 Addressed by Claude Code

Fixed in 1830429: checked socket2 0.6.5 itself — `set_tos_v4` excludes only fuchsia/redox/solaris/haiku/wasi and `set_tclass_v6` lists illumos, so both target lists now mirror those gates exactly instead of an older matrix copied from `irtt-client`; illumos gets the real setters on both paths. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Lochnair (Migrated from github.com) reviewed 2026-08-13 15:27:28 +00:00
@@ -0,0 +1,121 @@
//! Applying a reply's traffic class to the listener socket.
Lochnair (Migrated from github.com) commented 2026-08-13 15:27:28 +00:00

Fixed in 1830429: the fallback now reports Unsupported for every value including zero, so it never claims a socket was cleared; whether an unappliable zero is still safe to send moved to the runtime, which decides it from what this server itself applied rather than from an assumption about the socket it was handed.

🤖 Addressed by Claude Code

Fixed in 1830429: the fallback now reports `Unsupported` for every value including zero, so it never claims a socket was cleared; whether an unappliable zero is still safe to send moved to the runtime, which decides it from what this server itself applied rather than from an assumption about the socket it was handed. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Lochnair (Migrated from github.com) reviewed 2026-08-13 15:27:30 +00:00
@@ -66,3 +125,2 @@
/// writability. Per-packet send failures and short sends drop only that
/// reply; receive failures and internal core failures terminate the loop
/// with an error.
/// reply, and so does a failure to apply a marking that reply needed —
Lochnair (Migrated from github.com) commented 2026-08-13 15:27:30 +00:00

Fixed in 1830429: an apply failure now drops the reply only when a marking was actually wanted, or when this server has previously marked the socket itself; an unmarked reply on a never-marked socket is still sent, so a host whose IP_TOS is unsupported keeps serving sessions instead of silently answering nothing.

🤖 Addressed by Claude Code

Fixed in 1830429: an apply failure now drops the reply only when a marking was actually wanted, or when this server has previously marked the socket itself; an unmarked reply on a never-marked socket is still sent, so a host whose `IP_TOS` is unsupported keeps serving sessions instead of silently answering nothing. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Sign in to join this conversation.