diff --git a/Cargo.toml b/Cargo.toml index 6aa983c..7c592c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "netdev" -version = "0.37.3" +version = "0.38.0" authors = ["shellrow "] -edition = "2021" +edition = "2024" description = "Cross-platform library for network interface" repository = "https://github.com/shellrow/netdev" readme = "README.md" @@ -17,14 +17,14 @@ ipnet = { version = "2.11" } [target.'cfg(unix)'.dependencies] libc = "0.2" +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +netlink-packet-core = "0.8" +netlink-packet-route = "0.25" +netlink-sys = "0.8" + [target.'cfg(target_os = "android")'.dependencies] -# DL Open dlopen2 = { version = "0.5", default-features = false } once_cell = "1" -# netlink -netlink-packet-core = "0.7" -netlink-packet-route = "0.22.0" -netlink-sys = "0.8" [target.'cfg(windows)'.dependencies.windows-sys] version = "0.59" @@ -73,3 +73,4 @@ required-features = ["gateway"] [[example]] name = "stats" path = "examples/stats.rs" +required-features = ["gateway"] diff --git a/README.md b/README.md index 568e0b0..3f2b6da 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [license-badge]: https://img.shields.io/crates/l/netdev.svg [examples-url]: https://github.com/shellrow/netdev/tree/main/examples [doc-url]: https://docs.rs/netdev/latest/netdev -[doc-interface-url]: https://docs.rs/netdev/latest/netdev/interface/struct.Interface.html +[doc-interface-url]: https://docs.rs/netdev/latest/netdev/interface/interface/struct.Interface.html [netdev-github-url]: https://github.com/shellrow/netdev [default-net-github-url]: https://github.com/shellrow/default-net [default-net-crates-io-url]: https://crates.io/crates/default-net @@ -32,7 +32,7 @@ This project was rebranded from [default-net][default-net-crates-io-url] by the Add `netdev` to your dependencies ```toml:Cargo.toml [dependencies] -netdev = "0.37" +netdev = "0.38" ``` For more details, see [examples][examples-url] or [doc][doc-url]. @@ -58,9 +58,17 @@ For more details, see [examples][examples-url] or [doc][doc-url]. - 13.4.1 - 11.6 - Windows + - 11 24H2 26100.6584 - 11 23H2 22631.4602 - 11 Pro 22H2 22621.3155 - 11 22H2 22621.3155 - 10 21H2 19044.1586 - FreeBSD - 14 +- Android (arm64) + - 16.0 +- Android (x86_64) + - 16.0 +- iOS + - 18.6.2 + - 18.1.1 diff --git a/src/gateway/bsd.rs b/src/gateway/bsd.rs deleted file mode 100644 index 62cccb3..0000000 --- a/src/gateway/bsd.rs +++ /dev/null @@ -1,480 +0,0 @@ -#![allow(non_camel_case_types)] - -use crate::device::NetworkDevice; -use crate::mac::MacAddr; - -use std::{ - collections::HashMap, - io, - net::{IpAddr, Ipv4Addr, Ipv6Addr}, -}; - -const CTL_NET: u32 = 4; -const AF_INET: u32 = 2; -const AF_ROUTE: u32 = 17; -const AF_LINK: u32 = 18; -const AF_INET6: u32 = 30; -const PF_ROUTE: u32 = 17; -const NET_RT_DUMP: u32 = 1; -const NET_RT_FLAGS: u32 = 2; - -#[cfg(any(target_os = "freebsd", target_os = "openbsd"))] -const RTM_VERSION: u32 = 5; -#[cfg(target_os = "netbsd")] -const RTM_VERSION: u32 = 4; - -const RTF_LLINFO: u32 = 1024; -const RTF_WASCLONED: u32 = 0x20000; -const RTAX_DST: u32 = 0; -const RTAX_GATEWAY: u32 = 1; -const RTAX_NETMASK: u32 = 2; - -#[cfg(target_os = "freebsd")] -const RTAX_MAX: u32 = 8; -#[cfg(target_os = "netbsd")] -const RTAX_MAX: u32 = 9; -#[cfg(target_os = "openbsd")] -const RTAX_MAX: u32 = 15; - -type __int32_t = ::std::os::raw::c_int; -type __uint8_t = ::std::os::raw::c_uchar; -type __uint16_t = ::std::os::raw::c_ushort; -type __uint32_t = ::std::os::raw::c_uint; -type sa_family_t = __uint8_t; -type in_addr_t = __uint32_t; -type in_port_t = __uint16_t; -type u_int = ::std::os::raw::c_uint; -type u_short = ::std::os::raw::c_ushort; -type u_char = ::std::os::raw::c_uchar; -type u_int32_t = ::std::os::raw::c_uint; -type size_t = usize; -type pid_t = __int32_t; - -extern "C" { - fn sysctl( - arg1: *mut ::std::os::raw::c_int, - arg2: u_int, - arg3: *mut ::std::os::raw::c_void, - arg4: *mut size_t, - arg5: *mut ::std::os::raw::c_void, - arg6: size_t, - ) -> ::std::os::raw::c_int; -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -struct rt_msghdr { - pub rtm_msglen: u_short, - pub rtm_version: u_char, - pub rtm_type: u_char, - pub rtm_index: u_short, - pub rtm_flags: ::std::os::raw::c_int, - pub rtm_addrs: ::std::os::raw::c_int, - pub rtm_pid: pid_t, - pub rtm_seq: ::std::os::raw::c_int, - pub rtm_errno: ::std::os::raw::c_int, - pub rtm_use: ::std::os::raw::c_int, - pub rtm_inits: u_int32_t, - pub rtm_rmx: rt_metrics, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -struct rt_metrics { - pub rmx_locks: u_int32_t, - pub rmx_mtu: u_int32_t, - pub rmx_hopcount: u_int32_t, - pub rmx_expire: i32, - pub rmx_recvpipe: u_int32_t, - pub rmx_sendpipe: u_int32_t, - pub rmx_ssthresh: u_int32_t, - pub rmx_rtt: u_int32_t, - pub rmx_rttvar: u_int32_t, - pub rmx_pksent: u_int32_t, - pub rmx_weight: u_int32_t, - pub rmx_nhidx: u_int32_t, - pub rmx_filler: [u_int32_t; 2], -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -struct sockaddr { - pub sa_len: __uint8_t, - pub sa_family: sa_family_t, - pub sa_data: [::std::os::raw::c_char; 14usize], -} - -#[repr(C)] -#[derive(Copy, Clone)] -union in6_addr_bind { - pub __u6_addr8: [__uint8_t; 16usize], - pub __u6_addr16: [__uint16_t; 8usize], - pub __u6_addr32: [__uint32_t; 4usize], -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct in_addr { - pub s_addr: in_addr_t, -} - -#[repr(C)] -#[derive(Copy, Clone)] -struct in6_addr { - pub __u6_addr: in6_addr_bind, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -struct sockaddr_in { - pub sin_len: __uint8_t, - pub sin_family: sa_family_t, - pub sin_port: in_port_t, - pub sin_addr: in_addr, - pub sin_zero: [::std::os::raw::c_char; 8usize], -} - -#[repr(C)] -#[derive(Copy, Clone)] -struct sockaddr_in6 { - pub sin6_len: __uint8_t, - pub sin6_family: sa_family_t, - pub sin6_port: in_port_t, - pub sin6_flowinfo: __uint32_t, - pub sin6_addr: in6_addr, - pub sin6_scope_id: __uint32_t, -} - -fn code_to_error(err: i32) -> io::Error { - let kind = match err { - 17 => io::ErrorKind::AlreadyExists, // EEXIST - 3 => io::ErrorKind::NotFound, // ESRCH - 3436 => io::ErrorKind::OutOfMemory, // ENOBUFS - _ => io::ErrorKind::Other, - }; - - io::Error::new(kind, format!("rtm_errno {}", err)) -} - -fn socketaddr_to_ipaddr(sa: &sockaddr) -> Option { - match sa.sa_family as u32 { - AF_INET => { - let inet: &sockaddr_in = unsafe { std::mem::transmute(sa) }; - let octets: [u8; 4] = inet.sin_addr.s_addr.to_ne_bytes(); - Some(IpAddr::from(octets)) - } - AF_INET6 => { - let inet6: &sockaddr_in6 = unsafe { std::mem::transmute(sa) }; - let octets: [u8; 16] = unsafe { inet6.sin6_addr.__u6_addr.__u6_addr8 }; - Some(IpAddr::from(octets)) - } - AF_LINK => None, - _ => None, - } -} - -fn message_to_route(hdr: &rt_msghdr, msg: &[u8]) -> Option { - const MSG_START_INDEX: usize = 60; - let mut gateway = None; - if hdr.rtm_addrs & (1 << RTAX_DST) == 0 { - return None; - } - let mut route_addresses = [None; RTAX_MAX as usize]; - let mut cur_pos = MSG_START_INDEX; - for idx in 0..RTAX_MAX as usize { - if hdr.rtm_addrs & (1 << idx) != 0 { - let buf = &msg[cur_pos..]; - let sa: &sockaddr = unsafe { &*(buf.as_ptr() as *const sockaddr) }; - route_addresses[idx] = Some(sa); - let aligned_len = if sa.sa_len == 0 { - 4 - } else { - ((sa.sa_len - 1) | 0x3) + 1 - }; - cur_pos += aligned_len as usize; - } - } - let sa = match route_addresses[RTAX_DST as usize] { - Some(sa) => sa, - None => return None, - }; - let destination = socketaddr_to_ipaddr(sa)?; - - let mut prefix = match destination { - IpAddr::V4(_) => 32, - IpAddr::V6(_) => 128, - }; - - if hdr.rtm_addrs & (1 << RTAX_GATEWAY) != 0 { - let gw_sa = match route_addresses[RTAX_GATEWAY as usize] { - Some(sa) => sa, - None => return None, - }; - gateway = socketaddr_to_ipaddr(gw_sa); - if let Some(IpAddr::V6(ipv6gw)) = gateway { - let is_unicast_ll = ipv6gw.segments()[0] == 0xfe80; - let is_multicast = ipv6gw.octets()[0] == 0xff; - let multicast_scope = ipv6gw.octets()[1] & 0x0f; - if is_unicast_ll || (is_multicast && (multicast_scope == 1 || multicast_scope == 2)) { - let segs = ipv6gw.segments(); - gateway = Some(IpAddr::V6(Ipv6Addr::new( - segs[0], 0, segs[2], segs[3], segs[4], segs[5], segs[6], segs[7], - ))) - } - } - } - - if hdr.rtm_addrs & (1 << RTAX_NETMASK) != 0 { - let sa = route_addresses[RTAX_NETMASK as usize].unwrap(); - if sa.sa_len == 0 { - prefix = 0; - } else { - match destination { - IpAddr::V4(_) => { - let mask_sa: &sockaddr_in = unsafe { std::mem::transmute(sa) }; - prefix = u32::from_be(mask_sa.sin_addr.s_addr).leading_ones() as u8; - } - IpAddr::V6(_) => { - let mask_sa: &sockaddr_in6 = unsafe { std::mem::transmute(sa) }; - prefix = u128::from_be_bytes(unsafe { mask_sa.sin6_addr.__u6_addr.__u6_addr8 }) - .leading_ones() as u8; - } - } - } - } - - Some(Route { - destination, - prefix, - gateway, - ifindex: Some(hdr.rtm_index as u32), - }) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Route { - pub destination: IpAddr, - pub prefix: u8, - pub gateway: Option, - pub ifindex: Option, -} - -fn list_routes() -> io::Result> { - let mut mib: [u32; 7] = [0; 7]; - let mut len = 0; - - mib[0] = CTL_NET; - mib[1] = PF_ROUTE; - mib[2] = 0; - mib[3] = 0; - mib[4] = NET_RT_DUMP; - mib[5] = 0; - mib[6] = 0; - - if unsafe { - sysctl( - &mut mib as *mut _ as *mut _, - 7, - std::ptr::null_mut(), - &mut len, - std::ptr::null_mut(), - 0, - ) - } < 0 - { - return Err(io::Error::last_os_error()); - } - - let mut msgs_buf: Vec = vec![0; len as usize]; - - if unsafe { - sysctl( - &mut mib as *mut _ as *mut _, - 7, - msgs_buf.as_mut_ptr() as _, - &mut len, - std::ptr::null_mut(), - 0, - ) - } < 0 - { - return Err(io::Error::last_os_error()); - } - - let mut routes = vec![]; - let mut offset = 0; - - while offset + std::mem::size_of::() <= len as usize { - let buf = &mut msgs_buf[offset..]; - - if buf.len() < std::mem::size_of::() { - break; - } - - let rt_hdr = unsafe { std::mem::transmute::<_, &rt_msghdr>(buf.as_ptr()) }; - if rt_hdr.rtm_version as u32 != RTM_VERSION { - eprintln!( - "unexpected RTM_VERSION: {} in {:?}", - rt_hdr.rtm_version, rt_hdr - ); - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("unexpected RTM_VERSION: {}", rt_hdr.rtm_version), - )); - } - - if rt_hdr.rtm_errno != 0 { - return Err(code_to_error(rt_hdr.rtm_errno)); - } - - let msg_len = rt_hdr.rtm_msglen as usize; - offset += msg_len; - - if rt_hdr.rtm_flags as u32 & RTF_WASCLONED != 0 { - continue; - } - let rt_msg = &mut buf[std::mem::size_of::()..msg_len]; - - if let Some(route) = message_to_route(rt_hdr, rt_msg) { - routes.push(route); - } - } - - Ok(routes) -} - -fn message_to_arppair(msg_bytes: *mut u8) -> (IpAddr, MacAddr) { - const IP_START_INDEX: usize = 64; - const IP_END_INDEX: usize = 67; - const MAC_START_INDEX: usize = 84; - const MAC_END_INDEX: usize = 89; - let ip_bytes = unsafe { - std::slice::from_raw_parts( - msg_bytes.add(IP_START_INDEX), - IP_END_INDEX + 1 - IP_START_INDEX, - ) - }; - let mac_bytes = unsafe { - std::slice::from_raw_parts( - msg_bytes.add(MAC_START_INDEX), - MAC_END_INDEX + 1 - MAC_START_INDEX, - ) - }; - let ip_addr = IpAddr::V4(Ipv4Addr::new( - ip_bytes[0], - ip_bytes[1], - ip_bytes[2], - ip_bytes[3], - )); - let mac_addr = MacAddr::from_octets([ - mac_bytes[0], - mac_bytes[1], - mac_bytes[2], - mac_bytes[3], - mac_bytes[4], - mac_bytes[5], - ]); - (ip_addr, mac_addr) -} - -fn get_arp_table() -> io::Result> { - let mut arp_map: HashMap = HashMap::new(); - let mut mib: [u32; 6] = [CTL_NET, AF_ROUTE, 0, AF_INET, NET_RT_FLAGS, RTF_LLINFO]; - let mut len: libc::size_t = 0; - - unsafe { - if sysctl( - &mut mib as *mut _ as *mut _, - mib.len() as u32, - std::ptr::null_mut(), - &mut len, - std::ptr::null_mut(), - 0, - ) < 0 - { - return Err(io::Error::last_os_error()); - } - - let mut buf: Vec = vec![0; len as usize]; - if sysctl( - &mut mib as *mut _ as *mut _, - mib.len() as u32, - buf.as_mut_ptr() as *mut _, - &mut len, - std::ptr::null_mut(), - 0, - ) < 0 - { - return Err(io::Error::last_os_error()); - } - - let mut offset = 0; - while offset < len as usize { - let rt_hdr = &*(buf.as_ptr().add(offset) as *const rt_msghdr); - if rt_hdr.rtm_version as u32 != RTM_VERSION { - eprintln!( - "Unexpected RTM_VERSION: {} in {:?}", - rt_hdr.rtm_version, rt_hdr - ); - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Unexpected RTM_VERSION: {}", rt_hdr.rtm_version), - )); - } - - let msg_len = rt_hdr.rtm_msglen as usize; - offset += msg_len; - - let rt_msg: &mut [u8] = &mut buf[std::mem::size_of::()..msg_len]; - let (ip, mac) = message_to_arppair(rt_msg.as_mut_ptr()); - arp_map.insert(ip, mac); - } - } - - Ok(arp_map) -} - -fn get_default_routes() -> Vec { - let mut default_routes = Vec::new(); - match list_routes() { - Ok(routes) => { - for route in routes { - if (route.destination == Ipv4Addr::UNSPECIFIED - || route.destination == Ipv6Addr::UNSPECIFIED) - && route.prefix == 0 - && route.gateway != Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED)) - && route.gateway != Some(IpAddr::V6(Ipv6Addr::UNSPECIFIED)) - { - default_routes.push(route); - } - } - } - Err(_) => {} - } - default_routes -} - -pub fn get_gateway_map() -> HashMap { - let mut gateway_map: HashMap = HashMap::new(); - let routes = get_default_routes(); - let arp_map = get_arp_table().unwrap_or(HashMap::new()); - for route in routes { - if let Some(gw_ip) = route.gateway { - let gateway = gateway_map - .entry(route.ifindex.unwrap_or(0)) - .or_insert(NetworkDevice::new()); - if let Some(mac_addr) = arp_map.get(&gw_ip) { - gateway.mac_addr = mac_addr.clone(); - } - match gw_ip { - IpAddr::V4(ipv4) => { - gateway.ipv4.push(ipv4); - } - IpAddr::V6(ipv6) => { - gateway.ipv6.push(ipv6); - } - } - } - } - gateway_map -} diff --git a/src/gateway/macos.rs b/src/gateway/macos.rs deleted file mode 100644 index 8218c06..0000000 --- a/src/gateway/macos.rs +++ /dev/null @@ -1,495 +0,0 @@ -#![allow(non_camel_case_types)] - -use crate::device::NetworkDevice; -use crate::mac::MacAddr; - -use std::{ - collections::HashMap, - io, - net::{IpAddr, Ipv4Addr, Ipv6Addr}, -}; - -use libc::RTAX_MAX; - -const CTL_NET: u32 = 4; -const AF_INET: u32 = 2; -const AF_ROUTE: u32 = 17; -const AF_LINK: u32 = 18; -const AF_INET6: u32 = 30; -const PF_ROUTE: u32 = 17; -const NET_RT_DUMP: u32 = 1; -const NET_RT_FLAGS: u32 = 2; -const RTM_VERSION: u32 = 5; -const RTF_LLINFO: u32 = 1024; -const RTF_WASCLONED: u32 = 131072; -const RTAX_DST: u32 = 0; -const RTAX_GATEWAY: u32 = 1; -const RTAX_NETMASK: u32 = 2; - -type __int32_t = ::std::os::raw::c_int; -type __uint8_t = ::std::os::raw::c_uchar; -type __uint16_t = ::std::os::raw::c_ushort; -type __uint32_t = ::std::os::raw::c_uint; -type __darwin_size_t = ::std::os::raw::c_ulong; -type __darwin_pid_t = __int32_t; -type sa_family_t = __uint8_t; -type in_addr_t = __uint32_t; -type in_port_t = __uint16_t; -type u_int = ::std::os::raw::c_uint; -type u_short = ::std::os::raw::c_ushort; -type u_char = ::std::os::raw::c_uchar; -type u_int32_t = ::std::os::raw::c_uint; -type size_t = __darwin_size_t; -type pid_t = __darwin_pid_t; - -extern "C" { - fn sysctl( - arg1: *mut ::std::os::raw::c_int, - arg2: u_int, - arg3: *mut ::std::os::raw::c_void, - arg4: *mut size_t, - arg5: *mut ::std::os::raw::c_void, - arg6: size_t, - ) -> ::std::os::raw::c_int; -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -struct rt_msghdr { - pub rtm_msglen: u_short, - pub rtm_version: u_char, - pub rtm_type: u_char, - pub rtm_index: u_short, - pub rtm_flags: ::std::os::raw::c_int, - pub rtm_addrs: ::std::os::raw::c_int, - pub rtm_pid: pid_t, - pub rtm_seq: ::std::os::raw::c_int, - pub rtm_errno: ::std::os::raw::c_int, - pub rtm_use: ::std::os::raw::c_int, - pub rtm_inits: u_int32_t, - pub rtm_rmx: rt_metrics, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -struct rt_metrics { - pub rmx_locks: u_int32_t, - pub rmx_mtu: u_int32_t, - pub rmx_hopcount: u_int32_t, - pub rmx_expire: i32, - pub rmx_recvpipe: u_int32_t, - pub rmx_sendpipe: u_int32_t, - pub rmx_ssthresh: u_int32_t, - pub rmx_rtt: u_int32_t, - pub rmx_rttvar: u_int32_t, - pub rmx_pksent: u_int32_t, - pub rmx_state: u_int32_t, - pub rmx_filler: [u_int32_t; 3usize], -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -struct sockaddr { - pub sa_len: __uint8_t, - pub sa_family: sa_family_t, - pub sa_data: [::std::os::raw::c_char; 14usize], -} - -#[repr(C)] -#[derive(Copy, Clone)] -union in6_addr_bind { - pub __u6_addr8: [__uint8_t; 16usize], - pub __u6_addr16: [__uint16_t; 8usize], - pub __u6_addr32: [__uint32_t; 4usize], -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct in_addr { - pub s_addr: in_addr_t, -} - -#[repr(C)] -#[derive(Copy, Clone)] -struct in6_addr { - pub __u6_addr: in6_addr_bind, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -struct sockaddr_in { - pub sin_len: __uint8_t, - pub sin_family: sa_family_t, - pub sin_port: in_port_t, - pub sin_addr: in_addr, - pub sin_zero: [::std::os::raw::c_char; 8usize], -} - -#[repr(C)] -#[derive(Copy, Clone)] -struct sockaddr_in6 { - pub sin6_len: __uint8_t, - pub sin6_family: sa_family_t, - pub sin6_port: in_port_t, - pub sin6_flowinfo: __uint32_t, - pub sin6_addr: in6_addr, - pub sin6_scope_id: __uint32_t, -} - -fn code_to_error(err: i32) -> io::Error { - let kind = match err { - 17 => io::ErrorKind::AlreadyExists, // EEXIST - 3 => io::ErrorKind::NotFound, // ESRCH - 3436 => io::ErrorKind::OutOfMemory, // ENOBUFS - _ => io::ErrorKind::Other, - }; - - io::Error::new(kind, format!("rtm_errno {}", err)) -} - -fn socketaddr_to_ipaddr(sa: &sockaddr) -> Option { - match sa.sa_family as u32 { - AF_INET => { - let inet: &sockaddr_in = unsafe { std::mem::transmute(sa) }; - let octets: [u8; 4] = inet.sin_addr.s_addr.to_ne_bytes(); - Some(IpAddr::from(octets)) - } - AF_INET6 => { - let inet6: &sockaddr_in6 = unsafe { std::mem::transmute(sa) }; - let octets: [u8; 16] = unsafe { inet6.sin6_addr.__u6_addr.__u6_addr8 }; - Some(IpAddr::from(octets)) - } - AF_LINK => None, - _ => None, - } -} - -// https://opensource.apple.com/source/network_cmds/network_cmds-606.40.2/netstat.tproj/route.c.auto.html -// See function get_rtaddrs, macro ROUNDUP, function p_sockaddr -fn message_to_route(hdr: &rt_msghdr, msg: &[u8]) -> Option { - let mut gateway = None; - // Check if message has destination - if hdr.rtm_addrs & (1 << RTAX_DST) == 0 { - return None; - } - let mut route_addresses = [None; RTAX_MAX as usize]; - let mut cur_pos = 0; - for idx in 0..RTAX_MAX as usize { - if hdr.rtm_addrs & (1 << idx) != 0 { - let buf = &msg[cur_pos..]; - let sa: &sockaddr = unsafe { &*(buf.as_ptr() as *const sockaddr) }; - route_addresses[idx] = Some(sa); - let aligned_len = if sa.sa_len == 0 { - 4 - } else { - ((sa.sa_len - 1) | 0x3) + 1 - }; - cur_pos += aligned_len as usize; - } - } - - let sa = match route_addresses[RTAX_DST as usize] { - Some(sa) => sa, - None => return None, - }; - let destination = socketaddr_to_ipaddr(sa)?; - - let mut prefix = match destination { - IpAddr::V4(_) => 32, - IpAddr::V6(_) => 128, - }; - - // Check if message has a gateway - if hdr.rtm_addrs & (1 << RTAX_GATEWAY) != 0 { - let gw_sa = match route_addresses[RTAX_GATEWAY as usize] { - Some(sa) => sa, - None => return None, - }; - gateway = socketaddr_to_ipaddr(gw_sa); - if let Some(IpAddr::V6(ipv6gw)) = gateway { - // Unicast link local start with FE80:: - let is_unicast_ll = ipv6gw.segments()[0] == 0xfe80; - // IPv6 multicast starts with FF - let is_multicast = ipv6gw.octets()[0] == 0xff; - // lower 4 bit of byte1 encode the multicast scope - let multicast_scope = ipv6gw.octets()[1] & 0x0f; - // scope 1: interface/node-local - // scope 2: link-local - if is_unicast_ll || (is_multicast && (multicast_scope == 1 || multicast_scope == 2)) { - let segs = ipv6gw.segments(); - gateway = Some(IpAddr::V6(Ipv6Addr::new( - segs[0], 0, segs[2], segs[3], segs[4], segs[5], segs[6], segs[7], - ))) - } - } - } - - // Check if message has netmask - if hdr.rtm_addrs & (1 << RTAX_NETMASK) != 0 { - let sa = route_addresses[RTAX_NETMASK as usize].unwrap(); - if sa.sa_len == 0 { - prefix = 0; - } else { - match destination { - IpAddr::V4(_) => { - let mask_sa: &sockaddr_in = unsafe { std::mem::transmute(sa) }; - prefix = u32::from_be(mask_sa.sin_addr.s_addr).leading_ones() as u8; - } - IpAddr::V6(_) => { - let mask_sa: &sockaddr_in6 = unsafe { std::mem::transmute(sa) }; - prefix = u128::from_be_bytes(unsafe { mask_sa.sin6_addr.__u6_addr.__u6_addr8 }) - .leading_ones() as u8; - } - } - } - } - - Some(Route { - destination, - prefix, - gateway, - ifindex: Some(hdr.rtm_index as u32), - }) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Route { - pub destination: IpAddr, - pub prefix: u8, - pub gateway: Option, - pub ifindex: Option, -} - -fn list_routes() -> io::Result> { - let mut mib: [u32; 6] = [0; 6]; - let mut len = 0; - - mib[0] = CTL_NET; - mib[1] = AF_ROUTE; - mib[2] = 0; - mib[3] = 0; // AddressFamily: IPv4 & IPv6 - mib[4] = NET_RT_DUMP; - // mib[5] flags: 0 - - if unsafe { - sysctl( - &mut mib as *mut _ as *mut _, - 6, - std::ptr::null_mut(), - &mut len, - std::ptr::null_mut(), - 0, - ) - } < 0 - { - return Err(io::Error::last_os_error()); - } - - let mut msgs_buf: Vec = vec![0; len as usize]; - - if unsafe { - sysctl( - &mut mib as *mut _ as *mut _, - 6, - msgs_buf.as_mut_ptr() as _, - &mut len, - std::ptr::null_mut(), - 0, - ) - } < 0 - { - return Err(io::Error::last_os_error()); - } - - let mut routes = vec![]; - let mut offset = 0; - - while offset + std::mem::size_of::() <= len as usize { - let buf = &mut msgs_buf[offset..]; - - if buf.len() < std::mem::size_of::() { - break; - } - - let rt_hdr = unsafe { std::mem::transmute::<_, &rt_msghdr>(buf.as_ptr()) }; - if rt_hdr.rtm_version as u32 != RTM_VERSION { - eprintln!( - "Unexpected RTM_VERSION: {} in {:?}", - rt_hdr.rtm_version, rt_hdr - ); - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Unexpected RTM_VERSION: {}", rt_hdr.rtm_version), - )); - } - - if rt_hdr.rtm_errno != 0 { - return Err(code_to_error(rt_hdr.rtm_errno)); - } - - let msg_len = rt_hdr.rtm_msglen as usize; - offset += msg_len; - - if rt_hdr.rtm_flags as u32 & RTF_WASCLONED != 0 { - continue; - } - let rt_msg = &mut buf[std::mem::size_of::()..msg_len]; - - if let Some(route) = message_to_route(rt_hdr, rt_msg) { - routes.push(route); - } - } - - Ok(routes) -} - -fn message_to_arppair(msg_bytes: *mut u8) -> (IpAddr, MacAddr) { - const IP_START_INDEX: usize = 4; - const IP_END_INDEX: usize = 7; - const MAC_START_INDEX: usize = 24; - const MAC_END_INDEX: usize = 29; - let ip_bytes = unsafe { - std::slice::from_raw_parts( - msg_bytes.add(IP_START_INDEX), - IP_END_INDEX + 1 - IP_START_INDEX, - ) - }; - let mac_bytes = unsafe { - std::slice::from_raw_parts( - msg_bytes.add(MAC_START_INDEX), - MAC_END_INDEX + 1 - MAC_START_INDEX, - ) - }; - let ip_addr = IpAddr::V4(Ipv4Addr::new( - ip_bytes[0], - ip_bytes[1], - ip_bytes[2], - ip_bytes[3], - )); - let mac_addr = MacAddr::from_octets([ - mac_bytes[0], - mac_bytes[1], - mac_bytes[2], - mac_bytes[3], - mac_bytes[4], - mac_bytes[5], - ]); - (ip_addr, mac_addr) -} - -fn get_arp_table() -> io::Result> { - let mut arp_map: HashMap = HashMap::new(); - let mut mib: [u32; 6] = [0; 6]; - let mut len = 0; - mib[0] = CTL_NET; - mib[1] = PF_ROUTE; - mib[2] = 0; - mib[3] = AF_INET; - mib[4] = NET_RT_FLAGS; - mib[5] = RTF_LLINFO; - if unsafe { - sysctl( - &mut mib as *mut _ as *mut _, - 6, - std::ptr::null_mut(), - &mut len, - std::ptr::null_mut(), - 0, - ) - } < 0 - { - return Err(io::Error::last_os_error()); - } - - let mut msgs_buf: Vec = vec![0; len as usize]; - - if unsafe { - sysctl( - &mut mib as *mut _ as *mut _, - 6, - msgs_buf.as_mut_ptr() as _, - &mut len, - std::ptr::null_mut(), - 0, - ) - } < 0 - { - return Err(io::Error::last_os_error()); - } - let mut offset = 0; - loop { - let buf = &mut msgs_buf[offset..]; - - if buf.len() < std::mem::size_of::() { - break; - } - - let rt_hdr = unsafe { std::mem::transmute::<_, &rt_msghdr>(buf.as_ptr()) }; - if rt_hdr.rtm_version as u32 != RTM_VERSION { - eprintln!( - "Unexpected RTM_VERSION: {} in {:?}", - rt_hdr.rtm_version, rt_hdr - ); - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Unexpected RTM_VERSION: {}", rt_hdr.rtm_version), - )); - } - if rt_hdr.rtm_errno != 0 { - return Err(code_to_error(rt_hdr.rtm_errno)); - } - - let msg_len = rt_hdr.rtm_msglen as usize; - offset += msg_len; - - let rt_msg: &mut [u8] = &mut buf[std::mem::size_of::()..msg_len]; - let (ip, mac) = message_to_arppair(rt_msg.as_mut_ptr()); - arp_map.insert(ip, mac); - } - Ok(arp_map) -} - -fn get_default_routes() -> Vec { - let mut default_routes = Vec::new(); - match list_routes() { - Ok(routes) => { - for route in routes { - if (route.destination == Ipv4Addr::UNSPECIFIED - || route.destination == Ipv6Addr::UNSPECIFIED) - && route.prefix == 0 - && route.gateway != Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED)) - && route.gateway != Some(IpAddr::V6(Ipv6Addr::UNSPECIFIED)) - { - default_routes.push(route); - } - } - } - Err(_) => {} - } - default_routes -} - -pub fn get_gateway_map() -> HashMap { - let mut gateway_map: HashMap = HashMap::new(); - let routes = get_default_routes(); - let arp_map = get_arp_table().unwrap_or(HashMap::new()); - for route in routes { - if let Some(gw_ip) = route.gateway { - let gateway = gateway_map - .entry(route.ifindex.unwrap_or(0)) - .or_insert(NetworkDevice::new()); - if let Some(mac_addr) = arp_map.get(&gw_ip) { - gateway.mac_addr = mac_addr.clone(); - } - match gw_ip { - IpAddr::V4(ipv4) => { - gateway.ipv4.push(ipv4); - } - IpAddr::V6(ipv6) => { - gateway.ipv6.push(ipv6); - } - } - } - } - gateway_map -} diff --git a/src/gateway/windows.rs b/src/gateway/windows.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/interface/android.rs b/src/interface/android.rs deleted file mode 100644 index ea592c1..0000000 --- a/src/interface/android.rs +++ /dev/null @@ -1,320 +0,0 @@ -use once_cell::sync::OnceCell; - -pub fn get_libc_ifaddrs() -> Option<( - unsafe extern "C" fn(*mut *mut libc::ifaddrs) -> libc::c_int, - unsafe extern "C" fn(*mut libc::ifaddrs), -)> { - match (get_getifaddrs(), get_freeifaddrs()) { - (Some(a), Some(b)) => Some((a, b)), - _ => None, - } -} - -fn load_symbol(sym: &'static str) -> Option { - const LIB_NAME: &str = "libc.so"; - - match dlopen2::raw::Library::open(LIB_NAME) { - Ok(lib) => match unsafe { lib.symbol::(sym) } { - Ok(val) => Some(val), - Err(err) => { - eprintln!("failed to load symbol {} from {}: {:?}", sym, LIB_NAME, err); - None - } - }, - Err(err) => { - eprintln!("failed to load {}: {:?}", LIB_NAME, err); - None - } - } -} - -fn get_getifaddrs() -> Option libc::c_int> { - static INSTANCE: OnceCell< - Option libc::c_int>, - > = OnceCell::new(); - - *INSTANCE.get_or_init(|| load_symbol("getifaddrs")) -} - -fn get_freeifaddrs() -> Option { - static INSTANCE: OnceCell> = OnceCell::new(); - - *INSTANCE.get_or_init(|| load_symbol("freeifaddrs")) -} - -pub mod netlink { - //! Netlink based getifaddrs. - //! - //! Based on the logic found in https://git.musl-libc.org/cgit/musl/tree/src/network/getifaddrs.c - - use netlink_packet_core::{ - NetlinkHeader, NetlinkMessage, NetlinkPayload, NLM_F_DUMP, NLM_F_REQUEST, - }; - use netlink_packet_route::{ - address::{AddressAttribute, AddressMessage}, - link::{LinkAttribute, LinkMessage}, - RouteNetlinkMessage, - }; - use netlink_sys::{protocols::NETLINK_ROUTE, Socket}; - use std::{ - io, - net::{IpAddr, Ipv4Addr}, - }; - - use crate::{ - interface::{Interface, InterfaceType, Ipv4Net, Ipv6Net, OperState}, - mac::MacAddr, - }; - - use crate::stats::{get_stats, InterfaceStats}; - - pub fn unix_interfaces() -> Vec { - let mut ifaces = Vec::new(); - if let Ok(socket) = Socket::new(NETLINK_ROUTE) { - if let Err(err) = enumerate_netlink( - &socket, - RouteNetlinkMessage::GetLink(LinkMessage::default()), - &mut ifaces, - handle_new_link, - ) { - eprintln!("unable to list interfaces: {:?}", err); - }; - if let Err(err) = enumerate_netlink( - &socket, - RouteNetlinkMessage::GetAddress(AddressMessage::default()), - &mut ifaces, - handle_new_addr, - ) { - eprintln!("unable to list addresses: {:?}", err); - } - } - for iface in &mut ifaces { - let stats: Option = get_stats(None, &iface.name); - iface.stats = stats; - } - ifaces - } - - fn handle_new_link(ifaces: &mut Vec, msg: RouteNetlinkMessage) -> io::Result<()> { - if let RouteNetlinkMessage::NewLink(link_msg) = msg { - let mut interface: Interface = Interface { - index: link_msg.header.index, - name: String::new(), - friendly_name: None, - description: None, - if_type: InterfaceType::try_from(link_msg.header.link_layer_type as u32) - .unwrap_or(InterfaceType::Unknown), - mac_addr: None, - ipv4: Vec::new(), - ipv6: Vec::new(), - ipv6_scope_ids: Vec::new(), - flags: link_msg.header.flags.bits(), - oper_state: OperState::from_if_flags(link_msg.header.flags.bits()), - transmit_speed: None, - receive_speed: None, - stats: None, - #[cfg(feature = "gateway")] - gateway: None, - #[cfg(feature = "gateway")] - dns_servers: Vec::new(), - mtu: None, - #[cfg(feature = "gateway")] - default: false, - }; - - for nla in link_msg.attributes.into_iter() { - match nla { - LinkAttribute::IfName(name) => { - interface.name = name; - } - LinkAttribute::Address(addr) => { - match addr.len() { - 6 => { - interface.mac_addr = - Some(MacAddr::from_octets(addr.try_into().unwrap())); - } - 4 => { - let ip = Ipv4Addr::from(<[u8; 4]>::try_from(addr).unwrap()); - match Ipv4Net::with_netmask(ip, Ipv4Addr::UNSPECIFIED) { - Ok(ipv4) => interface.ipv4.push(ipv4), - Err(_) => {} - } - } - _ => { - // unclear what these would be - } - } - } - _ => {} - } - } - ifaces.push(interface); - } - - Ok(()) - } - - fn handle_new_addr(ifaces: &mut Vec, msg: RouteNetlinkMessage) -> io::Result<()> { - if let RouteNetlinkMessage::NewAddress(addr_msg) = msg { - if let Some(interface) = ifaces.iter_mut().find(|i| i.index == addr_msg.header.index) { - for nla in addr_msg.attributes.into_iter() { - if let AddressAttribute::Address(addr) = nla { - match addr { - IpAddr::V4(ip) => match Ipv4Net::new(ip, addr_msg.header.prefix_len) { - Ok(ipv4) => interface.ipv4.push(ipv4), - Err(_) => {} - }, - IpAddr::V6(ip) => match Ipv6Net::new(ip, addr_msg.header.prefix_len) { - Ok(ipv6) => { - interface.ipv6.push(ipv6); - - // Note: On Unix platforms the scope ID is equal to the interface index, or at least - // that's what the glibc devs seemed to think when implementing getifaddrs! - // https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/unix/sysv/linux/ifaddrs.c#L621 - interface.ipv6_scope_ids.push(interface.index); - } - Err(_) => {} - }, - } - } - } - } else { - eprintln!( - "found unknown interface with index: {}", - addr_msg.header.index - ); - } - } - Ok(()) - } - - struct NetlinkIter<'a> { - socket: &'a Socket, - /// Buffer for received data. - buf: Vec, - /// Size of the data available in `buf`. - size: usize, - /// Offset into the data currently in `buf`. - offset: usize, - /// Are we don iterating? - done: bool, - } - - impl<'a> NetlinkIter<'a> { - fn new(socket: &'a Socket, msg: RouteNetlinkMessage) -> io::Result { - let mut packet = - NetlinkMessage::new(NetlinkHeader::default(), NetlinkPayload::from(msg)); - packet.header.flags = NLM_F_DUMP | NLM_F_REQUEST; - packet.header.sequence_number = 1; - packet.finalize(); - - let mut buf = vec![0; packet.header.length as usize]; - if buf.len() != packet.buffer_len() { - return Err(io::Error::new( - io::ErrorKind::Other, - "Buffer length mismatch in netlink packet", - )); - } - packet.serialize(&mut buf[..]); - socket.send(&buf[..], 0)?; - - Ok(NetlinkIter { - socket, - offset: 0, - size: 0, - buf: vec![0u8; 4096], - done: false, - }) - } - } - - impl<'a> Iterator for NetlinkIter<'a> { - type Item = io::Result; - - fn next(&mut self) -> Option { - if self.done { - return None; - } - - while !self.done { - // Outer loop - if self.size == 0 { - match self.socket.recv(&mut &mut self.buf[..], 0) { - Ok(size) => { - self.size = size; - self.offset = 0; - } - Err(err) => { - self.done = true; - return Some(Err(err)); - } - } - } - - let bytes = &self.buf[self.offset..]; - match NetlinkMessage::::deserialize(bytes) { - Ok(packet) => { - self.offset += packet.header.length as usize; - if packet.header.length == 0 || self.offset == self.size { - // mark this message as fully read - self.size = 0; - } - match packet.payload { - NetlinkPayload::Done(_) => { - self.done = true; - return None; - } - NetlinkPayload::Error(err) => { - self.done = true; - return Some(Err(io::Error::new( - io::ErrorKind::Other, - err.to_string(), - ))); - } - NetlinkPayload::InnerMessage(msg) => return Some(Ok(msg)), - _ => { - continue; - } - } - } - Err(err) => { - self.done = true; - return Some(Err(io::Error::new(io::ErrorKind::Other, err.to_string()))); - } - } - } - - None - } - } - - fn enumerate_netlink( - socket: &Socket, - msg: RouteNetlinkMessage, - ifaces: &mut Vec, - cb: F, - ) -> io::Result<()> - where - F: Fn(&mut Vec, RouteNetlinkMessage) -> io::Result<()>, - { - let iter = NetlinkIter::new(socket, msg)?; - for msg in iter { - let msg = msg?; - cb(ifaces, msg)?; - } - - Ok(()) - } - - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn test_netlink_ifaddrs() { - let interfaces = unix_interfaces(); - dbg!(&interfaces); - assert!(!interfaces.is_empty()); - } - } -} diff --git a/src/interface/flags.rs b/src/interface/flags.rs new file mode 100644 index 0000000..cc63d5c --- /dev/null +++ b/src/interface/flags.rs @@ -0,0 +1,14 @@ +#[cfg(target_family = "unix")] +pub use crate::os::unix::flags::*; + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub use crate::os::linux::flags::*; + +#[cfg(target_vendor = "apple")] +pub use crate::os::darwin::flags::*; + +#[cfg(target_os = "windows")] +pub use crate::os::windows::flags::*; + +#[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))] +pub use crate::os::bsd::flags::*; diff --git a/src/interface/interface.rs b/src/interface/interface.rs new file mode 100644 index 0000000..ac95573 --- /dev/null +++ b/src/interface/interface.rs @@ -0,0 +1,248 @@ +use crate::interface::state::OperState; +use crate::ipnet::{Ipv4Net, Ipv6Net}; +use crate::net::ip::{is_global_ip, is_global_ipv4, is_global_ipv6}; +use crate::stats::counters::InterfaceStats; +use crate::{interface::types::InterfaceType, net::mac::MacAddr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[cfg(feature = "gateway")] +use crate::net::device::NetworkDevice; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Structure of Network Interface information +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Interface { + /// Index of network interface. This is an integer which uniquely identifies the interface + /// on this machine. + pub index: u32, + /// Machine-readable name of the network interface. On unix-like OSs, this is the interface + /// name, like 'eth0' or 'eno1'. On Windows, this is the interface's GUID as a string. + pub name: String, + /// Friendly name of network interface. On Windows, this is the network adapter configured + /// name, e.g. "Ethernet 5" or "Wi-Fi". On Mac, this is the interface display name, + /// such as "Ethernet" or "FireWire". If no friendly name is available, this is left as None. + pub friendly_name: Option, + /// Description of the network interface. On Windows, this is the network adapter model, such + /// as "Realtek USB GbE Family Controller #4" or "Software Loopback Interface 1". Currently + /// this is not available on platforms other than Windows. + pub description: Option, + /// Interface Type + pub if_type: InterfaceType, + /// MAC address of network interface + pub mac_addr: Option, + /// List of Ipv4Nets (IPv4 address + netmask) for the network interface + pub ipv4: Vec, + /// List of Ipv6Nets (IPv6 address + netmask) for the network interface + pub ipv6: Vec, + /// List of IPv6 Scope IDs for each of the corresponding elements in the ipv6 address vector. + /// The Scope ID is an integer which uniquely identifies this interface address on the system, + /// and must be provided when using link-local addressing to specify which interface + /// you wish to use. The scope ID can be the same as the interface index, but is not + /// required to be by the standard. + /// The scope ID can also be referred to as the zone index. + pub ipv6_scope_ids: Vec, + /// Flags for the network interface (OS Specific) + pub flags: u32, + /// Operational state at the time of interface discovery + pub oper_state: OperState, + /// Speed in bits per second of the transmit for the network interface, if known. + /// Currently only supported on Linux, Android, and Windows. + pub transmit_speed: Option, + /// Speed in bits per second of the receive for the network interface. + /// Currently only supported on Linux, Android, and Windows. + pub receive_speed: Option, + /// Statistics for this network interface, such as received and transmitted bytes. + /// + /// This field is populated at the time of interface discovery + /// (e.g., via [`crate::interface::get_interfaces()`] or [`crate::interface::get_default_interface()`]). + /// + /// The values represent a snapshot of total RX and TX bytes since system boot, + /// and include a timestamp (`SystemTime`) indicating when the snapshot was taken. + /// + /// If more up-to-date statistics are needed, use [`Interface::update_stats()`] to refresh this field. + pub stats: Option, + /// Default gateway for the network interface. This is the address of the router to which + /// IP packets are forwarded when they need to be sent to a device outside + /// of the local network. + #[cfg(feature = "gateway")] + pub gateway: Option, + /// DNS server addresses for the network interface + #[cfg(feature = "gateway")] + pub dns_servers: Vec, + /// Maximum Transmission Unit (MTU) for the network interface + pub mtu: Option, + /// Whether this is the default interface for accessing the Internet. + #[cfg(feature = "gateway")] + pub default: bool, +} + +impl Interface { + /// Construct a new default Interface instance + #[cfg(feature = "gateway")] + pub fn default() -> Result { + use crate::net::ip::get_local_ipaddr; + + let interfaces: Vec = super::interfaces(); + for iface in &interfaces { + if iface.default { + return Ok(iface.clone()); + } + } + let local_ip: IpAddr = match get_local_ipaddr() { + Some(local_ip) => local_ip, + None => return Err(String::from("Local IP address not found")), + }; + for iface in interfaces { + match local_ip { + IpAddr::V4(local_ipv4) => { + if iface.ipv4.iter().any(|x| x.addr() == local_ipv4) { + return Ok(iface); + } + } + IpAddr::V6(local_ipv6) => { + if iface.ipv6.iter().any(|x| x.addr() == local_ipv6) { + return Ok(iface); + } + } + } + } + Err(String::from("Default Interface not found")) + } + // Construct a dummy Interface instance + pub fn dummy() -> Interface { + Interface { + index: 0, + name: String::new(), + friendly_name: None, + description: None, + if_type: InterfaceType::Unknown, + mac_addr: None, + ipv4: Vec::new(), + ipv6: Vec::new(), + ipv6_scope_ids: Vec::new(), + flags: 0, + oper_state: OperState::Unknown, + transmit_speed: None, + receive_speed: None, + stats: None, + #[cfg(feature = "gateway")] + gateway: None, + #[cfg(feature = "gateway")] + dns_servers: Vec::new(), + mtu: None, + #[cfg(feature = "gateway")] + default: false, + } + } + /// Check if the network interface is up + pub fn is_up(&self) -> bool { + self.flags & (super::flags::IFF_UP as u32) != 0 + } + /// Check if the network interface is a Loopback interface + pub fn is_loopback(&self) -> bool { + self.flags & (super::flags::IFF_LOOPBACK as u32) != 0 + } + /// Check if the network interface is a Point-to-Point interface + pub fn is_point_to_point(&self) -> bool { + self.flags & (super::flags::IFF_POINTOPOINT as u32) != 0 + } + /// Check if the network interface is a Multicast interface + pub fn is_multicast(&self) -> bool { + self.flags & (super::flags::IFF_MULTICAST as u32) != 0 + } + /// Check if the network interface is a Broadcast interface + pub fn is_broadcast(&self) -> bool { + self.flags & (super::flags::IFF_BROADCAST as u32) != 0 + } + /// Check if the network interface is a TUN interface + pub fn is_tun(&self) -> bool { + self.is_up() && self.is_point_to_point() && !self.is_broadcast() && !self.is_loopback() + } + /// Check if the network interface is running and ready to send/receive packets + pub fn is_running(&self) -> bool { + super::flags::is_running(&self) + } + /// Check if the network interface is a physical interface + pub fn is_physical(&self) -> bool { + use crate::net::db::oui; + super::flags::is_physical_interface(&self) + && !oui::is_virtual_mac(&self.mac_addr.unwrap_or(MacAddr::zero())) + && !oui::is_known_loopback_mac(&self.mac_addr.unwrap_or(MacAddr::zero())) + } + /// Get the operational state of the network interface + pub fn oper_state(&self) -> OperState { + self.oper_state + } + /// Check if the operational state of the interface is up + pub fn is_oper_up(&self) -> bool { + self.oper_state == OperState::Up + } + /// Update the `oper_state` field by re-reading the current operstate from the system + pub fn update_oper_state(&mut self) { + self.oper_state = super::state::operstate(&self.name); + } + /// 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() + } + /// Updates the runtime traffic statistics for this interface (e.g., rx/tx byte counters). + pub fn update_stats(&mut self) -> std::io::Result<()> { + crate::stats::counters::update_interface_stats(self) + } +} diff --git a/src/interface/linux.rs b/src/interface/linux.rs deleted file mode 100644 index 51853ac..0000000 --- a/src/interface/linux.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::interface::{InterfaceType, OperState}; -use std::convert::TryFrom; -use std::fs::{read_link, read_to_string}; - -fn is_wifi_interface(interface_name: &str) -> bool { - let wireless_path = format!("/sys/class/net/{}/wireless", interface_name); - let phy80211_path = format!("/sys/class/net/{}/phy80211", interface_name); - std::path::Path::new(&wireless_path).exists() || std::path::Path::new(&phy80211_path).exists() -} - -pub fn is_virtual_interface(interface_name: &str) -> bool { - let device_path = format!("/sys/class/net/{}", interface_name); - match read_link(device_path) { - Ok(link_path) => { - // If the link path contains `virtual`, then it is a virtual interface. - link_path.to_string_lossy().contains("virtual") - } - Err(_) => false, - } -} - -pub fn get_interface_type(if_name: &str) -> InterfaceType { - let if_type_path: String = format!("/sys/class/net/{}/type", if_name); - let r = read_to_string(if_type_path); - match r { - Ok(content) => { - let if_type_string = content.trim().to_string(); - match if_type_string.parse::() { - Ok(if_type) => { - if if_type == crate::sys::if_arp::ARPHRD_ETHER { - // Since some Wi-Fi interfaces may also be reported as Ethernet, - // further check if the interface is actually Wi-Fi. - if is_wifi_interface(&if_name) { - return InterfaceType::Wireless80211; - } else { - return InterfaceType::Ethernet; - } - } else { - return InterfaceType::try_from(if_type).unwrap_or(InterfaceType::Unknown); - } - } - Err(_) => { - return InterfaceType::Unknown; - } - } - } - Err(_) => { - return InterfaceType::Unknown; - } - }; -} - -pub fn get_interface_speed(if_name: &str) -> Option { - let if_speed_path: String = format!("/sys/class/net/{}/speed", if_name); - let r = read_to_string(if_speed_path); - match r { - Ok(content) => { - let if_speed_string = content.trim().to_string(); - match if_speed_string.parse::() { - Ok(if_speed) => { - // Convert Mbps to bps - return Some(if_speed * 1000000); - } - Err(_) => { - return None; - } - } - } - Err(_) => { - return None; - } - }; -} - -pub fn operstate(if_name: &str) -> OperState { - let path = format!("/sys/class/net/{}/operstate", if_name); - match read_to_string(path) { - Ok(content) => content.trim().parse().unwrap_or(OperState::Unknown), - Err(_) => OperState::Unknown, - } -} diff --git a/src/interface/mod.rs b/src/interface/mod.rs index d5e0ce6..24be31f 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -1,294 +1,17 @@ -#[cfg(feature = "gateway")] -mod shared; -#[cfg(feature = "gateway")] -pub use self::shared::*; - -mod types; -pub use self::types::*; - -mod state; -pub use self::state::*; - -#[cfg(any( - target_os = "linux", - target_vendor = "apple", - target_os = "openbsd", - target_os = "freebsd", - target_os = "netbsd", - target_os = "android" -))] -mod unix; -#[cfg(any( - target_os = "linux", - target_vendor = "apple", - target_os = "openbsd", - target_os = "freebsd", - target_os = "netbsd", - target_os = "android" -))] -use self::unix::*; - -#[cfg(target_os = "windows")] -mod windows; -#[cfg(target_os = "windows")] -use self::windows::*; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -#[cfg(any(target_os = "linux", target_os = "android"))] -mod linux; +pub mod flags; +pub mod interface; +pub mod mtu; +pub mod state; +pub mod types; -#[cfg(target_os = "android")] -mod android; - -#[cfg(target_os = "macos")] -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::stats::InterfaceStats; -use crate::sys; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - -/// Structure of Network Interface information -#[derive(Clone, Eq, PartialEq, Hash, Debug)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Interface { - /// Index of network interface. This is an integer which uniquely identifies the interface - /// on this machine. - pub index: u32, - /// Machine-readable name of the network interface. On unix-like OSs, this is the interface - /// name, like 'eth0' or 'eno1'. On Windows, this is the interface's GUID as a string. - pub name: String, - /// Friendly name of network interface. On Windows, this is the network adapter configured - /// name, e.g. "Ethernet 5" or "Wi-Fi". On Mac, this is the interface display name, - /// such as "Ethernet" or "FireWire". If no friendly name is available, this is left as None. - pub friendly_name: Option, - /// Description of the network interface. On Windows, this is the network adapter model, such - /// as "Realtek USB GbE Family Controller #4" or "Software Loopback Interface 1". Currently - /// this is not available on platforms other than Windows. - pub description: Option, - /// Interface Type - pub if_type: InterfaceType, - /// MAC address of network interface - pub mac_addr: Option, - /// List of Ipv4Nets (IPv4 address + netmask) for the network interface - pub ipv4: Vec, - /// List of Ipv6Nets (IPv6 address + netmask) for the network interface - pub ipv6: Vec, - /// List of IPv6 Scope IDs for each of the corresponding elements in the ipv6 address vector. - /// The Scope ID is an integer which uniquely identifies this interface address on the system, - /// and must be provided when using link-local addressing to specify which interface - /// you wish to use. The scope ID can be the same as the interface index, but is not - /// required to be by the standard. - /// The scope ID can also be referred to as the zone index. - pub ipv6_scope_ids: Vec, - /// Flags for the network interface (OS Specific) - pub flags: u32, - /// Operational state at the time of interface discovery - pub oper_state: OperState, - /// Speed in bits per second of the transmit for the network interface, if known. - /// Currently only supported on Linux, Android, and Windows. - pub transmit_speed: Option, - /// Speed in bits per second of the receive for the network interface. - /// Currently only supported on Linux, Android, and Windows. - pub receive_speed: Option, - /// Statistics for this network interface, such as received and transmitted bytes. - /// - /// This field is populated at the time of interface discovery - /// (e.g., via [`get_interfaces()`] or [`get_default_interface()`]). - /// - /// The values represent a snapshot of total RX and TX bytes since system boot, - /// and include a timestamp (`SystemTime`) indicating when the snapshot was taken. - /// - /// If more up-to-date statistics are needed, use [`Interface::update_stats()`] to refresh this field. - pub stats: Option, - /// Default gateway for the network interface. This is the address of the router to which - /// IP packets are forwarded when they need to be sent to a device outside - /// of the local network. - #[cfg(feature = "gateway")] - pub gateway: Option, - /// DNS server addresses for the network interface - #[cfg(feature = "gateway")] - pub dns_servers: Vec, - /// Maximum Transmission Unit (MTU) for the network interface - pub mtu: Option, - /// Whether this is the default interface for accessing the Internet. - #[cfg(feature = "gateway")] - pub default: bool, -} - -impl Interface { - /// Construct a new default Interface instance - #[cfg(feature = "gateway")] - pub fn default() -> Result { - let interfaces: Vec = interfaces(); - for iface in &interfaces { - if iface.default { - return Ok(iface.clone()); - } - } - let local_ip: IpAddr = match get_local_ipaddr() { - Some(local_ip) => local_ip, - None => return Err(String::from("Local IP address not found")), - }; - for iface in interfaces { - match local_ip { - IpAddr::V4(local_ipv4) => { - if iface.ipv4.iter().any(|x| x.addr() == local_ipv4) { - return Ok(iface); - } - } - IpAddr::V6(local_ipv6) => { - if iface.ipv6.iter().any(|x| x.addr() == local_ipv6) { - return Ok(iface); - } - } - } - } - Err(String::from("Default Interface not found")) - } - // Construct a dummy Interface instance - pub fn dummy() -> Interface { - Interface { - index: 0, - name: String::new(), - friendly_name: None, - description: None, - if_type: InterfaceType::Unknown, - mac_addr: None, - ipv4: Vec::new(), - ipv6: Vec::new(), - ipv6_scope_ids: Vec::new(), - flags: 0, - oper_state: OperState::Unknown, - transmit_speed: None, - receive_speed: None, - stats: None, - #[cfg(feature = "gateway")] - gateway: None, - #[cfg(feature = "gateway")] - dns_servers: Vec::new(), - mtu: None, - #[cfg(feature = "gateway")] - default: false, - } - } - /// Check if the network interface is up - pub fn is_up(&self) -> bool { - self.flags & (sys::IFF_UP as u32) != 0 - } - /// Check if the network interface is a Loopback interface - pub fn is_loopback(&self) -> bool { - self.flags & (sys::IFF_LOOPBACK as u32) != 0 - } - /// Check if the network interface is a Point-to-Point interface - pub fn is_point_to_point(&self) -> bool { - self.flags & (sys::IFF_POINTOPOINT as u32) != 0 - } - /// Check if the network interface is a Multicast interface - pub fn is_multicast(&self) -> bool { - self.flags & (sys::IFF_MULTICAST as u32) != 0 - } - /// Check if the network interface is a Broadcast interface - pub fn is_broadcast(&self) -> bool { - self.flags & (sys::IFF_BROADCAST as u32) != 0 - } - /// Check if the network interface is a TUN interface - pub fn is_tun(&self) -> bool { - self.is_up() && self.is_point_to_point() && !self.is_broadcast() && !self.is_loopback() - } - /// Check if the network interface is running and ready to send/receive packets - pub fn is_running(&self) -> bool { - is_running(&self) - } - /// Check if the network interface is a physical interface - pub fn is_physical(&self) -> bool { - is_physical_interface(&self) - && !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())) - } - /// Get the operational state of the network interface - pub fn oper_state(&self) -> OperState { - self.oper_state - } - /// Check if the operational state of the interface is up - pub fn is_oper_up(&self) -> bool { - self.oper_state == OperState::Up - } - /// Update the `oper_state` field by re-reading the current operstate from the system - pub fn update_oper_state(&mut self) { - self.oper_state = operstate(&self.name); - } - /// 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() - } - /// Updates the runtime traffic statistics for this interface (e.g., rx/tx byte counters). - pub fn update_stats(&mut self) -> std::io::Result<()> { - crate::stats::update_interface_stats(self) - } -} +use crate::interface::interface::Interface; /// Get default Network Interface #[cfg(feature = "gateway")] pub fn get_default_interface() -> Result { + use crate::net::ip::get_local_ipaddr; + use std::net::IpAddr; + let interfaces: Vec = interfaces(); for iface in &interfaces { if iface.default { @@ -321,88 +44,30 @@ pub fn get_interfaces() -> Vec { interfaces() } -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_interfaces() { - let interfaces = get_interfaces(); - for interface in interfaces { - println!("{:#?}", interface); - } +pub(crate) fn interfaces() -> Vec { + #[cfg(target_os = "linux")] + { + crate::os::linux::interface::interfaces() } - #[test] - #[cfg(feature = "gateway")] - fn test_default_interface() { - println!("{:#?}", get_default_interface()); + #[cfg(target_os = "android")] + { + crate::os::android::interface::interfaces() } - - #[test] - fn sanity_check_loopback() { - let interfaces = get_interfaces(); - - assert!(interfaces.len() >= 2, "There should be at least 2 network interfaces on any machine, the loopback and one other one"); - - // Try and find the loopback interface - let loopback_interfaces: Vec<&Interface> = interfaces - .iter() - .filter(|iface| iface.if_type == InterfaceType::Loopback) - .collect(); - assert_eq!( - loopback_interfaces.len(), - 1, - "There should be exactly one loopback interface on the machine" - ); - let loopback = loopback_interfaces[0]; - - // Make sure that 127.0.0.1 is one of loopback's IPv4 addresses - let loopback_expected_ipv4: std::net::Ipv4Addr = "127.0.0.1".parse().unwrap(); - let matching_ipv4s: Vec<&Ipv4Net> = loopback - .ipv4 - .iter() - .filter(|&ipv4_net| ipv4_net.addr() == loopback_expected_ipv4) - .collect(); - assert_eq!( - matching_ipv4s.len(), - 1, - "The loopback interface should have IP 127.0.0.1" - ); - println!("Found IP {:?} on the loopback interface", matching_ipv4s[0]); - - // Make sure that ::1 is one of loopback's IPv6 addresses - let loopback_expected_ipv6: std::net::Ipv6Addr = "::1".parse().unwrap(); - let matching_ipv6s: Vec<&Ipv6Net> = loopback - .ipv6 - .iter() - .filter(|&ipv6_net| ipv6_net.addr() == loopback_expected_ipv6) - .collect(); - assert_eq!( - matching_ipv6s.len(), - 1, - "The loopback interface should have IP ::1" - ); - println!("Found IP {:?} on the loopback interface", matching_ipv6s[0]); - - // Make sure that the loopback interface has the same number of scope IDs as it does IPv6 addresses - assert_eq!(loopback.ipv6.len(), loopback.ipv6_scope_ids.len()); - - // Check flags - assert!( - loopback.is_running(), - "Loopback interface should be running!" - ); - assert!( - !loopback.is_physical(), - "Loopback interface should not be physical!" - ); - - // Make sure that, if the loopback interface has a MAC, it has a known loopback MAC - match loopback.mac_addr { - Some(mac) => assert!( - crate::db::oui::is_known_loopback_mac(&mac), - "Loopback interface MAC not a known loopback MAC" - ), - None => {} - } + #[cfg(target_os = "windows")] + { + crate::os::windows::interface::interfaces() + } + #[cfg(target_os = "macos")] + { + crate::os::macos::interface::interfaces() + } + //#[cfg(target_os = "ios")] + #[cfg(all(target_vendor = "apple", not(target_os = "macos")))] + { + crate::os::ios::interface::interfaces() + } + #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))] + { + crate::os::bsd::interface::interfaces() } } diff --git a/src/interface/mtu.rs b/src/interface/mtu.rs new file mode 100644 index 0000000..33ea824 --- /dev/null +++ b/src/interface/mtu.rs @@ -0,0 +1,10 @@ +#[cfg(any(target_os = "linux", target_os = "android"))] +pub(crate) fn get_mtu(_ifa: &libc::ifaddrs, name: &str) -> Option { + crate::os::linux::mtu::get_mtu(name) +} + +#[cfg(target_vendor = "apple")] +pub(crate) use crate::os::darwin::mtu::*; + +#[cfg(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd"))] +pub(crate) use crate::os::bsd::mtu::*; diff --git a/src/interface/shared.rs b/src/interface/shared.rs deleted file mode 100644 index 4b4dbcd..0000000 --- a/src/interface/shared.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::net::Ipv6Addr; -#[cfg(feature = "gateway")] -use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; - -fn try_ipv4() -> Option { - // Attempt to bind a UDP socket to an unspecified address and port. - let socket = match UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)) { - Ok(s) => s, - Err(_) => return None, - }; - // Attempt to connect the socket to a specific non-routable IP address. - // This does not send actual data but is used to determine the routing interface. - match socket.connect(SocketAddr::new( - IpAddr::V4(Ipv4Addr::new(10, 254, 254, 254)), - 1, - )) { - Ok(()) => (), - Err(_) => return None, - }; - // Retrieve and return the local IP address from the socket. - match socket.local_addr() { - Ok(addr) => return Some(addr.ip()), - Err(_) => return None, - }; -} - -fn try_ipv6() -> Option { - // same thing but for IPv6 - let socket = match UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)) { - Ok(s) => s, - Err(_) => return None, - }; - match socket.connect(SocketAddr::new( - IpAddr::V6(Ipv6Addr::new( - 0xfdff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, - )), - 1, - )) { - Ok(()) => (), - Err(_) => return None, - }; - match socket.local_addr() { - Ok(addr) => return Some(addr.ip()), - Err(_) => return None, - }; -} - -/// Retrieve the IP address of the default network interface. -/// -/// This function attempts to bind a UDP socket to an unspecified IP address (0.0.0.0 or ::) -/// and port (0), which allows the system to select an appropriate IP address and port. -/// After binding, it attempts to connect the socket to a designated non-routable IP address -/// (`10.254.254.254` or `fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff` on port 1). This is a -/// trick commonly used to prompt the OS to populate the socket's local address with the IP -/// address of the interface that would route to the specified address. -/// -/// The function returns the local IP address if these operations succeed. -/// If any operation fails (binding, connecting, or retrieving the address), -/// the function returns `None`, indicating the inability to determine the local IP. -/// -/// Returns: -/// - `Some(IpAddr)`: IP address of the default network interface if successful. -/// - `None`: If any error occurs during the operations. -#[cfg(feature = "gateway")] -pub fn get_local_ipaddr() -> Option { - // binding the IPv4 socket can actually succeed but a later step will fail in IPv6-only, - // we call `try_ipv6` in any case where `try_ipv4` fails - if let Some(addr) = try_ipv4() { - Some(addr) - } else { - try_ipv6() - } -} diff --git a/src/interface/state.rs b/src/interface/state.rs index 72ff780..366e9f8 100644 --- a/src/interface/state.rs +++ b/src/interface/state.rs @@ -1,4 +1,3 @@ -use crate::sys; use std::fmt; use std::str::FromStr; @@ -52,8 +51,8 @@ impl OperState { pub fn from_if_flags(if_flags: u32) -> Self { #[cfg(not(target_os = "windows"))] { - if if_flags & sys::IFF_UP as u32 != 0 { - if if_flags & sys::IFF_RUNNING as u32 != 0 { + if if_flags & super::flags::IFF_UP as u32 != 0 { + if if_flags & super::flags::IFF_RUNNING as u32 != 0 { OperState::Up } else { OperState::Dormant @@ -65,7 +64,7 @@ impl OperState { #[cfg(target_os = "windows")] { - if if_flags & sys::IFF_UP as u32 != 0 { + if if_flags & super::flags::IFF_UP as u32 != 0 { OperState::Up } else { OperState::Down @@ -96,3 +95,22 @@ impl FromStr for OperState { } } } + +pub fn operstate(if_name: &str) -> OperState { + #[cfg(any(target_os = "linux", target_os = "android"))] + { + crate::os::linux::state::operstate(if_name) + } + #[cfg(target_vendor = "apple")] + { + crate::os::darwin::state::operstate(if_name) + } + #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))] + { + crate::os::bsd::state::operstate(if_name) + } + #[cfg(target_os = "windows")] + { + crate::os::windows::state::operstate(if_name) + } +} diff --git a/src/interface/types.rs b/src/interface/types.rs index 3d3757f..5512def 100644 --- a/src/interface/types.rs +++ b/src/interface/types.rs @@ -114,21 +114,22 @@ impl InterfaceType { /// Returns OS-specific value of InterfaceType #[cfg(any(target_os = "linux", target_os = "android"))] pub fn value(&self) -> u32 { - use crate::sys; + use crate::os::linux::arp; + match *self { - InterfaceType::Ethernet => sys::if_arp::ARPHRD_ETHER, - InterfaceType::TokenRing => sys::if_arp::ARPHRD_IEEE802, - InterfaceType::Fddi => sys::if_arp::ARPHRD_FDDI, - InterfaceType::Ppp => sys::if_arp::ARPHRD_PPP, - InterfaceType::Loopback => sys::if_arp::ARPHRD_LOOPBACK, - InterfaceType::Ethernet3Megabit => sys::if_arp::ARPHRD_EETHER, - InterfaceType::Slip => sys::if_arp::ARPHRD_SLIP, - InterfaceType::Atm => sys::if_arp::ARPHRD_ATM, - InterfaceType::Wireless80211 => sys::if_arp::ARPHRD_IEEE80211, - InterfaceType::Tunnel => sys::if_arp::ARPHRD_TUNNEL, - InterfaceType::Isdn => sys::if_arp::ARPHRD_X25, - InterfaceType::HighPerformanceSerialBus => sys::if_arp::ARPHRD_IEEE1394, - InterfaceType::Can => sys::if_arp::ARPHRD_CAN, + InterfaceType::Ethernet => arp::ARPHRD_ETHER, + InterfaceType::TokenRing => arp::ARPHRD_IEEE802, + InterfaceType::Fddi => arp::ARPHRD_FDDI, + InterfaceType::Ppp => arp::ARPHRD_PPP, + InterfaceType::Loopback => arp::ARPHRD_LOOPBACK, + InterfaceType::Ethernet3Megabit => arp::ARPHRD_EETHER, + InterfaceType::Slip => arp::ARPHRD_SLIP, + InterfaceType::Atm => arp::ARPHRD_ATM, + InterfaceType::Wireless80211 => arp::ARPHRD_IEEE80211, + InterfaceType::Tunnel => arp::ARPHRD_TUNNEL, + InterfaceType::Isdn => arp::ARPHRD_X25, + InterfaceType::HighPerformanceSerialBus => arp::ARPHRD_IEEE1394, + InterfaceType::Can => arp::ARPHRD_CAN, InterfaceType::UnknownWithValue(v) => v, _ => u32::MAX, } diff --git a/src/interface/unix.rs b/src/interface/unix.rs deleted file mode 100644 index 36897df..0000000 --- a/src/interface/unix.rs +++ /dev/null @@ -1,645 +0,0 @@ -use super::Interface; -use super::MacAddr; -use super::OperState; - -#[cfg(feature = "gateway")] -use crate::gateway; - -use crate::interface::InterfaceType; -use crate::ipnet::{Ipv4Net, Ipv6Net}; -use crate::stats::{get_stats, InterfaceStats}; -use crate::sys; -use libc; -use std::ffi::{CStr, CString}; -use std::mem::{self, MaybeUninit}; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::os::raw::c_char; -use std::str::from_utf8_unchecked; - -#[cfg(feature = "gateway")] -use std::net::ToSocketAddrs; -#[cfg(feature = "gateway")] -pub fn get_system_dns_conf() -> Vec { - use std::fs::read_to_string; - const PATH_RESOLV_CONF: &str = "/etc/resolv.conf"; - let r = read_to_string(PATH_RESOLV_CONF); - match r { - Ok(content) => { - let conf_lines: Vec<&str> = content.trim().split('\n').collect(); - let mut dns_servers = Vec::new(); - for line in conf_lines { - let fields: Vec<&str> = line.split_whitespace().collect(); - if fields.len() >= 2 { - // field [0]: Configuration type (e.g., "nameserver", "domain", "search") - // field [1]: Corresponding value (e.g., IP address, domain name) - if fields[0] == "nameserver" { - let sock_addr = format!("{}:53", fields[1]); - if let Ok(mut addrs) = sock_addr.to_socket_addrs() { - if let Some(addr) = addrs.next() { - dns_servers.push(addr.ip()); - } - } else { - eprintln!("Invalid IP address format: {}", fields[1]); - } - } - } - } - dns_servers - } - Err(_) => Vec::new(), - } -} - -#[cfg(all(target_vendor = "apple", target_os = "macos"))] -pub fn interfaces() -> Vec { - use super::macos; - - let type_map = macos::get_if_type_map(); - let mut interfaces: Vec = unix_interfaces(); - - #[cfg(feature = "gateway")] - let local_ip_opt: Option = super::get_local_ipaddr(); - - #[cfg(feature = "gateway")] - let gateway_map = gateway::macos::get_gateway_map(); - - for iface in &mut interfaces { - if let Some(sc_interface) = type_map.get(&iface.name) { - iface.if_type = sc_interface.interface_type; - iface.friendly_name = sc_interface.friendly_name.clone(); - } - - #[cfg(feature = "gateway")] - { - if let Some(gateway) = gateway_map.get(&iface.index) { - iface.gateway = Some(gateway.clone()); - } - - if let Some(local_ip) = local_ip_opt { - iface.ipv4.iter().for_each(|ipv4| { - if IpAddr::V4(ipv4.addr()) == local_ip { - iface.dns_servers = get_system_dns_conf(); - iface.default = true; - } - }); - iface.ipv6.iter().for_each(|ipv6| { - if IpAddr::V6(ipv6.addr()) == local_ip { - iface.dns_servers = get_system_dns_conf(); - iface.default = true; - } - }); - } - } - } - - interfaces -} - -#[cfg(all(target_vendor = "apple", not(target_os = "macos")))] -pub fn interfaces() -> Vec { - let mut interfaces: Vec = unix_interfaces(); - - #[cfg(feature = "gateway")] - let local_ip_opt: Option = super::get_local_ipaddr(); - - #[cfg(feature = "gateway")] - let gateway_map = gateway::macos::get_gateway_map(); - - for iface in &mut interfaces { - #[cfg(feature = "gateway")] - { - if let Some(gateway) = gateway_map.get(&iface.index) { - iface.gateway = Some(gateway.clone()); - } - - if let Some(local_ip) = local_ip_opt { - iface.ipv4.iter().for_each(|ipv4| { - if IpAddr::V4(ipv4.addr()) == local_ip { - iface.dns_servers = get_system_dns_conf(); - iface.default = true; - } - }); - iface.ipv6.iter().for_each(|ipv6| { - if IpAddr::V6(ipv6.addr()) == local_ip { - iface.dns_servers = get_system_dns_conf(); - iface.default = true; - } - }); - } - } - } - - interfaces -} - -#[cfg(any(target_os = "linux", target_os = "android"))] -pub fn interfaces() -> Vec { - #[cfg(feature = "gateway")] - use crate::NetworkDevice; - #[cfg(feature = "gateway")] - use std::collections::HashMap; - - use super::linux; - - let mut interfaces: Vec = unix_interfaces(); - - #[cfg(feature = "gateway")] - let local_ip_opt: Option = super::get_local_ipaddr(); - - #[cfg(feature = "gateway")] - let gateway_map: HashMap = gateway::linux::get_gateway_map(); - - for iface in &mut interfaces { - iface.if_type = linux::get_interface_type(&iface.name); - let if_speed: Option = linux::get_interface_speed(&iface.name); - iface.transmit_speed = if_speed; - iface.receive_speed = if_speed; - - iface.oper_state = linux::operstate(&iface.name); - - #[cfg(feature = "gateway")] - if let Some(gateway) = gateway_map.get(&iface.name) { - iface.gateway = Some(gateway.clone()); - } - - #[cfg(feature = "gateway")] - if let Some(local_ip) = local_ip_opt { - match local_ip { - IpAddr::V4(local_ipv4) => { - if iface.ipv4.iter().any(|x| x.addr() == local_ipv4) { - iface.default = true; - iface.dns_servers = get_system_dns_conf(); - } - } - IpAddr::V6(local_ipv6) => { - if iface.ipv6.iter().any(|x| x.addr() == local_ipv6) { - iface.default = true; - iface.dns_servers = get_system_dns_conf(); - } - } - } - } - } - - interfaces -} - -#[cfg(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd"))] -pub fn interfaces() -> Vec { - let mut interfaces: Vec = unix_interfaces(); - - #[cfg(feature = "gateway")] - let local_ip_opt: Option = super::get_local_ipaddr(); - - #[cfg(feature = "gateway")] - { - let gateway_map = gateway::bsd::get_gateway_map(); - - for iface in &mut interfaces { - if let Some(gateway) = gateway_map.get(&iface.index) { - iface.gateway = Some(gateway.clone()); - } - - if let Some(local_ip) = local_ip_opt { - iface.ipv4.iter().for_each(|ipv4| { - if IpAddr::V4(ipv4.addr()) == local_ip { - iface.dns_servers = get_system_dns_conf(); - iface.default = true; - } - }); - iface.ipv6.iter().for_each(|ipv6| { - if IpAddr::V6(ipv6.addr()) == local_ip { - iface.dns_servers = get_system_dns_conf(); - iface.default = true; - } - }); - } - } - } - - interfaces -} - -// Convert a socket address struct into a Rust IP address or MAC address struct. -// If the socket address is an IPv6 address, also returns the scope ID. -#[cfg(any(target_os = "linux", target_os = "android"))] -pub(super) fn sockaddr_to_network_addr( - sa: *mut libc::sockaddr, -) -> (Option, Option, Option) { - use std::net::SocketAddr; - - unsafe { - if sa.is_null() { - (None, None, None) - } else if (*sa).sa_family as libc::c_int == libc::AF_PACKET { - let sll: *const libc::sockaddr_ll = mem::transmute(sa); - let mac = MacAddr( - (*sll).sll_addr[0], - (*sll).sll_addr[1], - (*sll).sll_addr[2], - (*sll).sll_addr[3], - (*sll).sll_addr[4], - (*sll).sll_addr[5], - ); - - (Some(mac), None, None) - } else { - let addr = - sys::sockaddr_to_addr(mem::transmute(sa), mem::size_of::()); - - match addr { - Ok(SocketAddr::V4(sa)) => (None, Some(IpAddr::V4(*sa.ip())), None), - Ok(SocketAddr::V6(sa)) => (None, Some(IpAddr::V6(*sa.ip())), Some(sa.scope_id())), - Err(_) => (None, None, None), - } - } - } -} - -#[cfg(any( - target_vendor = "apple", - target_os = "openbsd", - target_os = "freebsd", - target_os = "netbsd" -))] -fn sockaddr_to_network_addr( - sa: *mut libc::sockaddr, -) -> (Option, Option, Option) { - use std::net::SocketAddr; - - unsafe { - if sa.is_null() { - (None, None, None) - } else if (*sa).sa_family as libc::c_int == libc::AF_LINK { - let nlen: i8 = (*sa).sa_data[3] as i8; - let alen: i8 = (*sa).sa_data[4] as i8; - if alen > 0 && alen as u8 + nlen as u8 + 8 <= (*sa).sa_len { - let ptr = (*sa).sa_data.as_mut_ptr(); - let extended = - std::slice::from_raw_parts_mut(ptr, 6 + nlen as usize + alen as usize); - - let mac = MacAddr( - extended[6 + nlen as usize] as u8, - extended[6 + nlen as usize + 1] as u8, - extended[6 + nlen as usize + 2] as u8, - extended[6 + nlen as usize + 3] as u8, - extended[6 + nlen as usize + 4] as u8, - extended[6 + nlen as usize + 5] as u8, - ); - return (Some(mac), None, None); - } - (None, None, None) - } else { - let addr = - sys::sockaddr_to_addr(mem::transmute(sa), mem::size_of::()); - - match addr { - Ok(SocketAddr::V4(sa)) => (None, Some(IpAddr::V4(*sa.ip())), None), - Ok(SocketAddr::V6(sa)) => (None, Some(IpAddr::V6(*sa.ip())), Some(sa.scope_id())), - Err(_) => (None, None, None), - } - } - } -} - -#[cfg(any( - target_vendor = "apple", - target_os = "openbsd", - target_os = "freebsd", - target_os = "netbsd" -))] -fn get_interface_type(addr_ref: &libc::ifaddrs) -> InterfaceType { - if !addr_ref.ifa_data.is_null() { - let if_data = unsafe { &*(addr_ref.ifa_data as *const libc::if_data) }; - InterfaceType::try_from(if_data.ifi_type as u32).unwrap_or(InterfaceType::Unknown) - } else { - InterfaceType::Unknown - } -} - -#[cfg(any(target_os = "linux", target_os = "android"))] -fn get_interface_type(_addr_ref: &libc::ifaddrs) -> InterfaceType { - InterfaceType::Unknown -} - -pub fn is_running(interface: &Interface) -> bool { - interface.flags & (crate::sys::IFF_RUNNING as u32) != 0 -} - -#[cfg(any( - target_vendor = "apple", - target_os = "openbsd", - target_os = "freebsd", - target_os = "netbsd" -))] -pub fn is_physical_interface(interface: &Interface) -> bool { - interface.is_up() && interface.is_running() && !interface.is_tun() && !interface.is_loopback() -} - -#[cfg(any(target_os = "linux", target_os = "android"))] -pub fn is_physical_interface(interface: &Interface) -> bool { - use super::linux; - (interface.flags & (crate::sys::IFF_LOWER_UP as u32) != 0) - || (!interface.is_loopback() && !linux::is_virtual_interface(&interface.name)) -} - -#[cfg(any( - target_vendor = "apple", - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd" -))] -pub fn get_interface_flags(if_name: &str) -> std::io::Result { - use libc::{c_char, ioctl, socket, AF_INET, SOCK_DGRAM}; - use std::mem; - use std::os::unix::io::RawFd; - use std::ptr; - use sys::SIOCGIFFLAGS; - - #[cfg(target_os = "netbsd")] - #[repr(C)] - #[derive(Copy, Clone)] - struct IfReq { - ifr_name: [c_char; libc::IFNAMSIZ], - ifru_flags: [libc::c_short; 2], - } - - #[cfg(not(target_os = "netbsd"))] - use libc::ifreq as IfReq; - - let sock: RawFd = unsafe { socket(AF_INET, SOCK_DGRAM, 0) }; - if sock < 0 { - return Err(std::io::Error::last_os_error()); - } - - let mut ifr: IfReq = unsafe { mem::zeroed() }; - - let ifname_c = std::ffi::CString::new(if_name).map_err(|_| std::io::ErrorKind::InvalidInput)?; - let bytes = ifname_c.as_bytes_with_nul(); - - if bytes.len() > ifr.ifr_name.len() { - unsafe { libc::close(sock) }; - return Err(std::io::ErrorKind::InvalidInput.into()); - } - - unsafe { - ptr::copy_nonoverlapping( - bytes.as_ptr() as *const c_char, - ifr.ifr_name.as_mut_ptr(), - bytes.len(), - ); - } - - let res = unsafe { ioctl(sock, SIOCGIFFLAGS, &mut ifr) }; - unsafe { libc::close(sock) }; - - if res < 0 { - Err(std::io::Error::last_os_error()) - } else { - #[cfg(target_vendor = "apple")] - { - Ok(unsafe { ifr.ifr_ifru.ifru_flags as u32 }) - } - - #[cfg(target_os = "netbsd")] - { - Ok(unsafe { ifr.ifru_flags[0] as u32 }) - } - - #[cfg(all(not(target_vendor = "apple"), not(target_os = "netbsd")))] - { - Ok(unsafe { ifr.ifr_ifru.ifru_flags[0] as u32 }) - } - } -} - -#[cfg(any(target_os = "linux", target_os = "android"))] -pub use super::linux::operstate; - -#[cfg(not(any(target_os = "linux", target_os = "android")))] -pub fn operstate(if_name: &str) -> OperState { - match get_interface_flags(if_name) { - Ok(flags) => OperState::from_if_flags(flags), - Err(_) => OperState::Unknown, - } -} - -#[cfg(any( - target_vendor = "apple", - target_os = "openbsd", - target_os = "freebsd", - target_os = "netbsd" -))] -fn get_mtu(ifa: &libc::ifaddrs, _name: &str) -> Option { - if !ifa.ifa_data.is_null() { - let data = unsafe { &*(ifa.ifa_data as *mut libc::if_data) }; - Some(data.ifi_mtu as u32) - } else { - None - } -} - -#[cfg(any(target_os = "linux", target_os = "android"))] -fn get_mtu(_ifa: &libc::ifaddrs, name: &str) -> Option { - use libc::{c_char, c_int, close, ifreq, ioctl, socket, AF_INET, SIOCGIFMTU, SOCK_DGRAM}; - use std::os::unix::io::RawFd; - use std::ptr; - - // Create a socket for ioctl operations - let sock: RawFd = unsafe { socket(AF_INET, SOCK_DGRAM, 0) }; - if sock < 0 { - eprintln!( - "Failed to create socket: {:?}", - std::io::Error::last_os_error() - ); - return None; - } - - let mut ifr: ifreq = unsafe { mem::zeroed() }; - - // Set the interface name (must not exceed `IFNAMSIZ`) - let c_interface = CString::new(name).ok()?; - // Ensure null termination - let bytes = c_interface.to_bytes_with_nul(); - if bytes.len() > ifr.ifr_name.len() { - eprintln!("Interface name too long: {}", name); - unsafe { close(sock) }; - return None; - } - - unsafe { - ptr::copy_nonoverlapping( - bytes.as_ptr() as *const c_char, - ifr.ifr_name.as_mut_ptr(), - bytes.len(), - ); - } - - // Retrieve the MTU using ioctl - let ret: c_int = unsafe { ioctl(sock, SIOCGIFMTU as _, &mut ifr) }; - if ret < 0 { - eprintln!( - "ioctl(SIOCGIFMTU) failed for {}: {:?}", - name, - std::io::Error::last_os_error() - ); - unsafe { close(sock) }; - return None; - } - - let mtu = unsafe { ifr.ifr_ifru.ifru_mtu } as u32; - - // Close the socket - unsafe { close(sock) }; - - Some(mtu) -} - -#[cfg(target_os = "android")] -pub fn unix_interfaces() -> Vec { - use super::android; - - if let Some((getifaddrs, freeifaddrs)) = android::get_libc_ifaddrs() { - return unix_interfaces_inner(getifaddrs, freeifaddrs); - } - - android::netlink::unix_interfaces() -} - -#[cfg(not(target_os = "android"))] -pub fn unix_interfaces() -> Vec { - unix_interfaces_inner(libc::getifaddrs, libc::freeifaddrs) -} - -fn unix_interfaces_inner( - getifaddrs: unsafe extern "C" fn(*mut *mut libc::ifaddrs) -> libc::c_int, - freeifaddrs: unsafe extern "C" fn(*mut libc::ifaddrs), -) -> Vec { - let mut ifaces: Vec = vec![]; - let mut addrs: MaybeUninit<*mut libc::ifaddrs> = MaybeUninit::uninit(); - if unsafe { getifaddrs(addrs.as_mut_ptr()) } != 0 { - return ifaces; - } - let addrs = unsafe { addrs.assume_init() }; - let mut addr = addrs; - while !addr.is_null() { - let addr_ref: &libc::ifaddrs = unsafe { &*addr }; - let if_type = get_interface_type(addr_ref); - let c_str = addr_ref.ifa_name as *const c_char; - let bytes = unsafe { CStr::from_ptr(c_str).to_bytes() }; - let name = unsafe { from_utf8_unchecked(bytes).to_owned() }; - let (mac, ip, ipv6_scope_id) = - sockaddr_to_network_addr(addr_ref.ifa_addr as *mut libc::sockaddr); - let (_, netmask, _) = sockaddr_to_network_addr(addr_ref.ifa_netmask as *mut libc::sockaddr); - let stats: Option = get_stats(Some(addr_ref), &name); - let mut ini_ipv4: Option = None; - let mut ini_ipv6: Option = None; - if let Some(ip) = ip { - match ip { - IpAddr::V4(ipv4) => { - let netmask: Ipv4Addr = match netmask { - Some(netmask) => match netmask { - IpAddr::V4(netmask) => netmask, - IpAddr::V6(_) => Ipv4Addr::UNSPECIFIED, - }, - None => Ipv4Addr::UNSPECIFIED, - }; - match Ipv4Net::with_netmask(ipv4, netmask) { - Ok(ipv4_net) => ini_ipv4 = Some(ipv4_net), - Err(_) => {} - } - } - IpAddr::V6(ipv6) => { - let netmask: Ipv6Addr = match netmask { - Some(netmask) => match netmask { - IpAddr::V4(_) => Ipv6Addr::UNSPECIFIED, - IpAddr::V6(netmask) => netmask, - }, - None => Ipv6Addr::UNSPECIFIED, - }; - match Ipv6Net::with_netmask(ipv6, netmask) { - Ok(ipv6_net) => { - ini_ipv6 = Some(ipv6_net); - if ipv6_scope_id.is_none() { - panic!("IPv6 address without scope ID!") - } - } - Err(_) => {} - }; - } - } - } - - // Check if there is already an interface with this name (since getifaddrs returns one - // entry per address, so if the interface has multiple addresses, it returns multiple entries). - // If so, add the IP addresses from the current entry into the existing interface. Otherwise, add a new interface. - let mut found: bool = false; - for iface in &mut ifaces { - if name == iface.name { - if let Some(mac) = mac.clone() { - iface.mac_addr = Some(mac); - } - - if iface.stats.is_none() { - iface.stats = stats.clone(); - } - - if ini_ipv4.is_some() { - iface.ipv4.push(ini_ipv4.unwrap()); - } - - if ini_ipv6.is_some() { - iface.ipv6.push(ini_ipv6.unwrap()); - iface.ipv6_scope_ids.push(ipv6_scope_id.unwrap()); - } - found = true; - } - } - if !found { - let interface: Interface = Interface { - index: 0, // We will set these below - name: name.clone(), - friendly_name: None, - description: None, - if_type: if_type, - mac_addr: mac.clone(), - ipv4: match ini_ipv4 { - Some(ipv4_addr) => vec![ipv4_addr], - None => vec![], - }, - ipv6: match ini_ipv6 { - Some(ipv6_addr) => vec![ipv6_addr], - None => vec![], - }, - ipv6_scope_ids: match ini_ipv6 { - Some(_) => vec![ipv6_scope_id.unwrap()], - None => vec![], - }, - flags: addr_ref.ifa_flags, - oper_state: OperState::from_if_flags(addr_ref.ifa_flags), - transmit_speed: None, - receive_speed: None, - stats: stats, - #[cfg(feature = "gateway")] - gateway: None, - #[cfg(feature = "gateway")] - dns_servers: Vec::new(), - mtu: get_mtu(addr_ref, &name), - #[cfg(feature = "gateway")] - default: false, - }; - ifaces.push(interface); - } - addr = addr_ref.ifa_next; - } - unsafe { - freeifaddrs(addrs); - } - for iface in &mut ifaces { - let name = CString::new(iface.name.as_bytes()).unwrap(); - unsafe { - iface.index = libc::if_nametoindex(name.as_ptr()); - } - } - ifaces -} diff --git a/src/lib.rs b/src/lib.rs index 78436a5..f1359f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,19 @@ -mod db; -pub mod device; -#[cfg(feature = "gateway")] -pub mod gateway; pub mod interface; -mod ip; -pub mod mac; +pub mod net; +mod os; +pub mod prelude; +#[cfg(feature = "gateway")] +pub mod route; pub mod stats; -mod sys; -pub use device::NetworkDevice; -#[cfg(feature = "gateway")] -pub use gateway::get_default_gateway; +pub use ipnet; + +pub use interface::get_interfaces; +pub use interface::interface::Interface; +pub use net::device::NetworkDevice; +pub use net::mac::MacAddr; + #[cfg(feature = "gateway")] pub use interface::get_default_interface; -pub use interface::get_interfaces; -pub use interface::Interface; -pub use ipnet; -pub use mac::MacAddr; +#[cfg(feature = "gateway")] +pub use route::get_default_gateway; diff --git a/src/db/mod.rs b/src/net/db/mod.rs similarity index 100% rename from src/db/mod.rs rename to src/net/db/mod.rs diff --git a/src/db/oui.rs b/src/net/db/oui.rs similarity index 97% rename from src/db/oui.rs rename to src/net/db/oui.rs index c84a4e2..6169d0f 100644 --- a/src/db/oui.rs +++ b/src/net/db/oui.rs @@ -1,4 +1,4 @@ -use crate::mac::MacAddr; +use crate::net::mac::MacAddr; /// List of known Virtual Machine MAC prefixes pub const KNOWN_VM_MAC_PREFIXES: &[&str] = &[ diff --git a/src/device.rs b/src/net/device.rs similarity index 96% rename from src/device.rs rename to src/net/device.rs index fc77ec6..a6c8a39 100644 --- a/src/device.rs +++ b/src/net/device.rs @@ -1,7 +1,7 @@ #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::mac::MacAddr; +use super::mac::MacAddr; use std::net::{Ipv4Addr, Ipv6Addr}; /// Structure of NetworkDevice information diff --git a/src/ip.rs b/src/net/ip.rs similarity index 67% rename from src/ip.rs rename to src/net/ip.rs index af8ada1..77a4308 100644 --- a/src/ip.rs +++ b/src/net/ip.rs @@ -1,5 +1,80 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +#[cfg(feature = "gateway")] +use std::net::{SocketAddr, UdpSocket}; + +#[cfg(feature = "gateway")] +fn try_ipv4() -> Option { + // Attempt to bind a UDP socket to an unspecified address and port. + let socket = match UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)) { + Ok(s) => s, + Err(_) => return None, + }; + // Attempt to connect the socket to a specific non-routable IP address. + // This does not send actual data but is used to determine the routing interface. + match socket.connect(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(10, 254, 254, 254)), + 1, + )) { + Ok(()) => (), + Err(_) => return None, + }; + // Retrieve and return the local IP address from the socket. + match socket.local_addr() { + Ok(addr) => return Some(addr.ip()), + Err(_) => return None, + }; +} + +#[cfg(feature = "gateway")] +fn try_ipv6() -> Option { + // same thing but for IPv6 + let socket = match UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)) { + Ok(s) => s, + Err(_) => return None, + }; + match socket.connect(SocketAddr::new( + IpAddr::V6(Ipv6Addr::new( + 0xfdff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, + )), + 1, + )) { + Ok(()) => (), + Err(_) => return None, + }; + match socket.local_addr() { + Ok(addr) => return Some(addr.ip()), + Err(_) => return None, + }; +} + +/// Retrieve the IP address of the default network interface. +/// +/// This function attempts to bind a UDP socket to an unspecified IP address (0.0.0.0 or ::) +/// and port (0), which allows the system to select an appropriate IP address and port. +/// After binding, it attempts to connect the socket to a designated non-routable IP address +/// (`10.254.254.254` or `fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff` on port 1). This is a +/// trick commonly used to prompt the OS to populate the socket's local address with the IP +/// address of the interface that would route to the specified address. +/// +/// The function returns the local IP address if these operations succeed. +/// If any operation fails (binding, connecting, or retrieving the address), +/// the function returns `None`, indicating the inability to determine the local IP. +/// +/// Returns: +/// - `Some(IpAddr)`: IP address of the default network interface if successful. +/// - `None`: If any error occurs during the operations. +#[cfg(feature = "gateway")] +pub fn get_local_ipaddr() -> Option { + // binding the IPv4 socket can actually succeed but a later step will fail in IPv6-only, + // we call `try_ipv6` in any case where `try_ipv4` fails + if let Some(addr) = try_ipv4() { + Some(addr) + } else { + try_ipv6() + } +} + /// Returns [`true`] if the address appears to be globally routable. pub(crate) fn is_global_ip(ip_addr: &IpAddr) -> bool { match ip_addr { @@ -54,7 +129,7 @@ pub(crate) fn is_global_ipv6(ipv6_addr: &Ipv6Addr) -> bool { // 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, + // 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) diff --git a/src/mac.rs b/src/net/mac.rs similarity index 98% rename from src/mac.rs rename to src/net/mac.rs index 87eea7a..5b4e4da 100644 --- a/src/mac.rs +++ b/src/net/mac.rs @@ -3,7 +3,7 @@ use std::error; use std::fmt; #[cfg(feature = "serde")] -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; /// Structure of MAC address #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Debug)] diff --git a/src/net/mod.rs b/src/net/mod.rs new file mode 100644 index 0000000..942af6f --- /dev/null +++ b/src/net/mod.rs @@ -0,0 +1,4 @@ +pub mod db; +pub mod device; +pub mod ip; +pub mod mac; diff --git a/src/os/android/interface.rs b/src/os/android/interface.rs new file mode 100644 index 0000000..1f90bdb --- /dev/null +++ b/src/os/android/interface.rs @@ -0,0 +1,180 @@ +use super::netlink; +use crate::interface::interface::Interface; +use crate::interface::state::OperState; +use crate::ipnet::{Ipv4Net, Ipv6Net}; +use crate::net::mac::MacAddr; +use crate::os::linux::mtu; +use crate::os::linux::sysfs; +use crate::os::unix::interface::unix_interfaces; +use std::net::{Ipv4Addr, Ipv6Addr}; + +#[cfg(feature = "gateway")] +use crate::net::device::NetworkDevice; +#[cfg(feature = "gateway")] +use crate::net::ip::get_local_ipaddr; +#[cfg(feature = "gateway")] +use crate::os::linux::procfs; +#[cfg(feature = "gateway")] +use crate::os::unix::dns::get_system_dns_conf; +#[cfg(feature = "gateway")] +use std::collections::HashMap; +#[cfg(feature = "gateway")] +use std::net::IpAddr; + +fn push_ipv4(v: &mut Vec, add: (Ipv4Addr, u8)) { + if v.iter() + .any(|n| n.addr() == add.0 && n.prefix_len() == add.1) + { + return; + } + if let Ok(net) = Ipv4Net::new(add.0, add.1) { + v.push(net); + } +} + +fn push_ipv6(v: &mut Vec, add: (Ipv6Addr, u8)) { + if v.iter() + .any(|n| n.addr() == add.0 && n.prefix_len() == add.1) + { + return; + } + if let Ok(net) = Ipv6Net::new(add.0, add.1) { + v.push(net); + } +} + +#[inline] +fn calc_v6_scope_id(addr: &Ipv6Addr, ifindex: u32) -> u32 { + let seg0 = addr.segments()[0]; + if (seg0 & 0xffc0) == 0xfe80 { + ifindex + } else { + 0 + } +} + +pub fn interfaces() -> Vec { + let mut ifaces = Vec::new(); + + #[cfg(feature = "gateway")] + let local_ip_opt: Option = get_local_ipaddr(); + + // Fill ifaces via netlink first + // If netlink fails, fallback to unix_interfaces + match netlink::collect_interfaces() { + Ok(rows) => { + for r in rows { + let name = r.name.clone(); + let mut iface = Interface { + index: r.index, + name: name.clone(), + friendly_name: None, + description: None, + if_type: sysfs::get_interface_type(&name), + mac_addr: r.mac.map(MacAddr::from_octets), + ipv4: Vec::new(), + ipv6: Vec::new(), + ipv6_scope_ids: Vec::new(), + flags: r.flags, + oper_state: OperState::from_if_flags(r.flags), + transmit_speed: None, + receive_speed: None, + stats: None, + #[cfg(feature = "gateway")] + gateway: None, + #[cfg(feature = "gateway")] + dns_servers: Vec::new(), + mtu: r.mtu, + #[cfg(feature = "gateway")] + default: false, + }; + + for (a, p) in r.ipv4 { + push_ipv4(&mut iface.ipv4, (a, p)); + } + for (a, p) in r.ipv6 { + push_ipv6(&mut iface.ipv6, (a, p)); + iface.ipv6_scope_ids.push(calc_v6_scope_id(&a, iface.index)); + } + + ifaces.push(iface); + } + } + Err(_) => { + // Fallback: unix ifaddrs + ifaces = unix_interfaces(); + } + } + + // Fill gateway info if feature enabled + #[cfg(feature = "gateway")] + match netlink::collect_routes() { + Ok(gmap) => { + let by_index: HashMap = + gmap.iter().map(|(k, v)| (*k, v)).collect(); + for iface in &mut ifaces { + if iface.index == 0 { + continue; + } + if let Some(row) = by_index.get(&iface.index) { + let dev = NetworkDevice { + mac_addr: row + .mac + .map(|m| MacAddr::from_octets(m)) + .unwrap_or(MacAddr::zero()), + ipv4: row.gw_v4.clone(), + ipv6: row.gw_v6.clone(), + }; + iface.gateway = Some(dev); + } + } + } + Err(_) => { + // Fallback: procfs + let gateway_map: HashMap = procfs::get_gateway_map(); + for iface in &mut ifaces { + if let Some(gateway) = gateway_map.get(&iface.name) { + iface.gateway = Some(gateway.clone()); + } + } + } + } + + // Fill other info + for iface in &mut ifaces { + iface.if_type = sysfs::get_interface_type(&iface.name); + let if_speed = sysfs::get_interface_speed(&iface.name); + iface.transmit_speed = if_speed; + iface.receive_speed = if_speed; + iface.oper_state = sysfs::operstate(&iface.name); + + if iface.stats.is_none() { + iface.stats = crate::stats::counters::get_stats_from_name(&iface.name); + } + + if iface.mtu.is_none() { + iface.mtu = mtu::get_mtu(&iface.name); + } + + #[cfg(feature = "gateway")] + { + if let Some(local_ip) = local_ip_opt { + match local_ip { + IpAddr::V4(local_ipv4) => { + if iface.ipv4.iter().any(|x| x.addr() == local_ipv4) { + iface.default = true; + iface.dns_servers = get_system_dns_conf(); + } + } + IpAddr::V6(local_ipv6) => { + if iface.ipv6.iter().any(|x| x.addr() == local_ipv6) { + iface.default = true; + iface.dns_servers = get_system_dns_conf(); + } + } + } + } + } + } + ifaces +} diff --git a/src/os/android/mod.rs b/src/os/android/mod.rs new file mode 100644 index 0000000..af1c94c --- /dev/null +++ b/src/os/android/mod.rs @@ -0,0 +1,46 @@ +pub mod interface; +pub mod netlink; + +use once_cell::sync::OnceCell; + +pub fn get_libc_ifaddrs() -> Option<( + unsafe extern "C" fn(*mut *mut libc::ifaddrs) -> libc::c_int, + unsafe extern "C" fn(*mut libc::ifaddrs), +)> { + match (get_getifaddrs(), get_freeifaddrs()) { + (Some(a), Some(b)) => Some((a, b)), + _ => None, + } +} + +fn load_symbol(sym: &'static str) -> Option { + const LIB_NAME: &str = "libc.so"; + + match dlopen2::raw::Library::open(LIB_NAME) { + Ok(lib) => match unsafe { lib.symbol::(sym) } { + Ok(val) => Some(val), + Err(err) => { + eprintln!("failed to load symbol {} from {}: {:?}", sym, LIB_NAME, err); + None + } + }, + Err(err) => { + eprintln!("failed to load {}: {:?}", LIB_NAME, err); + None + } + } +} + +fn get_getifaddrs() -> Option libc::c_int> { + static INSTANCE: OnceCell< + Option libc::c_int>, + > = OnceCell::new(); + + *INSTANCE.get_or_init(|| load_symbol("getifaddrs")) +} + +fn get_freeifaddrs() -> Option { + static INSTANCE: OnceCell> = OnceCell::new(); + + *INSTANCE.get_or_init(|| load_symbol("freeifaddrs")) +} diff --git a/src/os/android/netlink.rs b/src/os/android/netlink.rs new file mode 100644 index 0000000..6cb5fd4 --- /dev/null +++ b/src/os/android/netlink.rs @@ -0,0 +1,449 @@ +use netlink_packet_core::{NLM_F_DUMP, NLM_F_REQUEST, NetlinkMessage, NetlinkPayload}; +use netlink_packet_route::{ + RouteNetlinkMessage, + address::{AddressAttribute, AddressMessage}, + link::{LinkAttribute, LinkMessage}, +}; +use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_ROUTE}; +use std::io::ErrorKind; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::{ + collections::HashMap, + io, thread, + time::{Duration, Instant}, +}; + +#[cfg(feature = "gateway")] +use netlink_packet_route::AddressFamily; +#[cfg(feature = "gateway")] +use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourMessage}; +#[cfg(feature = "gateway")] +use netlink_packet_route::route::{RouteAddress, RouteAttribute, RouteMessage}; + +const SEQ_BASE: u32 = 0x6E_64_65_76; // "ndev" +const RECV_BUFSZ: usize = 1 << 20; // 1MB +const RECV_TIMEOUT: Duration = Duration::from_secs(2); +const NLMSG_ALIGNTO: usize = 4; +const MIN_NLMSG_HEADER_LEN: usize = 16; + +#[inline] +fn nlmsg_align(n: usize) -> usize { + (n + NLMSG_ALIGNTO - 1) & !(NLMSG_ALIGNTO - 1) +} + +fn open_route_socket() -> io::Result { + let sock = Socket::new(NETLINK_ROUTE) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("netlink open: {e}")))?; + // On Android 11+, bind is denied by SELinux + //sock.bind_auto().map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bind_auto: {e}")))?; + sock.set_non_blocking(true).ok(); + Ok(sock) +} + +fn send_dump(sock: &mut Socket, msg: RouteNetlinkMessage, seq: u32) -> io::Result<()> { + let mut nl = NetlinkMessage::from(msg); + nl.header.flags = NLM_F_REQUEST | NLM_F_DUMP; + nl.header.sequence_number = seq; + nl.header.port_number = 0; + + // Finalize to set length + nl.finalize(); + + let blen = nl.buffer_len(); + if blen < MIN_NLMSG_HEADER_LEN { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("netlink message too short: buffer_len={}", blen), + )); + } + + let mut buf = vec![0; blen]; + nl.serialize(&mut buf); + + let kernel = SocketAddr::new(0, 0); + sock.send_to(&buf, &kernel, 0) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("netlink send: {e}")))?; + Ok(()) +} + +fn recv_multi( + sock: &mut Socket, + expect_seq: u32, +) -> io::Result>> { + let mut out = Vec::new(); + let mut buf = vec![0u8; RECV_BUFSZ]; + let kernel = SocketAddr::new(0, 0); + let deadline = Instant::now() + RECV_TIMEOUT; + + loop { + match sock.recv_from(&mut &mut buf[..], 0) { + Ok((size, from)) => { + let _ = from == kernel; + let mut offset = 0usize; + + while offset < size { + if size - offset < MIN_NLMSG_HEADER_LEN { + break; + } + + let bytes = &buf[offset..size]; + + let msg = + NetlinkMessage::::deserialize(bytes).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("deserialize: {e:?}"), + ) + })?; + + let consumed = msg.header.length as usize; + if consumed < MIN_NLMSG_HEADER_LEN || offset + consumed > size { + break; + } + + if msg.header.sequence_number != expect_seq { + offset += nlmsg_align(consumed); + continue; + } + + match &msg.payload { + NetlinkPayload::Done(_) => { + return Ok(out); + } + NetlinkPayload::Error(e) => { + if let Some(code) = e.code { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("netlink error: code={}", code), + )); + } + // code==None: possibly ACK ... ignore + } + NetlinkPayload::Noop | NetlinkPayload::Overrun(_) => { /* skip */ } + _ => out.push(msg), + } + + // Align to 4-byte boundary + offset += nlmsg_align(consumed); + } + } + Err(e) if e.kind() == ErrorKind::WouldBlock => { + if Instant::now() >= deadline { + // timeout + return Ok(out); + } + thread::sleep(Duration::from_millis(5)); + } + Err(e) => return Err(e), + } + } +} + +pub fn dump_links() -> io::Result> { + let mut sock = open_route_socket()?; + let seq = SEQ_BASE ^ 0x01; + send_dump( + &mut sock, + RouteNetlinkMessage::GetLink(LinkMessage::default()), + seq, + )?; + let msgs = recv_multi(&mut sock, seq)?; + let mut out = Vec::new(); + for m in msgs { + if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewLink(link)) = m.payload { + out.push(link); + } + } + Ok(out) +} + +pub fn dump_addrs() -> io::Result> { + let mut sock = open_route_socket()?; + let seq = SEQ_BASE ^ 0x02; + send_dump( + &mut sock, + RouteNetlinkMessage::GetAddress(AddressMessage::default()), + seq, + )?; + let msgs = recv_multi(&mut sock, seq)?; + let mut out = Vec::new(); + for m in msgs { + if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewAddress(addr)) = m.payload { + out.push(addr); + } + } + Ok(out) +} + +#[cfg(feature = "gateway")] +pub fn dump_routes() -> io::Result> { + let mut sock = open_route_socket()?; + let seq = SEQ_BASE ^ 0x03; + send_dump( + &mut sock, + RouteNetlinkMessage::GetRoute(RouteMessage::default()), + seq, + )?; + let msgs = recv_multi(&mut sock, seq)?; + let mut out = Vec::new(); + for m in msgs { + if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewRoute(rt)) = m.payload { + out.push(rt); + } + } + Ok(out) +} + +#[cfg(feature = "gateway")] +pub fn dump_neigh() -> io::Result> { + let mut sock = open_route_socket()?; + let seq = SEQ_BASE ^ 0x04; + send_dump( + &mut sock, + RouteNetlinkMessage::GetNeighbour(NeighbourMessage::default()), + seq, + )?; + let msgs = recv_multi(&mut sock, seq)?; + let mut out = Vec::new(); + for m in msgs { + if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewNeighbour(n)) = m.payload { + out.push(n); + } + } + Ok(out) +} + +fn mac_from_link(link: &LinkMessage) -> Option<[u8; 6]> { + for nla in &link.attributes { + if let LinkAttribute::Address(bytes) = nla { + if bytes.len() == 6 { + return Some([bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]]); + } + } + } + None +} + +fn name_from_link(link: &LinkMessage) -> Option { + for nla in &link.attributes { + if let LinkAttribute::IfName(n) = nla { + return Some(n.clone()); + } + } + None +} + +fn ip_from_addr(addr: &AddressMessage) -> Option<(IpAddr, u8)> { + let pfx = addr.header.prefix_len; + for nla in &addr.attributes { + match nla { + AddressAttribute::Local(ip) | AddressAttribute::Address(ip) => { + return Some((*ip, pfx)); + } + _ => {} + } + } + None +} + +#[cfg(feature = "gateway")] +fn route_addr_to_ip(a: &RouteAddress) -> Option { + match a { + RouteAddress::Inet(v4) => Some(IpAddr::V4(*v4)), + RouteAddress::Inet6(v6) => Some(IpAddr::V6(*v6)), + _ => None, + } +} + +#[cfg(feature = "gateway")] +fn route_extract(rt: &RouteMessage) -> (Option, Option, Option, Option) { + // (dst, prefix, gateway, oif) + let mut dst: Option = None; + let pfx: Option = Some(rt.header.destination_prefix_length); + let mut gw: Option = None; + let mut oif: Option = None; + + for nla in &rt.attributes { + match nla { + RouteAttribute::Destination(a) => dst = route_addr_to_ip(a), + RouteAttribute::Gateway(a) => gw = route_addr_to_ip(a), + RouteAttribute::Oif(i) => oif = Some(*i), + _ => {} + } + } + + // if dst is None and pfx is 0, it means default route + if dst.is_none() && pfx == Some(0) { + dst = match rt.header.address_family { + AddressFamily::Inet => Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED)), + AddressFamily::Inet6 => Some(IpAddr::V6(Ipv6Addr::UNSPECIFIED)), + _ => None, + }; + } + + (dst, pfx, gw, oif) +} + +#[cfg(feature = "gateway")] +fn neigh_addr_to_ip(a: &NeighbourAddress) -> Option { + match a { + NeighbourAddress::Inet(v4) => Some(IpAddr::V4(*v4)), + NeighbourAddress::Inet6(v6) => Some(IpAddr::V6(*v6)), + #[allow(unreachable_patterns)] + _ => None, + } +} + +#[cfg(feature = "gateway")] +fn neigh_extract(n: &NeighbourMessage) -> (Option, Option<[u8; 6]>, Option) { + let mut ip = None; + let mut mac = None; + let mut oif = Some(n.header.ifindex as u32); + + for nla in &n.attributes { + match nla { + NeighbourAttribute::Destination(a) => { + ip = neigh_addr_to_ip(a); + } + // Link-layer address (MAC) + NeighbourAttribute::LinkLocalAddress(v) => { + if v.len() == 6 { + mac = Some([v[0], v[1], v[2], v[3], v[4], v[5]]); + } + } + NeighbourAttribute::IfIndex(i) => oif = Some(*i as u32), + _ => {} + } + } + (ip, mac, oif) +} + +fn mtu_from_link(link: &LinkMessage) -> Option { + for nla in &link.attributes { + if let LinkAttribute::Mtu(m) = nla { + return Some(*m); + } + } + None +} + +#[derive(Debug, Clone)] +pub struct IfRow { + pub index: u32, + pub name: String, + pub mac: Option<[u8; 6]>, + pub ipv4: Vec<(Ipv4Addr, u8)>, + pub ipv6: Vec<(Ipv6Addr, u8)>, + pub flags: u32, + pub mtu: Option, +} + +pub fn collect_interfaces() -> io::Result> { + let links = dump_links()?; + let addrs = dump_addrs()?; + + let mut base: HashMap = HashMap::new(); + for l in links { + let idx = l.header.index as u32; + let name = name_from_link(&l).unwrap_or_else(|| idx.to_string()); + let mac = mac_from_link(&l); + let flags = l.header.flags.bits(); + let mtu_nl = mtu_from_link(&l); + base.insert( + idx, + IfRow { + index: idx, + name, + mac, + ipv4: vec![], + ipv6: vec![], + flags, + mtu: mtu_nl, + }, + ); + } + + for a in addrs { + let idx = a.header.index as u32; + if let Some((ip, pfx)) = ip_from_addr(&a) { + if let Some(row) = base.get_mut(&idx) { + match ip { + IpAddr::V4(v4) => row.ipv4.push((v4, pfx)), + IpAddr::V6(v6) => row.ipv6.push((v6, pfx)), + } + } + } + } + + Ok(base.into_values().collect()) +} + +#[cfg(feature = "gateway")] +#[derive(Debug, Clone)] +pub struct GwRow { + #[allow(dead_code)] + pub ifindex: u32, + pub gw_v4: Vec, + pub gw_v6: Vec, + pub mac: Option<[u8; 6]>, +} + +#[cfg(feature = "gateway")] +pub fn collect_routes() -> io::Result> { + let routes = dump_routes()?; + let neighs = dump_neigh().unwrap_or_default(); + + let mut m: HashMap = HashMap::new(); + for rt in routes { + let (_dst, pfx, gw, oif) = route_extract(&rt); + // default route only + if pfx != Some(0) { + continue; + } + let oif = match oif { + Some(i) => i, + None => continue, + }; + if let Some(gwip) = gw { + let e = m.entry(oif).or_insert(GwRow { + ifindex: oif, + gw_v4: vec![], + gw_v6: vec![], + mac: None, + }); + match gwip { + IpAddr::V4(v4) => { + if !e.gw_v4.contains(&v4) { + e.gw_v4.push(v4); + } + } + IpAddr::V6(v6) => { + if !e.gw_v6.contains(&v6) { + e.gw_v6.push(v6); + } + } + } + } + } + + for n in neighs { + let (ip, mac, ifi) = neigh_extract(&n); + let ifi = match ifi { + Some(i) => i, + None => continue, + }; + if let Some(row) = m.get_mut(&ifi) { + if let (Some(m6), Some(ip)) = (mac, ip) { + let hit = match ip { + IpAddr::V4(v4) => row.gw_v4.contains(&v4), + IpAddr::V6(v6) => row.gw_v6.contains(&v6), + }; + if hit { + row.mac = Some(m6); + } + } + } + } + + Ok(m) +} diff --git a/src/os/bsd/flags.rs b/src/os/bsd/flags.rs new file mode 100644 index 0000000..e7d4f1d --- /dev/null +++ b/src/os/bsd/flags.rs @@ -0,0 +1,76 @@ +use crate::interface::interface::Interface; + +const SIOCGIFFLAGS: libc::c_ulong = 0xc0206911; + +pub fn is_physical_interface(interface: &Interface) -> bool { + interface.is_up() && interface.is_running() && !interface.is_tun() && !interface.is_loopback() +} + +#[cfg(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" +))] +pub fn get_interface_flags(if_name: &str) -> std::io::Result { + use libc::{AF_INET, SOCK_DGRAM, c_char, ioctl, socket}; + use std::mem; + use std::os::unix::io::RawFd; + use std::ptr; + + #[cfg(target_os = "netbsd")] + #[repr(C)] + #[derive(Copy, Clone)] + struct IfReq { + ifr_name: [c_char; libc::IFNAMSIZ], + ifru_flags: [libc::c_short; 2], + } + + #[cfg(not(target_os = "netbsd"))] + use libc::ifreq as IfReq; + + let sock: RawFd = unsafe { socket(AF_INET, SOCK_DGRAM, 0) }; + if sock < 0 { + return Err(std::io::Error::last_os_error()); + } + + let mut ifr: IfReq = unsafe { mem::zeroed() }; + + let ifname_c = std::ffi::CString::new(if_name).map_err(|_| std::io::ErrorKind::InvalidInput)?; + let bytes = ifname_c.as_bytes_with_nul(); + + if bytes.len() > ifr.ifr_name.len() { + unsafe { libc::close(sock) }; + return Err(std::io::ErrorKind::InvalidInput.into()); + } + + unsafe { + ptr::copy_nonoverlapping( + bytes.as_ptr() as *const c_char, + ifr.ifr_name.as_mut_ptr(), + bytes.len(), + ); + } + + let res = unsafe { ioctl(sock, SIOCGIFFLAGS, &mut ifr) }; + unsafe { libc::close(sock) }; + + if res < 0 { + Err(std::io::Error::last_os_error()) + } else { + #[cfg(target_vendor = "apple")] + { + Ok(unsafe { ifr.ifr_ifru.ifru_flags as u32 }) + } + + #[cfg(target_os = "netbsd")] + { + Ok(ifr.ifru_flags[0] as u32) + } + + #[cfg(all(not(target_vendor = "apple"), not(target_os = "netbsd")))] + { + Ok(unsafe { ifr.ifr_ifru.ifru_flags[0] as u32 }) + } + } +} diff --git a/src/os/bsd/interface.rs b/src/os/bsd/interface.rs new file mode 100644 index 0000000..df4595d --- /dev/null +++ b/src/os/bsd/interface.rs @@ -0,0 +1,42 @@ +#[cfg(feature = "gateway")] +use std::net::IpAddr; + +use crate::{interface::interface::Interface, os::unix::interface::unix_interfaces}; + +pub fn interfaces() -> Vec { + #[cfg(not(feature = "gateway"))] + { + unix_interfaces() + } + #[cfg(feature = "gateway")] + { + use crate::os::unix::dns::get_system_dns_conf; + + let mut interfaces: Vec = unix_interfaces(); + let local_ip_opt: Option = crate::net::ip::get_local_ipaddr(); + + let gateway_map = super::route::get_gateway_map(); + + for iface in &mut interfaces { + if let Some(gateway) = gateway_map.get(&iface.index) { + iface.gateway = Some(gateway.clone()); + } + + if let Some(local_ip) = local_ip_opt { + iface.ipv4.iter().for_each(|ipv4| { + if IpAddr::V4(ipv4.addr()) == local_ip { + iface.dns_servers = get_system_dns_conf(); + iface.default = true; + } + }); + iface.ipv6.iter().for_each(|ipv6| { + if IpAddr::V6(ipv6.addr()) == local_ip { + iface.dns_servers = get_system_dns_conf(); + iface.default = true; + } + }); + } + } + interfaces + } +} diff --git a/src/os/bsd/mod.rs b/src/os/bsd/mod.rs new file mode 100644 index 0000000..679f688 --- /dev/null +++ b/src/os/bsd/mod.rs @@ -0,0 +1,7 @@ +pub mod flags; +pub mod interface; +pub mod mtu; +#[cfg(feature = "gateway")] +pub mod route; +pub mod state; +pub mod types; diff --git a/src/os/bsd/mtu.rs b/src/os/bsd/mtu.rs new file mode 100644 index 0000000..420b2f6 --- /dev/null +++ b/src/os/bsd/mtu.rs @@ -0,0 +1,8 @@ +pub(crate) fn get_mtu(ifa: &libc::ifaddrs, _name: &str) -> Option { + if !ifa.ifa_data.is_null() { + let data = unsafe { &*(ifa.ifa_data as *mut libc::if_data) }; + Some(data.ifi_mtu as u32) + } else { + None + } +} diff --git a/src/os/bsd/route.rs b/src/os/bsd/route.rs new file mode 100644 index 0000000..da7ed4a --- /dev/null +++ b/src/os/bsd/route.rs @@ -0,0 +1,567 @@ +#![allow(non_camel_case_types)] + +use libc::{c_int, pid_t, size_t}; +use std::{ + collections::HashMap, + ffi::c_void, + io, mem, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + ptr, +}; + +use crate::net::{device::NetworkDevice, mac::MacAddr}; + +const CTL_NET: c_int = libc::CTL_NET; +const NET_RT_DUMP: c_int = 1; +const NET_RT_FLAGS: c_int = 2; + +const RTF_LLINFO: u32 = 1024; +#[cfg(any(target_os = "freebsd", target_os = "openbsd"))] +const RTM_VERSION: u8 = 5; +#[cfg(target_os = "netbsd")] +const RTM_VERSION: u8 = 4; + +const RTF_WASCLONED: i32 = 0x20000; + +const RTAX_DST: usize = 0; +const RTAX_GATEWAY: usize = 1; +const RTAX_NETMASK: usize = 2; + +#[cfg(target_os = "freebsd")] +const RTAX_MAX: usize = 8; +#[cfg(target_os = "netbsd")] +const RTAX_MAX: usize = 9; +#[cfg(target_os = "openbsd")] +const RTAX_MAX: usize = 15; + +const SA_ALIGN: usize = 4; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct rt_metrics { + rmx_locks: u32, + rmx_mtu: u32, + rmx_hopcount: u32, + rmx_expire: i32, + rmx_recvpipe: u32, + rmx_sendpipe: u32, + rmx_ssthresh: u32, + rmx_rtt: u32, + rmx_rttvar: u32, + rmx_pksent: u32, + rmx_weight: u32, + rmx_nhidx: u32, + rmx_filler: [u32; 2], +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct rt_msghdr { + rtm_msglen: u16, + rtm_version: u8, + rtm_type: u8, + rtm_index: u16, + rtm_flags: c_int, + rtm_addrs: c_int, + rtm_pid: pid_t, + rtm_seq: c_int, + rtm_errno: c_int, + rtm_use: c_int, + rtm_inits: u32, + rtm_rmx: rt_metrics, +} + +unsafe extern "C" { + fn sysctl( + name: *mut c_int, + namelen: u32, + oldp: *mut c_void, + oldlenp: *mut size_t, + newp: *mut c_void, + newlen: size_t, + ) -> c_int; +} + +/// Fetches a sysctl value into a Vec. +fn sysctl_vec(mib: &mut [c_int]) -> io::Result> { + let mut len: size_t = 0; + let mut r = unsafe { + sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + ptr::null_mut(), + &mut len, + ptr::null_mut(), + 0, + ) + }; + if r < 0 { + return Err(io::Error::last_os_error()); + } + + let mut buf = vec![0u8; len as usize]; + r = unsafe { + sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + buf.as_mut_ptr() as *mut c_void, + &mut len, + ptr::null_mut(), + 0, + ) + }; + if r < 0 { + // If the value grew, kernel returns ENOMEM. Retry once. + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(libc::ENOMEM) { + let mut len2: size_t = 0; + let r2 = unsafe { + sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + ptr::null_mut(), + &mut len2, + ptr::null_mut(), + 0, + ) + }; + if r2 < 0 { + return Err(io::Error::last_os_error()); + } + buf.resize(len2 as usize, 0); + let r3 = unsafe { + sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + buf.as_mut_ptr() as *mut c_void, + &mut len2, + ptr::null_mut(), + 0, + ) + }; + if r3 < 0 { + return Err(io::Error::last_os_error()); + } + buf.truncate(len2 as usize); + return Ok(buf); + } + return Err(err); + } + buf.truncate(len as usize); + Ok(buf) +} + +#[inline] +fn roundup(len: usize) -> usize { + if len == 0 { + SA_ALIGN + } else { + (len + (SA_ALIGN - 1)) & !(SA_ALIGN - 1) + } +} + +#[inline] +fn normalize_scoped_v6(gw: Ipv6Addr) -> Ipv6Addr { + // Unicast link-local: fe80::/10 (in practice often fe80::/64) + let is_unicast_ll = gw.segments()[0] == 0xfe80; + + // Multicast check (ff00::/8) and local scopes: 0x1 (node-local) or 0x2 (link-local). + let oct = gw.octets(); + let is_multicast = oct[0] == 0xff; + let mscope = oct[1] & 0x0f; + let is_local_scope_mc = is_multicast && (mscope == 0x1 || mscope == 0x2); + + if is_unicast_ll || is_local_scope_mc { + let segs = gw.segments(); + Ipv6Addr::new( + segs[0], 0, segs[2], segs[3], segs[4], segs[5], segs[6], segs[7], + ) + } else { + gw + } +} + +#[inline] +fn normalize_gateway(ip: IpAddr) -> IpAddr { + match ip { + IpAddr::V4(v4) => IpAddr::V4(v4), + IpAddr::V6(v6) => IpAddr::V6(normalize_scoped_v6(v6)), + } +} + +/// Parse an IP address from a `sockaddr` +fn ip_from_sockaddr(sa: &libc::sockaddr) -> Option { + unsafe { + match sa.sa_family as libc::c_int { + x if x == libc::AF_INET => { + let want = core::mem::size_of::(); + if (sa.sa_len as usize) < want { + return None; + } + let sin = &*(sa as *const _ as *const libc::sockaddr_in); + let n = u32::from_be(sin.sin_addr.s_addr as u32); + Some(IpAddr::V4(Ipv4Addr::from(n))) + } + x if x == libc::AF_INET6 => { + let want = core::mem::size_of::(); + if (sa.sa_len as usize) < want { + return None; + } + let sin6 = &*(sa as *const _ as *const libc::sockaddr_in6); + // `s6_addr` is raw big-endian bytes; `Ipv6Addr::from([u8;16])` expects octets. + Some(IpAddr::V6(Ipv6Addr::from(sin6.sin6_addr.s6_addr))) + } + _ => None, + } + } +} + +fn masklen_from_sockaddr(dst: IpAddr, mask_sa: &libc::sockaddr) -> u8 { + unsafe { + let sa_len = mask_sa.sa_len as usize; + if sa_len == 0 { + return 0; + } + + match dst { + IpAddr::V4(_) => { + const OFF: usize = 4; + if sa_len <= OFF { + return 0; + } + let n = (sa_len - OFF).min(4); + + let base = (mask_sa as *const _ as *const u8).add(OFF); + let mut bytes = [0u8; 4]; + core::ptr::copy_nonoverlapping(base, bytes.as_mut_ptr(), n); + u32::from_be_bytes(bytes).leading_ones() as u8 + } + IpAddr::V6(_) => { + const OFF: usize = 8; + if sa_len <= OFF { + return 0; + } + let n = (sa_len - OFF).min(16); + + let base = (mask_sa as *const _ as *const u8).add(OFF); + let mut bytes = [0u8; 16]; + core::ptr::copy_nonoverlapping(base, bytes.as_mut_ptr(), n); + u128::from_be_bytes(bytes).leading_ones() as u8 + } + } + } +} + +fn code_to_error(err: i32) -> io::Error { + let kind = match err { + x if x == libc::EEXIST => io::ErrorKind::AlreadyExists, + x if x == libc::ESRCH => io::ErrorKind::NotFound, + x if x == libc::ENOBUFS => io::ErrorKind::OutOfMemory, + _ => io::ErrorKind::Other, + }; + io::Error::new(kind, format!("rtm_errno {}", err)) +} + +/// Extract `(IP, MAC)` pair from a routing message's address block. +fn message_to_arppair(msg: &[u8]) -> Option<(IpAddr, MacAddr)> { + let mut off = 0usize; + let mut ip: Option = None; + let mut mac: Option = None; + + while off + core::mem::size_of::() <= msg.len() { + let sa = unsafe { &*(msg[off..].as_ptr() as *const libc::sockaddr) }; + let sa_len = sa.sa_len as usize; + + if sa_len == 0 { + off += roundup(0); + continue; + } + if off + sa_len > msg.len() { + off += roundup(sa_len); + continue; + } + + match sa.sa_family as c_int { + x if x == libc::AF_INET => { + if let Some(IpAddr::V4(v4)) = ip_from_sockaddr(sa) { + ip = Some(v4); + if let (Some(v4), Some(m)) = (ip, mac) { + return Some((IpAddr::V4(v4), m)); + } + } + } + x if x == libc::AF_LINK => { + let sdl = unsafe { &*(sa as *const _ as *const libc::sockaddr_dl) }; + let nlen = sdl.sdl_nlen as usize; + let alen = sdl.sdl_alen as usize; + let total = sdl.sdl_len as usize; + + if total >= core::mem::size_of::() + && alen >= 6 + && sa_len >= total + { + let base = sa as *const _ as *const u8; + let data_base = &sdl.sdl_data as *const _ as *const u8; + let data_off = unsafe { data_base.offset_from(base) } as usize; + + if data_off + nlen + alen <= total { + let mac_ptr = unsafe { data_base.add(nlen) }; + let m = MacAddr::from_octets(unsafe { + [ + *mac_ptr.add(0), + *mac_ptr.add(1), + *mac_ptr.add(2), + *mac_ptr.add(3), + *mac_ptr.add(4), + *mac_ptr.add(5), + ] + }); + mac = Some(m); + if let (Some(v4), Some(m)) = (ip, mac) { + return Some((IpAddr::V4(v4), m)); + } + } + } + } + _ => {} + } + + off += roundup(sa_len); + } + + None +} + +#[derive(Debug)] +struct RawRoute { + #[allow(dead_code)] + dst: IpAddr, + prefix: u8, + gateway: Option, + ifindex: u32, + #[allow(dead_code)] + flags: c_int, +} + +fn parse_one_route(hdr: &rt_msghdr, addr_block: &[u8]) -> Option { + const MSG_START_INDEX: usize = 60; + let mut addrs: [Option<*const libc::sockaddr>; RTAX_MAX] = [None; RTAX_MAX]; + let mut off = MSG_START_INDEX; + + for idx in 0..RTAX_MAX { + if (hdr.rtm_addrs & (1 << idx)) != 0 { + if off + mem::size_of::() > addr_block.len() { + return None; + } + let sa = unsafe { &*(addr_block[off..].as_ptr() as *const libc::sockaddr) }; + addrs[idx] = Some(sa as *const libc::sockaddr); + + let sa_len = sa.sa_len as usize; + let step = roundup(if sa_len == 0 { 0 } else { sa_len }); + if off + step > addr_block.len() { + return None; + } + off += step; + } + } + + let dptr = addrs[RTAX_DST]? as *const libc::sockaddr; + let dst_sa = unsafe { &*dptr }; + let dst_ip = ip_from_sockaddr(dst_sa)?; + let mut prefix: u8 = match dst_ip { + IpAddr::V4(_) => 32, + IpAddr::V6(_) => 128, + }; + if let Some(mptr) = addrs[RTAX_NETMASK] { + let m_sa = unsafe { &*mptr }; + prefix = if m_sa.sa_len == 0 { + 0 + } else { + masklen_from_sockaddr(dst_ip, m_sa) + }; + } else if (hdr.rtm_flags & libc::RTF_HOST) != 0 { + prefix = match dst_ip { + IpAddr::V4(_) => 32, + _ => 128, + }; + } + + let gateway = if let Some(gptr) = addrs[RTAX_GATEWAY] { + let g_sa = unsafe { &*gptr }; + ip_from_sockaddr(g_sa).map(normalize_gateway) + } else { + None + }; + + Some(RawRoute { + dst: dst_ip, + prefix, + gateway, + ifindex: hdr.rtm_index as u32, + flags: hdr.rtm_flags, + }) +} + +/// Build an ARP/Neighbor table from the BSD/Darwin routing socket via `sysctl`. +fn get_arp_table() -> io::Result> { + let mut arp_map: HashMap = HashMap::new(); + + let mut mib = [ + CTL_NET, + libc::PF_ROUTE, + 0, + libc::AF_INET, + NET_RT_FLAGS, + RTF_LLINFO as i32, // ARP/neighbor entries + ]; + let buf = sysctl_vec(&mut mib)?; + + let mut off = 0usize; + while off + mem::size_of::() <= buf.len() { + let hdr = unsafe { &*(buf[off..].as_ptr() as *const rt_msghdr) }; + let msglen = hdr.rtm_msglen as usize; + if msglen == 0 || off + msglen > buf.len() { + break; + } + + if hdr.rtm_version != RTM_VERSION { + off += msglen; + continue; + } + if hdr.rtm_errno != 0 { + return Err(code_to_error(hdr.rtm_errno)); + } + + let addr_block = &buf[off + mem::size_of::()..off + msglen]; + if let Some((ip, mac)) = message_to_arppair(addr_block) { + arp_map.insert(ip, mac); + } + + off += msglen; + } + + Ok(arp_map) +} + +/// Dump the routing table via `sysctl` on BSD/Darwin and parse each `rt_msghdr`. +fn list_routes() -> io::Result> { + let mut mib = [ + CTL_NET, + libc::PF_ROUTE, + 0, + 0, // all families + NET_RT_DUMP, // dump routes + 0, + ]; + let buf = sysctl_vec(&mut mib)?; + + let mut out = Vec::::new(); + let mut off = 0usize; + + while off + mem::size_of::() <= buf.len() { + let hdr = unsafe { &*(buf[off..].as_ptr() as *const rt_msghdr) }; + let msglen = hdr.rtm_msglen as usize; + if msglen == 0 || off + msglen > buf.len() { + break; + } + + if hdr.rtm_version != RTM_VERSION { + off += msglen; + continue; + } + if (hdr.rtm_flags & RTF_WASCLONED) != 0 { + off += msglen; + continue; + } + if hdr.rtm_errno != 0 { + return Err(code_to_error(hdr.rtm_errno)); + } + + let addr_block = &buf[off + mem::size_of::()..off + msglen]; + if let Some(rr) = parse_one_route(hdr, addr_block) { + out.push(rr); + } + + off += msglen; + } + + Ok(out) +} + +/// Build a map `ifindex -> NetworkDevice` for default gateways on BSD/Darwin. +pub fn get_gateway_map() -> HashMap { + // Fetch routes; on failure just return an empty map. + let routes = match list_routes() { + Ok(v) => v, + Err(_) => return HashMap::new(), + }; + // ARP cache: IP -> MAC (empty if ARP cannot be read) + let arp_map = get_arp_table().unwrap_or_default(); + + // Accumulator: ifindex -> (optional MAC candidate, v4 list, v6 list) + #[derive(Default)] + struct Acc { + mac: Option, + v4: Vec, + v6: Vec, + } + let mut acc: HashMap = HashMap::new(); + + // Small helpers to avoid duplicates + fn push_v4(vec: &mut Vec, ip: Ipv4Addr) { + if !vec.iter().any(|x| *x == ip) { + vec.push(ip); + } + } + fn push_v6(vec: &mut Vec, ip: Ipv6Addr) { + if !vec.iter().any(|x| *x == ip) { + vec.push(ip); + } + } + + // Collect gateway candidates per outgoing interface + for r in routes.into_iter() { + // Only gateway routes (prefix==0) and those flagged as GATEWAY + if r.prefix != 0 || (r.flags & libc::RTF_GATEWAY) == 0 { + continue; + } + let gw = match r.gateway { + Some(ip) => ip, + None => continue, + }; + + let entry = acc.entry(r.ifindex).or_default(); + + // If this is an IPv4 gateway and ARP has the MAC, record it. + if let Some(mac) = arp_map.get(&gw).copied() { + entry.mac = Some(mac); + } + + match gw { + IpAddr::V4(v4) => { + push_v4(&mut entry.v4, v4); + } + IpAddr::V6(v6) => { + push_v6(&mut entry.v6, v6); + } + } + } + + // Shape the final output: ifindex -> NetworkDevice + let mut out: HashMap = HashMap::new(); + for (ifindex, a) in acc { + // If MAC is still unknown, use a zero MAC + // TODO: Implement NDP lookup for IPv6 + let mac = a.mac.unwrap_or_else(|| MacAddr::zero()); + out.insert( + ifindex, + NetworkDevice { + mac_addr: mac, + ipv4: a.v4, + ipv6: a.v6, + }, + ); + } + + out +} diff --git a/src/os/bsd/state.rs b/src/os/bsd/state.rs new file mode 100644 index 0000000..ce4b353 --- /dev/null +++ b/src/os/bsd/state.rs @@ -0,0 +1,8 @@ +use crate::interface::state::OperState; + +pub fn operstate(if_name: &str) -> OperState { + match super::flags::get_interface_flags(if_name) { + Ok(flags) => OperState::from_if_flags(flags), + Err(_) => OperState::Unknown, + } +} diff --git a/src/os/bsd/types.rs b/src/os/bsd/types.rs new file mode 100644 index 0000000..fc32a49 --- /dev/null +++ b/src/os/bsd/types.rs @@ -0,0 +1,10 @@ +use crate::interface::types::InterfaceType; + +pub fn get_interface_type(addr_ref: &libc::ifaddrs) -> InterfaceType { + if !addr_ref.ifa_data.is_null() { + let if_data = unsafe { &*(addr_ref.ifa_data as *const libc::if_data) }; + InterfaceType::try_from(if_data.ifi_type as u32).unwrap_or(InterfaceType::Unknown) + } else { + InterfaceType::Unknown + } +} diff --git a/src/os/darwin/flags.rs b/src/os/darwin/flags.rs new file mode 100644 index 0000000..196ac84 --- /dev/null +++ b/src/os/darwin/flags.rs @@ -0,0 +1,47 @@ +use crate::interface::interface::Interface; + +const SIOCGIFFLAGS: libc::c_ulong = 0xc0206911; + +pub fn is_physical_interface(interface: &Interface) -> bool { + interface.is_up() && interface.is_running() && !interface.is_tun() && !interface.is_loopback() +} + +pub fn get_interface_flags(if_name: &str) -> std::io::Result { + use libc::ifreq as IfReq; + use libc::{AF_INET, SOCK_DGRAM, c_char, ioctl, socket}; + use std::mem; + use std::os::unix::io::RawFd; + use std::ptr; + + let sock: RawFd = unsafe { socket(AF_INET, SOCK_DGRAM, 0) }; + if sock < 0 { + return Err(std::io::Error::last_os_error()); + } + + let mut ifr: IfReq = unsafe { mem::zeroed() }; + + let ifname_c = std::ffi::CString::new(if_name).map_err(|_| std::io::ErrorKind::InvalidInput)?; + let bytes = ifname_c.as_bytes_with_nul(); + + if bytes.len() > ifr.ifr_name.len() { + unsafe { libc::close(sock) }; + return Err(std::io::ErrorKind::InvalidInput.into()); + } + + unsafe { + ptr::copy_nonoverlapping( + bytes.as_ptr() as *const c_char, + ifr.ifr_name.as_mut_ptr(), + bytes.len(), + ); + } + + let res = unsafe { ioctl(sock, SIOCGIFFLAGS, &mut ifr) }; + unsafe { libc::close(sock) }; + + if res < 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(unsafe { ifr.ifr_ifru.ifru_flags as u32 }) + } +} diff --git a/src/os/darwin/mod.rs b/src/os/darwin/mod.rs new file mode 100644 index 0000000..f5630ac --- /dev/null +++ b/src/os/darwin/mod.rs @@ -0,0 +1,6 @@ +pub mod flags; +pub mod mtu; +#[cfg(feature = "gateway")] +pub mod route; +pub mod state; +pub mod types; diff --git a/src/os/darwin/mtu.rs b/src/os/darwin/mtu.rs new file mode 100644 index 0000000..420b2f6 --- /dev/null +++ b/src/os/darwin/mtu.rs @@ -0,0 +1,8 @@ +pub(crate) fn get_mtu(ifa: &libc::ifaddrs, _name: &str) -> Option { + if !ifa.ifa_data.is_null() { + let data = unsafe { &*(ifa.ifa_data as *mut libc::if_data) }; + Some(data.ifi_mtu as u32) + } else { + None + } +} diff --git a/src/os/darwin/route.rs b/src/os/darwin/route.rs new file mode 100644 index 0000000..c956558 --- /dev/null +++ b/src/os/darwin/route.rs @@ -0,0 +1,620 @@ +#![allow(non_camel_case_types)] + +use libc::{c_char, c_int, c_uchar, pid_t, size_t}; +use std::{ + collections::HashMap, + ffi::c_void, + io, mem, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + ptr, +}; + +use crate::net::{device::NetworkDevice, mac::MacAddr}; + +const CTL_NET: c_int = 4; +#[allow(dead_code)] +const AF_ROUTE: c_int = 17; +const PF_ROUTE: c_int = 17; +const AF_LINK: c_int = 18; +const AF_INET: c_int = 2; +const AF_INET6: c_int = 30; + +const NET_RT_DUMP: c_int = 1; +const NET_RT_FLAGS: c_int = 2; + +const RTM_VERSION: c_uchar = 5; +const RTF_LLINFO: c_int = 1024; + +// rtm_flags +#[allow(dead_code)] +const RTF_UP: c_int = 0x0001; +const RTF_GATEWAY: c_int = 0x0002; +const RTF_HOST: c_int = 0x0004; +#[allow(dead_code)] +const RTF_REJECT: c_int = 0x0008; +#[allow(dead_code)] +const RTF_STATIC: c_int = 0x0800; +const RTF_WASCLONED: c_int = 0x20000; + +// rtm_addrs +const RTAX_DST: usize = 0; +const RTAX_GATEWAY: usize = 1; +const RTAX_NETMASK: usize = 2; +const RTAX_MAX: usize = 8; + +// sockaddr alignment +const SA_ALIGN: usize = 4; + +#[repr(C)] +#[derive(Copy, Clone)] +struct sockaddr { + sa_len: c_uchar, + sa_family: c_uchar, + sa_data: [c_char; 14], +} +#[repr(C)] +#[derive(Copy, Clone)] +struct in_addr { + s_addr: u32, +} +#[repr(C)] +#[derive(Copy, Clone)] +struct in6_addr { + __u6_addr: in6_addr_bind, +} +#[repr(C)] +#[derive(Copy, Clone)] +union in6_addr_bind { + __u6_addr8: [u8; 16], + __u6_addr16: [u16; 8], + __u6_addr32: [u32; 4], +} +#[repr(C)] +#[derive(Copy, Clone)] +struct sockaddr_in { + sin_len: c_uchar, + sin_family: c_uchar, + sin_port: u16, + sin_addr: in_addr, + sin_zero: [c_char; 8], +} +#[repr(C)] +#[derive(Copy, Clone)] +struct sockaddr_in6 { + sin6_len: c_uchar, + sin6_family: c_uchar, + sin6_port: u16, + sin6_flowinfo: u32, + sin6_addr: in6_addr, + sin6_scope_id: u32, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct rt_metrics { + rmx_locks: u32, + rmx_mtu: u32, + rmx_hopcount: u32, + rmx_expire: i32, + rmx_recvpipe: u32, + rmx_sendpipe: u32, + rmx_ssthresh: u32, + rmx_rtt: u32, + rmx_rttvar: u32, + rmx_pksent: u32, + rmx_state: u32, + rmx_filler: [u32; 3], +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct rt_msghdr { + rtm_msglen: u16, + rtm_version: u8, + rtm_type: u8, + rtm_index: u16, + rtm_flags: c_int, + rtm_addrs: c_int, + rtm_pid: pid_t, + rtm_seq: c_int, + rtm_errno: c_int, + rtm_use: c_int, + rtm_inits: u32, + rtm_rmx: rt_metrics, +} + +unsafe extern "C" { + fn sysctl( + name: *mut c_int, + namelen: u32, + oldp: *mut c_void, + oldlenp: *mut size_t, + newp: *mut c_void, + newlen: size_t, + ) -> c_int; +} + +/// Fetches a sysctl value into a Vec. +fn sysctl_vec(mib: &mut [c_int]) -> io::Result> { + let mut len: size_t = 0; + let mut r = unsafe { + sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + ptr::null_mut(), + &mut len, + ptr::null_mut(), + 0, + ) + }; + if r < 0 { + return Err(io::Error::last_os_error()); + } + + let mut buf = vec![0u8; len as usize]; + r = unsafe { + sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + buf.as_mut_ptr() as *mut c_void, + &mut len, + ptr::null_mut(), + 0, + ) + }; + if r < 0 { + // If the value grew, kernel returns ENOMEM. Retry once. + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(libc::ENOMEM) { + let mut len2: size_t = 0; + let r2 = unsafe { + sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + ptr::null_mut(), + &mut len2, + ptr::null_mut(), + 0, + ) + }; + if r2 < 0 { + return Err(io::Error::last_os_error()); + } + buf.resize(len2 as usize, 0); + let r3 = unsafe { + sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + buf.as_mut_ptr() as *mut c_void, + &mut len2, + ptr::null_mut(), + 0, + ) + }; + if r3 < 0 { + return Err(io::Error::last_os_error()); + } + buf.truncate(len2 as usize); + return Ok(buf); + } + return Err(err); + } + buf.truncate(len as usize); + Ok(buf) +} + +#[inline] +fn roundup(len: usize) -> usize { + if len == 0 { + SA_ALIGN + } else { + (len + (SA_ALIGN - 1)) & !(SA_ALIGN - 1) + } +} + +#[inline] +fn normalize_scoped_v6(gw: Ipv6Addr) -> Ipv6Addr { + // Unicast link-local: fe80::/10 (in practice often fe80::/64) + let is_unicast_ll = gw.segments()[0] == 0xfe80; + + // Multicast check (ff00::/8) and local scopes: 0x1 (node-local) or 0x2 (link-local). + let oct = gw.octets(); + let is_multicast = oct[0] == 0xff; + let mscope = oct[1] & 0x0f; + let is_local_scope_mc = is_multicast && (mscope == 0x1 || mscope == 0x2); + + if is_unicast_ll || is_local_scope_mc { + let segs = gw.segments(); + Ipv6Addr::new( + segs[0], 0, segs[2], segs[3], segs[4], segs[5], segs[6], segs[7], + ) + } else { + gw + } +} + +#[inline] +fn normalize_gateway(ip: IpAddr) -> IpAddr { + match ip { + IpAddr::V4(v4) => IpAddr::V4(v4), + IpAddr::V6(v6) => IpAddr::V6(normalize_scoped_v6(v6)), + } +} + +/// Parse an IP address from a `sockaddr` +fn ip_from_sockaddr(sa: &sockaddr) -> Option { + unsafe { + match sa.sa_family as c_int { + AF_INET => { + let sin = &*(sa as *const _ as *const libc::sockaddr_in); + let n = u32::from_be(sin.sin_addr.s_addr as u32); + Some(IpAddr::V4(Ipv4Addr::from(n))) + } + AF_INET6 => { + // Require the full `sockaddr_in6` to be present. + let sin6 = &*(sa as *const _ as *const libc::sockaddr_in6); + let want = core::mem::size_of::(); + if (sa.sa_len as usize) < want { + // prevent reading a truncated variable-length sockaddr + return None; + } + // `s6_addr` is raw big-endian bytes; `Ipv6Addr::from([u8;16])` expects octets. + let addr_bytes = (*sin6).sin6_addr.s6_addr; + Some(IpAddr::V6(Ipv6Addr::from(addr_bytes))) + } + _ => None, + } + } +} + +fn masklen_from_sockaddr(dst: IpAddr, mask_sa: &sockaddr) -> u8 { + unsafe { + match dst { + IpAddr::V4(_) => { + let m: &sockaddr_in = &*(mask_sa as *const _ as *const sockaddr_in); + u32::from_be(m.sin_addr.s_addr).leading_ones() as u8 + } + IpAddr::V6(_) => { + let m: &sockaddr_in6 = &*(mask_sa as *const _ as *const sockaddr_in6); + let b = m.sin6_addr.__u6_addr.__u6_addr8; + let v = u128::from_be_bytes(b); + v.leading_ones() as u8 + } + } + } +} + +fn code_to_error(err: i32) -> io::Error { + let kind = match err { + 17 => io::ErrorKind::AlreadyExists, // EEXIST + 3 => io::ErrorKind::NotFound, // ESRCH + 3436 => io::ErrorKind::OutOfMemory, // ENOBUFS + _ => io::ErrorKind::Other, + }; + + io::Error::new(kind, format!("rtm_errno {}", err)) +} + +/// Extract `(IP, MAC)` pair from a routing message's address block. +fn message_to_arppair(msg: &[u8]) -> Option<(IpAddr, MacAddr)> { + let mut off = 0usize; + let mut ip: Option = None; + let mut mac: Option = None; + // Walk `sockaddr` records while there is room for a header. + while off + core::mem::size_of::() <= msg.len() { + // Read the sockaddr header + let sa = unsafe { &*(msg[off..].as_ptr() as *const sockaddr) }; + let sa_len = sa.sa_len as usize; + + // `sa_len == 0` can appear as "no address" (alignment-only slot). + // Advance by the platform's alignment unit (4 on BSD/Darwin). + if sa_len == 0 { + off += roundup(0); + continue; + } + // If the element claims to extend past the buffer, skip it conservatively. + if off + sa_len > msg.len() { + off += roundup(sa_len); + continue; + } + + match sa.sa_family as c_int { + AF_INET => { + // Target IPv4 of ARP. `sockaddr_in` and `sockaddr_inarp` share the initial layout, + // so `sin_addr` sits at the same position. + if let Some(IpAddr::V4(v4)) = ip_from_sockaddr(sa) { + ip = Some(v4); + if let (Some(v4), Some(m)) = (ip, mac) { + return Some((IpAddr::V4(v4), m)); + } + } + } + AF_LINK => { + // Extract LLADDR from `sockaddr_dl`. + let sdl = unsafe { &*(sa as *const _ as *const libc::sockaddr_dl) }; + let nlen = sdl.sdl_nlen as usize; + let alen = sdl.sdl_alen as usize; + let total = sdl.sdl_len as usize; + + // Validate against the *actual* struct length (`sdl_len`), and also + // make sure the caller-provided `sa_len` is at least that long. + if total >= core::mem::size_of::() + && alen >= 6 + && sa_len >= total + { + let base = sa as *const _ as *const u8; + let data_base = &sdl.sdl_data as *const _ as *const u8; + let data_off = unsafe { data_base.offset_from(base) } as usize; + + // LLADDR is at `sdl_data + sdl_nlen`. + if data_off + nlen + alen <= total { + let mac_ptr = unsafe { data_base.add(nlen) }; + let m = MacAddr::from_octets(unsafe { + [ + *mac_ptr.add(0), + *mac_ptr.add(1), + *mac_ptr.add(2), + *mac_ptr.add(3), + *mac_ptr.add(4), + *mac_ptr.add(5), + ] + }); + mac = Some(m); + if let (Some(v4), Some(m)) = (ip, mac) { + return Some((IpAddr::V4(v4), m)); + } + } + } + } + _ => {} + } + + // Advance to the next record; BSD/Darwin sockaddrs are 4-byte aligned. + off += roundup(sa_len); + } + + None +} + +#[derive(Debug)] +struct RawRoute { + #[allow(dead_code)] + dst: IpAddr, + prefix: u8, + gateway: Option, + ifindex: u32, + #[allow(dead_code)] + flags: c_int, +} + +fn parse_one_route(hdr: &rt_msghdr, addr_block: &[u8]) -> Option { + let mut addrs: [Option<*const sockaddr>; RTAX_MAX] = [None; RTAX_MAX]; + let mut off = 0usize; + + for idx in 0..RTAX_MAX { + if (hdr.rtm_addrs & (1 << idx)) != 0 { + if off + mem::size_of::() > addr_block.len() { + return None; + } + let sa = unsafe { &*(addr_block[off..].as_ptr() as *const sockaddr) }; + addrs[idx] = Some(sa as *const sockaddr); + + let sa_len = sa.sa_len as usize; + let step = roundup(if sa_len == 0 { 0 } else { sa_len }); + if off + step > addr_block.len() { + return None; + } + off += step; + } + } + + let dptr = addrs[RTAX_DST]? as *const sockaddr; + let dst_sa = unsafe { &*dptr }; + let dst_ip = ip_from_sockaddr(dst_sa)?; + let mut prefix: u8 = match dst_ip { + IpAddr::V4(_) => 32, + IpAddr::V6(_) => 128, + }; + if let Some(mptr) = addrs[RTAX_NETMASK] { + let m_sa = unsafe { &*mptr }; + // sa_len==0 is possible for default route + prefix = if m_sa.sa_len == 0 { + 0 + } else { + masklen_from_sockaddr(dst_ip, m_sa) + }; + } else if (hdr.rtm_flags & RTF_HOST) != 0 { + prefix = match dst_ip { + IpAddr::V4(_) => 32, + _ => 128, + }; + } + + // gateway + let gateway = if let Some(gptr) = addrs[RTAX_GATEWAY] { + let g_sa = unsafe { &*gptr }; + ip_from_sockaddr(g_sa).map(normalize_gateway) + } else { + None + }; + + Some(RawRoute { + dst: dst_ip, + prefix, + gateway, + ifindex: hdr.rtm_index as u32, + flags: hdr.rtm_flags, + }) +} + +/// Build an ARP/Neighbor table from the BSD/Darwin routing socket via `sysctl`. +fn get_arp_table() -> io::Result> { + let mut arp_map: HashMap = HashMap::new(); + // sysctl net.route dump for ARP/neighbor entries (IPv4 only here). + let mut mib = [ + CTL_NET, // net + PF_ROUTE, // route + 0, // 0 + AF_INET, // IPv4 + NET_RT_FLAGS, // flags + RTF_LLINFO, // ARP/neighbor entries + ]; + // Includes ENOMEM retry internally; length is truncated to actual bytes read. + let buf = sysctl_vec(&mut mib)?; + + let mut off = 0usize; + // Each record starts with `rt_msghdr` followed by a variable-length sockaddr block. + while off + mem::size_of::() <= buf.len() { + // Header view (no copy). + let hdr = unsafe { &*(buf[off..].as_ptr() as *const rt_msghdr) }; + let msglen = hdr.rtm_msglen as usize; + if msglen == 0 || off + msglen > buf.len() { + break; + } + + // Version mismatch: skip the record but keep reading. + if hdr.rtm_version != RTM_VERSION { + off += msglen; + continue; + } + if hdr.rtm_errno != 0 { + return Err(code_to_error(hdr.rtm_errno)); + } + + // Parse the sockaddr block right after the header. + let addr_block = &buf[off + mem::size_of::()..off + msglen]; + if let Some((ip, mac)) = message_to_arppair(addr_block) { + arp_map.insert(ip, mac); + } + + off += msglen; + } + + Ok(arp_map) +} + +/// Dump the routing table via `sysctl` on BSD/Darwin and parse each `rt_msghdr`. +fn list_routes() -> io::Result> { + // sysctl net.route dump (all families) + let mut mib = [ + CTL_NET, // net + PF_ROUTE, // route + 0, // 0 + 0, // all families (0) + NET_RT_DUMP, // dump routes + 0, + ]; + // Includes ENOMEM retry internally; buffer is right-sized and truncated. + let buf = sysctl_vec(&mut mib)?; + + let mut out = Vec::::new(); + let mut off = 0usize; + + while off + mem::size_of::() <= buf.len() { + let hdr = unsafe { &*(buf[off..].as_ptr() as *const rt_msghdr) }; + let msglen = hdr.rtm_msglen as usize; + if msglen == 0 || off + msglen > buf.len() { + break; + } + // Version mismatch: skip the record but keep reading. + if hdr.rtm_version != RTM_VERSION { + off += msglen; + continue; + } + // Skip cloned/old-style cache entries; keep only real routes. + if (hdr.rtm_flags & RTF_WASCLONED) != 0 { + off += msglen; + continue; + } + if hdr.rtm_errno != 0 { + return Err(code_to_error(hdr.rtm_errno)); + } + + let addr_block = &buf[off + mem::size_of::()..off + msglen]; + if let Some(rr) = parse_one_route(hdr, addr_block) { + out.push(rr); + } + + off += msglen; + } + + Ok(out) +} + +/// Build a map `ifindex -> NetworkDevice` for default gateways on BSD/Darwin. +pub fn get_gateway_map() -> HashMap { + // Fetch routes; on failure just return an empty map. + let routes = match list_routes() { + Ok(v) => v, + Err(_) => return HashMap::new(), + }; + // ARP cache: IP -> MAC (empty if ARP cannot be read) + let arp_map = get_arp_table().unwrap_or_default(); + + // Accumulator: ifindex -> (optional MAC candidate, v4 list, v6 list) + #[derive(Default)] + struct Acc { + mac: Option, + v4: Vec, + v6: Vec, + } + let mut acc: HashMap = HashMap::new(); + + // Small helpers to avoid duplicates + fn push_v4(vec: &mut Vec, ip: Ipv4Addr) { + if !vec.iter().any(|x| *x == ip) { + vec.push(ip); + } + } + fn push_v6(vec: &mut Vec, ip: Ipv6Addr) { + if !vec.iter().any(|x| *x == ip) { + vec.push(ip); + } + } + + // Collect gateway candidates per outgoing interface + for r in routes.into_iter() { + // Only gateway routes (prefix==0) and those flagged as GATEWAY + if r.prefix != 0 || (r.flags & RTF_GATEWAY) == 0 { + continue; + } + let gw = match r.gateway { + Some(ip) => ip, + None => continue, + }; + + let entry = acc.entry(r.ifindex).or_default(); + + // If this is an IPv4 gateway and ARP has the MAC, record it. + if let Some(mac) = arp_map.get(&gw).copied() { + entry.mac = Some(mac); + } + + match gw { + IpAddr::V4(v4) => { + push_v4(&mut entry.v4, v4); + } + IpAddr::V6(v6) => { + push_v6(&mut entry.v6, v6); + } + } + } + + // Shape the final output: ifindex -> NetworkDevice + let mut out: HashMap = HashMap::new(); + for (ifindex, a) in acc { + // If MAC is still unknown, use a zero MAC + // TODO: Implement NDP lookup for IPv6 + let mac = a.mac.unwrap_or_else(|| MacAddr::zero()); + out.insert( + ifindex, + NetworkDevice { + mac_addr: mac, + ipv4: a.v4, + ipv6: a.v6, + }, + ); + } + + out +} diff --git a/src/os/darwin/state.rs b/src/os/darwin/state.rs new file mode 100644 index 0000000..ce4b353 --- /dev/null +++ b/src/os/darwin/state.rs @@ -0,0 +1,8 @@ +use crate::interface::state::OperState; + +pub fn operstate(if_name: &str) -> OperState { + match super::flags::get_interface_flags(if_name) { + Ok(flags) => OperState::from_if_flags(flags), + Err(_) => OperState::Unknown, + } +} diff --git a/src/os/darwin/types.rs b/src/os/darwin/types.rs new file mode 100644 index 0000000..fc32a49 --- /dev/null +++ b/src/os/darwin/types.rs @@ -0,0 +1,10 @@ +use crate::interface::types::InterfaceType; + +pub fn get_interface_type(addr_ref: &libc::ifaddrs) -> InterfaceType { + if !addr_ref.ifa_data.is_null() { + let if_data = unsafe { &*(addr_ref.ifa_data as *const libc::if_data) }; + InterfaceType::try_from(if_data.ifi_type as u32).unwrap_or(InterfaceType::Unknown) + } else { + InterfaceType::Unknown + } +} diff --git a/src/os/ios/interface.rs b/src/os/ios/interface.rs new file mode 100644 index 0000000..8fdd7a0 --- /dev/null +++ b/src/os/ios/interface.rs @@ -0,0 +1,41 @@ +#[cfg(feature = "gateway")] +use std::net::IpAddr; + +use crate::{interface::interface::Interface, os::unix::interface::unix_interfaces}; + +pub fn interfaces() -> Vec { + let mut interfaces: Vec = unix_interfaces(); + + #[cfg(feature = "gateway")] + let local_ip_opt: Option = crate::net::ip::get_local_ipaddr(); + + #[cfg(feature = "gateway")] + let gateway_map = crate::os::darwin::route::get_gateway_map(); + + for iface in &mut interfaces { + #[cfg(feature = "gateway")] + { + use crate::os::unix::dns::get_system_dns_conf; + if let Some(gateway) = gateway_map.get(&iface.index) { + iface.gateway = Some(gateway.clone()); + } + + if let Some(local_ip) = local_ip_opt { + iface.ipv4.iter().for_each(|ipv4| { + if IpAddr::V4(ipv4.addr()) == local_ip { + iface.dns_servers = get_system_dns_conf(); + iface.default = true; + } + }); + iface.ipv6.iter().for_each(|ipv6| { + if IpAddr::V6(ipv6.addr()) == local_ip { + iface.dns_servers = get_system_dns_conf(); + iface.default = true; + } + }); + } + } + } + + interfaces +} diff --git a/src/os/ios/mod.rs b/src/os/ios/mod.rs new file mode 100644 index 0000000..8d3d626 --- /dev/null +++ b/src/os/ios/mod.rs @@ -0,0 +1 @@ +pub mod interface; diff --git a/src/os/linux/arp.rs b/src/os/linux/arp.rs new file mode 100644 index 0000000..75e7bab --- /dev/null +++ b/src/os/linux/arp.rs @@ -0,0 +1,15 @@ +// ARP protocol HARDWARE identifiers + +pub const ARPHRD_ETHER: u32 = libc::ARPHRD_ETHER as u32; +pub const ARPHRD_IEEE802: u32 = libc::ARPHRD_IEEE802 as u32; +pub const ARPHRD_FDDI: u32 = libc::ARPHRD_FDDI as u32; +pub const ARPHRD_PPP: u32 = libc::ARPHRD_PPP as u32; +pub const ARPHRD_LOOPBACK: u32 = libc::ARPHRD_LOOPBACK as u32; +pub const ARPHRD_EETHER: u32 = libc::ARPHRD_EETHER as u32; +pub const ARPHRD_SLIP: u32 = libc::ARPHRD_SLIP as u32; +pub const ARPHRD_ATM: u32 = libc::ARPHRD_ATM as u32; +pub const ARPHRD_IEEE80211: u32 = libc::ARPHRD_IEEE80211 as u32; +pub const ARPHRD_TUNNEL: u32 = libc::ARPHRD_TUNNEL as u32; +pub const ARPHRD_X25: u32 = libc::ARPHRD_X25 as u32; +pub const ARPHRD_IEEE1394: u32 = libc::ARPHRD_IEEE1394 as u32; +pub const ARPHRD_CAN: u32 = libc::ARPHRD_CAN as u32; diff --git a/src/os/linux/flags.rs b/src/os/linux/flags.rs new file mode 100644 index 0000000..9eb5823 --- /dev/null +++ b/src/os/linux/flags.rs @@ -0,0 +1,7 @@ +use crate::interface::interface::Interface; +pub use libc::IFF_LOWER_UP; + +pub fn is_physical_interface(interface: &Interface) -> bool { + (interface.flags & (IFF_LOWER_UP as u32) != 0) + || (!interface.is_loopback() && !super::sysfs::is_virtual_interface(&interface.name)) +} diff --git a/src/os/linux/interface.rs b/src/os/linux/interface.rs new file mode 100644 index 0000000..cecf193 --- /dev/null +++ b/src/os/linux/interface.rs @@ -0,0 +1,176 @@ +use super::netlink; +use crate::interface::interface::Interface; +use crate::interface::state::OperState; +use crate::ipnet::{Ipv4Net, Ipv6Net}; +use crate::net::mac::MacAddr; +use crate::os::unix::interface::unix_interfaces; +use std::net::{Ipv4Addr, Ipv6Addr}; + +#[cfg(feature = "gateway")] +use crate::net::device::NetworkDevice; +#[cfg(feature = "gateway")] +use crate::net::ip::get_local_ipaddr; +#[cfg(feature = "gateway")] +use crate::os::unix::dns::get_system_dns_conf; +#[cfg(feature = "gateway")] +use std::collections::HashMap; +#[cfg(feature = "gateway")] +use std::net::IpAddr; + +fn push_ipv4(v: &mut Vec, add: (Ipv4Addr, u8)) { + if v.iter() + .any(|n| n.addr() == add.0 && n.prefix_len() == add.1) + { + return; + } + if let Ok(net) = Ipv4Net::new(add.0, add.1) { + v.push(net); + } +} + +fn push_ipv6(v: &mut Vec, add: (Ipv6Addr, u8)) { + if v.iter() + .any(|n| n.addr() == add.0 && n.prefix_len() == add.1) + { + return; + } + if let Ok(net) = Ipv6Net::new(add.0, add.1) { + v.push(net); + } +} + +#[inline] +fn calc_v6_scope_id(addr: &Ipv6Addr, ifindex: u32) -> u32 { + let seg0 = addr.segments()[0]; + if (seg0 & 0xffc0) == 0xfe80 { + ifindex + } else { + 0 + } +} + +pub fn interfaces() -> Vec { + let mut ifaces = Vec::new(); + + #[cfg(feature = "gateway")] + let local_ip_opt: Option = get_local_ipaddr(); + + // Fill ifaces via netlink first + // If netlink fails, fallback to unix_interfaces + match netlink::collect_interfaces() { + Ok(rows) => { + for r in rows { + let name = r.name.clone(); + let mut iface = Interface { + index: r.index, + name: name.clone(), + friendly_name: None, + description: None, + if_type: super::sysfs::get_interface_type(&name), + mac_addr: r.mac.map(MacAddr::from_octets), + ipv4: Vec::new(), + ipv6: Vec::new(), + ipv6_scope_ids: Vec::new(), + flags: r.flags, + oper_state: OperState::from_if_flags(r.flags), + transmit_speed: None, + receive_speed: None, + stats: None, + #[cfg(feature = "gateway")] + gateway: None, + #[cfg(feature = "gateway")] + dns_servers: Vec::new(), + mtu: r.mtu, + #[cfg(feature = "gateway")] + default: false, + }; + + for (a, p) in r.ipv4 { + push_ipv4(&mut iface.ipv4, (a, p)); + } + for (a, p) in r.ipv6 { + push_ipv6(&mut iface.ipv6, (a, p)); + iface.ipv6_scope_ids.push(calc_v6_scope_id(&a, iface.index)); + } + + ifaces.push(iface); + } + } + Err(_) => { + // Fallback: unix ifaddrs + ifaces = unix_interfaces(); + } + } + + // Fill gateway info if feature enabled + #[cfg(feature = "gateway")] + match netlink::collect_routes() { + Ok(gmap) => { + let by_index: HashMap = + gmap.iter().map(|(k, v)| (*k, v)).collect(); + for iface in &mut ifaces { + if iface.index == 0 { + continue; + } + if let Some(row) = by_index.get(&iface.index) { + let dev = NetworkDevice { + mac_addr: row + .mac + .map(|m| MacAddr::from_octets(m)) + .unwrap_or(MacAddr::zero()), + ipv4: row.gw_v4.clone(), + ipv6: row.gw_v6.clone(), + }; + iface.gateway = Some(dev); + } + } + } + Err(_) => { + // Fallback: procfs + let gateway_map: HashMap = super::procfs::get_gateway_map(); + for iface in &mut ifaces { + if let Some(gateway) = gateway_map.get(&iface.name) { + iface.gateway = Some(gateway.clone()); + } + } + } + } + + // Fill other info + for iface in &mut ifaces { + iface.if_type = super::sysfs::get_interface_type(&iface.name); + let if_speed = super::sysfs::get_interface_speed(&iface.name); + iface.transmit_speed = if_speed; + iface.receive_speed = if_speed; + iface.oper_state = super::sysfs::operstate(&iface.name); + + if iface.stats.is_none() { + iface.stats = crate::stats::counters::get_stats_from_name(&iface.name); + } + + if iface.mtu.is_none() { + iface.mtu = super::mtu::get_mtu(&iface.name); + } + + #[cfg(feature = "gateway")] + { + if let Some(local_ip) = local_ip_opt { + match local_ip { + IpAddr::V4(local_ipv4) => { + if iface.ipv4.iter().any(|x| x.addr() == local_ipv4) { + iface.default = true; + iface.dns_servers = get_system_dns_conf(); + } + } + IpAddr::V6(local_ipv6) => { + if iface.ipv6.iter().any(|x| x.addr() == local_ipv6) { + iface.default = true; + iface.dns_servers = get_system_dns_conf(); + } + } + } + } + } + } + ifaces +} diff --git a/src/os/linux/mod.rs b/src/os/linux/mod.rs new file mode 100644 index 0000000..e1b0499 --- /dev/null +++ b/src/os/linux/mod.rs @@ -0,0 +1,11 @@ +pub mod arp; +pub mod flags; +#[cfg(not(target_os = "android"))] +pub mod interface; +pub mod mtu; +#[cfg(not(target_os = "android"))] +pub mod netlink; +#[cfg(feature = "gateway")] +pub mod procfs; +pub mod state; +pub mod sysfs; diff --git a/src/os/linux/mtu.rs b/src/os/linux/mtu.rs new file mode 100644 index 0000000..4d6326e --- /dev/null +++ b/src/os/linux/mtu.rs @@ -0,0 +1,56 @@ +use libc::{AF_INET, SIOCGIFMTU, SOCK_DGRAM, c_char, c_int, close, ifreq, ioctl, socket}; +use std::ffi::CString; +use std::mem; +use std::os::unix::io::RawFd; +use std::ptr; + +pub(crate) fn get_mtu(name: &str) -> Option { + // Create a socket for ioctl operations + let sock: RawFd = unsafe { socket(AF_INET, SOCK_DGRAM, 0) }; + if sock < 0 { + eprintln!( + "Failed to create socket: {:?}", + std::io::Error::last_os_error() + ); + return None; + } + + let mut ifr: ifreq = unsafe { mem::zeroed() }; + + // Set the interface name (must not exceed `IFNAMSIZ`) + let c_interface = CString::new(name).ok()?; + // Ensure null termination + let bytes = c_interface.to_bytes_with_nul(); + if bytes.len() > ifr.ifr_name.len() { + eprintln!("Interface name too long: {}", name); + unsafe { close(sock) }; + return None; + } + + unsafe { + ptr::copy_nonoverlapping( + bytes.as_ptr() as *const c_char, + ifr.ifr_name.as_mut_ptr(), + bytes.len(), + ); + } + + // Retrieve the MTU using ioctl + let ret: c_int = unsafe { ioctl(sock, SIOCGIFMTU as _, &mut ifr) }; + if ret < 0 { + eprintln!( + "ioctl(SIOCGIFMTU) failed for {}: {:?}", + name, + std::io::Error::last_os_error() + ); + unsafe { close(sock) }; + return None; + } + + let mtu = unsafe { ifr.ifr_ifru.ifru_mtu } as u32; + + // Close the socket + unsafe { close(sock) }; + + Some(mtu) +} diff --git a/src/os/linux/netlink.rs b/src/os/linux/netlink.rs new file mode 100644 index 0000000..65fd6a1 --- /dev/null +++ b/src/os/linux/netlink.rs @@ -0,0 +1,449 @@ +use netlink_packet_core::{NLM_F_DUMP, NLM_F_REQUEST, NetlinkMessage, NetlinkPayload}; +use netlink_packet_route::{ + RouteNetlinkMessage, + address::{AddressAttribute, AddressMessage}, + link::{LinkAttribute, LinkMessage}, +}; +use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_ROUTE}; +use std::io::ErrorKind; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::{ + collections::HashMap, + io, thread, + time::{Duration, Instant}, +}; + +#[cfg(feature = "gateway")] +use netlink_packet_route::AddressFamily; +#[cfg(feature = "gateway")] +use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourMessage}; +#[cfg(feature = "gateway")] +use netlink_packet_route::route::{RouteAddress, RouteAttribute, RouteMessage}; + +const SEQ_BASE: u32 = 0x6E_64_65_76; // "ndev" +const RECV_BUFSZ: usize = 1 << 20; // 1MB +const RECV_TIMEOUT: Duration = Duration::from_secs(2); +const NLMSG_ALIGNTO: usize = 4; +const MIN_NLMSG_HEADER_LEN: usize = 16; + +#[inline] +fn nlmsg_align(n: usize) -> usize { + (n + NLMSG_ALIGNTO - 1) & !(NLMSG_ALIGNTO - 1) +} + +fn open_route_socket() -> io::Result { + let mut sock = Socket::new(NETLINK_ROUTE) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("netlink open: {e}")))?; + sock.bind_auto() + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bind_auto: {e}")))?; + sock.set_non_blocking(true).ok(); + Ok(sock) +} + +fn send_dump(sock: &mut Socket, msg: RouteNetlinkMessage, seq: u32) -> io::Result<()> { + let mut nl = NetlinkMessage::from(msg); + nl.header.flags = NLM_F_REQUEST | NLM_F_DUMP; + nl.header.sequence_number = seq; + nl.header.port_number = 0; + + // Finalize to set length + nl.finalize(); + + let blen = nl.buffer_len(); + if blen < MIN_NLMSG_HEADER_LEN { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("netlink message too short: buffer_len={}", blen), + )); + } + + let mut buf = vec![0; blen]; + nl.serialize(&mut buf); + + let kernel = SocketAddr::new(0, 0); + sock.send_to(&buf, &kernel, 0) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("netlink send: {e}")))?; + Ok(()) +} + +fn recv_multi( + sock: &mut Socket, + expect_seq: u32, +) -> io::Result>> { + let mut out = Vec::new(); + let mut buf = vec![0u8; RECV_BUFSZ]; + let kernel = SocketAddr::new(0, 0); + let deadline = Instant::now() + RECV_TIMEOUT; + + loop { + match sock.recv_from(&mut &mut buf[..], 0) { + Ok((size, from)) => { + let _ = from == kernel; + let mut offset = 0usize; + + while offset < size { + if size - offset < MIN_NLMSG_HEADER_LEN { + break; + } + + let bytes = &buf[offset..size]; + + let msg = + NetlinkMessage::::deserialize(bytes).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("deserialize: {e:?}"), + ) + })?; + + let consumed = msg.header.length as usize; + if consumed < MIN_NLMSG_HEADER_LEN || offset + consumed > size { + break; + } + + if msg.header.sequence_number != expect_seq { + offset += nlmsg_align(consumed); + continue; + } + + match &msg.payload { + NetlinkPayload::Done(_) => { + return Ok(out); + } + NetlinkPayload::Error(e) => { + if let Some(code) = e.code { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("netlink error: code={}", code), + )); + } + // code==None: possibly ACK ... ignore + } + NetlinkPayload::Noop | NetlinkPayload::Overrun(_) => { /* skip */ } + _ => out.push(msg), + } + + // Align to 4-byte boundary + offset += nlmsg_align(consumed); + } + } + Err(e) if e.kind() == ErrorKind::WouldBlock => { + if Instant::now() >= deadline { + // timeout + return Ok(out); + } + thread::sleep(Duration::from_millis(5)); + } + Err(e) => return Err(e), + } + } +} + +pub fn dump_links() -> io::Result> { + let mut sock = open_route_socket()?; + let seq = SEQ_BASE ^ 0x01; + send_dump( + &mut sock, + RouteNetlinkMessage::GetLink(LinkMessage::default()), + seq, + )?; + let msgs = recv_multi(&mut sock, seq)?; + let mut out = Vec::new(); + for m in msgs { + if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewLink(link)) = m.payload { + out.push(link); + } + } + Ok(out) +} + +pub fn dump_addrs() -> io::Result> { + let mut sock = open_route_socket()?; + let seq = SEQ_BASE ^ 0x02; + send_dump( + &mut sock, + RouteNetlinkMessage::GetAddress(AddressMessage::default()), + seq, + )?; + let msgs = recv_multi(&mut sock, seq)?; + let mut out = Vec::new(); + for m in msgs { + if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewAddress(addr)) = m.payload { + out.push(addr); + } + } + Ok(out) +} + +#[cfg(feature = "gateway")] +pub fn dump_routes() -> io::Result> { + let mut sock = open_route_socket()?; + let seq = SEQ_BASE ^ 0x03; + send_dump( + &mut sock, + RouteNetlinkMessage::GetRoute(RouteMessage::default()), + seq, + )?; + let msgs = recv_multi(&mut sock, seq)?; + let mut out = Vec::new(); + for m in msgs { + if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewRoute(rt)) = m.payload { + out.push(rt); + } + } + Ok(out) +} + +#[cfg(feature = "gateway")] +pub fn dump_neigh() -> io::Result> { + let mut sock = open_route_socket()?; + let seq = SEQ_BASE ^ 0x04; + send_dump( + &mut sock, + RouteNetlinkMessage::GetNeighbour(NeighbourMessage::default()), + seq, + )?; + let msgs = recv_multi(&mut sock, seq)?; + let mut out = Vec::new(); + for m in msgs { + if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewNeighbour(n)) = m.payload { + out.push(n); + } + } + Ok(out) +} + +fn mac_from_link(link: &LinkMessage) -> Option<[u8; 6]> { + for nla in &link.attributes { + if let LinkAttribute::Address(bytes) = nla { + if bytes.len() == 6 { + return Some([bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]]); + } + } + } + None +} + +fn name_from_link(link: &LinkMessage) -> Option { + for nla in &link.attributes { + if let LinkAttribute::IfName(n) = nla { + return Some(n.clone()); + } + } + None +} + +fn ip_from_addr(addr: &AddressMessage) -> Option<(IpAddr, u8)> { + let pfx = addr.header.prefix_len; + for nla in &addr.attributes { + match nla { + AddressAttribute::Local(ip) | AddressAttribute::Address(ip) => { + return Some((*ip, pfx)); + } + _ => {} + } + } + None +} + +#[cfg(feature = "gateway")] +fn route_addr_to_ip(a: &RouteAddress) -> Option { + match a { + RouteAddress::Inet(v4) => Some(IpAddr::V4(*v4)), + RouteAddress::Inet6(v6) => Some(IpAddr::V6(*v6)), + _ => None, + } +} + +#[cfg(feature = "gateway")] +fn route_extract(rt: &RouteMessage) -> (Option, Option, Option, Option) { + // (dst, prefix, gateway, oif) + let mut dst: Option = None; + let pfx: Option = Some(rt.header.destination_prefix_length); + let mut gw: Option = None; + let mut oif: Option = None; + + for nla in &rt.attributes { + match nla { + RouteAttribute::Destination(a) => dst = route_addr_to_ip(a), + RouteAttribute::Gateway(a) => gw = route_addr_to_ip(a), + RouteAttribute::Oif(i) => oif = Some(*i), + _ => {} + } + } + + // if dst is None and pfx is 0, it means default route + if dst.is_none() && pfx == Some(0) { + dst = match rt.header.address_family { + AddressFamily::Inet => Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED)), + AddressFamily::Inet6 => Some(IpAddr::V6(Ipv6Addr::UNSPECIFIED)), + _ => None, + }; + } + + (dst, pfx, gw, oif) +} + +#[cfg(feature = "gateway")] +fn neigh_addr_to_ip(a: &NeighbourAddress) -> Option { + match a { + NeighbourAddress::Inet(v4) => Some(IpAddr::V4(*v4)), + NeighbourAddress::Inet6(v6) => Some(IpAddr::V6(*v6)), + #[allow(unreachable_patterns)] + _ => None, + } +} + +#[cfg(feature = "gateway")] +fn neigh_extract(n: &NeighbourMessage) -> (Option, Option<[u8; 6]>, Option) { + let mut ip = None; + let mut mac = None; + let mut oif = Some(n.header.ifindex as u32); + + for nla in &n.attributes { + match nla { + NeighbourAttribute::Destination(a) => { + ip = neigh_addr_to_ip(a); + } + // Link-layer address (MAC) + NeighbourAttribute::LinkLocalAddress(v) => { + if v.len() == 6 { + mac = Some([v[0], v[1], v[2], v[3], v[4], v[5]]); + } + } + NeighbourAttribute::IfIndex(i) => oif = Some(*i as u32), + _ => {} + } + } + (ip, mac, oif) +} + +fn mtu_from_link(link: &LinkMessage) -> Option { + for nla in &link.attributes { + if let LinkAttribute::Mtu(m) = nla { + return Some(*m); + } + } + None +} + +#[derive(Debug, Clone)] +pub struct IfRow { + pub index: u32, + pub name: String, + pub mac: Option<[u8; 6]>, + pub ipv4: Vec<(Ipv4Addr, u8)>, + pub ipv6: Vec<(Ipv6Addr, u8)>, + pub flags: u32, + pub mtu: Option, +} + +pub fn collect_interfaces() -> io::Result> { + let links = dump_links()?; + let addrs = dump_addrs()?; + + let mut base: HashMap = HashMap::new(); + for l in links { + let idx = l.header.index as u32; + let name = name_from_link(&l).unwrap_or_else(|| idx.to_string()); + let mac = mac_from_link(&l); + let flags = l.header.flags.bits(); + let mtu_nl = mtu_from_link(&l); + base.insert( + idx, + IfRow { + index: idx, + name, + mac, + ipv4: vec![], + ipv6: vec![], + flags, + mtu: mtu_nl, + }, + ); + } + + for a in addrs { + let idx = a.header.index as u32; + if let Some((ip, pfx)) = ip_from_addr(&a) { + if let Some(row) = base.get_mut(&idx) { + match ip { + IpAddr::V4(v4) => row.ipv4.push((v4, pfx)), + IpAddr::V6(v6) => row.ipv6.push((v6, pfx)), + } + } + } + } + + Ok(base.into_values().collect()) +} + +#[cfg(feature = "gateway")] +#[derive(Debug, Clone)] +pub struct GwRow { + #[allow(dead_code)] + pub ifindex: u32, + pub gw_v4: Vec, + pub gw_v6: Vec, + pub mac: Option<[u8; 6]>, +} + +#[cfg(feature = "gateway")] +pub fn collect_routes() -> io::Result> { + let routes = dump_routes()?; + let neighs = dump_neigh().unwrap_or_default(); + + let mut m: HashMap = HashMap::new(); + for rt in routes { + let (_dst, pfx, gw, oif) = route_extract(&rt); + // default route only + if pfx != Some(0) { + continue; + } + let oif = match oif { + Some(i) => i, + None => continue, + }; + if let Some(gwip) = gw { + let e = m.entry(oif).or_insert(GwRow { + ifindex: oif, + gw_v4: vec![], + gw_v6: vec![], + mac: None, + }); + match gwip { + IpAddr::V4(v4) => { + if !e.gw_v4.contains(&v4) { + e.gw_v4.push(v4); + } + } + IpAddr::V6(v6) => { + if !e.gw_v6.contains(&v6) { + e.gw_v6.push(v6); + } + } + } + } + } + + for n in neighs { + let (ip, mac, ifi) = neigh_extract(&n); + let ifi = match ifi { + Some(i) => i, + None => continue, + }; + if let Some(row) = m.get_mut(&ifi) { + if let (Some(m6), Some(ip)) = (mac, ip) { + let hit = match ip { + IpAddr::V4(v4) => row.gw_v4.contains(&v4), + IpAddr::V6(v6) => row.gw_v6.contains(&v6), + }; + if hit { + row.mac = Some(m6); + } + } + } + } + + Ok(m) +} diff --git a/src/gateway/linux.rs b/src/os/linux/procfs.rs similarity index 98% rename from src/gateway/linux.rs rename to src/os/linux/procfs.rs index 34543b9..290b089 100644 --- a/src/gateway/linux.rs +++ b/src/os/linux/procfs.rs @@ -1,5 +1,5 @@ -use crate::device::NetworkDevice; -use crate::mac::MacAddr; +use crate::net::device::NetworkDevice; +use crate::net::mac::MacAddr; use std::collections::HashMap; use std::fs::read_to_string; use std::net::{Ipv4Addr, Ipv6Addr}; diff --git a/src/os/linux/state.rs b/src/os/linux/state.rs new file mode 100644 index 0000000..bb9be0a --- /dev/null +++ b/src/os/linux/state.rs @@ -0,0 +1 @@ +pub(crate) use super::sysfs::operstate; diff --git a/src/os/linux/sysfs.rs b/src/os/linux/sysfs.rs new file mode 100644 index 0000000..3cc06b7 --- /dev/null +++ b/src/os/linux/sysfs.rs @@ -0,0 +1,89 @@ +use crate::interface::state::OperState; +use crate::interface::types::InterfaceType; +use std::convert::TryFrom; +use std::fs; +use std::path::{Path, PathBuf}; + +fn read_trimmed(path: impl AsRef) -> Option { + fs::read_to_string(path).ok().map(|s| s.trim().to_owned()) +} + +fn exists(path: impl AsRef) -> bool { + Path::new(path.as_ref()).exists() +} + +/// Check if the interface is a Wi-Fi (802.11) interface. +fn is_wifi_interface(ifname: &str) -> bool { + let base = PathBuf::from("/sys/class/net").join(ifname); + + // 1) Check uevent file for DEVTYPE=wlan + if let Some(ue) = read_trimmed(base.join("uevent")) { + if ue.lines().any(|l| l.trim() == "DEVTYPE=wlan") { + return true; + } + } + + // 2) Check for wireless or phy80211 directories + exists(base.join("wireless")) || exists(base.join("phy80211")) +} + +/// Check if the interface is a virtual interface. +pub fn is_virtual_interface(ifname: &str) -> bool { + let dev_link = PathBuf::from("/sys/class/net").join(ifname).join("device"); + + // If device symlink is missing, it's likely virtual + if !dev_link.exists() { + return true; + } + + // Follow the symlink to see if it points to /sys/devices/virtual/ + // If we can't resolve the path, be conservative and say it's not virtual + match dev_link.canonicalize() { + Ok(real) => real.starts_with("/sys/devices/virtual"), + Err(_) => false, + } +} + +/// Get the interface type. +pub fn get_interface_type(ifname: &str) -> InterfaceType { + // Check for Wi-Fi first + // Since some Wi-Fi interfaces may also be reported as Ethernet, + // we prioritize Wi-Fi detection. + if is_wifi_interface(ifname) { + return InterfaceType::Wireless80211; + } + // Read the type from sysfs + let p = PathBuf::from("/sys/class/net").join(ifname).join("type"); + let ty = match read_trimmed(&p).and_then(|s| s.parse::().ok()) { + Some(v) => v, + None => return InterfaceType::Unknown, + }; + + if ty == super::arp::ARPHRD_ETHER { + InterfaceType::Ethernet + } else { + InterfaceType::try_from(ty).unwrap_or(InterfaceType::Unknown) + } +} + +/// Get the interface speed in bps (bits per second). +pub fn get_interface_speed(ifname: &str) -> Option { + let p = PathBuf::from("/sys/class/net").join(ifname).join("speed"); + let s = read_trimmed(p)?; + let mbps: i64 = s.parse().ok()?; + if mbps <= 0 { + return None; + } + // Convert Mbps to bps + Some((mbps as u64) * 1_000_000) +} + +/// Get the operational state of the interface. +pub fn operstate(ifname: &str) -> OperState { + let p = PathBuf::from("/sys/class/net") + .join(ifname) + .join("operstate"); + read_trimmed(p) + .and_then(|s| s.parse::().ok()) + .unwrap_or(OperState::Unknown) +} diff --git a/src/os/macos/interface.rs b/src/os/macos/interface.rs new file mode 100644 index 0000000..cd6876e --- /dev/null +++ b/src/os/macos/interface.rs @@ -0,0 +1,58 @@ +#[cfg(feature = "gateway")] +use std::net::IpAddr; + +use crate::{ + interface::{interface::Interface, types::InterfaceType}, + os::unix::interface::unix_interfaces, +}; + +#[derive(Debug)] +pub struct SCInterface { + #[allow(dead_code)] + pub name: String, + pub friendly_name: Option, + pub interface_type: InterfaceType, +} + +pub fn interfaces() -> Vec { + let type_map = super::types::get_if_type_map(); + let mut interfaces: Vec = unix_interfaces(); + + #[cfg(feature = "gateway")] + let local_ip_opt: Option = crate::net::ip::get_local_ipaddr(); + + #[cfg(feature = "gateway")] + let gateway_map = crate::os::darwin::route::get_gateway_map(); + + for iface in &mut interfaces { + if let Some(sc_interface) = type_map.get(&iface.name) { + iface.if_type = sc_interface.interface_type; + iface.friendly_name = sc_interface.friendly_name.clone(); + } + + #[cfg(feature = "gateway")] + { + use crate::os::unix::dns::get_system_dns_conf; + if let Some(gateway) = gateway_map.get(&iface.index) { + iface.gateway = Some(gateway.clone()); + } + + if let Some(local_ip) = local_ip_opt { + iface.ipv4.iter().for_each(|ipv4| { + if IpAddr::V4(ipv4.addr()) == local_ip { + iface.dns_servers = get_system_dns_conf(); + iface.default = true; + } + }); + iface.ipv6.iter().for_each(|ipv6| { + if IpAddr::V6(ipv6.addr()) == local_ip { + iface.dns_servers = get_system_dns_conf(); + iface.default = true; + } + }); + } + } + } + + interfaces +} diff --git a/src/os/macos/mod.rs b/src/os/macos/mod.rs new file mode 100644 index 0000000..09a6797 --- /dev/null +++ b/src/os/macos/mod.rs @@ -0,0 +1,2 @@ +pub mod interface; +pub mod types; diff --git a/src/interface/macos.rs b/src/os/macos/types.rs similarity index 87% rename from src/interface/macos.rs rename to src/os/macos/types.rs index 47a4353..1ffa56c 100644 --- a/src/interface/macos.rs +++ b/src/os/macos/types.rs @@ -1,4 +1,4 @@ -use crate::interface::InterfaceType; +use crate::{interface::types::InterfaceType, os::macos::interface::SCInterface}; use std::collections::HashMap; use system_configuration::network_configuration; @@ -14,14 +14,6 @@ fn get_if_type_from_id(type_id: String) -> InterfaceType { } } -#[derive(Debug)] -pub struct SCInterface { - #[allow(dead_code)] - pub name: String, - pub friendly_name: Option, - pub interface_type: InterfaceType, -} - pub fn get_if_type_map() -> HashMap { let mut map: HashMap = HashMap::new(); let interfaces = network_configuration::get_interfaces(); diff --git a/src/os/mod.rs b/src/os/mod.rs new file mode 100644 index 0000000..78d2fb9 --- /dev/null +++ b/src/os/mod.rs @@ -0,0 +1,24 @@ +#[cfg(target_family = "unix")] +pub mod unix; + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub mod linux; + +#[cfg(target_os = "android")] +pub mod android; + +#[cfg(target_vendor = "apple")] +pub mod darwin; + +#[cfg(target_os = "macos")] +pub mod macos; + +//#[cfg(target_os = "ios")] +#[cfg(all(target_vendor = "apple", not(target_os = "macos")))] +pub mod ios; + +#[cfg(target_os = "windows")] +pub mod windows; + +#[cfg(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd"))] +pub mod bsd; diff --git a/src/os/unix/dns.rs b/src/os/unix/dns.rs new file mode 100644 index 0000000..5bf7601 --- /dev/null +++ b/src/os/unix/dns.rs @@ -0,0 +1,33 @@ +use std::fs::read_to_string; +use std::net::IpAddr; +use std::net::ToSocketAddrs; + +pub fn get_system_dns_conf() -> Vec { + const PATH_RESOLV_CONF: &str = "/etc/resolv.conf"; + let r = read_to_string(PATH_RESOLV_CONF); + match r { + Ok(content) => { + let conf_lines: Vec<&str> = content.trim().split('\n').collect(); + let mut dns_servers = Vec::new(); + for line in conf_lines { + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() >= 2 { + // field [0]: Configuration type (e.g., "nameserver", "domain", "search") + // field [1]: Corresponding value (e.g., IP address, domain name) + if fields[0] == "nameserver" { + let sock_addr = format!("{}:53", fields[1]); + if let Ok(mut addrs) = sock_addr.to_socket_addrs() { + if let Some(addr) = addrs.next() { + dns_servers.push(addr.ip()); + } + } else { + eprintln!("Invalid IP address format: {}", fields[1]); + } + } + } + } + dns_servers + } + Err(_) => Vec::new(), + } +} diff --git a/src/os/unix/flags.rs b/src/os/unix/flags.rs new file mode 100644 index 0000000..0f1e37f --- /dev/null +++ b/src/os/unix/flags.rs @@ -0,0 +1,7 @@ +pub use libc::{IFF_BROADCAST, IFF_LOOPBACK, IFF_MULTICAST, IFF_POINTOPOINT, IFF_RUNNING, IFF_UP}; + +use crate::interface::interface::Interface; + +pub fn is_running(interface: &Interface) -> bool { + interface.flags & (IFF_RUNNING as u32) != 0 +} diff --git a/src/os/unix/interface.rs b/src/os/unix/interface.rs new file mode 100644 index 0000000..589a388 --- /dev/null +++ b/src/os/unix/interface.rs @@ -0,0 +1,185 @@ +use std::ffi::{CStr, CString}; +use std::mem::MaybeUninit; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::os::raw::c_char; +use std::str::from_utf8_unchecked; + +use super::sockaddr::{SockaddrRef, compute_sockaddr_len, netmask_ip_autolen, try_mac_from_raw}; +use crate::interface::interface::Interface; +use crate::interface::mtu::get_mtu; +use crate::interface::state::OperState; +use crate::ipnet::{Ipv4Net, Ipv6Net}; +use crate::os::unix::types::get_interface_type; +use crate::stats::counters::{InterfaceStats, get_stats}; + +#[cfg(target_os = "android")] +pub fn unix_interfaces() -> Vec { + use crate::os::android; + + if let Some((getifaddrs, freeifaddrs)) = android::get_libc_ifaddrs() { + return unix_interfaces_inner(getifaddrs, freeifaddrs); + } + Vec::new() +} + +#[cfg(not(target_os = "android"))] +pub fn unix_interfaces() -> Vec { + unix_interfaces_inner(libc::getifaddrs, libc::freeifaddrs) +} + +fn unix_interfaces_inner( + getifaddrs: unsafe extern "C" fn(*mut *mut libc::ifaddrs) -> libc::c_int, + freeifaddrs: unsafe extern "C" fn(*mut libc::ifaddrs), +) -> Vec { + let mut ifaces: Vec = vec![]; + let mut addrs: MaybeUninit<*mut libc::ifaddrs> = MaybeUninit::uninit(); + if unsafe { getifaddrs(addrs.as_mut_ptr()) } != 0 { + return ifaces; + } + let addrs = unsafe { addrs.assume_init() }; + let mut addr = addrs; + while !addr.is_null() { + let addr_ref: &libc::ifaddrs = unsafe { &*addr }; + let if_type = get_interface_type(addr_ref); + let c_str = addr_ref.ifa_name as *const c_char; + let bytes = unsafe { CStr::from_ptr(c_str).to_bytes() }; + let name: String = unsafe { from_utf8_unchecked(bytes).to_owned() }; + let cap: libc::socklen_t = super::sockaddr::sockaddr_storage_cap(); + let addr_len_opt = unsafe { compute_sockaddr_len(addr_ref.ifa_addr, None, Some(cap)) }; + let (mac, ip, ipv6_scope_id) = match addr_len_opt { + Some(addr_len) => { + let mac = unsafe { + try_mac_from_raw(addr_ref.ifa_addr as *const libc::sockaddr, addr_len) + }; + let (ip, scope) = unsafe { + match SockaddrRef::from_raw( + addr_ref.ifa_addr as *const libc::sockaddr, + addr_len, + ) { + Some(sa_ref) => { + let ip = sa_ref.to_ip(); + let scope = sa_ref.to_ipv6_scope(); + (Some(ip), scope) + } + None => (None, None), + } + }; + + (mac, ip, scope) + } + None => (None, None, None), + }; + let netmask: Option = + unsafe { netmask_ip_autolen(addr_ref.ifa_netmask as *const libc::sockaddr) }; + let stats: Option = get_stats(Some(addr_ref), &name); + let mut ini_ipv4: Option = None; + let mut ini_ipv6: Option = None; + if let Some(ip) = ip { + match ip { + IpAddr::V4(ipv4) => { + let netmask: Ipv4Addr = match netmask { + Some(netmask) => match netmask { + IpAddr::V4(netmask) => netmask, + IpAddr::V6(_) => Ipv4Addr::UNSPECIFIED, + }, + None => Ipv4Addr::UNSPECIFIED, + }; + match Ipv4Net::with_netmask(ipv4, netmask) { + Ok(ipv4_net) => ini_ipv4 = Some(ipv4_net), + Err(_) => {} + } + } + IpAddr::V6(ipv6) => { + let netmask: Ipv6Addr = match netmask { + Some(netmask) => match netmask { + IpAddr::V4(_) => Ipv6Addr::UNSPECIFIED, + IpAddr::V6(netmask) => netmask, + }, + None => Ipv6Addr::UNSPECIFIED, + }; + match Ipv6Net::with_netmask(ipv6, netmask) { + Ok(ipv6_net) => { + ini_ipv6 = Some(ipv6_net); + if ipv6_scope_id.is_none() { + panic!("IPv6 address without scope ID!") + } + } + Err(_) => {} + }; + } + } + } + + // Check if there is already an interface with this name (since getifaddrs returns one + // entry per address, so if the interface has multiple addresses, it returns multiple entries). + // If so, add the IP addresses from the current entry into the existing interface. Otherwise, add a new interface. + let mut found: bool = false; + for iface in &mut ifaces { + if name == iface.name { + if let Some(mac) = mac.clone() { + iface.mac_addr = Some(mac); + } + + if iface.stats.is_none() { + iface.stats = stats.clone(); + } + + if ini_ipv4.is_some() { + iface.ipv4.push(ini_ipv4.unwrap()); + } + + if ini_ipv6.is_some() { + iface.ipv6.push(ini_ipv6.unwrap()); + iface.ipv6_scope_ids.push(ipv6_scope_id.unwrap()); + } + found = true; + } + } + if !found { + let interface: Interface = Interface { + index: 0, // We will set these below + name: name.clone(), + friendly_name: None, + description: None, + if_type: if_type, + mac_addr: mac.clone(), + ipv4: match ini_ipv4 { + Some(ipv4_addr) => vec![ipv4_addr], + None => vec![], + }, + ipv6: match ini_ipv6 { + Some(ipv6_addr) => vec![ipv6_addr], + None => vec![], + }, + ipv6_scope_ids: match ini_ipv6 { + Some(_) => vec![ipv6_scope_id.unwrap()], + None => vec![], + }, + flags: addr_ref.ifa_flags, + oper_state: OperState::from_if_flags(addr_ref.ifa_flags), + transmit_speed: None, + receive_speed: None, + stats: stats, + #[cfg(feature = "gateway")] + gateway: None, + #[cfg(feature = "gateway")] + dns_servers: Vec::new(), + mtu: get_mtu(addr_ref, &name), + #[cfg(feature = "gateway")] + default: false, + }; + ifaces.push(interface); + } + addr = addr_ref.ifa_next; + } + unsafe { + freeifaddrs(addrs); + } + for iface in &mut ifaces { + let name = CString::new(iface.name.as_bytes()).unwrap(); + unsafe { + iface.index = libc::if_nametoindex(name.as_ptr()); + } + } + ifaces +} diff --git a/src/os/unix/mod.rs b/src/os/unix/mod.rs new file mode 100644 index 0000000..b26c222 --- /dev/null +++ b/src/os/unix/mod.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "gateway")] +pub mod dns; +pub mod flags; +pub mod interface; +pub mod sockaddr; +pub mod types; diff --git a/src/os/unix/sockaddr.rs b/src/os/unix/sockaddr.rs new file mode 100644 index 0000000..52d0641 --- /dev/null +++ b/src/os/unix/sockaddr.rs @@ -0,0 +1,542 @@ +use crate::net::mac::MacAddr; +use core::mem; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; + +pub(crate) enum SockaddrRef<'a> { + V4(&'a libc::sockaddr_in), + V6(&'a libc::sockaddr_in6), +} + +impl<'a> SockaddrRef<'a> { + /// # Safety + /// - `sa` must point to a valid, readable memory region of at least `len` bytes. + /// - `sa` must point to the beginning of a `sockaddr`-compatible structure + pub(crate) unsafe fn from_raw(sa: *const libc::sockaddr, len: libc::socklen_t) -> Option { + if sa.is_null() || len < mem::size_of::() as libc::socklen_t { + return None; + } + let family = unsafe { (*sa).sa_family as libc::c_int }; + + match family { + libc::AF_INET => { + if len as usize >= mem::size_of::() { + let sa: &libc::sockaddr_in = unsafe { &*(sa as *const libc::sockaddr_in) }; + Some(SockaddrRef::V4(sa)) + } else { + None + } + } + libc::AF_INET6 => { + if len as usize >= mem::size_of::() { + let sa: &libc::sockaddr_in6 = unsafe { &*(sa as *const libc::sockaddr_in6) }; + Some(SockaddrRef::V6(sa)) + } else { + None + } + } + _ => None, + } + } + + #[allow(dead_code)] + #[inline] + pub(crate) fn family(&self) -> libc::c_int { + match self { + SockaddrRef::V4(_) => libc::AF_INET, + SockaddrRef::V6(_) => libc::AF_INET6, + } + } + + #[inline] + pub(crate) fn to_ip(&self) -> IpAddr { + match self { + SockaddrRef::V4(sin) => { + let addr = u32::from_be((*sin).sin_addr.s_addr as u32); + IpAddr::V4(Ipv4Addr::from(addr)) + } + SockaddrRef::V6(sin6) => { + let bytes = (*sin6).sin6_addr.s6_addr; + IpAddr::V6(Ipv6Addr::from(bytes)) + } + } + } + + #[inline] + pub(crate) fn to_ipv6_scope(&self) -> Option { + match self { + SockaddrRef::V4(_) => None, + SockaddrRef::V6(sin6) => { + let scope = (*sin6).sin6_scope_id as u32; + Some(scope) + } + } + } + + #[allow(dead_code)] + #[inline] + pub(crate) fn port(&self) -> u16 { + match self { + SockaddrRef::V4(sin) => u16::from_be((*sin).sin_port), + SockaddrRef::V6(sin6) => u16::from_be((*sin6).sin6_port), + } + } + + #[allow(dead_code)] + #[inline] + pub(crate) fn to_socket_addr(&self) -> SocketAddr { + let ip = self.to_ip(); + let port = self.port(); + match self { + SockaddrRef::V4(_) => SocketAddr::V4(SocketAddrV4::new( + match ip { + IpAddr::V4(v4) => v4, + _ => unreachable!(), + }, + port, + )), + SockaddrRef::V6(_) => { + let scope = self.to_ipv6_scope().unwrap_or(0); + let port = self.port(); + SocketAddr::V6(SocketAddrV6::new( + match ip { + IpAddr::V6(v6) => v6, + _ => unreachable!(), + }, + port, + 0, + scope, + )) + } + } + } + + #[allow(dead_code)] + #[inline] + pub(crate) fn netmask_ip(&self) -> IpAddr { + match self { + SockaddrRef::V4(sin) => { + let m = u32::from_be((*sin).sin_addr.s_addr as u32); + IpAddr::V4(Ipv4Addr::from(m)) + } + SockaddrRef::V6(sin6) => { + let bytes = (*sin6).sin6_addr.s6_addr; + IpAddr::V6(Ipv6Addr::from(bytes)) + } + } + } + + #[allow(dead_code)] + #[inline] + pub(crate) fn netmask_prefix(&self) -> Option { + match self { + SockaddrRef::V4(sin) => { + let m = u32::from_be((*sin).sin_addr.s_addr as u32); + mask_to_prefix_u32(m) + } + SockaddrRef::V6(sin6) => { + let bytes = (*sin6).sin6_addr.s6_addr; + mask_to_prefix_128(&bytes) + } + } + } +} + +#[inline] +fn mask_to_prefix_u32(m: u32) -> Option { + if m == 0 { + return Some(0); + } + let leading = m.leading_ones(); + if (m << leading) == 0 { + Some(leading as u8) + } else { + None + } +} + +#[inline] +fn mask_to_prefix_128(b: &[u8; 16]) -> Option { + let mut count = 0u8; + let mut flipped = false; + for byte in b { + if *byte == 0xFF { + if flipped { + return None; + } + count += 8; + } else if *byte == 0 { + flipped = true; + } else { + if flipped { + return None; + } + let ones = byte.leading_ones() as u8; + if (*byte << ones) != 0 { + return None; + } + count += ones; + flipped = true; + } + } + Some(count) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub(crate) unsafe fn try_mac_from_raw( + sa: *const libc::sockaddr, + len: libc::socklen_t, +) -> Option { + if sa.is_null() || (len as usize) < core::mem::size_of::() { + return None; + } + + let family = unsafe { (*sa).sa_family as libc::c_int }; + if family != libc::AF_PACKET { + return None; + } + + if (len as usize) < core::mem::size_of::() { + return None; + } + + let sll = unsafe { &*(sa as *const libc::sockaddr_ll) }; + + let halen = sll.sll_halen as usize; + if halen < 6 { + return None; + } + + Some(MacAddr::from_octets([ + sll.sll_addr[0], + sll.sll_addr[1], + sll.sll_addr[2], + sll.sll_addr[3], + sll.sll_addr[4], + sll.sll_addr[5], + ])) +} + +#[cfg(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +pub(crate) unsafe fn try_mac_from_raw( + sa: *const libc::sockaddr, + len: libc::socklen_t, +) -> Option { + // Validate pointer and minimal length (must contain sa_family) + if sa.is_null() || (len as usize) < core::mem::size_of::() { + return None; + } + + let sa_dr = unsafe { *sa }; + if sa_dr.sa_family as libc::c_int != libc::AF_LINK { + return None; + } + + // Length sanity check + let sa_len = sa_dr.sa_len as usize; + if sa_len == 0 { + return None; + } + let eff_len = core::cmp::min(sa_len, len as usize); + if eff_len < 8 { + return None; + } + + // Access sa_data region (starts after sa_len + sa_family) + let base = sa as *const u8; + let sa_data = unsafe { base.add(2) }; + // Offsets inside sockaddr_dl + let nlen = unsafe { *sa_data.add(3) } as usize; + let alen = unsafe { *sa_data.add(4) } as usize; + + if alen < 6 { + return None; + } + // LLADDR is located at an offset of 6 + nlen bytes from the beginning of sa_data + // (6 bytes correspond to index(2) + type(1) + nlen(1) + alen(1) + slen(1)). + let lladdr_off_in_data = 6usize + nlen; + let mac_end_in_data = lladdr_off_in_data + 6; + + // The usable length of sa_data is (eff_len - 2), excluding sa_len and sa_family. + if mac_end_in_data > eff_len.saturating_sub(2) { + return None; + } + + let mac_ptr = unsafe { sa_data.add(lladdr_off_in_data) }; + unsafe { + Some(MacAddr( + *mac_ptr.add(0), + *mac_ptr.add(1), + *mac_ptr.add(2), + *mac_ptr.add(3), + *mac_ptr.add(4), + *mac_ptr.add(5), + )) + } +} + +/// Computes the effective length of a `sockaddr` structure +#[inline] +pub(crate) unsafe fn compute_sockaddr_len( + sa: *const libc::sockaddr, + provided_len: Option, + capacity_hint: Option, +) -> Option { + if sa.is_null() { + return None; + } + + // 1) Use caller-provided explicit length if available. + if let Some(mut len) = provided_len { + if let Some(cap) = capacity_hint { + if len > cap { + len = cap; + } + } + // Must be at least large enough to include sa_family_t. + if len >= core::mem::size_of::() as libc::socklen_t { + return Some(len); + } + } + + let sa_dr = unsafe { *sa }; + + // 2) BSD / Darwin platforms store sa_len as the first byte of sockaddr. + #[cfg(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + let len = sa_dr.sa_len as libc::socklen_t; + if len >= core::mem::size_of::() as libc::socklen_t { + let len = if let Some(cap) = capacity_hint { + len.min(cap) + } else { + len + }; + return Some(len); + } + } + + // 3) Fallback: Guess length from address family (for Linux / Windows). + let fam = sa_dr.sa_family as libc::c_int; + let guessed = guess_len_from_family(fam)?; + let guessed = if let Some(cap) = capacity_hint { + guessed.min(cap) + } else { + guessed + }; + + Some(guessed) +} + +/// Estimates the appropriate size of a `sockaddr` structure based on its address family. +#[inline] +fn guess_len_from_family(family: libc::c_int) -> Option { + // IPv4 / IPv6 are universally consistent across all OSes. + if family == libc::AF_INET { + return Some(core::mem::size_of::() as libc::socklen_t); + } + if family == libc::AF_INET6 { + return Some(core::mem::size_of::() as libc::socklen_t); + } + + // Linux / Android: Layer 2 (AF_PACKET) + #[cfg(any(target_os = "linux", target_os = "android"))] + if family == libc::AF_PACKET { + return Some(core::mem::size_of::() as libc::socklen_t); + } + + // Darwin / BSD: Layer 2 (AF_LINK) + #[cfg(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if family == libc::AF_LINK { + // sockaddr_dl is variable-length. + // Return the minimum structure size; the true length should be read from sa_len. + return Some(core::mem::size_of::() as libc::socklen_t); + } + + // Unknown or unsupported family + None +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[inline] +pub(crate) unsafe fn netmask_ip_autolen(sa: *const libc::sockaddr) -> Option { + if sa.is_null() { + return None; + } + + match unsafe { (*sa).sa_family as libc::c_int } { + libc::AF_INET => { + let sin = unsafe { &*(sa as *const libc::sockaddr_in) }; + let n = u32::from_be(sin.sin_addr.s_addr as u32); + Some(IpAddr::V4(Ipv4Addr::from(n))) + } + libc::AF_INET6 => { + let sin6 = unsafe { &*(sa as *const libc::sockaddr_in6) }; + let bytes = sin6.sin6_addr.s6_addr; // [u8; 16] + Some(IpAddr::V6(Ipv6Addr::from(bytes))) + } + _ => None, + } +} + +#[cfg(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +/// Reads a netmask `IpAddr` from a BSD/Darwin `sockaddr` whose actual length is encoded in `sa_len`. +#[inline] +pub(crate) unsafe fn netmask_ip_autolen(sa: *const libc::sockaddr) -> Option { + if sa.is_null() { + return None; + } + + // BSD/Darwin carry the effective length in the first byte (sa_len). + let sa_dr = unsafe { *sa }; + let sa_len = sa_dr.sa_len as usize; + if sa_len == 0 { + // Zero-length sockaddr (e.g., default route). Treat as no mask. + return None; + } + + let fam = sa_dr.sa_family as libc::c_int; + let base = sa as *const u8; + + match fam { + libc::AF_INET => { + // Layout on BSD/Darwin: + // offset 0: sa_len (1) + // offset 1: sa_family (1) + // offset 2..3: sin_port (2) + // offset 4..7: sin_addr (4) + const OFF_SIN_ADDR: usize = 4; + if sa_len <= OFF_SIN_ADDR { + return Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); + } + // Copy up to 4 bytes; tolerate short/truncated structures. + let n = (sa_len - OFF_SIN_ADDR).min(4); + + let mut bytes = [0u8; 4]; + unsafe { + core::ptr::copy_nonoverlapping(base.add(OFF_SIN_ADDR), bytes.as_mut_ptr(), n) + }; + Some(IpAddr::V4(Ipv4Addr::from(u32::from_be_bytes(bytes)))) + } + libc::AF_INET6 => { + // Layout on BSD/Darwin: + // offset 0: sa_len (1) + // offset 1: sa_family (1) + // offset 2..3: sin6_port (2) + // offset 4..7: sin6_flowinfo (4) + // offset 8..23: sin6_addr (16) + const OFF_SIN6_ADDR: usize = 8; + if sa_len <= OFF_SIN6_ADDR { + return Some(IpAddr::V6(Ipv6Addr::UNSPECIFIED)); + } + // Copy up to 16 bytes; tolerate short/truncated structures. + let n = (sa_len - OFF_SIN6_ADDR).min(16); + + let mut bytes = [0u8; 16]; + unsafe { + core::ptr::copy_nonoverlapping(base.add(OFF_SIN6_ADDR), bytes.as_mut_ptr(), n) + }; + Some(IpAddr::V6(Ipv6Addr::from(bytes))) + } + _ => None, + } +} + +#[allow(dead_code)] +#[cfg(any(target_os = "linux", target_os = "android"))] +#[inline] +pub(crate) unsafe fn netmask_prefix_autolen(sa: *const libc::sockaddr) -> Option { + if sa.is_null() { + return None; + } + + match unsafe { (*sa).sa_family as libc::c_int } { + libc::AF_INET => { + let sin = unsafe { &*(sa as *const libc::sockaddr_in) }; + let m = u32::from_be(sin.sin_addr.s_addr as u32); + Some(m.leading_ones() as u8) + } + libc::AF_INET6 => { + let sin6 = unsafe { &*(sa as *const libc::sockaddr_in6) }; + // [u8; 16] + let bytes = sin6.sin6_addr.s6_addr; + Some(u128::from_be_bytes(bytes).leading_ones() as u8) + } + _ => None, + } +} + +#[allow(dead_code)] +#[cfg(any( + target_vendor = "apple", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +#[inline] +pub(crate) unsafe fn netmask_prefix_autolen(sa: *const libc::sockaddr) -> Option { + if sa.is_null() { + return None; + } + let sa_dr = unsafe { *sa }; + let sa_len = sa_dr.sa_len as usize; + if sa_len == 0 { + return Some(0); + } + + let fam = sa_dr.sa_family as libc::c_int; + let base = sa as *const u8; + + match fam { + libc::AF_INET => { + const OFF_SIN_ADDR: usize = 4; + if sa_len <= OFF_SIN_ADDR { + return Some(0); + } + let n = (sa_len - OFF_SIN_ADDR).min(4); + + let mut bytes = [0u8; 4]; + unsafe { + core::ptr::copy_nonoverlapping(base.add(OFF_SIN_ADDR), bytes.as_mut_ptr(), n); + } + Some(u32::from_be_bytes(bytes).leading_ones() as u8) + } + libc::AF_INET6 => { + const OFF_SIN6_ADDR: usize = 8; + if sa_len <= OFF_SIN6_ADDR { + return Some(0); + } + let n = (sa_len - OFF_SIN6_ADDR).min(16); + + let mut bytes = [0u8; 16]; + unsafe { + core::ptr::copy_nonoverlapping(base.add(OFF_SIN6_ADDR), bytes.as_mut_ptr(), n); + } + Some(u128::from_be_bytes(bytes).leading_ones() as u8) + } + _ => None, + } +} + +#[inline] +pub(crate) fn sockaddr_storage_cap() -> libc::socklen_t { + mem::size_of::().try_into().unwrap() +} diff --git a/src/os/unix/types.rs b/src/os/unix/types.rs new file mode 100644 index 0000000..8729910 --- /dev/null +++ b/src/os/unix/types.rs @@ -0,0 +1,20 @@ +#[cfg(any(target_os = "linux", target_os = "android"))] +use crate::interface::types::InterfaceType; + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn get_interface_type(addr_ref: &libc::ifaddrs) -> InterfaceType { + use std::ffi::CStr; + use std::os::raw::c_char; + use std::str::from_utf8_unchecked; + + let c_str = addr_ref.ifa_name as *const c_char; + let bytes = unsafe { CStr::from_ptr(c_str).to_bytes() }; + let name: String = unsafe { from_utf8_unchecked(bytes).to_owned() }; + crate::os::linux::sysfs::get_interface_type(&name) +} + +#[cfg(target_vendor = "apple")] +pub use crate::os::darwin::types::get_interface_type; + +#[cfg(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd"))] +pub use crate::os::bsd::types::get_interface_type; diff --git a/src/os/windows/flags.rs b/src/os/windows/flags.rs new file mode 100644 index 0000000..25edaed --- /dev/null +++ b/src/os/windows/flags.rs @@ -0,0 +1,47 @@ +use crate::interface::interface::Interface; +use windows_sys::Win32::NetworkManagement::IpHelper::{GetIfEntry2, MIB_IF_ROW2, MIB_IF_ROW2_0}; +use windows_sys::Win32::Networking::WinSock as ws; + +pub const IFF_UP: u32 = ws::IFF_UP; +pub const IFF_BROADCAST: u32 = ws::IFF_BROADCAST; +pub const IFF_LOOPBACK: u32 = ws::IFF_LOOPBACK; +pub const IFF_POINTOPOINT: u32 = ws::IFF_POINTTOPOINT; +pub const IFF_MULTICAST: u32 = ws::IFF_MULTICAST; + +//const IFF_HARDWARE_INTERFACE: u8 = 0b0000_0001; +//const IFF_FILTER_INTERFACE: u8 = 0b0000_0010; +const IFF_CONNECTOR_PRESENT: u8 = 0b0000_0100; +//const IFF_NOT_AUTHENTICATED: u8 = 0b0000_1000; +//const IFF_NOT_MEDIA_CONNECTED: u8 = 0b0001_0000; +//const IFF_PAUSED: u8 = 0b0010_0000; +//const IFF_LOW_POWER: u8 = 0b0100_0000; +//const IFF_END_POINT_INTERFACE: u8 = 0b1000_0000; + +pub fn is_physical_interface(interface: &Interface) -> bool { + is_connector_present(interface.index) + || (interface.is_up() + && interface.is_running() + && !interface.is_tun() + && !interface.is_loopback()) +} + +pub fn is_running(interface: &Interface) -> bool { + interface.is_up() +} + +/// Check if a network interface has a connector present, indicating it is a physical interface. +pub fn is_connector_present(if_index: u32) -> bool { + // Initialize MIB_IF_ROW2 + let mut row: MIB_IF_ROW2 = unsafe { std::mem::zeroed() }; + row.InterfaceIndex = if_index; + // Retrieve interface information using GetIfEntry2 + unsafe { + if GetIfEntry2(&mut row) != 0 { + eprintln!("Failed to get interface entry for index: {}", if_index); + return false; + } + } + // Check if the connector is present + let oper_status_flags: MIB_IF_ROW2_0 = row.InterfaceAndOperStatusFlags; + oper_status_flags._bitfield & IFF_CONNECTOR_PRESENT != 0 +} diff --git a/src/interface/windows.rs b/src/os/windows/interface.rs similarity index 64% rename from src/interface/windows.rs rename to src/os/windows/interface.rs index 19a96dc..9097de0 100644 --- a/src/interface/windows.rs +++ b/src/os/windows/interface.rs @@ -1,52 +1,39 @@ -use std::convert::TryFrom; -use std::net::{IpAddr, Ipv4Addr}; +use std::net::IpAddr; use windows_sys::Win32::Foundation::{ERROR_BUFFER_OVERFLOW, NO_ERROR}; use windows_sys::Win32::NetworkManagement::IpHelper::{ - GetAdaptersAddresses, GetIfEntry2, SendARP, GAA_FLAG_INCLUDE_GATEWAYS, IP_ADAPTER_ADDRESSES_LH, - MIB_IF_ROW2, MIB_IF_ROW2_0, + GAA_FLAG_INCLUDE_GATEWAYS, GetAdaptersAddresses, IP_ADAPTER_ADDRESSES_LH, }; use windows_sys::Win32::NetworkManagement::Ndis::NET_IF_OPER_STATUS_UP; use windows_sys::Win32::Networking::WinSock::{ AF_INET, AF_INET6, AF_UNSPEC, SOCKADDR_INET, SOCKET_ADDRESS, }; -use crate::device::NetworkDevice; -use crate::interface::{Interface, InterfaceType, OperState}; +use super::flags; +use super::macros::linked_list_iter; +use crate::interface::interface::Interface; +use crate::interface::state::OperState; +use crate::interface::types::InterfaceType; use crate::ipnet::{Ipv4Net, Ipv6Net}; -use crate::mac::MacAddr; -use crate::stats::InterfaceStats; -use crate::sys; +use crate::net::mac::MacAddr; +use crate::stats::counters::InterfaceStats; use std::ffi::CStr; -use std::mem::MaybeUninit; - -//const IFF_HARDWARE_INTERFACE: u8 = 0b0000_0001; -//const IFF_FILTER_INTERFACE: u8 = 0b0000_0010; -const IFF_CONNECTOR_PRESENT: u8 = 0b0000_0100; -//const IFF_NOT_AUTHENTICATED: u8 = 0b0000_1000; -//const IFF_NOT_MEDIA_CONNECTED: u8 = 0b0001_0000; -//const IFF_PAUSED: u8 = 0b0010_0000; -//const IFF_LOW_POWER: u8 = 0b0100_0000; -//const IFF_END_POINT_INTERFACE: u8 = 0b1000_0000; -// Note: We take `&*mut T` instead of just `*mut T` to tie the lifetime of all the returned items -// to the lifetime of the pointer for some extra safety. -unsafe fn linked_list_iter(ptr: &*mut T, next: fn(&T) -> *mut T) -> impl Iterator { - let mut ptr = ptr.cast_const(); - - std::iter::from_fn(move || { - let cur = ptr.as_ref()?; - ptr = next(cur); - Some(cur) - }) -} +#[cfg(feature = "gateway")] +use crate::net::device::NetworkDevice; +#[cfg(feature = "gateway")] +use crate::net::ip::get_local_ipaddr; +#[cfg(feature = "gateway")] +use std::mem::MaybeUninit; +#[cfg(feature = "gateway")] +use std::net::Ipv4Addr; +#[cfg(feature = "gateway")] +use windows_sys::Win32::NetworkManagement::IpHelper::SendARP; -// The `Next` element is always the same, so use a macro to avoid the repetition. -macro_rules! linked_list_iter { - ($ptr:expr) => { - linked_list_iter($ptr, |cur| cur.Next) - }; +fn sanitize_u64(val: u64) -> Option { + if val == u64::MAX { None } else { Some(val) } } +#[cfg(feature = "gateway")] fn get_mac_through_arp(src_ip: Ipv4Addr, dst_ip: Ipv4Addr) -> MacAddr { let src_ip_int = u32::from_ne_bytes(src_ip.octets()); let dst_ip_int = u32::from_ne_bytes(dst_ip.octets()); @@ -70,9 +57,9 @@ fn get_mac_through_arp(src_ip: Ipv4Addr, dst_ip: Ipv4Addr) -> MacAddr { // Convert a socket address into a Rust IpAddr object and also a scope ID if it's an // IPv6 address unsafe fn socket_address_to_ipaddr(addr: &SOCKET_ADDRESS) -> (Option, Option) { - match addr.lpSockaddr.cast::().as_ref() { + match unsafe { addr.lpSockaddr.cast::().as_ref() } { None => (None, None), - Some(sockaddr) => match sockaddr.si_family { + Some(sockaddr) => match unsafe { sockaddr.si_family } { AF_INET => { let addr: IpAddr = unsafe { sockaddr.Ipv4.sin_addr.S_un.S_addr } .to_ne_bytes() @@ -81,7 +68,7 @@ unsafe fn socket_address_to_ipaddr(addr: &SOCKET_ADDRESS) -> (Option, Op } AF_INET6 => { let addr: IpAddr = unsafe { sockaddr.Ipv6.sin6_addr.u.Byte }.into(); - let scope_id = sockaddr.Ipv6.Anonymous.sin6_scope_id; + let scope_id = unsafe { sockaddr.Ipv6.Anonymous.sin6_scope_id }; (Some(addr), Some(scope_id)) } _ => (None, None), @@ -89,102 +76,19 @@ unsafe fn socket_address_to_ipaddr(addr: &SOCKET_ADDRESS) -> (Option, Op } } -pub fn is_running(interface: &Interface) -> bool { - interface.is_up() -} - -/// Return the operational state of a given Windows interface by its adapter name (GUID string) -pub fn operstate(if_name: &str) -> OperState { - let mut mem = Vec::::with_capacity(15000); - let mut retries = 3; - loop { - let mut dwsize = mem.capacity() as u32; - let ret = unsafe { - GetAdaptersAddresses( - AF_UNSPEC as u32, - GAA_FLAG_INCLUDE_GATEWAYS, - std::ptr::null_mut(), - mem.as_mut_ptr().cast(), - &mut dwsize, - ) - }; - match ret { - 0 => { - unsafe { - mem.set_len(dwsize as usize); - } - break; - } - 111 if retries > 0 => { - mem.reserve(dwsize as usize); - retries -= 1; - } - _ => return OperState::Unknown, - } - } - - let ptr = mem.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH; - for cur in unsafe { linked_list_iter!(&ptr) } { - let adapter_name = unsafe { - CStr::from_ptr(cur.AdapterName.cast()) - .to_string_lossy() - .to_string() - }; - if adapter_name == if_name { - return match cur.OperStatus { - 1 => OperState::Up, - 2 => OperState::Down, - 3 => OperState::Testing, - 4 => OperState::Unknown, - 5 => OperState::Dormant, - 6 => OperState::NotPresent, - 7 => OperState::LowerLayerDown, - _ => OperState::Unknown, - }; - } - } - - OperState::Unknown -} - -/// Check if a network interface has a connector present, indicating it is a physical interface. -fn is_connector_present(if_index: u32) -> bool { - // Initialize MIB_IF_ROW2 - let mut row: MIB_IF_ROW2 = unsafe { std::mem::zeroed() }; - row.InterfaceIndex = if_index; - // Retrieve interface information using GetIfEntry2 - unsafe { - if GetIfEntry2(&mut row) != 0 { - eprintln!("Failed to get interface entry for index: {}", if_index); - return false; - } - } - // Check if the connector is present - let oper_status_flags: MIB_IF_ROW2_0 = row.InterfaceAndOperStatusFlags; - oper_status_flags._bitfield & IFF_CONNECTOR_PRESENT != 0 -} - -pub fn is_physical_interface(interface: &Interface) -> bool { - is_connector_present(interface.index) - || (interface.is_up() - && interface.is_running() - && !interface.is_tun() - && !interface.is_loopback()) -} - unsafe fn from_wide_string(ptr: *const u16) -> String { let mut len = 0; - while *ptr.add(len) != 0 { + while unsafe { *ptr.add(len) } != 0 { len += 1; } - String::from_utf16_lossy(std::slice::from_raw_parts(ptr, len)) + String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(ptr, len) }) } // Get network interfaces using the IP Helper API // Reference: https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses pub fn interfaces() -> Vec { #[cfg(feature = "gateway")] - let local_ip: IpAddr = match super::get_local_ipaddr() { + let local_ip: IpAddr = match get_local_ipaddr() { Some(local_ip) => local_ip, None => IpAddr::V4(Ipv4Addr::LOCALHOST), }; @@ -234,23 +138,23 @@ pub fn interfaces() -> Vec { // Flags and Status let mut flags: u32 = 0; if cur.OperStatus == NET_IF_OPER_STATUS_UP { - flags |= sys::IFF_UP; + flags |= flags::IFF_UP; } match if_type { InterfaceType::Ethernet | InterfaceType::TokenRing | InterfaceType::Wireless80211 | InterfaceType::HighPerformanceSerialBus => { - flags |= sys::IFF_BROADCAST | sys::IFF_MULTICAST; + flags |= flags::IFF_BROADCAST | flags::IFF_MULTICAST; } InterfaceType::Ppp | InterfaceType::Tunnel => { - flags |= sys::IFF_POINTOPOINT | sys::IFF_MULTICAST; + flags |= flags::IFF_POINTOPOINT | flags::IFF_MULTICAST; } InterfaceType::Loopback => { - flags |= sys::IFF_LOOPBACK | sys::IFF_MULTICAST; + flags |= flags::IFF_LOOPBACK | flags::IFF_MULTICAST; } InterfaceType::Atm => { - flags |= sys::IFF_BROADCAST | sys::IFF_POINTOPOINT | sys::IFF_MULTICAST; + flags |= flags::IFF_BROADCAST | flags::IFF_POINTOPOINT | flags::IFF_MULTICAST; } _ => {} } @@ -304,7 +208,7 @@ pub fn interfaces() -> Vec { #[cfg(feature = "gateway")] let mut default_gateway: NetworkDevice = NetworkDevice::new(); #[cfg(feature = "gateway")] - if flags & sys::IFF_UP != 0 { + if flags & flags::IFF_UP != 0 { for gateway_ip in gateway_ips { match gateway_ip { IpAddr::V4(ipv4) => { @@ -332,7 +236,7 @@ pub fn interfaces() -> Vec { IpAddr::V4(local_ipv4) => ipv4_vec.iter().any(|x| x.addr() == local_ipv4), IpAddr::V6(local_ipv6) => ipv6_vec.iter().any(|x| x.addr() == local_ipv6), }; - let stats: Option = crate::stats::get_stats_from_index(index); + let stats: Option = super::stats::get_stats_from_index(index); let interface: Interface = Interface { index, name: adapter_name, @@ -345,8 +249,8 @@ pub fn interfaces() -> Vec { ipv6_scope_ids: ipv6_scope_id_vec, flags, oper_state, - transmit_speed: sys::sanitize_u64(cur.TransmitLinkSpeed), - receive_speed: sys::sanitize_u64(cur.ReceiveLinkSpeed), + transmit_speed: sanitize_u64(cur.TransmitLinkSpeed), + receive_speed: sanitize_u64(cur.ReceiveLinkSpeed), stats, #[cfg(feature = "gateway")] gateway: if default_gateway.mac_addr == MacAddr::zero() { diff --git a/src/os/windows/macros.rs b/src/os/windows/macros.rs new file mode 100644 index 0000000..c05fe67 --- /dev/null +++ b/src/os/windows/macros.rs @@ -0,0 +1,23 @@ +// Note: We take `&*mut T` instead of just `*mut T` to tie the lifetime of all the returned items +// to the lifetime of the pointer for some extra safety. +pub(crate) unsafe fn linked_list_iter_fn( + ptr: &*mut T, + next: fn(&T) -> *mut T, +) -> impl Iterator { + let mut ptr = ptr.cast_const(); + + std::iter::from_fn(move || { + let cur = unsafe { ptr.as_ref()? }; + ptr = next(cur); + Some(cur) + }) +} + +// The `Next` element is always the same, so use a macro to avoid the repetition. +macro_rules! linked_list_iter { + ($ptr:expr) => { + $crate::os::windows::macros::linked_list_iter_fn($ptr, |cur| cur.Next) + }; +} + +pub(crate) use linked_list_iter; diff --git a/src/os/windows/mod.rs b/src/os/windows/mod.rs new file mode 100644 index 0000000..fce716d --- /dev/null +++ b/src/os/windows/mod.rs @@ -0,0 +1,5 @@ +pub mod flags; +pub mod interface; +pub mod macros; +pub mod state; +pub mod stats; diff --git a/src/os/windows/state.rs b/src/os/windows/state.rs new file mode 100644 index 0000000..a224a77 --- /dev/null +++ b/src/os/windows/state.rs @@ -0,0 +1,61 @@ +use super::macros::linked_list_iter; +use crate::interface::state::OperState; +use std::ffi::CStr; +use windows_sys::Win32::NetworkManagement::IpHelper::{ + GAA_FLAG_INCLUDE_GATEWAYS, GetAdaptersAddresses, IP_ADAPTER_ADDRESSES_LH, +}; +use windows_sys::Win32::Networking::WinSock::AF_UNSPEC; + +/// Return the operational state of a given Windows interface by its adapter name (GUID string) +pub fn operstate(if_name: &str) -> OperState { + let mut mem = Vec::::with_capacity(15000); + let mut retries = 3; + loop { + let mut dwsize = mem.capacity() as u32; + let ret = unsafe { + GetAdaptersAddresses( + AF_UNSPEC as u32, + GAA_FLAG_INCLUDE_GATEWAYS, + std::ptr::null_mut(), + mem.as_mut_ptr().cast(), + &mut dwsize, + ) + }; + match ret { + 0 => { + unsafe { + mem.set_len(dwsize as usize); + } + break; + } + 111 if retries > 0 => { + mem.reserve(dwsize as usize); + retries -= 1; + } + _ => return OperState::Unknown, + } + } + + let ptr = mem.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH; + for cur in unsafe { linked_list_iter!(&ptr) } { + let adapter_name = unsafe { + CStr::from_ptr(cur.AdapterName.cast()) + .to_string_lossy() + .to_string() + }; + if adapter_name == if_name { + return match cur.OperStatus { + 1 => OperState::Up, + 2 => OperState::Down, + 3 => OperState::Testing, + 4 => OperState::Unknown, + 5 => OperState::Dormant, + 6 => OperState::NotPresent, + 7 => OperState::LowerLayerDown, + _ => OperState::Unknown, + }; + } + } + + OperState::Unknown +} diff --git a/src/os/windows/stats.rs b/src/os/windows/stats.rs new file mode 100644 index 0000000..d9c83ca --- /dev/null +++ b/src/os/windows/stats.rs @@ -0,0 +1,22 @@ +use crate::stats::counters::InterfaceStats; + +pub(crate) fn get_stats_from_index(index: u32) -> Option { + use std::mem::zeroed; + use std::time::SystemTime; + use windows_sys::Win32::NetworkManagement::IpHelper::{GetIfEntry2, MIB_IF_ROW2}; + + let mut row: MIB_IF_ROW2 = unsafe { zeroed() }; + row.InterfaceIndex = index; + + unsafe { + if GetIfEntry2(&mut row) == 0 { + Some(InterfaceStats { + rx_bytes: row.InOctets as u64, + tx_bytes: row.OutOctets as u64, + timestamp: Some(SystemTime::now()), + }) + } else { + None + } + } +} diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..8cbe1bf --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,13 @@ +pub use crate::interface::get_interfaces; +pub use crate::interface::interface::Interface; +pub use crate::interface::state::OperState; +pub use crate::interface::types::InterfaceType; +pub use crate::net::device::NetworkDevice; +pub use crate::net::mac::MacAddr; +pub use crate::stats::counters::InterfaceStats; +pub use ipnet::{Ipv4Net, Ipv6Net}; + +#[cfg(feature = "gateway")] +pub use crate::interface::get_default_interface; +#[cfg(feature = "gateway")] +pub use crate::route::get_default_gateway; diff --git a/src/gateway/mod.rs b/src/route/mod.rs similarity index 60% rename from src/gateway/mod.rs rename to src/route/mod.rs index 80dee7c..8dc21ce 100644 --- a/src/gateway/mod.rs +++ b/src/route/mod.rs @@ -1,23 +1,14 @@ -#[cfg(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd"))] -pub(crate) mod bsd; - -#[cfg(target_vendor = "apple")] -pub(crate) mod macos; - -#[cfg(any(target_os = "linux", target_os = "android"))] -pub(crate) mod linux; - -use crate::device::NetworkDevice; -use crate::interface::{self, Interface}; +use crate::interface::interface::Interface; +use crate::net::device::NetworkDevice; use std::net::IpAddr; /// Get default Gateway pub fn get_default_gateway() -> Result { - let local_ip: IpAddr = match interface::get_local_ipaddr() { + let local_ip: IpAddr = match crate::net::ip::get_local_ipaddr() { Some(local_ip) => local_ip, None => return Err(String::from("Local IP address not found")), }; - let interfaces: Vec = interface::get_interfaces(); + let interfaces: Vec = crate::interface::get_interfaces(); for iface in interfaces { match local_ip { IpAddr::V4(local_ipv4) => { @@ -38,12 +29,3 @@ pub fn get_default_gateway() -> Result { } Err(String::from("Default Gateway not found")) } - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_default_gateway() { - println!("{:?}", get_default_gateway()); - } -} diff --git a/src/stats.rs b/src/stats/counters.rs similarity index 97% rename from src/stats.rs rename to src/stats/counters.rs index 0d13489..6373beb 100644 --- a/src/stats.rs +++ b/src/stats/counters.rs @@ -3,7 +3,7 @@ use std::time::SystemTime; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::Interface; +use crate::interface::interface::Interface; /// Interface traffic statistics at a given point in time. #[derive(Clone, Eq, PartialEq, Hash, Debug)] @@ -47,7 +47,7 @@ pub(crate) fn get_stats(_ifa: Option<&libc::ifaddrs>, name: &str) -> Option Option { +pub(crate) fn get_stats_from_name(name: &str) -> Option { use std::fs::read_to_string; let rx_path = format!("/sys/class/net/{}/statistics/rx_bytes", name); let tx_path = format!("/sys/class/net/{}/statistics/tx_bytes", name); diff --git a/src/stats/mod.rs b/src/stats/mod.rs new file mode 100644 index 0000000..25b17c6 --- /dev/null +++ b/src/stats/mod.rs @@ -0,0 +1 @@ +pub mod counters; diff --git a/src/sys/linux.rs b/src/sys/linux.rs deleted file mode 100644 index a8f9d70..0000000 --- a/src/sys/linux.rs +++ /dev/null @@ -1,18 +0,0 @@ -// ARP protocol HARDWARE identifiers -pub mod if_arp { - pub const ARPHRD_ETHER: u32 = libc::ARPHRD_ETHER as u32; - pub const ARPHRD_IEEE802: u32 = libc::ARPHRD_IEEE802 as u32; - pub const ARPHRD_FDDI: u32 = libc::ARPHRD_FDDI as u32; - pub const ARPHRD_PPP: u32 = libc::ARPHRD_PPP as u32; - pub const ARPHRD_LOOPBACK: u32 = libc::ARPHRD_LOOPBACK as u32; - pub const ARPHRD_EETHER: u32 = libc::ARPHRD_EETHER as u32; - pub const ARPHRD_SLIP: u32 = libc::ARPHRD_SLIP as u32; - pub const ARPHRD_ATM: u32 = libc::ARPHRD_ATM as u32; - pub const ARPHRD_IEEE80211: u32 = libc::ARPHRD_IEEE80211 as u32; - pub const ARPHRD_TUNNEL: u32 = libc::ARPHRD_TUNNEL as u32; - pub const ARPHRD_X25: u32 = libc::ARPHRD_X25 as u32; - pub const ARPHRD_IEEE1394: u32 = libc::ARPHRD_IEEE1394 as u32; - pub const ARPHRD_CAN: u32 = libc::ARPHRD_CAN as u32; -} - -pub use libc::IFF_LOWER_UP; diff --git a/src/sys/mod.rs b/src/sys/mod.rs deleted file mode 100644 index b5c87ee..0000000 --- a/src/sys/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[cfg(not(target_os = "windows"))] -mod unix; -#[cfg(not(target_os = "windows"))] -pub use self::unix::*; - -#[cfg(target_os = "windows")] -mod windows; -#[cfg(target_os = "windows")] -pub use self::windows::*; - -#[cfg(any(target_os = "linux", target_os = "android"))] -mod linux; -#[cfg(any(target_os = "linux", target_os = "android"))] -pub use self::linux::*; diff --git a/src/sys/unix.rs b/src/sys/unix.rs deleted file mode 100644 index 75f8d34..0000000 --- a/src/sys/unix.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::io; -use std::mem; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; - -pub type SockAddrIn = libc::sockaddr_in; -pub type SockAddrIn6 = libc::sockaddr_in6; -pub type SockAddrStorage = libc::sockaddr_storage; -pub type InAddr = libc::in_addr; - -pub const AF_INET: libc::c_int = libc::AF_INET; -pub const AF_INET6: libc::c_int = libc::AF_INET6; - -pub use libc::{IFF_BROADCAST, IFF_LOOPBACK, IFF_MULTICAST, IFF_POINTOPOINT, IFF_RUNNING, IFF_UP}; - -#[cfg(any( - target_vendor = "apple", - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd" -))] -pub const SIOCGIFFLAGS: libc::c_ulong = 0xc0206911; - -fn ntohs(u: u16) -> u16 { - u16::from_be(u) -} - -// Converts libc socket address type to Rust SocketAddr struct -pub fn sockaddr_to_addr(storage: &SockAddrStorage, len: usize) -> io::Result { - match storage.ss_family as libc::c_int { - AF_INET => { - assert!(len as usize >= mem::size_of::()); - let storage: &SockAddrIn = unsafe { mem::transmute(storage) }; - let ip = ipv4_addr_int(storage.sin_addr); - // octets - let o1 = (ip >> 24) as u8; - let o2 = (ip >> 16) as u8; - let o3 = (ip >> 8) as u8; - let o4 = ip as u8; - let sockaddrv4 = - SocketAddrV4::new(Ipv4Addr::new(o1, o2, o3, o4), ntohs(storage.sin_port)); - Ok(SocketAddr::V4(sockaddrv4)) - } - AF_INET6 => { - assert!(len as usize >= mem::size_of::()); - let storage: &SockAddrIn6 = unsafe { mem::transmute(storage) }; - let arr: [u16; 8] = unsafe { mem::transmute(storage.sin6_addr.s6_addr) }; - // hextets - let h1 = ntohs(arr[0]); - let h2 = ntohs(arr[1]); - let h3 = ntohs(arr[2]); - let h4 = ntohs(arr[3]); - let h5 = ntohs(arr[4]); - let h6 = ntohs(arr[5]); - let h7 = ntohs(arr[6]); - let h8 = ntohs(arr[7]); - let ip = Ipv6Addr::new(h1, h2, h3, h4, h5, h6, h7, h8); - Ok(SocketAddr::V6(SocketAddrV6::new( - ip, - ntohs(storage.sin6_port), - u32::from_be(storage.sin6_flowinfo), - storage.sin6_scope_id, - ))) - } - _ => Err(io::Error::new(io::ErrorKind::InvalidData, "Not supported")), - } -} - -#[inline(always)] -pub fn ipv4_addr_int(addr: InAddr) -> u32 { - (addr.s_addr as u32).to_be() -} diff --git a/src/sys/windows.rs b/src/sys/windows.rs deleted file mode 100644 index ce80782..0000000 --- a/src/sys/windows.rs +++ /dev/null @@ -1,17 +0,0 @@ -use windows_sys::Win32::Networking::WinSock as ws; - -pub const IFF_UP: u32 = ws::IFF_UP; -pub const IFF_BROADCAST: u32 = ws::IFF_BROADCAST; -pub const IFF_LOOPBACK: u32 = ws::IFF_LOOPBACK; -pub const IFF_POINTOPOINT: u32 = ws::IFF_POINTTOPOINT; -pub const IFF_MULTICAST: u32 = ws::IFF_MULTICAST; - -/// Convert u64::MAX to None. -/// Used for Windows APIs that return invalid max values. -pub(crate) fn sanitize_u64(val: u64) -> Option { - if val == u64::MAX { - None - } else { - Some(val) - } -}