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}