#![cfg(test)]
use crate::{Context, Packet, SourceFilter, SubscriptionConfig};
use socket2::SockRef;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;
use std::time::{Duration, Instant};
static IPV6_MULTICAST_LOOPBACK_AVAILABLE: OnceLock<bool> = OnceLock::new();
static NEXT_TEST_PORT: AtomicU32 = AtomicU32::new(0);
pub(crate) fn sample_config(port: u16) -> SubscriptionConfig {
let mut config = SubscriptionConfig {
group: IpAddr::V4(Ipv4Addr::new(239, 1, 2, 3)),
source: SourceFilter::Any,
dst_port: port,
interface: None,
interface_index: None,
};
config.interface = Some(IpAddr::V4(Ipv4Addr::LOCALHOST));
config
}
pub(crate) fn unused_udp_port_v4() -> u16 {
unused_udp_port(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0).into())
}
pub(crate) fn unused_udp_port_v6() -> u16 {
unused_udp_port(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 0, 0, 0).into())
}
fn unused_udp_port(bind_addr: SocketAddr) -> u16 {
for _ in 0..10_000 {
let port = 20_000 + (NEXT_TEST_PORT.fetch_add(1, Ordering::Relaxed) % 10_000) as u16;
let candidate: SocketAddr = match bind_addr {
SocketAddr::V4(address) => SocketAddrV4::new(*address.ip(), port).into(),
SocketAddr::V6(address) => {
SocketAddrV6::new(*address.ip(), port, address.flowinfo(), address.scope_id())
.into()
}
};
match UdpSocket::bind(candidate) {
Ok(_) => return port,
Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => continue,
Err(err) if cfg!(windows) && err.kind() == std::io::ErrorKind::PermissionDenied => {
continue;
}
Err(err) => panic!("failed to probe UDP test port {port}: {err}"),
}
}
panic!("failed to find an available UDP test port in 20000..30000");
}
pub(crate) fn sample_config_on_unused_port() -> SubscriptionConfig {
sample_config(unused_udp_port_v4())
}
pub(crate) fn sample_config_v6_on_unused_port() -> SubscriptionConfig {
sample_config_v6(unused_udp_port_v6())
}
pub(crate) fn sample_config_v6(port: u16) -> SubscriptionConfig {
let mut config = SubscriptionConfig::asm_v6("ff01::1234".parse().unwrap(), port);
config.interface = Some(IpAddr::V6(Ipv6Addr::LOCALHOST));
config
}
pub(crate) fn sample_ssm_config_v6(port: u16) -> Option<SubscriptionConfig> {
let interface = ssm_test_interface_v6()?;
#[cfg(target_vendor = "apple")]
let source = ssm_test_routable_source_v6(interface);
#[cfg(not(target_vendor = "apple"))]
let source = interface;
let group = if interface == Ipv6Addr::LOCALHOST {
"ff31::1234"
} else {
"ff3e::1234"
};
let mut config = SubscriptionConfig::ssm_v6(group.parse().unwrap(), source, port);
config.interface = Some(IpAddr::V6(interface));
Some(config)
}
pub(crate) fn sample_ssm_receive_config_v6(port: u16) -> Option<SubscriptionConfig> {
#[cfg(target_vendor = "apple")]
{
let _ = port;
None
}
#[cfg(not(target_vendor = "apple"))]
{
if !ipv6_multicast_loopback_available() {
return None;
}
sample_ssm_config_v6(port)
}
}
pub(crate) fn ipv6_multicast_loopback_available() -> bool {
*IPV6_MULTICAST_LOOPBACK_AVAILABLE.get_or_init(|| {
let interface = Ipv6Addr::LOCALHOST;
let ifindex = match crate::platform::resolve_ipv6_interface_index(interface) {
Ok(ifindex) => ifindex,
Err(_) => return false,
};
let sender = match UdpSocket::bind(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0)) {
Ok(sender) => sender,
Err(_) => return false,
};
if sender.set_multicast_loop_v6(true).is_err() {
return false;
}
let socket = SockRef::from(&sender);
if socket.set_multicast_hops_v6(1).is_err() || socket.set_multicast_if_v6(ifindex).is_err()
{
return false;
}
sender
.send_to(
&[],
SocketAddrV6::new(
Ipv6Addr::from([0xff, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
9,
0,
ifindex,
),
)
.is_ok()
})
}
pub(crate) fn sample_ssm_receive_config_v6_on_unused_port() -> Option<SubscriptionConfig> {
sample_ssm_receive_config_v6(unused_udp_port_v6())
}
pub(crate) fn recv_next_packet(context: &mut Context, deadline: Instant) -> Packet {
loop {
match context.try_recv_any().unwrap() {
Some(packet) => return packet,
None if Instant::now() < deadline => {
thread::sleep(Duration::from_millis(10));
}
None => panic!("timed out waiting for packet from context"),
}
}
}
pub(crate) fn make_multicast_sender() -> UdpSocket {
let sender = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)).unwrap();
sender.set_multicast_loop_v4(true).unwrap();
sender.set_multicast_ttl_v4(1).unwrap();
SockRef::from(&sender)
.set_multicast_if_v4(&Ipv4Addr::LOCALHOST)
.unwrap();
sender
}
pub(crate) fn make_multicast_sender_v6(interface: Ipv6Addr) -> UdpSocket {
let sender = UdpSocket::bind(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0)).unwrap();
sender.set_multicast_loop_v6(true).unwrap();
let socket = SockRef::from(&sender);
socket.set_multicast_hops_v6(1).unwrap();
let ifindex = crate::platform::resolve_ipv6_interface_index(interface).unwrap();
socket.set_multicast_if_v6(ifindex).unwrap();
sender
}
pub(crate) fn make_multicast_sender_v6_for_source(source: Ipv6Addr) -> UdpSocket {
let ifindex = crate::platform::resolve_ipv6_interface_index(source).unwrap();
let scope_id = if source.is_unicast_link_local() {
ifindex
} else {
0
};
let sender = UdpSocket::bind(SocketAddrV6::new(source, 0, 0, scope_id)).unwrap();
sender.set_multicast_loop_v6(true).unwrap();
let socket = SockRef::from(&sender);
socket.set_multicast_hops_v6(1).unwrap();
socket.set_multicast_if_v6(ifindex).unwrap();
sender
}
pub(crate) fn ipv6_group(config: &SubscriptionConfig) -> Ipv6Addr {
config.ipv6_membership().unwrap().group
}
pub(crate) fn ipv6_group_socket_addr(config: &SubscriptionConfig) -> SocketAddrV6 {
let ifindex = match config.interface_index {
Some(interface_index) => interface_index,
None => {
let interface = match config.interface {
Some(IpAddr::V6(interface)) => interface,
_ => Ipv6Addr::LOCALHOST,
};
crate::platform::resolve_ipv6_interface_index(interface).unwrap()
}
};
SocketAddrV6::new(ipv6_group(config), config.dst_port, 0, ifindex)
}
#[cfg(target_vendor = "apple")]
fn ssm_test_interface_v6() -> Option<Ipv6Addr> {
unsafe {
let mut ifaddrs = std::ptr::null_mut();
if libc::getifaddrs(&mut ifaddrs) != 0 {
return None;
}
let mut cursor = ifaddrs;
let mut fallback = None;
while !cursor.is_null() {
let addr = (*cursor).ifa_addr;
let flags = (*cursor).ifa_flags as libc::c_int;
if !addr.is_null()
&& (*addr).sa_family as libc::c_int == libc::AF_INET6
&& (flags & libc::IFF_LOOPBACK) == 0
{
let sockaddr = &*(addr as *const libc::sockaddr_in6);
let candidate = Ipv6Addr::from(sockaddr.sin6_addr.s6_addr);
if candidate.is_unspecified() || candidate.is_multicast() {
cursor = (*cursor).ifa_next;
continue;
}
if !candidate.is_unicast_link_local() {
libc::freeifaddrs(ifaddrs);
return Some(candidate);
}
fallback.get_or_insert(candidate);
}
cursor = (*cursor).ifa_next;
}
libc::freeifaddrs(ifaddrs);
fallback
}
}
#[cfg(not(target_vendor = "apple"))]
fn ssm_test_interface_v6() -> Option<Ipv6Addr> {
Some(Ipv6Addr::LOCALHOST)
}
#[cfg(target_vendor = "apple")]
fn ssm_test_routable_source_v6(interface: Ipv6Addr) -> Ipv6Addr {
let mut octets = interface.octets();
octets[15] ^= 1;
let candidate = Ipv6Addr::from(octets);
if candidate.is_multicast() || candidate.is_unspecified() {
interface
} else {
candidate
}
}