1pub mod adapter_hw_stats;
2pub mod adapters;
3pub mod arp;
4pub mod bufferbloat;
5pub mod captive_portal;
6pub mod connection_states;
7pub mod connections;
8pub mod dhcp;
9pub mod dns;
10pub mod dns_benchmark;
11pub mod dns_cache;
12pub mod firewall;
13pub mod gateway;
14pub mod interfaces;
15pub mod ipv6;
16pub mod latency;
17pub mod listening_ports;
18pub mod mtu;
19pub mod nat;
20pub mod ntp;
21pub mod packet_loss;
22pub mod ping;
23pub mod ports;
24pub mod protocol_stats;
25pub mod proxy;
26pub mod public_ip;
27pub mod reverse_dns;
28pub mod route_path;
29pub mod routing_table;
30pub mod shared_cache;
31pub mod speed;
32pub mod tls_inspection;
33pub mod traffic_counters;
34pub mod util;
35pub mod vpn;
36pub mod wifi;
37
38use serde::Serialize;
39
40use crate::config::Config;
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
43pub enum DiagnosticStatus {
44 Ok,
45 Warn,
46 Fail,
47 Skip,
48}
49
50#[derive(Debug, Clone, Serialize)]
51pub struct DiagnosticResult {
52 pub category: String,
53 pub status: DiagnosticStatus,
54 pub summary: String,
55 pub details: Option<String>,
56 #[serde(skip_serializing_if = "std::ops::Not::not")]
61 pub timed_out: bool,
62}
63
64impl DiagnosticResult {
65 pub fn ok(category: impl Into<String>, summary: impl Into<String>) -> Self {
66 Self {
67 category: category.into(),
68 status: DiagnosticStatus::Ok,
69 summary: summary.into(),
70 details: None,
71 timed_out: false,
72 }
73 }
74
75 pub fn warn(category: impl Into<String>, summary: impl Into<String>) -> Self {
76 Self {
77 category: category.into(),
78 status: DiagnosticStatus::Warn,
79 summary: summary.into(),
80 details: None,
81 timed_out: false,
82 }
83 }
84
85 pub fn fail(category: impl Into<String>, summary: impl Into<String>) -> Self {
86 Self {
87 category: category.into(),
88 status: DiagnosticStatus::Fail,
89 summary: summary.into(),
90 details: None,
91 timed_out: false,
92 }
93 }
94
95 pub fn skip(category: impl Into<String>, summary: impl Into<String>) -> Self {
96 Self {
97 category: category.into(),
98 status: DiagnosticStatus::Skip,
99 summary: summary.into(),
100 details: None,
101 timed_out: false,
102 }
103 }
104
105 pub fn timed_out_fail(category: impl Into<String>) -> Self {
109 Self {
110 category: category.into(),
111 status: DiagnosticStatus::Fail,
112 summary: "Timed out — network severely degraded".to_string(),
113 details: None,
114 timed_out: true,
115 }
116 }
117
118 pub fn with_details(mut self, details: impl Into<String>) -> Self {
119 self.details = Some(details.into());
120 self
121 }
122}
123
124#[derive(Debug, Clone, Serialize)]
125pub struct DiagnosticResults {
126 pub timestamp: String,
127 pub adapters: DiagnosticResult,
128 pub interfaces: DiagnosticResult,
129 pub gateway: DiagnosticResult,
130 pub dns: DiagnosticResult,
131 pub public_ip: DiagnosticResult,
132 pub latency: DiagnosticResult,
133 pub speed: DiagnosticResult,
134 pub ports: DiagnosticResult,
135
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub interface_details: Option<Vec<interfaces::InterfaceInfo>>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub adapter_details: Option<Vec<adapters::AdapterInfo>>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub gateway_details: Option<gateway::GatewayInfo>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub dns_details: Option<dns::DnsInfo>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub public_ip_details: Option<public_ip::PublicIpInfo>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub latency_details: Option<Vec<latency::LatencyResult>>,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub speed_details: Option<crate::speedtest::SpeedTestResult>,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub port_details: Option<Vec<ports::PortResult>>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub technician: Option<TechnicianResults>,
157
158 #[serde(skip_serializing_if = "std::ops::Not::not")]
162 pub timed_out: bool,
163}
164
165#[derive(Debug, Clone, Serialize)]
166pub struct TechnicianResults {
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub arp_table: Option<Vec<arp::ArpEntry>>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub routing_table: Option<Vec<routing_table::RouteEntry>>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub active_connections: Option<Vec<connections::ConnectionEntry>>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub listening_ports: Option<Vec<listening_ports::ListeningPort>>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub dhcp_info: Option<Vec<dhcp::DhcpLease>>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub protocol_stats: Option<protocol_stats::ProtocolStatistics>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub adapter_hw_stats: Option<Vec<adapter_hw_stats::AdapterHwStat>>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub proxy_config: Option<proxy::ProxyConfig>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub vpn_info: Option<Vec<vpn::VpnAdapter>>,
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub firewall_info: Option<firewall::FirewallInfo>,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub dns_cache: Option<Vec<dns_cache::DnsCacheEntry>>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub ipv6_info: Option<ipv6::Ipv6Info>,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub mtu_info: Option<Vec<mtu::MtuInfo>>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub connection_states: Option<connection_states::ConnectionStates>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub bufferbloat: Option<bufferbloat::BufferbloatResult>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub reverse_dns: Option<Vec<reverse_dns::ReverseDnsEntry>>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub tls_inspection: Option<tls_inspection::TlsInspectionResult>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub traffic_counters: Option<Vec<traffic_counters::TrafficCounter>>,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub route_path: Option<route_path::RoutePath>,
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub packet_loss: Option<Vec<packet_loss::LossResult>>,
209 #[serde(skip_serializing_if = "Option::is_none")]
210 pub nat_analysis: Option<nat::NatAnalysis>,
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub wifi: Option<Vec<wifi::WifiLink>>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub dns_benchmark: Option<dns_benchmark::DnsBenchmark>,
215 #[serde(skip_serializing_if = "Option::is_none")]
216 pub captive_portal: Option<captive_portal::CaptivePortalResult>,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub clock_sync: Option<ntp::ClockSync>,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub path_mtu: Option<mtu::PathMtu>,
221 #[serde(skip_serializing_if = "Option::is_none")]
222 pub arp_health: Option<arp::ArpHealth>,
223}
224
225pub fn run_all_cap(config: &Config) -> std::time::Duration {
238 const RUN_ALL_CAP_FLOOR: std::time::Duration = std::time::Duration::from_secs(90);
239 const RUN_ALL_CAP_HEADROOM: u64 = 30;
240 const TECH_DEEP_BUDGET_SECS: u64 = 150;
245
246 let base = if config.skip_speed {
247 RUN_ALL_CAP_FLOOR
248 } else {
249 std::time::Duration::from_secs(4 * config.speed_duration + RUN_ALL_CAP_HEADROOM)
250 .max(RUN_ALL_CAP_FLOOR)
251 };
252
253 if config.is_tech_mode() {
254 base + std::time::Duration::from_secs(TECH_DEEP_BUDGET_SECS)
255 } else {
256 base
257 }
258}
259
260pub async fn run_all(config: &Config, cap: std::time::Duration) -> DiagnosticResults {
261 let deadline = tokio::time::Instant::now() + cap;
262 let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
263 let mut timed_out = false;
264
265 let mut adapters_h = tokio::spawn(adapters::check());
269 let mut interfaces_h = tokio::spawn(interfaces::check());
270 let mut gateway_h = tokio::spawn(gateway::check());
271 let mut dns_h = tokio::spawn(dns::check());
272 let mut public_ip_h = tokio::spawn(public_ip::check());
273 let mut latency_h = tokio::spawn(latency::check());
274 let mut ports_h = tokio::spawn(ports::check());
275
276 let completed = tokio::select! {
280 results = async {
281 tokio::join!(
282 &mut adapters_h,
283 &mut interfaces_h,
284 &mut gateway_h,
285 &mut dns_h,
286 &mut public_ip_h,
287 &mut latency_h,
288 &mut ports_h,
289 )
290 } => Some(results),
291 _ = tokio::time::sleep_until(deadline) => None,
292 };
293
294 let (
295 (adapters_result, adapter_details),
296 (interfaces_result, interface_details),
297 (gateway_result, gateway_details),
298 (dns_result, dns_details),
299 (public_ip_result, public_ip_details),
300 (latency_result, latency_details),
301 (ports_result, port_details),
302 ) = match completed {
303 Some((a, i, g, d, p, l, po)) => (
304 a.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Adapters"), Vec::new())),
307 i.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Network"), Vec::new())),
308 g.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Gateway"), None)),
309 d.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("DNS"), None)),
310 p.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Internet"), None)),
311 l.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Latency"), Vec::new())),
312 po.unwrap_or_else(|_| (DiagnosticResult::timed_out_fail("Ports"), Vec::new())),
313 ),
314 None => {
315 timed_out = true;
316 (
317 util::harvest_or(
318 adapters_h,
319 (DiagnosticResult::timed_out_fail("Adapters"), Vec::new()),
320 )
321 .await,
322 util::harvest_or(
323 interfaces_h,
324 (DiagnosticResult::timed_out_fail("Network"), Vec::new()),
325 )
326 .await,
327 util::harvest_or(
328 gateway_h,
329 (DiagnosticResult::timed_out_fail("Gateway"), None),
330 )
331 .await,
332 util::harvest_or(dns_h, (DiagnosticResult::timed_out_fail("DNS"), None)).await,
333 util::harvest_or(
334 public_ip_h,
335 (DiagnosticResult::timed_out_fail("Internet"), None),
336 )
337 .await,
338 util::harvest_or(
339 latency_h,
340 (DiagnosticResult::timed_out_fail("Latency"), Vec::new()),
341 )
342 .await,
343 util::harvest_or(
344 ports_h,
345 (DiagnosticResult::timed_out_fail("Ports"), Vec::new()),
346 )
347 .await,
348 )
349 }
350 };
351
352 let now = tokio::time::Instant::now();
355 let (speed_result, speed_details) = if config.skip_speed {
356 (
357 DiagnosticResult::skip("Speed", "Speed test skipped (--fast)"),
358 None,
359 )
360 } else if timed_out || now >= deadline {
361 timed_out = true;
362 (
363 DiagnosticResult::skip("Speed", "Skipped — diagnostics timed out"),
364 None,
365 )
366 } else {
367 match tokio::time::timeout_at(deadline, speed::check(config)).await {
368 Ok(outcome) => outcome,
369 Err(_) => {
370 timed_out = true;
371 (DiagnosticResult::timed_out_fail("Speed"), None)
372 }
373 }
374 };
375
376 let mut adapter_details = adapter_details;
379 let technician = if config.is_tech_mode() {
380 if tokio::time::Instant::now() >= deadline {
381 timed_out = true;
382 None
383 } else {
384 let tech = tokio::time::timeout_at(
385 deadline,
386 Box::pin(async {
387 adapters::enrich_driver_info(&mut adapter_details).await;
388 run_technician_diagnostics(config, public_ip_details.as_ref()).await
389 }),
390 )
391 .await;
392 match tech {
393 Ok(t) => Some(t),
394 Err(_) => {
395 timed_out = true;
396 None
397 }
398 }
399 }
400 } else {
401 None
402 };
403
404 DiagnosticResults {
405 timestamp,
406 adapters: adapters_result,
407 interfaces: interfaces_result,
408 gateway: gateway_result,
409 dns: dns_result,
410 public_ip: public_ip_result,
411 latency: latency_result,
412 speed: speed_result,
413 ports: ports_result,
414 interface_details: Some(interface_details),
415 adapter_details: Some(adapter_details),
416 gateway_details,
417 dns_details,
418 public_ip_details,
419 latency_details: Some(latency_details),
420 speed_details,
421 port_details: Some(port_details),
422 technician,
423 timed_out,
424 }
425}
426
427async fn run_technician_diagnostics(
428 config: &Config,
429 public_ip: Option<&public_ip::PublicIpInfo>,
430) -> TechnicianResults {
431 if config.verbose {
432 eprintln!("[verbose] Running technician deep diagnostics...");
433 }
434
435 let cache = shared_cache::SharedCache::build_for_tech_mode().await;
437
438 let (
444 arp_table,
445 routing,
446 conns,
447 listeners,
448 dhcp_info,
449 proto_stats,
450 hw_stats,
451 proxy_cfg,
452 vpn_adapters,
453 fw_info,
454 dns_c,
455 ipv6_i,
456 mtu_i,
457 conn_states,
458 rdns,
459 tls_insp,
460 traffic,
461 wifi_links,
462 dns_bench,
463 portal,
464 clock,
465 ) = tokio::join!(
466 Box::pin(arp::collect()),
467 Box::pin(routing_table::collect()),
468 Box::pin(connections::collect_with_cache(&cache)),
469 Box::pin(listening_ports::collect_with_cache(&cache)),
470 Box::pin(dhcp::collect_with_cache(&cache)),
471 Box::pin(protocol_stats::collect()),
472 Box::pin(adapter_hw_stats::collect_with_cache(&cache)),
473 Box::pin(proxy::collect()),
474 Box::pin(vpn::collect_with_cache(&cache)),
475 Box::pin(firewall::collect()),
476 Box::pin(dns_cache::collect()),
477 Box::pin(ipv6::collect_with_cache(&cache)),
478 Box::pin(mtu::collect()),
479 Box::pin(connection_states::collect_with_cache(&cache)),
480 Box::pin(reverse_dns::collect_with_cache(&cache)),
481 Box::pin(tls_inspection::collect()),
482 Box::pin(traffic_counters::collect_with_cache(&cache)),
483 Box::pin(wifi::collect()),
484 Box::pin(dns_benchmark::collect()),
485 Box::pin(captive_portal::collect()),
486 Box::pin(ntp::collect()),
487 );
488
489 let (route, loss, path_mtu) = tokio::join!(
493 Box::pin(route_path::collect()),
494 Box::pin(packet_loss::collect()),
495 Box::pin(mtu::probe_path_mtu()),
496 );
497
498 let nat_analysis = nat::analyze(
500 cache.gateway_ip.as_deref(),
501 public_ip,
502 route.as_ref().map(|r| r.hops.as_slice()),
503 );
504 let arp_health = arp::assess_health(arp_table.as_deref(), cache.gateway_ip.as_deref());
505
506 let bufferbloat = Box::pin(bufferbloat::collect(config)).await;
509
510 TechnicianResults {
511 arp_table,
512 routing_table: routing,
513 active_connections: conns,
514 listening_ports: listeners,
515 dhcp_info,
516 protocol_stats: proto_stats,
517 adapter_hw_stats: hw_stats,
518 proxy_config: proxy_cfg,
519 vpn_info: vpn_adapters,
520 firewall_info: fw_info,
521 dns_cache: dns_c,
522 ipv6_info: ipv6_i,
523 mtu_info: mtu_i,
524 connection_states: conn_states,
525 bufferbloat,
526 reverse_dns: rdns,
527 tls_inspection: tls_insp,
528 traffic_counters: traffic,
529 route_path: route,
530 packet_loss: loss,
531 nat_analysis,
532 wifi: wifi_links,
533 dns_benchmark: dns_bench,
534 captive_portal: portal,
535 clock_sync: clock,
536 path_mtu,
537 arp_health,
538 }
539}