//! Contains helpers for looking up system network interfaces.
use std::{collections::HashMap, fmt, net::IpAddr};
#[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "macos",
target_os = "ios"
))]
pub(super) mod bsd;
#[cfg(any(target_os = "linux", target_os = "android"))]
mod linux;
#[cfg(target_os = "windows")]
mod windows;
pub(crate) use netdev::ipnet::{Ipv4Net, Ipv6Net};
#[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "macos",
target_os = "ios"
))]
use self::bsd::default_route;
#[cfg(any(target_os = "linux", target_os = "android"))]
use self::linux::default_route;
#[cfg(target_os = "windows")]
use self::windows::default_route;
#[cfg(not(wasm_browser))]
use crate::ip::is_link_local;
use crate::ip::{LocalAddresses, is_private_v6, is_up};
#[cfg(not(wasm_browser))]
use crate::netmon::is_interesting_interface;
/// Represents a network interface.
#[derive(Debug, Clone)]
pub struct Interface {
iface: netdev::Interface,
}
impl fmt::Display for Interface {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}. {} {:?} ipv4={:?} ipv6={:?}",
self.iface.index, self.iface.name, self.iface.if_type, self.iface.ipv4, self.iface.ipv6
)
}
}
impl PartialEq for Interface {
fn eq(&self, other: &Self) -> bool {
self.iface.index == other.iface.index
&& self.iface.name == other.iface.name
&& self.iface.flags == other.iface.flags
&& self.iface.mac_addr.as_ref().map(|a| a.octets())
== other.iface.mac_addr.as_ref().map(|a| a.octets())
}
}
impl Eq for Interface {}
impl Interface {
/// Is this interface up?
pub fn is_up(&self) -> bool {
is_up(&self.iface)
}
/// The name of the interface.
pub fn name(&self) -> &str {
&self.iface.name
}
/// A list of all ip addresses of this interface.
pub fn addrs(&self) -> impl Iterator<Item = IpNet> + '_ {
self.iface
.ipv4
.iter()
.cloned()
.map(IpNet::V4)
.chain(self.iface.ipv6.iter().cloned().map(IpNet::V6))
}
/// Creates a fake interface for usage in tests.
///
/// This allows tests to be independent of the host interfaces.
pub(crate) fn fake() -> Self {
use std::net::Ipv4Addr;
use netdev::{
NetworkDevice,
prelude::{InterfaceType, MacAddr, OperState},
};
Self {
iface: netdev::Interface {
index: 2,
name: String::from("wifi0"),
friendly_name: None,
description: None,
if_type: InterfaceType::Ethernet,
mac_addr: Some(MacAddr::new(2, 3, 4, 5, 6, 7)),
ipv4: vec![Ipv4Net::new(Ipv4Addr::new(192, 168, 0, 189), 24).unwrap()],
ipv6: vec![],
flags: 69699,
transmit_speed: None,
receive_speed: None,
gateway: Some(NetworkDevice {
mac_addr: MacAddr::new(2, 3, 4, 5, 6, 8),
ipv4: vec![Ipv4Addr::from([192, 168, 0, 1])],
ipv6: vec![],
}),
dns_servers: vec![],
default: false,
ipv6_scope_ids: vec![],
stats: None,
mtu: None,
oper_state: OperState::Up,
},
}
}
}
/// Structure of an IP network, either IPv4 or IPv6.
#[derive(Clone, Debug)]
pub enum IpNet {
/// Structure of IPv4 Network.
V4(Ipv4Net),
/// Structure of IPv6 Network.
V6(Ipv6Net),
}
impl PartialEq for IpNet {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(IpNet::V4(a), IpNet::V4(b)) => {
a.addr() == b.addr()
&& a.prefix_len() == b.prefix_len()
&& a.netmask() == b.netmask()
}
(IpNet::V6(a), IpNet::V6(b)) => {
a.addr() == b.addr()
&& a.prefix_len() == b.prefix_len()
&& a.netmask() == b.netmask()
}
_ => false,
}
}
}
impl Eq for IpNet {}
impl IpNet {
/// The IP address of this structure.
pub fn addr(&self) -> IpAddr {
match self {
IpNet::V4(a) => IpAddr::V4(a.addr()),
IpNet::V6(a) => IpAddr::V6(a.addr()),
}
}
}
/// Intended to store the state of the machine's network interfaces, routing table, and
/// other network configuration. For now it's pretty basic.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct State {
/// Maps from an interface name interface.
pub interfaces: HashMap<String, Interface>,
/// List of machine's local IP addresses.
pub local_addresses: LocalAddresses,
/// Whether this machine has an IPv6 Global or Unique Local Address
/// which might provide connectivity.
pub have_v6: bool,
/// Whether the machine has some non-localhost, non-link-local IPv4 address.
pub have_v4: bool,
/// Whether the current network interface is considered "expensive", which currently means LTE/etc
/// instead of Wifi. This field is not populated by `get_state`.
pub is_expensive: bool,
/// The interface name for the machine's default route.
///
/// It is not yet populated on all OSes.
///
/// When set, its value is the map key into `interface` and `interface_ips`.
pub default_route_interface: Option<String>,
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut ifaces: Vec<_> = self.interfaces.values().collect();
ifaces.sort_by_key(|iface| iface.iface.index);
for iface in ifaces {
write!(f, "{iface}")?;
if let Some(ref default_if) = self.default_route_interface
&& iface.name() == default_if
{
write!(f, " (default)")?;
}
if f.alternate() {
writeln!(f)?;
} else {
write!(f, "; ")?;
}
}
Ok(())
}
}
impl State {
/// Returns the state of all the current machine's network interfaces.
///
/// It does not set the returned `State.is_expensive`. The caller can populate that.
pub async fn new() -> Self {
let mut interfaces = HashMap::new();
let mut have_v6 = false;
let mut have_v4 = false;
let ifaces = netdev::interface::get_interfaces();
let local_addresses = LocalAddresses::from_raw_interfaces(&ifaces);
for iface in ifaces {
let ni = Interface { iface };
let if_up = ni.is_up();
let name = ni.iface.name.clone();
let pfxs: Vec<_> = ni.addrs().collect();
if if_up {
for pfx in &pfxs {
if pfx.addr().is_loopback() {
continue;
}
have_v6 |= is_usable_v6(&pfx.addr());
have_v4 |= is_usable_v4(&pfx.addr());
}
}
interfaces.insert(name, ni);
}
let default_route_interface = default_route_interface().await;
State {
interfaces,
local_addresses,
have_v4,
have_v6,
is_expensive: false,
default_route_interface,
}
}
/// Creates a fake interface state for usage in tests.
///
/// This allows tests to be independent of the host interfaces.
pub fn fake() -> Self {
let fake = Interface::fake();
let ifname = fake.iface.name.clone();
Self {
interfaces: [(ifname.clone(), fake)].into_iter().collect(),
local_addresses: LocalAddresses::from_raw_interfaces(&[]),
have_v6: true,
have_v4: true,
is_expensive: false,
default_route_interface: Some(ifname),
}
}
/// Is this a major change compared to the `old` one?.
pub fn is_major_change(&self, old: &State) -> bool {
if self.have_v6 != old.have_v6
|| self.have_v4 != old.have_v4
|| self.is_expensive != old.is_expensive
|| self.default_route_interface != old.default_route_interface
{
return true;
}
for (iname, i) in &old.interfaces {
if !is_interesting_interface(i.name()) {
continue;
}
let Some(i2) = self.interfaces.get(iname) else {
return true;
};
if i != i2 || !prefixes_major_equal(i.addrs(), i2.addrs()) {
return true;
}
}
false
}
}
/// Reports whether ip is a usable IPv4 address which should have Internet connectivity.
///
/// Globally routable and private IPv4 addresses are always Usable, and link local
/// 169.254.x.x addresses are in some environments.
fn is_usable_v4(ip: &IpAddr) -> bool {
if !ip.is_ipv4() || ip.is_loopback() {
return false;
}
true
}
/// Reports whether ip is a usable IPv6 address which should have Internet connectivity.
///
/// Globally routable IPv6 addresses are always Usable, and Unique Local Addresses
/// (fc00::/7) are in some environments used with address translation.
///
/// We consider all 2000::/3 addresses to be routable, which is the interpretation of
/// <https://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.xhtml>
/// as well. However this probably includes some addresses which should not be routed,
/// e.g. documentation addresses. See also
/// <https://doc.rust-lang.org/std/net/struct.Ipv6Addr.html#method.is_global> for an
/// alternative implementation which is both stricter and laxer in some regards.
fn is_usable_v6(ip: &IpAddr) -> bool {
match ip {
IpAddr::V6(ip) => {
// V6 Global1 2000::/3
let mask: u16 = 0b1110_0000_0000_0000;
let base: u16 = 0x2000;
let segment1 = ip.segments()[0];
if (base & mask) == (segment1 & mask) {
return true;
}
is_private_v6(ip)
}
IpAddr::V4(_) => false,
}
}
/// The details about a default route.
#[derive(Debug, Clone)]
pub struct DefaultRouteDetails {
/// The interface name.
/// It's like "eth0" (Linux), "Ethernet 2" (Windows), "en0" (macOS).
pub interface_name: String,
}
impl DefaultRouteDetails {
/// Reads the default route from the current system and returns the details.
pub async fn new() -> Option<Self> {
default_route().await
}
}
/// Like `DefaultRoutDetails::new` but only returns the interface name.
pub async fn default_route_interface() -> Option<String> {
DefaultRouteDetails::new().await.map(|v| v.interface_name)
}
/// Likely IPs of the residentla router, and the ip address of the current
/// machine using it.
#[derive(Debug, Clone)]
pub struct HomeRouter {
/// Ip of the router.
pub gateway: IpAddr,
/// Our local Ip if known.
pub my_ip: Option<IpAddr>,
}
impl HomeRouter {
/// Returns the likely IP of the residential router, which will always
/// be a private address, if found.
/// In addition, it returns the IP address of the current machine on
/// the LAN using that gateway.
/// This is used as the destination for UPnP, NAT-PMP, PCP, etc queries.
pub fn new() -> Option<Self> {
let gateway = Self::get_default_gateway()?;
let my_ip = netdev::net::ip::get_local_ipaddr();
Some(HomeRouter { gateway, my_ip })
}
#[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "macos",
target_os = "ios"
))]
fn get_default_gateway() -> Option<IpAddr> {
// netdev doesn't work yet
// See: https://github.com/shellrow/default-net/issues/34
bsd::likely_home_router()
}
#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
fn get_default_gateway() -> Option<IpAddr> {
let gateway = netdev::get_default_gateway().ok()?;
gateway
.ipv4
.iter()
.cloned()
.map(IpAddr::V4)
.chain(gateway.ipv6.iter().cloned().map(IpAddr::V6))
.next()
}
}
/// Checks whether `a` and `b` are equal after ignoring uninteresting
/// things, like link-local, loopback and multicast addresses.
#[cfg(not(wasm_browser))]
fn prefixes_major_equal(a: impl Iterator<Item = IpNet>, b: impl Iterator<Item = IpNet>) -> bool {
fn is_interesting(p: &IpNet) -> bool {
let a = p.addr();
if is_link_local(a) || a.is_loopback() || a.is_multicast() {
return false;
}
true
}
let a = a.filter(is_interesting);
let b = b.filter(is_interesting);
for (a, b) in a.zip(b) {
if a != b {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use std::net::Ipv6Addr;
use super::*;
#[tokio::test]
async fn test_default_route() {
let default_route = DefaultRouteDetails::new()
.await
.expect("missing default route");
println!("default_route: {default_route:#?}");
}
#[tokio::test]
async fn test_likely_home_router() {
let home_router = HomeRouter::new().expect("missing home router");
println!("home router: {home_router:#?}");
}
#[test]
fn test_is_usable_v6() {
let loopback = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0x1);
assert!(!is_usable_v6(&loopback.into()));
let link_local = Ipv6Addr::new(0xfe80, 0, 0, 0, 0xcbc9, 0x6aff, 0x5b07, 0x4a9e);
assert!(!is_usable_v6(&link_local.into()));
let relay_use1 = Ipv6Addr::new(0x2a01, 0x4ff, 0xf0, 0xc4a1, 0, 0, 0, 0x1);
assert!(is_usable_v6(&relay_use1.into()));
let random_2603 = Ipv6Addr::new(0x2603, 0x3ff, 0xf1, 0xc3aa, 0x1, 0x2, 0x3, 0x1);
assert!(is_usable_v6(&random_2603.into()));
}
}
//! Contains helpers for looking up system network interfaces.
use ;
pub
pub use ;
use default_route;
use default_route;
use default_route;
use crateis_link_local;
use crate;
use crateis_interesting_interface;
/// Represents a network interface.
/// Structure of an IP network, either IPv4 or IPv6.
/// Intended to store the state of the machine's network interfaces, routing table, and
/// other network configuration. For now it's pretty basic.
/// Reports whether ip is a usable IPv4 address which should have Internet connectivity.
///
/// Globally routable and private IPv4 addresses are always Usable, and link local
/// 169.254.x.x addresses are in some environments.
/// Reports whether ip is a usable IPv6 address which should have Internet connectivity.
///
/// Globally routable IPv6 addresses are always Usable, and Unique Local Addresses
/// (fc00::/7) are in some environments used with address translation.
///
/// We consider all 2000::/3 addresses to be routable, which is the interpretation of
/// <https://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.xhtml>
/// as well. However this probably includes some addresses which should not be routed,
/// e.g. documentation addresses. See also
/// <https://doc.rust-lang.org/std/net/struct.Ipv6Addr.html#method.is_global> for an
/// alternative implementation which is both stricter and laxer in some regards.
/// The details about a default route.
/// Like `DefaultRoutDetails::new` but only returns the interface name.
pub async
/// Likely IPs of the residentla router, and the ip address of the current
/// machine using it.
/// Checks whether `a` and `b` are equal after ignoring uninteresting
/// things, like link-local, loopback and multicast addresses.