From 71cee9eb46b1279e24fcc4e433b5ee09023b19f5 Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 26 May 2025 21:32:14 +0200 Subject: [PATCH 1/6] feat: DNS resolver trait --- iroh-dns-server/examples/resolve.rs | 2 +- iroh-dns-server/src/lib.rs | 8 +- iroh-relay/src/dns.rs | 216 ++++++++++++++++++--------- iroh-relay/src/node_info.rs | 110 ++++---------- iroh-relay/src/server.rs | 2 +- iroh-relay/src/server/http_server.rs | 4 +- iroh/src/dns.rs | 6 +- iroh/src/magicsock.rs | 5 +- iroh/src/magicsock/relay_actor.rs | 4 +- iroh/src/net_report/dns.rs | 2 +- 10 files changed, 192 insertions(+), 167 deletions(-) diff --git a/iroh-dns-server/examples/resolve.rs b/iroh-dns-server/examples/resolve.rs index 11299405d85..444c5b9d31c 100644 --- a/iroh-dns-server/examples/resolve.rs +++ b/iroh-dns-server/examples/resolve.rs @@ -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")) } diff --git a/iroh-dns-server/src/lib.rs b/iroh-dns-server/src/lib.rs index 25708fabec6..832fc0c7321 100644 --- a/iroh-dns-server/src/lib.rs +++ b/iroh-dns-server/src/lib.rs @@ -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::>(); + let records = res.map(|t| t.to_string()).collect::>(); 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::>(); + let records = res.map(|t| t.to_string()).collect::>(); 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::>(); + let records = res.map(|t| t.to_string()).collect::>(); 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::>(); + let records = res.map(|t| t.to_string()).collect::>(); assert_eq!(records, vec!["hi3".to_string(), "hi4".to_string()]); // resolve A record diff --git a/iroh-relay/src/dns.rs b/iroh-relay/src/dns.rs index 1d179393b2d..ca6960ca20a 100644 --- a/iroh-relay/src/dns.rs +++ b/iroh-relay/src/dns.rs @@ -3,36 +3,41 @@ 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`. +/// The default DNS resolver used throughout `iroh`. #[derive(Debug, Clone)] -pub struct DnsResolver(TokioResolver); +struct HickoryResolver(TokioResolver); -impl DnsResolver { +impl HickoryResolver { /// Create 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 { + fn new() -> Self { let (system_config, mut options) = hickory_resolver::system_conf::read_system_conf().unwrap_or_default(); @@ -57,11 +62,11 @@ impl DnsResolver { let mut builder = TokioResolver::builder_with_config(config, TokioConnectionProvider::default()); *builder.options_mut() = options; - DnsResolver(builder.build()) + HickoryResolver(builder.build()) } /// Create a new DNS resolver configured with a single UDP DNS nameserver. - pub fn with_nameserver(nameserver: SocketAddr) -> Self { + fn with_nameserver(nameserver: SocketAddr) -> Self { let mut config = hickory_resolver::config::ResolverConfig::new(); let nameserver_config = hickory_resolver::config::NameServerConfig::new( nameserver, @@ -71,19 +76,115 @@ impl DnsResolver { let builder = TokioResolver::builder_with_config(config, TokioConnectionProvider::default()); - DnsResolver(builder.build()) + HickoryResolver(builder.build()) + } +} + +impl Default for HickoryResolver { + fn default() -> Self { + Self::new() } +} - /// Removes all entries from the cache. - pub fn clear_cache(&self) { +/// Trait for DNS resolvers used in iroh. +pub trait Resolver: std::fmt::Debug + Send + Sync + 'static { + /// Lookup an IPv4 address. + fn lookup_ipv4(&self, host: String) -> BoxFuture>>; + /// Lookup an IPv6 address. + fn lookup_ipv6(&self, host: String) -> BoxFuture>>; + /// Lookup TXT records. + fn lookup_txt(&self, host: String) -> BoxFuture>>; + /// Clear the internal cache. + fn clear_cache(&self); +} + +/// Boxed iterator alias. +pub type BoxIter = Box + Send + 'static>; + +impl Resolver for HickoryResolver { + fn lookup_ipv4(&self, host: String) -> BoxFuture>> { + let this = self.0.clone(); + Box::pin(async move { + let addrs = this.ipv4_lookup(host).await?; + let iter: BoxIter = Box::new(addrs.into_iter().map(Ipv4Addr::from)); + Ok(iter) + }) + } + + fn lookup_ipv6(&self, host: String) -> BoxFuture>> { + let this = self.0.clone(); + Box::pin(async move { + let addrs = this.ipv6_lookup(host).await?; + let iter: BoxIter = Box::new(addrs.into_iter().map(Ipv6Addr::from)); + Ok(iter) + }) + } + + fn lookup_txt(&self, host: String) -> BoxFuture>> { + let this = self.0.clone(); + Box::pin(async move { + let lookup = this.txt_lookup(host).await?; + let iter: BoxIter = Box::new( + lookup + .into_iter() + .map(|txt| TxtRecord::from_iter(txt.iter().cloned())), + ); + Ok(iter) + }) + } + + fn clear_cache(&self) { self.0.clear_cache(); } +} + +impl From for HickoryResolver { + fn from(resolver: TokioResolver) -> Self { + HickoryResolver(resolver) + } +} + +/// +#[derive(Debug, Clone)] +pub struct DnsResolver(Arc); + +impl std::ops::Deref for DnsResolver { + type Target = dyn Resolver; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +impl Default for DnsResolver { + fn default() -> Self { + Self::new_with_system_defaults() + } +} + +impl DnsResolver { + /// + pub fn new(resolver: impl Resolver) -> Self { + Self(Arc::new(resolver)) + } + + /// + pub fn new_with_system_defaults() -> Self { + Self::new(HickoryResolver::new()) + } + + /// + pub fn with_nameserver(nameserver: SocketAddr) -> Self { + Self::new(HickoryResolver::with_nameserver(nameserver)) + } /// Lookup a TXT record. - pub async fn lookup_txt(&self, host: impl ToString, timeout: Duration) -> Result { - let host = host.to_string(); - let res = time::timeout(timeout, self.0.txt_lookup(host)).await??; - Ok(TxtLookup(res)) + pub async fn lookup_txt( + &self, + host: impl ToString, + timeout: Duration, + ) -> Result> { + let res = time::timeout(timeout, self.0.lookup_txt(host.to_string())).await??; + Ok(res) } /// Perform an ipv4 lookup with a timeout. @@ -92,9 +193,8 @@ impl DnsResolver { host: impl ToString, timeout: Duration, ) -> Result> { - 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(|ip| IpAddr::V4(ip))) } /// Perform an ipv6 lookup with a timeout. @@ -103,9 +203,8 @@ impl DnsResolver { host: impl ToString, timeout: Duration, ) -> Result> { - 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(|ip| IpAddr::V6(ip))) } /// Resolve IPv4 and IPv6 in parallel with a timeout. @@ -221,21 +320,19 @@ 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 { - let attrs = crate::node_info::TxtAttrs::::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?; + let attrs = node_info::TxtAttrs::from_txt_lookup(name, lookup)?; + Ok(attrs.into()) } /// Looks up node info by DNS name. pub async fn lookup_node_by_domain_name(&self, name: &str) -> Result { - let attrs = - crate::node_info::TxtAttrs::::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?; + let attrs = node_info::TxtAttrs::from_txt_lookup(name, lookup)?; + Ok(attrs.into()) } /// Looks up node info by DNS name in a staggered fashion. @@ -270,52 +367,35 @@ impl DnsResolver { } } -impl Default for DnsResolver { - fn default() -> Self { - Self::new() - } -} - -impl From for DnsResolver { - fn from(resolver: TokioResolver) -> Self { - DnsResolver(resolver) - } -} - -/// TXT records returned from [`DnsResolver::lookup_txt`] +/// Record data for a TXT record. #[derive(Debug, Clone)] -pub struct TxtLookup(pub(crate) hickory_resolver::lookup::TxtLookup); +pub struct TxtRecord(Vec>); -impl From for TxtLookup { - fn from(value: hickory_resolver::lookup::TxtLookup) -> Self { - Self(value) +impl TxtRecord { + /// + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|x| x.as_ref()) } -} - -impl IntoIterator for TxtLookup { - type Item = TXT; - - type IntoIter = Box>; - fn into_iter(self) -> Self::IntoIter { - Box::new(self.0.into_iter().map(TXT)) + /// + pub fn to_strings(&self) -> impl Iterator + '_ { + self.iter() + .map(|cstr| String::from_utf8_lossy(&cstr).to_string()) } } -/// Record data for a TXT record -#[derive(Debug, Clone)] -pub struct TXT(hickory_resolver::proto::rr::rdata::TXT); - -impl TXT { - /// Returns the raw character strings of this TXT record. - pub fn txt_data(&self) -> &[Box<[u8]>] { - self.0.txt_data() +impl std::fmt::Display for TxtRecord { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for s in self.iter() { + write!(f, "{}", &String::from_utf8_lossy(s))? + } + Ok(()) } } -impl fmt::Display for TXT { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) +impl FromIterator> for TxtRecord { + fn from_iter>>(iter: T) -> Self { + Self(iter.into_iter().collect()) } } diff --git a/iroh-relay/src/node_info.rs b/iroh-relay/src/node_info.rs index 47e082fadfb..3fc864f2350 100644 --- a/iroh-relay/src/node_info.rs +++ b/iroh-relay/src/node_info.rs @@ -42,14 +42,11 @@ use std::{ use anyhow::{anyhow, Result}; #[cfg(not(wasm_browser))] -use hickory_resolver::{proto::ProtoError, Name}; use iroh_base::{NodeAddr, NodeId, RelayUrl, SecretKey}; #[cfg(not(wasm_browser))] -use tracing::warn; use url::Url; #[cfg(not(wasm_browser))] -use crate::{defaults::timeouts::DNS_TIMEOUT, dns::DnsResolver}; /// The DNS name for the iroh TXT record. pub const IROH_TXT_NAME: &str = "_iroh"; @@ -349,8 +346,11 @@ impl NodeInfo { #[cfg(not(wasm_browser))] /// Parses a [`NodeInfo`] from a TXT records lookup. - pub fn from_txt_lookup(lookup: crate::dns::TxtLookup) -> Result { - let attrs = TxtAttrs::from_txt_lookup(lookup)?; + pub fn from_txt_lookup( + name: String, + lookup: impl Iterator, + ) -> Result { + let attrs = TxtAttrs::from_txt_lookup(name, lookup)?; Ok(attrs.into()) } @@ -396,16 +396,13 @@ impl std::ops::DerefMut for NodeInfo { /// [`IROH_TXT_NAME`] and the second label to be a z32 encoded [`NodeId`]. Ignores /// subsequent labels. #[cfg(not(wasm_browser))] -fn node_id_from_hickory_name(name: &hickory_resolver::proto::rr::Name) -> Option { - if name.num_labels() < 2 { - return None; - } - let mut labels = name.iter(); - let label = std::str::from_utf8(labels.next().expect("num_labels checked")).ok()?; +fn node_id_from_txt_name(name: &str) -> Option { + let mut labels = name.split("."); + let label = labels.next()?; if label != IROH_TXT_NAME { return None; } - let label = std::str::from_utf8(labels.next().expect("num_labels checked")).ok()?; + let label = labels.next()?; let node_id = NodeId::from_z32(label).ok()?; Some(node_id) } @@ -482,32 +479,6 @@ impl TxtAttrs { Ok(Self { attrs, node_id }) } - #[cfg(not(wasm_browser))] - async fn lookup(resolver: &DnsResolver, name: Name) -> Result { - let name = ensure_iroh_txt_label(name)?; - let lookup = resolver.lookup_txt(name, DNS_TIMEOUT).await?; - let attrs = Self::from_txt_lookup(lookup)?; - Ok(attrs) - } - - /// Looks up attributes by [`NodeId`] and origin domain. - #[cfg(not(wasm_browser))] - pub(crate) async fn lookup_by_id( - resolver: &DnsResolver, - node_id: &NodeId, - origin: &str, - ) -> Result { - let name = node_domain(node_id, origin)?; - TxtAttrs::lookup(resolver, name).await - } - - /// Looks up attributes by DNS name. - #[cfg(not(wasm_browser))] - pub(crate) async fn lookup_by_name(resolver: &DnsResolver, name: &str) -> Result { - let name = Name::from_str(name)?; - TxtAttrs::lookup(resolver, name).await - } - /// Returns the parsed attributes. pub(crate) fn attrs(&self) -> &BTreeMap> { &self.attrs @@ -544,43 +515,13 @@ impl TxtAttrs { /// Parses a TXT records lookup. #[cfg(not(wasm_browser))] - pub(crate) fn from_txt_lookup(lookup: crate::dns::TxtLookup) -> Result { - let queried_node_id = node_id_from_hickory_name(lookup.0.query().name()) + pub(crate) fn from_txt_lookup( + name: String, + lookup: impl Iterator, + ) -> Result { + let queried_node_id = node_id_from_txt_name(&name) .ok_or_else(|| anyhow!("invalid DNS answer: not a query for _iroh.z32encodedpubkey"))?; - - let strings = lookup.0.as_lookup().record_iter().filter_map(|record| { - match node_id_from_hickory_name(record.name()) { - // Filter out only TXT record answers that match the node_id we searched for. - Some(n) if n == queried_node_id => match record.data().as_txt() { - Some(txt) => Some(txt.to_string()), - None => { - warn!( - ?queried_node_id, - data = ?record.data(), - "unexpected record type for DNS discovery query" - ); - None - } - }, - Some(answered_node_id) => { - warn!( - ?queried_node_id, - ?answered_node_id, - "unexpected node ID answered for DNS query" - ); - None - } - None => { - warn!( - ?queried_node_id, - name = ?record.name(), - "unexpected answer record name for DNS query" - ); - None - } - } - }); - + let strings = lookup.map(|record| record.to_string()); Self::from_strings(queried_node_id, strings) } @@ -614,19 +555,18 @@ impl TxtAttrs { } #[cfg(not(wasm_browser))] -fn ensure_iroh_txt_label(name: Name) -> Result { - if name.iter().next() == Some(IROH_TXT_NAME.as_bytes()) { - Ok(name) +pub(crate) fn ensure_iroh_txt_label(name: String) -> String { + let mut parts = name.split("."); + if parts.next() == Some(IROH_TXT_NAME) { + name } else { - Name::parse(IROH_TXT_NAME, Some(&name)) + [IROH_TXT_NAME, ".", &name].join(".") } } #[cfg(not(wasm_browser))] -fn node_domain(node_id: &NodeId, origin: &str) -> Result { - let domain = format!("{}.{}", NodeId::to_z32(node_id), origin); - let domain = Name::from_str(&domain)?; - Ok(domain) +pub(crate) fn node_domain(node_id: &NodeId, origin: &str) -> String { + format!("{}.{}", NodeId::to_z32(node_id), origin) } #[cfg(test)] @@ -648,6 +588,7 @@ mod tests { use testresult::TestResult; use super::{NodeData, NodeIdExt, NodeInfo}; + use crate::dns::TxtRecord; #[test] fn txt_attr_roundtrip() { @@ -738,8 +679,11 @@ mod tests { ]; let lookup = Lookup::new_with_max_ttl(query, Arc::new(records)); let lookup = hickory_resolver::lookup::TxtLookup::from(lookup); + let lookup = lookup + .into_iter() + .map(|txt| TxtRecord::from_iter(txt.iter().cloned())); - let node_info = NodeInfo::from_txt_lookup(lookup.into())?; + let node_info = NodeInfo::from_txt_lookup(name.to_string(), lookup)?; let expected_node_info = NodeInfo::new(NodeId::from_str( "1992d53c02cdc04566e5c0edb1ce83305cd550297953a047a445ea3264b54b18", diff --git a/iroh-relay/src/server.rs b/iroh-relay/src/server.rs index ca8d4935cbc..29365fc9728 100644 --- a/iroh-relay/src/server.rs +++ b/iroh-relay/src/server.rs @@ -869,7 +869,7 @@ mod tests { } fn dns_resolver() -> DnsResolver { - DnsResolver::new() + DnsResolver::default() } #[tokio::test] diff --git a/iroh-relay/src/server/http_server.rs b/iroh-relay/src/server/http_server.rs index ca4ea755edb..248d44f8315 100644 --- a/iroh-relay/src/server/http_server.rs +++ b/iroh-relay/src/server/http_server.rs @@ -840,8 +840,8 @@ mod tests { async fn create_test_client(key: SecretKey, server_url: Url) -> Result<(PublicKey, Client)> { let public_key = key.public(); - let client = - ClientBuilder::new(server_url, key, DnsResolver::new()).insecure_skip_cert_verify(true); + let client = ClientBuilder::new(server_url, key, DnsResolver::default()) + .insecure_skip_cert_verify(true); let client = client.connect().await?; Ok((public_key, client)) diff --git a/iroh/src/dns.rs b/iroh/src/dns.rs index 260cd85ea66..7eada1d1a41 100644 --- a/iroh/src/dns.rs +++ b/iroh/src/dns.rs @@ -7,7 +7,9 @@ //! See the [`node_info`](crate::node_info) module documentation for details on how //! iroh node records are structured. -pub use iroh_relay::dns::{DnsResolver, N0_DNS_NODE_ORIGIN_PROD, N0_DNS_NODE_ORIGIN_STAGING}; +pub use iroh_relay::dns::{ + DnsResolver, Resolver, N0_DNS_NODE_ORIGIN_PROD, N0_DNS_NODE_ORIGIN_STAGING, +}; #[cfg(test)] pub(crate) mod tests { @@ -24,7 +26,7 @@ pub(crate) mod tests { #[tokio::test] #[traced_test] async fn test_dns_lookup_ipv4_ipv6() { - let resolver = DnsResolver::new(); + let resolver = DnsResolver::default(); let res: Vec<_> = resolver .lookup_ipv4_ipv6_staggered(NA_RELAY_HOSTNAME, TIMEOUT, STAGGERING_DELAYS) .await diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 21936aa2cbf..265d52ee72b 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -3455,7 +3455,6 @@ mod tests { use super::*; use crate::{ defaults::staging::{self, EU_RELAY_HOSTNAME}, - dns::DnsResolver, tls, watcher::Watcher as _, Endpoint, RelayMode, @@ -3477,7 +3476,7 @@ mod tests { node_map: None, discovery: None, proxy_url: None, - dns_resolver: DnsResolver::new(), + dns_resolver: DnsResolver::default(), server_config, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: false, @@ -4080,7 +4079,7 @@ mod tests { let mut server_config = ServerConfig::with_crypto(Arc::new(quic_server_config)); server_config.transport_config(Arc::new(quinn::TransportConfig::default())); - let dns_resolver = DnsResolver::new(); + let dns_resolver = DnsResolver::default(); let opts = Options { addr_v4: None, addr_v6: None, diff --git a/iroh/src/magicsock/relay_actor.rs b/iroh/src/magicsock/relay_actor.rs index d603724fe19..1cc89b294e0 100644 --- a/iroh/src/magicsock/relay_actor.rs +++ b/iroh/src/magicsock/relay_actor.rs @@ -1288,7 +1288,7 @@ mod tests { use tracing_test::traced_test; use super::*; - use crate::{dns::DnsResolver, test_utils}; + use crate::test_utils; #[test] fn test_packetize_iter() { @@ -1341,7 +1341,7 @@ mod tests { relay_datagrams_recv, connection_opts: RelayConnectionOptions { secret_key, - dns_resolver: DnsResolver::new(), + dns_resolver: DnsResolver::default(), proxy_url: None, prefer_ipv6: Arc::new(AtomicBool::new(true)), insecure_skip_cert_verify: true, diff --git a/iroh/src/net_report/dns.rs b/iroh/src/net_report/dns.rs index 1a46b339436..d477b5e0437 100644 --- a/iroh/src/net_report/dns.rs +++ b/iroh/src/net_report/dns.rs @@ -7,6 +7,6 @@ pub(crate) mod tests { /// Get a DNS resolver suitable for testing. pub fn resolver() -> DnsResolver { - DnsResolver::new() + DnsResolver::default() } } From 446b96e22422971ec7332ce150fd26b1cfeb2921 Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 27 May 2025 09:48:07 +0200 Subject: [PATCH 2/6] cleanups and docs --- iroh-relay/src/dns.rs | 245 +++++++++++++++++++++--------------------- iroh/src/dns.rs | 4 +- iroh/src/endpoint.rs | 2 +- 3 files changed, 122 insertions(+), 129 deletions(-) diff --git a/iroh-relay/src/dns.rs b/iroh-relay/src/dns.rs index ca6960ca20a..c90c1f1096a 100644 --- a/iroh-relay/src/dns.rs +++ b/iroh-relay/src/dns.rs @@ -27,17 +27,62 @@ 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 default 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>>; + + /// Looks up an IPv6 address. + fn lookup_ipv6(&self, host: String) -> BoxFuture>>; + + /// Looks up TXT records. + fn lookup_txt(&self, host: String) -> BoxFuture>>; + + /// Clears the internal cache. + fn clear_cache(&self); +} + +/// Boxed iterator alias. +pub type BoxIter = Box + 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)] -struct HickoryResolver(TokioResolver); +pub struct DnsResolver(Arc); -impl HickoryResolver { - /// Create a new DNS resolver with sensible cross-platform defaults. +impl std::ops::Deref for DnsResolver { + type Target = dyn Resolver; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +impl Default for DnsResolver { + fn default() -> Self { + Self::new_with_system_defaults() + } +} + +impl DnsResolver { + /// 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`. - 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(); @@ -62,11 +107,11 @@ impl HickoryResolver { let mut builder = TokioResolver::builder_with_config(config, TokioConnectionProvider::default()); *builder.options_mut() = options; - HickoryResolver(builder.build()) + Self::new(builder.build()) } - /// Create a new DNS resolver configured with a single UDP DNS nameserver. - fn with_nameserver(nameserver: SocketAddr) -> Self { + /// 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( nameserver, @@ -76,108 +121,10 @@ impl HickoryResolver { let builder = TokioResolver::builder_with_config(config, TokioConnectionProvider::default()); - HickoryResolver(builder.build()) + Self::new(builder.build()) } -} -impl Default for HickoryResolver { - fn default() -> Self { - Self::new() - } -} - -/// Trait for DNS resolvers used in iroh. -pub trait Resolver: std::fmt::Debug + Send + Sync + 'static { - /// Lookup an IPv4 address. - fn lookup_ipv4(&self, host: String) -> BoxFuture>>; - /// Lookup an IPv6 address. - fn lookup_ipv6(&self, host: String) -> BoxFuture>>; - /// Lookup TXT records. - fn lookup_txt(&self, host: String) -> BoxFuture>>; - /// Clear the internal cache. - fn clear_cache(&self); -} - -/// Boxed iterator alias. -pub type BoxIter = Box + Send + 'static>; - -impl Resolver for HickoryResolver { - fn lookup_ipv4(&self, host: String) -> BoxFuture>> { - let this = self.0.clone(); - Box::pin(async move { - let addrs = this.ipv4_lookup(host).await?; - let iter: BoxIter = Box::new(addrs.into_iter().map(Ipv4Addr::from)); - Ok(iter) - }) - } - - fn lookup_ipv6(&self, host: String) -> BoxFuture>> { - let this = self.0.clone(); - Box::pin(async move { - let addrs = this.ipv6_lookup(host).await?; - let iter: BoxIter = Box::new(addrs.into_iter().map(Ipv6Addr::from)); - Ok(iter) - }) - } - - fn lookup_txt(&self, host: String) -> BoxFuture>> { - let this = self.0.clone(); - Box::pin(async move { - let lookup = this.txt_lookup(host).await?; - let iter: BoxIter = Box::new( - lookup - .into_iter() - .map(|txt| TxtRecord::from_iter(txt.iter().cloned())), - ); - Ok(iter) - }) - } - - fn clear_cache(&self) { - self.0.clear_cache(); - } -} - -impl From for HickoryResolver { - fn from(resolver: TokioResolver) -> Self { - HickoryResolver(resolver) - } -} - -/// -#[derive(Debug, Clone)] -pub struct DnsResolver(Arc); - -impl std::ops::Deref for DnsResolver { - type Target = dyn Resolver; - fn deref(&self) -> &Self::Target { - &*self.0 - } -} - -impl Default for DnsResolver { - fn default() -> Self { - Self::new_with_system_defaults() - } -} - -impl DnsResolver { - /// - pub fn new(resolver: impl Resolver) -> Self { - Self(Arc::new(resolver)) - } - - /// - pub fn new_with_system_defaults() -> Self { - Self::new(HickoryResolver::new()) - } - - /// - pub fn with_nameserver(nameserver: SocketAddr) -> Self { - Self::new(HickoryResolver::with_nameserver(nameserver)) - } - - /// Lookup a TXT record. + /// Performs a TXT lookup with a timeout. pub async fn lookup_txt( &self, host: impl ToString, @@ -187,7 +134,7 @@ impl DnsResolver { 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, @@ -197,7 +144,7 @@ impl DnsResolver { Ok(addrs.map(|ip| IpAddr::V4(ip))) } - /// Perform an ipv6 lookup with a timeout. + /// Performs an IPv6 lookup with a timeout. pub async fn lookup_ipv6( &self, host: impl ToString, @@ -207,7 +154,7 @@ impl DnsResolver { Ok(addrs.map(|ip| IpAddr::V6(ip))) } - /// 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 @@ -233,7 +180,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, @@ -263,7 +210,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 @@ -280,7 +227,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 @@ -297,7 +244,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 @@ -367,24 +314,66 @@ impl DnsResolver { } } +/// Implementation of [`Resolver`] for [`hickory_resolver::TokioResolver`]. +impl Resolver for TokioResolver { + fn lookup_ipv4(&self, host: String) -> BoxFuture>> { + let this = self.clone(); + Box::pin(async move { + let addrs = this.ipv4_lookup(host).await?; + let iter: BoxIter = Box::new(addrs.into_iter().map(Ipv4Addr::from)); + Ok(iter) + }) + } + + fn lookup_ipv6(&self, host: String) -> BoxFuture>> { + let this = self.clone(); + Box::pin(async move { + let addrs = this.ipv6_lookup(host).await?; + let iter: BoxIter = Box::new(addrs.into_iter().map(Ipv6Addr::from)); + Ok(iter) + }) + } + + fn lookup_txt(&self, host: String) -> BoxFuture>> { + let this = self.clone(); + Box::pin(async move { + let lookup = this.txt_lookup(host).await?; + let iter: BoxIter = Box::new( + lookup + .into_iter() + .map(|txt| TxtRecord::from_iter(txt.iter().cloned())), + ); + Ok(iter) + }) + } + + fn clear_cache(&self) { + self.clear_cache(); + } +} + /// Record data for a TXT record. +/// +/// This contains a list of character strings, as defined in [RFC 1035 Section 3.3.14]. +/// +/// [`TxtRecord`] 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 seperator. +/// +/// 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 TxtRecord(Vec>); +pub struct TxtRecord(Box<[Box<[u8]>]>); impl TxtRecord { - /// + /// Returns an iterator over the character strings contained in this TXT record. pub fn iter(&self) -> impl Iterator { self.0.iter().map(|x| x.as_ref()) } - - /// - pub fn to_strings(&self) -> impl Iterator + '_ { - self.iter() - .map(|cstr| String::from_utf8_lossy(&cstr).to_string()) - } } -impl std::fmt::Display for TxtRecord { +impl fmt::Display for TxtRecord { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for s in self.iter() { write!(f, "{}", &String::from_utf8_lossy(s))? @@ -399,6 +388,12 @@ impl FromIterator> for TxtRecord { } } +impl From>> for TxtRecord { + fn from(value: Vec>) -> Self { + Self(value.into_boxed_slice()) + } +} + /// Deprecated IPv6 site-local anycast addresses still configured by windows. /// /// Windows still configures these site-local addresses as soon even as an IPv6 loopback diff --git a/iroh/src/dns.rs b/iroh/src/dns.rs index 7eada1d1a41..a74aa38f8d2 100644 --- a/iroh/src/dns.rs +++ b/iroh/src/dns.rs @@ -7,9 +7,7 @@ //! See the [`node_info`](crate::node_info) module documentation for details on how //! iroh node records are structured. -pub use iroh_relay::dns::{ - DnsResolver, Resolver, N0_DNS_NODE_ORIGIN_PROD, N0_DNS_NODE_ORIGIN_STAGING, -}; +pub use iroh_relay::dns::*; #[cfg(test)] pub(crate) mod tests { diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index ba948f1fadf..8df217d86ea 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -1991,7 +1991,7 @@ impl Connection { /// [`Connecting::handshake_data()`] succeeds. See that method's documentations for /// details on the returned value. /// - /// [`Connection::handshake_data()`]: crate::Connecting::handshake_data + /// [`Connection::handshake_data()`]: crate::endpoint::Connecting::handshake_data #[inline] pub fn handshake_data(&self) -> Option> { self.inner.handshake_data() From cdae14f20307ee0fd8baa5d2a9f27895d1e0851a Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 27 May 2025 10:17:39 +0200 Subject: [PATCH 3/6] chore: clippy --- iroh-relay/src/dns.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iroh-relay/src/dns.rs b/iroh-relay/src/dns.rs index c90c1f1096a..a295d4aca05 100644 --- a/iroh-relay/src/dns.rs +++ b/iroh-relay/src/dns.rs @@ -141,7 +141,7 @@ impl DnsResolver { timeout: Duration, ) -> Result> { let addrs = time::timeout(timeout, self.0.lookup_ipv4(host.to_string())).await??; - Ok(addrs.map(|ip| IpAddr::V4(ip))) + Ok(addrs.map(IpAddr::V4)) } /// Performs an IPv6 lookup with a timeout. @@ -151,7 +151,7 @@ impl DnsResolver { timeout: Duration, ) -> Result> { let addrs = time::timeout(timeout, self.0.lookup_ipv6(host.to_string())).await??; - Ok(addrs.map(|ip| IpAddr::V6(ip))) + Ok(addrs.map(IpAddr::V6)) } /// Resolves IPv4 and IPv6 in parallel with a timeout. From 5168b02396db2791ae0d7ce976f8e06dc0d7448c Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 27 May 2025 13:09:58 +0200 Subject: [PATCH 4/6] fixup --- iroh-relay/src/dns.rs | 2 +- iroh-relay/src/node_info.rs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/iroh-relay/src/dns.rs b/iroh-relay/src/dns.rs index a295d4aca05..a385cb9cd06 100644 --- a/iroh-relay/src/dns.rs +++ b/iroh-relay/src/dns.rs @@ -358,7 +358,7 @@ impl Resolver for TokioResolver { /// /// [`TxtRecord`] 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 seperator. +/// [`String::from_utf8_lossy`] and then concatenate all strings without a separator. /// /// If you want to process each character string individually, use [`Self::iter`]. /// diff --git a/iroh-relay/src/node_info.rs b/iroh-relay/src/node_info.rs index 3fc864f2350..82768b05ed3 100644 --- a/iroh-relay/src/node_info.rs +++ b/iroh-relay/src/node_info.rs @@ -41,13 +41,9 @@ use std::{ }; use anyhow::{anyhow, Result}; -#[cfg(not(wasm_browser))] use iroh_base::{NodeAddr, NodeId, RelayUrl, SecretKey}; -#[cfg(not(wasm_browser))] use url::Url; -#[cfg(not(wasm_browser))] - /// The DNS name for the iroh TXT record. pub const IROH_TXT_NAME: &str = "_iroh"; @@ -560,7 +556,7 @@ pub(crate) fn ensure_iroh_txt_label(name: String) -> String { if parts.next() == Some(IROH_TXT_NAME) { name } else { - [IROH_TXT_NAME, ".", &name].join(".") + format!("{}.{}", IROH_TXT_NAME, name) } } From b4ed00a5965026d08bf5d91c15be9fcc44f9d295 Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 27 May 2025 13:26:10 +0200 Subject: [PATCH 5/6] cleanup --- iroh-relay/src/dns.rs | 30 +++++++++++++----------------- iroh-relay/src/node_info.rs | 6 +++--- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/iroh-relay/src/dns.rs b/iroh-relay/src/dns.rs index a385cb9cd06..0bf14e06369 100644 --- a/iroh-relay/src/dns.rs +++ b/iroh-relay/src/dns.rs @@ -52,19 +52,6 @@ pub type BoxIter = Box + Send + 'static>; #[derive(Debug, Clone)] pub struct DnsResolver(Arc); -impl std::ops::Deref for DnsResolver { - type Target = dyn Resolver; - fn deref(&self) -> &Self::Target { - &*self.0 - } -} - -impl Default for DnsResolver { - fn default() -> Self { - Self::new_with_system_defaults() - } -} - impl DnsResolver { /// Creates a new [`DnsResolver`] from a struct that implements [`Resolver`]. /// @@ -270,16 +257,14 @@ impl DnsResolver { 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?; - let attrs = node_info::TxtAttrs::from_txt_lookup(name, lookup)?; - Ok(attrs.into()) + 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 { let name = node_info::ensure_iroh_txt_label(name.to_string()); let lookup = self.lookup_txt(name.clone(), DNS_TIMEOUT).await?; - let attrs = node_info::TxtAttrs::from_txt_lookup(name, lookup)?; - Ok(attrs.into()) + NodeInfo::from_txt_lookup(name, lookup) } /// Looks up node info by DNS name in a staggered fashion. @@ -312,6 +297,17 @@ 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_with_system_defaults() + } } /// Implementation of [`Resolver`] for [`hickory_resolver::TokioResolver`]. diff --git a/iroh-relay/src/node_info.rs b/iroh-relay/src/node_info.rs index 82768b05ed3..478fab02cdb 100644 --- a/iroh-relay/src/node_info.rs +++ b/iroh-relay/src/node_info.rs @@ -340,14 +340,14 @@ impl NodeInfo { self.into() } - #[cfg(not(wasm_browser))] /// Parses a [`NodeInfo`] from a TXT records lookup. - pub fn from_txt_lookup( + #[cfg(not(wasm_browser))] + pub(crate) fn from_txt_lookup( name: String, lookup: impl Iterator, ) -> Result { let attrs = TxtAttrs::from_txt_lookup(name, lookup)?; - Ok(attrs.into()) + Ok(Self::from(attrs)) } /// Parses a [`NodeInfo`] from a [`pkarr::SignedPacket`]. From 5c8060958927b6d00cfe3d16d1b349beda035ae4 Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 27 May 2025 13:29:30 +0200 Subject: [PATCH 6/6] cleanup more --- iroh-relay/src/dns.rs | 22 +++++++++++----------- iroh-relay/src/node_info.rs | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/iroh-relay/src/dns.rs b/iroh-relay/src/dns.rs index 0bf14e06369..d60d8a53729 100644 --- a/iroh-relay/src/dns.rs +++ b/iroh-relay/src/dns.rs @@ -36,7 +36,7 @@ pub trait Resolver: fmt::Debug + Send + Sync + 'static { fn lookup_ipv6(&self, host: String) -> BoxFuture>>; /// Looks up TXT records. - fn lookup_txt(&self, host: String) -> BoxFuture>>; + fn lookup_txt(&self, host: String) -> BoxFuture>>; /// Clears the internal cache. fn clear_cache(&self); @@ -116,7 +116,7 @@ impl DnsResolver { &self, host: impl ToString, timeout: Duration, - ) -> Result> { + ) -> Result> { let res = time::timeout(timeout, self.0.lookup_txt(host.to_string())).await??; Ok(res) } @@ -330,14 +330,14 @@ impl Resolver for TokioResolver { }) } - fn lookup_txt(&self, host: String) -> BoxFuture>> { + fn lookup_txt(&self, host: String) -> BoxFuture>> { let this = self.clone(); Box::pin(async move { let lookup = this.txt_lookup(host).await?; - let iter: BoxIter = Box::new( + let iter: BoxIter = Box::new( lookup .into_iter() - .map(|txt| TxtRecord::from_iter(txt.iter().cloned())), + .map(|txt| TxtRecordData::from_iter(txt.iter().cloned())), ); Ok(iter) }) @@ -352,7 +352,7 @@ impl Resolver for TokioResolver { /// /// This contains a list of character strings, as defined in [RFC 1035 Section 3.3.14]. /// -/// [`TxtRecord`] implements [`fmt::Display`], so you can call [`ToString::to_string`] to +/// [`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. /// @@ -360,16 +360,16 @@ impl Resolver for TokioResolver { /// /// [RFC 1035 Section 3.3.14]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 #[derive(Debug, Clone)] -pub struct TxtRecord(Box<[Box<[u8]>]>); +pub struct TxtRecordData(Box<[Box<[u8]>]>); -impl TxtRecord { +impl TxtRecordData { /// Returns an iterator over the character strings contained in this TXT record. pub fn iter(&self) -> impl Iterator { self.0.iter().map(|x| x.as_ref()) } } -impl fmt::Display for TxtRecord { +impl fmt::Display for TxtRecordData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for s in self.iter() { write!(f, "{}", &String::from_utf8_lossy(s))? @@ -378,13 +378,13 @@ impl fmt::Display for TxtRecord { } } -impl FromIterator> for TxtRecord { +impl FromIterator> for TxtRecordData { fn from_iter>>(iter: T) -> Self { Self(iter.into_iter().collect()) } } -impl From>> for TxtRecord { +impl From>> for TxtRecordData { fn from(value: Vec>) -> Self { Self(value.into_boxed_slice()) } diff --git a/iroh-relay/src/node_info.rs b/iroh-relay/src/node_info.rs index 478fab02cdb..3e8180e19e5 100644 --- a/iroh-relay/src/node_info.rs +++ b/iroh-relay/src/node_info.rs @@ -344,7 +344,7 @@ impl NodeInfo { #[cfg(not(wasm_browser))] pub(crate) fn from_txt_lookup( name: String, - lookup: impl Iterator, + lookup: impl Iterator, ) -> Result { let attrs = TxtAttrs::from_txt_lookup(name, lookup)?; Ok(Self::from(attrs)) @@ -513,7 +513,7 @@ impl TxtAttrs { #[cfg(not(wasm_browser))] pub(crate) fn from_txt_lookup( name: String, - lookup: impl Iterator, + lookup: impl Iterator, ) -> Result { let queried_node_id = node_id_from_txt_name(&name) .ok_or_else(|| anyhow!("invalid DNS answer: not a query for _iroh.z32encodedpubkey"))?; @@ -584,7 +584,7 @@ mod tests { use testresult::TestResult; use super::{NodeData, NodeIdExt, NodeInfo}; - use crate::dns::TxtRecord; + use crate::dns::TxtRecordData; #[test] fn txt_attr_roundtrip() { @@ -677,7 +677,7 @@ mod tests { let lookup = hickory_resolver::lookup::TxtLookup::from(lookup); let lookup = lookup .into_iter() - .map(|txt| TxtRecord::from_iter(txt.iter().cloned())); + .map(|txt| TxtRecordData::from_iter(txt.iter().cloned())); let node_info = NodeInfo::from_txt_lookup(name.to_string(), lookup)?;