diff --git a/Cargo.toml b/Cargo.toml index ca02956..f1c401e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ netlink-sys = "0.8.8" [target.'cfg(target_os = "android")'.dependencies] dlopen2 = { version = "0.8.2", default-features = false } +jni = { version = "0.21.1", optional = true } +ndk-context = { version = "0.1.1", optional = true } once_cell = "1" [target.'cfg(windows)'.dependencies.windows-sys] @@ -53,9 +55,10 @@ block2 = "0.6" serde_json = "1.0" [features] -default = ["gateway", "apple-system-configuration-extra"] +default = ["gateway", "apple-system-configuration-extra", "android-extra"] serde = ["dep:serde", "mac-addr/serde", "ipnet/serde"] gateway = [] +android-extra = ["dep:jni", "dep:ndk-context"] apple-system-configuration-extra = [ "objc2-system-configuration/SCPreferences", "objc2-system-configuration/SCDynamicStore", diff --git a/README.md b/README.md index f5090c8..dacbddd 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,9 @@ For more details, see [examples][examples-url] or [doc][doc-url]. - `apple-system-configuration-extra` (default) - Enables deeper Apple metadata enrichment using `SystemConfiguration` APIs. - On Apple targets, this adds metadata such as interface display names, DHCP hints, and iOS DNS resolver lookup when the platform exposes them. +- `android-extra` (default) + - Enables deeper Android metadata enrichment using Android platform APIs through JNI bindings. + - On Android, this can add metadata such as traffic stats, DNS servers, DHCP hints, and Wi-Fi link speed when the app provides the required Android context and permissions. To opt out of the deeper Apple enrichment while keeping gateway helpers: @@ -62,6 +65,9 @@ netdev = { version = "0.43", default-features = false, features = ["gateway"] } `netdev` links `SystemConfiguration.framework` automatically on `macOS` and `iOS` through the crate's own build script. If your final app link is performed by Xcode, you may still need to add `SystemConfiguration.framework` to the app target manually. +## Android behavior +If you want Android-specific values such as DNS servers, DHCP hints, or Wi-Fi link speed, your app may still need to initialize the Android context for Rust and declare Android permissions such as `ACCESS_NETWORK_STATE` and `ACCESS_WIFI_STATE`. + ## Project History This crate was originally published as [default-net][default-net-crates-io-url] and later rebranded to `netdev` by the author myself for future expansion, clearer naming, and long-term maintenance. diff --git a/src/os/android/api.rs b/src/os/android/api.rs new file mode 100644 index 0000000..5351f66 --- /dev/null +++ b/src/os/android/api.rs @@ -0,0 +1,520 @@ +use crate::stats::counters::InterfaceStats; +use jni::JavaVM; +use jni::objects::{JByteArray, JObject, JObjectArray, JString, JValue}; +use std::collections::{HashMap, HashSet}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::panic::{self, PanicHookInfo}; +use std::sync::{Mutex, OnceLock}; +use std::time::SystemTime; + +#[derive(Clone, Debug, Default)] +pub(crate) struct InterfaceExtras { + pub(crate) transmit_speed: Option, + pub(crate) receive_speed: Option, + pub(crate) auto_negotiate: Option, + pub(crate) dhcp_v4_enabled: Option, + pub(crate) dhcp_v6_enabled: Option, + pub(crate) stats: Option, + #[cfg(feature = "gateway")] + pub(crate) dns_servers: Vec, +} + +pub(crate) fn get_interface_stats(name: &str) -> Option { + with_android_env(|env, _| { + let name = env.new_string(name).ok()?; + let rx_bytes = call_static_long( + env, + "android/net/TrafficStats", + "getRxBytes", + "(Ljava/lang/String;)J", + &[JValue::Object(name.as_ref())], + )?; + let tx_bytes = call_static_long( + env, + "android/net/TrafficStats", + "getTxBytes", + "(Ljava/lang/String;)J", + &[JValue::Object(name.as_ref())], + )?; + + if rx_bytes < 0 || tx_bytes < 0 { + return None; + } + + Some(InterfaceStats { + rx_bytes: rx_bytes as u64, + tx_bytes: tx_bytes as u64, + timestamp: Some(SystemTime::now()), + }) + }) +} + +pub(crate) fn collect_interface_extras(if_names: &[String]) -> HashMap { + let mut extras = HashMap::new(); + + with_android_env(|env, context| { + populate_traffic_stats(env, if_names, &mut extras); + + let mut wifi_ifaces = HashSet::new(); + populate_connectivity_extras(env, context, &mut extras, &mut wifi_ifaces); + populate_wifi_speed(env, context, &mut extras, &wifi_ifaces); + + Some(()) + }); + + extras +} + +fn with_android_env(f: F) -> Option +where + F: FnOnce(&mut jni::AttachGuard<'_>, &JObject<'static>) -> Option, +{ + let ctx = try_android_context()?; + let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()) }.ok()?; + let mut env = vm.attach_current_thread().ok()?; + let context = unsafe { JObject::from_raw(ctx.context().cast()) }; + if is_null(&env, &context) { + return None; + } + f(&mut env, &context) +} + +fn try_android_context() -> Option { + static ANDROID_CONTEXT_AVAILABLE: OnceLock = OnceLock::new(); + + if *ANDROID_CONTEXT_AVAILABLE.get_or_init(try_init_android_context) { + Some(ndk_context::android_context()) + } else { + None + } +} + +fn try_init_android_context() -> bool { + static PANIC_HOOK_GUARD: Mutex<()> = Mutex::new(()); + + let Ok(_guard) = PANIC_HOOK_GUARD.lock() else { + return false; + }; + let previous_hook = panic::take_hook(); + panic::set_hook(Box::new(silent_panic_hook)); + let result = panic::catch_unwind(ndk_context::android_context).is_ok(); + panic::set_hook(previous_hook); + result +} + +fn silent_panic_hook(_info: &PanicHookInfo<'_>) {} + +fn populate_traffic_stats( + env: &mut jni::AttachGuard<'_>, + if_names: &[String], + extras: &mut HashMap, +) { + for if_name in if_names { + if let Some(stats) = get_traffic_stats(env, if_name) { + extras.entry(if_name.clone()).or_default().stats = Some(stats); + } + } +} + +fn populate_connectivity_extras( + env: &mut jni::AttachGuard<'_>, + context: &JObject<'static>, + extras: &mut HashMap, + wifi_ifaces: &mut HashSet, +) { + let Some(connectivity_manager) = get_system_service(env, context, "CONNECTIVITY_SERVICE") + else { + return; + }; + if is_null(env, &connectivity_manager) { + return; + } + + let Some(networks_obj) = call_object_method( + env, + &connectivity_manager, + "getAllNetworks", + "()[Landroid/net/Network;", + &[], + ) else { + return; + }; + if is_null(env, &networks_obj) { + return; + } + + let networks = JObjectArray::from(networks_obj); + let Ok(length) = env.get_array_length(&networks) else { + return; + }; + + let transport_wifi = get_static_int( + env, + "android/net/NetworkCapabilities", + "TRANSPORT_WIFI", + "I", + ); + + for index in 0..length { + let Ok(network) = env.get_object_array_element(&networks, index) else { + clear_pending_exception(env); + continue; + }; + if is_null(env, &network) { + continue; + } + + let Some(link_properties) = call_object_method( + env, + &connectivity_manager, + "getLinkProperties", + "(Landroid/net/Network;)Landroid/net/LinkProperties;", + &[JValue::Object(network.as_ref())], + ) else { + continue; + }; + if is_null(env, &link_properties) { + continue; + } + + let Some(if_name) = call_string_method( + env, + &link_properties, + "getInterfaceName", + "()Ljava/lang/String;", + &[], + ) else { + continue; + }; + + #[cfg(feature = "gateway")] + if let Some(dns_servers) = get_dns_servers(env, &link_properties) { + extras.entry(if_name.clone()).or_default().dns_servers = dns_servers; + } + + if has_dhcp_v4(env, &link_properties) { + extras.entry(if_name.clone()).or_default().dhcp_v4_enabled = Some(true); + } + + if let Some(transport_wifi) = transport_wifi + && network_has_transport(env, &connectivity_manager, &network, transport_wifi) + { + wifi_ifaces.insert(if_name); + } + } +} + +fn populate_wifi_speed( + env: &mut jni::AttachGuard<'_>, + context: &JObject<'static>, + extras: &mut HashMap, + wifi_ifaces: &HashSet, +) { + if wifi_ifaces.is_empty() { + return; + } + + let Some(wifi_manager) = get_system_service(env, context, "WIFI_SERVICE") else { + return; + }; + if is_null(env, &wifi_manager) { + return; + } + + let Some(wifi_info) = call_object_method( + env, + &wifi_manager, + "getConnectionInfo", + "()Landroid/net/wifi/WifiInfo;", + &[], + ) else { + return; + }; + if is_null(env, &wifi_info) { + return; + } + + let tx_speed = call_int_method(env, &wifi_info, "getTxLinkSpeedMbps", "()I", &[]) + .filter(|speed| *speed > 0) + .map(|speed| (speed as u64) * 1_000_000); + let rx_speed = call_int_method(env, &wifi_info, "getRxLinkSpeedMbps", "()I", &[]) + .filter(|speed| *speed > 0) + .map(|speed| (speed as u64) * 1_000_000); + let link_speed = call_int_method(env, &wifi_info, "getLinkSpeed", "()I", &[]) + .filter(|speed| *speed > 0) + .map(|speed| (speed as u64) * 1_000_000); + + for if_name in wifi_ifaces { + let extra = extras.entry(if_name.clone()).or_default(); + if extra.transmit_speed.is_none() { + extra.transmit_speed = tx_speed.or(link_speed); + } + if extra.receive_speed.is_none() { + extra.receive_speed = rx_speed.or(link_speed); + } + } +} + +fn get_traffic_stats(env: &mut jni::AttachGuard<'_>, if_name: &str) -> Option { + let if_name = env.new_string(if_name).ok()?; + let rx_bytes = call_static_long( + env, + "android/net/TrafficStats", + "getRxBytes", + "(Ljava/lang/String;)J", + &[JValue::Object(if_name.as_ref())], + )?; + let tx_bytes = call_static_long( + env, + "android/net/TrafficStats", + "getTxBytes", + "(Ljava/lang/String;)J", + &[JValue::Object(if_name.as_ref())], + )?; + + if rx_bytes < 0 || tx_bytes < 0 { + return None; + } + + Some(InterfaceStats { + rx_bytes: rx_bytes as u64, + tx_bytes: tx_bytes as u64, + timestamp: Some(SystemTime::now()), + }) +} + +fn get_system_service( + env: &mut jni::AttachGuard<'_>, + context: &JObject<'static>, + field_name: &str, +) -> Option> { + let service = env + .get_static_field("android/content/Context", field_name, "Ljava/lang/String;") + .ok()? + .l() + .ok()?; + let service_obj = call_object_method( + env, + context, + "getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + &[JValue::Object(service.as_ref())], + )?; + Some(unsafe { JObject::from_raw(service_obj.into_raw()) }) +} + +#[cfg(feature = "gateway")] +fn get_dns_servers( + env: &mut jni::AttachGuard<'_>, + link_properties: &JObject<'_>, +) -> Option> { + let dns_list = call_object_method( + env, + link_properties, + "getDnsServers", + "()Ljava/util/List;", + &[], + )?; + if is_null(env, &dns_list) { + return Some(Vec::new()); + } + + let size = call_int_method(env, &dns_list, "size", "()I", &[])? as i32; + let mut dns_servers = Vec::new(); + + for index in 0..size { + let Some(entry) = call_object_method( + env, + &dns_list, + "get", + "(I)Ljava/lang/Object;", + &[JValue::Int(index)], + ) else { + continue; + }; + if let Some(ip) = inet_address_to_ip(env, &entry) + && !dns_servers.contains(&ip) + { + dns_servers.push(ip); + } + } + + Some(dns_servers) +} + +fn has_dhcp_v4(env: &mut jni::AttachGuard<'_>, link_properties: &JObject<'_>) -> bool { + let Some(server) = call_object_method( + env, + link_properties, + "getDhcpServerAddress", + "()Ljava/net/Inet4Address;", + &[], + ) else { + return false; + }; + !is_null(env, &server) +} + +fn network_has_transport( + env: &mut jni::AttachGuard<'_>, + connectivity_manager: &JObject<'_>, + network: &JObject<'_>, + transport: i32, +) -> bool { + let Some(capabilities) = call_object_method( + env, + connectivity_manager, + "getNetworkCapabilities", + "(Landroid/net/Network;)Landroid/net/NetworkCapabilities;", + &[JValue::Object(network.as_ref())], + ) else { + return false; + }; + if is_null(env, &capabilities) { + return false; + } + + call_bool_method( + env, + &capabilities, + "hasTransport", + "(I)Z", + &[JValue::Int(transport)], + ) + .unwrap_or(false) +} + +fn inet_address_to_ip( + env: &mut jni::AttachGuard<'_>, + inet_address: &JObject<'_>, +) -> Option { + let addr = call_object_method(env, inet_address, "getAddress", "()[B", &[])?; + if is_null(env, &addr) { + return None; + } + + let bytes = env.convert_byte_array(&JByteArray::from(addr)).ok()?; + match bytes.as_slice() { + [a, b, c, d] => Some(IpAddr::V4(Ipv4Addr::new(*a, *b, *c, *d))), + bytes if bytes.len() == 16 => { + let mut octets = [0u8; 16]; + octets.copy_from_slice(bytes); + Some(IpAddr::V6(Ipv6Addr::from(octets))) + } + _ => None, + } +} + +fn call_object_method( + env: &mut jni::AttachGuard<'_>, + obj: &JObject<'_>, + name: &str, + sig: &str, + args: &[JValue], +) -> Option> { + let value = env.call_method(obj, name, sig, args); + match value { + Ok(value) => value + .l() + .ok() + .map(|obj| unsafe { JObject::from_raw(obj.into_raw()) }), + Err(_) => { + clear_pending_exception(env); + None + } + } +} + +fn call_string_method( + env: &mut jni::AttachGuard<'_>, + obj: &JObject<'_>, + name: &str, + sig: &str, + args: &[JValue], +) -> Option { + let value = call_object_method(env, obj, name, sig, args)?; + if is_null(env, &value) { + return None; + } + + let value = JString::from(value); + env.get_string(&value).ok().map(|s| s.into()) +} + +fn call_int_method( + env: &mut jni::AttachGuard<'_>, + obj: &JObject<'_>, + name: &str, + sig: &str, + args: &[JValue], +) -> Option { + let value = env.call_method(obj, name, sig, args); + match value { + Ok(value) => value.i().ok(), + Err(_) => { + clear_pending_exception(env); + None + } + } +} + +fn call_bool_method( + env: &mut jni::AttachGuard<'_>, + obj: &JObject<'_>, + name: &str, + sig: &str, + args: &[JValue], +) -> Option { + let value = env.call_method(obj, name, sig, args); + match value { + Ok(value) => value.z().ok(), + Err(_) => { + clear_pending_exception(env); + None + } + } +} + +fn call_static_long( + env: &mut jni::AttachGuard<'_>, + class: &str, + name: &str, + sig: &str, + args: &[JValue], +) -> Option { + let value = env.call_static_method(class, name, sig, args); + match value { + Ok(value) => value.j().ok(), + Err(_) => { + clear_pending_exception(env); + None + } + } +} + +fn get_static_int( + env: &mut jni::AttachGuard<'_>, + class: &str, + name: &str, + sig: &str, +) -> Option { + let value = env.get_static_field(class, name, sig); + match value { + Ok(value) => value.i().ok(), + Err(_) => { + clear_pending_exception(env); + None + } + } +} + +fn is_null(env: &jni::AttachGuard<'_>, obj: &JObject<'_>) -> bool { + let null = JObject::null(); + env.is_same_object(obj, &null).unwrap_or(true) +} + +fn clear_pending_exception(env: &jni::AttachGuard<'_>) { + if env.exception_check().unwrap_or(false) { + let _ = env.exception_clear(); + } +} diff --git a/src/os/android/interface.rs b/src/os/android/interface.rs index c6c46e5..a6558ca 100644 --- a/src/os/android/interface.rs +++ b/src/os/android/interface.rs @@ -1,6 +1,7 @@ use super::netlink; use crate::interface::interface::Interface; use crate::interface::state::OperState; +use crate::interface::types::InterfaceType; use crate::ipnet::{Ipv4Net, Ipv6Net}; use crate::net::mac::MacAddr; use std::net::{Ipv4Addr, Ipv6Addr}; @@ -23,14 +24,24 @@ fn push_ipv4(v: &mut Vec, add: (Ipv4Addr, u8)) { } } -fn push_ipv6(v: &mut Vec, add: (Ipv6Addr, u8)) -> bool { - if v.iter() +fn push_ipv6( + addrs: &mut Vec, + scope_ids: &mut Vec, + addr_flags: &mut Vec, + add: (Ipv6Addr, u8), + scope_id: u32, + flags: crate::interface::ipv6_addr_flags::Ipv6AddrFlags, +) -> bool { + if addrs + .iter() .any(|n| n.addr() == add.0 && n.prefix_len() == add.1) { return false; } if let Ok(net) = Ipv6Net::new(add.0, add.1) { - v.push(net); + addrs.push(net); + scope_ids.push(scope_id); + addr_flags.push(flags); return true; } false @@ -38,18 +49,117 @@ fn push_ipv6(v: &mut Vec, add: (Ipv6Addr, u8)) -> bool { #[inline] fn calc_v6_scope_id(addr: &Ipv6Addr, ifindex: u32) -> u32 { - let seg0 = addr.segments()[0]; - if (seg0 & 0xffc0) == 0xfe80 { + if addr.is_unicast_link_local() { ifindex } else { 0 } } +fn type_is_ambiguous(if_type: InterfaceType) -> bool { + matches!( + if_type, + InterfaceType::Unknown | InterfaceType::UnknownWithValue(_) | InterfaceType::Ethernet + ) +} + +fn type_is_more_specific(current: InterfaceType, candidate: InterfaceType) -> bool { + if candidate == current { + return false; + } + + match (current, candidate) { + (InterfaceType::Unknown, _) | (InterfaceType::UnknownWithValue(_), _) => true, + (InterfaceType::Ethernet, candidate) => { + matches!( + candidate, + InterfaceType::Loopback + | InterfaceType::Wireless80211 + | InterfaceType::Tunnel + | InterfaceType::Wwan + | InterfaceType::Wwanpp + | InterfaceType::Wwanpp2 + | InterfaceType::Bridge + | InterfaceType::PeerToPeerWireless + | InterfaceType::ProprietaryVirtual + ) + } + (InterfaceType::Wwan, candidate) => { + matches!(candidate, InterfaceType::Wwanpp | InterfaceType::Wwanpp2) + } + _ => false, + } +} + +#[cfg(feature = "android-extra")] +fn finalize_interface(iface: &mut Interface, extras: Option<&super::api::InterfaceExtras>) { + if let Some(sysfs_type) = super::sysfs::get_interface_type(&iface.name) { + if type_is_more_specific(iface.if_type, sysfs_type) { + iface.if_type = sysfs_type; + } + } + + if type_is_ambiguous(iface.if_type) + && let Some(guessed_type) = super::types::guess_type_by_name(&iface.name) + { + iface.if_type = guessed_type; + } + + if let Some(extra) = extras { + if iface.transmit_speed.is_none() { + iface.transmit_speed = extra.transmit_speed; + } + if iface.receive_speed.is_none() { + iface.receive_speed = extra.receive_speed; + } + if iface.auto_negotiate.is_none() { + iface.auto_negotiate = extra.auto_negotiate; + } + if iface.stats.is_none() { + iface.stats = extra.stats.clone(); + } + if iface.dhcp_v4_enabled.is_none() { + iface.dhcp_v4_enabled = extra.dhcp_v4_enabled; + } + if iface.dhcp_v6_enabled.is_none() { + iface.dhcp_v6_enabled = extra.dhcp_v6_enabled; + } + #[cfg(feature = "gateway")] + if iface.dns_servers.is_empty() { + iface.dns_servers = extra.dns_servers.clone(); + } + } + + if iface.transmit_speed.is_none() || iface.receive_speed.is_none() { + let speed = super::sysfs::get_interface_speed(&iface.name); + if iface.transmit_speed.is_none() { + iface.transmit_speed = speed; + } + if iface.receive_speed.is_none() { + iface.receive_speed = speed; + } + } + + if iface.stats.is_none() { + iface.stats = crate::stats::counters::get_stats_from_name(&iface.name); + } + + if iface.mtu.is_none() { + iface.mtu = crate::os::linux::mtu::get_mtu(&iface.name); + } +} + +#[cfg(not(feature = "android-extra"))] fn finalize_interface(iface: &mut Interface) { if let Some(sysfs_type) = super::sysfs::get_interface_type(&iface.name) { - iface.if_type = sysfs_type; - } else if let Some(guessed_type) = super::types::guess_type_by_name(&iface.name) { + if type_is_more_specific(iface.if_type, sysfs_type) { + iface.if_type = sysfs_type; + } + } + + if type_is_ambiguous(iface.if_type) + && let Some(guessed_type) = super::types::guess_type_by_name(&iface.name) + { iface.if_type = guessed_type; } @@ -84,7 +194,7 @@ pub fn interfaces() -> Vec { name: name.clone(), friendly_name: None, description: None, - if_type: super::types::guess_type_by_name(&name).unwrap_or(r.if_type), + if_type: r.if_type, mac_addr: r.mac.map(MacAddr::from_octets), ipv4: Vec::new(), ipv6: Vec::new(), @@ -111,13 +221,17 @@ pub fn interfaces() -> Vec { push_ipv4(&mut iface.ipv4, (a, p)); } for (i, (a, p)) in r.ipv6.into_iter().enumerate() { - if push_ipv6(&mut iface.ipv6, (a, p)) { - iface.ipv6_scope_ids.push(calc_v6_scope_id(&a, iface.index)); - let raw = r.ipv6_addr_flags.get(i).copied().unwrap_or(0); - iface - .ipv6_addr_flags - .push(crate::os::linux::ipv6_addr_flags::from_netlink_flags(raw)); - } + let raw = r.ipv6_addr_flags.get(i).copied().unwrap_or(0); + let flags = crate::os::linux::ipv6_addr_flags::from_netlink_flags(raw); + let scope_id = calc_v6_scope_id(&a, iface.index); + push_ipv6( + &mut iface.ipv6, + &mut iface.ipv6_scope_ids, + &mut iface.ipv6_addr_flags, + (a, p), + scope_id, + flags, + ); } ifaces.push(iface); @@ -129,8 +243,21 @@ pub fn interfaces() -> Vec { } } - for iface in &mut ifaces { - finalize_interface(iface); + #[cfg(feature = "android-extra")] + { + let interface_names: Vec = ifaces.iter().map(|iface| iface.name.clone()).collect(); + let extras = super::api::collect_interface_extras(&interface_names); + + for iface in &mut ifaces { + finalize_interface(iface, extras.get(&iface.name)); + } + } + + #[cfg(not(feature = "android-extra"))] + { + for iface in &mut ifaces { + finalize_interface(iface); + } } // Fill gateway info @@ -155,7 +282,9 @@ pub fn interfaces() -> Vec { if let Some(idx) = crate::interface::pick_default_iface_index(&ifaces, local_ip) { if let Some(iface) = ifaces.iter_mut().find(|it| it.index == idx) { iface.default = true; - iface.dns_servers = get_system_dns_conf(); + if iface.dns_servers.is_empty() { + iface.dns_servers = get_system_dns_conf(); + } } } } @@ -163,3 +292,106 @@ pub fn interfaces() -> Vec { ifaces } + +#[cfg(test)] +mod tests { + use super::{calc_v6_scope_id, push_ipv4, push_ipv6, type_is_ambiguous, type_is_more_specific}; + use crate::interface::ipv6_addr_flags::Ipv6AddrFlags; + use crate::interface::types::InterfaceType; + use crate::ipnet::{Ipv4Net, Ipv6Net}; + use std::net::{Ipv4Addr, Ipv6Addr}; + + #[test] + fn calculates_link_local_scope_id() { + let addr = "fe80::1".parse::().unwrap(); + assert_eq!(calc_v6_scope_id(&addr, 42), 42); + } + + #[test] + fn keeps_global_ipv6_scope_id_zero() { + let addr = "2001:db8::1".parse::().unwrap(); + assert_eq!(calc_v6_scope_id(&addr, 42), 0); + } + + #[test] + fn deduplicates_ipv4_addresses() { + let mut addrs = Vec::::new(); + push_ipv4(&mut addrs, (Ipv4Addr::new(192, 0, 2, 10), 24)); + push_ipv4(&mut addrs, (Ipv4Addr::new(192, 0, 2, 10), 24)); + + assert_eq!(addrs.len(), 1); + } + + #[test] + fn deduplicates_ipv6_addresses() { + let mut addrs = Vec::::new(); + let mut scope_ids = Vec::new(); + let mut addr_flags = Vec::new(); + let first = "fe80::1".parse::().unwrap(); + let second = "2001:db8::1".parse::().unwrap(); + + assert!(push_ipv6( + &mut addrs, + &mut scope_ids, + &mut addr_flags, + (first, 64), + 7, + Ipv6AddrFlags { + temporary: true, + ..Ipv6AddrFlags::default() + }, + )); + assert!(!push_ipv6( + &mut addrs, + &mut scope_ids, + &mut addr_flags, + (first, 64), + 99, + Ipv6AddrFlags { + deprecated: true, + ..Ipv6AddrFlags::default() + }, + )); + assert!(push_ipv6( + &mut addrs, + &mut scope_ids, + &mut addr_flags, + (second, 64), + 0, + Ipv6AddrFlags { + permanent: true, + ..Ipv6AddrFlags::default() + }, + )); + + assert_eq!(addrs.len(), 2); + assert_eq!(scope_ids, vec![7, 0]); + assert_eq!(addr_flags.len(), 2); + assert!(addr_flags[0].temporary); + assert!(addr_flags[1].permanent); + } + + #[test] + fn prefers_more_specific_sysfs_types() { + assert!(type_is_more_specific( + InterfaceType::Ethernet, + InterfaceType::Wireless80211 + )); + assert!(type_is_more_specific( + InterfaceType::Wwan, + InterfaceType::Wwanpp + )); + assert!(!type_is_more_specific( + InterfaceType::Tunnel, + InterfaceType::Wireless80211 + )); + } + + #[test] + fn marks_ambiguous_types() { + assert!(type_is_ambiguous(InterfaceType::Unknown)); + assert!(type_is_ambiguous(InterfaceType::UnknownWithValue(999))); + assert!(type_is_ambiguous(InterfaceType::Ethernet)); + assert!(!type_is_ambiguous(InterfaceType::Tunnel)); + } +} diff --git a/src/os/android/mod.rs b/src/os/android/mod.rs index ca2f419..8b76a71 100644 --- a/src/os/android/mod.rs +++ b/src/os/android/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "android-extra")] +pub mod api; pub mod flags; pub mod interface; pub mod netlink; diff --git a/src/os/android/netlink.rs b/src/os/android/netlink.rs index 49e54f3..c90a787 100644 --- a/src/os/android/netlink.rs +++ b/src/os/android/netlink.rs @@ -338,17 +338,9 @@ fn mtu_from_link(link: &LinkMessage) -> Option { None } -fn if_type_from_link(link: &LinkMessage, name: &str) -> InterfaceType { +fn if_type_from_link(link: &LinkMessage, _name: &str) -> InterfaceType { let arphrd = link.header.link_layer_type as u32; - let mut t = InterfaceType::try_from(arphrd).unwrap_or(InterfaceType::UnknownWithValue(arphrd)); - - // override by name guess - // ARPHRD may be unreliable on some devices - if let Some(guess) = super::types::guess_type_by_name(name) { - t = guess; - } - - t + InterfaceType::try_from(arphrd).unwrap_or(InterfaceType::UnknownWithValue(arphrd)) } fn stats_from_link(link: &LinkMessage) -> Option { @@ -402,7 +394,7 @@ pub struct IfRow { pub fn collect_interfaces() -> io::Result> { let links = dump_links()?; - let addrs = dump_addrs()?; + let addrs = dump_addrs().unwrap_or_default(); let mut base: HashMap = HashMap::new(); for l in links { diff --git a/src/os/android/types.rs b/src/os/android/types.rs index 4ad2802..5f946f6 100644 --- a/src/os/android/types.rs +++ b/src/os/android/types.rs @@ -3,31 +3,30 @@ use crate::interface::types::InterfaceType; pub fn guess_type_by_name(name: &str) -> Option { let n = name.as_bytes(); - // Loopback: lo if n == b"lo" { return Some(InterfaceType::Loopback); } - // Wi-Fi: wlan0 / wlan1 / wifi0 if n.starts_with(b"wlan") || n.starts_with(b"wifi") { return Some(InterfaceType::Wireless80211); } - // Cellular: rmnet_data0 / rmnet0 / ccmni0 / pdp0 - if n.starts_with(b"rmnet") || n.starts_with(b"ccmni") || n.starts_with(b"pdp") { - return Some(InterfaceType::Wwanpp); - } - // Wi-Fi Direct: p2p0 - if n.starts_with(b"p2p") { + if n.starts_with(b"p2p") || n.starts_with(b"swlan") { return Some(InterfaceType::PeerToPeerWireless); } - // Tunnel: tun0 / tap0 / ipsec0 / clat4 + if n.starts_with(b"rmnet") + || n.starts_with(b"rmnet_data") + || n.starts_with(b"ccmni") + || n.starts_with(b"pdp") + { + return Some(InterfaceType::Wwanpp); + } if n.starts_with(b"tun") || n.starts_with(b"tap") || n.starts_with(b"ipsec") || n.starts_with(b"clat") + || n.starts_with(b"v4-") { return Some(InterfaceType::Tunnel); } - // Bridge / veth if n.starts_with(b"br-") || n.starts_with(b"bridge") { return Some(InterfaceType::Bridge); } @@ -37,3 +36,72 @@ pub fn guess_type_by_name(name: &str) -> Option { None } + +#[cfg(test)] +mod tests { + use super::guess_type_by_name; + use crate::interface::types::InterfaceType; + + #[test] + fn test_name_guess_lo() { + assert_eq!(guess_type_by_name("lo"), Some(InterfaceType::Loopback)); + } + + #[test] + fn test_name_guess_wlan() { + assert_eq!( + guess_type_by_name("wlan0"), + Some(InterfaceType::Wireless80211) + ); + assert_eq!( + guess_type_by_name("wifi-aware0"), + Some(InterfaceType::Wireless80211) + ); + } + + #[test] + fn test_name_guess_peer_to_peer_wireless() { + assert_eq!( + guess_type_by_name("p2p0"), + Some(InterfaceType::PeerToPeerWireless) + ); + assert_eq!( + guess_type_by_name("swlan0"), + Some(InterfaceType::PeerToPeerWireless) + ); + } + + #[test] + fn test_name_guess_cellular() { + assert_eq!( + guess_type_by_name("rmnet_data0"), + Some(InterfaceType::Wwanpp) + ); + assert_eq!(guess_type_by_name("ccmni0"), Some(InterfaceType::Wwanpp)); + assert_eq!(guess_type_by_name("pdp0"), Some(InterfaceType::Wwanpp)); + } + + #[test] + fn test_name_guess_tunnel() { + assert_eq!(guess_type_by_name("tun0"), Some(InterfaceType::Tunnel)); + assert_eq!(guess_type_by_name("tap0"), Some(InterfaceType::Tunnel)); + assert_eq!(guess_type_by_name("ipsec0"), Some(InterfaceType::Tunnel)); + assert_eq!(guess_type_by_name("clat4"), Some(InterfaceType::Tunnel)); + assert_eq!(guess_type_by_name("v4-wlan0"), Some(InterfaceType::Tunnel)); + } + + #[test] + fn test_name_guess_bridge_and_virtual() { + assert_eq!(guess_type_by_name("br-lan"), Some(InterfaceType::Bridge)); + assert_eq!(guess_type_by_name("bridge0"), Some(InterfaceType::Bridge)); + assert_eq!( + guess_type_by_name("veth1234"), + Some(InterfaceType::ProprietaryVirtual) + ); + } + + #[test] + fn test_name_guess_ambiguous_usb() { + assert_eq!(guess_type_by_name("usb0"), None); + } +} diff --git a/src/stats/counters.rs b/src/stats/counters.rs index 656b63d..6a512ae 100644 --- a/src/stats/counters.rs +++ b/src/stats/counters.rs @@ -50,8 +50,29 @@ pub(crate) fn get_stats(_ifa: Option<&libc::ifaddrs>, name: &str) -> Option 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 = read_to_string(rx_path).ok()?.trim().parse::().ok()?; + let tx_bytes = read_to_string(tx_path).ok()?.trim().parse::().ok()?; + + Some(InterfaceStats { + rx_bytes, + tx_bytes, + timestamp: Some(SystemTime::now()), + }) +} + +#[cfg(target_os = "android")] pub(crate) fn get_stats_from_name(name: &str) -> Option { + #[cfg(feature = "android-extra")] + if let Some(stats) = crate::os::android::api::get_interface_stats(name) { + return Some(stats); + } + 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);