Skip to main content

torrust_tracker_deployer_lib/infrastructure/dns/
resolver.rs

1//! DNS resolver implementation
2//!
3//! Provides system DNS resolution to check if configured domains resolve to
4//! the expected instance IP addresses.
5
6use std::net::{IpAddr, ToSocketAddrs};
7
8use thiserror::Error;
9use tracing::{debug, instrument};
10
11use crate::shared::domain_name::DomainName;
12
13/// Errors that can occur during DNS resolution
14#[derive(Error, Debug)]
15pub enum DnsResolutionError {
16    /// DNS resolution failed (domain doesn't resolve or network error)
17    #[error("DNS resolution failed for domain '{domain}': {source}")]
18    ResolutionFailed {
19        domain: String,
20        #[source]
21        source: std::io::Error,
22    },
23
24    /// Domain resolved but to a different IP than expected
25    #[error("Domain '{domain}' resolves to {resolved_ip} but expected {expected_ip}")]
26    IpMismatch {
27        domain: String,
28        resolved_ip: IpAddr,
29        expected_ip: IpAddr,
30    },
31}
32
33/// DNS resolver for validating domain name resolution
34///
35/// Uses the system's DNS resolver (`std::net::ToSocketAddrs`) to check if
36/// domains resolve to expected IP addresses. This checks actual DNS configuration
37/// including system DNS servers, `/etc/hosts`, and mDNS for `.local` domains.
38///
39/// # Examples
40///
41/// ```no_run
42/// use std::net::IpAddr;
43/// use torrust_tracker_deployer_lib::infrastructure::dns::DnsResolver;
44/// use torrust_tracker_deployer_lib::shared::domain_name::DomainName;
45///
46/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
47/// let resolver = DnsResolver::new();
48/// let domain = DomainName::new("tracker.local")?;
49/// let expected_ip: IpAddr = "10.140.190.254".parse()?;
50///
51/// match resolver.resolve_and_verify(&domain, expected_ip) {
52///     Ok(()) => println!("✓ Domain resolves correctly"),
53///     Err(e) => eprintln!("⚠ DNS check failed: {}", e),
54/// }
55/// # Ok(())
56/// # }
57/// ```
58#[derive(Debug, Clone, Copy)]
59pub struct DnsResolver;
60
61impl DnsResolver {
62    /// Create a new DNS resolver
63    #[must_use]
64    pub const fn new() -> Self {
65        Self
66    }
67
68    /// Resolve a domain name to IP addresses using system DNS
69    ///
70    /// # Arguments
71    /// * `domain` - Domain name to resolve
72    ///
73    /// # Returns
74    /// Vector of resolved IP addresses (may be empty if resolution fails)
75    ///
76    /// # Errors
77    /// Returns `DnsResolutionError::ResolutionFailed` if DNS resolution fails
78    #[instrument(skip(self), fields(domain = %domain.as_str()))]
79    pub fn resolve(&self, domain: &DomainName) -> Result<Vec<IpAddr>, DnsResolutionError> {
80        debug!("Resolving domain via system DNS");
81
82        // Use port 80 as a dummy port for ToSocketAddrs
83        // The port doesn't matter for DNS resolution, but ToSocketAddrs requires it
84        let address_with_port = format!("{}:80", domain.as_str());
85
86        let addresses: Vec<IpAddr> = address_with_port
87            .to_socket_addrs()
88            .map_err(|e| DnsResolutionError::ResolutionFailed {
89                domain: domain.as_str().to_string(),
90                source: e,
91            })?
92            .map(|addr| addr.ip())
93            .collect();
94
95        debug!(resolved_ips = ?addresses, "Domain resolved successfully");
96
97        Ok(addresses)
98    }
99
100    /// Resolve a domain and verify it matches the expected IP address
101    ///
102    /// This method resolves the domain and checks if any of the resolved IPs
103    /// match the expected IP. It's common for domains to resolve to multiple
104    /// IPs (IPv4 and IPv6, or multiple servers).
105    ///
106    /// # Arguments
107    /// * `domain` - Domain name to resolve and verify
108    /// * `expected_ip` - Expected IP address
109    ///
110    /// # Returns
111    /// `Ok(())` if the domain resolves and matches the expected IP
112    ///
113    /// # Errors
114    /// - `DnsResolutionError::ResolutionFailed` if DNS resolution fails
115    /// - `DnsResolutionError::IpMismatch` if domain resolves but not to expected IP
116    #[instrument(skip(self), fields(domain = %domain.as_str(), expected_ip = %expected_ip))]
117    pub fn resolve_and_verify(
118        &self,
119        domain: &DomainName,
120        expected_ip: IpAddr,
121    ) -> Result<(), DnsResolutionError> {
122        let resolved_ips = self.resolve(domain)?;
123
124        if resolved_ips.is_empty() {
125            return Err(DnsResolutionError::ResolutionFailed {
126                domain: domain.as_str().to_string(),
127                source: std::io::Error::new(
128                    std::io::ErrorKind::NotFound,
129                    "DNS resolution returned no addresses",
130                ),
131            });
132        }
133
134        // Check if any of the resolved IPs match the expected IP
135        if resolved_ips.contains(&expected_ip) {
136            debug!("Domain resolves to expected IP");
137            Ok(())
138        } else {
139            // Return the first resolved IP in the error for clarity
140            Err(DnsResolutionError::IpMismatch {
141                domain: domain.as_str().to_string(),
142                resolved_ip: resolved_ips[0],
143                expected_ip,
144            })
145        }
146    }
147}
148
149impl Default for DnsResolver {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn it_should_create_new_resolver() {
161        let resolver = DnsResolver::new();
162        assert!(matches!(resolver, DnsResolver));
163    }
164
165    // Note: We can't test actual DNS resolution in unit tests as it depends on
166    // the system's DNS configuration and network connectivity. These tests would
167    // be better suited for integration tests with a controlled DNS environment.
168    //
169    // For comprehensive testing, see E2E tests which validate DNS checks against
170    // running infrastructure.
171}