Skip to content

Commit 1d6b825

Browse files
authored
feat: Support eth domains
1 parent ee9677d commit 1d6b825

File tree

10 files changed

+198
-63
lines changed

10 files changed

+198
-63
lines changed

iroh-gateway/src/bad_bits.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ mod tests {
112112
use hex_literal::hex;
113113
use http::StatusCode;
114114
use iroh_resolver::content_loader::{FullLoader, FullLoaderConfig, GatewayUrl};
115+
use iroh_resolver::dns_resolver::Config as DnsResolverConfig;
115116
use iroh_rpc_client::{Client as RpcClient, Config as RpcClientConfig};
116117

117118
#[tokio::test]
@@ -205,6 +206,7 @@ mod tests {
205206
rpc_addr,
206207
Arc::new(Some(RwLock::new(bbits))),
207208
content_loader,
209+
DnsResolverConfig::default(),
208210
)
209211
.await
210212
.unwrap();

iroh-gateway/src/client.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use iroh_metrics::{
1313
gateway::{GatewayHistograms, GatewayMetrics},
1414
observe, record,
1515
};
16+
use iroh_resolver::dns_resolver::Config;
1617
use iroh_resolver::{
1718
content_loader::ContentLoader,
1819
resolver::{
@@ -90,9 +91,9 @@ impl<T: ContentLoader + std::marker::Unpin> http_body::Body for PrettyStreamBody
9091
}
9192

9293
impl<T: ContentLoader + std::marker::Unpin> Client<T> {
93-
pub fn new(rpc_client: &T) -> Self {
94+
pub fn new(rpc_client: &T, dns_resolver_config: Config) -> Self {
9495
Self {
95-
resolver: Resolver::new(rpc_client.clone()),
96+
resolver: Resolver::with_dns_resolver(rpc_client.clone(), dns_resolver_config),
9697
}
9798
}
9899

iroh-gateway/src/config.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use headers::{
66
AccessControlAllowHeaders, AccessControlAllowMethods, AccessControlAllowOrigin, HeaderMapExt,
77
};
88
use iroh_metrics::config::Config as MetricsConfig;
9+
use iroh_resolver::dns_resolver::Config as DnsResolverConfig;
910
use iroh_rpc_client::Config as RpcClientConfig;
1011
use iroh_rpc_types::{gateway::GatewayServerAddr, Addr};
1112
use iroh_util::insert_into_config_map;
@@ -29,10 +30,13 @@ pub struct Config {
2930
/// flag to toggle whether the gateway should use denylist on requests
3031
pub use_denylist: bool,
3132
/// URL of gateways to be used by the racing resolver.
32-
/// strings can either be urls or subdomain gateway roots
33+
/// Strings can either be urls or subdomain gateway roots
3334
/// values without https:// prefix are treated as subdomain gateways (eg: dweb.link)
3435
/// values with are treated as IPFS path gateways (eg: https://ipfs.io)
3536
pub http_resolvers: Option<Vec<String>>,
37+
/// Separate resolvers for particular TLDs
38+
#[serde(default = "DnsResolverConfig::default")]
39+
pub dns_resolver: DnsResolverConfig,
3640
/// Indexer node to use.
3741
pub indexer_endpoint: Option<String>,
3842
/// rpc addresses for the gateway & addresses for the rpc client to dial
@@ -54,6 +58,7 @@ impl Config {
5458
port,
5559
rpc_client,
5660
http_resolvers: None,
61+
dns_resolver: DnsResolverConfig::default(),
5762
indexer_endpoint: None,
5863
metrics: MetricsConfig::default(),
5964
use_denylist: false,
@@ -123,6 +128,7 @@ impl Default for Config {
123128
port: DEFAULT_PORT,
124129
rpc_client,
125130
http_resolvers: None,
131+
dns_resolver: DnsResolverConfig::default(),
126132
indexer_endpoint: None,
127133
metrics: MetricsConfig::default(),
128134
use_denylist: false,

iroh-gateway/src/core.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use axum::Router;
22
use iroh_resolver::content_loader::ContentLoader;
33
use iroh_rpc_types::gateway::GatewayServerAddr;
44

5+
use iroh_resolver::dns_resolver::Config as DnsResolverConfig;
56
use std::{collections::HashMap, sync::Arc};
67
use tokio::sync::RwLock;
78

@@ -33,6 +34,7 @@ impl<T: ContentLoader + std::marker::Unpin> Core<T> {
3334
rpc_addr: GatewayServerAddr,
3435
bad_bits: Arc<Option<RwLock<BadBits>>>,
3536
content_loader: T,
37+
dns_resolver_config: DnsResolverConfig,
3638
) -> anyhow::Result<Self> {
3739
tokio::spawn(async move {
3840
if let Err(err) = rpc::new(rpc_addr, Gateway::default()).await {
@@ -48,7 +50,7 @@ impl<T: ContentLoader + std::marker::Unpin> Core<T> {
4850
"not_found".to_string(),
4951
templates::NOT_FOUND_TEMPLATE.to_string(),
5052
);
51-
let client = Client::<T>::new(&content_loader);
53+
let client = Client::<T>::new(&content_loader, dns_resolver_config);
5254

5355
Ok(Self {
5456
state: Arc::new(State {
@@ -76,6 +78,7 @@ impl<T: ContentLoader + std::marker::Unpin> Core<T> {
7678
config: Arc<dyn StateConfig>,
7779
bad_bits: Arc<Option<RwLock<BadBits>>>,
7880
content_loader: T,
81+
dns_resolver_config: DnsResolverConfig,
7982
) -> anyhow::Result<Arc<State<T>>> {
8083
let mut templates = HashMap::new();
8184
templates.insert(
@@ -86,7 +89,7 @@ impl<T: ContentLoader + std::marker::Unpin> Core<T> {
8689
"not_found".to_string(),
8790
templates::NOT_FOUND_TEMPLATE.to_string(),
8891
);
89-
let client = Client::new(&content_loader);
92+
let client = Client::new(&content_loader, dns_resolver_config);
9093
Ok(Arc::new(State {
9194
config,
9295
client,
@@ -147,9 +150,15 @@ mod tests {
147150
};
148151
let content_loader =
149152
FullLoader::new(rpc_client.clone(), loader_config).expect("invalid config");
150-
let core = Core::new(config, rpc_addr, Arc::new(None), content_loader)
151-
.await
152-
.unwrap();
153+
let core = Core::new(
154+
config,
155+
rpc_addr,
156+
Arc::new(None),
157+
content_loader,
158+
DnsResolverConfig::default(),
159+
)
160+
.await
161+
.unwrap();
153162
let server = core.server();
154163
let addr = server.local_addr();
155164
let core_task = tokio::spawn(async move {

iroh-gateway/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ async fn main() -> Result<()> {
3939
println!("{:#?}", config);
4040

4141
let metrics_config = config.metrics.clone();
42+
let dns_resolver_config = config.dns_resolver.clone();
4243
let bad_bits = match config.use_denylist {
4344
true => Arc::new(Some(RwLock::new(BadBits::new()))),
4445
false => Arc::new(None),
@@ -70,6 +71,7 @@ async fn main() -> Result<()> {
7071
rpc_addr,
7172
Arc::clone(&bad_bits),
7273
content_loader,
74+
dns_resolver_config,
7375
)
7476
.await?;
7577

iroh-one/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ async fn main() -> Result<()> {
9191
Arc::new(config.clone()),
9292
Arc::clone(&bad_bits),
9393
content_loader,
94+
config.gateway.dns_resolver,
9495
)
9596
.await?;
9697

iroh-resolver/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ serde_json = "1.0.87"
3535
tokio = { version = "1", features = ["fs"] }
3636
tokio-util = { version = "0.7", features = ["io"] }
3737
tracing = "0.1.34"
38-
trust-dns-resolver = { version = "0.22.0", features = ["tokio-runtime"] }
38+
trust-dns-resolver = { version = "0.22.0", features = ["dns-over-https-rustls", "serde-config", "tokio-runtime"] }
3939
unsigned-varint = "0.7.1"
4040

4141
[dev-dependencies]

iroh-resolver/src/dns_resolver.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use std::collections::HashMap;
2+
use std::net::{IpAddr, Ipv4Addr};
3+
4+
use anyhow::Result;
5+
use serde::{Deserialize, Serialize};
6+
use trust_dns_resolver::config::{NameServerConfigGroup, ResolverConfig, ResolverOpts};
7+
use trust_dns_resolver::{AsyncResolver, TokioAsyncResolver};
8+
9+
use crate::resolver::Path;
10+
11+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
12+
pub struct Config {
13+
/// Mapping from TLD to the specific instance of resolver
14+
tld_resolvers: Option<HashMap<String, ResolverConfig>>,
15+
}
16+
17+
impl Config {
18+
pub fn empty() -> Self {
19+
Config {
20+
tld_resolvers: None,
21+
}
22+
}
23+
}
24+
25+
impl Default for Config {
26+
fn default() -> Self {
27+
Config {
28+
/// Documentation on .eth TLD lives on https://eth.link/
29+
tld_resolvers: Some(HashMap::from_iter(vec![(
30+
"eth".to_string(),
31+
ResolverConfig::from_parts(
32+
None,
33+
vec![],
34+
NameServerConfigGroup::from_ips_https(
35+
&[
36+
IpAddr::V4(Ipv4Addr::new(104, 18, 165, 219)),
37+
IpAddr::V4(Ipv4Addr::new(104, 18, 166, 219)),
38+
],
39+
443,
40+
"resolver.cloudflare-eth.com".to_string(),
41+
true,
42+
),
43+
),
44+
)])),
45+
}
46+
}
47+
}
48+
49+
#[derive(Debug)]
50+
pub struct DnsResolver {
51+
default_resolver: TokioAsyncResolver,
52+
tld_resolvers: Option<HashMap<String, TokioAsyncResolver>>,
53+
}
54+
55+
impl DnsResolver {
56+
/// Creates resolver from its config
57+
pub fn from_config(dns_resolver_config: Config) -> DnsResolver {
58+
let tld_resolvers = dns_resolver_config
59+
.tld_resolvers
60+
.map(|dns_resolver_config| {
61+
dns_resolver_config
62+
.into_iter()
63+
.map(|(tld, config)| {
64+
(
65+
tld,
66+
AsyncResolver::tokio(config, ResolverOpts::default()).unwrap(),
67+
)
68+
})
69+
.collect::<HashMap<_, _>>()
70+
});
71+
DnsResolver {
72+
default_resolver: AsyncResolver::tokio(
73+
ResolverConfig::default(),
74+
ResolverOpts::default(),
75+
)
76+
.unwrap(),
77+
tld_resolvers,
78+
}
79+
}
80+
81+
#[tracing::instrument]
82+
pub async fn resolve_dnslink(&self, url: &str) -> Result<Vec<Path>> {
83+
let url = format!("_dnslink.{}.", url);
84+
let records = self.resolve_txt_record(&url).await?;
85+
let records = records
86+
.into_iter()
87+
.filter(|r| r.starts_with("dnslink="))
88+
.map(|r| {
89+
let p = r.trim_start_matches("dnslink=").trim();
90+
p.parse()
91+
})
92+
.collect::<Result<_>>()?;
93+
Ok(records)
94+
}
95+
96+
pub async fn resolve_txt_record(&self, url: &str) -> Result<Vec<String>> {
97+
let tld = url.split('.').filter(|s| !s.is_empty()).last();
98+
let resolver = tld
99+
.and_then(|tld| {
100+
self.tld_resolvers
101+
.as_ref()
102+
.and_then(|tld_resolvers| tld_resolvers.get(tld))
103+
})
104+
.unwrap_or(&self.default_resolver);
105+
let txt_response = resolver.txt_lookup(url).await?;
106+
let out = txt_response.into_iter().map(|r| r.to_string()).collect();
107+
Ok(out)
108+
}
109+
}
110+
111+
impl Default for DnsResolver {
112+
fn default() -> Self {
113+
DnsResolver::from_config(Config::default())
114+
}
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use super::DnsResolver;
120+
use crate::resolver::PathType;
121+
122+
#[tokio::test]
123+
async fn test_resolve_txt_record() {
124+
let resolver = DnsResolver::default();
125+
let result = resolver
126+
.resolve_txt_record("_dnslink.ipfs.io.")
127+
.await
128+
.unwrap();
129+
assert!(!result.is_empty());
130+
assert_eq!(result[0], "dnslink=/ipns/website.ipfs.io");
131+
132+
let result = resolver
133+
.resolve_txt_record("_dnslink.website.ipfs.io.")
134+
.await
135+
.unwrap();
136+
assert!(!result.is_empty());
137+
assert!(&result[0].starts_with("dnslink=/ipfs"));
138+
}
139+
140+
#[tokio::test]
141+
async fn test_resolve_dnslink() {
142+
let resolver = DnsResolver::default();
143+
let result = resolver.resolve_dnslink("ipfs.io").await.unwrap();
144+
assert!(!result.is_empty());
145+
assert_eq!(result[0], "/ipns/website.ipfs.io".parse().unwrap());
146+
147+
let result = resolver.resolve_dnslink("website.ipfs.io").await.unwrap();
148+
assert_eq!(result.len(), 1);
149+
assert_eq!(result[0].typ(), PathType::Ipfs);
150+
}
151+
152+
#[tokio::test]
153+
async fn test_resolve_eth_domain() {
154+
let resolver = DnsResolver::default();
155+
let result = resolver.resolve_dnslink("ipfs.eth").await.unwrap();
156+
assert!(!result.is_empty());
157+
assert_eq!(result[0].typ(), PathType::Ipfs);
158+
}
159+
}

iroh-resolver/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod balanced_tree;
22
pub mod chunker;
33
pub mod codecs;
44
pub mod content_loader;
5+
pub mod dns_resolver;
56
pub mod hamt;
67
pub mod indexer;
78
pub mod resolver;

0 commit comments

Comments
 (0)