From d4d9be274d22d0c85add4543aa0c021dd37be486 Mon Sep 17 00:00:00 2001 From: Kristof Mattei <864376+kristof-mattei@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:27:21 -0700 Subject: [PATCH] Use `u_char` for `IP_MULTICAST_LOOP` and `IP_MULTICAST_TTL` on OpenBSD, NetBSD, illumos and Solaris `getsockopt` writes 1 byte on all four platforms. `setsockopt` requires a 1-byte value on OpenBSD and illumos. NetBSD's `setsockopt` also accepts a 4-byte value, but returns `EINVAL` for a TTL above 255, so a 1-byte value does not reduce the TTL range. Also prevents `set_multicast_ttl_v4(u32::MAX)` from wrapping to `-1` on Linux, which sets a TTL of 1 instead of returning an error. --- .github/workflows/main.yml | 13 ++++++++++ src/socket.rs | 20 ++++++--------- src/sys/unix.rs | 15 +++++++++++ src/sys/windows.rs | 2 ++ tests/socket.rs | 51 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dcb329a8..6f7afa6e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -190,3 +190,16 @@ jobs: run: | . $HOME/.cargo/env cargo hack test --target ${{ matrix.target }} --feature-powerset + openbsd: + name: OpenBSD/x86_64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Test on OpenBSD + uses: vmactions/openbsd-vm@v1 + with: + release: "8.0" + prepare: | + pkg_add rust # `rustup` has no toolchain for OpenBSD + run: | + cargo test && cargo test --features all diff --git a/src/socket.rs b/src/socket.rs index 7ea57f4b..4215e087 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -20,7 +20,7 @@ use std::os::fd::{FromRawFd, IntoRawFd}; use std::os::windows::io::{FromRawSocket, IntoRawSocket}; use std::time::Duration; -use crate::sys::{self, c_int, getsockopt, setsockopt, Bool}; +use crate::sys::{self, c_int, getsockopt, setsockopt, Bool, IpMulticastOpt}; #[cfg(all(unix, not(any(target_os = "redox", target_os = "horizon"))))] use crate::MsgHdrMut; use crate::{Domain, Protocol, SockAddr, TcpKeepalive, Type}; @@ -1562,7 +1562,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) } } @@ -1577,7 +1577,7 @@ impl Socket { self.as_raw(), sys::IPPROTO_IP, sys::IP_MULTICAST_LOOP, - loop_v4 as c_int, + IpMulticastOpt::from(loop_v4), ) } } @@ -1589,7 +1589,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) } } @@ -1602,14 +1602,10 @@ impl Socket { /// /// Note that this may not have any affect on IPv6 sockets. pub fn set_multicast_ttl_v4(&self, ttl: u32) -> io::Result<()> { - unsafe { - setsockopt( - self.as_raw(), - sys::IPPROTO_IP, - sys::IP_MULTICAST_TTL, - ttl as c_int, - ) - } + let ttl = IpMulticastOpt::try_from(ttl).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "multicast TTL out of range") + })?; + unsafe { setsockopt(self.as_raw(), sys::IPPROTO_IP, sys::IP_MULTICAST_TTL, ttl) } } /// Get the value of the `IP_TTL` option for this socket. diff --git a/src/sys/unix.rs b/src/sys/unix.rs index 8027d325..98cf0171 100644 --- a/src/sys/unix.rs +++ b/src/sys/unix.rs @@ -338,6 +338,21 @@ pub(crate) use libc::{TCP_KEEPCNT, TCP_KEEPINTVL}; // See this type in the Windows file. pub(crate) type Bool = c_int; +#[cfg(any( + target_os = "illumos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "solaris", +))] +pub(crate) type IpMulticastOpt = libc::c_uchar; +#[cfg(not(any( + target_os = "illumos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "solaris", +)))] +pub(crate) type IpMulticastOpt = c_int; + #[cfg(any( target_os = "ios", target_os = "visionos", diff --git a/src/sys/windows.rs b/src/sys/windows.rs index b1fa20fc..53032018 100644 --- a/src/sys/windows.rs +++ b/src/sys/windows.rs @@ -94,6 +94,8 @@ pub(crate) use windows_sys::Win32::Networking::WinSock::{ pub(crate) const IPPROTO_IP: c_int = windows_sys::Win32::Networking::WinSock::IPPROTO_IP as c_int; pub(crate) const SOL_SOCKET: c_int = windows_sys::Win32::Networking::WinSock::SOL_SOCKET as c_int; +pub(crate) type IpMulticastOpt = c_int; + // This is so we can special case MaybeUninit::assume_init for Bool. // See Bool for why. pub(crate) trait GetsockoptOutput: Sized { diff --git a/tests/socket.rs b/tests/socket.rs index 0f390e3a..d8c7a967 100644 --- a/tests/socket.rs +++ b/tests/socket.rs @@ -1648,6 +1648,57 @@ test!(IPv4 multicast_all_v4, set_multicast_all_v4(false)); #[cfg(all(feature = "all", target_os = "linux"))] test!(IPv6 multicast_all_v6, set_multicast_all_v6(false)); +#[test] +fn multicast_loop_v4() { + let socket = Socket::new(Domain::IPV4, Type::DGRAM, None).unwrap(); + + let initial = socket + .multicast_loop_v4() + .expect("failed to get initial value"); + + assert_ne!(initial, false, "initial value and argument are the same"); + + socket + .set_multicast_loop_v4(false) + .expect("failed to set option"); + + let got = socket.multicast_loop_v4().expect("failed to get value"); + + assert_eq!(got, false, "set and get values differ"); +} + +#[test] +fn multicast_ttl_v4() { + let socket = Socket::new(Domain::IPV4, Type::DGRAM, None).unwrap(); + + let initial = socket + .multicast_ttl_v4() + .expect("failed to get initial value"); + + assert_ne!(initial, 255, "initial value and argument are the same"); + + socket + .set_multicast_ttl_v4(255) + .expect("failed to set option"); + + let got = socket.multicast_ttl_v4().expect("failed to get value"); + + assert_eq!(got, 255, "set and get values differ"); +} + +#[test] +fn multicast_ttl_v4_out_of_range() { + let socket = Socket::new(Domain::IPV4, Type::DGRAM, None).unwrap(); + + for ttl in [256, u32::MAX] { + match socket.set_multicast_ttl_v4(ttl) { + Ok(()) => panic!("unexpected success for {}", ttl), + Err(ref err) if err.kind() == io::ErrorKind::InvalidInput => {} + Err(err) => panic!("unexpected error {}", err), + } + } +} + #[test] #[cfg(not(any( target_os = "haiku",