From 28fc66b0cff6a9a9aaa859e9db5cfb6c0080a855 Mon Sep 17 00:00:00 2001 From: Rudi Floren Date: Tue, 29 Sep 2026 18:40:56 +0200 Subject: [PATCH] fix(net): use IPPROTO_IPV6 for IPV6_MULTICAST_HOPS Both backends set and read IPV6_MULTICAST_HOPS at IPPROTO_IP. It is an IPv6 option, so the call fails with EINVAL and the multicast hop limit of an IPv6 socket cannot be set at all. This is the surviving half of the copy+pasto that #639 fixed in April 2023. That commit corrected the option constant and left the protocol level. #855 later moved the code to its present file, which hides the origin from git blame. The test did not catch it because it accepted Err(INVAL), which is the exact error a wrong protocol level returns. It also read the option on a stream socket, where the value is meaningless, so the Ok arm never ran. Read the option on a datagram socket instead, and add a round trip for the setter. The round trip uses 200 because no platform defaults to it, so it cannot pass on an untouched value. The Ok arm now runs, and the default is 1, not 0. The deleted comment read that 1 as a NetBSD quirk. NetBSD was the only system reporting the true value; elsewhere the call errored into the INVAL arm. The default of 1 is measured on Linux and macOS. NetBSD is untested here. --- src/backend/libc/net/sockopt.rs | 4 +-- src/backend/linux_raw/net/sockopt.rs | 4 +-- tests/net/sockopt.rs | 37 +++++++++++++++++++++------- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/backend/libc/net/sockopt.rs b/src/backend/libc/net/sockopt.rs index 971048830..286adde62 100644 --- a/src/backend/libc/net/sockopt.rs +++ b/src/backend/libc/net/sockopt.rs @@ -618,12 +618,12 @@ pub(crate) fn ipv6_multicast_loop(fd: BorrowedFd<'_>) -> io::Result { #[inline] pub(crate) fn set_ipv6_multicast_hops(fd: BorrowedFd<'_>, multicast_hops: u32) -> io::Result<()> { - setsockopt(fd, c::IPPROTO_IP, c::IPV6_MULTICAST_HOPS, multicast_hops) + setsockopt(fd, c::IPPROTO_IPV6, c::IPV6_MULTICAST_HOPS, multicast_hops) } #[inline] pub(crate) fn ipv6_multicast_hops(fd: BorrowedFd<'_>) -> io::Result { - getsockopt(fd, c::IPPROTO_IP, c::IPV6_MULTICAST_HOPS) + getsockopt(fd, c::IPPROTO_IPV6, c::IPV6_MULTICAST_HOPS) } #[inline] diff --git a/src/backend/linux_raw/net/sockopt.rs b/src/backend/linux_raw/net/sockopt.rs index a8b281925..33ed5c67e 100644 --- a/src/backend/linux_raw/net/sockopt.rs +++ b/src/backend/linux_raw/net/sockopt.rs @@ -580,12 +580,12 @@ pub(crate) fn ipv6_multicast_loop(fd: BorrowedFd<'_>) -> io::Result { #[inline] pub(crate) fn set_ipv6_multicast_hops(fd: BorrowedFd<'_>, multicast_hops: u32) -> io::Result<()> { - setsockopt(fd, c::IPPROTO_IP, c::IPV6_MULTICAST_HOPS, multicast_hops) + setsockopt(fd, c::IPPROTO_IPV6, c::IPV6_MULTICAST_HOPS, multicast_hops) } #[inline] pub(crate) fn ipv6_multicast_hops(fd: BorrowedFd<'_>) -> io::Result { - getsockopt(fd, c::IPPROTO_IP, c::IPV6_MULTICAST_HOPS) + getsockopt(fd, c::IPPROTO_IPV6, c::IPV6_MULTICAST_HOPS) } #[inline] diff --git a/tests/net/sockopt.rs b/tests/net/sockopt.rs index d139d9a5c..d79c93307 100644 --- a/tests/net/sockopt.rs +++ b/tests/net/sockopt.rs @@ -398,6 +398,7 @@ fn test_sockopts_ipv6() { crate::init(); let s = rustix::net::socket(AddressFamily::INET6, SocketType::STREAM, None).unwrap(); + let ds = rustix::net::socket(AddressFamily::INET6, SocketType::DGRAM, None).unwrap(); test_sockopts_socket(&s); @@ -415,7 +416,7 @@ fn test_sockopts_ipv6() { assert_eq!(sockopt::socket_domain(&s).unwrap(), AddressFamily::INET6); assert_ne!(sockopt::ipv6_unicast_hops(&s).unwrap(), 0); - match sockopt::ipv6_multicast_loop(&s) { + match sockopt::ipv6_multicast_loop(&ds) { Ok(multicast_loop) => assert!(multicast_loop), Err(io::Errno::OPNOTSUPP) => (), Err(io::Errno::INVAL) => (), @@ -424,13 +425,31 @@ fn test_sockopts_ipv6() { } assert_ne!(sockopt::ipv6_unicast_hops(&s).unwrap(), 0); - // On NetBSD, `get_ipv6_multicasthops` returns 1 here. It's not evident - // why it differs from other OS's. - #[cfg(not(target_os = "netbsd"))] - match sockopt::ipv6_multicast_hops(&s) { - Ok(hops) => assert_eq!(hops, 0), + match sockopt::ipv6_multicast_hops(&ds) { + Ok(hops) => assert_eq!(hops, 1), + Err(io::Errno::NOPROTOOPT) => (), + Err(err) => panic!("{:?}", err), + } + + // Test IPv6 multicast hops roundtrip, use a value that is not a default on a known system. + let before = sockopt::ipv6_multicast_hops(&ds).unwrap(); + match sockopt::set_ipv6_multicast_hops(&ds, 200) { + Ok(()) => { + assert_eq!(sockopt::ipv6_multicast_hops(&ds).unwrap(), 200); + sockopt::set_ipv6_multicast_hops(&ds, before).unwrap(); + } + Err(io::Errno::NOPROTOOPT) => (), + Err(err) => panic!("{:?}", err), + } + + // Test IPv6 unicast hops roundtrip, use a value that is not a default on a known system. + let before = sockopt::ipv6_unicast_hops(&s).unwrap(); + match sockopt::set_ipv6_unicast_hops(&s, Some(200)) { + Ok(()) => { + assert_eq!(sockopt::ipv6_unicast_hops(&s).unwrap(), 200); + sockopt::set_ipv6_unicast_hops(&s, Some(before)).unwrap(); + } Err(io::Errno::NOPROTOOPT) => (), - Err(io::Errno::INVAL) => (), Err(err) => panic!("{:?}", err), } @@ -442,10 +461,10 @@ fn test_sockopts_ipv6() { assert_eq!(sockopt::ipv6_v6only(&s).unwrap(), !v6only); // Set the IPv6 multicast loop value. - match sockopt::set_ipv6_multicast_loop(&s, false) { + match sockopt::set_ipv6_multicast_loop(&ds, false) { Ok(()) => { // Check that the IPv6 multicast loop value is set. - match sockopt::ipv6_multicast_loop(&s) { + match sockopt::ipv6_multicast_loop(&ds) { Ok(multicast_loop) => assert!(!multicast_loop), Err(err) => panic!("{:?}", err), }