From 750f83618b967c620bbfdf6ca04de7362bdb42b5 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Thu, 13 Jan 2022 11:39:43 -0500 Subject: [PATCH] Implement {join,leave}_multicast_v4_n These methods allow the local interface to be specified by its index or an address assigned to it. This is supported in Linux since 2.2, in FreeBSD since 13.0.0, and in macOS since 10.7. DragonFlyBSD: https://github.com/DragonFlyBSD/DragonFlyBSD/commit/1926f58. OpenBSD: https://github.com/openbsd/src/commit/c0ba2d2. Haiku, illumos, netbsd, redox, and solaris (and perhaps others) do not support ip_mreqn, so these functions are not available on those systems. Requires libc with https://github.com/rust-lang/libc/commit/8cba30b, released in https://crates.io/crates/libc/0.2.113; the libc dependency is bumped to this version. Fixes #283. --- Cargo.toml | 2 +- src/lib.rs | 9 ++++++ src/socket.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++++ src/sys/unix.rs | 25 +++++++++++++++ src/sys/windows.rs | 26 ++++++++++++++++ tests/socket.rs | 28 +++++++++++++++++ 6 files changed, 167 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 05416a49..071c169e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ rustdoc-args = ["--cfg", "docsrs"] features = ["all"] [target."cfg(unix)".dependencies] -libc = "0.2.107" +libc = "0.2.113" [target."cfg(windows)".dependencies] winapi = { version = "0.3.9", features = ["handleapi", "ws2ipdef", "ws2tcpip"] } diff --git a/src/lib.rs b/src/lib.rs index c7d5e5d7..d01b652c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -132,6 +132,15 @@ pub use sockaddr::SockAddr; pub use socket::Socket; pub use sockref::SockRef; +#[cfg(not(any( + target_os = "haiku", + target_os = "illumos", + target_os = "netbsd", + target_os = "redox", + target_os = "solaris", +)))] +pub use socket::InterfaceIndexOrAddress; + /// Specification of the communication domain for a socket. /// /// This is a newtype wrapper around an integer which provides a nicer API in diff --git a/src/socket.rs b/src/socket.rs index 9c83475d..44bbfd9f 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -713,6 +713,25 @@ fn set_common_flags(socket: Socket) -> io::Result { Ok(socket) } +/// A local interface specified by its index or an address assigned to it. +/// +/// `Index(0)` and `Address(Ipv4Addr::UNSPECIFIED)` are equivalent and indicate +/// that an appropriate interface should be selected by the system. +#[cfg(not(any( + target_os = "haiku", + target_os = "illumos", + target_os = "netbsd", + target_os = "redox", + target_os = "solaris", +)))] +#[derive(Debug)] +pub enum InterfaceIndexOrAddress { + /// An interface index. + Index(u32), + /// An address assigned to an interface. + Address(Ipv4Addr), +} + /// Socket options get/set using `SOL_SOCKET`. /// /// Additional documentation can be found in documentation of the OS. @@ -1106,6 +1125,65 @@ impl Socket { } } + /// Join a multicast group using `IP_ADD_MEMBERSHIP` option on this socket. + /// + /// This function specifies a new multicast group for this socket to join. + /// The address must be a valid multicast address, and `interface` specifies + /// the local interface with which the system should join the multicast + /// group. See [`InterfaceIndexOrAddress`]. + /// + /// [`InterfaceIndexOrAddress`]: Socket::InterfaceIndexOrAddress + #[cfg(not(any( + target_os = "haiku", + target_os = "illumos", + target_os = "netbsd", + target_os = "redox", + target_os = "solaris", + )))] + pub fn join_multicast_v4_n( + &self, + multiaddr: &Ipv4Addr, + interface: &InterfaceIndexOrAddress, + ) -> io::Result<()> { + let mreqn = sys::to_mreqn(multiaddr, interface); + unsafe { + setsockopt( + self.as_raw(), + sys::IPPROTO_IP, + sys::IP_ADD_MEMBERSHIP, + mreqn, + ) + } + } + + /// Leave a multicast group using `IP_DROP_MEMBERSHIP` option on this socket. + /// + /// For more information about this option, see [`join_multicast_v4_n`]. + /// + /// [`join_multicast_v4_n`]: Socket::join_multicast_v4_n + #[cfg(not(any( + target_os = "haiku", + target_os = "illumos", + target_os = "netbsd", + target_os = "redox", + target_os = "solaris", + )))] + pub fn leave_multicast_v4_n( + &self, + multiaddr: &Ipv4Addr, + interface: &InterfaceIndexOrAddress, + ) -> io::Result<()> { + let mreqn = sys::to_mreqn(multiaddr, interface); + unsafe { + setsockopt( + self.as_raw(), + sys::IPPROTO_IP, + sys::IP_DROP_MEMBERSHIP, + mreqn, + ) + } + } + /// Get the value of the `IP_MULTICAST_IF` option for this socket. /// /// For more information about this option, see [`set_multicast_if_v4`]. diff --git a/src/sys/unix.rs b/src/sys/unix.rs index 44bfefcc..873b3238 100644 --- a/src/sys/unix.rs +++ b/src/sys/unix.rs @@ -1000,6 +1000,31 @@ pub(crate) fn from_in6_addr(addr: in6_addr) -> Ipv6Addr { Ipv6Addr::from(addr.s6_addr) } +#[cfg(not(any( + target_os = "haiku", + target_os = "illumos", + target_os = "netbsd", + target_os = "redox", + target_os = "solaris", +)))] +pub(crate) fn to_mreqn( + multiaddr: &Ipv4Addr, + interface: &crate::socket::InterfaceIndexOrAddress, +) -> libc::ip_mreqn { + match interface { + crate::socket::InterfaceIndexOrAddress::Index(interface) => libc::ip_mreqn { + imr_multiaddr: to_in_addr(multiaddr), + imr_address: to_in_addr(&Ipv4Addr::UNSPECIFIED), + imr_ifindex: *interface as _, + }, + crate::socket::InterfaceIndexOrAddress::Address(interface) => libc::ip_mreqn { + imr_multiaddr: to_in_addr(multiaddr), + imr_address: to_in_addr(interface), + imr_ifindex: 0, + }, + } +} + /// Unix only API. impl crate::Socket { /// Accept a new incoming connection from this listener. diff --git a/src/sys/windows.rs b/src/sys/windows.rs index 6652105c..ab598399 100644 --- a/src/sys/windows.rs +++ b/src/sys/windows.rs @@ -741,6 +741,32 @@ pub(crate) fn from_in6_addr(addr: in6_addr) -> Ipv6Addr { Ipv6Addr::from(*unsafe { addr.u.Byte() }) } +pub(crate) fn to_mreqn( + multiaddr: &Ipv4Addr, + interface: &crate::socket::InterfaceIndexOrAddress, +) -> IpMreq { + IpMreq { + imr_multiaddr: to_in_addr(multiaddr), + // Per https://docs.microsoft.com/en-us/windows/win32/api/ws2ipdef/ns-ws2ipdef-ip_mreq#members: + // + // imr_interface + // + // The local IPv4 address of the interface or the interface index on + // which the multicast group should be joined or dropped. This value is + // in network byte order. If this member specifies an IPv4 address of + // 0.0.0.0, the default IPv4 multicast interface is used. + // + // To use an interface index of 1 would be the same as an IP address of + // 0.0.0.1. + imr_interface: match interface { + crate::socket::InterfaceIndexOrAddress::Index(interface) => { + to_in_addr(&(*interface).into()) + } + crate::socket::InterfaceIndexOrAddress::Address(interface) => to_in_addr(interface), + }, + } +} + /// Windows only API. impl crate::Socket { /// Sets `HANDLE_FLAG_INHERIT` using `SetHandleInformation`. diff --git a/tests/socket.rs b/tests/socket.rs index 728f1681..d0ee0e3b 100644 --- a/tests/socket.rs +++ b/tests/socket.rs @@ -1191,6 +1191,34 @@ test!( set_tcp_user_timeout(Some(Duration::from_secs(10))) ); +#[test] +#[cfg(not(any( + target_os = "haiku", + target_os = "illumos", + target_os = "netbsd", + target_os = "redox", + target_os = "solaris", +)))] +fn join_leave_multicast_v4_n() { + let socket = Socket::new(Domain::IPV4, Type::DGRAM, None).unwrap(); + let multiaddr = Ipv4Addr::new(224, 0, 1, 1); + let interface = socket2::InterfaceIndexOrAddress::Index(0); + match socket.leave_multicast_v4_n(&multiaddr, &interface) { + Ok(()) => panic!("leaving an unjoined group should fail"), + Err(err) => { + assert_eq!(err.kind(), io::ErrorKind::AddrNotAvailable); + #[cfg(unix)] + assert_eq!(err.raw_os_error(), Some(libc::EADDRNOTAVAIL)); + } + }; + let () = socket + .join_multicast_v4_n(&multiaddr, &interface) + .expect("join multicast group"); + let () = socket + .leave_multicast_v4_n(&multiaddr, &interface) + .expect("leave multicast group"); +} + #[test] #[cfg(all(feature = "all", not(target_os = "redox")))] fn header_included() {