diff --git a/iroh-gateway/src/bad_bits.rs b/iroh-gateway/src/bad_bits.rs index df4231cae3..7ab920b078 100644 --- a/iroh-gateway/src/bad_bits.rs +++ b/iroh-gateway/src/bad_bits.rs @@ -112,6 +112,7 @@ mod tests { use hex_literal::hex; use http::StatusCode; use iroh_resolver::content_loader::{FullLoader, FullLoaderConfig, GatewayUrl}; + use iroh_resolver::dns_resolver::Config as DnsResolverConfig; use iroh_rpc_client::{Client as RpcClient, Config as RpcClientConfig}; #[tokio::test] @@ -205,6 +206,7 @@ mod tests { rpc_addr, Arc::new(Some(RwLock::new(bbits))), content_loader, + DnsResolverConfig::default(), ) .await .unwrap(); diff --git a/iroh-gateway/src/client.rs b/iroh-gateway/src/client.rs index dfaa1e6b90..58bb360525 100644 --- a/iroh-gateway/src/client.rs +++ b/iroh-gateway/src/client.rs @@ -13,6 +13,7 @@ use iroh_metrics::{ gateway::{GatewayHistograms, GatewayMetrics}, observe, record, }; +use iroh_resolver::dns_resolver::Config; use iroh_resolver::{ content_loader::ContentLoader, resolver::{ @@ -90,9 +91,9 @@ impl http_body::Body for PrettyStreamBody } impl Client { - pub fn new(rpc_client: &T) -> Self { + pub fn new(rpc_client: &T, dns_resolver_config: Config) -> Self { Self { - resolver: Resolver::new(rpc_client.clone()), + resolver: Resolver::with_dns_resolver(rpc_client.clone(), dns_resolver_config), } } diff --git a/iroh-gateway/src/config.rs b/iroh-gateway/src/config.rs index 1fc2a0e932..1f7ccafd56 100644 --- a/iroh-gateway/src/config.rs +++ b/iroh-gateway/src/config.rs @@ -6,6 +6,7 @@ use headers::{ AccessControlAllowHeaders, AccessControlAllowMethods, AccessControlAllowOrigin, HeaderMapExt, }; use iroh_metrics::config::Config as MetricsConfig; +use iroh_resolver::dns_resolver::Config as DnsResolverConfig; use iroh_rpc_client::Config as RpcClientConfig; use iroh_rpc_types::{gateway::GatewayServerAddr, Addr}; use iroh_util::insert_into_config_map; @@ -29,10 +30,13 @@ pub struct Config { /// flag to toggle whether the gateway should use denylist on requests pub use_denylist: bool, /// URL of gateways to be used by the racing resolver. - /// strings can either be urls or subdomain gateway roots + /// Strings can either be urls or subdomain gateway roots /// values without https:// prefix are treated as subdomain gateways (eg: dweb.link) /// values with are treated as IPFS path gateways (eg: https://ipfs.io) pub http_resolvers: Option>, + /// Separate resolvers for particular TLDs + #[serde(default = "DnsResolverConfig::default")] + pub dns_resolver: DnsResolverConfig, /// Indexer node to use. pub indexer_endpoint: Option, /// rpc addresses for the gateway & addresses for the rpc client to dial @@ -54,6 +58,7 @@ impl Config { port, rpc_client, http_resolvers: None, + dns_resolver: DnsResolverConfig::default(), indexer_endpoint: None, metrics: MetricsConfig::default(), use_denylist: false, @@ -123,6 +128,7 @@ impl Default for Config { port: DEFAULT_PORT, rpc_client, http_resolvers: None, + dns_resolver: DnsResolverConfig::default(), indexer_endpoint: None, metrics: MetricsConfig::default(), use_denylist: false, diff --git a/iroh-gateway/src/core.rs b/iroh-gateway/src/core.rs index ffdf20419b..45898aeca9 100644 --- a/iroh-gateway/src/core.rs +++ b/iroh-gateway/src/core.rs @@ -2,6 +2,7 @@ use axum::Router; use iroh_resolver::content_loader::ContentLoader; use iroh_rpc_types::gateway::GatewayServerAddr; +use iroh_resolver::dns_resolver::Config as DnsResolverConfig; use std::{collections::HashMap, sync::Arc}; use tokio::sync::RwLock; @@ -33,6 +34,7 @@ impl Core { rpc_addr: GatewayServerAddr, bad_bits: Arc>>, content_loader: T, + dns_resolver_config: DnsResolverConfig, ) -> anyhow::Result { tokio::spawn(async move { if let Err(err) = rpc::new(rpc_addr, Gateway::default()).await { @@ -48,7 +50,7 @@ impl Core { "not_found".to_string(), templates::NOT_FOUND_TEMPLATE.to_string(), ); - let client = Client::::new(&content_loader); + let client = Client::::new(&content_loader, dns_resolver_config); Ok(Self { state: Arc::new(State { @@ -76,6 +78,7 @@ impl Core { config: Arc, bad_bits: Arc>>, content_loader: T, + dns_resolver_config: DnsResolverConfig, ) -> anyhow::Result>> { let mut templates = HashMap::new(); templates.insert( @@ -86,7 +89,7 @@ impl Core { "not_found".to_string(), templates::NOT_FOUND_TEMPLATE.to_string(), ); - let client = Client::new(&content_loader); + let client = Client::new(&content_loader, dns_resolver_config); Ok(Arc::new(State { config, client, @@ -147,9 +150,15 @@ mod tests { }; let content_loader = FullLoader::new(rpc_client.clone(), loader_config).expect("invalid config"); - let core = Core::new(config, rpc_addr, Arc::new(None), content_loader) - .await - .unwrap(); + let core = Core::new( + config, + rpc_addr, + Arc::new(None), + content_loader, + DnsResolverConfig::default(), + ) + .await + .unwrap(); let server = core.server(); let addr = server.local_addr(); let core_task = tokio::spawn(async move { diff --git a/iroh-gateway/src/main.rs b/iroh-gateway/src/main.rs index c99a74083d..655d71a75e 100644 --- a/iroh-gateway/src/main.rs +++ b/iroh-gateway/src/main.rs @@ -39,6 +39,7 @@ async fn main() -> Result<()> { println!("{:#?}", config); let metrics_config = config.metrics.clone(); + let dns_resolver_config = config.dns_resolver.clone(); let bad_bits = match config.use_denylist { true => Arc::new(Some(RwLock::new(BadBits::new()))), false => Arc::new(None), @@ -70,6 +71,7 @@ async fn main() -> Result<()> { rpc_addr, Arc::clone(&bad_bits), content_loader, + dns_resolver_config, ) .await?; diff --git a/iroh-one/src/main.rs b/iroh-one/src/main.rs index 5fa79d353c..62bf0298fa 100644 --- a/iroh-one/src/main.rs +++ b/iroh-one/src/main.rs @@ -91,6 +91,7 @@ async fn main() -> Result<()> { Arc::new(config.clone()), Arc::clone(&bad_bits), content_loader, + config.gateway.dns_resolver, ) .await?; diff --git a/iroh-resolver/Cargo.toml b/iroh-resolver/Cargo.toml index b9e9194150..8b22c12470 100644 --- a/iroh-resolver/Cargo.toml +++ b/iroh-resolver/Cargo.toml @@ -35,7 +35,7 @@ serde_json = "1.0.87" tokio = { version = "1", features = ["fs"] } tokio-util = { version = "0.7", features = ["io"] } tracing = "0.1.34" -trust-dns-resolver = { version = "0.22.0", features = ["tokio-runtime"] } +trust-dns-resolver = { version = "0.22.0", features = ["dns-over-https-rustls", "serde-config", "tokio-runtime"] } unsigned-varint = "0.7.1" [dev-dependencies] diff --git a/iroh-resolver/src/dns_resolver.rs b/iroh-resolver/src/dns_resolver.rs new file mode 100644 index 0000000000..7814d6a269 --- /dev/null +++ b/iroh-resolver/src/dns_resolver.rs @@ -0,0 +1,159 @@ +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr}; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use trust_dns_resolver::config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}; +use trust_dns_resolver::{AsyncResolver, TokioAsyncResolver}; + +use crate::resolver::Path; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct Config { + /// Mapping from TLD to the specific instance of resolver + tld_resolvers: Option>, +} + +impl Config { + pub fn empty() -> Self { + Config { + tld_resolvers: None, + } + } +} + +impl Default for Config { + fn default() -> Self { + Config { + /// Documentation on .eth TLD lives on https://eth.link/ + tld_resolvers: Some(HashMap::from_iter(vec![( + "eth".to_string(), + ResolverConfig::from_parts( + None, + vec![], + NameServerConfigGroup::from_ips_https( + &[ + IpAddr::V4(Ipv4Addr::new(104, 18, 165, 219)), + IpAddr::V4(Ipv4Addr::new(104, 18, 166, 219)), + ], + 443, + "resolver.cloudflare-eth.com".to_string(), + true, + ), + ), + )])), + } + } +} + +#[derive(Debug)] +pub struct DnsResolver { + default_resolver: TokioAsyncResolver, + tld_resolvers: Option>, +} + +impl DnsResolver { + /// Creates resolver from its config + pub fn from_config(dns_resolver_config: Config) -> DnsResolver { + let tld_resolvers = dns_resolver_config + .tld_resolvers + .map(|dns_resolver_config| { + dns_resolver_config + .into_iter() + .map(|(tld, config)| { + ( + tld, + AsyncResolver::tokio(config, ResolverOpts::default()).unwrap(), + ) + }) + .collect::>() + }); + DnsResolver { + default_resolver: AsyncResolver::tokio( + ResolverConfig::default(), + ResolverOpts::default(), + ) + .unwrap(), + tld_resolvers, + } + } + + #[tracing::instrument] + pub async fn resolve_dnslink(&self, url: &str) -> Result> { + let url = format!("_dnslink.{}.", url); + let records = self.resolve_txt_record(&url).await?; + let records = records + .into_iter() + .filter(|r| r.starts_with("dnslink=")) + .map(|r| { + let p = r.trim_start_matches("dnslink=").trim(); + p.parse() + }) + .collect::>()?; + Ok(records) + } + + pub async fn resolve_txt_record(&self, url: &str) -> Result> { + let tld = url.split('.').filter(|s| !s.is_empty()).last(); + let resolver = tld + .and_then(|tld| { + self.tld_resolvers + .as_ref() + .and_then(|tld_resolvers| tld_resolvers.get(tld)) + }) + .unwrap_or(&self.default_resolver); + let txt_response = resolver.txt_lookup(url).await?; + let out = txt_response.into_iter().map(|r| r.to_string()).collect(); + Ok(out) + } +} + +impl Default for DnsResolver { + fn default() -> Self { + DnsResolver::from_config(Config::default()) + } +} + +#[cfg(test)] +mod tests { + use super::DnsResolver; + use crate::resolver::PathType; + + #[tokio::test] + async fn test_resolve_txt_record() { + let resolver = DnsResolver::default(); + let result = resolver + .resolve_txt_record("_dnslink.ipfs.io.") + .await + .unwrap(); + assert!(!result.is_empty()); + assert_eq!(result[0], "dnslink=/ipns/website.ipfs.io"); + + let result = resolver + .resolve_txt_record("_dnslink.website.ipfs.io.") + .await + .unwrap(); + assert!(!result.is_empty()); + assert!(&result[0].starts_with("dnslink=/ipfs")); + } + + #[tokio::test] + async fn test_resolve_dnslink() { + let resolver = DnsResolver::default(); + let result = resolver.resolve_dnslink("ipfs.io").await.unwrap(); + assert!(!result.is_empty()); + assert_eq!(result[0], "/ipns/website.ipfs.io".parse().unwrap()); + + let result = resolver.resolve_dnslink("website.ipfs.io").await.unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].typ(), PathType::Ipfs); + } + + #[tokio::test] + async fn test_resolve_eth_domain() { + let resolver = DnsResolver::default(); + let result = resolver.resolve_dnslink("ipfs.eth").await.unwrap(); + assert!(!result.is_empty()); + assert_eq!(result[0].typ(), PathType::Ipfs); + } +} diff --git a/iroh-resolver/src/lib.rs b/iroh-resolver/src/lib.rs index 6c2348ce3e..2872feaa99 100644 --- a/iroh-resolver/src/lib.rs +++ b/iroh-resolver/src/lib.rs @@ -2,6 +2,7 @@ pub mod balanced_tree; pub mod chunker; pub mod codecs; pub mod content_loader; +pub mod dns_resolver; pub mod hamt; pub mod indexer; pub mod resolver; diff --git a/iroh-resolver/src/resolver.rs b/iroh-resolver/src/resolver.rs index 5a0071d7d1..056dc8247e 100644 --- a/iroh-resolver/src/resolver.rs +++ b/iroh-resolver/src/resolver.rs @@ -32,6 +32,7 @@ use iroh_metrics::{ use crate::codecs::Codec; use crate::content_loader::ContentLoader; +use crate::dns_resolver::{Config, DnsResolver}; use crate::unixfs::{ poll_read_buf_at_pos, DataType, Link, UnixfsChildStream, UnixfsContentReader, UnixfsNode, }; @@ -658,6 +659,7 @@ pub enum Source { #[derive(Debug, Clone)] pub struct Resolver { loader: T, + dns_resolver: Arc, next_id: Arc, _worker: Arc>, session_closer: async_channel::Sender, @@ -734,8 +736,11 @@ struct InnerLoaderContext { impl Resolver { pub fn new(loader: T) -> Self { - let (session_closer_s, session_closer_r) = async_channel::bounded(2048); + Self::with_dns_resolver(loader, Config::default()) + } + pub fn with_dns_resolver(loader: T, dns_resolver_config: Config) -> Self { + let (session_closer_s, session_closer_r) = async_channel::bounded(2048); let loader_thread = loader.clone(); let worker = tokio::task::spawn(async move { // GC Loop for sessions @@ -754,6 +759,7 @@ impl Resolver { Resolver { loader, + dns_resolver: Arc::new(DnsResolver::from_config(dns_resolver_config)), next_id: Arc::new(AtomicU64::new(0)), _worker: Arc::new(worker), session_closer: session_closer_s, @@ -1189,7 +1195,7 @@ impl Resolver { current = Path::from_cid(c); } CidOrDomain::Domain(ref domain) => { - let mut records = resolve_dnslink(domain).await?; + let mut records = self.dns_resolver.resolve_dnslink(domain).await?; if records.is_empty() { bail!("no valid dnslink records found for {}", domain); } @@ -1243,34 +1249,6 @@ pub fn parse_links(cid: &Cid, bytes: &[u8]) -> Result> { Ok(links) } -#[tracing::instrument] -async fn resolve_dnslink(url: &str) -> Result> { - let url = format!("_dnslink.{}.", url); - let records = resolve_txt_record(&url).await?; - let records = records - .into_iter() - .filter(|r| r.starts_with("dnslink=")) - .map(|r| { - let p = r.trim_start_matches("dnslink=").trim(); - p.parse() - }) - .collect::>()?; - Ok(records) -} - -async fn resolve_txt_record(url: &str) -> Result> { - use trust_dns_resolver::config::*; - use trust_dns_resolver::AsyncResolver; - - // Construct a new Resolver with default configuration options - let resolver = AsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default())?; - - let txt_response = resolver.txt_lookup(url).await?; - - let out = txt_response.into_iter().map(|r| r.to_string()).collect(); - Ok(out) -} - #[cfg(test)] mod tests { use std::{ @@ -2511,30 +2489,6 @@ mod tests { } } - #[tokio::test] - async fn test_resolve_txt_record() { - let result = resolve_txt_record("_dnslink.ipfs.io.").await.unwrap(); - assert!(!result.is_empty()); - assert_eq!(result[0], "dnslink=/ipns/website.ipfs.io"); - - let result = resolve_txt_record("_dnslink.website.ipfs.io.") - .await - .unwrap(); - assert!(!result.is_empty()); - assert!(&result[0].starts_with("dnslink=/ipfs")); - } - - #[tokio::test] - async fn test_resolve_dnslink() { - let result = resolve_dnslink("ipfs.io").await.unwrap(); - assert!(!result.is_empty()); - assert_eq!(result[0], "/ipns/website.ipfs.io".parse().unwrap()); - - let result = resolve_dnslink("website.ipfs.io").await.unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result[0].typ(), PathType::Ipfs); - } - #[tokio::test] async fn test_unixfs_hamt_dir() { // Test content