nd_300/diagnostics/
dns.rs1use 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 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, 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 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 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 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 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 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 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 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}