diff --git a/Cargo.lock b/Cargo.lock index 9bf1cb5c85..c57fe9f62b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2521,6 +2521,7 @@ dependencies = [ "iroh-net", "iroh-test", "lru", + "mainline", "parking_lot", "pkarr", "rcgen 0.12.1", diff --git a/iroh-dns-server/Cargo.toml b/iroh-dns-server/Cargo.toml index 0fb9b2a1d5..4a14e7095d 100644 --- a/iroh-dns-server/Cargo.toml +++ b/iroh-dns-server/Cargo.toml @@ -54,3 +54,4 @@ z32 = "1.1.1" hickory-resolver = "0.24.0" iroh-net = { version = "0.14.0", path = "../iroh-net" } iroh-test = { path = "../iroh-test" } +mainline = "<1.5.0" diff --git a/iroh-dns-server/src/config.rs b/iroh-dns-server/src/config.rs index 1041f62a72..a377f28780 100644 --- a/iroh-dns-server/src/config.rs +++ b/iroh-dns-server/src/config.rs @@ -69,12 +69,16 @@ impl MetricsConfig { pub struct MainlineConfig { /// Set to true to enable the mainline lookup. pub enabled: bool, + /// Set custom bootstrap nodes. + /// + /// If empty this will use the default bittorrent mainline bootstrap nodes as defined by pkarr. + pub bootstrap: Vec, } #[allow(clippy::derivable_impls)] impl Default for MainlineConfig { fn default() -> Self { - Self { enabled: false } + Self { enabled: false, bootstrap: vec![]} } } @@ -121,11 +125,12 @@ impl Config { } } - pub(crate) fn mainline_enabled(&self) -> bool { - self.mainline - .as_ref() - .map(|x| x.enabled) - .unwrap_or_default() + pub(crate) fn mainline_enabled(&self) -> Option<&Vec> { + match self.mainline.as_ref() { + None => None, + Some(config) if !config.enabled => None, + Some(config) => Some(&config.bootstrap), + } } } diff --git a/iroh-dns-server/src/lib.rs b/iroh-dns-server/src/lib.rs index bb62a969e9..86639ee85e 100644 --- a/iroh-dns-server/src/lib.rs +++ b/iroh-dns-server/src/lib.rs @@ -13,7 +13,7 @@ mod util; #[cfg(test)] mod tests { - use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; + use std::{net::{Ipv4Addr, Ipv6Addr, SocketAddr}, str::FromStr}; use anyhow::Result; use hickory_resolver::{ @@ -28,7 +28,7 @@ mod tests { }, key::SecretKey, }; - use pkarr::SignedPacket; + use pkarr::{PkarrClient, SignedPacket}; use url::Url; use crate::server::Server; @@ -177,6 +177,48 @@ mod tests { Ok(()) } + #[tokio::test] + async fn integration_mainline() -> Result<()> { + iroh_test::logging::setup_multithreaded(); + + // run a mainline testnet + let testnet = mainline::dht::Testnet::new(5); + let bootstrap = testnet + .bootstrap + .iter() + .map(|addr| SocketAddr::from_str(addr).unwrap()) + .collect::>(); + + // spawn our server with mainline support + let (server, nameserver, _http_url) = Server::spawn_for_tests_with_mainline(Some(bootstrap)).await?; + + let origin = "irohdns.example."; + + // create a signed packet + let secret_key = SecretKey::generate(); + let node_id = secret_key.public(); + let relay_url: Url = "https://relay.example.".parse()?; + let node_info = NodeInfo::new(node_id, Some(relay_url.clone())); + let signed_packet = node_info.to_pkarr_signed_packet(&secret_key, 30)?; + + // publish the signed packet to our DHT + let pkarr = PkarrClient::builder().bootstrap(&testnet.bootstrap).build(); + pkarr.publish(&signed_packet).await?; + + // resolve via DNS from our server, which will lookup from our DHT + let resolver = test_resolver(nameserver); + let res = lookup_by_id(&resolver, &node_id, origin).await?; + + assert_eq!(res.node_id, node_id); + assert_eq!(res.info.relay_url.map(Url::from), Some(relay_url)); + + server.shutdown().await?; + for node in testnet.nodes { + node.shutdown(); + } + Ok(()) + } + fn test_resolver(nameserver: SocketAddr) -> DnsResolver { let mut config = ResolverConfig::new(); let nameserver_config = NameServerConfig::new(nameserver, Protocol::Udp); diff --git a/iroh-dns-server/src/server.rs b/iroh-dns-server/src/server.rs index 0669cb6f48..bfb1076fcd 100644 --- a/iroh-dns-server/src/server.rs +++ b/iroh-dns-server/src/server.rs @@ -1,4 +1,5 @@ //! The main server which combines the DNS and HTTP(S) servers. + use anyhow::Result; use iroh_metrics::metrics::start_metrics_server; use tracing::info; @@ -14,9 +15,9 @@ use crate::{ /// Spawn the server and run until the `Ctrl-C` signal is received, then shutdown. pub async fn run_with_config_until_ctrl_c(config: Config) -> Result<()> { let mut store = ZoneStore::persistent(Config::signed_packet_store_path()?)?; - if config.mainline_enabled() { + if let Some(bootstrap) = config.mainline_enabled() { info!("mainline fallback enabled"); - store = store.with_pkarr(Default::default()); + store = store.with_mainline_fallback(Some(bootstrap)); }; let server = Server::spawn(config, store).await?; tokio::signal::ctrl_c().await?; @@ -89,7 +90,16 @@ impl Server { /// HTTP server. #[cfg(test)] pub async fn spawn_for_tests() -> Result<(Self, std::net::SocketAddr, url::Url)> { - use crate::config::MetricsConfig; + Self::spawn_for_tests_with_mainline(None).await + } + + /// Spawn a server suitable for testing, while optionally enabling mainline with custom + /// bootstrap addresses. + #[cfg(test)] + pub async fn spawn_for_tests_with_mainline( + bootstrap: Option>, + ) -> Result<(Self, std::net::SocketAddr, url::Url)> { + use crate::config::{MainlineConfig, MetricsConfig}; use std::net::{IpAddr, Ipv4Addr}; let mut config = Config::default(); @@ -99,8 +109,16 @@ impl Server { config.http.as_mut().unwrap().bind_addr = Some(IpAddr::V4(Ipv4Addr::LOCALHOST)); config.https = None; config.metrics = Some(MetricsConfig::disabled()); + config.mainline = bootstrap.map(|bootstrap| MainlineConfig { + enabled: true, + bootstrap, + }); - let store = ZoneStore::in_memory()?; + let mut store = ZoneStore::in_memory()?; + if let Some(bootstrap) = config.mainline_enabled() { + info!("mainline fallback enabled"); + store = store.with_mainline_fallback(Some(bootstrap)); + }; let server = Self::spawn(config, store).await?; let dns_addr = server.dns_server.local_addr(); let http_addr = server.http_server.http_addr().expect("http is set"); diff --git a/iroh-dns-server/src/store.rs b/iroh-dns-server/src/store.rs index 7c7ede9f7e..ff36402240 100644 --- a/iroh-dns-server/src/store.rs +++ b/iroh-dns-server/src/store.rs @@ -1,6 +1,9 @@ //! Pkarr packet store used to resolve DNS queries. -use std::{collections::BTreeMap, num::NonZeroUsize, path::Path, sync::Arc, time::Duration}; +use std::{ + collections::BTreeMap, net::SocketAddr, num::NonZeroUsize, path::Path, sync::Arc, + time::Duration, +}; use anyhow::Result; use hickory_proto::rr::{Name, RecordSet, RecordType, RrKey}; @@ -55,12 +58,29 @@ impl ZoneStore { Ok(Self::new(packet_store)) } - /// Configure a pkarr client for resolution of packets from the bittorrent - /// mainline DHT. + /// Configure a pkarr client for resolution of packets from the bittorent mainline DHT. /// /// This will be used only as a fallback if there is no local info available. - pub fn with_pkarr(self, pkarr: Option>) -> Self { - Self { pkarr, ..self } + /// + /// Optionally set custom bootstrap nodes. If `bootstrap` is empty it will use the default + /// mainline bootstrap nodes. + pub fn with_mainline_fallback(self, bootstrap: Option<&Vec>) -> Self { + let pkarr_client = match bootstrap { + None => PkarrClient::default(), + Some(addrs) if addrs.is_empty() => PkarrClient::default(), + Some(addrs) => PkarrClient::builder() + .bootstrap( + &addrs + .iter() + .map(|addr| addr.to_string()) + .collect::>(), + ) + .build(), + }; + Self { + pkarr: Some(Arc::new(pkarr_client)), + ..self + } } /// Create a new zone store.