use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
use hickory_resolver::config::{NameServerConfig, ResolverConfig, ResolverOpts};
use hickory_resolver::name_server::TokioConnectionProvider;
use hickory_resolver::proto::xfer::Protocol;
use hickory_resolver::{Resolver, TokioResolver};
use tokio::time::Instant;
use tracing::{debug, trace, warn};
use super::provider::{challenge_record_fqdn, DnsProviderError};
#[derive(Debug, Clone)]
pub struct PropagationConfig {
pub initial_delay: Duration,
pub check_interval: Duration,
pub timeout: Duration,
pub nameservers: Vec<IpAddr>,
}
impl Default for PropagationConfig {
fn default() -> Self {
Self {
initial_delay: Duration::from_secs(10),
check_interval: Duration::from_secs(5),
timeout: Duration::from_secs(120),
nameservers: vec![
IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), IpAddr::V4(Ipv4Addr::new(9, 9, 9, 9)), ],
}
}
}
#[derive(Debug)]
pub struct PropagationChecker {
config: PropagationConfig,
resolver: TokioResolver,
}
impl PropagationChecker {
pub fn new() -> Result<Self, DnsProviderError> {
Self::with_config(PropagationConfig::default())
}
pub fn with_config(config: PropagationConfig) -> Result<Self, DnsProviderError> {
let resolver = Self::create_resolver(&config)?;
Ok(Self { config, resolver })
}
fn create_resolver(config: &PropagationConfig) -> Result<TokioResolver, DnsProviderError> {
let resolver_config = if config.nameservers.is_empty() {
ResolverConfig::default()
} else {
let mut resolver_config = ResolverConfig::new();
for ip in &config.nameservers {
resolver_config.add_name_server(NameServerConfig::new(
SocketAddr::new(*ip, 53),
Protocol::Udp,
));
}
resolver_config
};
let mut opts = ResolverOpts::default();
opts.timeout = Duration::from_secs(5);
opts.attempts = 3;
opts.cache_size = 0;
let resolver =
Resolver::builder_with_config(resolver_config, TokioConnectionProvider::default())
.with_options(opts)
.build();
Ok(resolver)
}
pub async fn wait_for_propagation(
&self,
domain: &str,
expected_value: &str,
) -> Result<(), DnsProviderError> {
let record_name = challenge_record_fqdn(domain);
let start = Instant::now();
let deadline = start + self.config.timeout;
debug!(
record = %record_name,
timeout_secs = self.config.timeout.as_secs(),
"Waiting for DNS propagation"
);
tokio::time::sleep(self.config.initial_delay).await;
loop {
match self.check_record(&record_name, expected_value).await {
Ok(true) => {
let elapsed = start.elapsed();
debug!(
record = %record_name,
elapsed_secs = elapsed.as_secs(),
"DNS propagation confirmed"
);
return Ok(());
}
Ok(false) => {
trace!(record = %record_name, "Record not yet propagated");
}
Err(e) => {
warn!(record = %record_name, error = %e, "DNS lookup error");
}
}
if Instant::now() > deadline {
return Err(DnsProviderError::Timeout {
elapsed_secs: self.config.timeout.as_secs(),
});
}
tokio::time::sleep(self.config.check_interval).await;
}
}
async fn check_record(
&self,
record_name: &str,
expected_value: &str,
) -> Result<bool, DnsProviderError> {
let lookup = self.resolver.txt_lookup(record_name).await;
match lookup {
Ok(records) => {
for record in records.iter() {
let value: String = record
.txt_data()
.iter()
.map(|data| String::from_utf8_lossy(data))
.collect();
trace!(
record = %record_name,
found_value = %value,
expected_value = %expected_value,
"Checking TXT record"
);
if value == expected_value {
return Ok(true);
}
}
Ok(false)
}
Err(e) => {
let err_str = e.to_string().to_lowercase();
if err_str.contains("no records found")
|| err_str.contains("nxdomain")
|| err_str.contains("no connections available")
|| err_str.contains("record not found")
{
Ok(false)
} else {
Err(DnsProviderError::ApiRequest(format!(
"DNS lookup failed for '{}': {}",
record_name, e
)))
}
}
}
}
pub async fn verify_record_exists(
&self,
domain: &str,
expected_value: &str,
) -> Result<bool, DnsProviderError> {
let record_name = challenge_record_fqdn(domain);
self.check_record(&record_name, expected_value).await
}
pub fn config(&self) -> &PropagationConfig {
&self.config
}
}
impl Default for PropagationChecker {
fn default() -> Self {
Self::new().expect("Failed to create default PropagationChecker")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = PropagationConfig::default();
assert_eq!(config.initial_delay, Duration::from_secs(10));
assert_eq!(config.check_interval, Duration::from_secs(5));
assert_eq!(config.timeout, Duration::from_secs(120));
assert!(!config.nameservers.is_empty());
}
#[tokio::test]
async fn test_propagation_checker_creation() {
let checker = PropagationChecker::new();
assert!(checker.is_ok());
}
#[tokio::test]
async fn test_custom_config() {
let config = PropagationConfig {
initial_delay: Duration::from_secs(5),
check_interval: Duration::from_secs(2),
timeout: Duration::from_secs(60),
nameservers: vec![IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))],
};
let checker = PropagationChecker::with_config(config.clone());
assert!(checker.is_ok());
let checker = checker.unwrap();
assert_eq!(checker.config().initial_delay, Duration::from_secs(5));
}
}