Skip to main content

nd_300/diagnostics/
interfaces.rs

1use serde::Serialize;
2use sysinfo::Networks;
3
4use super::DiagnosticResult;
5
6#[derive(Debug, Clone, Serialize)]
7pub struct InterfaceInfo {
8    pub name: String,
9    pub mac: String,
10    pub ip_addresses: Vec<String>,
11    pub is_up: bool,
12    pub interface_type: String,
13    pub rx_bytes: u64,
14    pub tx_bytes: u64,
15}
16
17pub async fn check() -> (DiagnosticResult, Vec<InterfaceInfo>) {
18    let networks = Networks::new_with_refreshed_list();
19    let mut details = Vec::new();
20    let mut active_count = 0;
21    let mut wifi_info = String::new();
22
23    for (name, data) in &networks {
24        let mac = format_mac(data.mac_address().0);
25        let ip_addrs: Vec<String> = data
26            .ip_networks()
27            .iter()
28            .map(|n| n.addr.to_string())
29            .collect();
30        let is_up = !ip_addrs.is_empty() && data.total_received() > 0;
31
32        let iface_type = detect_interface_type(name);
33
34        if is_up {
35            active_count += 1;
36            if iface_type == "Wi-Fi" && wifi_info.is_empty() {
37                wifi_info = get_wifi_summary().await;
38            }
39        }
40
41        details.push(InterfaceInfo {
42            name: name.clone(),
43            mac,
44            ip_addresses: ip_addrs,
45            is_up,
46            interface_type: iface_type,
47            rx_bytes: data.total_received(),
48            tx_bytes: data.total_transmitted(),
49        });
50    }
51
52    let result = if active_count == 0 {
53        DiagnosticResult::fail("Network", "No active network interfaces found")
54    } else if !wifi_info.is_empty() {
55        DiagnosticResult::ok("Network", format!("Connected via {}", wifi_info))
56    } else if active_count == 1 {
57        let active = details.iter().find(|i| i.is_up);
58        let desc = match active {
59            Some(iface) => format!("Connected via {}", iface.interface_type),
60            None => "Connected".to_string(),
61        };
62        DiagnosticResult::ok("Network", desc)
63    } else {
64        DiagnosticResult::ok("Network", format!("{} active interfaces", active_count))
65    };
66
67    (result, details)
68}
69
70fn detect_interface_type(name: &str) -> String {
71    let lower = name.to_lowercase();
72    if lower.contains("wi-fi")
73        || lower.contains("wifi")
74        || lower.contains("wlan")
75        || lower.contains("wlp")
76    {
77        "Wi-Fi".to_string()
78    } else if lower.contains("eth")
79        || lower.contains("enp")
80        || lower.contains("eno")
81        || lower.contains("ethernet")
82    {
83        "Ethernet".to_string()
84    } else if lower == "lo" || lower == "lo0" || (lower.starts_with("lo") && lower.len() <= 3) {
85        "Loopback".to_string()
86    } else if lower.contains("tun")
87        || lower.contains("tap")
88        || lower.contains("wg")
89        || lower.contains("utun")
90    {
91        "VPN/Tunnel".to_string()
92    } else if lower.contains("bluetooth") || lower.contains("bnep") {
93        "Bluetooth".to_string()
94    } else if lower.contains("docker") || lower.contains("veth") || lower.contains("br-") {
95        "Virtual".to_string()
96    } else {
97        "Unknown".to_string()
98    }
99}
100
101fn format_mac(bytes: [u8; 6]) -> String {
102    if bytes == [0, 0, 0, 0, 0, 0] {
103        return "N/A".to_string();
104    }
105    format!(
106        "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
107        bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]
108    )
109}
110
111async fn get_wifi_summary() -> String {
112    #[cfg(windows)]
113    {
114        let mut cmd = tokio::process::Command::new("netsh");
115        cmd.args(["wlan", "show", "interfaces"]);
116        match super::util::run_with_timeout(cmd, super::util::QUICK).await {
117            Some(output) => {
118                let text = String::from_utf8_lossy(&output.stdout);
119                let mut band = String::new();
120                let mut ssid = String::new();
121
122                for line in text.lines() {
123                    let line = line.trim();
124                    if line.starts_with("SSID")
125                        && !line.starts_with("SSID B")
126                        && !line.starts_with("SSID name")
127                    {
128                        if let Some(val) = line.split(':').nth(1) {
129                            ssid = val.trim().to_string();
130                        }
131                    }
132                    if line.starts_with("Radio type") || line.starts_with("Band") {
133                        if let Some(val) = line.split(':').nth(1) {
134                            let val = val.trim();
135                            if val.contains("6 GHz") || val.contains("6E") {
136                                band = "6 GHz".to_string();
137                            } else if val.contains("5 GHz")
138                                || val.contains("802.11a")
139                                || val.contains("802.11ac")
140                            {
141                                band = "5 GHz".to_string();
142                            } else if val.contains("802.11ax") {
143                                // 802.11ax can be 2.4/5/6 GHz; leave band empty to rely on channel
144                                band = String::new();
145                            } else {
146                                band = "2.4 GHz".to_string();
147                            }
148                        }
149                    }
150                    // Also check "Channel" for band detection fallback
151                    if line.starts_with("Channel") {
152                        if let Some(val) = line.split(':').nth(1) {
153                            if let Ok(ch) = val.trim().parse::<u32>() {
154                                if band.is_empty() {
155                                    if ch > 14 && ch <= 177 {
156                                        band = "5 GHz".to_string();
157                                    } else if ch <= 14 {
158                                        band = "2.4 GHz".to_string();
159                                    }
160                                }
161                            }
162                        }
163                    }
164                }
165
166                if !ssid.is_empty() {
167                    if !band.is_empty() {
168                        format!("Wi-Fi ({}) - {}", band, ssid)
169                    } else {
170                        format!("Wi-Fi - {}", ssid)
171                    }
172                } else {
173                    "Wi-Fi".to_string()
174                }
175            }
176            None => "Wi-Fi".to_string(),
177        }
178    }
179
180    #[cfg(target_os = "macos")]
181    {
182        let mut cmd = tokio::process::Command::new("/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport");
183        cmd.args(["-I"]);
184        match super::util::run_with_timeout(cmd, super::util::QUICK).await {
185            Some(output) => {
186                let text = String::from_utf8_lossy(&output.stdout);
187                let mut ssid = String::new();
188                let mut channel = 0u32;
189                for line in text.lines() {
190                    let line = line.trim();
191                    if line.starts_with("SSID:") {
192                        ssid = line
193                            .split(':')
194                            .nth(1)
195                            .map(|s| s.trim().to_string())
196                            .unwrap_or_default();
197                    }
198                    if line.starts_with("channel:") {
199                        if let Some(val) = line.split(':').nth(1) {
200                            channel = val
201                                .trim()
202                                .split(',')
203                                .next()
204                                .and_then(|s| s.parse().ok())
205                                .unwrap_or(0);
206                        }
207                    }
208                }
209                let band = if channel > 14 && channel <= 177 {
210                    "5 GHz"
211                } else if channel <= 14 && channel > 0 {
212                    "2.4 GHz"
213                } else {
214                    ""
215                };
216                if !ssid.is_empty() {
217                    if !band.is_empty() {
218                        format!("Wi-Fi ({}) - {}", band, ssid)
219                    } else {
220                        format!("Wi-Fi - {}", ssid)
221                    }
222                } else {
223                    "Wi-Fi".to_string()
224                }
225            }
226            None => "Wi-Fi".to_string(),
227        }
228    }
229
230    #[cfg(target_os = "linux")]
231    {
232        let mut cmd = tokio::process::Command::new("iwgetid");
233        cmd.args(["-r"]);
234        match super::util::run_with_timeout(cmd, super::util::QUICK).await {
235            Some(output) => {
236                let ssid = String::from_utf8_lossy(&output.stdout).trim().to_string();
237                // Try to get frequency
238                let mut freq_cmd = tokio::process::Command::new("iwgetid");
239                freq_cmd.args(["--freq", "-r"]);
240                let band = match super::util::run_with_timeout(freq_cmd, super::util::QUICK).await {
241                    Some(freq_out) => {
242                        let freq_str = String::from_utf8_lossy(&freq_out.stdout).trim().to_string();
243                        if let Ok(freq) = freq_str.parse::<f64>() {
244                            if freq > 5.0 {
245                                "5 GHz".to_string()
246                            } else if freq > 2.0 {
247                                "2.4 GHz".to_string()
248                            } else {
249                                String::new()
250                            }
251                        } else {
252                            String::new()
253                        }
254                    }
255                    None => String::new(),
256                };
257
258                if !ssid.is_empty() {
259                    if !band.is_empty() {
260                        format!("Wi-Fi ({}) - {}", band, ssid)
261                    } else {
262                        format!("Wi-Fi - {}", ssid)
263                    }
264                } else {
265                    "Wi-Fi".to_string()
266                }
267            }
268            None => "Wi-Fi".to_string(),
269        }
270    }
271}