acme_validation_propagation/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3use hickory_resolver::{
4    config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ResolverOpts},
5    Resolver,
6};
7use std::{convert::identity, net::IpAddr, thread::sleep, time::Duration};
8
9use crate::error::Error;
10use resolver::ResolverType;
11
12mod error;
13mod resolver;
14
15pub type Result<T> = std::result::Result<T, Error>;
16
17const MAX_RETRIES: usize = 720;
18const WAIT_SECONDS: u64 = 5;
19
20fn ipv6_resolver(
21    group: NameServerConfigGroup,
22    recursion: bool,
23    ipv6_only: bool,
24) -> Result<Resolver> {
25    let config = ResolverConfig::from_parts(None, vec![], group);
26    let mut options = ResolverOpts::default();
27    if ipv6_only {
28        options.ip_strategy = LookupIpStrategy::Ipv6Only;
29    }
30    options.recursion_desired = recursion;
31    options.use_hosts_file = false;
32    Resolver::new(config, options).map_err(Error::from)
33}
34
35fn recursive_resolver(ips: &[IpAddr], ipv6_only: bool) -> Result<Resolver> {
36    let group = NameServerConfigGroup::from_ips_clear(ips, 53, false);
37    ipv6_resolver(group, true, ipv6_only)
38}
39
40/// wait checks the authoritive nameservers periodically.
41/// It returns Ok(()) when all nameservers have the challenge.
42/// It returns an error after several attempts failed.
43pub fn wait<S>(domain_name: S, challenge: S) -> Result<()>
44where
45    S: AsRef<str>,
46{
47    let resolvers = ResolverType::Google
48        .recursive_resolver(false)
49        .and_then(|resolver| resolver.authoritive_resolvers(domain_name.as_ref()))?;
50
51    let mut i: usize = 0;
52
53    sleep(Duration::from_secs(1));
54    while !resolvers
55        .iter()
56        .map(|resolver| resolver.has_single_acme(domain_name.as_ref(), challenge.as_ref()))
57        .collect::<Result<Vec<_>>>()?
58        .into_iter()
59        .all(identity)
60        && i < MAX_RETRIES
61    {
62        i += 1;
63        tracing::warn!("Attempt {} failed", i);
64        sleep(Duration::from_secs(WAIT_SECONDS));
65    }
66    if i >= MAX_RETRIES {
67        tracing::error!("Timeout checking acme challenge record");
68        Err(Error::AcmeChallege)
69    } else {
70        Ok(())
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use std::{fmt::Display, net::IpAddr};
77
78    use hickory_resolver::{
79        lookup::{Ipv6Lookup, NsLookup},
80        proto::rr::rdata::{AAAA, NS},
81        Resolver,
82    };
83
84    use crate::{error::Error, ResolverType};
85
86    fn to_string<D: Display>(d: D) -> String {
87        d.to_string()
88    }
89
90    fn aaaa_to_ipv6(aaaa: AAAA) -> IpAddr {
91        IpAddr::V6(*aaaa)
92    }
93
94    fn lookup(name: &str) -> impl Fn(Resolver) -> Result<Ipv6Lookup, Error> + '_ {
95        move |resolver| resolver.ipv6_lookup(name).map_err(Error::from)
96    }
97
98    fn ns_lookup(name: &str) -> impl Fn(Resolver) -> Result<NsLookup, Error> + '_ {
99        move |resolver| resolver.ns_lookup(name).map_err(Error::from)
100    }
101
102    fn aaaa_mapper(f: fn(AAAA) -> IpAddr) -> impl Fn(Ipv6Lookup) -> Vec<IpAddr> {
103        move |lookup| lookup.into_iter().map(f).collect()
104    }
105
106    fn ns_mapper(f: fn(NS) -> String) -> impl Fn(NsLookup) -> Vec<String> {
107        move |lookup| lookup.into_iter().map(f).collect()
108    }
109
110    fn ipv6_address_lookup(name: &str) -> Result<Vec<IpAddr>, Error> {
111        ResolverType::Google
112            .resolver(true)
113            .and_then(lookup(name))
114            .map(aaaa_mapper(aaaa_to_ipv6))
115    }
116
117    fn nameservers_lookup(name: &str) -> Result<Vec<String>, Error> {
118        ResolverType::Google
119            .resolver(true)
120            .and_then(ns_lookup(name))
121            .map(ns_mapper(to_string))
122    }
123
124    #[test]
125    fn test_www_paulmin_nl() {
126        let addresses = ipv6_address_lookup("www.paulmin.nl.").unwrap();
127        assert!(addresses.contains(&"2606:50c0:8000::153".parse::<IpAddr>().unwrap()),);
128        assert!(addresses.contains(&"2606:50c0:8001::153".parse::<IpAddr>().unwrap()),);
129        assert!(addresses.contains(&"2606:50c0:8002::153".parse::<IpAddr>().unwrap()),);
130        assert!(addresses.contains(&"2606:50c0:8003::153".parse::<IpAddr>().unwrap()),);
131    }
132
133    #[test]
134    fn test_ns0_transip_net() {
135        assert_eq!(
136            ipv6_address_lookup("ns0.transip.net").unwrap(),
137            vec!["2a01:7c8:dddd:195::195".parse::<IpAddr>().unwrap(),],
138        );
139    }
140
141    #[test]
142    fn test_ns1_transip_nl() {
143        assert_eq!(
144            ipv6_address_lookup("ns1.transip.nl.").unwrap(),
145            vec!["2a01:7c8:7000:195::195".parse::<IpAddr>().unwrap(),],
146        );
147    }
148
149    #[test]
150    fn test_ns2_transip_eu() {
151        assert_eq!(
152            ipv6_address_lookup("ns2.transip.eu.").unwrap(),
153            vec!["2a01:7c8:f:c1f::195".parse::<IpAddr>().unwrap(),],
154        );
155    }
156
157    #[test]
158    fn test_domain_ns() {
159        let mut domain = nameservers_lookup("paulmin.nl").unwrap();
160        domain.sort();
161        assert_eq!(
162            domain,
163            vec![
164                "ns0.transip.net.".to_owned(),
165                "ns1.transip.nl.".to_owned(),
166                "ns2.transip.eu.".to_owned(),
167            ],
168        );
169    }
170}