seer_core/dns/propagation/
checker.rs1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3use std::time::{Duration, Instant};
4
5use futures::future::join_all;
6use tokio::sync::Semaphore;
7use tracing::{debug, instrument, warn};
8
9use super::analysis::{
10 analyze_results, build_nameserver_consensus, build_nameserver_inconsistencies, PerVantage,
11};
12use super::servers::default_dns_servers;
13use super::types::{DnsServer, NameserverDetails, PropagationResult, ServerResult};
14use crate::dns::records::{RecordData, RecordType};
15use crate::dns::resolver::DnsResolver;
16use crate::error::{Result, SeerError};
17
18const MAX_CONCURRENT_NS_LOOKUPS: usize = 50;
21
22#[derive(Debug, Clone)]
24pub struct PropagationChecker {
25 resolver: DnsResolver,
26 servers: Vec<DnsServer>,
27}
28
29impl Default for PropagationChecker {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl PropagationChecker {
36 pub fn new() -> Self {
37 Self {
38 resolver: DnsResolver::new().with_timeout(Duration::from_secs(5)),
39 servers: default_dns_servers().to_vec(),
40 }
41 }
42
43 pub fn with_servers(mut self, servers: Vec<DnsServer>) -> Self {
44 self.servers = servers;
45 self
46 }
47
48 pub fn add_server(mut self, server: DnsServer) -> Self {
49 self.servers.push(server);
50 self
51 }
52
53 pub fn with_timeout(mut self, timeout: Duration) -> Self {
54 self.resolver = DnsResolver::new().with_timeout(timeout);
55 self
56 }
57
58 const PROPAGATION_TIMEOUT: Duration = Duration::from_secs(15);
62
63 const NS_RESOLUTION_TIMEOUT: Duration = Duration::from_secs(8);
71
72 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
73 pub async fn check(&self, domain: &str, record_type: RecordType) -> Result<PropagationResult> {
74 debug!(servers = self.servers.len(), "Starting propagation check");
75
76 let futures: Vec<_> = self
77 .servers
78 .iter()
79 .map(|server| self.query_server(domain, record_type, server.clone()))
80 .collect();
81
82 let results = tokio::time::timeout(Self::PROPAGATION_TIMEOUT, join_all(futures))
83 .await
84 .map_err(|_| {
85 warn!(
86 domain = %domain,
87 timeout_secs = Self::PROPAGATION_TIMEOUT.as_secs(),
88 "Propagation check timed out"
89 );
90 SeerError::Timeout(format!(
91 "propagation check for {} timed out after {}s",
92 domain,
93 Self::PROPAGATION_TIMEOUT.as_secs()
94 ))
95 })?;
96
97 let servers_checked = results.len();
98 let servers_responding = results.iter().filter(|r| r.success).count();
99
100 let outcome = analyze_results(&results, record_type);
101
102 let nameserver_details = if record_type == RecordType::NS {
111 match tokio::time::timeout(
112 Self::NS_RESOLUTION_TIMEOUT,
113 self.resolve_nameserver_details(&results),
114 )
115 .await
116 {
117 Ok(details) => details,
118 Err(_) => {
119 warn!(
120 domain = %domain,
121 timeout_secs = Self::NS_RESOLUTION_TIMEOUT.as_secs(),
122 "Per-vantage nameserver IP enrichment timed out; returning results without IP annotations"
123 );
124 None
125 }
126 }
127 } else {
128 None
129 };
130
131 Ok(PropagationResult {
132 domain: domain.to_string(),
133 record_type,
134 servers_checked,
135 servers_responding,
136 propagation_percentage: outcome.propagation_percentage,
137 results,
138 consensus_values: outcome.consensus_values,
139 inconsistencies: outcome.inconsistencies,
140 unreachable_servers: outcome.unreachable_servers,
141 dnssec_validated: false,
145 nameserver_details,
146 })
147 }
148
149 async fn resolve_nameserver_details(
164 &self,
165 results: &[ServerResult],
166 ) -> Option<NameserverDetails> {
167 let unique: HashSet<String> = results
168 .iter()
169 .flat_map(|sr| sr.records.iter())
170 .filter_map(|r| match &r.data {
171 RecordData::NS { nameserver } => Some(nameserver.to_ascii_lowercase()),
172 _ => None,
173 })
174 .collect();
175
176 if unique.is_empty() {
177 return None;
178 }
179
180 let unique_vec: Vec<String> = unique.into_iter().collect();
185 let sem = Arc::new(Semaphore::new(MAX_CONCURRENT_NS_LOOKUPS));
190 let mut tasks = Vec::new();
191 for sr in results.iter().filter(|sr| sr.success) {
192 for ns in &unique_vec {
193 let resolver = self.resolver.clone();
194 let server_ip = sr.server.ip.clone();
195 let ns = ns.clone();
196 let sem = sem.clone();
197 tasks.push(async move {
198 let _permit = sem.acquire().await.ok();
200 let (a_res, aaaa_res) = tokio::join!(
201 resolver.resolve(&ns, RecordType::A, Some(&server_ip)),
202 resolver.resolve(&ns, RecordType::AAAA, Some(&server_ip)),
203 );
204 let mut ips: Vec<String> = Vec::new();
205 if let Ok(records) = a_res {
206 for r in &records {
207 if let RecordData::A { address } = &r.data {
208 ips.push(address.clone());
209 }
210 }
211 }
212 if let Ok(records) = aaaa_res {
213 for r in &records {
214 if let RecordData::AAAA { address } = &r.data {
215 ips.push(address.clone());
216 }
217 }
218 }
219 ips.sort();
220 ips.dedup();
221 (server_ip, ns, ips)
222 });
223 }
224 }
225
226 let outputs = join_all(tasks).await;
227 let mut per_vantage: PerVantage = HashMap::new();
228 for (server_ip, ns, ips) in outputs {
229 per_vantage.entry(server_ip).or_default().insert(ns, ips);
230 }
231
232 let consensus = build_nameserver_consensus(results, &per_vantage, &unique_vec);
233 let inconsistencies = build_nameserver_inconsistencies(results, &per_vantage, &consensus);
234
235 Some(NameserverDetails {
236 consensus,
237 per_vantage,
238 inconsistencies,
239 })
240 }
241
242 async fn query_server(
243 &self,
244 domain: &str,
245 record_type: RecordType,
246 server: DnsServer,
247 ) -> ServerResult {
248 let start = Instant::now();
249
250 match self
251 .resolver
252 .resolve(domain, record_type, Some(&server.ip))
253 .await
254 {
255 Ok(records) => {
256 let response_time_ms = start.elapsed().as_millis() as u64;
257 debug!(
258 server = %server.name,
259 records = records.len(),
260 time_ms = response_time_ms,
261 "Server responded"
262 );
263 ServerResult {
264 server,
265 records,
266 response_time_ms,
267 success: true,
268 error: None,
269 }
270 }
271 Err(e) => {
272 let response_time_ms = start.elapsed().as_millis() as u64;
273 debug!(
274 server = %server.name,
275 error = %e,
276 "Server query failed"
277 );
278 ServerResult {
279 server,
280 records: vec![],
281 response_time_ms,
282 success: false,
283 error: Some(e.sanitized_message()),
285 }
286 }
287 }
288 }
289}