From ecedfc091d4ea924605a734616da92b99648f676 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 28 Oct 2026 13:41:10 +1100 Subject: [PATCH] fix(udp): honor IPv4 source addresses on Apple platforms Darwin ignores `IP_RECVDSTADDR` as an outgoing IPv4 source-address override. A wildcard socket therefore sends from the route's default address, regardless of what we set in `src_ip`. Instead of `IP_RECVDSTADDR`, we now use `IP_PKTINFO` for IPv4, similar to `IPV6_PKTINFO` for IPv6. `ipi_ifindex` remains set to zero so routing selects the interface, otherwise we would force callers to provide an interface index. The provided test fails prior to the fix and is kept separate due to its particular setup requirements. --- Cargo.lock | 23 ++++++++++++++++ quinn-udp/Cargo.toml | 3 +++ quinn-udp/src/apple_fast.rs | 12 +++++---- quinn-udp/src/unix.rs | 6 ++--- quinn-udp/tests/tests.rs | 53 +++++++++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c186423159..bd1228a4b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1591,6 +1591,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1634,6 +1643,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "7.1.3" @@ -2024,6 +2046,7 @@ dependencies = [ "criterion", "libc", "log", + "nix", "socket2", "tokio", "tracing", diff --git a/quinn-udp/Cargo.toml b/quinn-udp/Cargo.toml index fc8475f833..6ea67ad41e 100644 --- a/quinn-udp/Cargo.toml +++ b/quinn-udp/Cargo.toml @@ -36,6 +36,9 @@ windows-sys = { workspace = true } criterion = { version = "0.8", default-features = false, features = ["async_tokio"] } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "net"] } +[target.'cfg(target_vendor = "apple")'.dev-dependencies] +nix = { version = "0.31.3", default-features = false, features = ["net"] } + [build-dependencies] cfg_aliases = { workspace = true } diff --git a/quinn-udp/src/apple_fast.rs b/quinn-udp/src/apple_fast.rs index 4adbfa13f5..6e900dc5b1 100644 --- a/quinn-udp/src/apple_fast.rs +++ b/quinn-udp/src/apple_fast.rs @@ -133,12 +133,14 @@ fn prepare_msg_x( if let Some(ip) = &transmit.src_ip { match ip { IpAddr::V4(v4) => { - if encode_src_ip { - let addr = libc::in_addr { + let pktinfo = libc::in_pktinfo { + ipi_ifindex: 0, + ipi_spec_dst: libc::in_addr { s_addr: u32::from_ne_bytes(v4.octets()), - }; - encoder.push(libc::IPPROTO_IP, libc::IP_RECVDSTADDR, addr); - } + }, + ipi_addr: libc::in_addr { s_addr: 0 }, + }; + encoder.push(libc::IPPROTO_IP, libc::IP_PKTINFO, pktinfo); } IpAddr::V6(v6) => { let pktinfo = libc::in6_pktinfo { diff --git a/quinn-udp/src/unix.rs b/quinn-udp/src/unix.rs index db943e8d4b..bc2a64a546 100644 --- a/quinn-udp/src/unix.rs +++ b/quinn-udp/src/unix.rs @@ -685,7 +685,7 @@ fn prepare_msg( hdr: &mut libc::msghdr, iov: &mut libc::iovec, ctrl: &mut cmsg::Aligned<[u8; cmsg::LEN]>, - #[allow(unused_variables)] // only used on FreeBSD & macOS + #[allow(unused_variables)] // only used on BSD & Solaris encode_src_ip: bool, sendmsg_einval: bool, ) { @@ -734,7 +734,7 @@ fn prepare_msg( if let Some(ip) = &transmit.src_ip { match ip { IpAddr::V4(v4) => { - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "android", apple))] { let pktinfo = libc::in_pktinfo { ipi_ifindex: 0, @@ -745,7 +745,7 @@ fn prepare_msg( }; encoder.push(libc::IPPROTO_IP, libc::IP_PKTINFO, pktinfo); } - #[cfg(any(bsd, apple, solarish))] + #[cfg(any(bsd, solarish))] { if encode_src_ip { let addr = libc::in_addr { diff --git a/quinn-udp/tests/tests.rs b/quinn-udp/tests/tests.rs index 7c95d2ef67..f51d6a2e9c 100644 --- a/quinn-udp/tests/tests.rs +++ b/quinn-udp/tests/tests.rs @@ -79,6 +79,59 @@ fn basic_src_ip() { ); } +#[test] +#[cfg(apple)] +fn ipv4_source_address_override() { + use nix::{ifaddrs::getifaddrs, net::if_::InterfaceFlags}; + + let Some(src_ip) = getifaddrs() + .unwrap() + .filter(|interface| interface.flags.contains(InterfaceFlags::IFF_UP)) + .filter_map(|interface| Some(interface.address?.as_sockaddr_in()?.ip())) + .find(|ip| { + *ip != Ipv4Addr::LOCALHOST + && !ip.is_unspecified() + && !ip.is_multicast() + && !ip.is_broadcast() + }) + else { + eprintln!("skipping source override test: no alternate assigned IPv4 address"); + return; + }; + + let send = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap(); + let recv = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + recv.set_read_timeout(Some(std::time::Duration::from_secs(3))) + .unwrap(); + let send_state = UdpSocketState::new((&send).into()).unwrap(); + #[cfg(apple_fast)] + // SAFETY: sendmsg_x is available on the macOS test host. + unsafe { + send_state.set_apple_fast_path(); + } + UdpSockRef::from(&send).set_nonblocking(false).unwrap(); + + let contents = b"hello"; + let sent = send_state + .try_send( + (&send).into(), + &Transmit { + destination: recv.local_addr().unwrap(), + ecn: None, + contents, + segment_size: None, + src_ip: Some(src_ip.into()), + }, + ) + .unwrap(); + assert_eq!(sent.get(), 1); + + let mut buf = [0; 32]; + let (received, source) = recv.recv_from(&mut buf).unwrap(); + assert_eq!(&buf[..received], contents); + assert_eq!(source.ip(), src_ip); +} + // The WASI backend sets no ECN codepoint and always reports `None`. #[test] #[cfg(not(target_os = "wasi"))]