sentinel_proxy/acme/dns/
propagation.rs1use std::net::{IpAddr, Ipv4Addr, SocketAddr};
7use std::time::Duration;
8
9use hickory_resolver::config::{NameServerConfig, ResolverConfig, ResolverOpts};
10use hickory_resolver::name_server::TokioConnectionProvider;
11use hickory_resolver::proto::xfer::Protocol;
12use hickory_resolver::{Resolver, TokioResolver};
13use tokio::time::Instant;
14use tracing::{debug, trace, warn};
15
16use super::provider::{challenge_record_fqdn, DnsProviderError};
17
18#[derive(Debug, Clone)]
20pub struct PropagationConfig {
21 pub initial_delay: Duration,
23 pub check_interval: Duration,
25 pub timeout: Duration,
27 pub nameservers: Vec<IpAddr>,
29}
30
31impl Default for PropagationConfig {
32 fn default() -> Self {
33 Self {
34 initial_delay: Duration::from_secs(10),
35 check_interval: Duration::from_secs(5),
36 timeout: Duration::from_secs(120),
37 nameservers: vec![
38 IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), IpAddr::V4(Ipv4Addr::new(9, 9, 9, 9)), ],
42 }
43 }
44}
45
46#[derive(Debug)]
50pub struct PropagationChecker {
51 config: PropagationConfig,
52 resolver: TokioResolver,
53}
54
55impl PropagationChecker {
56 pub fn new() -> Result<Self, DnsProviderError> {
58 Self::with_config(PropagationConfig::default())
59 }
60
61 pub fn with_config(config: PropagationConfig) -> Result<Self, DnsProviderError> {
63 let resolver = Self::create_resolver(&config)?;
64
65 Ok(Self { config, resolver })
66 }
67
68 fn create_resolver(config: &PropagationConfig) -> Result<TokioResolver, DnsProviderError> {
70 let resolver_config = if config.nameservers.is_empty() {
71 ResolverConfig::default()
72 } else {
73 let mut resolver_config = ResolverConfig::new();
74 for ip in &config.nameservers {
75 resolver_config.add_name_server(NameServerConfig::new(
76 SocketAddr::new(*ip, 53),
77 Protocol::Udp,
78 ));
79 }
80 resolver_config
81 };
82
83 let mut opts = ResolverOpts::default();
84 opts.timeout = Duration::from_secs(5);
85 opts.attempts = 3;
86 opts.cache_size = 0; let resolver = Resolver::builder_with_config(resolver_config, TokioConnectionProvider::default())
90 .with_options(opts)
91 .build();
92 Ok(resolver)
93 }
94
95 pub async fn wait_for_propagation(
107 &self,
108 domain: &str,
109 expected_value: &str,
110 ) -> Result<(), DnsProviderError> {
111 let record_name = challenge_record_fqdn(domain);
112 let start = Instant::now();
113 let deadline = start + self.config.timeout;
114
115 debug!(
116 record = %record_name,
117 timeout_secs = self.config.timeout.as_secs(),
118 "Waiting for DNS propagation"
119 );
120
121 tokio::time::sleep(self.config.initial_delay).await;
123
124 loop {
125 match self.check_record(&record_name, expected_value).await {
126 Ok(true) => {
127 let elapsed = start.elapsed();
128 debug!(
129 record = %record_name,
130 elapsed_secs = elapsed.as_secs(),
131 "DNS propagation confirmed"
132 );
133 return Ok(());
134 }
135 Ok(false) => {
136 trace!(record = %record_name, "Record not yet propagated");
137 }
138 Err(e) => {
139 warn!(record = %record_name, error = %e, "DNS lookup error");
140 }
141 }
142
143 if Instant::now() > deadline {
144 return Err(DnsProviderError::Timeout {
145 elapsed_secs: self.config.timeout.as_secs(),
146 });
147 }
148
149 tokio::time::sleep(self.config.check_interval).await;
150 }
151 }
152
153 async fn check_record(&self, record_name: &str, expected_value: &str) -> Result<bool, DnsProviderError> {
155 let lookup = self.resolver.txt_lookup(record_name).await;
156
157 match lookup {
158 Ok(records) => {
159 for record in records.iter() {
160 let value: String = record.txt_data().iter()
162 .map(|data| String::from_utf8_lossy(data))
163 .collect();
164
165 trace!(
166 record = %record_name,
167 found_value = %value,
168 expected_value = %expected_value,
169 "Checking TXT record"
170 );
171
172 if value == expected_value {
173 return Ok(true);
174 }
175 }
176 Ok(false)
177 }
178 Err(e) => {
179 let err_str = e.to_string().to_lowercase();
182 if err_str.contains("no records found")
183 || err_str.contains("nxdomain")
184 || err_str.contains("no connections available")
185 || err_str.contains("record not found")
186 {
187 Ok(false)
188 } else {
189 Err(DnsProviderError::ApiRequest(format!(
190 "DNS lookup failed for '{}': {}",
191 record_name, e
192 )))
193 }
194 }
195 }
196 }
197
198 pub async fn verify_record_exists(
202 &self,
203 domain: &str,
204 expected_value: &str,
205 ) -> Result<bool, DnsProviderError> {
206 let record_name = challenge_record_fqdn(domain);
207 self.check_record(&record_name, expected_value).await
208 }
209
210 pub fn config(&self) -> &PropagationConfig {
212 &self.config
213 }
214}
215
216impl Default for PropagationChecker {
217 fn default() -> Self {
218 Self::new().expect("Failed to create default PropagationChecker")
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_default_config() {
228 let config = PropagationConfig::default();
229 assert_eq!(config.initial_delay, Duration::from_secs(10));
230 assert_eq!(config.check_interval, Duration::from_secs(5));
231 assert_eq!(config.timeout, Duration::from_secs(120));
232 assert!(!config.nameservers.is_empty());
233 }
234
235 #[tokio::test]
236 async fn test_propagation_checker_creation() {
237 let checker = PropagationChecker::new();
238 assert!(checker.is_ok());
239 }
240
241 #[tokio::test]
242 async fn test_custom_config() {
243 let config = PropagationConfig {
244 initial_delay: Duration::from_secs(5),
245 check_interval: Duration::from_secs(2),
246 timeout: Duration::from_secs(60),
247 nameservers: vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))],
248 };
249
250 let checker = PropagationChecker::with_config(config.clone());
251 assert!(checker.is_ok());
252
253 let checker = checker.unwrap();
254 assert_eq!(checker.config().initial_delay, Duration::from_secs(5));
255 }
256}