1use std::collections::HashMap;
2use std::time::{Duration, Instant};
3
4use futures::future::join_all;
5use serde::{Deserialize, Serialize};
6use tracing::{debug, instrument};
7
8use super::records::{DnsRecord, RecordType};
9use super::resolver::DnsResolver;
10use crate::error::Result;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct DnsServer {
14 pub name: String,
15 pub ip: String,
16 pub location: String,
17 pub provider: String,
18}
19
20impl DnsServer {
21 pub fn new(name: &str, ip: &str, location: &str, provider: &str) -> Self {
22 Self {
23 name: name.to_string(),
24 ip: ip.to_string(),
25 location: location.to_string(),
26 provider: provider.to_string(),
27 }
28 }
29}
30
31pub fn default_dns_servers() -> Vec<DnsServer> {
32 vec![
33 DnsServer::new("Google", "8.8.8.8", "North America", "Google"),
35 DnsServer::new("Cloudflare", "1.1.1.1", "North America", "Cloudflare"),
36 DnsServer::new("OpenDNS", "208.67.222.222", "North America", "Cisco OpenDNS"),
37 DnsServer::new("Quad9", "9.9.9.9", "North America", "Quad9"),
38 DnsServer::new("Level3", "4.2.2.1", "North America", "Lumen"),
39 DnsServer::new("DNS.Watch", "84.200.69.80", "Europe", "DNS.Watch"),
41 DnsServer::new("Mullvad", "194.242.2.2", "Europe", "Mullvad"),
42 DnsServer::new("dns0.eu", "193.110.81.0", "Europe", "dns0.eu"),
43 DnsServer::new("Yandex", "77.88.8.8", "Europe", "Yandex"),
44 DnsServer::new("UncensoredDNS", "91.239.100.100", "Europe", "UncensoredDNS"),
45 DnsServer::new("AliDNS", "223.5.5.5", "Asia Pacific", "Alibaba"),
47 DnsServer::new("114DNS", "114.114.114.114", "Asia Pacific", "114DNS"),
48 DnsServer::new("Tencent DNSPod", "119.29.29.29", "Asia Pacific", "Tencent"),
49 DnsServer::new("TWNIC", "101.101.101.101", "Asia Pacific", "TWNIC"),
50 DnsServer::new("HiNet", "168.95.1.1", "Asia Pacific", "Chunghwa Telecom"),
51 DnsServer::new("Claro Brasil", "200.248.178.54", "Latin America", "Claro"),
53 DnsServer::new("Telefonica Brasil", "200.176.2.10", "Latin America", "Telefonica"),
54 DnsServer::new("Antel Uruguay", "200.40.30.245", "Latin America", "Antel"),
55 DnsServer::new("Telmex Mexico", "200.33.146.217", "Latin America", "Telmex"),
56 DnsServer::new("CenturyLink LATAM", "200.75.51.132", "Latin America", "CenturyLink"),
57 DnsServer::new("Liquid Telecom", "41.63.64.74", "Africa", "Liquid Telecom"),
59 DnsServer::new("SEACOM", "196.216.2.1", "Africa", "SEACOM"),
60 DnsServer::new("Safaricom Kenya", "196.201.214.40", "Africa", "Safaricom"),
61 DnsServer::new("MTN South Africa", "196.11.180.20", "Africa", "MTN"),
62 DnsServer::new("Telecom Egypt", "196.205.152.10", "Africa", "Telecom Egypt"),
63 DnsServer::new("Etisalat UAE", "213.42.20.20", "Middle East", "Etisalat"),
65 DnsServer::new("STC Saudi", "212.118.129.106", "Middle East", "STC"),
66 DnsServer::new("Bezeq Israel", "192.115.106.81", "Middle East", "Bezeq"),
67 DnsServer::new("Turk Telekom", "195.175.39.39", "Middle East", "Turk Telekom"),
68 DnsServer::new("Ooredoo Qatar", "212.77.192.10", "Middle East", "Ooredoo"),
69 ]
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ServerResult {
74 pub server: DnsServer,
75 pub records: Vec<DnsRecord>,
76 pub response_time_ms: u64,
77 pub success: bool,
78 pub error: Option<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PropagationResult {
83 pub domain: String,
84 pub record_type: RecordType,
85 pub servers_checked: usize,
86 pub servers_responding: usize,
87 pub propagation_percentage: f64,
88 pub results: Vec<ServerResult>,
89 pub consensus_values: Vec<String>,
90 pub inconsistencies: Vec<String>,
91}
92
93impl PropagationResult {
94 pub fn is_fully_propagated(&self) -> bool {
95 self.propagation_percentage >= 100.0
96 }
97
98 pub fn has_inconsistencies(&self) -> bool {
99 !self.inconsistencies.is_empty()
100 }
101}
102
103#[derive(Debug, Clone)]
104pub struct PropagationChecker {
105 resolver: DnsResolver,
106 servers: Vec<DnsServer>,
107}
108
109impl Default for PropagationChecker {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115impl PropagationChecker {
116 pub fn new() -> Self {
117 Self {
118 resolver: DnsResolver::new().with_timeout(Duration::from_secs(5)),
119 servers: default_dns_servers(),
120 }
121 }
122
123 pub fn with_servers(mut self, servers: Vec<DnsServer>) -> Self {
124 self.servers = servers;
125 self
126 }
127
128 pub fn add_server(mut self, server: DnsServer) -> Self {
129 self.servers.push(server);
130 self
131 }
132
133 pub fn with_timeout(mut self, timeout: Duration) -> Self {
134 self.resolver = DnsResolver::new().with_timeout(timeout);
135 self
136 }
137
138 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
139 pub async fn check(
140 &self,
141 domain: &str,
142 record_type: RecordType,
143 ) -> Result<PropagationResult> {
144 debug!(servers = self.servers.len(), "Starting propagation check");
145
146 let futures: Vec<_> = self
147 .servers
148 .iter()
149 .map(|server| self.query_server(domain, record_type, server.clone()))
150 .collect();
151
152 let results = join_all(futures).await;
153
154 let servers_checked = results.len();
155 let servers_responding = results.iter().filter(|r| r.success).count();
156
157 let (propagation_percentage, consensus_values, inconsistencies) =
159 analyze_results(&results, record_type);
160
161 Ok(PropagationResult {
162 domain: domain.to_string(),
163 record_type,
164 servers_checked,
165 servers_responding,
166 propagation_percentage,
167 results,
168 consensus_values,
169 inconsistencies,
170 })
171 }
172
173 async fn query_server(
174 &self,
175 domain: &str,
176 record_type: RecordType,
177 server: DnsServer,
178 ) -> ServerResult {
179 let start = Instant::now();
180
181 match self
182 .resolver
183 .resolve(domain, record_type, Some(&server.ip))
184 .await
185 {
186 Ok(records) => {
187 let response_time_ms = start.elapsed().as_millis() as u64;
188 debug!(
189 server = %server.name,
190 records = records.len(),
191 time_ms = response_time_ms,
192 "Server responded"
193 );
194 ServerResult {
195 server,
196 records,
197 response_time_ms,
198 success: true,
199 error: None,
200 }
201 }
202 Err(e) => {
203 let response_time_ms = start.elapsed().as_millis() as u64;
204 debug!(
205 server = %server.name,
206 error = %e,
207 "Server query failed"
208 );
209 ServerResult {
210 server,
211 records: vec![],
212 response_time_ms,
213 success: false,
214 error: Some(e.to_string()),
215 }
216 }
217 }
218 }
219}
220
221fn analyze_results(
222 results: &[ServerResult],
223 record_type: RecordType,
224) -> (f64, Vec<String>, Vec<String>) {
225 let successful: Vec<_> = results.iter().filter(|r| r.success).collect();
226
227 if successful.is_empty() {
228 return (0.0, vec![], vec!["No servers responded".to_string()]);
229 }
230
231 let mut value_counts: HashMap<Vec<String>, usize> = HashMap::new();
233
234 for result in &successful {
235 let mut values: Vec<String> = result
236 .records
237 .iter()
238 .map(|r| r.format_short())
239 .collect();
240 values.sort();
241 *value_counts.entry(values).or_insert(0) += 1;
242 }
243
244 let (consensus_values, consensus_count) = value_counts
246 .iter()
247 .max_by_key(|(_, count)| *count)
248 .map(|(values, count)| (values.clone(), *count))
249 .unwrap_or((vec![], 0));
250
251 let propagation_percentage = if successful.is_empty() {
253 0.0
254 } else {
255 (consensus_count as f64 / successful.len() as f64) * 100.0
256 };
257
258 let mut inconsistencies = Vec::new();
260 for result in &successful {
261 let mut values: Vec<String> = result
262 .records
263 .iter()
264 .map(|r| r.format_short())
265 .collect();
266 values.sort();
267
268 if values != consensus_values {
269 let inconsistency = format!(
270 "{} ({}): {} vs consensus: {}",
271 result.server.name,
272 result.server.ip,
273 if values.is_empty() {
274 "NXDOMAIN".to_string()
275 } else {
276 values.join(", ")
277 },
278 if consensus_values.is_empty() {
279 "NXDOMAIN".to_string()
280 } else {
281 consensus_values.join(", ")
282 }
283 );
284 inconsistencies.push(inconsistency);
285 }
286 }
287
288 for result in results.iter().filter(|r| !r.success) {
290 let error_msg = result.error.as_deref().unwrap_or("Unknown error");
291 inconsistencies.push(format!(
292 "{} ({}): {}",
293 result.server.name, result.server.ip, error_msg
294 ));
295 }
296
297 if consensus_values.is_empty() && record_type != RecordType::A && record_type != RecordType::AAAA {
299 }
301
302 (propagation_percentage, consensus_values, inconsistencies)
303}