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}