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 =
90 Resolver::builder_with_config(resolver_config, TokioConnectionProvider::default())
91 .with_options(opts)
92 .build();
93 Ok(resolver)
94 }
95
96 pub async fn wait_for_propagation(
108 &self,
109 domain: &str,
110 expected_value: &str,
111 ) -> Result<(), DnsProviderError> {
112 let record_name = challenge_record_fqdn(domain);
113 let start = Instant::now();
114 let deadline = start + self.config.timeout;
115
116 debug!(
117 record = %record_name,
118 timeout_secs = self.config.timeout.as_secs(),
119 "Waiting for DNS propagation"
120 );
121
122 tokio::time::sleep(self.config.initial_delay).await;
124
125 loop {
126 match self.check_record(&record_name, expected_value).await {
127 Ok(true) => {
128 let elapsed = start.elapsed();
129 debug!(
130 record = %record_name,
131 elapsed_secs = elapsed.as_secs(),
132 "DNS propagation confirmed"
133 );
134 return Ok(());
135 }
136 Ok(false) => {
137 trace!(record = %record_name, "Record not yet propagated");
138 }
139 Err(e) => {
140 warn!(record = %record_name, error = %e, "DNS lookup error");
141 }
142 }
143
144 if Instant::now() > deadline {
145 return Err(DnsProviderError::Timeout {
146 elapsed_secs: self.config.timeout.as_secs(),
147 });
148 }
149
150 tokio::time::sleep(self.config.check_interval).await;
151 }
152 }
153
154 async fn check_record(
156 &self,
157 record_name: &str,
158 expected_value: &str,
159 ) -> Result<bool, DnsProviderError> {
160 let lookup = self.resolver.txt_lookup(record_name).await;
161
162 match lookup {
163 Ok(records) => {
164 for record in records.iter() {
165 let value: String = record
167 .txt_data()
168 .iter()
169 .map(|data| String::from_utf8_lossy(data))
170 .collect();
171
172 trace!(
173 record = %record_name,
174 found_value = %value,
175 expected_value = %expected_value,
176 "Checking TXT record"
177 );
178
179 if value == expected_value {
180 return Ok(true);
181 }
182 }
183 Ok(false)
184 }
185 Err(e) => {
186 let err_str = e.to_string().to_lowercase();
189 if err_str.contains("no records found")
190 || err_str.contains("nxdomain")
191 || err_str.contains("no connections available")
192 || err_str.contains("record not found")
193 {
194 Ok(false)
195 } else {
196 Err(DnsProviderError::ApiRequest(format!(
197 "DNS lookup failed for '{}': {}",
198 record_name, e
199 )))
200 }
201 }
202 }
203 }
204
205 pub async fn verify_record_exists(
209 &self,
210 domain: &str,
211 expected_value: &str,
212 ) -> Result<bool, DnsProviderError> {
213 let record_name = challenge_record_fqdn(domain);
214 self.check_record(&record_name, expected_value).await
215 }
216
217 pub fn config(&self) -> &PropagationConfig {
219 &self.config
220 }
221}
222
223impl Default for PropagationChecker {
224 fn default() -> Self {
225 Self::new().expect("Failed to create default PropagationChecker")
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_default_config() {
235 let config = PropagationConfig::default();
236 assert_eq!(config.initial_delay, Duration::from_secs(10));
237 assert_eq!(config.check_interval, Duration::from_secs(5));
238 assert_eq!(config.timeout, Duration::from_secs(120));
239 assert!(!config.nameservers.is_empty());
240 }
241
242 #[tokio::test]
243 async fn test_propagation_checker_creation() {
244 let checker = PropagationChecker::new();
245 assert!(checker.is_ok());
246 }
247
248 #[tokio::test]
249 async fn test_custom_config() {
250 let config = PropagationConfig {
251 initial_delay: Duration::from_secs(5),
252 check_interval: Duration::from_secs(2),
253 timeout: Duration::from_secs(60),
254 nameservers: vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))],
255 };
256
257 let checker = PropagationChecker::with_config(config.clone());
258 assert!(checker.is_ok());
259
260 let checker = checker.unwrap();
261 assert_eq!(checker.config().initial_delay, Duration::from_secs(5));
262 }
263}