Skip to main content

async_rs/implementors/
hickory.rs

1use crate::{
2    traits::AsyncToSocketAddrs,
3    util::{self, SocketAddrsFromIpAddrs},
4};
5use hickory_resolver::{TokioResolver, proto::rr::IntoName};
6use std::{
7    io,
8    net::{IpAddr, SocketAddr, ToSocketAddrs},
9    str::FromStr,
10    sync::OnceLock,
11    vec,
12};
13
14static RESOLVER: OnceLock<TokioResolver> = OnceLock::new();
15
16/// Build a resolver, which binds to the tokio runtime it is used from as it opens connections.
17fn new_resolver() -> io::Result<TokioResolver> {
18    TokioResolver::builder_tokio()
19        .map_err(io::Error::other)?
20        .build()
21        .map_err(io::Error::other)
22}
23
24/// The shared resolver, for lookups running on a runtime the caller brought and keeps.
25///
26/// Only ever initialised from such a lookup: a resolver caches its name-server connections, so one
27/// built on a runtime which is about to go away would poison this for every later caller.
28fn get_or_init_resolver() -> io::Result<&'static TokioResolver> {
29    // FIXME: replace with RESOLVER.get_or_try_init(...) once it stabilises (rust#109737)
30    if let Some(r) = RESOLVER.get() {
31        return Ok(r);
32    }
33    let resolver = new_resolver()?;
34    Ok(RESOLVER.get_or_init(|| resolver))
35}
36
37/// Perform async DNS resolution using hickory-dns
38#[derive(Debug, Clone)]
39pub struct HickoryToSocketAddrs<T: IntoName + Send + 'static> {
40    host: T,
41    port: u16,
42}
43
44impl<H: IntoName + Send + 'static> HickoryToSocketAddrs<H> {
45    /// Create a `HickoryToSocketAddrs` from split host and port components.
46    ///
47    /// The host is passed to the resolver as given. An IP literal resolves without a query, but
48    /// only in the form `IpAddr` itself parses, so hand over `::1` rather than `[::1]` — the
49    /// bracketed form is a socket-address spelling and would go out as a hostname. Parsing a
50    /// whole `host:port` string with [`FromStr`] unwraps the brackets for you.
51    pub fn new(host: H, port: u16) -> Self {
52        Self { host, port }
53    }
54
55    async fn lookup(self) -> io::Result<SocketAddrsFromIpAddrs<vec::IntoIter<IpAddr>>> {
56        if !util::inside_tokio() {
57            return Err(io::Error::other(
58                "hickory-dns is only supported in a tokio context",
59            ));
60        }
61
62        self.lookup_with(get_or_init_resolver()?).await
63    }
64
65    async fn lookup_with(
66        self,
67        resolver: &TokioResolver,
68    ) -> io::Result<SocketAddrsFromIpAddrs<vec::IntoIter<IpAddr>>> {
69        Ok(SocketAddrsFromIpAddrs(
70            resolver
71                .lookup_ip(self.host)
72                .await
73                .map_err(io::Error::other)?
74                .iter()
75                .collect::<Vec<_>>() // FIXME: don't collect if we get back into_iter
76                .into_iter(),
77            self.port,
78        ))
79    }
80}
81
82impl FromStr for HickoryToSocketAddrs<String> {
83    type Err = io::Error;
84
85    fn from_str(s: &str) -> io::Result<Self> {
86        fn invalid(msg: &'static str) -> io::Error {
87            io::Error::new(io::ErrorKind::InvalidInput, msg)
88        }
89
90        // hickory shortcuts a host which parses as an IP address and answers without a query, but
91        // only if we hand it the bare address, so let std unwrap the brackets an IPv6 literal
92        // comes in. Brackets are reserved for that spelling, so anything else in them is malformed
93        // rather than a hostname we should quietly go and resolve.
94        if let Ok(addr) = s.parse::<SocketAddr>() {
95            // A numeric zone id survives `SocketAddr` but not the trip back out through `IpAddr`,
96            // and there is nowhere to put one in a host and a port anyway. Dropping it silently
97            // would connect to a link-local address over whichever interface the kernel picks.
98            if matches!(addr, SocketAddr::V6(addr) if addr.scope_id() != 0) {
99                return Err(invalid("IPv6 scope ids are not supported"));
100            }
101            return Ok(Self::new(addr.ip().to_string(), addr.port()));
102        }
103        if s.starts_with('[') {
104            return Err(invalid("bracketed host is not an IP address"));
105        }
106        let (host, port_str) = s
107            .rsplit_once(':')
108            .ok_or_else(|| invalid("invalid socket address"))?;
109        // The empty name is the DNS root, which resolves to no address at all, so a lookup for it
110        // is a round trip whose only possible outcome is a misleading "couldn't resolve host".
111        if host.is_empty() {
112            return Err(invalid("empty host"));
113        }
114        // An unbracketed IPv6 literal keeps its own colons, so the split above would hand us its
115        // last hextet as the port. Bracket it if it is meant to carry one.
116        if host.contains(':') {
117            return Err(invalid("IPv6 literals must be bracketed"));
118        }
119        let port = port_str
120            .parse()
121            .map_err(|_| invalid("invalid port value"))?;
122        Ok(Self::new(host.to_owned(), port))
123    }
124}
125
126impl<T: IntoName + Clone + Send + 'static> ToSocketAddrs for HickoryToSocketAddrs<T> {
127    type Iter = SocketAddrsFromIpAddrs<vec::IntoIter<IpAddr>>;
128
129    fn to_socket_addrs(&self) -> io::Result<Self::Iter> {
130        if util::inside_tokio() {
131            return util::block_on_tokio(self.clone().lookup());
132        }
133        // Off a tokio thread, `block_on_tokio` builds a runtime which dies with this call, so the
134        // resolver has to die with it: caching one whose name-server connections are registered on
135        // a driver that is gone leaves every later lookup, from anywhere, talking to a corpse.
136        let this = self.clone();
137        util::block_on_tokio(async move { this.lookup_with(&new_resolver()?).await })
138    }
139}
140
141impl<T: IntoName + Send + 'static> AsyncToSocketAddrs for HickoryToSocketAddrs<T> {
142    fn to_socket_addrs(
143        self,
144    ) -> impl Future<Output = io::Result<impl Iterator<Item = SocketAddr> + Send + 'static>>
145    + Send
146    + 'static {
147        self.lookup()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    fn parse(s: &str) -> (String, u16) {
156        let addrs: HickoryToSocketAddrs<String> = s.parse().expect("parse");
157        (addrs.host, addrs.port)
158    }
159
160    #[test]
161    fn from_str_splits_host_and_port() {
162        assert_eq!(parse("example.com:80"), ("example.com".to_owned(), 80));
163    }
164
165    #[test]
166    fn from_str_keeps_ip_literals_parseable_as_ip() {
167        // hickory only shortcuts these if IpAddr::from_str accepts what we hand it, which it does
168        // not do for the bracketed form.
169        for (input, host) in [("127.0.0.1:80", "127.0.0.1"), ("[::1]:80", "::1")] {
170            let (parsed, port) = parse(input);
171            assert_eq!((parsed.as_str(), port), (host, 80));
172            assert!(parsed.parse::<IpAddr>().is_ok(), "{input}");
173        }
174    }
175
176    #[test]
177    fn from_str_rejects_garbage() {
178        for input in [
179            "example.com",
180            "example.com:http",
181            // An unbracketed IPv6 literal: splitting on the last colon would take `1` for a port
182            // and leave `2001:db8:` as the host, which resolves to nothing.
183            "2001:db8::1",
184            "::1",
185            // Brackets are the IP-literal spelling, so a hostname in them is malformed. Unwrapping
186            // it would resolve a host the caller never asked for.
187            "[example.com]:80",
188            // A zone id does not survive `IpAddr::from_str`, so this would go out as a query for a
189            // bogus name rather than shortcut. Both spellings, because std draws the line between
190            // them and we do not: it rejects the named form outright, but parses the numeric one
191            // and then loses the zone on the way back out to a string.
192            "[fe80::1%eth0]:80",
193            "[fe80::1%1]:80",
194            // The empty host is the DNS root, not a hostname anybody meant to look up.
195            ":80",
196            // Half-bracketed input used to leak through as host `[::1`.
197            "[::1:80",
198            "[::1]",
199            "[::1]80",
200        ] {
201            assert!(
202                input.parse::<HickoryToSocketAddrs<String>>().is_err(),
203                "{input}"
204            );
205        }
206    }
207}