diff --git a/iroh-net/src/dns.rs b/iroh-net/src/dns.rs index ce31f5d443..068166bb54 100644 --- a/iroh-net/src/dns.rs +++ b/iroh-net/src/dns.rs @@ -5,7 +5,8 @@ use std::net::{IpAddr, Ipv6Addr}; use std::time::Duration; use anyhow::Result; -use hickory_resolver::{AsyncResolver, IntoName, TokioAsyncResolver, TryParseIp}; +use futures_lite::Future; +use hickory_resolver::{AsyncResolver, IntoName, TokioAsyncResolver}; use once_cell::sync::Lazy; pub mod node_info; @@ -67,83 +68,97 @@ fn create_default_resolver() -> Result { } } - // lookup IPv4 and IPv6 in parallel + // see [`ResolverExt::lookup_ipv4_ipv6`] for info on why we avoid `LookupIpStrategy::Ipv4AndIpv6` options.ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4thenIpv6; let resolver = AsyncResolver::tokio(config, options); Ok(resolver) } -pub(crate) async fn lookup_ipv4( - resolver: &DnsResolver, - host: N, - timeout: Duration, -) -> Result> { - let addrs = tokio::time::timeout(timeout, resolver.ipv4_lookup(host)).await??; - Ok(addrs.into_iter().map(|ip| IpAddr::V4(ip.0)).collect()) +/// Extension trait to [`DnsResolver`]. +pub trait ResolverExt { + /// Perform an ipv4 lookup with a timeout. + fn lookup_ipv4( + &self, + host: N, + timeout: Duration, + ) -> impl Future>>; + + /// Perform an ipv6 lookup with a timeout. + fn lookup_ipv6( + &self, + host: N, + timeout: Duration, + ) -> impl Future>>; + + /// Race an ipv4 and ipv6 lookup with a timeout. + fn lookup_ipv4_ipv6( + &self, + host: N, + timeout: Duration, + ) -> impl Future>>; } -pub(crate) async fn lookup_ipv6( - resolver: &DnsResolver, - host: N, - timeout: Duration, -) -> Result> { - let addrs = tokio::time::timeout(timeout, resolver.ipv6_lookup(host)).await??; - Ok(addrs.into_iter().map(|ip| IpAddr::V6(ip.0)).collect()) -} +impl ResolverExt for DnsResolver { + async fn lookup_ipv4( + &self, + host: N, + timeout: Duration, + ) -> Result> { + let addrs = tokio::time::timeout(timeout, self.ipv4_lookup(host)).await??; + Ok(addrs.into_iter().map(|ip| IpAddr::V4(ip.0))) + } -/// Resolve IPv4 and IPv6 in parallel. -/// -/// `LookupIpStrategy::Ipv4AndIpv6` will wait for ipv6 resolution timeout, even if it is -/// not usable on the stack, so we manually query both lookups concurrently and time them out -/// individually. -pub async fn lookup_ipv4_ipv6( - resolver: &DnsResolver, - host: N, - timeout: Duration, -) -> Result> { - let ipv4 = resolver.ipv4_lookup(host.clone()); - let ipv6 = resolver.ipv6_lookup(host); - let ipv4 = tokio::time::timeout(timeout, ipv4); - let ipv6 = tokio::time::timeout(timeout, ipv6); - - let res = tokio::join!(ipv4, ipv6); - match res { - (Ok(Ok(ipv4)), Ok(Ok(ipv6))) => { - let res = ipv4 - .into_iter() - .map(|ip| IpAddr::V4(ip.0)) - .chain(ipv6.into_iter().map(|ip| IpAddr::V6(ip.0))) - .collect(); - Ok(res) - } - (Ok(Ok(ipv4)), Err(_timeout)) => { - let res = ipv4.into_iter().map(|ip| IpAddr::V4(ip.0)).collect(); - Ok(res) - } - (Ok(Ok(ipv4)), Ok(Err(_err))) => { - let res = ipv4.into_iter().map(|ip| IpAddr::V4(ip.0)).collect(); - Ok(res) - } - (Ok(Err(_err)), Ok(Ok(ipv6))) => { - let res = ipv6.into_iter().map(|ip| IpAddr::V6(ip.0)).collect(); - Ok(res) - } - (Ok(Err(err1)), Ok(Err(err2))) => { - anyhow::bail!("Ipv4: {:?}, Ipv6: {:?}", err1, err2); - } - (Ok(Err(err1)), Err(err2)) => { - anyhow::bail!("Ipv4: {:?}, Ipv6: {:?}", err1, err2); - } - (Err(_timeout), Ok(Ok(ipv6))) => { - let res = ipv6.into_iter().map(|ip| IpAddr::V6(ip.0)).collect(); - Ok(res) - } - (Err(err1), Ok(Err(err2))) => { - anyhow::bail!("Ipv4: {:?}, Ipv6: {:?}", err1, err2); + async fn lookup_ipv6( + &self, + host: N, + timeout: Duration, + ) -> Result> { + let addrs = tokio::time::timeout(timeout, self.ipv6_lookup(host)).await??; + Ok(addrs.into_iter().map(|ip| IpAddr::V6(ip.0))) + } + + /// Resolve IPv4 and IPv6 in parallel. + /// + /// `LookupIpStrategy::Ipv4AndIpv6` will wait for ipv6 resolution timeout, even if it is + /// not usable on the stack, so we manually query both lookups concurrently and time them out + /// individually. + async fn lookup_ipv4_ipv6( + &self, + host: N, + timeout: Duration, + ) -> Result> { + let res = tokio::join!( + self.lookup_ipv4(host.clone(), timeout), + self.lookup_ipv6(host, timeout) + ); + + match res { + (Ok(ipv4), Ok(ipv6)) => Ok(LookupIter::Both(ipv4.chain(ipv6))), + (Ok(ipv4), Err(_)) => Ok(LookupIter::Ipv4(ipv4)), + (Err(_), Ok(ipv6)) => Ok(LookupIter::Ipv6(ipv6)), + (Err(ipv4_err), Err(ipv6_err)) => { + anyhow::bail!("Ipv4: {:?}, Ipv6: {:?}", ipv4_err, ipv6_err) + } } - (Err(timeout1), Err(timeout2)) => { - anyhow::bail!("Ipv4: {:?}, Ipv6: {:?}", timeout1, timeout2); + } +} + +/// Helper enum to give a unified type to the iterators of [`ResolverExt::lookup_ipv4_ipv6`] +enum LookupIter { + Ipv4(A), + Ipv6(B), + Both(std::iter::Chain), +} + +impl, B: Iterator> Iterator for LookupIter { + type Item = IpAddr; + + fn next(&mut self) -> Option { + match self { + LookupIter::Ipv4(iter) => iter.next(), + LookupIter::Ipv6(iter) => iter.next(), + LookupIter::Both(iter) => iter.next(), } } } @@ -170,9 +185,11 @@ pub(crate) mod tests { async fn test_dns_lookup_ipv4_ipv6() { let _logging = iroh_test::logging::setup(); let resolver = default_resolver(); - let res = lookup_ipv4_ipv6(resolver, NA_RELAY_HOSTNAME, Duration::from_secs(5)) + let res: Vec<_> = resolver + .lookup_ipv4_ipv6(NA_RELAY_HOSTNAME, Duration::from_secs(5)) .await - .unwrap(); + .unwrap() + .collect(); assert!(!res.is_empty()); dbg!(res); } diff --git a/iroh-net/src/netcheck/reportgen.rs b/iroh-net/src/netcheck/reportgen.rs index d6ffdf83d0..a8b8b74de4 100644 --- a/iroh-net/src/netcheck/reportgen.rs +++ b/iroh-net/src/netcheck/reportgen.rs @@ -32,7 +32,7 @@ use tracing::{debug, debug_span, error, info_span, trace, warn, Instrument, Span use super::NetcheckMetrics; use crate::defaults::DEFAULT_RELAY_STUN_PORT; -use crate::dns::{lookup_ipv4, lookup_ipv6, DnsResolver}; +use crate::dns::{DnsResolver, ResolverExt}; use crate::net::interfaces; use crate::net::ip; use crate::net::UdpSocket; @@ -945,10 +945,10 @@ async fn get_relay_addr( ProbeProto::StunIpv4 | ProbeProto::IcmpV4 => match relay_node.url.host() { Some(url::Host::Domain(hostname)) => { debug!(?proto, %hostname, "Performing DNS A lookup for relay addr"); - match lookup_ipv4(dns_resolver, hostname, DNS_TIMEOUT).await { - Ok(addrs) => addrs - .first() - .map(|addr| ip::to_canonical(*addr)) + match dns_resolver.lookup_ipv4(hostname, DNS_TIMEOUT).await { + Ok(mut addrs) => addrs + .next() + .map(|addr| ip::to_canonical(addr)) .map(|addr| SocketAddr::new(addr, port)) .ok_or(anyhow!("No suitable relay addr found")), Err(err) => Err(err.context("No suitable relay addr found")), @@ -962,10 +962,10 @@ async fn get_relay_addr( ProbeProto::StunIpv6 | ProbeProto::IcmpV6 => match relay_node.url.host() { Some(url::Host::Domain(hostname)) => { debug!(?proto, %hostname, "Performing DNS AAAA lookup for relay addr"); - match lookup_ipv6(dns_resolver, hostname, DNS_TIMEOUT).await { - Ok(addrs) => addrs - .first() - .map(|addr| ip::to_canonical(*addr)) + match dns_resolver.lookup_ipv6(hostname, DNS_TIMEOUT).await { + Ok(mut addrs) => addrs + .next() + .map(|addr| ip::to_canonical(addr)) .map(|addr| SocketAddr::new(addr, port)) .ok_or(anyhow!("No suitable relay addr found")), Err(err) => Err(err.context("No suitable relay addr found")), diff --git a/iroh-net/src/relay/http/client.rs b/iroh-net/src/relay/http/client.rs index f522651d11..e88f61c6f4 100644 --- a/iroh-net/src/relay/http/client.rs +++ b/iroh-net/src/relay/http/client.rs @@ -22,7 +22,7 @@ use tokio::time::Instant; use tracing::{debug, error, info_span, trace, warn, Instrument}; use url::Url; -use crate::dns::{lookup_ipv4_ipv6, DnsResolver}; +use crate::dns::{DnsResolver, ResolverExt}; use crate::key::{PublicKey, SecretKey}; use crate::relay::RelayUrl; use crate::relay::{ @@ -862,13 +862,14 @@ async fn resolve_host( match host { url::Host::Domain(domain) => { // Need to do a DNS lookup - let addrs = lookup_ipv4_ipv6(resolver, domain, DNS_TIMEOUT) + let mut addrs = resolver + .lookup_ipv4_ipv6(domain, DNS_TIMEOUT) .await .map_err(|e| ClientError::Dns(Some(e)))?; if prefer_ipv6 { - if let Some(addr) = addrs.iter().find(|addr| addr.is_ipv6()) { - return Ok(*addr); + if let Some(addr) = addrs.find(|addr| addr.is_ipv6()) { + return Ok(addr); } } addrs