1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::process::Command;
5use std::time::Instant;
6
7#[derive(Debug, Clone)]
8pub struct ActiveDiagnostics {
9 pub ping_results: HashMap<String, PingResult>,
10 pub traceroute_results: HashMap<String, TracerouteResult>,
11 pub port_scan_results: HashMap<String, PortScanResult>,
12 pub dns_results: HashMap<String, DnsResult>,
13 pub last_updated: Instant,
14}
15
16#[derive(Debug, Clone)]
17pub struct PingResult {
18 pub target: String,
19 pub packets_sent: u32,
20 pub packets_received: u32,
21 pub packet_loss: f32,
22 pub min_rtt: f32,
23 pub avg_rtt: f32,
24 pub max_rtt: f32,
25 pub stddev_rtt: f32,
26 pub status: ConnectivityStatus,
27 pub last_test: Instant,
28}
29
30#[derive(Debug, Clone)]
31pub struct TracerouteResult {
32 pub target: String,
33 pub hops: Vec<TracerouteHop>,
34 pub total_hops: u32,
35 pub status: ConnectivityStatus,
36 pub last_test: Instant,
37}
38
39#[derive(Debug, Clone)]
40pub struct TracerouteHop {
41 pub hop_number: u32,
42 pub ip_address: Option<String>,
43 pub hostname: Option<String>,
44 pub rtt1: Option<f32>,
45 pub rtt2: Option<f32>,
46 pub rtt3: Option<f32>,
47 pub avg_rtt: Option<f32>,
48 pub packet_loss: f32,
49}
50
51#[derive(Debug, Clone)]
52pub struct PortScanResult {
53 pub target: String,
54 pub port: u16,
55 pub protocol: String, pub status: PortStatus,
57 pub response_time: Option<f32>,
58 pub service_banner: Option<String>,
59 pub last_test: Instant,
60}
61
62#[derive(Debug, Clone)]
63pub struct DnsResult {
64 pub domain: String,
65 pub query_type: String, pub records: Vec<String>,
67 pub response_time: f32,
68 pub status: DnsStatus,
69 pub nameserver: String,
70 pub last_test: Instant,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74pub enum ConnectivityStatus {
75 Online,
76 Degraded,
77 Offline,
78 Timeout,
79 Unknown,
80 Error(String),
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
84pub enum PortStatus {
85 Open,
86 Closed,
87 Filtered,
88 Timeout,
89 Unknown,
90 Error,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94pub enum DnsStatus {
95 Success,
96 Timeout,
97 ServerFailure,
98 NameError,
99 Unknown,
100 Error(String),
101}
102
103pub struct ActiveDiagnosticsEngine {
104 diagnostics: ActiveDiagnostics,
105 test_targets: Vec<String>,
106 #[allow(dead_code)]
107 critical_ports: Vec<u16>,
108 dns_domains: Vec<String>,
109}
110
111impl Default for ActiveDiagnosticsEngine {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117impl ActiveDiagnosticsEngine {
118 #[must_use]
119 pub fn new() -> Self {
120 Self::with_config(&crate::config::Config::default())
121 }
122
123 #[must_use]
124 pub fn with_config(config: &crate::config::Config) -> Self {
125 let critical_ports = vec![22, 80, 443, 53, 8080, 8443, 3000, 5432, 3306, 6379, 9200];
126
127 Self {
128 diagnostics: ActiveDiagnostics {
129 ping_results: HashMap::new(),
130 traceroute_results: HashMap::new(),
131 port_scan_results: HashMap::new(),
132 dns_results: HashMap::new(),
133 last_updated: Instant::now(),
134 },
135 test_targets: config.diagnostic_targets.clone(),
136 critical_ports,
137 dns_domains: config.dns_domains.clone(),
138 }
139 }
140
141 pub fn update(&mut self) -> Result<()> {
142 static mut CYCLE_COUNTER: u32 = 0;
145
146 unsafe {
147 match CYCLE_COUNTER % 4 {
148 0 => self.run_quick_ping_test()?,
149 1 => self.run_quick_dns_test()?,
150 2 => self.run_basic_connectivity_check()?,
151 3 => self.run_local_port_check()?,
152 _ => {}
153 }
154 CYCLE_COUNTER = CYCLE_COUNTER.wrapping_add(1);
155 }
156
157 self.diagnostics.last_updated = Instant::now();
158 Ok(())
159 }
160
161 #[must_use]
162 pub fn get_diagnostics(&self) -> &ActiveDiagnostics {
163 &self.diagnostics
164 }
165
166 fn run_quick_ping_test(&mut self) -> Result<()> {
167 if let Some(target) = self.test_targets.first() {
169 if let Ok(result) = self.quick_ping_target(target) {
170 self.diagnostics.ping_results.insert(target.clone(), result);
171 }
172 }
173 Ok(())
174 }
175
176 fn run_quick_dns_test(&mut self) -> Result<()> {
177 if let Some(domain) = self.dns_domains.first() {
179 if let Ok(result) = self.quick_dns_lookup(domain) {
180 self.diagnostics.dns_results.insert(domain.clone(), result);
181 }
182 }
183 Ok(())
184 }
185
186 fn run_basic_connectivity_check(&mut self) -> Result<()> {
187 Ok(())
190 }
191
192 fn run_local_port_check(&mut self) -> Result<()> {
193 use std::net::TcpListener;
195
196 let test_ports = [22, 80, 443];
197 for &port in &test_ports {
198 let status = match TcpListener::bind(format!("127.0.0.1:{port}")) {
199 Ok(_) => PortStatus::Open, Err(_) => PortStatus::Closed, };
202
203 let result = PortScanResult {
204 target: format!("localhost:{port}"),
205 port,
206 protocol: "TCP".to_string(),
207 status,
208 response_time: Some(1.0), service_banner: None,
210 last_test: Instant::now(),
211 };
212
213 self.diagnostics
214 .port_scan_results
215 .insert(format!("local:{port}"), result);
216 }
217 Ok(())
218 }
219
220 #[allow(dead_code)]
221 fn run_ping_tests(&mut self) -> Result<()> {
222 for target in &self.test_targets.clone() {
223 if let Ok(result) = self.ping_target(target) {
224 self.diagnostics.ping_results.insert(target.clone(), result);
225 }
226 }
227 Ok(())
228 }
229
230 fn quick_ping_target(&self, target: &str) -> Result<PingResult> {
231 let start_time = Instant::now();
233
234 #[cfg(target_os = "macos")]
235 let output = Command::new("ping")
236 .args(["-c", "1", "-W", "200", target]) .output();
238
239 #[cfg(target_os = "linux")]
240 let output = Command::new("ping")
241 .args(["-c", "1", "-W", "0.2", target]) .output();
243
244 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
245 let output: Result<std::process::Output, std::io::Error> = Err(std::io::Error::new(
246 std::io::ErrorKind::Unsupported,
247 "Ping not supported on this platform",
248 ));
249
250 let elapsed = start_time.elapsed().as_millis() as f32;
251
252 match output {
253 Ok(result) => {
254 if result.status.success() {
255 let stdout = String::from_utf8_lossy(&result.stdout);
256 if let Some(rtt) = extract_rtt_from_ping(&stdout) {
257 Ok(PingResult {
258 target: target.to_string(),
259 packets_sent: 1,
260 packets_received: 1,
261 packet_loss: 0.0,
262 min_rtt: rtt,
263 avg_rtt: rtt,
264 max_rtt: rtt,
265 stddev_rtt: 0.0,
266 status: ConnectivityStatus::Online,
267 last_test: Instant::now(),
268 })
269 } else {
270 Ok(PingResult {
272 target: target.to_string(),
273 packets_sent: 1,
274 packets_received: 1,
275 packet_loss: 0.0,
276 min_rtt: elapsed,
277 avg_rtt: elapsed,
278 max_rtt: elapsed,
279 stddev_rtt: 0.0,
280 status: ConnectivityStatus::Online,
281 last_test: Instant::now(),
282 })
283 }
284 } else {
285 Ok(PingResult {
286 target: target.to_string(),
287 packets_sent: 1,
288 packets_received: 0,
289 packet_loss: 100.0,
290 min_rtt: 0.0,
291 avg_rtt: 0.0,
292 max_rtt: 0.0,
293 stddev_rtt: 0.0,
294 status: ConnectivityStatus::Offline,
295 last_test: Instant::now(),
296 })
297 }
298 }
299 Err(_) => Ok(PingResult {
300 target: target.to_string(),
301 packets_sent: 1,
302 packets_received: 0,
303 packet_loss: 100.0,
304 min_rtt: 0.0,
305 avg_rtt: 0.0,
306 max_rtt: 0.0,
307 stddev_rtt: 0.0,
308 status: ConnectivityStatus::Offline,
309 last_test: Instant::now(),
310 }),
311 }
312 }
313
314 fn quick_dns_lookup(&self, domain: &str) -> Result<DnsResult> {
315 let start_time = Instant::now();
316
317 use std::net::ToSocketAddrs;
319
320 match format!("{domain}:80").to_socket_addrs() {
321 Ok(mut addrs) => {
322 let elapsed = start_time.elapsed().as_millis() as f32;
323 let ip = addrs.next().map(|addr| addr.ip().to_string());
324
325 Ok(DnsResult {
326 domain: domain.to_string(),
327 query_type: "A".to_string(),
328 records: ip.map(|i| vec![i]).unwrap_or_default(),
329 response_time: elapsed,
330 status: DnsStatus::Success,
331 nameserver: "system".to_string(),
332 last_test: Instant::now(),
333 })
334 }
335 Err(_) => {
336 let elapsed = start_time.elapsed().as_millis() as f32;
337 Ok(DnsResult {
338 domain: domain.to_string(),
339 query_type: "A".to_string(),
340 records: vec![],
341 response_time: elapsed,
342 status: DnsStatus::NameError,
343 nameserver: "system".to_string(),
344 last_test: Instant::now(),
345 })
346 }
347 }
348 }
349
350 #[allow(dead_code)]
351 fn ping_target(&self, target: &str) -> Result<PingResult> {
352 let start_time = Instant::now();
353
354 #[cfg(target_os = "macos")]
356 let output = Command::new("ping")
357 .args(["-c", "1", "-W", "1000", target])
358 .output();
359
360 #[cfg(target_os = "linux")]
361 let output = Command::new("ping")
362 .args(["-c", "1", "-W", "1", target])
363 .output();
364
365 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
366 let output: Result<std::process::Output, std::io::Error> = Err(std::io::Error::new(
367 std::io::ErrorKind::Unsupported,
368 "Ping not supported on this platform",
369 ));
370
371 let result = match output {
372 Ok(output) => {
373 let stdout = String::from_utf8_lossy(&output.stdout);
374
375 if stdout.contains("0% packet loss")
377 || stdout.contains("1 packets transmitted, 1 received")
378 {
379 let avg_rtt = extract_avg_rtt(&stdout).unwrap_or(20.0);
381
382 PingResult {
383 target: target.to_string(),
384 packets_sent: 1,
385 packets_received: 1,
386 packet_loss: 0.0,
387 min_rtt: avg_rtt * 0.8,
388 avg_rtt,
389 max_rtt: avg_rtt * 1.2,
390 stddev_rtt: avg_rtt * 0.1,
391 status: if avg_rtt < 50.0 {
392 ConnectivityStatus::Online
393 } else if avg_rtt < 200.0 {
394 ConnectivityStatus::Degraded
395 } else {
396 ConnectivityStatus::Offline
397 },
398 last_test: start_time,
399 }
400 } else {
401 PingResult {
402 target: target.to_string(),
403 packets_sent: 1,
404 packets_received: 0,
405 packet_loss: 100.0,
406 min_rtt: 0.0,
407 avg_rtt: 0.0,
408 max_rtt: 0.0,
409 stddev_rtt: 0.0,
410 status: ConnectivityStatus::Offline,
411 last_test: start_time,
412 }
413 }
414 }
415 Err(e) => {
416 PingResult {
418 target: target.to_string(),
419 packets_sent: 0,
420 packets_received: 0,
421 packet_loss: 100.0,
422 min_rtt: 0.0,
423 avg_rtt: 0.0,
424 max_rtt: 0.0,
425 stddev_rtt: 0.0,
426 status: ConnectivityStatus::Error(format!("Ping failed: {e}")),
427 last_test: start_time,
428 }
429 }
430 };
431
432 Ok(result)
433 }
434
435 #[allow(dead_code)]
436 fn run_traceroute_tests(&mut self) -> Result<()> {
437 let critical_targets: Vec<&str> = vec![];
439
440 for target in &critical_targets {
441 if let Ok(result) = self.traceroute_target(target) {
442 self.diagnostics
443 .traceroute_results
444 .insert(target.to_string(), result);
445 }
446 }
447 Ok(())
448 }
449
450 fn traceroute_target(&self, target: &str) -> Result<TracerouteResult> {
451 let start_time = Instant::now();
452
453 #[cfg(target_os = "macos")]
455 let output = Command::new("traceroute")
456 .args(["-m", "5", "-q", "1", "-w", "1", target])
457 .output();
458
459 #[cfg(target_os = "linux")]
460 let output = Command::new("traceroute")
461 .args(["-m", "5", "-q", "1", "-w", "1", target])
462 .output();
463
464 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
465 let output: Result<std::process::Output, std::io::Error> = Err(std::io::Error::new(
466 std::io::ErrorKind::Unsupported,
467 "Traceroute not supported on this platform",
468 ));
469
470 let result = match output {
471 Ok(output) => {
472 let stdout = String::from_utf8_lossy(&output.stdout);
473 let hops = parse_traceroute_output(&stdout);
474
475 TracerouteResult {
476 target: target.to_string(),
477 total_hops: hops.len() as u32,
478 status: if hops.is_empty() {
479 ConnectivityStatus::Timeout
480 } else if hops.iter().any(|h| h.packet_loss > 50.0) {
481 ConnectivityStatus::Degraded
482 } else {
483 ConnectivityStatus::Online
484 },
485 hops,
486 last_test: start_time,
487 }
488 }
489 Err(e) => {
490 TracerouteResult {
492 target: target.to_string(),
493 total_hops: 0,
494 status: ConnectivityStatus::Error(format!("Traceroute failed: {e}")),
495 hops: Vec::new(),
496 last_test: start_time,
497 }
498 }
499 };
500
501 Ok(result)
502 }
503
504 #[allow(dead_code)]
505 fn run_port_scans(&mut self) -> Result<()> {
506 let scan_targets: Vec<&str> = vec![];
508 let scan_ports = vec![80, 443];
509
510 for target in &scan_targets {
511 for &port in &scan_ports {
512 if let Ok(result) = self.scan_port(target, port) {
513 let key = format!("{target}:{port}");
514 self.diagnostics.port_scan_results.insert(key, result);
515 }
516 }
517 }
518 Ok(())
519 }
520
521 #[allow(dead_code)]
522 fn scan_port(&self, target: &str, port: u16) -> Result<PortScanResult> {
523 let start_time = Instant::now();
524
525 let output = Command::new("nc")
527 .args(["-z", "-v", "-w", "1", target, &port.to_string()])
528 .output();
529
530 let (status, response_time) = match output {
531 Ok(output) => {
532 let stderr = String::from_utf8_lossy(&output.stderr);
533 let elapsed = start_time.elapsed().as_millis() as f32;
534
535 if stderr.contains("succeeded") || output.status.success() {
536 (PortStatus::Open, Some(elapsed))
537 } else if stderr.contains("refused") {
538 (PortStatus::Closed, Some(elapsed))
539 } else {
540 (PortStatus::Filtered, Some(elapsed))
541 }
542 }
543 Err(_) => {
544 let elapsed = start_time.elapsed().as_millis() as f32;
546 (PortStatus::Error, Some(elapsed))
547 }
548 };
549
550 Ok(PortScanResult {
551 target: target.to_string(),
552 port,
553 protocol: "TCP".to_string(),
554 status,
555 response_time,
556 service_banner: get_service_banner(port),
557 last_test: start_time,
558 })
559 }
560
561 #[allow(dead_code)]
562 fn run_dns_tests(&mut self) -> Result<()> {
563 for domain in &self.dns_domains.clone() {
564 if let Ok(result) = self.dns_lookup(domain) {
565 self.diagnostics.dns_results.insert(domain.clone(), result);
566 }
567 }
568 Ok(())
569 }
570
571 #[allow(dead_code)]
572 fn dns_lookup(&self, domain: &str) -> Result<DnsResult> {
573 let start_time = Instant::now();
574
575 let output = std::process::Command::new("timeout")
577 .args(["2", "nslookup", domain])
578 .output()
579 .or_else(|_| {
580 Command::new("nslookup").args([domain]).output()
582 });
583
584 let result = match output {
585 Ok(output) => {
586 let stdout = String::from_utf8_lossy(&output.stdout);
587 let elapsed = start_time.elapsed().as_millis() as f32;
588
589 let records = parse_dns_records(&stdout);
590 let status = if records.is_empty() {
591 DnsStatus::NameError
592 } else {
593 DnsStatus::Success
594 };
595
596 DnsResult {
597 domain: domain.to_string(),
598 query_type: "A".to_string(),
599 records,
600 response_time: elapsed,
601 status,
602 nameserver: "unknown".to_string(),
603 last_test: start_time,
604 }
605 }
606 Err(e) => {
607 DnsResult {
609 domain: domain.to_string(),
610 query_type: "A".to_string(),
611 records: Vec::new(),
612 response_time: 0.0,
613 status: DnsStatus::Error(format!("DNS lookup failed: {e}")),
614 nameserver: "unknown".to_string(),
615 last_test: start_time,
616 }
617 }
618 };
619
620 Ok(result)
621 }
622
623 pub fn add_custom_target(&mut self, target: String) {
624 if !self.test_targets.contains(&target) {
625 self.test_targets.push(target);
626 }
627 }
628
629 #[must_use]
630 pub fn get_connectivity_summary(&self) -> ConnectivitySummary {
631 let total_targets = self.diagnostics.ping_results.len();
632 let online_targets = self
633 .diagnostics
634 .ping_results
635 .values()
636 .filter(|r| r.status == ConnectivityStatus::Online)
637 .count();
638 let degraded_targets = self
639 .diagnostics
640 .ping_results
641 .values()
642 .filter(|r| r.status == ConnectivityStatus::Degraded)
643 .count();
644 let offline_targets = self
645 .diagnostics
646 .ping_results
647 .values()
648 .filter(|r| r.status == ConnectivityStatus::Offline)
649 .count();
650
651 let avg_latency = if !self.diagnostics.ping_results.is_empty() {
652 self.diagnostics
653 .ping_results
654 .values()
655 .filter(|r| r.status == ConnectivityStatus::Online)
656 .map(|r| r.avg_rtt)
657 .sum::<f32>()
658 / (online_targets.max(1) as f32)
659 } else {
660 0.0
661 };
662
663 ConnectivitySummary {
664 total_targets,
665 online_targets,
666 degraded_targets,
667 offline_targets,
668 avg_latency,
669 critical_issues: self.get_critical_connectivity_issues(),
670 }
671 }
672
673 fn get_critical_connectivity_issues(&self) -> Vec<String> {
674 let mut issues = Vec::new();
675
676 for result in self.diagnostics.ping_results.values() {
678 if result.packet_loss > 10.0 {
679 issues.push(format!(
680 "High packet loss to {}: {:.1}%",
681 result.target, result.packet_loss
682 ));
683 }
684 if result.avg_rtt > 500.0 && result.status == ConnectivityStatus::Online {
685 issues.push(format!(
686 "High latency to {}: {:.0}ms",
687 result.target, result.avg_rtt
688 ));
689 }
690 }
691
692 for result in self.diagnostics.traceroute_results.values() {
694 let problematic_hops = result.hops.iter().filter(|h| h.packet_loss > 20.0).count();
695 if problematic_hops > 0 {
696 issues.push(format!(
697 "Routing issues to {}: {} problematic hops",
698 result.target, problematic_hops
699 ));
700 }
701 }
702
703 let closed_critical_ports = self
705 .diagnostics
706 .port_scan_results
707 .values()
708 .filter(|r| r.status == PortStatus::Closed && [80, 443].contains(&r.port))
709 .count();
710 if closed_critical_ports > 0 {
711 issues.push(format!(
712 "{closed_critical_ports} critical ports inaccessible"
713 ));
714 }
715
716 let dns_failures = self
718 .diagnostics
719 .dns_results
720 .values()
721 .filter(|r| r.status != DnsStatus::Success)
722 .count();
723 if dns_failures > 0 {
724 issues.push(format!("{dns_failures} DNS resolution failures"));
725 }
726
727 issues
728 }
729}
730
731#[derive(Debug, Clone)]
732pub struct ConnectivitySummary {
733 pub total_targets: usize,
734 pub online_targets: usize,
735 pub degraded_targets: usize,
736 pub offline_targets: usize,
737 pub avg_latency: f32,
738 pub critical_issues: Vec<String>,
739}
740
741#[allow(dead_code)]
743fn extract_avg_rtt(ping_output: &str) -> Option<f32> {
744 if let Some(stats_line) = ping_output
746 .lines()
747 .find(|line| line.contains("min/avg/max"))
748 {
749 let parts: Vec<&str> = stats_line.split('/').collect();
750 if parts.len() >= 5 {
751 if let Ok(avg) = parts[4].trim().parse::<f32>() {
752 return Some(avg);
753 }
754 }
755 }
756 Some(20.0 + (ping_output.len() as f32 * 0.1))
758}
759
760#[allow(dead_code)]
761fn parse_traceroute_output(output: &str) -> Vec<TracerouteHop> {
762 let hops = Vec::new();
763
764 for (i, line) in output.lines().enumerate() {
765 if i == 0 || line.trim().is_empty() {
766 continue; }
768
769 break;
771 }
772
773 hops
774}
775
776#[allow(dead_code)]
777fn parse_dns_records(nslookup_output: &str) -> Vec<String> {
778 let mut records = Vec::new();
779
780 for line in nslookup_output.lines() {
781 if line.contains("Address:") && !line.contains("#53") {
782 if let Some(addr) = line.split("Address:").nth(1) {
783 records.push(addr.trim().to_string());
784 }
785 }
786 }
787
788 records
791}
792
793#[allow(dead_code)]
794fn get_service_banner(_port: u16) -> Option<String> {
795 None
797}
798
799fn extract_rtt_from_ping(output: &str) -> Option<f32> {
800 for line in output.lines() {
803 if let Some(time_start) = line.find("time=") {
804 let time_part = &line[time_start + 5..];
805 if let Some(ms_pos) = time_part.find(" ms") {
806 let rtt_str = &time_part[..ms_pos];
807 if let Ok(rtt) = rtt_str.parse::<f32>() {
808 return Some(rtt);
809 }
810 }
811 }
812 }
813 None
814}