use crate::platform::{self, Adapter};
use ipnetwork::Ipv4Network;
use pnet_base::MacAddr;
use std::net::{IpAddr, Ipv4Addr, UdpSocket};
const MAX_PREFIX: u8 = 22;
pub const MAX_NET_PREFIX: u8 = 16;
const HIDDEN_MAC: MacAddr = MacAddr(0x02, 0, 0, 0, 0, 0);
pub struct Iface {
pub iface: Adapter,
pub ip: Ipv4Addr,
pub mac: Option<MacAddr>,
pub net: Ipv4Network,
pub narrowed_from: Option<Ipv4Network>,
pub link: Ipv4Network,
pub gateway: Option<Ipv4Addr>,
pub own_ips: Vec<Ipv4Addr>,
}
impl Iface {
pub fn targets(&self) -> Vec<Ipv4Addr> {
let (network, broadcast) = (self.net.network(), self.net.broadcast());
let ends = self.net.prefix() < 31;
self.net
.iter()
.filter(|&a| !(ends && (a == network || a == broadcast)) && a != self.ip)
.collect()
}
pub fn on_link(&self) -> bool {
overlap(self.net, self.link)
}
}
pub fn parse_net(s: &str) -> Result<Ipv4Network, String> {
let Some((ip, prefix)) = s.split_once('/') else {
return Err(format!("give the size too, like {s}/24"));
};
let ip: Ipv4Addr = ip
.parse()
.map_err(|_| format!("'{ip}' isn't an IPv4 address"))?;
let prefix: u8 = prefix
.parse()
.ok()
.filter(|p| *p <= 32)
.ok_or_else(|| format!("'{prefix}' isn't a prefix length from 0 to 32"))?;
if prefix < MAX_NET_PREFIX {
return Err(format!(
"a /{prefix} is too large; lsnet scans at most a /{MAX_NET_PREFIX} (65,536 addresses)"
));
}
let net = Ipv4Network::new(ip, prefix).expect("valid prefix");
Ok(Ipv4Network::new(net.network(), prefix).expect("valid prefix"))
}
fn overlap(a: Ipv4Network, b: Ipv4Network) -> bool {
a.contains(b.network()) || b.contains(a.network())
}
pub fn detect(name: Option<&str>, net: Option<Ipv4Network>) -> Result<Iface, String> {
let all = platform::adapters();
let own_ips = all.iter().flat_map(|a| &a.ips).map(|n| n.ip()).collect();
let on_net = net.and_then(|n| all.iter().flat_map(|a| &a.ips).find(|a| overlap(**a, n)));
let on_net = on_net.map(|a| a.ip());
let iface = match name {
Some(n) => all
.into_iter()
.find(|a| a.name == n || (cfg!(windows) && a.name.eq_ignore_ascii_case(n)))
.ok_or_else(|| format!("no interface named '{n}'"))?,
None => pick(all, on_net.or_else(outbound_ip))
.ok_or("couldn't find an active network interface (try --interface)")?,
};
let v4 = *net
.and_then(|n| iface.ips.iter().find(|a| overlap(**a, n)))
.or(iface.ips.first())
.ok_or_else(|| format!("{} has no IPv4 address", iface.name))?;
let mac = iface
.mac
.filter(|&m| m != MacAddr::zero() && m != HIDDEN_MAC);
let full = Ipv4Network::new(v4.network(), v4.prefix()).expect("valid network");
let (net, narrowed_from) = if let Some(net) = net {
(net, None)
} else if v4.prefix() < MAX_PREFIX {
let local = Ipv4Network::new(v4.ip(), 24).expect("valid prefix");
(
Ipv4Network::new(local.network(), 24).expect("valid prefix"),
Some(full),
)
} else {
(full, None)
};
Ok(Iface {
gateway: platform::default_gateway(&iface),
ip: v4.ip(),
mac,
net,
narrowed_from,
link: full,
own_ips,
iface,
})
}
fn outbound_ip() -> Option<Ipv4Addr> {
let sock = UdpSocket::bind("0.0.0.0:0").ok()?;
sock.connect("1.1.1.1:53").ok()?;
match sock.local_addr().ok()?.ip() {
IpAddr::V4(v4) => Some(v4),
_ => None,
}
}
fn pick(all: Vec<Adapter>, preferred: Option<Ipv4Addr>) -> Option<Adapter> {
let usable = |a: &&Adapter| {
a.up && !a.loopback && a.mac.is_some_and(|m| m != MacAddr::zero()) && !a.ips.is_empty()
};
if let Some(ip) = preferred
&& let Some(a) = all
.iter()
.filter(usable)
.find(|a| a.ips.iter().any(|n| n.ip() == ip))
{
return Some(a.clone());
}
let mut usable = all.into_iter().filter(|a| usable(&a));
let first = usable.next()?;
if first.physical {
return Some(first);
}
Some(usable.find(|a| a.physical).unwrap_or(first))
}
#[cfg(test)]
mod tests {
use super::*;
fn iface(ip: &str, net: &str) -> Iface {
Iface {
iface: Adapter {
name: "en0".into(),
index: 0,
mac: None,
ips: Vec::new(),
up: true,
loopback: false,
physical: true,
gateway: None,
},
ip: ip.parse().unwrap(),
mac: None,
net: net.parse().unwrap(),
narrowed_from: None,
link: "192.168.1.0/24".parse().unwrap(),
gateway: None,
own_ips: Vec::new(),
}
}
#[test]
fn net_takes_cidr_up_to_a_slash_16() {
assert_eq!(parse_net("10.0.0.0/16").unwrap().to_string(), "10.0.0.0/16");
assert_eq!(
parse_net("192.168.1.77/24").unwrap().to_string(),
"192.168.1.0/24"
);
assert!(parse_net("10.0.0.0/8").unwrap_err().contains("too large"));
assert!(
parse_net("192.168.1.0")
.unwrap_err()
.contains("192.168.1.0/24")
);
assert!(parse_net("192.168.1.0/33").is_err());
assert!(
parse_net("nonsense/24")
.unwrap_err()
.contains("IPv4 address")
);
assert!(parse_net("/24").unwrap_err().contains("IPv4 address"));
}
#[test]
fn targets_skip_network_broadcast_and_self() {
let t = iface("192.168.1.5", "192.168.1.0/24").targets();
assert_eq!(t.len(), 253);
assert!(!t.contains(&"192.168.1.0".parse().unwrap()));
assert!(!t.contains(&"192.168.1.255".parse().unwrap()));
assert_eq!(iface("192.168.1.5", "10.0.0.0/31").targets().len(), 2);
assert_eq!(iface("192.168.1.5", "10.0.0.9/32").targets().len(), 1);
}
#[test]
fn on_link_when_the_networks_overlap() {
assert!(iface("192.168.1.5", "192.168.1.128/25").on_link());
assert!(iface("192.168.1.5", "192.168.0.0/16").on_link());
assert!(!iface("192.168.1.5", "10.0.0.0/24").on_link());
}
}