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.
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:
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.
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.
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>
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.
**<sub><sub></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 👍 / 👎.
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.
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)_
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>
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.
**<sub><sub></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 👍 / 👎.
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>
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.
**<sub><sub></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 👍 / 👎.
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.
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)_
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
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.
recv_from/send_topathunchanged, on every target.
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
ServerCore,SessionandOutboundDatagramare unchanged and know nothing about localaddresses, interfaces or packet info.
recvmsgthrough Tokio readiness(
readable()+try_io,MSG_DONTWAIT), the same shape the client'sancillary receive path already uses.
sendmsgwith a per-packet source controlmessage, integrated the same way through
writable()+try_io.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.
ServerCore, one sequential sender. No per-datagramtask, no lock, no transport trait, no spawned thread.
MSG_CTRUNC, orMSG_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.
only, never terminates the listener and never rolls back core state.
#![forbid(unsafe_code)]is intact and everything goesthrough
nix's saferecvmsg/sendmsg,ControlMessage,ControlMessageOwnedandsockoptwrappers — no raw syscall, noCMSG_*walking, no hand-written ABI layer.
nixis a target-specific dependency andis not pulled in on Windows.
Per-platform mechanisms:
IP_PKTINFOIP_PKTINFOIPV6_RECVPKTINFOIPV6_PKTINFOIP_RECVDSTADDRIP_PKTINFOIPV6_RECVPKTINFOIPV6_PKTINFOIP_RECVDSTADDRIP_SENDSRCADDRIPV6_RECVPKTINFOIPV6_PKTINFOEach direction's control message is built explicitly:
in_pktinfocarries threefields 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_socketis now fallible: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::bindpropagates it. There is oneconstruction path — no
try_from_socketbeside an infalliblefrom_socket.Two new
ServerRuntimeErrorvariants carry it:WildcardSourceSelectionUnsupported { addr }andSourceSelectionSetup { addr, source }. Neither exposes anixerror type.Tests
127.0.0.2:<port>where the hostcarries the whole
127/8range, falling back to127.0.0.1where it does not(macOS assigns only
127.0.0.1tolo0, and configuring an alias needsprivileges). A real
AsyncClienton a connected socket is the assertion: areply from any other source is discarded by the kernel, so open/echo/close
completing proves the source address without inspecting it.
session requests DSCP 46 and asserts the reply's
PacketMetastill reportstraffic_class == Some(0xb8),dscp == Some(46),ecn == Some(0)(assertedon Linux, where the client exposes receive-side ancillary metadata).
[::]:0serving[::1], exercisingIPV6_RECVPKTINFO, the received interface index and the source-selected IPv6send. Skips only on recognized address-family unavailability.
bind succeeds, and a wildcard bind either configures source selection and
succeeds or returns
WildcardSourceSelectionUnsupported— which of the two isexactly what the target decides.
apart from the
from_socketsignature.Scope exclusions
Serverstill owns one socket, onecore and one session namespace.
ServerFill; echo payloads stay zero-filled.irtt-protochanges, noirtt-clientproduction changes, no
ServerCorebehavior changes.under
docs/protocol/**, anddocs/validation/UPSTREAM_CLIENT_INTEROP.mdisleft as the historical record it is.
unavailable on Windows, not the server.
Verification
Native (macOS,
aarch64-apple-darwin), all passing:Target-specific
cargo check -p irtt-server --all-targets --locked, allpassing:
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 primaryserver deployment environment.
aarch64-unknown-linux-gnuandx86_64-pc-windows-msvcare not installed locally and were not checked; notoolchains were installed for this change.
cargo tree -p irtt-server --edges normalconfirmsnixappears only on theLinux/macOS/FreeBSD targets and not on
x86_64-pc-windows-gnu,socket2remainspresent everywhere, and
irtt-clientremains 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:0served a 1 s client run at 100 ms: 10 sent,10 received, 0 loss.
irtt-server --bind 127.0.0.1:0likewise: 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.
💡 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
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".
@@ -47,3 +69,4 @@select_reply_source: bool,/// Whether this server has ever successfully applied a nonzero traffic/// class to the socket.///When
from_socketreceives a socket bound to[::ffff:0.0.0.0]:PORT, Linux treats it as an IPv4 wildcard socket, butIpv6Addr::is_unspecified()returns false. Consequentlyselect_reply_sourcestays disabled, and a connected client contacting a secondary local address such as127.0.0.2times out because the plain send path can reply from127.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 👍 / 👎.
@@ -47,3 +69,4 @@select_reply_source: bool,/// Whether this server has ever successfully applied a nonzero traffic/// class to the socket.///Confirmed and fixed in
e154508: reproduced on Linux (getsocknamereturns::ffff:0.0.0.0verbatim and the plain reply was discarded by the connected client), sois_wildcardnow also treats an IPv4-mapped unspecified address as a wildcard; fixing it surfaced a second bug the same bind exposes — macOS reports anAF_INETpeer 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
💡 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
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".
@@ -0,0 +1,376 @@//! Destination-address ancillary data on Linux, macOS and FreeBSD.When a wildcard listener receives a request on one interface but the route back to the peer uses another, copying
ipi6_ifindexinto every send forces the reply onto the ingress interface, sosendmsgcan 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 👍 / 👎.
💡 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
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".
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 storesfalse, soprepare_reply_traffic_classsetsIPV6_TCLASS; on Linux that leaves the emitted packet's IPv4 TOS at zero, silently discarding every negotiated nonzero DSCP on this newly supported path. SelectIP_TOSfor mapped/IPv4 replies or ensure both family-specific options are applied.AGENTS.md reference: crates/irtt-server/AGENTS.md:L219-L226
Useful? React with 👍 / 👎.
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 underIPV6_TCLASSand marks correctly underIP_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 rejectsIP_TOSon anAF_INET6socket 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