From bebe541d20a153c76570ed183a82991c4bacb27d Mon Sep 17 00:00:00 2001 From: shellrow Date: Mon, 26 May 2025 23:51:23 +0900 Subject: [PATCH 1/2] Add IP methods to Interface --- Cargo.toml | 5 ++ examples/global_ips.rs | 29 +++++++++ src/interface/mod.rs | 61 +++++++++++++++++- src/ip.rs | 137 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 5 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 examples/global_ips.rs create mode 100644 src/ip.rs diff --git a/Cargo.toml b/Cargo.toml index c042fd3..9b6d849 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,3 +59,8 @@ required-features = ["gateway"] name = "serialize" path = "examples/serialize.rs" required-features = ["serde", "gateway"] + +[[example]] +name = "global_ips" +path = "examples/global_ips.rs" +required-features = ["gateway"] diff --git a/examples/global_ips.rs b/examples/global_ips.rs new file mode 100644 index 0000000..edce4cd --- /dev/null +++ b/examples/global_ips.rs @@ -0,0 +1,29 @@ +// This example shows how to retrieve the global IP addresses of the default network interface. + +fn main() { + match netdev::get_default_interface() { + Ok(interface) => { + if interface.has_global_ipv4() { + let global_addrs = interface.global_ipv4_addrs(); + println!("Default Interface has global IPv4 addresses:"); + for ip in global_addrs { + println!("\t- {}", ip); + } + } else { + println!("Default Interface does not have a global IPv4 address."); + } + if interface.has_global_ipv6() { + let global_addrs = interface.global_ipv6_addrs(); + println!("Default Interface has global IPv6 addresses:"); + for ip in global_addrs { + println!("\t- {}", ip); + } + } else { + println!("Default Interface does not have a global IPv6 address."); + } + } + Err(e) => { + println!("Error: {}", e); + } + } +} diff --git a/src/interface/mod.rs b/src/interface/mod.rs index e26dd1a..7bd2f29 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -48,8 +48,8 @@ use crate::device::NetworkDevice; use crate::ipnet::{Ipv4Net, Ipv6Net}; use crate::mac::MacAddr; use crate::sys; -#[cfg(feature = "gateway")] -use std::net::IpAddr; +use crate::ip::{is_global_ip, is_global_ipv4, is_global_ipv6}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; /// Structure of Network Interface information #[derive(Clone, Eq, PartialEq, Hash, Debug)] @@ -195,6 +195,63 @@ impl Interface { && !crate::db::oui::is_virtual_mac(&self.mac_addr.unwrap_or(MacAddr::zero())) && !crate::db::oui::is_known_loopback_mac(&self.mac_addr.unwrap_or(MacAddr::zero())) } + /// Returns a list of IPv4 addresses assigned to this interface. + pub fn ipv4_addrs(&self) -> Vec { + self.ipv4.iter().map(|net| net.addr()).collect() + } + /// Returns a list of IPv6 addresses assigned to this interface. + pub fn ipv6_addrs(&self) -> Vec { + self.ipv6.iter().map(|net| net.addr()).collect() + } + /// Returns a list of all IP addresses (both IPv4 and IPv6) assigned to this interface. + pub fn ip_addrs(&self) -> Vec { + self.ipv4_addrs() + .into_iter() + .map(IpAddr::V4) + .chain(self.ipv6_addrs().into_iter().map(IpAddr::V6)) + .collect() + } + /// Returns true if this interface has at least one IPv4 address. + pub fn has_ipv4(&self) -> bool { + !self.ipv4.is_empty() + } + /// Returns true if this interface has at least one IPv6 address. + pub fn has_ipv6(&self) -> bool { + !self.ipv6.is_empty() + } + /// Returns true if this interface has at least one globally routable IPv4 address. + pub fn has_global_ipv4(&self) -> bool { + self.ipv4_addrs().iter().any(|ip| is_global_ipv4(ip)) + } + /// Returns true if this interface has at least one globally routable IPv6 address. + pub fn has_global_ipv6(&self) -> bool { + self.ipv6_addrs().iter().any(|ip| is_global_ipv6(ip)) + } + /// Returns true if this interface has at least one globally routable IP address (v4 or v6). + pub fn has_global_ip(&self) -> bool { + self.ip_addrs().iter().any(|ip| is_global_ip(ip)) + } + /// Returns a list of globally routable IPv4 addresses assigned to this interface. + pub fn global_ipv4_addrs(&self) -> Vec { + self.ipv4_addrs() + .into_iter() + .filter(|ip| is_global_ipv4(ip)) + .collect() + } + /// Returns a list of globally routable IPv6 addresses assigned to this interface. + pub fn global_ipv6_addrs(&self) -> Vec { + self.ipv6_addrs() + .into_iter() + .filter(|ip| is_global_ipv6(ip)) + .collect() + } + /// Returns a list of globally routable IP addresses (both IPv4 and IPv6). + pub fn global_ip_addrs(&self) -> Vec { + self.ip_addrs() + .into_iter() + .filter(|ip| is_global_ip(ip)) + .collect() + } } /// Get default Network Interface diff --git a/src/ip.rs b/src/ip.rs new file mode 100644 index 0000000..ba6dec3 --- /dev/null +++ b/src/ip.rs @@ -0,0 +1,137 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +/// Returns [`true`] if the address appears to be globally routable. +pub(crate) fn is_global_ip(ip_addr: &IpAddr) -> bool { + match ip_addr { + IpAddr::V4(ip) => is_global_ipv4(ip), + IpAddr::V6(ip) => is_global_ipv6(ip), + } +} + +/// Returns [`true`] if the address appears to be globally reachable +/// as specified by the [IANA IPv4 Special-Purpose Address Registry]. +pub(crate) fn is_global_ipv4(ipv4_addr: &Ipv4Addr) -> bool { + !(ipv4_addr.octets()[0] == 0 // "This network" + || ipv4_addr.is_private() + || is_shared_ipv4(ipv4_addr) + || ipv4_addr.is_loopback() + || ipv4_addr.is_link_local() + // addresses reserved for future protocols (`192.0.0.0/24`) + // .9 and .10 are documented as globally reachable so they're excluded + || ( + ipv4_addr.octets()[0] == 192 && ipv4_addr.octets()[1] == 0 && ipv4_addr.octets()[2] == 0 + && ipv4_addr.octets()[3] != 9 && ipv4_addr.octets()[3] != 10 + ) + || ipv4_addr.is_documentation() + || is_benchmarking_ipv4(ipv4_addr) + || is_reserved_ipv4(ipv4_addr) + || ipv4_addr.is_broadcast()) +} + +/// Returns [`true`] if the address appears to be globally reachable +/// as specified by the [IANA IPv6 Special-Purpose Address Registry]. +pub(crate) fn is_global_ipv6(ipv6_addr: &Ipv6Addr) -> bool { + !(ipv6_addr.is_unspecified() + || ipv6_addr.is_loopback() + // IPv4-mapped Address (`::ffff:0:0/96`) + || matches!(ipv6_addr.segments(), [0, 0, 0, 0, 0, 0xffff, _, _]) + // IPv4-IPv6 Translat. (`64:ff9b:1::/48`) + || matches!(ipv6_addr.segments(), [0x64, 0xff9b, 1, _, _, _, _, _]) + // Discard-Only Address Block (`100::/64`) + || matches!(ipv6_addr.segments(), [0x100, 0, 0, 0, _, _, _, _]) + // IETF Protocol Assignments (`2001::/23`) + || (matches!(ipv6_addr.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200) + && !( + // Port Control Protocol Anycast (`2001:1::1`) + u128::from_be_bytes(ipv6_addr.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001 + // Traversal Using Relays around NAT Anycast (`2001:1::2`) + || u128::from_be_bytes(ipv6_addr.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002 + // AMT (`2001:3::/32`) + || matches!(ipv6_addr.segments(), [0x2001, 3, _, _, _, _, _, _]) + // AS112-v6 (`2001:4:112::/48`) + || matches!(ipv6_addr.segments(), [0x2001, 4, 0x112, _, _, _, _, _]) + // ORCHIDv2 (`2001:20::/28`) + // Drone Remote ID Protocol Entity Tags (DETs) Prefix (`2001:30::/28`)` + || matches!(ipv6_addr.segments(), [0x2001, b, _, _, _, _, _, _] if b >= 0x20 && b <= 0x3F) + )) + // 6to4 (`2002::/16`) – it's not explicitly documented as globally reachable, + // IANA says N/A. + || matches!(ipv6_addr.segments(), [0x2002, _, _, _, _, _, _, _]) + || is_documentation_ipv6(ipv6_addr) + || ipv6_addr.is_unique_local() + || ipv6_addr.is_unicast_link_local()) +} + +/// Returns [`true`] if this address is part of the Shared Address Space defined in +/// [IETF RFC 6598] (`100.64.0.0/10`). +/// +/// [IETF RFC 6598]: https://tools.ietf.org/html/rfc6598 +fn is_shared_ipv4(ipv4_addr: &Ipv4Addr) -> bool { + ipv4_addr.octets()[0] == 100 && (ipv4_addr.octets()[1] & 0b1100_0000 == 0b0100_0000) +} + +/// Returns [`true`] if this address part of the `198.18.0.0/15` range, which is reserved for +/// network devices benchmarking. +fn is_benchmarking_ipv4(ipv4_addr: &Ipv4Addr) -> bool { + ipv4_addr.octets()[0] == 198 && (ipv4_addr.octets()[1] & 0xfe) == 18 +} + +/// Returns [`true`] if this address is reserved by IANA for future use. +fn is_reserved_ipv4(ipv4_addr: &Ipv4Addr) -> bool { + ipv4_addr.octets()[0] & 240 == 240 && !ipv4_addr.is_broadcast() +} + +/// Returns [`true`] if this is an address reserved for documentation +/// (`2001:db8::/32` and `3fff::/20`). +fn is_documentation_ipv6(ipv6_addr: &Ipv6Addr) -> bool { + matches!(ipv6_addr.segments(), [0x2001, 0xdb8, ..] | [0x3fff, 0..=0x0fff, ..]) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + #[test] + fn test_is_global_ipv4() { + let global = Ipv4Addr::new(1, 1, 1, 1); // Cloudflare + let private = Ipv4Addr::new(192, 168, 1, 1); + let loopback = Ipv4Addr::new(127, 0, 0, 1); + let shared = Ipv4Addr::new(100, 64, 0, 1); // RFC6598 + let doc = Ipv4Addr::new(192, 0, 2, 1); // Documentation + + assert!(is_global_ipv4(&global)); + assert!(!is_global_ipv4(&private)); + assert!(!is_global_ipv4(&loopback)); + assert!(!is_global_ipv4(&shared)); + assert!(!is_global_ipv4(&doc)); + } + + #[test] + fn test_is_global_ipv6() { + let global = Ipv6Addr::new(0x2606, 0x4700, 0, 0, 0, 0, 0, 0x1111); // Cloudflare + let loopback = Ipv6Addr::LOCALHOST; + let unspecified = Ipv6Addr::UNSPECIFIED; + let unique_local = Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1); + let doc = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1); // Documentation + + assert!(is_global_ipv6(&global)); + assert!(!is_global_ipv6(&loopback)); + assert!(!is_global_ipv6(&unspecified)); + assert!(!is_global_ipv6(&unique_local)); + assert!(!is_global_ipv6(&doc)); + } + + #[test] + fn test_is_global_ip() { + let ip_v4 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + let ip_v6 = IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0, 0, 0, 0, 0, 0x1111)); // Cloudflare + let ip_private = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)); + let ip_ula = IpAddr::V6(Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1)); + + assert!(is_global_ip(&ip_v4)); + assert!(is_global_ip(&ip_v6)); + assert!(!is_global_ip(&ip_private)); + assert!(!is_global_ip(&ip_ula)); + } +} diff --git a/src/lib.rs b/src/lib.rs index a9508d3..11887cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod gateway; pub mod interface; pub mod mac; mod sys; +mod ip; pub use device::NetworkDevice; #[cfg(feature = "gateway")] From 1a151803500641f1b60d22a9cc612f66f1793afd Mon Sep 17 00:00:00 2001 From: shellrow Date: Tue, 27 May 2025 00:10:54 +0900 Subject: [PATCH 2/2] Run cargo fmt --- src/interface/mod.rs | 2 +- src/ip.rs | 5 ++++- src/lib.rs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/interface/mod.rs b/src/interface/mod.rs index 7bd2f29..6ff3c23 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -45,10 +45,10 @@ mod android; mod macos; #[cfg(feature = "gateway")] use crate::device::NetworkDevice; +use crate::ip::{is_global_ip, is_global_ipv4, is_global_ipv6}; use crate::ipnet::{Ipv4Net, Ipv6Net}; use crate::mac::MacAddr; use crate::sys; -use crate::ip::{is_global_ip, is_global_ipv4, is_global_ipv6}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; /// Structure of Network Interface information diff --git a/src/ip.rs b/src/ip.rs index ba6dec3..af8ada1 100644 --- a/src/ip.rs +++ b/src/ip.rs @@ -84,7 +84,10 @@ fn is_reserved_ipv4(ipv4_addr: &Ipv4Addr) -> bool { /// Returns [`true`] if this is an address reserved for documentation /// (`2001:db8::/32` and `3fff::/20`). fn is_documentation_ipv6(ipv6_addr: &Ipv6Addr) -> bool { - matches!(ipv6_addr.segments(), [0x2001, 0xdb8, ..] | [0x3fff, 0..=0x0fff, ..]) + matches!( + ipv6_addr.segments(), + [0x2001, 0xdb8, ..] | [0x3fff, 0..=0x0fff, ..] + ) } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 11887cb..c354324 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,9 +3,9 @@ pub mod device; #[cfg(feature = "gateway")] pub mod gateway; pub mod interface; +mod ip; pub mod mac; mod sys; -mod ip; pub use device::NetworkDevice; #[cfg(feature = "gateway")]