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
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 band = String::new();
145 } else {
146 band = "2.4 GHz".to_string();
147 }
148 }
149 }
150 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 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}