Skip to content

Commit

Permalink
refactor(iroh-net): allow to set a custom DNS resolver on the magic e…
Browse files Browse the repository at this point in the history
…ndpoint (#2116)

## Description

This makes the DNS resolver to be used in the context of a MagicEndpoint
configurable. Up to now, we used a single, global, unconfigurable DNS
resolver, stored in a per-process global static. This PR changes this so
that we can set a DNS resolver in the builder of the MagicEndpoint. This
resolver is passed through to all places where we need a DNS resolver -
which is all places where we need to resolve relay URLs.

The default is unchanged: A single, shared DNS resolver is used for all
endpoints. However, this default can now be changed per-endpoint. The
global resolver is only used as a default in the endpoint builder if no
custom resolver is set, and in the doctor, and in tests.

This change will make testing things that use DNS - prominently: #2045 -
much easier. And we now have the means in place for people to customize
the DNS resolving, if needed.

## Notes & open questions

This makes the `hickory_resolver::TokioAsyncResolver` part of the public
API surface of `iroh-net`.

## Change checklist

- [x] Self-review.
- [x] Documentation updates if relevant.
- [ ] Tests if relevant.
  • Loading branch information
Frando authored Mar 23, 2024
1 parent bed14d4 commit 8dcb196
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 42 deletions.
8 changes: 6 additions & 2 deletions iroh-cli/src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use iroh::{
},
net::{
defaults::DEFAULT_RELAY_STUN_PORT,
dns::default_resolver,
key::{PublicKey, SecretKey},
magic_endpoint,
magicsock::EndpointInfo,
Expand Down Expand Up @@ -289,7 +290,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) => {
Expand Down Expand Up @@ -775,10 +777,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);
}
Expand Down
6 changes: 4 additions & 2 deletions iroh-net/src/bin/iroh-relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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");
Expand Down
39 changes: 29 additions & 10 deletions iroh-net/src/dns.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
//! 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;

use anyhow::Result;
use hickory_resolver::{AsyncResolver, IntoName, TokioAsyncResolver, TryParseIp};
use once_cell::sync::Lazy;

pub static DNS_RESOLVER: Lazy<TokioAsyncResolver> =
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<TokioAsyncResolver> =
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.
///
Expand All @@ -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<TokioAsyncResolver> {
fn create_default_resolver() -> Result<TokioAsyncResolver> {
let (system_config, mut options) =
hickory_resolver::system_conf::read_system_conf().unwrap_or_default();

Expand All @@ -54,18 +68,20 @@ fn get_resolver() -> Result<TokioAsyncResolver> {
}

pub(crate) async fn lookup_ipv4<N: IntoName + TryParseIp + Clone>(
resolver: &DnsResolver,
host: N,
timeout: Duration,
) -> Result<Vec<IpAddr>> {
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<N: IntoName + TryParseIp + Clone>(
resolver: &DnsResolver,
host: N,
timeout: Duration,
) -> Result<Vec<IpAddr>> {
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())
}

Expand All @@ -74,12 +90,13 @@ pub(crate) async fn lookup_ipv6<N: IntoName + TryParseIp + Clone>(
/// `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<N: IntoName + TryParseIp + Clone>(
pub async fn lookup_ipv4_ipv6<N: IntoName + TryParseIp + Clone>(
resolver: &DnsResolver,
host: N,
timeout: Duration,
) -> Result<Vec<IpAddr>> {
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);

Expand Down Expand Up @@ -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);
Expand All @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion iroh-net/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions iroh-net/src/magic_endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -39,6 +40,7 @@ pub struct MagicEndpointBuilder {
discovery: Option<Box<dyn Discovery>>,
/// Path for known peers. See [`MagicEndpointBuilder::peers_data_path`].
peers_path: Option<PathBuf>,
dns_resolver: Option<DnsResolver>,
}

impl Default for MagicEndpointBuilder {
Expand All @@ -52,6 +54,7 @@ impl Default for MagicEndpointBuilder {
keylog: Default::default(),
discovery: Default::default(),
peers_path: None,
dns_resolver: None,
}
}
}
Expand Down Expand Up @@ -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.
Expand All @@ -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
}
Expand Down
18 changes: 15 additions & 3 deletions iroh-net/src/magicsock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -113,6 +113,12 @@ pub struct Options {

/// Optional node discovery mechanism.
pub discovery: Option<Box<dyn Discovery>>,

/// 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 {
Expand All @@ -123,6 +129,7 @@ impl Default for Options {
relay_map: RelayMap::empty(),
nodes_path: None,
discovery: None,
dns_resolver: crate::dns::default_resolver().clone(),
}
}
}
Expand Down Expand Up @@ -161,6 +168,9 @@ struct Inner {
network_recv_wakers: parking_lot::Mutex<Option<Waker>>,
network_send_wakers: parking_lot::Mutex<Option<Waker>>,

/// The DNS resolver to be used in this magicsock.
dns_resolver: DnsResolver,

/// Key for this node.
secret_key: SecretKey,

Expand Down Expand Up @@ -1135,6 +1145,7 @@ impl MagicSock {
relay_map,
discovery,
nodes_path,
dns_resolver,
} = opts;

let nodes_path = match nodes_path {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion iroh-net/src/magicsock/relay_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading

0 comments on commit 8dcb196

Please sign in to comment.