diff --git a/iroh-cli/src/commands/doctor.rs b/iroh-cli/src/commands/doctor.rs index 1d774296ed8..9746dfe5cd8 100644 --- a/iroh-cli/src/commands/doctor.rs +++ b/iroh-cli/src/commands/doctor.rs @@ -21,6 +21,7 @@ use iroh::{ bytes::store::ReadableStore, net::{ defaults::DEFAULT_RELAY_STUN_PORT, + dns::default_resolver, key::{PublicKey, SecretKey}, magic_endpoint, magicsock::EndpointInfo, @@ -275,7 +276,8 @@ async fn report( config: &NodeConfig, ) -> anyhow::Result<()> { let port_mapper = portmapper::Client::default(); - let mut client = netcheck::Client::new(Some(port_mapper))?; + let dns_resolver = default_resolver().clone(); + let mut client = netcheck::Client::new(Some(port_mapper), dns_resolver)?; let dm = match stun_host { Some(host_name) => { @@ -761,10 +763,12 @@ async fn relay_urls(count: usize, config: NodeConfig) -> anyhow::Result<()> { println!("No relay nodes specified in the config file."); } + let dns_resolver = default_resolver(); let mut clients = HashMap::new(); for node in &config.relay_nodes { let secret_key = key.clone(); - let client = iroh::net::relay::http::ClientBuilder::new(node.url.clone()).build(secret_key); + let client = iroh::net::relay::http::ClientBuilder::new(node.url.clone()) + .build(secret_key, dns_resolver.clone()); clients.insert(node.url.clone(), client); } diff --git a/iroh-net/src/bin/iroh-relay.rs b/iroh-net/src/bin/iroh-relay.rs index 7fe65649e4b..e7b435cb8c8 100644 --- a/iroh-net/src/bin/iroh-relay.rs +++ b/iroh-net/src/bin/iroh-relay.rs @@ -954,8 +954,9 @@ mod tests { // set up clients let a_secret_key = SecretKey::generate(); let a_key = a_secret_key.public(); + let resolver = iroh_net::dns::default_resolver().clone(); let (client_a, mut client_a_receiver) = - ClientBuilder::new(relay_server_url.clone()).build(a_secret_key); + ClientBuilder::new(relay_server_url.clone()).build(a_secret_key, resolver); let connect_client = client_a.clone(); // give the relay server some time to set up @@ -977,8 +978,9 @@ mod tests { let b_secret_key = SecretKey::generate(); let b_key = b_secret_key.public(); + let resolver = iroh_net::dns::default_resolver().clone(); let (client_b, mut client_b_receiver) = - ClientBuilder::new(relay_server_url.clone()).build(b_secret_key); + ClientBuilder::new(relay_server_url.clone()).build(b_secret_key, resolver); client_b.connect().await?; let msg = Bytes::from("hello, b"); diff --git a/iroh-net/src/dns.rs b/iroh-net/src/dns.rs index 094feffb2ec..60c1d6eabd0 100644 --- a/iroh-net/src/dns.rs +++ b/iroh-net/src/dns.rs @@ -1,3 +1,6 @@ +//! This module exports a DNS resolver, which is also the default resolver used in the +//! [`crate::MagicEndpoint`] if no custom resolver is configured. + use std::net::{IpAddr, Ipv6Addr}; use std::time::Duration; @@ -5,8 +8,19 @@ use anyhow::Result; use hickory_resolver::{AsyncResolver, IntoName, TokioAsyncResolver, TryParseIp}; use once_cell::sync::Lazy; -pub static DNS_RESOLVER: Lazy = - Lazy::new(|| get_resolver().expect("unable to create DNS resolver")); +/// The DNS resolver type used throughout `iroh-net`. +pub type DnsResolver = TokioAsyncResolver; + +static DNS_RESOLVER: Lazy = + Lazy::new(|| create_default_resolver().expect("unable to create DNS resolver")); + +/// Get a reference to the default DNS resolver. +/// +/// The default resolver can be cheaply cloned and is shared throughout the running process. +/// It is configured to use the system's DNS configuration. +pub fn default_resolver() -> &'static DnsResolver { + &DNS_RESOLVER +} /// Deprecated IPv6 site-local anycast addresses still configured by windows. /// @@ -27,7 +41,7 @@ const WINDOWS_BAD_SITE_LOCAL_DNS_SERVERS: [IpAddr; 3] = [ /// We first try to read the system's resolver from `/etc/resolv.conf`. /// This does not work at least on some Androids, therefore we fallback /// to the default `ResolverConfig` which uses eg. to google's `8.8.8.8` or `8.8.4.4`. -fn get_resolver() -> Result { +fn create_default_resolver() -> Result { let (system_config, mut options) = hickory_resolver::system_conf::read_system_conf().unwrap_or_default(); @@ -54,18 +68,20 @@ fn get_resolver() -> Result { } pub(crate) async fn lookup_ipv4( + resolver: &DnsResolver, host: N, timeout: Duration, ) -> Result> { - let addrs = tokio::time::timeout(timeout, DNS_RESOLVER.ipv4_lookup(host)).await??; + let addrs = tokio::time::timeout(timeout, resolver.ipv4_lookup(host)).await??; Ok(addrs.into_iter().map(|ip| IpAddr::V4(ip.0)).collect()) } pub(crate) async fn lookup_ipv6( + resolver: &DnsResolver, host: N, timeout: Duration, ) -> Result> { - let addrs = tokio::time::timeout(timeout, DNS_RESOLVER.ipv6_lookup(host)).await??; + let addrs = tokio::time::timeout(timeout, resolver.ipv6_lookup(host)).await??; Ok(addrs.into_iter().map(|ip| IpAddr::V6(ip.0)).collect()) } @@ -74,12 +90,13 @@ pub(crate) async fn lookup_ipv6( /// `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(crate) async fn lookup_ipv4_ipv6( +pub async fn lookup_ipv4_ipv6( + resolver: &DnsResolver, host: N, timeout: Duration, ) -> Result> { - let ipv4 = DNS_RESOLVER.ipv4_lookup(host.clone()); - let ipv6 = DNS_RESOLVER.ipv6_lookup(host); + 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); @@ -134,7 +151,8 @@ mod tests { #[cfg_attr(target_os = "windows", ignore = "flaky")] async fn test_dns_lookup_basic() { let _logging = iroh_test::logging::setup(); - let res = DNS_RESOLVER.lookup_ip(NA_RELAY_HOSTNAME).await.unwrap(); + let resolver = default_resolver(); + let res = resolver.lookup_ip(NA_RELAY_HOSTNAME).await.unwrap(); let res: Vec<_> = res.iter().collect(); assert!(!res.is_empty()); dbg!(res); @@ -144,7 +162,8 @@ mod tests { #[cfg_attr(target_os = "windows", ignore = "flaky")] async fn test_dns_lookup_ipv4_ipv6() { let _logging = iroh_test::logging::setup(); - let res = lookup_ipv4_ipv6(NA_RELAY_HOSTNAME, Duration::from_secs(5)) + let resolver = default_resolver(); + let res = lookup_ipv4_ipv6(resolver, NA_RELAY_HOSTNAME, Duration::from_secs(5)) .await .unwrap(); assert!(!res.is_empty()); diff --git a/iroh-net/src/lib.rs b/iroh-net/src/lib.rs index 128ab42a39c..5c7dffc8032 100644 --- a/iroh-net/src/lib.rs +++ b/iroh-net/src/lib.rs @@ -15,7 +15,7 @@ pub mod defaults; pub mod dialer; mod disco; pub mod discovery; -mod dns; +pub mod dns; pub mod magic_endpoint; pub mod magicsock; pub mod metrics; diff --git a/iroh-net/src/magic_endpoint.rs b/iroh-net/src/magic_endpoint.rs index 816773d1539..6301fd695df 100644 --- a/iroh-net/src/magic_endpoint.rs +++ b/iroh-net/src/magic_endpoint.rs @@ -13,6 +13,7 @@ use crate::{ config, defaults::default_relay_map, discovery::{Discovery, DiscoveryTask}, + dns::{default_resolver, DnsResolver}, key::{PublicKey, SecretKey}, magicsock::{self, MagicSock}, relay::{RelayMap, RelayMode, RelayUrl}, @@ -39,6 +40,7 @@ pub struct MagicEndpointBuilder { discovery: Option>, /// Path for known peers. See [`MagicEndpointBuilder::peers_data_path`]. peers_path: Option, + dns_resolver: Option, } impl Default for MagicEndpointBuilder { @@ -52,6 +54,7 @@ impl Default for MagicEndpointBuilder { keylog: Default::default(), discovery: Default::default(), peers_path: None, + dns_resolver: None, } } } @@ -141,6 +144,18 @@ impl MagicEndpointBuilder { self } + /// Optionally set a custom DNS resolver to use for this endpoint. + /// + /// The DNS resolver is used to resolve relay hostnames. + /// + /// By default, all magic endpoints share a DNS resolver, which is configured to use the + /// host system's DNS configuration. You can pass a custom instance of [`DnsResolver`] + /// here to use a differently configured DNS resolver for this endpoint. + pub fn dns_resolver(mut self, dns_resolver: DnsResolver) -> Self { + self.dns_resolver = Some(dns_resolver); + self + } + /// Bind the magic endpoint on the specified socket address. /// /// The *bind_port* is the port that should be bound locally. @@ -166,12 +181,17 @@ impl MagicEndpointBuilder { if let Some(c) = self.concurrent_connections { server_config.concurrent_connections(c); } + let dns_resolver = self + .dns_resolver + .unwrap_or_else(|| default_resolver().clone()); + let msock_opts = magicsock::Options { port: bind_port, secret_key, relay_map, nodes_path: self.peers_path, discovery: self.discovery, + dns_resolver, }; MagicEndpoint::bind(Some(server_config), msock_opts, self.keylog).await } diff --git a/iroh-net/src/magicsock.rs b/iroh-net/src/magicsock.rs index 717b5657dcb..b6302b44dbb 100644 --- a/iroh-net/src/magicsock.rs +++ b/iroh-net/src/magicsock.rs @@ -55,7 +55,7 @@ use crate::{ config, disco::{self, SendAddr}, discovery::Discovery, - dns::DNS_RESOLVER, + dns::DnsResolver, key::{PublicKey, SecretKey, SharedSecret}, magic_endpoint::NodeAddr, net::{interfaces, ip::LocalAddresses, netmon, IpFamily}, @@ -113,6 +113,12 @@ pub struct Options { /// Optional node discovery mechanism. pub discovery: Option>, + + /// A DNS resolver to use for resolving relay URLs. + /// + /// You can use [`crate::dns::default_resolver`] for a resolver that uses the system's DNS + /// configuration. + pub dns_resolver: DnsResolver, } impl Default for Options { @@ -123,6 +129,7 @@ impl Default for Options { relay_map: RelayMap::empty(), nodes_path: None, discovery: None, + dns_resolver: crate::dns::default_resolver().clone(), } } } @@ -161,6 +168,9 @@ struct Inner { network_recv_wakers: parking_lot::Mutex>, network_send_wakers: parking_lot::Mutex>, + /// The DNS resolver to be used in this magicsock. + dns_resolver: DnsResolver, + /// Key for this node. secret_key: SecretKey, @@ -1135,6 +1145,7 @@ impl MagicSock { relay_map, discovery, nodes_path, + dns_resolver, } = opts; let nodes_path = match nodes_path { @@ -1164,7 +1175,7 @@ impl MagicSock { let ipv4_addr = pconn4.local_addr()?; let ipv6_addr = pconn6.as_ref().and_then(|c| c.local_addr().ok()); - let net_checker = netcheck::Client::new(Some(port_mapper.clone()))?; + let net_checker = netcheck::Client::new(Some(port_mapper.clone()), dns_resolver.clone())?; let (actor_sender, actor_receiver) = mpsc::channel(256); let (relay_actor_sender, relay_actor_receiver) = mpsc::channel(256); @@ -1214,6 +1225,7 @@ impl MagicSock { endpoints: Watchable::new(Default::default()), pending_call_me_maybes: Default::default(), endpoints_update_state: EndpointUpdateState::new(), + dns_resolver, }); let mut actor_tasks = JoinSet::default(); @@ -1696,7 +1708,7 @@ impl Actor { debug!("link change detected: major? {}", is_major); if is_major { - DNS_RESOLVER.clear_cache(); + self.inner.dns_resolver.clear_cache(); self.inner.re_stun("link-change-major"); self.close_stale_relay_connections().await; self.reset_endpoint_states(); diff --git a/iroh-net/src/magicsock/relay_actor.rs b/iroh-net/src/magicsock/relay_actor.rs index f0585e9a88d..eb25b666447 100644 --- a/iroh-net/src/magicsock/relay_actor.rs +++ b/iroh-net/src/magicsock/relay_actor.rs @@ -487,7 +487,7 @@ impl RelayActor { }) .can_ack_pings(true) .is_preferred(my_relay.as_ref() == Some(&url1)) - .build(self.conn.secret_key.clone()); + .build(self.conn.secret_key.clone(), self.conn.dns_resolver.clone()); let (s, r) = mpsc::channel(64); diff --git a/iroh-net/src/netcheck.rs b/iroh-net/src/netcheck.rs index 60f91de5672..61139068679 100644 --- a/iroh-net/src/netcheck.rs +++ b/iroh-net/src/netcheck.rs @@ -19,6 +19,7 @@ use tokio::time::{Duration, Instant}; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info_span, trace, warn, Instrument}; +use crate::dns::DnsResolver; use crate::net::ip::to_canonical; use crate::net::{IpFamily, UdpSocket}; use crate::relay::RelayUrl; @@ -206,8 +207,8 @@ impl Client { /// /// This starts a connected actor in the background. Once the client is dropped it will /// stop running. - pub fn new(port_mapper: Option) -> Result { - let mut actor = Actor::new(port_mapper)?; + pub fn new(port_mapper: Option, dns_resolver: DnsResolver) -> Result { + let mut actor = Actor::new(port_mapper, dns_resolver)?; let addr = actor.addr(); let task = tokio::spawn(async move { actor.run().await }.instrument(info_span!("netcheck.actor"))); @@ -407,6 +408,9 @@ struct Actor { in_flight_stun_requests: HashMap, /// The [`reportgen`] actor currently generating a report. current_report_run: Option, + + /// The DNS resolver to use for probes that need to perform DNS lookups + dns_resolver: DnsResolver, } impl Actor { @@ -414,7 +418,7 @@ impl Actor { /// /// This does not start the actor, see [`Actor::run`] for this. You should not /// normally create this directly but rather create a [`Client`]. - fn new(port_mapper: Option) -> Result { + fn new(port_mapper: Option, dns_resolver: DnsResolver) -> Result { // TODO: consider an instrumented flume channel so we have metrics. let (sender, receiver) = mpsc::channel(32); Ok(Self { @@ -424,6 +428,7 @@ impl Actor { port_mapper, in_flight_stun_requests: Default::default(), current_report_run: None, + dns_resolver, }) } @@ -525,6 +530,7 @@ impl Actor { relay_map, stun_sock_v4, stun_sock_v6, + self.dns_resolver.clone(), ); self.current_report_run = Some(ReportRun { @@ -793,7 +799,8 @@ mod tests { let (stun_addr, stun_stats, _cleanup_guard) = stun::test::serve("0.0.0.0".parse().unwrap()).await?; - let mut client = Client::new(None)?; + let resolver = crate::dns::default_resolver(); + let mut client = Client::new(None, resolver.clone())?; let dm = stun::test::relay_map_of([stun_addr].into_iter()); // Note that the ProbePlan will change with each iteration. @@ -830,7 +837,8 @@ mod tests { async fn test_iroh_computer_stun() -> Result<()> { let _guard = iroh_test::logging::setup(); - let mut client = Client::new(None).context("failed to create netcheck client")?; + let resolver = crate::dns::default_resolver().clone(); + let mut client = Client::new(None, resolver).context("failed to create netcheck client")?; let url: RelayUrl = format!("https://{}", EU_RELAY_HOSTNAME).parse().unwrap(); let dm = RelayMap::from_nodes([RelayNode { @@ -887,7 +895,8 @@ mod tests { let dm = stun::test::relay_map_of_opts([(stun_addr, false)].into_iter()); // Now create a client and generate a report. - let mut client = Client::new(None)?; + let resolver = crate::dns::default_resolver().clone(); + let mut client = Client::new(None, resolver)?; let r = client.get_report(dm, None, None).await?; let mut r: Report = (*r).clone(); @@ -1089,7 +1098,8 @@ mod tests { ]; for mut tt in tests { println!("test: {}", tt.name); - let mut actor = Actor::new(None).unwrap(); + let resolver = crate::dns::default_resolver().clone(); + let mut actor = Actor::new(None, resolver).unwrap(); for s in &mut tt.steps { // trigger the timer time::advance(Duration::from_secs(s.after)).await; @@ -1123,7 +1133,8 @@ mod tests { let dm = stun::test::relay_map_of([stun_addr].into_iter()); dbg!(&dm); - let mut client = Client::new(None)?; + let resolver = crate::dns::default_resolver().clone(); + let mut client = Client::new(None, resolver)?; // Set up an external socket to send STUN requests from, this will be discovered as // our public socket address by STUN. We send back any packets received on this diff --git a/iroh-net/src/netcheck/reportgen.rs b/iroh-net/src/netcheck/reportgen.rs index 5f9ae7eb923..517ccd95955 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}; +use crate::dns::{lookup_ipv4, lookup_ipv6, DnsResolver}; use crate::net::interfaces; use crate::net::ip; use crate::net::UdpSocket; @@ -94,6 +94,7 @@ impl Client { relay_map: RelayMap, stun_sock4: Option>, stun_sock6: Option>, + dns_resolver: DnsResolver, ) -> Self { let (msg_tx, msg_rx) = mpsc::channel(32); let addr = Addr { @@ -113,6 +114,7 @@ impl Client { report: Report::default(), hairpin_actor: hairpin::Client::new(netcheck, addr), outstanding_tasks: OutstandingTasks::default(), + dns_resolver, }; let task = tokio::spawn( async move { actor.run().await }.instrument(info_span!("reportgen.actor")), @@ -194,6 +196,8 @@ struct Actor { /// /// This is essentially the summary of all the work the [`Actor`] is doing. outstanding_tasks: OutstandingTasks, + /// The DNS resolver to use for probes that need to resolve DNS records + dns_resolver: DnsResolver, } impl Actor { @@ -555,6 +559,7 @@ impl Actor { let probe = probe.clone(); let netcheck = self.netcheck.clone(); let pinger = pinger.clone(); + let dns_resolver = self.dns_resolver.clone(); set.spawn( run_probe( @@ -565,6 +570,7 @@ impl Actor { probe.clone(), netcheck, pinger, + dns_resolver, ) .instrument(debug_span!("run_probe", %probe)), ); @@ -682,6 +688,7 @@ async fn run_probe( probe: Probe, netcheck: netcheck::Addr, pinger: Pinger, + dns_resolver: DnsResolver, ) -> Result { if !probe.delay().is_zero() { trace!("delaying probe"); @@ -715,7 +722,7 @@ async fn run_probe( )); } - let relay_addr = get_relay_addr(&relay_node, probe.proto()) + let relay_addr = get_relay_addr(&dns_resolver, &relay_node, probe.proto()) .await .context("no relay node addr") .map_err(|e| ProbeError::AbortSet(e, probe.clone()))?; @@ -923,7 +930,11 @@ async fn check_captive_portal(dm: &RelayMap, preferred_relay: Option) /// *proto* specifies the protocol of the probe. Depending on the protocol we may return /// different results. Obviously IPv4 vs IPv6 but a [`RelayNode`] may also have disabled /// some protocols. -async fn get_relay_addr(relay_node: &RelayNode, proto: ProbeProto) -> Result { +async fn get_relay_addr( + dns_resolver: &DnsResolver, + relay_node: &RelayNode, + proto: ProbeProto, +) -> Result { let port = if relay_node.stun_port == 0 { DEFAULT_RELAY_STUN_PORT } else { @@ -938,7 +949,7 @@ async fn get_relay_addr(relay_node: &RelayNode, proto: ProbeProto) -> Result match relay_node.url.host() { Some(url::Host::Domain(hostname)) => { debug!(?proto, %hostname, "Performing DNS A lookup for relay addr"); - match lookup_ipv4(hostname, DNS_TIMEOUT).await { + match lookup_ipv4(dns_resolver, hostname, DNS_TIMEOUT).await { Ok(addrs) => addrs .first() .map(|addr| ip::to_canonical(*addr)) @@ -955,7 +966,7 @@ async fn get_relay_addr(relay_node: &RelayNode, proto: ProbeProto) -> Result match relay_node.url.host() { Some(url::Host::Domain(hostname)) => { debug!(?proto, %hostname, "Performing DNS AAAA lookup for relay addr"); - match lookup_ipv6(hostname, DNS_TIMEOUT).await { + match lookup_ipv6(dns_resolver, hostname, DNS_TIMEOUT).await { Ok(addrs) => addrs .first() .map(|addr| ip::to_canonical(*addr)) @@ -1314,7 +1325,8 @@ mod tests { let _logging_guard = iroh_test::logging::setup(); let pinger = Pinger::new(); let relay = default_eu_relay_node(); - let addr = get_relay_addr(&relay, ProbeProto::IcmpV4) + let resolver = crate::dns::default_resolver(); + let addr = get_relay_addr(resolver, &relay, ProbeProto::IcmpV4) .await .map_err(|err| format!("{err:#}")) .unwrap(); diff --git a/iroh-net/src/relay/http.rs b/iroh-net/src/relay/http.rs index 8d8a82ff2c3..120722a0687 100644 --- a/iroh-net/src/relay/http.rs +++ b/iroh-net/src/relay/http.rs @@ -129,7 +129,8 @@ mod tests { Client, ) { let client = ClientBuilder::new(server_url); - let (client, mut client_reader) = client.build(key.clone()); + let dns_resolver = crate::dns::default_resolver(); + let (client, mut client_reader) = client.build(key.clone(), dns_resolver.clone()); let public_key = key.public(); let (received_msg_s, received_msg_r) = tokio::sync::mpsc::channel(10); let client_reader_task = tokio::spawn( diff --git a/iroh-net/src/relay/http/client.rs b/iroh-net/src/relay/http/client.rs index 64203335c15..e7c3346b72f 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; +use crate::dns::{lookup_ipv4_ipv6, DnsResolver}; use crate::key::{PublicKey, SecretKey}; use crate::relay::RelayUrl; use crate::relay::{ @@ -161,6 +161,7 @@ struct Actor { tls_connector: tokio_rustls::TlsConnector, pings: PingTracker, ping_tasks: JoinSet<()>, + dns_resolver: DnsResolver, } #[derive(Default, Debug)] @@ -265,7 +266,7 @@ impl ClientBuilder { } /// Build the [`Client`] - pub fn build(self, key: SecretKey) -> (Client, ClientReceiver) { + pub fn build(self, key: SecretKey, dns_resolver: DnsResolver) -> (Client, ClientReceiver) { // TODO: review TLS config let mut roots = rustls::RootCertStore::empty(); roots.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| { @@ -303,6 +304,7 @@ impl ClientBuilder { server_public_key: self.server_public_key, url: self.url, tls_connector, + dns_resolver, }; let (msg_sender, inbox) = mpsc::channel(64); @@ -750,7 +752,7 @@ impl Actor { debug!(%self.url, "dial url"); let prefer_ipv6 = self.prefer_ipv6().await; - let dst_ip = resolve_host(&self.url, prefer_ipv6).await?; + let dst_ip = resolve_host(&self.dns_resolver, &self.url, prefer_ipv6).await?; let port = self .url_port() @@ -825,14 +827,18 @@ impl Actor { } } -async fn resolve_host(url: &Url, prefer_ipv6: bool) -> Result { +async fn resolve_host( + resolver: &DnsResolver, + url: &Url, + prefer_ipv6: bool, +) -> Result { let host = url .host() .ok_or_else(|| ClientError::InvalidUrl("missing host".into()))?; match host { url::Host::Domain(domain) => { // Need to do a DNS lookup - let addrs = lookup_ipv4_ipv6(domain, DNS_TIMEOUT) + let addrs = lookup_ipv4_ipv6(resolver, domain, DNS_TIMEOUT) .await .map_err(|e| ClientError::Dns(Some(e)))?; @@ -904,16 +910,20 @@ impl rustls::client::ServerCertVerifier for NoCertVerifier { #[cfg(test)] mod tests { - use super::*; - use anyhow::Result; + use crate::dns::default_resolver; + + use super::*; + #[tokio::test] async fn test_recv_detail_connect_error() -> Result<()> { let key = SecretKey::generate(); let bad_url: Url = "https://bad.url".parse().unwrap(); + let dns_resolver = default_resolver(); - let (_client, mut client_receiver) = ClientBuilder::new(bad_url).build(key.clone()); + let (_client, mut client_receiver) = + ClientBuilder::new(bad_url).build(key.clone(), dns_resolver.clone()); // ensure that the client will bubble up any connection error & not // just loop ad infinitum attempting to connect