From dfcc6890b09d4bf565c12db2dc5f4ddad5927bee Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 29 Jun 2025 21:47:18 +0900 Subject: [PATCH 1/4] Add cross build scripts --- .gitattributes | 1 + scripts/build-all.ps1 | 20 ++++++++++++++++++++ scripts/build-all.sh | 25 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 .gitattributes create mode 100644 scripts/build-all.ps1 create mode 100755 scripts/build-all.sh 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/scripts/build-all.ps1 b/scripts/build-all.ps1 new file mode 100644 index 0000000..1cc46e8 --- /dev/null +++ b/scripts/build-all.ps1 @@ -0,0 +1,20 @@ +# build-all.ps1 + +$targets = @( + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-unknown-freebsd", + "aarch64-linux-android", + "x86_64-linux-android" +) + +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!" From 85c9b7a8fdc7c6136bc2f5932b2b17ab48b27a62 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 29 Jun 2025 22:03:02 +0900 Subject: [PATCH 2/4] Update build-all.ps1 --- scripts/build-all.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-all.ps1 b/scripts/build-all.ps1 index 1cc46e8..7c5581b 100644 --- a/scripts/build-all.ps1 +++ b/scripts/build-all.ps1 @@ -1,5 +1,4 @@ -# build-all.ps1 - +# target platforms $targets = @( "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", @@ -8,6 +7,7 @@ $targets = @( "x86_64-linux-android" ) +# cross build foreach ($target in $targets) { Write-Host "==> Building for $target..." $result = & cross build --target $target From 5506dfa5223df444ca3b70793a2448e347bf364a Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 29 Jun 2025 22:12:55 +0900 Subject: [PATCH 3/4] #120 Add interface traffic stats --- Cargo.toml | 4 + examples/default_interface.rs | 1 + examples/list_interfaces.rs | 1 + examples/stats.rs | 37 ++++++++ src/interface/android.rs | 7 ++ src/interface/mod.rs | 16 ++++ src/interface/unix.rs | 7 ++ src/interface/windows.rs | 3 + src/lib.rs | 1 + src/stats.rs | 160 ++++++++++++++++++++++++++++++++++ 10 files changed, 237 insertions(+) create mode 100644 examples/stats.rs create mode 100644 src/stats.rs 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..a0340fd --- /dev/null +++ b/examples/stats.rs @@ -0,0 +1,37 @@ +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/src/interface/android.rs b/src/interface/android.rs index a608f4b..d5bf9b2 100644 --- a/src/interface/android.rs +++ b/src/interface/android.rs @@ -66,6 +66,8 @@ pub mod netlink { mac::MacAddr, }; + use crate::stats::{InterfaceStats, get_stats}; + 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..3eeff35 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::{InterfaceStats, get_stats}; 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..13e0136 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod interface; mod ip; pub mod mac; mod sys; +pub mod stats; pub use device::NetworkDevice; #[cfg(feature = "gateway")] diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..b27c657 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,160 @@ +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 windows_sys::Win32::NetworkManagement::IpHelper::{GetIfEntry2, MIB_IF_ROW2}; + use std::mem::zeroed; + use std::time::SystemTime; + + 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(()) +} From eed9a9507dae63ad0955164db3a99f5bdede89d7 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 29 Jun 2025 22:14:36 +0900 Subject: [PATCH 4/4] format with cargo fmt --- examples/stats.rs | 9 +++++++-- src/interface/android.rs | 2 +- src/interface/unix.rs | 2 +- src/lib.rs | 2 +- src/stats.rs | 16 ++++++++++------ 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/examples/stats.rs b/examples/stats.rs index a0340fd..8d16f91 100644 --- a/examples/stats.rs +++ b/examples/stats.rs @@ -5,7 +5,10 @@ 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); + println!( + "Monitoring default interface: [{}]{}\n", + iface.index, iface.name + ); // Initial stats println!("[Initial stats]"); @@ -27,7 +30,9 @@ fn print_stats(iface: &Interface) { Some(stats) => { println!( "RX: {:>12} bytes, TX: {:>12} bytes at {:?}", - stats.rx_bytes, stats.tx_bytes, stats.timestamp.unwrap_or(SystemTime::UNIX_EPOCH) + stats.rx_bytes, + stats.tx_bytes, + stats.timestamp.unwrap_or(SystemTime::UNIX_EPOCH) ); } None => { diff --git a/src/interface/android.rs b/src/interface/android.rs index d5bf9b2..c91965b 100644 --- a/src/interface/android.rs +++ b/src/interface/android.rs @@ -66,7 +66,7 @@ pub mod netlink { mac::MacAddr, }; - use crate::stats::{InterfaceStats, get_stats}; + use crate::stats::{get_stats, InterfaceStats}; pub fn unix_interfaces() -> Vec { let mut ifaces = Vec::new(); diff --git a/src/interface/unix.rs b/src/interface/unix.rs index 3eeff35..f80b31b 100644 --- a/src/interface/unix.rs +++ b/src/interface/unix.rs @@ -4,7 +4,7 @@ use super::MacAddr; use crate::gateway; use crate::interface::InterfaceType; use crate::ipnet::{Ipv4Net, Ipv6Net}; -use crate::stats::{InterfaceStats, get_stats}; +use crate::stats::{get_stats, InterfaceStats}; use crate::sys; use libc; use std::ffi::{CStr, CString}; diff --git a/src/lib.rs b/src/lib.rs index 13e0136..78436a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,8 @@ pub mod gateway; pub mod interface; mod ip; pub mod mac; -mod sys; pub mod stats; +mod sys; pub use device::NetworkDevice; #[cfg(feature = "gateway")] diff --git a/src/stats.rs b/src/stats.rs index b27c657..0d13489 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -28,10 +28,10 @@ pub(crate) fn get_stats(ifa: Option<&libc::ifaddrs>, _name: &str) -> Option Option { Ok(s) => s.trim().parse::().unwrap_or(0), Err(_) => 0, }; - Some(InterfaceStats { rx_bytes, tx_bytes, timestamp: Some(SystemTime::now()) }) + Some(InterfaceStats { + rx_bytes, + tx_bytes, + timestamp: Some(SystemTime::now()), + }) } #[cfg(any( @@ -118,9 +122,9 @@ fn get_stats_from_name(name: &str) -> Option { #[cfg(target_os = "windows")] pub(crate) fn get_stats_from_index(index: u32) -> Option { - use windows_sys::Win32::NetworkManagement::IpHelper::{GetIfEntry2, MIB_IF_ROW2}; 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;