Skip to main content

nd_300/diagnostics/
dns.rs

1use serde::Serialize;
2use std::time::Instant;
3
4use super::DiagnosticResult;
5
6#[derive(Debug, Clone, Serialize)]
7pub struct DnsInfo {
8    pub servers: Vec<DnsServer>,
9    pub resolution_test: Option<DnsResolutionTest>,
10}
11
12#[derive(Debug, Clone, Serialize)]
13pub struct DnsServer {
14    pub address: String,
15    pub reachable: bool,
16    pub latency_ms: Option<f64>,
17}
18
19#[derive(Debug, Clone, Serialize)]
20pub struct DnsResolutionTest {
21    pub domain: String,
22    pub resolved: bool,
23    pub resolution_time_ms: f64,
24    pub resolved_ips: Vec<String>,
25}
26
27pub async fn check() -> (DiagnosticResult, Option<DnsInfo>) {
28    let servers = get_dns_servers().await;
29
30    if servers.is_empty() {
31        return (
32            DiagnosticResult::fail("DNS", "No DNS servers configured"),
33            None,
34        );
35    }
36
37    // Test DNS resolution
38    let resolution = test_dns_resolution().await;
39
40    let server_results: Vec<DnsServer> = servers
41        .iter()
42        .map(|s| DnsServer {
43            address: s.clone(),
44            reachable: true, // We know they're configured
45            latency_ms: None,
46        })
47        .collect();
48
49    let info = DnsInfo {
50        servers: server_results,
51        resolution_test: resolution.clone(),
52    };
53
54    let result = match resolution {
55        Some(ref test) if test.resolved => {
56            let time_ms = test.resolution_time_ms;
57            if time_ms > 500.0 {
58                DiagnosticResult::warn("DNS", format!("Resolving slowly ({:.0}ms)", time_ms))
59            } else if time_ms > 200.0 {
60                DiagnosticResult::warn(
61                    "DNS",
62                    format!("Resolving with moderate latency ({:.0}ms)", time_ms),
63                )
64            } else {
65                DiagnosticResult::ok("DNS", format!("Resolving normally ({:.0}ms)", time_ms))
66            }
67        }
68        _ => DiagnosticResult::fail("DNS", "DNS resolution failed"),
69    };
70
71    (result, Some(info))
72}
73
74async fn get_dns_servers() -> Vec<String> {
75    #[cfg(windows)]
76    {
77        get_dns_servers_windows().await
78    }
79
80    #[cfg(target_os = "macos")]
81    {
82        get_dns_servers_macos().await
83    }
84
85    #[cfg(target_os = "linux")]
86    {
87        get_dns_servers_linux().await
88    }
89}
90
91#[cfg(windows)]
92async fn get_dns_servers_windows() -> Vec<String> {
93    let mut servers = Vec::new();
94
95    let mut cmd = tokio::process::Command::new("netsh");
96    cmd.args(["interface", "ip", "show", "dns"]);
97    if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
98        let text = String::from_utf8_lossy(&output.stdout);
99        for line in text.lines() {
100            let line = line.trim();
101            // Look for IP addresses in the output
102            if let Some(ip) = extract_ip(line) {
103                if !servers.contains(&ip) {
104                    servers.push(ip);
105                }
106            }
107        }
108    }
109
110    if servers.is_empty() {
111        // Fallback: try ipconfig
112        let mut cmd = tokio::process::Command::new("ipconfig");
113        cmd.args(["/all"]);
114        if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
115            let text = String::from_utf8_lossy(&output.stdout);
116            let mut in_dns_section = false;
117            for line in text.lines() {
118                if line.contains("DNS Servers") {
119                    in_dns_section = true;
120                    if let Some(ip) = extract_ip(line) {
121                        servers.push(ip);
122                    }
123                } else if in_dns_section {
124                    let trimmed = line.trim();
125                    if trimmed.is_empty()
126                        || trimmed.contains(':') && !trimmed.starts_with(char::is_numeric)
127                    {
128                        in_dns_section = false;
129                    } else if let Some(ip) = extract_ip(trimmed) {
130                        servers.push(ip);
131                    }
132                }
133            }
134        }
135    }
136
137    servers
138}
139
140#[cfg(target_os = "macos")]
141async fn get_dns_servers_macos() -> Vec<String> {
142    let mut servers = Vec::new();
143
144    let mut cmd = tokio::process::Command::new("scutil");
145    cmd.args(["--dns"]);
146    if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
147        let text = String::from_utf8_lossy(&output.stdout);
148        for line in text.lines() {
149            if line.contains("nameserver") {
150                if let Some(ip) = extract_ip(line) {
151                    if !servers.contains(&ip) {
152                        servers.push(ip);
153                    }
154                }
155            }
156        }
157    }
158
159    servers
160}
161
162#[cfg(target_os = "linux")]
163async fn get_dns_servers_linux() -> Vec<String> {
164    let mut servers = Vec::new();
165
166    // Try /etc/resolv.conf first
167    if let Ok(content) = tokio::fs::read_to_string("/etc/resolv.conf").await {
168        for line in content.lines() {
169            if line.starts_with("nameserver") {
170                if let Some(ip) = line.split_whitespace().nth(1) {
171                    servers.push(ip.to_string());
172                }
173            }
174        }
175    }
176
177    // Fallback: try resolvectl
178    if servers.is_empty() || servers.iter().all(|s| s == "127.0.0.53") {
179        let mut cmd = tokio::process::Command::new("resolvectl");
180        cmd.args(["status"]);
181        if let Some(output) = super::util::run_with_timeout(cmd, super::util::SLOW).await {
182            let text = String::from_utf8_lossy(&output.stdout);
183            for line in text.lines() {
184                if line.contains("DNS Servers") || line.contains("Current DNS") {
185                    if let Some(ip) = extract_ip(line) {
186                        if ip != "127.0.0.53" && !servers.contains(&ip) {
187                            servers.push(ip);
188                        }
189                    }
190                }
191            }
192        }
193    }
194
195    servers
196}
197
198fn extract_ip(text: &str) -> Option<String> {
199    for word in text.split_whitespace() {
200        // Try IPv4 first
201        let cleaned = word.trim_matches(|c: char| !c.is_ascii_digit() && c != '.');
202        let parts: Vec<&str> = cleaned.split('.').collect();
203        if parts.len() == 4 && parts.iter().all(|p| p.parse::<u8>().is_ok()) {
204            return Some(cleaned.to_string());
205        }
206
207        // Try IPv6: must contain at least two colons and only hex digits/colons
208        let trimmed = word.trim_matches(|c: char| !c.is_ascii_hexdigit() && c != ':');
209        if trimmed.matches(':').count() >= 2
210            && !trimmed.is_empty()
211            && trimmed.chars().all(|c| c.is_ascii_hexdigit() || c == ':')
212        {
213            return Some(trimmed.to_string());
214        }
215    }
216    None
217}
218
219async fn test_dns_resolution() -> Option<DnsResolutionTest> {
220    let domain = "dns.google";
221    let start = Instant::now();
222
223    // Use system DNS resolution (bounded so a black-holed resolver can't hang).
224    match super::util::lookup_host_timeout(format!("{}:443", domain), super::util::RESOLVE).await {
225        Some(addrs) => {
226            let elapsed = start.elapsed().as_secs_f64() * 1000.0;
227            let ips: Vec<String> = addrs.into_iter().map(|a| a.ip().to_string()).collect();
228            Some(DnsResolutionTest {
229                domain: domain.to_string(),
230                resolved: !ips.is_empty(),
231                resolution_time_ms: elapsed,
232                resolved_ips: ips,
233            })
234        }
235        None => Some(DnsResolutionTest {
236            domain: domain.to_string(),
237            resolved: false,
238            resolution_time_ms: start.elapsed().as_secs_f64() * 1000.0,
239            resolved_ips: vec![],
240        }),
241    }
242}