Skip to main content

netrunner_core/
diagnostics.rs

1//! Network diagnostics engine (UI-agnostic).
2//!
3//! Gathers gateway, DNS, route, IPv6 and interface information. Progress is
4//! reported through [`TestEvent`]s; when no channel is supplied it runs
5//! silently. The heavy "cyberpunk" presentation lives in the front-ends.
6
7use dns_lookup::lookup_host;
8use rand::RngExt as _;
9use std::net::{IpAddr, Ipv4Addr};
10use std::time::{Duration, Instant};
11use tokio::time::sleep;
12
13use crate::events::{emit, EventSender, TestEvent};
14use crate::types::{NetworkDiagnostics, RouteHop, TestConfig};
15
16pub struct NetworkDiagnosticsTool {
17    #[allow(dead_code)]
18    config: TestConfig,
19    events: Option<EventSender>,
20}
21
22impl NetworkDiagnosticsTool {
23    pub fn new(config: TestConfig) -> Self {
24        Self {
25            config,
26            events: None,
27        }
28    }
29
30    /// Create a diagnostics tool that reports progress through an [`EventSender`].
31    pub fn with_events(config: TestConfig, events: Option<EventSender>) -> Self {
32        Self { config, events }
33    }
34
35    /// Attach or replace the progress event channel.
36    pub fn set_events(&mut self, events: Option<EventSender>) {
37        self.events = events;
38    }
39
40    fn status(&self, msg: impl Into<String>) {
41        emit(&self.events, TestEvent::Status(msg.into()));
42    }
43
44    pub async fn run_diagnostics(&self) -> Result<NetworkDiagnostics, Box<dyn std::error::Error>> {
45        // Determine gateway
46        let gateway_ip = self.detect_gateway().await?;
47
48        // Get DNS servers
49        let dns_servers = self.detect_dns_servers().await?;
50
51        // Measure DNS response time
52        let dns_response_time = self.measure_dns_response_time().await?;
53
54        // Trace route
55        let route_hops = self.trace_route("8.8.8.8").await?;
56
57        // Check IPv6 availability
58        let is_ipv6_available = self.check_ipv6().await?;
59
60        // Determine connection type (wired/wireless)
61        let connection_type = self.detect_connection_type().await?;
62
63        // Get network interface
64        let network_interface = self.detect_network_interface().await?;
65
66        let diagnostics = NetworkDiagnostics {
67            gateway_ip,
68            dns_servers,
69            dns_response_time_ms: dns_response_time,
70            route_hops,
71            is_ipv6_available,
72            connection_type: Some(connection_type),
73            network_interface: Some(network_interface),
74        };
75
76        emit(
77            &self.events,
78            TestEvent::DiagnosticsComplete(Box::new(diagnostics.clone())),
79        );
80
81        Ok(diagnostics)
82    }
83
84    async fn detect_gateway(&self) -> Result<Option<IpAddr>, Box<dyn std::error::Error>> {
85        self.status("Scanning network topology...");
86
87        // This is a simplified approach. In a real implementation, you'd:
88        // 1. On Windows: Use "ipconfig" and parse the "Default Gateway" line
89        // 2. On Linux/macOS: Use "ip route | grep default" or "netstat -nr | grep default"
90        sleep(Duration::from_millis(800)).await;
91
92        let gateway = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
93        if let Some(gw) = gateway {
94            self.status(format!("Gateway node detected: {}", gw));
95        }
96        Ok(gateway)
97    }
98
99    async fn detect_dns_servers(&self) -> Result<Vec<IpAddr>, Box<dyn std::error::Error>> {
100        self.status("Probing DNS infrastructure...");
101
102        // Simplified/simulated; real code would read resolv.conf / scutil / ipconfig.
103        sleep(Duration::from_millis(700)).await;
104
105        let dns_servers = vec![
106            IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)),
107            IpAddr::V4(Ipv4Addr::new(8, 8, 4, 4)),
108        ];
109
110        self.status(format!("DNS nodes identified: {}", dns_servers.len()));
111        Ok(dns_servers)
112    }
113
114    async fn measure_dns_response_time(&self) -> Result<f64, Box<dyn std::error::Error>> {
115        self.status("Measuring DNS response time...");
116
117        let domains = vec![
118            "google.com",
119            "amazon.com",
120            "facebook.com",
121            "microsoft.com",
122            "apple.com",
123        ];
124
125        let mut total_time = 0.0;
126        let mut successful_lookups = 0;
127
128        for domain in domains {
129            let start = Instant::now();
130            match lookup_host(domain) {
131                Ok(_) => {
132                    let duration = start.elapsed().as_millis() as f64;
133                    total_time += duration;
134                    successful_lookups += 1;
135                    self.status(format!("Resolved {} in {:.2}ms", domain, duration));
136                }
137                Err(e) => {
138                    self.status(format!("Failed to resolve {}: {}", domain, e));
139                }
140            }
141
142            sleep(Duration::from_millis(100)).await;
143        }
144
145        let avg_time = if successful_lookups > 0 {
146            total_time / successful_lookups as f64
147        } else {
148            0.0
149        };
150
151        self.status(format!("Average DNS response: {:.2}ms", avg_time));
152        Ok(avg_time)
153    }
154
155    async fn trace_route(&self, target: &str) -> Result<Vec<RouteHop>, Box<dyn std::error::Error>> {
156        self.status(format!("Tracing route to {}...", target));
157
158        let max_hops = 15;
159        let mut hops = Vec::new();
160
161        // Simplified/simulated traceroute.
162        for hop_number in 1..=max_hops {
163            let mut rng = rand::rng();
164            let delay = if hop_number < 3 {
165                rng.random_range(1..10)
166            } else if hop_number < 8 {
167                rng.random_range(10..50)
168            } else {
169                rng.random_range(50..150)
170            };
171
172            sleep(Duration::from_millis(delay)).await;
173
174            // Simulate sometimes missing hops
175            let address = if hop_number != 6 && hop_number != 9 {
176                let fake_ip = format!("192.168.{}.{}", hop_number, hop_number * 10);
177                Some(fake_ip.parse::<IpAddr>()?)
178            } else {
179                None
180            };
181
182            let response_time = if address.is_some() {
183                Some(delay as f64)
184            } else {
185                None
186            };
187
188            hops.push(RouteHop {
189                hop_number: hop_number as u32,
190                address,
191                hostname: None,
192                response_time_ms: response_time,
193            });
194
195            // Last hop should be the target
196            if hop_number == max_hops {
197                let target_ip = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
198                hops.pop(); // Remove the last simulated hop
199                hops.push(RouteHop {
200                    hop_number: hop_number as u32,
201                    address: Some(target_ip),
202                    hostname: Some(target.to_string()),
203                    response_time_ms: Some(delay as f64),
204                });
205            }
206        }
207
208        self.status(format!("Route to {} mapped: {} hops", target, hops.len()));
209        Ok(hops)
210    }
211
212    async fn check_ipv6(&self) -> Result<bool, Box<dyn std::error::Error>> {
213        self.status("Checking IPv6 connectivity...");
214        sleep(Duration::from_millis(600)).await;
215
216        // Randomly determine if IPv6 is available (simulated)
217        let ipv6_available = rand::rng().random_bool(0.7);
218        self.status(if ipv6_available {
219            "IPv6: active"
220        } else {
221            "IPv6: inactive"
222        });
223        Ok(ipv6_available)
224    }
225
226    async fn detect_connection_type(&self) -> Result<String, Box<dyn std::error::Error>> {
227        self.status("Detecting connection type...");
228        sleep(Duration::from_millis(500)).await;
229
230        let connection_type = if rand::rng().random_bool(0.6) {
231            "Wireless (Wi-Fi)".to_string()
232        } else {
233            "Wired (Ethernet)".to_string()
234        };
235
236        self.status(format!("Connection type: {}", connection_type));
237        Ok(connection_type)
238    }
239
240    async fn detect_network_interface(&self) -> Result<String, Box<dyn std::error::Error>> {
241        self.status("Detecting network interface...");
242        sleep(Duration::from_millis(400)).await;
243
244        let interface = if cfg!(target_os = "windows") {
245            "Ethernet".to_string()
246        } else if cfg!(target_os = "macos") {
247            "en0".to_string()
248        } else {
249            "eth0".to_string()
250        };
251
252        self.status(format!("Network interface: {}", interface));
253        Ok(interface)
254    }
255}