Skip to main content

etchdns/
probe.rs

1use crate::dns_parser;
2use crate::errors::{DnsError, EtchDnsResult};
3use crate::stats::SharedStats;
4use log::{debug, error, info, warn};
5use rand::RngExt;
6use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9use tokio::net::UdpSocket;
10use tokio::time;
11
12fn wildcard_address(peer: SocketAddr) -> SocketAddr {
13    let ip = if peer.is_ipv4() {
14        IpAddr::V4(Ipv4Addr::UNSPECIFIED)
15    } else {
16        IpAddr::V6(Ipv6Addr::UNSPECIFIED)
17    };
18    SocketAddr::new(ip, 0)
19}
20
21/// Minimum interval between probe queries (in seconds)
22#[allow(dead_code)]
23const MIN_PROBE_INTERVAL_SECS: u64 = 60;
24
25/// Structure to manage periodic probing of upstream DNS servers
26#[allow(dead_code)]
27pub struct ServerProber {
28    /// List of upstream DNS servers to probe
29    upstream_servers: Vec<String>,
30
31    /// Statistics tracker
32    stats: Arc<SharedStats>,
33
34    /// Interval between probes (in seconds)
35    probe_interval: u64,
36
37    /// Timeout for probe queries (in seconds)
38    probe_timeout: u64,
39}
40
41#[allow(dead_code)]
42impl ServerProber {
43    /// Create a new ServerProber
44    ///
45    /// The probe_timeout defaults to server_timeout if not specified.
46    /// The probe_interval defaults to max(60 seconds, server_timeout) if not specified.
47    #[allow(dead_code)]
48    pub fn new(
49        upstream_servers: Vec<String>,
50        stats: Arc<SharedStats>,
51        server_timeout: u64,
52        probe_interval: Option<u64>,
53        probe_timeout: Option<u64>,
54    ) -> Self {
55        // Default probe timeout is the same as server_timeout
56        let default_probe_timeout = server_timeout;
57
58        // Default probe interval is max(60 seconds, server_timeout)
59        let default_probe_interval = std::cmp::max(MIN_PROBE_INTERVAL_SECS, server_timeout);
60
61        Self {
62            upstream_servers,
63            stats,
64            probe_interval: probe_interval.unwrap_or(default_probe_interval),
65            probe_timeout: probe_timeout.unwrap_or(default_probe_timeout),
66        }
67    }
68
69    /// Start the background probing task
70    pub fn start(self) {
71        tokio::spawn(async move {
72            info!(
73                "Starting server prober with interval of {} seconds",
74                self.probe_interval
75            );
76
77            // Create a ticker that fires at the specified interval
78            let mut interval = time::interval(Duration::from_secs(self.probe_interval));
79
80            loop {
81                // Wait for the next tick
82                interval.tick().await;
83
84                // Probe all servers
85                self.probe_all_servers().await;
86            }
87        });
88    }
89
90    /// Probe all upstream servers
91    async fn probe_all_servers(&self) {
92        debug!("Probing all upstream DNS servers");
93
94        for server in &self.upstream_servers {
95            // Parse the server address
96            match server.parse::<SocketAddr>() {
97                Ok(addr) => {
98                    // Probe the server
99                    match self.probe_server(addr).await {
100                        Ok(response_time) => {
101                            debug!("Probe to {addr} completed in {response_time:.2?}");
102
103                            // Record the success in stats
104                            self.stats.record_success(addr, response_time).await;
105                        }
106                        Err(e) => {
107                            warn!("Probe to {addr} failed: {e}");
108
109                            // Record the failure in stats
110                            self.stats.record_failure(addr).await;
111                        }
112                    }
113                }
114                Err(e) => {
115                    error!("Failed to parse server address {server}: {e}");
116                }
117            }
118        }
119    }
120
121    /// Probe a single server and return the response time
122    async fn probe_server(&self, server_addr: SocketAddr) -> EtchDnsResult<Duration> {
123        // Create a random DNS query
124        let query = self.create_random_query();
125
126        // Create a UDP socket for the probe
127        let socket = UdpSocket::bind(wildcard_address(server_addr))
128            .await
129            .map_err(|e| {
130                DnsError::UpstreamError(format!("Failed to bind socket for probe: {e}"))
131            })?;
132
133        // Start timing
134        let start_time = Instant::now();
135
136        // Send the query
137        socket.send_to(&query, server_addr).await.map_err(|e| {
138            DnsError::UpstreamError(format!("Failed to send probe to {server_addr}: {e}"))
139        })?;
140
141        // Set up a buffer for the response
142        let mut buf = vec![0u8; dns_parser::DNS_MAX_PACKET_SIZE];
143
144        // Wait for a response with timeout
145        match time::timeout(
146            Duration::from_secs(self.probe_timeout),
147            socket.recv_from(&mut buf),
148        )
149        .await
150        {
151            Ok(Ok((len, response_addr))) => {
152                if response_addr != server_addr {
153                    return Err(DnsError::UpstreamError(format!(
154                        "Unexpected probe response source {response_addr}, expected {server_addr}"
155                    ))
156                    .into());
157                }
158
159                if len < 2 || buf[0] != query[0] || buf[1] != query[1] {
160                    return Err(DnsError::UpstreamError(format!(
161                        "Mismatched DNS transaction ID in response from {server_addr}"
162                    ))
163                    .into());
164                }
165
166                // Got a response
167                let response_time = start_time.elapsed();
168
169                // Validate the response
170                if let Err(e) = dns_parser::validate_dns_response(&buf[..len]) {
171                    return Err(DnsError::UpstreamError(format!(
172                        "Invalid DNS response from {server_addr}: {e}"
173                    ))
174                    .into());
175                }
176
177                Ok(response_time)
178            }
179            Ok(Err(e)) => {
180                // Socket error
181                Err(DnsError::UpstreamError(format!(
182                    "Failed to receive response from {server_addr}: {e}"
183                ))
184                .into())
185            }
186            Err(_) => {
187                // Timeout
188                Err(DnsError::UpstreamTimeout.into())
189            }
190        }
191    }
192
193    /// Create a random DNS query
194    fn create_random_query(&self) -> Vec<u8> {
195        // Create a simple query for a common domain
196        // This is a query for google.com with a random transaction ID
197        let mut query = vec![
198            0x00, 0x00, // Transaction ID (will be replaced)
199            0x01, 0x00, // Flags (standard query)
200            0x00, 0x01, // Questions: 1
201            0x00, 0x00, // Answer RRs: 0
202            0x00, 0x00, // Authority RRs: 0
203            0x00, 0x00, // Additional RRs: 0
204            // google.com domain name
205            0x06, b'g', b'o', b'o', b'g', b'l', b'e', 0x03, b'c', b'o', b'm',
206            0x00, // Null terminator
207            0x00, 0x01, // Type: A (Host Address)
208            0x00, 0x01, // Class: IN (Internet)
209        ];
210
211        // Generate a random transaction ID
212        let tid: u16 = rand::rng().random();
213        query[0] = (tid >> 8) as u8;
214        query[1] = tid as u8;
215
216        query
217    }
218}
219
220/// Probe a single server and return the response time
221/// This is a public function that can be used outside of the ServerProber struct
222pub async fn probe_server(server_addr: SocketAddr, timeout_secs: u64) -> EtchDnsResult<Duration> {
223    // Create a random DNS query
224    let query = create_random_query();
225
226    // Create a UDP socket for the probe
227    let socket = UdpSocket::bind(wildcard_address(server_addr))
228        .await
229        .map_err(|e| DnsError::UpstreamError(format!("Failed to bind socket for probe: {e}")))?;
230
231    // Start timing
232    let start_time = Instant::now();
233
234    // Send the query
235    socket.send_to(&query, server_addr).await.map_err(|e| {
236        DnsError::UpstreamError(format!("Failed to send probe to {server_addr}: {e}"))
237    })?;
238
239    // Set up a buffer for the response
240    let mut buf = vec![0u8; dns_parser::DNS_MAX_PACKET_SIZE];
241
242    // Wait for a response with timeout
243    match time::timeout(
244        Duration::from_secs(timeout_secs),
245        socket.recv_from(&mut buf),
246    )
247    .await
248    {
249        Ok(Ok((len, response_addr))) => {
250            if response_addr != server_addr {
251                return Err(DnsError::UpstreamError(format!(
252                    "Unexpected probe response source {response_addr}, expected {server_addr}"
253                ))
254                .into());
255            }
256
257            if len < 2 || buf[0] != query[0] || buf[1] != query[1] {
258                return Err(DnsError::UpstreamError(format!(
259                    "Mismatched DNS transaction ID in response from {server_addr}"
260                ))
261                .into());
262            }
263
264            // Got a response
265            let response_time = start_time.elapsed();
266
267            // Validate the response
268            if let Err(e) = dns_parser::validate_dns_response(&buf[..len]) {
269                return Err(DnsError::UpstreamError(format!(
270                    "Invalid DNS response from {server_addr}: {e}"
271                ))
272                .into());
273            }
274
275            Ok(response_time)
276        }
277        Ok(Err(e)) => {
278            // Socket error
279            Err(DnsError::UpstreamError(format!(
280                "Failed to receive response from {server_addr}: {e}"
281            ))
282            .into())
283        }
284        Err(_) => {
285            // Timeout
286            Err(DnsError::UpstreamTimeout.into())
287        }
288    }
289}
290
291/// Create a random DNS query
292/// This is a helper function for the probe_server function
293fn create_random_query() -> Vec<u8> {
294    // Create a simple query for a common domain
295    // This is a query for google.com with a random transaction ID
296    let mut query = vec![
297        0x00, 0x00, // Transaction ID (will be replaced)
298        0x01, 0x00, // Flags (standard query)
299        0x00, 0x01, // Questions: 1
300        0x00, 0x00, // Answer RRs: 0
301        0x00, 0x00, // Authority RRs: 0
302        0x00, 0x00, // Additional RRs: 0
303        // google.com domain name
304        0x06, b'g', b'o', b'o', b'g', b'l', b'e', 0x03, b'c', b'o', b'm',
305        0x00, // Null terminator
306        0x00, 0x01, // Type: A (Host Address)
307        0x00, 0x01, // Class: IN (Internet)
308    ];
309
310    // Generate a random transaction ID
311    let tid: u16 = rand::rng().random_range(0..65535);
312    query[0] = (tid >> 8) as u8;
313    query[1] = tid as u8;
314
315    query
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use std::net::SocketAddr;
322    use std::time::Duration;
323
324    #[test]
325    fn test_wildcard_address_matches_peer_family() {
326        let ipv4 = wildcard_address("192.0.2.1:53".parse().unwrap());
327        let ipv6 = wildcard_address("[2001:db8::1]:53".parse().unwrap());
328
329        assert!(ipv4.is_ipv4());
330        assert!(ipv6.is_ipv6());
331        assert_eq!(ipv4.port(), 0);
332        assert_eq!(ipv6.port(), 0);
333    }
334
335    #[tokio::test]
336    async fn test_create_random_query() {
337        let stats = Arc::new(SharedStats::new());
338        let server_timeout = 5; // 5 seconds
339        let prober = ServerProber::new(
340            vec!["8.8.8.8:53".to_string()],
341            stats,
342            server_timeout,
343            None,
344            None,
345        );
346
347        let query = prober.create_random_query();
348
349        // Check that the query is a valid DNS query
350        assert!(query.len() > 12); // DNS header is 12 bytes
351
352        // Check that the query has the correct flags (standard query)
353        assert_eq!(query[2], 0x01); // QR=0, Opcode=0, AA=0, TC=0, RD=1
354        assert_eq!(query[3], 0x00); // RA=0, Z=0, RCODE=0
355
356        // Check that the query has one question
357        assert_eq!(query[4], 0x00);
358        assert_eq!(query[5], 0x01);
359
360        // Check that the query has no answers, authority, or additional records
361        assert_eq!(query[6], 0x00);
362        assert_eq!(query[7], 0x00);
363        assert_eq!(query[8], 0x00);
364        assert_eq!(query[9], 0x00);
365        assert_eq!(query[10], 0x00);
366        assert_eq!(query[11], 0x00);
367    }
368
369    #[tokio::test]
370    async fn test_probe_server() {
371        // This test requires an actual DNS server to be running
372        // We'll use Google's public DNS server for testing
373        let stats = Arc::new(SharedStats::new());
374        let server_timeout = 5; // 5 seconds
375        let prober = ServerProber::new(
376            vec!["8.8.8.8:53".to_string()],
377            stats.clone(),
378            server_timeout,
379            None,
380            None,
381        );
382
383        // Try to probe the server
384        let addr = "8.8.8.8:53".parse::<SocketAddr>().unwrap();
385        match prober.probe_server(addr).await {
386            Ok(response_time) => {
387                // If the probe succeeds, check that the response time is reasonable
388                assert!(response_time > Duration::from_micros(0));
389
390                // Manually update the stats since the probe_server method doesn't update them
391                stats.record_success(addr, response_time).await;
392
393                // Check that the stats were updated
394                let resolvers_by_speed = stats.get_resolvers_by_speed().await;
395                assert!(!resolvers_by_speed.is_empty());
396            }
397            Err(e) => {
398                // If the probe fails, it's probably because we're not connected to the internet
399                // or the DNS server is not responding
400                println!("Probe failed: {}", e);
401                // This is not a failure of the test, just a limitation of the testing environment
402            }
403        }
404    }
405}