Skip to content

feat!: Add a Resolver trait to abstract over DNS resolvers #3326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion iroh-dns-server/examples/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async fn main() -> anyhow::Result<()> {
DnsResolver::with_nameserver(addr)
} else {
match args.env {
Env::Staging | Env::Prod => DnsResolver::new(),
Env::Staging | Env::Prod => DnsResolver::default(),
Env::Dev => {
DnsResolver::with_nameserver(DEV_DNS_SERVER.parse().expect("valid address"))
}
Expand Down
8 changes: 4 additions & 4 deletions iroh-dns-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,25 +116,25 @@ mod tests {
// resolve root record
let name = Name::from_utf8(format!("{pubkey}."))?;
let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
let records = res.map(|t| t.to_string()).collect::<Vec<_>>();
assert_eq!(records, vec!["hi0".to_string()]);

// resolve level one record
let name = Name::from_utf8(format!("_hello.{pubkey}."))?;
let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
let records = res.map(|t| t.to_string()).collect::<Vec<_>>();
assert_eq!(records, vec!["hi1".to_string()]);

// resolve level two record
let name = Name::from_utf8(format!("_hello.world.{pubkey}."))?;
let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
let records = res.map(|t| t.to_string()).collect::<Vec<_>>();
assert_eq!(records, vec!["hi2".to_string()]);

// resolve multiple records for same name
let name = Name::from_utf8(format!("multiple.{pubkey}."))?;
let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
let records = res.map(|t| t.to_string()).collect::<Vec<_>>();
assert_eq!(records, vec!["hi3".to_string(), "hi4".to_string()]);

// resolve A record
Expand Down
213 changes: 142 additions & 71 deletions iroh-relay/src/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,73 @@
use std::{
fmt::{self, Write},
future::Future,
net::{IpAddr, Ipv6Addr, SocketAddr},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
sync::Arc,
};

use anyhow::{bail, Context, Result};
use hickory_resolver::{name_server::TokioConnectionProvider, TokioResolver};
use iroh_base::NodeId;
use n0_future::{
boxed::BoxFuture,
time::{self, Duration},
StreamExt,
};
use url::Url;

use crate::node_info::NodeInfo;
use crate::{
defaults::timeouts::DNS_TIMEOUT,
node_info::{self, NodeInfo},
};

/// The n0 testing DNS node origin, for production.
pub const N0_DNS_NODE_ORIGIN_PROD: &str = "dns.iroh.link";
/// The n0 testing DNS node origin, for testing.
pub const N0_DNS_NODE_ORIGIN_STAGING: &str = "staging-dns.iroh.link";

/// The DNS resolver used throughout `iroh`.
/// Trait for DNS resolvers used in iroh.
pub trait Resolver: fmt::Debug + Send + Sync + 'static {
/// Looks up an IPv4 address.
fn lookup_ipv4(&self, host: String) -> BoxFuture<Result<BoxIter<Ipv4Addr>>>;

/// Looks up an IPv6 address.
fn lookup_ipv6(&self, host: String) -> BoxFuture<Result<BoxIter<Ipv6Addr>>>;

/// Looks up TXT records.
fn lookup_txt(&self, host: String) -> BoxFuture<Result<BoxIter<TxtRecordData>>>;

/// Clears the internal cache.
fn clear_cache(&self);
}

/// Boxed iterator alias.
pub type BoxIter<T> = Box<dyn Iterator<Item = T> + Send + 'static>;

/// DNS resolver for use in iroh.
///
/// This internally contains a [`dyn Resolver`]. See the public methods for how to construct
/// a [`DnsResolver`] with sensible defaults or with a custom resolver.
#[derive(Debug, Clone)]
pub struct DnsResolver(TokioResolver);
pub struct DnsResolver(Arc<dyn Resolver>);

impl DnsResolver {
/// Create a new DNS resolver with sensible cross-platform defaults.
/// Creates a new [`DnsResolver`] from a struct that implements [`Resolver`].
///
/// [`Resolver`] is implemented for [`hickory_resolver::TokioResolver`], so you can construct
/// a [`TokioResolver`] and pass that to this function.
///
/// To use a different DNS resolver, you need to implement [`Resolver`] for your custom resolver
/// and then pass to this function.
pub fn new(resolver: impl Resolver) -> Self {
Self(Arc::new(resolver))
}

/// Creates a new DNS resolver with sensible cross-platform defaults.
///
/// 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`.
pub fn new() -> Self {
/// to the default `ResolverConfig` which uses Google's `8.8.8.8` or `8.8.4.4`.
pub fn new_with_system_defaults() -> Self {
let (system_config, mut options) =
hickory_resolver::system_conf::read_system_conf().unwrap_or_default();

Expand All @@ -57,10 +94,10 @@ impl DnsResolver {
let mut builder =
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
*builder.options_mut() = options;
DnsResolver(builder.build())
Self::new(builder.build())
}

/// Create a new DNS resolver configured with a single UDP DNS nameserver.
/// Creates a new DNS resolver configured with a single UDP DNS nameserver.
pub fn with_nameserver(nameserver: SocketAddr) -> Self {
let mut config = hickory_resolver::config::ResolverConfig::new();
let nameserver_config = hickory_resolver::config::NameServerConfig::new(
Expand All @@ -71,44 +108,40 @@ impl DnsResolver {

let builder =
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
DnsResolver(builder.build())
Self::new(builder.build())
}

/// Removes all entries from the cache.
pub fn clear_cache(&self) {
self.0.clear_cache();
}

/// Lookup a TXT record.
pub async fn lookup_txt(&self, host: impl ToString, timeout: Duration) -> Result<TxtLookup> {
let host = host.to_string();
let res = time::timeout(timeout, self.0.txt_lookup(host)).await??;
Ok(TxtLookup(res))
/// Performs a TXT lookup with a timeout.
pub async fn lookup_txt(
&self,
host: impl ToString,
timeout: Duration,
) -> Result<impl Iterator<Item = TxtRecordData>> {
let res = time::timeout(timeout, self.0.lookup_txt(host.to_string())).await??;
Ok(res)
}

/// Perform an ipv4 lookup with a timeout.
/// Performs an IPv4 lookup with a timeout.
pub async fn lookup_ipv4(
&self,
host: impl ToString,
timeout: Duration,
) -> Result<impl Iterator<Item = IpAddr>> {
let host = host.to_string();
let addrs = time::timeout(timeout, self.0.ipv4_lookup(host)).await??;
Ok(addrs.into_iter().map(|ip| IpAddr::V4(ip.0)))
let addrs = time::timeout(timeout, self.0.lookup_ipv4(host.to_string())).await??;
Ok(addrs.map(IpAddr::V4))
}

/// Perform an ipv6 lookup with a timeout.
/// Performs an IPv6 lookup with a timeout.
pub async fn lookup_ipv6(
&self,
host: impl ToString,
timeout: Duration,
) -> Result<impl Iterator<Item = IpAddr>> {
let host = host.to_string();
let addrs = time::timeout(timeout, self.0.ipv6_lookup(host)).await??;
Ok(addrs.into_iter().map(|ip| IpAddr::V6(ip.0)))
let addrs = time::timeout(timeout, self.0.lookup_ipv6(host.to_string())).await??;
Ok(addrs.map(IpAddr::V6))
}

/// Resolve IPv4 and IPv6 in parallel with a timeout.
/// Resolves IPv4 and IPv6 in parallel with a timeout.
///
/// `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
Expand All @@ -134,7 +167,7 @@ impl DnsResolver {
}
}

/// Resolve a hostname from a URL to an IP address.
/// Resolves a hostname from a URL to an IP address.
pub async fn resolve_host(
&self,
url: &Url,
Expand Down Expand Up @@ -164,7 +197,7 @@ impl DnsResolver {
}
}

/// Perform an ipv4 lookup with a timeout in a staggered fashion.
/// Performs an IPv4 lookup with a timeout in a staggered fashion.
///
/// From the moment this function is called, each lookup is scheduled after the delays in
/// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
Expand All @@ -181,7 +214,7 @@ impl DnsResolver {
stagger_call(f, delays_ms).await
}

/// Perform an ipv6 lookup with a timeout in a staggered fashion.
/// Performs an IPv6 lookup with a timeout in a staggered fashion.
///
/// From the moment this function is called, each lookup is scheduled after the delays in
/// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
Expand All @@ -198,7 +231,7 @@ impl DnsResolver {
stagger_call(f, delays_ms).await
}

/// Race an ipv4 and ipv6 lookup with a timeout in a staggered fashion.
/// Races an IPv4 and IPv6 lookup with a timeout in a staggered fashion.
///
/// From the moment this function is called, each lookup is scheduled after the delays in
/// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
Expand All @@ -221,21 +254,17 @@ impl DnsResolver {
/// To lookup nodes that published their node info to the DNS servers run by n0,
/// pass [`N0_DNS_NODE_ORIGIN_PROD`] as `origin`.
pub async fn lookup_node_by_id(&self, node_id: &NodeId, origin: &str) -> Result<NodeInfo> {
let attrs = crate::node_info::TxtAttrs::<crate::node_info::IrohAttr>::lookup_by_id(
self, node_id, origin,
)
.await?;
let info = attrs.into();
Ok(info)
let name = node_info::node_domain(node_id, origin);
let name = node_info::ensure_iroh_txt_label(name);
let lookup = self.lookup_txt(name.clone(), DNS_TIMEOUT).await?;
NodeInfo::from_txt_lookup(name, lookup)
}

/// Looks up node info by DNS name.
pub async fn lookup_node_by_domain_name(&self, name: &str) -> Result<NodeInfo> {
let attrs =
crate::node_info::TxtAttrs::<crate::node_info::IrohAttr>::lookup_by_name(self, name)
.await?;
let info = attrs.into();
Ok(info)
let name = node_info::ensure_iroh_txt_label(name.to_string());
let lookup = self.lookup_txt(name.clone(), DNS_TIMEOUT).await?;
NodeInfo::from_txt_lookup(name, lookup)
}

/// Looks up node info by DNS name in a staggered fashion.
Expand Down Expand Up @@ -268,54 +297,96 @@ impl DnsResolver {
let f = || self.lookup_node_by_id(node_id, origin);
stagger_call(f, delays_ms).await
}

/// Removes all entries from the cache.
pub fn clear_cache(&self) {
self.0.clear_cache();
}
}

impl Default for DnsResolver {
fn default() -> Self {
Self::new()
Self::new_with_system_defaults()
}
}

impl From<TokioResolver> for DnsResolver {
fn from(resolver: TokioResolver) -> Self {
DnsResolver(resolver)
/// Implementation of [`Resolver`] for [`hickory_resolver::TokioResolver`].
impl Resolver for TokioResolver {
fn lookup_ipv4(&self, host: String) -> BoxFuture<Result<BoxIter<Ipv4Addr>>> {
let this = self.clone();
Box::pin(async move {
let addrs = this.ipv4_lookup(host).await?;
let iter: BoxIter<Ipv4Addr> = Box::new(addrs.into_iter().map(Ipv4Addr::from));
Ok(iter)
})
}
}

/// TXT records returned from [`DnsResolver::lookup_txt`]
#[derive(Debug, Clone)]
pub struct TxtLookup(pub(crate) hickory_resolver::lookup::TxtLookup);

impl From<hickory_resolver::lookup::TxtLookup> for TxtLookup {
fn from(value: hickory_resolver::lookup::TxtLookup) -> Self {
Self(value)
fn lookup_ipv6(&self, host: String) -> BoxFuture<Result<BoxIter<Ipv6Addr>>> {
let this = self.clone();
Box::pin(async move {
let addrs = this.ipv6_lookup(host).await?;
let iter: BoxIter<Ipv6Addr> = Box::new(addrs.into_iter().map(Ipv6Addr::from));
Ok(iter)
})
}
}

impl IntoIterator for TxtLookup {
type Item = TXT;

type IntoIter = Box<dyn Iterator<Item = TXT>>;
fn lookup_txt(&self, host: String) -> BoxFuture<Result<BoxIter<TxtRecordData>>> {
let this = self.clone();
Box::pin(async move {
let lookup = this.txt_lookup(host).await?;
let iter: BoxIter<TxtRecordData> = Box::new(
lookup
.into_iter()
.map(|txt| TxtRecordData::from_iter(txt.iter().cloned())),
);
Ok(iter)
})
}

fn into_iter(self) -> Self::IntoIter {
Box::new(self.0.into_iter().map(TXT))
fn clear_cache(&self) {
self.clear_cache();
}
}

/// Record data for a TXT record
/// Record data for a TXT record.
///
/// This contains a list of character strings, as defined in [RFC 1035 Section 3.3.14].
///
/// [`TxtRecordData`] implements [`fmt::Display`], so you can call [`ToString::to_string`] to
/// convert the record data into a string. This will parse each character string with
/// [`String::from_utf8_lossy`] and then concatenate all strings without a separator.
///
/// If you want to process each character string individually, use [`Self::iter`].
///
/// [RFC 1035 Section 3.3.14]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14
#[derive(Debug, Clone)]
pub struct TXT(hickory_resolver::proto::rr::rdata::TXT);
pub struct TxtRecordData(Box<[Box<[u8]>]>);

impl TXT {
/// Returns the raw character strings of this TXT record.
pub fn txt_data(&self) -> &[Box<[u8]>] {
self.0.txt_data()
impl TxtRecordData {
/// Returns an iterator over the character strings contained in this TXT record.
pub fn iter(&self) -> impl Iterator<Item = &[u8]> {
self.0.iter().map(|x| x.as_ref())
}
}

impl fmt::Display for TXT {
impl fmt::Display for TxtRecordData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
for s in self.iter() {
write!(f, "{}", &String::from_utf8_lossy(s))?
}
Ok(())
}
}

impl FromIterator<Box<[u8]>> for TxtRecordData {
fn from_iter<T: IntoIterator<Item = Box<[u8]>>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}

impl From<Vec<Box<[u8]>>> for TxtRecordData {
fn from(value: Vec<Box<[u8]>>) -> Self {
Self(value.into_boxed_slice())
}
}

Expand Down
Loading
Loading