From 86ee96aa766969a631755ecf1f4792f878144ecd Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Sun, 5 Apr 2026 17:27:39 +0100 Subject: [PATCH 1/4] Add FreeBSD multicast TTL regression test --- src/socket.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/socket.rs b/src/socket.rs index b258c93c..6c731c6c 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -2445,3 +2445,23 @@ from!(net::UdpSocket, Socket); from!(Socket, net::TcpStream); from!(Socket, net::TcpListener); from!(Socket, net::UdpSocket); + +#[cfg(test)] +mod tests { + #[test] + #[cfg(target_os = "freebsd")] + fn set_multicast_ttl_v4_matches_std_for_values_above_u8_range() { + use super::Socket; + use crate::{Domain, Protocol, Type}; + use std::net::{Ipv4Addr, UdpSocket}; + + let std_socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + std_socket.set_multicast_ttl_v4(258).unwrap(); + let std_ttl = std_socket.multicast_ttl_v4().unwrap(); + assert_eq!(std_ttl, 2); + + let socket2_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap(); + socket2_socket.set_multicast_ttl_v4(258).unwrap(); + assert_eq!(socket2_socket.multicast_ttl_v4().unwrap(), std_ttl); + } +} From 7475821a04ca8861364da10d2ce4474fb78c5287 Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Sun, 5 Apr 2026 17:28:20 +0100 Subject: [PATCH 2/4] Match BSD multicast ABI to system headers --- src/socket.rs | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/socket.rs b/src/socket.rs index 6c731c6c..363bec31 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -16,6 +16,16 @@ use std::net::Ipv6Addr; use std::net::{self, Ipv4Addr, Shutdown}; #[cfg(any(unix, all(target_os = "wasi", not(target_env = "p1"))))] use std::os::fd::{FromRawFd, IntoRawFd}; +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "nto", +))] +use std::os::raw::c_uchar; #[cfg(windows)] use std::os::windows::io::{FromRawSocket, IntoRawSocket}; use std::time::Duration; @@ -27,6 +37,30 @@ use crate::{Domain, Protocol, SockAddr, TcpKeepalive, Type}; #[cfg(not(any(target_os = "redox", target_os = "wasi")))] use crate::{MaybeUninitSlice, MsgHdr, RecvFlags}; +// Match the system headers for these IPv4 multicast socket options. These +// targets declare `unsigned char` rather than `int`. +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "nto", +))] +type IpV4MultiCastType = c_uchar; + +#[cfg(not(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "nto", +)))] +type IpV4MultiCastType = c_int; + /// Owned wrapper around a system socket. /// /// This type simply wraps an instance of a file descriptor (`c_int`) on Unix @@ -1546,7 +1580,7 @@ impl Socket { /// [`set_multicast_loop_v4`]: Socket::set_multicast_loop_v4 pub fn multicast_loop_v4(&self) -> io::Result { unsafe { - getsockopt::(self.as_raw(), sys::IPPROTO_IP, sys::IP_MULTICAST_LOOP) + getsockopt::(self.as_raw(), sys::IPPROTO_IP, sys::IP_MULTICAST_LOOP) .map(|loop_v4| loop_v4 != 0) } } @@ -1561,7 +1595,7 @@ impl Socket { self.as_raw(), sys::IPPROTO_IP, sys::IP_MULTICAST_LOOP, - loop_v4 as c_int, + loop_v4 as IpV4MultiCastType, ) } } @@ -1573,7 +1607,7 @@ impl Socket { /// [`set_multicast_ttl_v4`]: Socket::set_multicast_ttl_v4 pub fn multicast_ttl_v4(&self) -> io::Result { unsafe { - getsockopt::(self.as_raw(), sys::IPPROTO_IP, sys::IP_MULTICAST_TTL) + getsockopt::(self.as_raw(), sys::IPPROTO_IP, sys::IP_MULTICAST_TTL) .map(|ttl| ttl as u32) } } @@ -1591,7 +1625,7 @@ impl Socket { self.as_raw(), sys::IPPROTO_IP, sys::IP_MULTICAST_TTL, - ttl as c_int, + ttl as IpV4MultiCastType, ) } } From 92be67df7b0f9894bdbfabb1062c4782973c1172 Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Mon, 6 Apr 2026 22:17:06 +0100 Subject: [PATCH 3/4] Incorporate review feedback --- src/socket.rs | 20 --------- tests/socket.rs | 105 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 20 deletions(-) diff --git a/src/socket.rs b/src/socket.rs index 363bec31..45d8ea55 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -2479,23 +2479,3 @@ from!(net::UdpSocket, Socket); from!(Socket, net::TcpStream); from!(Socket, net::TcpListener); from!(Socket, net::UdpSocket); - -#[cfg(test)] -mod tests { - #[test] - #[cfg(target_os = "freebsd")] - fn set_multicast_ttl_v4_matches_std_for_values_above_u8_range() { - use super::Socket; - use crate::{Domain, Protocol, Type}; - use std::net::{Ipv4Addr, UdpSocket}; - - let std_socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); - std_socket.set_multicast_ttl_v4(258).unwrap(); - let std_ttl = std_socket.multicast_ttl_v4().unwrap(); - assert_eq!(std_ttl, 2); - - let socket2_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap(); - socket2_socket.set_multicast_ttl_v4(258).unwrap(); - assert_eq!(socket2_socket.multicast_ttl_v4().unwrap(), std_ttl); - } -} diff --git a/tests/socket.rs b/tests/socket.rs index 6dc1aea0..d4ac1cbf 100644 --- a/tests/socket.rs +++ b/tests/socket.rs @@ -1,4 +1,14 @@ #![allow(clippy::bool_assert_comparison)] +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "nto", +))] +use std::ffi::c_uchar; #[cfg(all( feature = "all", any( @@ -21,6 +31,16 @@ use std::io::Write; #[cfg(not(target_os = "vita"))] use std::mem::MaybeUninit; use std::mem::{self}; +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "nto", +))] +use std::net::UdpSocket; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpStream}; #[cfg(not(any(target_os = "redox", target_os = "vita")))] use std::net::{Ipv6Addr, SocketAddrV6}; @@ -1970,3 +1990,88 @@ fn set_busy_poll() { assert!(socket.busy_poll().unwrap() == i); } } + +/// A helper type to allow testing socket options on both `Socket` and `UdpSocket`. +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "nto", +))] +pub enum SocketRef<'a> { + Socket(&'a Socket), + UdpSocket(&'a UdpSocket), +} + +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "nto", +))] +impl<'a> From<&'a Socket> for SocketRef<'a> { + fn from(socket: &'a Socket) -> Self { + SocketRef::Socket(socket) + } +} + +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "nto", +))] +impl<'a> From<&'a UdpSocket> for SocketRef<'a> { + fn from(socket: &'a UdpSocket) -> Self { + SocketRef::UdpSocket(socket) + } +} + +/// Assert that `multicast_ttl_v4` is set to a given value on `socket`. +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "nto", +))] +#[track_caller] +pub fn assert_multicast_ttl_v4<'a>(socket: impl Into>, want: c_uchar) { + let socket = socket.into(); + let ttl = match socket { + SocketRef::Socket(socket) => socket.multicast_ttl_v4().unwrap(), + SocketRef::UdpSocket(socket) => socket.multicast_ttl_v4().unwrap(), + }; + assert_eq!(ttl, u32::from(want), "multicast_ttl_v4 option"); +} + +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "nto", +))] +#[test] +fn multicast_v4_bsd_abi() { + let socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + socket.set_multicast_ttl_v4(258).unwrap(); + assert_multicast_ttl_v4(&socket, 2); + + let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap(); + socket.set_multicast_ttl_v4(258).unwrap(); + assert_multicast_ttl_v4(&socket, 2); +} From 7ddafc0053d77d4c9e486aa1edd568967f34d051 Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Mon, 6 Apr 2026 22:36:04 +0100 Subject: [PATCH 4/4] Remove cfgs from assert helpers, use platform independent 'want' type --- tests/socket.rs | 62 +++---------------------------------------------- 1 file changed, 3 insertions(+), 59 deletions(-) diff --git a/tests/socket.rs b/tests/socket.rs index d4ac1cbf..d3a9a564 100644 --- a/tests/socket.rs +++ b/tests/socket.rs @@ -1,14 +1,4 @@ #![allow(clippy::bool_assert_comparison)] -#[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "solaris", - target_os = "illumos", - target_os = "nto", -))] -use std::ffi::c_uchar; #[cfg(all( feature = "all", any( @@ -31,17 +21,7 @@ use std::io::Write; #[cfg(not(target_os = "vita"))] use std::mem::MaybeUninit; use std::mem::{self}; -#[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "solaris", - target_os = "illumos", - target_os = "nto", -))] -use std::net::UdpSocket; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpStream}; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpStream, UdpSocket}; #[cfg(not(any(target_os = "redox", target_os = "vita")))] use std::net::{Ipv6Addr, SocketAddrV6}; #[cfg(all( @@ -1992,44 +1972,17 @@ fn set_busy_poll() { } /// A helper type to allow testing socket options on both `Socket` and `UdpSocket`. -#[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "solaris", - target_os = "illumos", - target_os = "nto", -))] pub enum SocketRef<'a> { Socket(&'a Socket), UdpSocket(&'a UdpSocket), } -#[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "solaris", - target_os = "illumos", - target_os = "nto", -))] impl<'a> From<&'a Socket> for SocketRef<'a> { fn from(socket: &'a Socket) -> Self { SocketRef::Socket(socket) } } -#[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "solaris", - target_os = "illumos", - target_os = "nto", -))] impl<'a> From<&'a UdpSocket> for SocketRef<'a> { fn from(socket: &'a UdpSocket) -> Self { SocketRef::UdpSocket(socket) @@ -2037,23 +1990,14 @@ impl<'a> From<&'a UdpSocket> for SocketRef<'a> { } /// Assert that `multicast_ttl_v4` is set to a given value on `socket`. -#[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "solaris", - target_os = "illumos", - target_os = "nto", -))] #[track_caller] -pub fn assert_multicast_ttl_v4<'a>(socket: impl Into>, want: c_uchar) { +pub fn assert_multicast_ttl_v4<'a>(socket: impl Into>, want: u32) { let socket = socket.into(); let ttl = match socket { SocketRef::Socket(socket) => socket.multicast_ttl_v4().unwrap(), SocketRef::UdpSocket(socket) => socket.multicast_ttl_v4().unwrap(), }; - assert_eq!(ttl, u32::from(want), "multicast_ttl_v4 option"); + assert_eq!(ttl, want, "multicast_ttl_v4 option"); } #[cfg(any(