feat(server): pin wildcard reply source address #85

Merged
Lochnair merged 5 commits from feat/server-wildcard-source-selection into main 2026-08-14 05:11:17 +00:00
Lochnair commented 2026-08-13 17:31:18 +00:00 (Migrated from github.com)

Summary

A reply must leave from the exact address its request was sent to, because
clients read from connected UDP sockets and discard anything else. An
explicit-address listener satisfies that by construction; a wildcard listener
did not, because the routing table chose the source.

Wildcard listeners now ask the kernel for each request's local destination as
ancillary metadata and send that request's reply from the same address. A client
on the host's second address is answered from the endpoint it contacted rather
than from whichever one the routing table would have picked.

  • Supported on Linux, macOS and FreeBSD, per address family.
  • Explicit-address listeners keep the existing recv_from/send_to path
    unchanged, on every target.
  • On a target without a safe source-selection path — Windows included — a
    wildcard bind now fails at construction with a clear error suggesting an
    explicit bind, instead of silently starting a listener whose replies leave
    from a routing-selected source and look like packet loss to the client.

Runtime architecture

  • Destination metadata is runtime transport state only. ServerCore,
    Session and OutboundDatagram are unchanged and know nothing about local
    addresses, interfaces or packet info.
  • Wildcard receive uses nonblocking recvmsg through Tokio readiness
    (readable() + try_io, MSG_DONTWAIT), the same shape the client's
    ancillary receive path already uses.
  • Wildcard send uses nonblocking sendmsg with a per-packet source control
    message, integrated the same way through writable() + try_io.
  • The traffic class stays the existing socket-wide setting applied before every
    send. Two transport mechanisms deliberately: the marking policy is already
    implemented and tested across a wider target matrix, and moving it into
    control messages in the same change would double the review surface for no
    gain.
  • Still one socket, one ServerCore, one sequential sender. No per-datagram
    task, no lock, no transport trait, no spawned thread.
  • A wildcard request whose local destination did not arrive — missing metadata,
    MSG_CTRUNC, or MSG_TRUNC — is dropped before the core, so no receive,
    rate or lifetime state moves for a request that cannot be answered correctly.
    There is no fall back to routing-table source selection.
  • A source-selected send failure remains per-packet loss: it drops that reply
    only, never terminates the listener and never rolls back core state.
  • No unsafe code. #![forbid(unsafe_code)] is intact and everything goes
    through nix's safe recvmsg/sendmsg, ControlMessage,
    ControlMessageOwned and sockopt wrappers — no raw syscall, no CMSG_*
    walking, no hand-written ABI layer. nix is a target-specific dependency and
    is not pulled in on Windows.

Per-platform mechanisms:

receive send
Linux IPv4 IP_PKTINFO IP_PKTINFO
Linux IPv6 IPV6_RECVPKTINFO IPV6_PKTINFO
macOS IPv4 IP_RECVDSTADDR IP_PKTINFO
macOS IPv6 IPV6_RECVPKTINFO IPV6_PKTINFO
FreeBSD IPv4 IP_RECVDSTADDR IP_SENDSRCADDR
FreeBSD IPv6 IPV6_RECVPKTINFO IPV6_PKTINFO

Each direction's control message is built explicitly: in_pktinfo carries three
fields on receive of which only two mean anything on send, and the BSD IPv4
options are not Linux's under other names.

API

Server::from_socket is now fallible:

pub fn from_socket(
    socket: tokio::net::UdpSocket,
    config: ServerConfig,
) -> Result<Self, ServerRuntimeError>

A wildcard listener has setup to do before it may serve anything, and that setup
can fail or be unavailable. Storing a latent error, or delaying an obvious
socket-configuration failure until traffic arrives, would both be worse than a
deliberate pre-1.0 breaking change. Server::bind propagates it. There is one
construction path — no try_from_socket beside an infallible from_socket.

Two new ServerRuntimeError variants carry it:
WildcardSourceSelectionUnsupported { addr } and
SourceSelectionSetup { addr, source }. Neither exposes a nix error type.

Tests

  • Wildcard IPv4 end-to-end, against 127.0.0.2:<port> where the host
    carries the whole 127/8 range, falling back to 127.0.0.1 where it does not
    (macOS assigns only 127.0.0.1 to lo0, and configuring an alias needs
    privileges). A real AsyncClient on a connected socket is the assertion: a
    reply from any other source is discarded by the kernel, so open/echo/close
    completing proves the source address without inspecting it.
  • Source selection composes with the traffic class: the same wildcard
    session requests DSCP 46 and asserts the reply's PacketMeta still reports
    traffic_class == Some(0xb8), dscp == Some(46), ecn == Some(0) (asserted
    on Linux, where the client exposes receive-side ancillary metadata).
  • Wildcard IPv6 loopback: [::]:0 serving [::1], exercising
    IPV6_RECVPKTINFO, the received interface index and the source-selected IPv6
    send. Skips only on recognized address-family unavailability.
  • Construction behavior, asserted from one test on every target: an explicit
    bind succeeds, and a wildcard bind either configures source selection and
    succeeds or returns WildcardSourceSelectionUnsupported — which of the two is
    exactly what the target decides.
  • Existing explicit-bind IPv4/IPv6 runtime and traffic-class tests are unchanged
    apart from the from_socket signature.

Scope exclusions

  • No multiple listeners in one process; one Server still owns one socket, one
    core and one session namespace.
  • No ServerFill; echo payloads stay zero-filled.
  • No protocol semantics changed, no irtt-proto changes, no irtt-client
    production changes, no ServerCore behavior changes.
  • No new upstream research or upstream execution; no protocol-evidence changes
    under docs/protocol/**, and docs/validation/UPSTREAM_CLIENT_INTEROP.md is
    left as the historical record it is.
  • No Winsock ancillary implementation; only wildcard multi-address binding is
    unavailable on Windows, not the server.
  • No unsafe FFI.

Verification

Native (macOS, aarch64-apple-darwin), all passing:

cargo fmt --all -- --check
cargo check --workspace --all-targets --all-features --locked
cargo test --workspace --all-targets --all-features --locked
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
cargo test -p irtt-client --all-features --locked

Target-specific cargo check -p irtt-server --all-targets --locked, all
passing: x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl,
aarch64-unknown-linux-musl, x86_64-unknown-freebsd, i686-unknown-linux-gnu,
x86_64-pc-windows-gnu. The musl targets matter because OpenWrt is a primary
server deployment environment. aarch64-unknown-linux-gnu and
x86_64-pc-windows-msvc are not installed locally and were not checked; no
toolchains were installed for this change.

cargo tree -p irtt-server --edges normal confirms nix appears only on the
Linux/macOS/FreeBSD targets and not on x86_64-pc-windows-gnu, socket2 remains
present everywhere, and irtt-client remains dev-only.

Manual CLI smoke, both clean with the server surviving the session and shutting
down on Ctrl-C:

  • irtt-server --bind 0.0.0.0:0 served a 1 s client run at 100 ms: 10 sent,
    10 received, 0 loss.
  • irtt-server --bind 127.0.0.1:0 likewise: 10 sent, 10 received, 0 loss.

macOS additionally verified the send-side mechanism is honored rather than
ignored: temporarily pinning a wrong source address made the connected client
time out, and restoring it made the session pass again.

## Summary A reply must leave from the exact address its request was sent to, because clients read from connected UDP sockets and discard anything else. An explicit-address listener satisfies that by construction; a wildcard listener did not, because the routing table chose the source. Wildcard listeners now ask the kernel for each request's local destination as ancillary metadata and send that request's reply from the same address. A client on the host's second address is answered from the endpoint it contacted rather than from whichever one the routing table would have picked. - Supported on Linux, macOS and FreeBSD, per address family. - Explicit-address listeners keep the existing `recv_from`/`send_to` path unchanged, on every target. - On a target without a safe source-selection path — Windows included — a wildcard bind now **fails at construction** with a clear error suggesting an explicit bind, instead of silently starting a listener whose replies leave from a routing-selected source and look like packet loss to the client. ## Runtime architecture - Destination metadata is runtime transport state only. `ServerCore`, `Session` and `OutboundDatagram` are unchanged and know nothing about local addresses, interfaces or packet info. - Wildcard receive uses nonblocking `recvmsg` through Tokio readiness (`readable()` + `try_io`, `MSG_DONTWAIT`), the same shape the client's ancillary receive path already uses. - Wildcard send uses nonblocking `sendmsg` with a per-packet source control message, integrated the same way through `writable()` + `try_io`. - The traffic class stays the existing socket-wide setting applied before every send. Two transport mechanisms deliberately: the marking policy is already implemented and tested across a wider target matrix, and moving it into control messages in the same change would double the review surface for no gain. - Still one socket, one `ServerCore`, one sequential sender. No per-datagram task, no lock, no transport trait, no spawned thread. - A wildcard request whose local destination did not arrive — missing metadata, `MSG_CTRUNC`, or `MSG_TRUNC` — is dropped *before* the core, so no receive, rate or lifetime state moves for a request that cannot be answered correctly. There is no fall back to routing-table source selection. - A source-selected send failure remains per-packet loss: it drops that reply only, never terminates the listener and never rolls back core state. - No unsafe code. `#![forbid(unsafe_code)]` is intact and everything goes through `nix`'s safe `recvmsg`/`sendmsg`, `ControlMessage`, `ControlMessageOwned` and `sockopt` wrappers — no raw syscall, no `CMSG_*` walking, no hand-written ABI layer. `nix` is a target-specific dependency and is not pulled in on Windows. Per-platform mechanisms: | | receive | send | |---|---|---| | Linux IPv4 | `IP_PKTINFO` | `IP_PKTINFO` | | Linux IPv6 | `IPV6_RECVPKTINFO` | `IPV6_PKTINFO` | | macOS IPv4 | `IP_RECVDSTADDR` | `IP_PKTINFO` | | macOS IPv6 | `IPV6_RECVPKTINFO` | `IPV6_PKTINFO` | | FreeBSD IPv4 | `IP_RECVDSTADDR` | `IP_SENDSRCADDR` | | FreeBSD IPv6 | `IPV6_RECVPKTINFO` | `IPV6_PKTINFO` | Each direction's control message is built explicitly: `in_pktinfo` carries three fields on receive of which only two mean anything on send, and the BSD IPv4 options are not Linux's under other names. ## API `Server::from_socket` is now fallible: ```rust pub fn from_socket( socket: tokio::net::UdpSocket, config: ServerConfig, ) -> Result<Self, ServerRuntimeError> ``` A wildcard listener has setup to do before it may serve anything, and that setup can fail or be unavailable. Storing a latent error, or delaying an obvious socket-configuration failure until traffic arrives, would both be worse than a deliberate pre-1.0 breaking change. `Server::bind` propagates it. There is one construction path — no `try_from_socket` beside an infallible `from_socket`. Two new `ServerRuntimeError` variants carry it: `WildcardSourceSelectionUnsupported { addr }` and `SourceSelectionSetup { addr, source }`. Neither exposes a `nix` error type. ## Tests - **Wildcard IPv4 end-to-end**, against `127.0.0.2:<port>` where the host carries the whole `127/8` range, falling back to `127.0.0.1` where it does not (macOS assigns only `127.0.0.1` to `lo0`, and configuring an alias needs privileges). A real `AsyncClient` on a *connected* socket is the assertion: a reply from any other source is discarded by the kernel, so open/echo/close completing proves the source address without inspecting it. - **Source selection composes with the traffic class**: the same wildcard session requests DSCP 46 and asserts the reply's `PacketMeta` still reports `traffic_class == Some(0xb8)`, `dscp == Some(46)`, `ecn == Some(0)` (asserted on Linux, where the client exposes receive-side ancillary metadata). - **Wildcard IPv6 loopback**: `[::]:0` serving `[::1]`, exercising `IPV6_RECVPKTINFO`, the received interface index and the source-selected IPv6 send. Skips only on recognized address-family unavailability. - **Construction behavior**, asserted from one test on every target: an explicit bind succeeds, and a wildcard bind either configures source selection and succeeds or returns `WildcardSourceSelectionUnsupported` — which of the two is exactly what the target decides. - Existing explicit-bind IPv4/IPv6 runtime and traffic-class tests are unchanged apart from the `from_socket` signature. ## Scope exclusions - No multiple listeners in one process; one `Server` still owns one socket, one core and one session namespace. - No `ServerFill`; echo payloads stay zero-filled. - No protocol semantics changed, no `irtt-proto` changes, no `irtt-client` production changes, no `ServerCore` behavior changes. - No new upstream research or upstream execution; no protocol-evidence changes under `docs/protocol/**`, and `docs/validation/UPSTREAM_CLIENT_INTEROP.md` is left as the historical record it is. - No Winsock ancillary implementation; only wildcard multi-address binding is unavailable on Windows, not the server. - No unsafe FFI. ## Verification Native (macOS, `aarch64-apple-darwin`), all passing: ``` cargo fmt --all -- --check cargo check --workspace --all-targets --all-features --locked cargo test --workspace --all-targets --all-features --locked cargo clippy --workspace --all-targets --all-features --locked -- -D warnings RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps cargo test -p irtt-client --all-features --locked ``` Target-specific `cargo check -p irtt-server --all-targets --locked`, all passing: `x86_64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl`, `x86_64-unknown-freebsd`, `i686-unknown-linux-gnu`, `x86_64-pc-windows-gnu`. The musl targets matter because OpenWrt is a primary server deployment environment. `aarch64-unknown-linux-gnu` and `x86_64-pc-windows-msvc` are not installed locally and were not checked; no toolchains were installed for this change. `cargo tree -p irtt-server --edges normal` confirms `nix` appears only on the Linux/macOS/FreeBSD targets and not on `x86_64-pc-windows-gnu`, `socket2` remains present everywhere, and `irtt-client` remains dev-only. Manual CLI smoke, both clean with the server surviving the session and shutting down on `Ctrl-C`: - `irtt-server --bind 0.0.0.0:0` served a 1 s client run at 100 ms: 10 sent, 10 received, 0 loss. - `irtt-server --bind 127.0.0.1:0` likewise: 10 sent, 10 received, 0 loss. macOS additionally verified the send-side mechanism is honored rather than ignored: temporarily pinning a wrong source address made the connected client time out, and restoring it made the session pass again.
chatgpt-codex-connector[bot] (Migrated from github.com) reviewed 2026-08-13 17:38:44 +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: 3c554a78d7

ℹ️ 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:** `3c554a78d7` <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>
@@ -47,3 +69,4 @@
select_reply_source: bool,
/// Whether this server has ever successfully applied a nonzero traffic
/// class to the socket.
///
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-08-13 17:38:44 +00:00

P2 Badge Recognize IPv4-mapped wildcard binds

When from_socket receives a socket bound to [::ffff:0.0.0.0]:PORT, Linux treats it as an IPv4 wildcard socket, but Ipv6Addr::is_unspecified() returns false. Consequently select_reply_source stays disabled, and a connected client contacting a secondary local address such as 127.0.0.2 times out because the plain send path can reply from 127.0.0.1. Detect an IPv4-mapped unspecified address as wildcard, or explicitly refuse this bind.

AGENTS.md reference: crates/irtt-server/AGENTS.md:L68-L72

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Recognize IPv4-mapped wildcard binds** When `from_socket` receives a socket bound to `[::ffff:0.0.0.0]:PORT`, Linux treats it as an IPv4 wildcard socket, but `Ipv6Addr::is_unspecified()` returns false. Consequently `select_reply_source` stays disabled, and a connected client contacting a secondary local address such as `127.0.0.2` times out because the plain send path can reply from `127.0.0.1`. Detect an IPv4-mapped unspecified address as wildcard, or explicitly refuse this bind. AGENTS.md reference: [crates/irtt-server/AGENTS.md:L68-L72](https://github.com/Lochnair/irtt-rs/blob/3c554a78d7ba1e984a7a7a6325b81562dc952efa/crates/irtt-server/AGENTS.md#L68-L72) Useful? React with 👍 / 👎.
Lochnair (Migrated from github.com) reviewed 2026-08-13 17:50:43 +00:00
@@ -47,3 +69,4 @@
select_reply_source: bool,
/// Whether this server has ever successfully applied a nonzero traffic
/// class to the socket.
///
Lochnair (Migrated from github.com) commented 2026-08-13 17:50:43 +00:00

Confirmed and fixed in e154508: reproduced on Linux (getsockname returns ::ffff:0.0.0.0 verbatim and the plain reply was discarded by the connected client), so is_wildcard now also treats an IPv4-mapped unspecified address as a wildcard; fixing it surfaced a second bug the same bind exposes — macOS reports an AF_INET peer while delivering IPv6 packet info, so the source-selected send now states the destination in the control message's family — with a classification unit test and an end-to-end mapped-wildcard test added.

🤖 Addressed by Claude Code

Confirmed and fixed in e154508: reproduced on Linux (`getsockname` returns `::ffff:0.0.0.0` verbatim and the plain reply was discarded by the connected client), so `is_wildcard` now also treats an IPv4-mapped unspecified address as a wildcard; fixing it surfaced a second bug the same bind exposes — macOS reports an `AF_INET` peer while delivering IPv6 packet info, so the source-selected send now states the destination in the control message's family — with a classification unit test and an end-to-end mapped-wildcard test added. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
chatgpt-codex-connector[bot] (Migrated from github.com) reviewed 2026-08-13 17:57:31 +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: e154508cb1

ℹ️ 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:** `e154508cb1` <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,376 @@
//! Destination-address ancillary data on Linux, macOS and FreeBSD.
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-08-13 17:57:31 +00:00

P2 Badge Do not pin global IPv6 replies to the ingress interface

When a wildcard listener receives a request on one interface but the route back to the peer uses another, copying ipi6_ifindex into every send forces the reply onto the ingress interface, so sendmsg can fail and the reply is silently lost even though the recovered source address is valid. Preserve the interface index only where the source address is scoped (such as link-local); for global IPv6 and IPv4-mapped destinations, leave it zero so routing can select the egress interface while still pinning the source address.

AGENTS.md reference: crates/irtt-server/AGENTS.md:L63-L72

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Do not pin global IPv6 replies to the ingress interface** When a wildcard listener receives a request on one interface but the route back to the peer uses another, copying `ipi6_ifindex` into every send forces the reply onto the ingress interface, so `sendmsg` can fail and the reply is silently lost even though the recovered source address is valid. Preserve the interface index only where the source address is scoped (such as link-local); for global IPv6 and IPv4-mapped destinations, leave it zero so routing can select the egress interface while still pinning the source address. AGENTS.md reference: [crates/irtt-server/AGENTS.md:L63-L72](https://github.com/Lochnair/irtt-rs/blob/e154508cb1c3c13780a2cefa75758a0c1ca27322/crates/irtt-server/AGENTS.md#L63-L72) Useful? React with 👍 / 👎.
chatgpt-codex-connector[bot] (Migrated from github.com) reviewed 2026-08-14 04:35:33 +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: 2d88939ed4

ℹ️ 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:** `2d88939ed4` <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>
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-08-14 04:35:33 +00:00

P2 Badge Apply IPv4 TOS to dual-stack IPv4 replies

When an IPv6 wildcard listener serves an IPv4 peer—particularly [::ffff:0.0.0.0] on Linux—local_addr() is IPv6-shaped, but the mapped reply leaves as IPv4. This stores false, so prepare_reply_traffic_class sets IPV6_TCLASS; on Linux that leaves the emitted packet's IPv4 TOS at zero, silently discarding every negotiated nonzero DSCP on this newly supported path. Select IP_TOS for mapped/IPv4 replies or ensure both family-specific options are applied.

AGENTS.md reference: crates/irtt-server/AGENTS.md:L219-L226

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Apply IPv4 TOS to dual-stack IPv4 replies** When an IPv6 wildcard listener serves an IPv4 peer—particularly `[::ffff:0.0.0.0]` on Linux—`local_addr()` is IPv6-shaped, but the mapped reply leaves as IPv4. This stores `false`, so `prepare_reply_traffic_class` sets `IPV6_TCLASS`; on Linux that leaves the emitted packet's IPv4 TOS at zero, silently discarding every negotiated nonzero DSCP on this newly supported path. Select `IP_TOS` for mapped/IPv4 replies or ensure both family-specific options are applied. AGENTS.md reference: [crates/irtt-server/AGENTS.md:L219-L226](https://github.com/Lochnair/irtt-rs/blob/2d88939ed47130862635fb7952f1ce77c313c22d/crates/irtt-server/AGENTS.md#L219-L226) Useful? React with 👍 / 👎.
Lochnair (Migrated from github.com) reviewed 2026-08-14 04:49:31 +00:00
Lochnair (Migrated from github.com) commented 2026-08-14 04:49:31 +00:00

Confirmed on both kernels and fixed in aed7eef, but scoped narrower than suggested. On Linux a mapped bind ([::ffff:0.0.0.0], which this PR newly supports) does leave TOS at 0 under IPV6_TCLASS and marks correctly under IP_TOS, so a mapped bound address is now read as the IPv4 listener it is — with a unit test and a Linux DSCP assertion on the mapped-wildcard end-to-end test. I deliberately did not select per reply for a genuinely dual-stack [::] listener: macOS rejects IP_TOS on an AF_INET6 socket with EINVAL and marks nothing either way, so choosing per reply would trade an unmarked reply for a dropped one there (an unappliable marking stops the send, by PR #84's policy). That case predates this PR and is now recorded in AGENTS.md as a known gap for its own slice.

🤖 Addressed by Claude Code

Confirmed on both kernels and fixed in aed7eef, but scoped narrower than suggested. On Linux a mapped bind (`[::ffff:0.0.0.0]`, which this PR newly supports) does leave TOS at 0 under `IPV6_TCLASS` and marks correctly under `IP_TOS`, so a mapped bound address is now read as the IPv4 listener it is — with a unit test and a Linux DSCP assertion on the mapped-wildcard end-to-end test. I deliberately did **not** select per reply for a genuinely dual-stack `[::]` listener: macOS rejects `IP_TOS` on an `AF_INET6` socket with EINVAL and marks nothing either way, so choosing per reply would trade an unmarked reply for a *dropped* one there (an unappliable marking stops the send, by PR #84's policy). That case predates this PR and is now recorded in AGENTS.md as a known gap for its own slice. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Sign in to join this conversation.