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