use std::io;
use std::net::Ipv4Addr;
pub mod flags {
pub const IFF_UP: u32 = 0x1;
pub const IFF_BROADCAST: u32 = 0x2;
pub const IFF_LOOPBACK: u32 = 0x8;
pub const IFF_POINTOPOINT: u32 = 0x10;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IfaceV4 {
pub name: String,
pub ip: Ipv4Addr,
pub netmask: Option<Ipv4Addr>,
pub dest: Option<Ipv4Addr>,
pub flags: u32,
}
impl IfaceV4 {
pub fn is_up(&self) -> bool {
self.flags & flags::IFF_UP != 0
}
pub fn is_loopback(&self) -> bool {
self.flags & flags::IFF_LOOPBACK != 0
}
pub fn is_broadcast(&self) -> bool {
self.flags & flags::IFF_BROADCAST != 0
}
pub fn is_point_to_point(&self) -> bool {
self.flags & flags::IFF_POINTOPOINT != 0
}
pub fn is_eligible(&self) -> bool {
self.is_up() && !self.is_loopback()
}
pub fn search_destination(&self) -> Option<Ipv4Addr> {
if !self.is_eligible() {
return None;
}
if self.is_broadcast() {
return self.dest.filter(|b| !b.is_unspecified());
}
if self.is_point_to_point() {
return self.dest;
}
None
}
}
pub fn enumerate() -> io::Result<Vec<IfaceV4>> {
sys::enumerate()
}
pub fn broadcast_addrs() -> Vec<Ipv4Addr> {
let Ok(ifaces) = enumerate() else {
return Vec::new();
};
let mut out: Vec<Ipv4Addr> = Vec::new();
for iface in ifaces {
if let Some(dest) = iface.search_destination() {
if !out.contains(&dest) {
out.push(dest);
}
}
}
out
}
pub fn local_addr() -> Ipv4Addr {
let Ok(ifaces) = enumerate() else {
return Ipv4Addr::LOCALHOST;
};
ifaces
.into_iter()
.find(|i| i.is_eligible())
.map(|i| i.ip)
.unwrap_or(Ipv4Addr::LOCALHOST)
}
#[cfg(unix)]
mod sys {
use super::IfaceV4;
use std::ffi::CStr;
use std::io;
use std::net::Ipv4Addr;
#[repr(C)]
struct IfAddrs {
ifa_next: *mut IfAddrs,
ifa_name: *mut libc::c_char,
ifa_flags: libc::c_uint,
ifa_addr: *mut libc::sockaddr,
ifa_netmask: *mut libc::sockaddr,
ifa_dstaddr: *mut libc::sockaddr,
ifa_data: *mut libc::c_void,
}
#[cfg(not(epics_embedded_target))]
const LAYOUT_MATCHES_LIBC: bool = size_of::<IfAddrs>() == size_of::<libc::ifaddrs>()
&& align_of::<IfAddrs>() == align_of::<libc::ifaddrs>()
&& core::mem::offset_of!(IfAddrs, ifa_next)
== core::mem::offset_of!(libc::ifaddrs, ifa_next)
&& core::mem::offset_of!(IfAddrs, ifa_name)
== core::mem::offset_of!(libc::ifaddrs, ifa_name)
&& core::mem::offset_of!(IfAddrs, ifa_flags)
== core::mem::offset_of!(libc::ifaddrs, ifa_flags)
&& core::mem::offset_of!(IfAddrs, ifa_addr)
== core::mem::offset_of!(libc::ifaddrs, ifa_addr)
&& core::mem::offset_of!(IfAddrs, ifa_netmask)
== core::mem::offset_of!(libc::ifaddrs, ifa_netmask)
&& core::mem::offset_of!(IfAddrs, ifa_data)
== core::mem::offset_of!(libc::ifaddrs, ifa_data);
#[cfg(not(epics_embedded_target))]
const _: () = assert!(
LAYOUT_MATCHES_LIBC,
"net::iface_v4's `struct ifaddrs` no longer matches this platform's \
`libc::ifaddrs`. The declaration exists because neither embedded \
target's `libc` has the struct; this assertion is what keeps it \
honest on the platforms whose `libc` does. Re-derive the field order \
from the platform header before changing it."
);
#[cfg(not(epics_embedded_target))]
unsafe fn get_ifaddrs(head: *mut *mut IfAddrs) -> libc::c_int {
unsafe { libc::getifaddrs(head as *mut *mut libc::ifaddrs) }
}
#[cfg(not(epics_embedded_target))]
unsafe fn free_ifaddrs(head: *mut IfAddrs) {
unsafe { libc::freeifaddrs(head as *mut libc::ifaddrs) }
}
#[cfg(epics_embedded_target)]
unsafe extern "C" {
#[link_name = "getifaddrs"]
fn getifaddrs_raw(ifap: *mut *mut IfAddrs) -> libc::c_int;
#[link_name = "freeifaddrs"]
fn freeifaddrs_raw(ifa: *mut IfAddrs);
}
#[cfg(epics_embedded_target)]
unsafe fn get_ifaddrs(head: *mut *mut IfAddrs) -> libc::c_int {
unsafe { getifaddrs_raw(head) }
}
#[cfg(epics_embedded_target)]
unsafe fn free_ifaddrs(head: *mut IfAddrs) {
unsafe { freeifaddrs_raw(head) }
}
unsafe fn sockaddr_ipv4(sa: *const libc::sockaddr) -> Option<Ipv4Addr> {
if sa.is_null() {
return None;
}
let sin: libc::sockaddr_in = unsafe { std::ptr::read_unaligned(sa as *const _) };
if libc::c_int::from(sin.sin_family) != libc::AF_INET {
return None;
}
Some(Ipv4Addr::from(u32::from_be(sin.sin_addr.s_addr)))
}
pub(super) fn enumerate() -> io::Result<Vec<IfaceV4>> {
let mut head: *mut IfAddrs = std::ptr::null_mut();
if unsafe { get_ifaddrs(&mut head) } != 0 {
return Err(io::Error::last_os_error());
}
if head.is_null() {
return Ok(Vec::new());
}
let mut out = Vec::new();
let mut cur = head;
unsafe {
while !cur.is_null() {
let node = &*cur;
cur = node.ifa_next;
let Some(ip) = sockaddr_ipv4(node.ifa_addr) else {
continue;
};
let name = if node.ifa_name.is_null() {
String::new()
} else {
CStr::from_ptr(node.ifa_name).to_string_lossy().into_owned()
};
out.push(IfaceV4 {
name,
ip,
netmask: sockaddr_ipv4(node.ifa_netmask),
dest: sockaddr_ipv4(node.ifa_dstaddr),
flags: node.ifa_flags,
});
}
free_ifaddrs(head);
}
Ok(out)
}
}
#[cfg(not(unix))]
mod sys {
use super::{IfaceV4, flags};
use std::io;
use std::net::IpAddr;
pub(super) fn enumerate() -> io::Result<Vec<IfaceV4>> {
let ifs = if_addrs::get_if_addrs()?;
let mut out = Vec::new();
for iface in ifs {
let if_addrs::IfAddr::V4(v4) = &iface.addr else {
continue;
};
let IpAddr::V4(ip) = iface.ip() else {
continue;
};
let mut f = flags::IFF_UP;
if iface.is_loopback() {
f |= flags::IFF_LOOPBACK;
}
if v4.broadcast.is_some() {
f |= flags::IFF_BROADCAST;
}
out.push(IfaceV4 {
name: iface.name.clone(),
ip,
netmask: Some(v4.netmask),
dest: v4.broadcast,
flags: f,
});
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn iface(flags: u32, dest: Option<Ipv4Addr>) -> IfaceV4 {
IfaceV4 {
name: "test0".to_string(),
ip: Ipv4Addr::new(10, 0, 0, 5),
netmask: Some(Ipv4Addr::new(255, 255, 255, 0)),
dest,
flags,
}
}
#[test]
fn a_down_interface_is_never_a_search_destination() {
let down = iface(flags::IFF_BROADCAST, Some(Ipv4Addr::new(10, 0, 0, 255)));
assert!(!down.is_eligible());
assert_eq!(down.search_destination(), None);
}
#[test]
fn loopback_is_never_a_search_destination() {
let lo = iface(
flags::IFF_UP | flags::IFF_LOOPBACK | flags::IFF_BROADCAST,
Some(Ipv4Addr::new(127, 255, 255, 255)),
);
assert_eq!(lo.search_destination(), None);
}
#[test]
fn a_broadcast_interface_yields_its_broadcast_address() {
let up = iface(
flags::IFF_UP | flags::IFF_BROADCAST,
Some(Ipv4Addr::new(10, 0, 0, 255)),
);
assert_eq!(up.search_destination(), Some(Ipv4Addr::new(10, 0, 0, 255)));
}
#[test]
fn an_unspecified_broadcast_address_is_discarded() {
let up = iface(
flags::IFF_UP | flags::IFF_BROADCAST,
Some(Ipv4Addr::UNSPECIFIED),
);
assert_eq!(up.search_destination(), None);
}
#[test]
fn a_point_to_point_interface_yields_its_peer_address() {
let p2p = iface(
flags::IFF_UP | flags::IFF_POINTOPOINT,
Some(Ipv4Addr::new(192, 168, 9, 1)),
);
assert_eq!(
p2p.search_destination(),
Some(Ipv4Addr::new(192, 168, 9, 1))
);
}
#[test]
fn an_interface_that_is_neither_broadcast_nor_p2p_yields_nothing() {
let odd = iface(flags::IFF_UP, Some(Ipv4Addr::new(10, 0, 0, 255)));
assert_eq!(odd.search_destination(), None);
}
#[test]
fn enumeration_runs_and_reports_consistent_entries() {
let ifaces = enumerate().expect("getifaddrs must succeed on the host");
for iface in &ifaces {
if iface.ip.is_loopback() {
assert!(
iface.is_loopback() || iface.name.is_empty(),
"an interface with a loopback address must carry IFF_LOOPBACK: {iface:?}"
);
}
if let Some(dest) = iface.search_destination() {
assert!(iface.is_up(), "a destination came from a down interface");
assert!(!iface.is_loopback(), "a destination came from loopback");
assert!(!dest.is_unspecified() || iface.is_point_to_point());
}
}
}
#[test]
fn local_addr_is_an_eligible_interface_or_loopback() {
let addr = local_addr();
if addr.is_loopback() {
return;
}
let ifaces = enumerate().expect("getifaddrs must succeed on the host");
assert!(
ifaces.iter().any(|i| i.is_eligible() && i.ip == addr),
"local_addr returned {addr}, which is not an eligible interface"
);
}
#[test]
fn broadcast_addrs_are_deduplicated_eligible_destinations() {
let addrs = broadcast_addrs();
let mut seen = Vec::new();
for a in &addrs {
assert!(!seen.contains(a), "broadcast_addrs repeated {a}");
seen.push(*a);
}
let ifaces = enumerate().expect("getifaddrs must succeed on the host");
for a in &addrs {
assert!(
ifaces.iter().any(|i| i.search_destination() == Some(*a)),
"broadcast_addrs reported {a}, which no eligible interface names"
);
}
}
}