synapse_pingora/crawler/
dns_resolver.rs1use std::net::IpAddr;
9use std::sync::Arc;
10use std::time::Duration;
11use thiserror::Error;
12use tokio::sync::Semaphore;
13use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
14use trust_dns_resolver::TokioAsyncResolver;
15
16#[derive(Debug, Error, Clone)]
18pub enum DnsError {
19 #[error("DNS resolver creation failed: {0}")]
20 ResolverCreation(String),
21 #[error("DNS lookup failed: {0}")]
22 LookupFailed(String),
23 #[error("DNS timeout after {0}ms")]
24 Timeout(u64),
25 #[error("DNS rate limit exceeded, try again later")]
26 RateLimited,
27 #[error(
28 "DNS verification failed: IP not in forward lookup results (possible rebinding attack)"
29 )]
30 IpMismatch,
31}
32
33impl From<trust_dns_resolver::error::ResolveError> for DnsError {
34 fn from(e: trust_dns_resolver::error::ResolveError) -> Self {
35 DnsError::ResolverCreation(e.to_string())
36 }
37}
38
39#[derive(Debug)]
41pub struct DnsResolver {
42 resolver: TokioAsyncResolver,
43 timeout: Duration,
44 semaphore: Arc<Semaphore>,
46 max_concurrent: usize,
48}
49
50impl DnsResolver {
51 pub async fn new(timeout_ms: u64, max_concurrent: usize) -> Result<Self, DnsError> {
57 let mut opts = ResolverOpts::default();
58 opts.timeout = Duration::from_millis(timeout_ms);
59 opts.attempts = 2;
60
61 let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), opts);
62
63 Ok(Self {
64 resolver,
65 timeout: Duration::from_millis(timeout_ms),
66 semaphore: Arc::new(Semaphore::new(max_concurrent)),
67 max_concurrent,
68 })
69 }
70
71 pub fn available_permits(&self) -> usize {
73 self.semaphore.available_permits()
74 }
75
76 pub fn max_concurrent(&self) -> usize {
78 self.max_concurrent
79 }
80
81 async fn acquire_permit(&self) -> Option<tokio::sync::SemaphorePermit<'_>> {
84 match self.semaphore.try_acquire() {
87 Ok(permit) => Some(permit),
88 Err(_) => {
89 tracing::warn!(
90 "DNS rate limit reached: {}/{} permits in use",
91 self.max_concurrent - self.semaphore.available_permits(),
92 self.max_concurrent
93 );
94 None
95 }
96 }
97 }
98
99 pub async fn reverse_lookup(&self, ip: IpAddr) -> Result<Option<String>, DnsError> {
103 let _permit = self.acquire_permit().await.ok_or(DnsError::RateLimited)?;
104
105 match tokio::time::timeout(self.timeout, self.resolver.reverse_lookup(ip)).await {
106 Ok(Ok(response)) => {
107 if let Some(record) = response.iter().next() {
109 Ok(Some(record.to_string().trim_end_matches('.').to_string()))
110 } else {
111 Ok(None)
112 }
113 }
114 Ok(Err(e)) => {
115 tracing::debug!("Reverse DNS lookup for {} failed: {}", ip, e);
117 Ok(None)
118 }
119 Err(_) => {
120 tracing::debug!(
121 "Reverse DNS lookup for {} timed out after {}ms",
122 ip,
123 self.timeout.as_millis()
124 );
125 Err(DnsError::Timeout(self.timeout.as_millis() as u64))
126 }
127 }
128 }
129
130 pub async fn forward_lookup(&self, hostname: &str) -> Result<Vec<IpAddr>, DnsError> {
134 let _permit = self.acquire_permit().await.ok_or(DnsError::RateLimited)?;
135
136 match tokio::time::timeout(self.timeout, self.resolver.lookup_ip(hostname)).await {
137 Ok(Ok(response)) => Ok(response.iter().collect()),
138 Ok(Err(e)) => {
139 tracing::debug!("Forward DNS lookup for {} failed: {}", hostname, e);
140 Err(DnsError::LookupFailed(e.to_string()))
141 }
142 Err(_) => {
143 tracing::debug!(
144 "Forward DNS lookup for {} timed out after {}ms",
145 hostname,
146 self.timeout.as_millis()
147 );
148 Err(DnsError::Timeout(self.timeout.as_millis() as u64))
149 }
150 }
151 }
152
153 pub async fn verify_ip(&self, ip: IpAddr) -> Result<(bool, Option<String>), DnsError> {
167 let hostname = match self.reverse_lookup(ip).await? {
169 Some(h) => h,
170 None => return Ok((false, None)),
171 };
172
173 let resolved_ips = match self.forward_lookup(&hostname).await {
175 Ok(ips) => ips,
176 Err(DnsError::RateLimited) => return Err(DnsError::RateLimited),
177 Err(_) => return Ok((false, Some(hostname))),
178 };
179
180 let verified = resolved_ips.contains(&ip);
183
184 if !verified {
185 tracing::warn!(
186 ip = %ip,
187 hostname = %hostname,
188 resolved_ips = ?resolved_ips,
189 "DNS rebinding check failed: requesting IP not in forward lookup results"
190 );
191 }
192
193 Ok((verified, Some(hostname)))
194 }
195
196 pub async fn verify_ip_strict(&self, ip: IpAddr) -> Result<String, DnsError> {
201 let hostname = match self.reverse_lookup(ip).await? {
203 Some(h) => h,
204 None => return Err(DnsError::LookupFailed("No PTR record".to_string())),
205 };
206
207 let resolved_ips = self.forward_lookup(&hostname).await?;
209
210 if !resolved_ips.contains(&ip) {
212 tracing::warn!(
213 ip = %ip,
214 hostname = %hostname,
215 resolved_ips = ?resolved_ips,
216 "DNS rebinding attack detected: IP not in forward lookup results"
217 );
218 return Err(DnsError::IpMismatch);
219 }
220
221 Ok(hostname)
222 }
223}