use std::{
collections::HashMap,
io,
net::{IpAddr, Ipv4Addr, SocketAddr},
};
use log::{error, warn};
use crate::{
rtps::{
constant::{payload_budget_for_mtu, FALLBACK_MAX_AGGREGATED_DATAGRAM_SIZE},
transmit::InterfaceSelector,
},
structure::locator::Locator,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct IfAddr {
pub ip: IpAddr,
pub index: u32,
pub is_loopback: bool,
pub is_multicast: bool,
pub netmask: Option<Ipv4Addr>,
pub mtu: Option<u32>,
}
fn enumerate_interfaces() -> io::Result<Vec<IfAddr>> {
let mut result = Vec::new();
for iface in netdev::get_interfaces() {
let index = iface.index;
let is_loopback = iface.is_loopback();
let is_multicast = iface.is_multicast();
let mtu = iface.mtu;
for net in &iface.ipv4 {
result.push(IfAddr {
ip: IpAddr::V4(net.addr()),
index,
is_loopback,
is_multicast,
netmask: Some(net.netmask()),
mtu,
});
}
for net in &iface.ipv6 {
result.push(IfAddr {
ip: IpAddr::V6(net.addr()),
index,
is_loopback,
is_multicast,
netmask: None,
mtu,
});
}
}
Ok(result)
}
pub fn local_interface_table() -> Vec<IfAddr> {
match enumerate_interfaces() {
Ok(ifaces) => ifaces,
Err(e) => {
error!("Cannot enumerate local interfaces for path-MTU resolution: {e:?}");
Vec::new()
}
}
}
pub fn path_mtu_payload_for_peer(ifaces: &[IfAddr], peer_ip: IpAddr) -> usize {
if let IpAddr::V4(peer) = peer_ip {
for ifa in ifaces {
let (IpAddr::V4(local), Some(mask)) = (ifa.ip, ifa.netmask) else {
continue;
};
if same_subnet_v4(local, mask, peer) {
if let Some(mtu) = ifa.mtu {
return payload_budget_for_mtu(mtu);
}
}
}
}
FALLBACK_MAX_AGGREGATED_DATAGRAM_SIZE
}
fn same_subnet_v4(local: Ipv4Addr, mask: Ipv4Addr, peer: Ipv4Addr) -> bool {
let m = u32::from(mask);
(u32::from(local) & m) == (u32::from(peer) & m)
}
pub fn get_local_multicast_locators(port: u16) -> Vec<Locator> {
let saddr = SocketAddr::new("239.255.0.1".parse().unwrap(), port);
vec![Locator::from(saddr)]
}
pub fn localhost_spdp_peer_locators(
domain_id: u16,
own_participant_id: u16,
max_participants: u16,
) -> Vec<Locator> {
(0..max_participants)
.filter(|pid| *pid != own_participant_id)
.map(|pid| {
Locator::from(SocketAddr::new(
IpAddr::V4(Ipv4Addr::LOCALHOST),
crate::network::constant::spdp_well_known_unicast_port(domain_id, pid),
))
})
.collect()
}
pub fn get_local_unicast_locators_filtered(
port: u16,
only_networks: Option<&[IpAddr]>,
) -> Vec<Locator> {
match enumerate_interfaces() {
Ok(ifaces) => {
let result = get_local_unicast_locators_inner(&ifaces, port, only_networks);
if result.is_empty() {
if let Some(nets) = only_networks {
warn!(
"only_networks filter {:?} matched no unicast interfaces; this participant will be \
invisible to peers.",
nets,
);
}
}
result
}
Err(e) => {
error!("Cannot get local network interfaces: {e:?}");
vec![]
}
}
}
fn get_local_unicast_locators_inner(
ifaces: &[IfAddr],
port: u16,
only_networks: Option<&[IpAddr]>,
) -> Vec<Locator> {
ifaces
.iter()
.filter(|ifa| only_networks.is_none_or(|nets| nets.contains(&ifa.ip)))
.map(|ifa| Locator::from(SocketAddr::new(ifa.ip, port)))
.collect()
}
pub fn get_local_multicast_ip_addrs_filtered(
only_networks: Option<&[IpAddr]>,
) -> io::Result<Vec<IpAddr>> {
let ifaces = enumerate_interfaces()?;
let result = get_local_multicast_ip_addrs_inner(&ifaces, only_networks);
Ok(result)
}
fn get_local_multicast_ip_addrs_inner(
ifaces: &[IfAddr],
only_networks: Option<&[IpAddr]>,
) -> Vec<IpAddr> {
ifaces
.iter()
.filter(|ifa| ifa.is_multicast)
.filter(|ifa| only_networks.is_none_or(|nets| nets.contains(&ifa.ip)))
.map(|ifa| ifa.ip)
.filter(IpAddr::is_ipv4)
.collect()
}
pub fn build_ifindex_to_interface_map() -> HashMap<u32, InterfaceSelector> {
match enumerate_interfaces() {
Ok(ifaces) => build_ifindex_map_inner(&ifaces),
Err(e) => {
error!("Cannot build interface-index map: {e:?}");
HashMap::new()
}
}
}
fn build_ifindex_map_inner(ifaces: &[IfAddr]) -> HashMap<u32, InterfaceSelector> {
let mut map: HashMap<u32, InterfaceSelector> = HashMap::new();
for ifa in ifaces {
if ifa.index == 0 {
continue;
}
match map.get(&ifa.index) {
None => {
map.insert(ifa.index, InterfaceSelector::Ip(ifa.ip));
}
Some(InterfaceSelector::Ip(existing)) if ifa.ip.is_ipv4() && !existing.is_ipv4() => {
map.insert(ifa.index, InterfaceSelector::Ip(ifa.ip));
}
Some(_) => {}
}
}
map
}
#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use super::{
build_ifindex_map_inner, get_local_multicast_ip_addrs_inner, get_local_unicast_locators_inner,
localhost_spdp_peer_locators, path_mtu_payload_for_peer, IfAddr, InterfaceSelector,
};
use crate::{
network::constant::spdp_well_known_unicast_port,
rtps::constant::FALLBACK_MAX_AGGREGATED_DATAGRAM_SIZE, structure::locator::Locator,
};
fn v4(a: u8, b: u8, c: u8, d: u8) -> IpAddr {
IpAddr::V4(Ipv4Addr::new(a, b, c, d))
}
fn iface(ip: IpAddr, index: u32, is_loopback: bool, is_multicast: bool) -> IfAddr {
IfAddr {
ip,
index,
is_loopback,
is_multicast,
netmask: None,
mtu: None,
}
}
fn iface_v4(a: u8, b: u8, c: u8, d: u8, prefix: u8, mtu: u32, is_loopback: bool) -> IfAddr {
let mask = if prefix == 0 {
0u32
} else {
u32::MAX << (32 - u32::from(prefix))
};
IfAddr {
ip: v4(a, b, c, d),
index: 1,
is_loopback,
is_multicast: !is_loopback,
netmask: Some(Ipv4Addr::from(mask)),
mtu: Some(mtu),
}
}
#[test]
fn test_get_local_multicast_ip_addrs() {
let ifaces = vec![
iface(IpAddr::V4(Ipv4Addr::LOCALHOST), 0, true, false),
iface(IpAddr::V6(Ipv6Addr::LOCALHOST), 0, true, false),
iface(v4(192, 168, 0, 137), 1, false, true),
iface(
IpAddr::V6(Ipv6Addr::new(0xfd73, 0x40a2, 0x1c3e, 0, 0, 0, 0, 0)),
1,
false,
true,
),
];
let ips = get_local_multicast_ip_addrs_inner(&ifaces, None);
assert_eq!(ips.len(), 1, "should only contain the non-loopback iface");
assert!(ips.contains(&v4(192, 168, 0, 137)));
}
#[test]
fn no_multicast() {
let mut ifaces = Vec::new();
for index in 0..10 {
ifaces.push(iface(
v4(192, 168, 0, rand::random()),
index + 1,
false,
false, ));
}
let ips = get_local_multicast_ip_addrs_inner(&ifaces, None);
assert!(
ips.is_empty(),
"we only want interfaces w/ multicast support"
);
}
#[test]
fn empty_interfaces() {
let ips = get_local_multicast_ip_addrs_inner(&[], None);
assert!(
ips.is_empty(),
"blank iface list should result in empty list of ips"
);
}
#[test]
fn multicast_filter_respects_only_networks() {
let ifaces = vec![
iface(v4(192, 168, 0, 10), 1, false, true),
iface(v4(10, 0, 0, 10), 2, false, true),
];
let only_networks = [v4(10, 0, 0, 10)];
let ips = get_local_multicast_ip_addrs_inner(&ifaces, Some(&only_networks));
assert_eq!(ips, vec![v4(10, 0, 0, 10)]);
}
#[test]
fn unicast_filter_respects_only_networks() {
let only_networks = [v4(10, 0, 0, 10)];
let ifaces = vec![
iface(v4(192, 168, 0, 10), 1, false, true),
iface(v4(10, 0, 0, 10), 2, false, true),
];
let filtered = get_local_unicast_locators_inner(&ifaces, 7412, Some(&only_networks));
assert_eq!(
filtered,
vec![Locator::from(SocketAddr::new(v4(10, 0, 0, 10), 7412))]
);
}
#[test]
fn ifindex_map_prefers_ipv4_and_skips_index_zero() {
let ifaces = vec![
iface(IpAddr::V4(Ipv4Addr::LOCALHOST), 0, true, false),
iface(
IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)),
3,
false,
true,
),
iface(v4(10, 0, 0, 7), 3, false, true),
];
let map = build_ifindex_map_inner(&ifaces);
assert!(!map.contains_key(&0), "index 0 must be skipped");
assert_eq!(
map.get(&3),
Some(&InterfaceSelector::Ip(v4(10, 0, 0, 7))),
"should prefer the IPv4 address"
);
}
#[test]
fn path_mtu_same_subnet_uses_iface_mtu() {
let ifaces = vec![
iface_v4(127, 0, 0, 1, 8, 16384, true),
iface_v4(192, 168, 1, 10, 24, 1500, false),
];
assert_eq!(
path_mtu_payload_for_peer(&ifaces, v4(192, 168, 1, 55)),
1500 - 48
);
}
#[test]
fn path_mtu_loopback_peer_uses_loopback_mtu() {
let ifaces = vec![
iface_v4(127, 0, 0, 1, 8, 16384, true),
iface_v4(192, 168, 1, 10, 24, 1500, false),
];
assert_eq!(
path_mtu_payload_for_peer(&ifaces, v4(127, 0, 0, 1)),
16384 - 48
);
}
#[test]
fn path_mtu_behind_router_uses_default() {
let ifaces = vec![iface_v4(192, 168, 1, 10, 24, 1500, false)];
assert_eq!(
path_mtu_payload_for_peer(&ifaces, v4(10, 20, 30, 40)),
FALLBACK_MAX_AGGREGATED_DATAGRAM_SIZE
);
}
#[test]
fn path_mtu_ipv6_peer_uses_default() {
let ifaces = vec![iface_v4(192, 168, 1, 10, 24, 9000, false)];
let peer = IpAddr::V6(Ipv6Addr::LOCALHOST);
assert_eq!(
path_mtu_payload_for_peer(&ifaces, peer),
FALLBACK_MAX_AGGREGATED_DATAGRAM_SIZE
);
}
#[test]
fn path_mtu_jumbo_frame() {
let ifaces = vec![iface_v4(10, 0, 0, 1, 8, 9000, false)];
assert_eq!(
path_mtu_payload_for_peer(&ifaces, v4(10, 1, 2, 3)),
9000 - 48
);
}
#[test]
fn path_mtu_empty_table_uses_default() {
assert_eq!(
path_mtu_payload_for_peer(&[], v4(192, 168, 1, 1)),
FALLBACK_MAX_AGGREGATED_DATAGRAM_SIZE
);
}
#[test]
fn localhost_spdp_peers_cover_range_excluding_self() {
let domain = 0;
let own = 2;
let peers = localhost_spdp_peer_locators(domain, own, 4);
let expected: Vec<Locator> = [0u16, 1, 3]
.iter()
.map(|pid| {
Locator::from(SocketAddr::new(
IpAddr::V4(Ipv4Addr::LOCALHOST),
spdp_well_known_unicast_port(domain, *pid),
))
})
.collect();
assert_eq!(peers, expected);
assert!(peers
.iter()
.all(|l| SocketAddr::from(*l).ip().is_loopback()));
}
}