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 everysend_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.
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.
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.
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>
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></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 👍 / 👎.
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.
**<sub><sub></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 👍 / 👎.
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.
**<sub><sub></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 👍 / 👎.
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.
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)_
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.
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)_
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.
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)_
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
The server negotiated a DSCP parameter per session and then never told the socket about it. This slice closes that gap.
ServerCore::handle_datagramnow returns anOutboundDatagram— 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.FLAG_CLOSE, and keeps its session's marking.0..=255is still negotiated, returned and stored exactly as requested, but is transported unmarked.Architecture
Option<u8>and no "unset" state.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.Server, and adding concurrent sends would require per-packet control messages instead.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 rawsetsockoptand nosendmsg/cmsgmachinery.Compatibility / policy
Two different kinds of claim, kept apart deliberately:
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 uses0xbbspecifically so a codepoint round-trip would lose its ECN bits and fail.irtt-rspolicy, 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:-1does not become255by wrapping and256does not become0by 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
ServerFill; echo payloads stay zero-filled.irtt-server/irtt-rs serverapplets gain the behavior through the library; a server-side--dscpwould mean something else entirely.docs/protocol/**,docs/clean-room/**ordocs/validation/UPSTREAM_CLIENT_INTEROP.md— that validation predates this work and says DSCP was outside its matrix.irtt-proto,irtt-clientorirtt-cliproduction changes. The only cross-crate change is theirtt-clientdev-dependency gaining theancillaryfeature.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 -- --checkcargo check --workspace --all-targets --all-features --lockedcargo test --workspace --all-targets --all-features --locked— all suites passcargo clippy --workspace --all-targets --all-features --locked -- -D warningsRUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-depscargo clippy -p irtt-server --all-targets --all-features --locked --target x86_64-unknown-linux-gnu -- -D warningsLinux (
rust:1-slim, aarch64):cargo test -p irtt-server -p irtt-client --all-features --locked— all pass, including the three new wire tests.traffic_class = Some(0xb8),dscp = Some(46),ecn = Some(0).0xb8, CS1 8 →0x20, unmarked 0 →0x00), driven marked → other-marked → back to marked → unmarked. Each reply carried its own session's byte.[::1]: requested DSCP 46, observedtraffic_class = Some(0xb8).Dependencies:
cargo tree -p irtt-server --edges normalshowssocket2 0.6.5as a normal dependency and noirtt-client, which stays dev-only.Manual smoke:
irtt-server --bind 127.0.0.1:22112withirtt-rs client --dscp 46 --duration 1s --interval 200ms— 5/5 replies, no loss, server stayed up and shut down cleanly on SIGINT.💡 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
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".
@@ -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 —On Windows versions where
IP_TOSis unsupported,socket2::set_tos_v4returns 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 normalServer::bindpath 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.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 bothset_tos_v4andset_tclass_v6there. Consequently, a session negotiating any nonzero in-range DSCP gets an OPEN reply but has every ECHO reply discarded when applying the class returnsUnsupported; 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 👍 / 👎.
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, reportUnsupportedfor 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 👍 / 👎.
@@ -0,0 +1,121 @@//! Applying a reply's traffic class to the listener socket.Fixed in
1830429: checked socket2 0.6.5 itself —set_tos_v4excludes only fuchsia/redox/solaris/haiku/wasi andset_tclass_v6lists illumos, so both target lists now mirror those gates exactly instead of an older matrix copied fromirtt-client; illumos gets the real setters on both paths.🤖 Addressed by Claude Code
@@ -0,0 +1,121 @@//! Applying a reply's traffic class to the listener socket.Fixed in
1830429: the fallback now reportsUnsupportedfor 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
@@ -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 —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 whoseIP_TOSis unsupported keeps serving sessions instead of silently answering nothing.🤖 Addressed by Claude Code