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() {