use serde::Serialize;
use sysinfo::Networks;
use super::DiagnosticResult;
#[derive(Debug, Clone, Serialize)]
pub struct InterfaceInfo {
pub name: String,
pub mac: String,
pub ip_addresses: Vec<String>,
pub is_up: bool,
pub interface_type: String,
pub rx_bytes: u64,
pub tx_bytes: u64,
}
struct RawInterface {
name: String,
mac_bytes: [u8; 6],
ip_addrs: Vec<String>,
rx_bytes: u64,
tx_bytes: u64,
}
pub async fn check() -> (DiagnosticResult, Vec<InterfaceInfo>) {
let raw_interfaces: Vec<RawInterface> = tokio::task::spawn_blocking(|| {
let networks = Networks::new_with_refreshed_list();
networks
.iter()
.map(|(name, data)| RawInterface {
name: name.clone(),
mac_bytes: data.mac_address().0,
ip_addrs: data
.ip_networks()
.iter()
.map(|n| n.addr.to_string())
.collect(),
rx_bytes: data.total_received(),
tx_bytes: data.total_transmitted(),
})
.collect()
})
.await
.unwrap_or_default();
let mut details = Vec::new();
let mut active_count = 0;
let mut wifi_info = String::new();
for raw in raw_interfaces {
let mac = format_mac(raw.mac_bytes);
let ip_addrs = raw.ip_addrs;
let is_up = !ip_addrs.is_empty() && raw.rx_bytes > 0;
let iface_type = detect_interface_type(&raw.name);
if is_up {
active_count += 1;
if iface_type == "Wi-Fi" && wifi_info.is_empty() {
wifi_info = get_wifi_summary().await;
}
}
details.push(InterfaceInfo {
name: raw.name,
mac,
ip_addresses: ip_addrs,
is_up,
interface_type: iface_type,
rx_bytes: raw.rx_bytes,
tx_bytes: raw.tx_bytes,
});
}
let result = if active_count == 0 {
DiagnosticResult::fail("Network", "No active network interfaces found")
} else if !wifi_info.is_empty() {
DiagnosticResult::ok("Network", format!("Connected via {}", wifi_info))
} else if active_count == 1 {
let active = details.iter().find(|i| i.is_up);
let desc = match active {
Some(iface) => format!("Connected via {}", iface.interface_type),
None => "Connected".to_string(),
};
DiagnosticResult::ok("Network", desc)
} else {
DiagnosticResult::ok("Network", format!("{} active interfaces", active_count))
};
(result, details)
}
fn detect_interface_type(name: &str) -> String {
let lower = name.to_lowercase();
if lower.contains("wi-fi")
|| lower.contains("wifi")
|| lower.contains("wlan")
|| lower.contains("wlp")
{
"Wi-Fi".to_string()
} else if lower.contains("eth")
|| lower.contains("enp")
|| lower.contains("eno")
|| lower.contains("ethernet")
{
"Ethernet".to_string()
} else if lower == "lo" || lower == "lo0" || (lower.starts_with("lo") && lower.len() <= 3) {
"Loopback".to_string()
} else if lower.contains("tun")
|| lower.contains("tap")
|| lower.contains("wg")
|| lower.contains("utun")
{
"VPN/Tunnel".to_string()
} else if lower.contains("bluetooth") || lower.contains("bnep") {
"Bluetooth".to_string()
} else if lower.contains("docker") || lower.contains("veth") || lower.contains("br-") {
"Virtual".to_string()
} else {
"Unknown".to_string()
}
}
fn format_mac(bytes: [u8; 6]) -> String {
if bytes == [0, 0, 0, 0, 0, 0] {
return "N/A".to_string();
}
format!(
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]
)
}
async fn get_wifi_summary() -> String {
#[cfg(windows)]
{
let mut cmd = tokio::process::Command::new("netsh");
cmd.args(["wlan", "show", "interfaces"]);
match super::util::run_with_timeout(cmd, super::util::QUICK).await {
Some(output) => {
let text = String::from_utf8_lossy(&output.stdout);
let mut band = String::new();
let mut ssid = String::new();
for line in text.lines() {
let line = line.trim();
if line.starts_with("SSID")
&& !line.starts_with("SSID B")
&& !line.starts_with("SSID name")
{
if let Some(val) = line.split(':').nth(1) {
ssid = val.trim().to_string();
}
}
if line.starts_with("Radio type") || line.starts_with("Band") {
if let Some(val) = line.split(':').nth(1) {
let val = val.trim();
if val.contains("6 GHz") || val.contains("6E") {
band = "6 GHz".to_string();
} else if val.contains("5 GHz")
|| val.contains("802.11a")
|| val.contains("802.11ac")
{
band = "5 GHz".to_string();
} else if val.contains("802.11ax") {
band = String::new();
} else {
band = "2.4 GHz".to_string();
}
}
}
if line.starts_with("Channel") {
if let Some(val) = line.split(':').nth(1) {
if let Ok(ch) = val.trim().parse::<u32>() {
if band.is_empty() {
if ch > 14 && ch <= 177 {
band = "5 GHz".to_string();
} else if ch <= 14 {
band = "2.4 GHz".to_string();
}
}
}
}
}
}
if !ssid.is_empty() {
if !band.is_empty() {
format!("Wi-Fi ({}) - {}", band, ssid)
} else {
format!("Wi-Fi - {}", ssid)
}
} else {
"Wi-Fi".to_string()
}
}
None => "Wi-Fi".to_string(),
}
}
#[cfg(target_os = "macos")]
{
let mut cmd = tokio::process::Command::new("/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport");
cmd.args(["-I"]);
match super::util::run_with_timeout(cmd, super::util::QUICK).await {
Some(output) => {
let text = String::from_utf8_lossy(&output.stdout);
let mut ssid = String::new();
let mut channel = 0u32;
for line in text.lines() {
let line = line.trim();
if line.starts_with("SSID:") {
ssid = line
.split(':')
.nth(1)
.map(|s| s.trim().to_string())
.unwrap_or_default();
}
if line.starts_with("channel:") {
if let Some(val) = line.split(':').nth(1) {
channel = val
.trim()
.split(',')
.next()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
}
}
}
let band = if channel > 14 && channel <= 177 {
"5 GHz"
} else if channel <= 14 && channel > 0 {
"2.4 GHz"
} else {
""
};
if !ssid.is_empty() {
if !band.is_empty() {
format!("Wi-Fi ({}) - {}", band, ssid)
} else {
format!("Wi-Fi - {}", ssid)
}
} else {
"Wi-Fi".to_string()
}
}
None => "Wi-Fi".to_string(),
}
}
#[cfg(target_os = "linux")]
{
let mut cmd = tokio::process::Command::new("iwgetid");
cmd.args(["-r"]);
match super::util::run_with_timeout(cmd, super::util::QUICK).await {
Some(output) => {
let ssid = String::from_utf8_lossy(&output.stdout).trim().to_string();
let mut freq_cmd = tokio::process::Command::new("iwgetid");
freq_cmd.args(["--freq", "-r"]);
let band = match super::util::run_with_timeout(freq_cmd, super::util::QUICK).await {
Some(freq_out) => {
let freq_str = String::from_utf8_lossy(&freq_out.stdout).trim().to_string();
if let Ok(freq) = freq_str.parse::<f64>() {
if freq > 5.0 {
"5 GHz".to_string()
} else if freq > 2.0 {
"2.4 GHz".to_string()
} else {
String::new()
}
} else {
String::new()
}
}
None => String::new(),
};
if !ssid.is_empty() {
if !band.is_empty() {
format!("Wi-Fi ({}) - {}", band, ssid)
} else {
format!("Wi-Fi - {}", ssid)
}
} else {
"Wi-Fi".to_string()
}
}
None => "Wi-Fi".to_string(),
}
}
}