diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c194f0d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +scripts/* linguist-vendored diff --git a/Cargo.toml b/Cargo.toml index 79ea24c..a24124e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,3 +69,7 @@ required-features = ["serde", "gateway"] name = "global_ips" path = "examples/global_ips.rs" required-features = ["gateway"] + +[[example]] +name = "stats" +path = "examples/stats.rs" diff --git a/examples/default_interface.rs b/examples/default_interface.rs index cc3dfb7..2ff0ab2 100644 --- a/examples/default_interface.rs +++ b/examples/default_interface.rs @@ -27,6 +27,7 @@ fn main() { println!("\tIPv6: {:?}", interface.ipv6); println!("\tTransmit Speed: {:?}", interface.transmit_speed); println!("\tReceive Speed: {:?}", interface.receive_speed); + println!("\tStats: {:?}", interface.stats); if let Some(gateway) = interface.gateway { println!("Default Gateway"); println!("\tMAC Address: {}", gateway.mac_addr); diff --git a/examples/list_interfaces.rs b/examples/list_interfaces.rs index 6f686bd..57a18b4 100644 --- a/examples/list_interfaces.rs +++ b/examples/list_interfaces.rs @@ -36,6 +36,7 @@ fn main() { println!("\tTransmit Speed: {:?}", interface.transmit_speed); println!("\tReceive Speed: {:?}", interface.receive_speed); + println!("\tStats: {:?}", interface.stats); #[cfg(feature = "gateway")] if let Some(gateway) = interface.gateway { println!("Gateway"); diff --git a/examples/stats.rs b/examples/stats.rs new file mode 100644 index 0000000..8d16f91 --- /dev/null +++ b/examples/stats.rs @@ -0,0 +1,42 @@ +use std::thread::sleep; +use std::time::{Duration, SystemTime}; + +use netdev::{self, Interface}; + +fn main() -> std::io::Result<()> { + let mut iface = netdev::get_default_interface().expect("No default interface found"); + println!( + "Monitoring default interface: [{}]{}\n", + iface.index, iface.name + ); + + // Initial stats + println!("[Initial stats]"); + print_stats(&iface); + + // Update stats every second for 3 seconds + for i in 1..=3 { + sleep(Duration::from_secs(1)); + iface.update_stats()?; + println!("\n[Update {}]", i); + print_stats(&iface); + } + + Ok(()) +} + +fn print_stats(iface: &Interface) { + match &iface.stats { + Some(stats) => { + println!( + "RX: {:>12} bytes, TX: {:>12} bytes at {:?}", + stats.rx_bytes, + stats.tx_bytes, + stats.timestamp.unwrap_or(SystemTime::UNIX_EPOCH) + ); + } + None => { + println!("No statistics available for interface: {}", iface.name); + } + } +} diff --git a/scripts/build-all.ps1 b/scripts/build-all.ps1 new file mode 100644 index 0000000..7c5581b --- /dev/null +++ b/scripts/build-all.ps1 @@ -0,0 +1,20 @@ +# target platforms +$targets = @( + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-unknown-freebsd", + "aarch64-linux-android", + "x86_64-linux-android" +) + +# cross build +foreach ($target in $targets) { + Write-Host "==> Building for $target..." + $result = & cross build --target $target + Write-Host "✅ Success: $target" + if ($LASTEXITCODE -ne 0) { + Write-Error "❌ Build failed for $target" + exit 1 + } +} +Write-Host "✅ All builds succeeded." diff --git a/scripts/build-all.sh b/scripts/build-all.sh new file mode 100755 index 0000000..8ef7b24 --- /dev/null +++ b/scripts/build-all.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -e + +# target platforms +TARGETS=( + x86_64-unknown-linux-gnu + aarch64-unknown-linux-gnu + x86_64-unknown-freebsd + aarch64-linux-android + x86_64-linux-android +) + +# cross build +for target in "${TARGETS[@]}"; do + echo "==> Building for $target..." + if cross build --target "$target"; then + echo "✅ Success: $target" + else + echo "❌ Failed: $target" + exit 1 + fi +done + +echo "" +echo "✅ All builds succeeded!" diff --git a/src/interface/android.rs b/src/interface/android.rs index a608f4b..c91965b 100644 --- a/src/interface/android.rs +++ b/src/interface/android.rs @@ -66,6 +66,8 @@ pub mod netlink { 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) { @@ -86,6 +88,10 @@ pub mod netlink { eprintln!("unable to list addresses: {:?}", err); } } + for iface in &mut ifaces { + let stats: Option = get_stats(None, &iface.name); + iface.stats = stats; + } ifaces } @@ -105,6 +111,7 @@ pub mod netlink { flags: link_msg.header.flags.bits(), transmit_speed: None, receive_speed: None, + stats: None, #[cfg(feature = "gateway")] gateway: None, #[cfg(feature = "gateway")] diff --git a/src/interface/mod.rs b/src/interface/mod.rs index 1163cf8..1837356 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -46,6 +46,7 @@ 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}; @@ -90,6 +91,16 @@ pub struct Interface { /// 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. @@ -150,6 +161,7 @@ impl Interface { flags: 0, transmit_speed: None, receive_speed: None, + stats: None, #[cfg(feature = "gateway")] gateway: None, #[cfg(feature = "gateway")] @@ -250,6 +262,10 @@ impl Interface { .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) + } } /// Get default Network Interface diff --git a/src/interface/unix.rs b/src/interface/unix.rs index e20b1da..f80b31b 100644 --- a/src/interface/unix.rs +++ b/src/interface/unix.rs @@ -4,6 +4,7 @@ use super::MacAddr; 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}; @@ -407,6 +408,7 @@ fn unix_interfaces_inner( 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 { @@ -455,6 +457,10 @@ fn unix_interfaces_inner( 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()); } @@ -489,6 +495,7 @@ fn unix_interfaces_inner( flags: addr_ref.ifa_flags, transmit_speed: None, receive_speed: None, + stats: stats, #[cfg(feature = "gateway")] gateway: None, #[cfg(feature = "gateway")] diff --git a/src/interface/windows.rs b/src/interface/windows.rs index f639af6..a8d9cad 100644 --- a/src/interface/windows.rs +++ b/src/interface/windows.rs @@ -14,6 +14,7 @@ use crate::device::NetworkDevice; use crate::interface::{Interface, InterfaceType}; use crate::ipnet::{Ipv4Net, Ipv6Net}; use crate::mac::MacAddr; +use crate::stats::InterfaceStats; use crate::sys; use std::ffi::CStr; use std::mem::MaybeUninit; @@ -265,6 +266,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 interface: Interface = Interface { index, name: adapter_name, @@ -278,6 +280,7 @@ pub fn interfaces() -> Vec { flags, transmit_speed: sys::sanitize_u64(cur.TransmitLinkSpeed), receive_speed: sys::sanitize_u64(cur.ReceiveLinkSpeed), + stats, #[cfg(feature = "gateway")] gateway: if default_gateway.mac_addr == MacAddr::zero() { None diff --git a/src/lib.rs b/src/lib.rs index c354324..78436a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod gateway; pub mod interface; mod ip; pub mod mac; +pub mod stats; mod sys; pub use device::NetworkDevice; diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..0d13489 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,164 @@ +use std::time::SystemTime; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::Interface; + +/// Interface traffic statistics at a given point in time. +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct InterfaceStats { + /// Total received bytes on this interface. + pub rx_bytes: u64, + /// Total transmitted bytes on this interface. + pub tx_bytes: u64, + /// The system timestamp when this snapshot was taken. + /// May be `None` if the platform does not support it. + pub timestamp: Option, +} + +#[cfg(any( + target_vendor = "apple", + target_os = "openbsd", + target_os = "freebsd", + target_os = "netbsd" +))] +pub(crate) fn get_stats(ifa: Option<&libc::ifaddrs>, _name: &str) -> Option { + if let Some(ifa) = ifa { + if !ifa.ifa_data.is_null() { + let data = unsafe { &*(ifa.ifa_data as *const libc::if_data) }; + Some(InterfaceStats { + rx_bytes: data.ifi_ibytes as u64, + tx_bytes: data.ifi_obytes as u64, + timestamp: Some(SystemTime::now()), + }) + } else { + None + } + } else { + None + } +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub(crate) fn get_stats(_ifa: Option<&libc::ifaddrs>, name: &str) -> Option { + get_stats_from_name(name) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +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); + let rx_bytes = match read_to_string(rx_path) { + Ok(s) => s.trim().parse::().unwrap_or(0), + Err(_) => 0, + }; + let tx_bytes = match read_to_string(tx_path) { + Ok(s) => s.trim().parse::().unwrap_or(0), + Err(_) => 0, + }; + Some(InterfaceStats { + rx_bytes, + tx_bytes, + timestamp: Some(SystemTime::now()), + }) +} + +#[cfg(any( + target_vendor = "apple", + target_os = "openbsd", + target_os = "freebsd", + target_os = "netbsd" +))] +fn get_stats_from_name(name: &str) -> Option { + use std::ffi::CStr; + let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut(); + + // 1. getifaddrs() + if unsafe { libc::getifaddrs(&mut ifap) } != 0 { + return None; + } + + let mut current = ifap; + let mut result = None; + + // 2. Iterate through the list of ifaddrs + while !current.is_null() { + unsafe { + let ifa = &*current; + + if ifa.ifa_name.is_null() { + current = ifa.ifa_next; + continue; + } + + let ifa_name = CStr::from_ptr(ifa.ifa_name).to_string_lossy(); + + if ifa_name == name { + if !ifa.ifa_data.is_null() { + let data = &*(ifa.ifa_data as *const libc::if_data); + result = Some(InterfaceStats { + rx_bytes: data.ifi_ibytes as u64, + tx_bytes: data.ifi_obytes as u64, + timestamp: Some(SystemTime::now()), + }); + break; + } + } + + current = ifa.ifa_next; + } + } + + // 3. freeifaddrs() + unsafe { + libc::freeifaddrs(ifap); + } + + result +} + +#[cfg(target_os = "windows")] +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 + } + } +} + +pub(crate) fn update_interface_stats(iface: &mut Interface) -> std::io::Result<()> { + #[cfg(any(target_os = "linux", target_os = "android"))] + { + iface.stats = get_stats_from_name(iface.name.as_str()); + } + #[cfg(any( + target_vendor = "apple", + target_os = "openbsd", + target_os = "freebsd", + target_os = "netbsd" + ))] + { + iface.stats = get_stats_from_name(iface.name.as_str()); + } + #[cfg(target_os = "windows")] + { + iface.stats = get_stats_from_index(iface.index); + } + Ok(()) +}