From be4fd78546a7bc936fad2ef91c805bc71a51dcec Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 7 Mar 2024 17:05:48 +0100 Subject: [PATCH] refactor: move iroh-dns into iroh-net, add docs, cleanup --- Cargo.lock | 42 +- Cargo.toml | 1 - iroh-dns/Cargo.toml | 38 -- iroh-dns/examples/publish.rs | 91 ----- iroh-dns/examples/resolve.rs | 59 --- iroh-dns/src/discovery.rs | 69 ---- iroh-dns/src/lib.rs | 18 - iroh-dns/src/packet.rs | 364 ------------------ iroh-dns/src/publish.rs | 86 ----- iroh-dns/src/resolve.rs | 117 ------ iroh-net/Cargo.toml | 4 + iroh-net/src/discovery.rs | 3 + iroh-net/src/discovery/dns.rs | 58 +++ iroh-net/src/discovery/pkarr_relay_publish.rs | 113 ++++++ iroh-net/src/dns.rs | 11 +- iroh-net/src/dns/node_info.rs | 249 ++++++++++++ iroh-net/src/lib.rs | 2 +- iroh/Cargo.toml | 1 - iroh/src/commands/start.rs | 11 +- 19 files changed, 451 insertions(+), 886 deletions(-) delete mode 100644 iroh-dns/Cargo.toml delete mode 100644 iroh-dns/examples/publish.rs delete mode 100644 iroh-dns/examples/resolve.rs delete mode 100644 iroh-dns/src/discovery.rs delete mode 100644 iroh-dns/src/lib.rs delete mode 100644 iroh-dns/src/packet.rs delete mode 100644 iroh-dns/src/publish.rs delete mode 100644 iroh-dns/src/resolve.rs create mode 100644 iroh-net/src/discovery/dns.rs create mode 100644 iroh-net/src/discovery/pkarr_relay_publish.rs create mode 100644 iroh-net/src/dns/node_info.rs diff --git a/Cargo.lock b/Cargo.lock index 0afe6c70093..ed46f63a41c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1784,29 +1784,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "091a6fbccf4860009355e3efc52ff4acf37a63489aad7435372d44ceeb6fbbcf" dependencies = [ "async-trait", - "bytes", "cfg-if", "data-encoding", "enum-as-inner", "futures-channel", "futures-io", "futures-util", - "h2", - "http 0.2.11", "idna 0.4.0", "ipnet", "once_cell", "rand", - "ring 0.16.20", - "rustls", - "rustls-pemfile", "thiserror", "tinyvec", "tokio", - "tokio-rustls", "tracing", "url", - "webpki-roots", ] [[package]] @@ -1824,13 +1816,10 @@ dependencies = [ "parking_lot", "rand", "resolv-conf", - "rustls", "smallvec", "thiserror", "tokio", - "tokio-rustls", "tracing", - "webpki-roots", ] [[package]] @@ -2237,7 +2226,6 @@ dependencies = [ "indicatif", "iroh-base", "iroh-bytes", - "iroh-dns", "iroh-gossip", "iroh-io", "iroh-metrics", @@ -2367,32 +2355,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "iroh-dns" -version = "0.12.0" -dependencies = [ - "anyhow", - "clap", - "derive_more", - "ed25519-dalek", - "futures", - "hex", - "hickory-proto", - "hickory-resolver", - "iroh-base", - "iroh-net", - "parking_lot", - "pkarr", - "reqwest", - "ring 0.16.20", - "rustls", - "tokio", - "tracing", - "tracing-subscriber", - "url", - "z32", -] - [[package]] name = "iroh-gossip" version = "0.12.0" @@ -2474,10 +2436,12 @@ dependencies = [ "der", "derive_more", "duct", + "ed25519-dalek", "flume", "futures", "governor", "hex", + "hickory-proto", "hickory-resolver", "hostname", "http 1.0.0", @@ -2496,6 +2460,7 @@ dependencies = [ "num_enum", "once_cell", "parking_lot", + "pkarr", "postcard", "pretty_assertions", "proptest", @@ -2539,6 +2504,7 @@ dependencies = [ "windows 0.51.1", "wmi", "x509-parser", + "z32", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d1cdd26c07b..751f092c909 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ members = [ "iroh", "iroh-bytes", "iroh-base", - "iroh-dns", "iroh-gossip", "iroh-metrics", "iroh-net", diff --git a/iroh-dns/Cargo.toml b/iroh-dns/Cargo.toml deleted file mode 100644 index 251d4907a3a..00000000000 --- a/iroh-dns/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "iroh-dns" -version = "0.12.0" -edition = "2021" -readme = "README.md" -description = "DNS resolver and pkarr publisher for iroh" -license = "MIT OR Apache-2.0" -authors = ["n0 team"] -repository = "https://github.com/n0-computer/iroh" -keywords = ["networking", "p2p", "holepunching", "ipfs"] - -# Sadly this also needs to be updated in .github/workflows/ci.yml -rust-version = "1.75" - -[dependencies] -anyhow = "1.0.80" -derive_more = { version = "1.0.0-beta.1", features = ["debug", "display"] } -ed25519-dalek = { version = "2.1.1", features = ["pkcs8"] } -futures = "0.3.30" -hex = "0.4.3" -hickory-proto = { version = "0.24.0", features = ["dnssec", "ring"] } -hickory-resolver = { version = "0.24.0", features = ["dns-over-https", "dns-over-tls", "tokio-rustls", "webpki-roots", "dns-over-rustls", "dns-over-https-rustls"] } -iroh-base = { version = "0.12.0", path = "../iroh-base", default_features = false, features = ["base32"] } -iroh-net = { version = "0.12.0", path = "../iroh-net", default_features = false } -parking_lot = "0.12.1" -pkarr = { version = "1.1.1", features = ["async", "relay"], default_features = false } -reqwest = { version = "0.11.24", default_features = false, features = ["rustls-tls"] } -ring = "0.16" -rustls = "0.21" -tokio = { version = "1", features = ["rt", "sync"] } -tracing = "0.1" -url = { version = "2", features = ["serde"] } -z32 = "1.0.3" - -[dev-dependencies] -clap = { version = "4.5.1", features = ["derive"] } -tokio = { version = "1", features = ["full"] } -tracing-subscriber = "0.3" diff --git a/iroh-dns/examples/publish.rs b/iroh-dns/examples/publish.rs deleted file mode 100644 index 9faf5181524..00000000000 --- a/iroh-dns/examples/publish.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::str::FromStr; - -use anyhow::{bail, Result}; -use clap::{Parser, ValueEnum}; -use iroh_net::{key::SecretKey, AddrInfo, NodeId}; -use url::Url; - -use iroh_dns::{ - packet::IROH_NODE_TXT_LABEL, - publish::{Config, Publisher}, - resolve::{EXAMPLE_DOMAIN, IROH_TEST_DOMAIN}, - to_z32, -}; - -#[derive(ValueEnum, Clone, Debug, Default, Copy)] -pub enum Env { - /// Use the irohdns test server at testdns.iroh.link - #[default] - IrohTest, - /// Use a relay listening at localhost:8080 - LocalDev, -} - -/// Publish a record to an irohdns server. -/// -/// You have to set the IROH_SECRET environment variable to the node secret for which to publish. -#[derive(Parser, Debug)] -struct Cli { - /// Environment to publish to. - #[clap(value_enum, short, long, default_value_t = Env::IrohTest)] - env: Env, - /// Relay URL. If set, the --env option will be ignored. - #[clap(short, long, conflicts_with = "env")] - relay: Option, - /// Home Derp server to publish for this node - #[clap(short, long)] - derp_url: Url, - /// Create a new node secret if IROH_SECRET is unset. Only for development / debugging. - #[clap(short, long)] - create: bool, -} - -#[tokio::main] -async fn main() -> Result<()> { - tracing_subscriber::fmt::init(); - let args = Cli::parse(); - let secret_key = match std::env::var("IROH_SECRET") { - Ok(s) => SecretKey::from_str(&s)?, - Err(_) if args.create => { - let s = SecretKey::generate(); - println!("Generated a new node secret. To reuse, set"); - println!("IROH_SECRET={s}"); - s - } - Err(_) => { - bail!("Environtment variable IROH_SECRET is not set. To create a new secret, use the --create option.") - } - }; - let node_id = secret_key.public(); - println!("node: {node_id}"); - println!("derp: {}", args.derp_url); - let config = match (args.relay, args.env) { - (Some(pkarr_relay), _) => Config::new(secret_key, pkarr_relay), - (None, Env::IrohTest) => Config::with_iroh_test(secret_key), - (None, Env::LocalDev) => Config::localhost_dev(secret_key), - }; - let publisher = Publisher::new(config); - - let info = AddrInfo { - derp_url: Some(args.derp_url.into()), - direct_addresses: Default::default(), - }; - // let an = NodeAnnounce::new(node_id, Some(args.home_derp), vec![]); - publisher.publish_addr_info(&info).await?; - println!( - "published signed record to {}! Resolve with ", - publisher.pkarr_relay() - ); - match args.env { - Env::IrohTest => println!("dig {} TXT", node_domain(&node_id, IROH_TEST_DOMAIN)), - Env::LocalDev => println!( - "dig @localhost -p 5353 {} TXT", - node_domain(&node_id, EXAMPLE_DOMAIN) - ), - } - Ok(()) -} - -fn node_domain(node_id: &NodeId, origin: &str) -> String { - format!("{}.{}.{}", IROH_NODE_TXT_LABEL, to_z32(node_id), origin) -} diff --git a/iroh-dns/examples/resolve.rs b/iroh-dns/examples/resolve.rs deleted file mode 100644 index 91a7fdcd3e2..00000000000 --- a/iroh-dns/examples/resolve.rs +++ /dev/null @@ -1,59 +0,0 @@ -use clap::Parser; -use clap::ValueEnum; -use iroh_dns::resolve::{Config, Resolver}; -use iroh_net::NodeId; - -#[derive(ValueEnum, Clone, Debug, Default)] -pub enum Env { - /// Use cloudflare and the irohdns test server at testdns.iroh.link - #[default] - IrohTest, - /// Use a localhost domain server listening on port 5353 - LocalDev, -} - -#[derive(Debug, Parser)] -struct Cli { - #[clap(value_enum, short, long, default_value_t = Env::IrohTest)] - env: Env, - #[clap(subcommand)] - command: Command, -} - -#[derive(Debug, Parser)] -enum Command { - /// Resolve node info by node id. - Node { node_id: NodeId }, - /// Resolve node info by domain. - Domain { domain: String }, -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let args = Cli::parse(); - let config = match args.env { - Env::IrohTest => Config::with_cloudflare_and_iroh_test(), - Env::LocalDev => Config::localhost_dev(), - }; - let resolver = Resolver::new(config)?; - match args.command { - Command::Node { node_id } => { - let addr = resolver.resolve_node_by_id(node_id).await?; - let derp_url = addr.derp_url.map(|u| u.to_string()).unwrap_or_default(); - println!("node_id: {node_id}"); - println!("derp_url: {derp_url}"); - } - Command::Domain { domain } => { - let addr = resolver.resolve_node_by_domain(&domain).await?; - let node_id = addr.node_id; - let derp_url = addr - .info - .derp_url - .map(|u| u.to_string()) - .unwrap_or_default(); - println!("node_id: {node_id}"); - println!("derp_url: {derp_url}"); - } - } - Ok(()) -} diff --git a/iroh-dns/src/discovery.rs b/iroh-dns/src/discovery.rs deleted file mode 100644 index e1ba53d0ec9..00000000000 --- a/iroh-dns/src/discovery.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use futures::{future::FutureExt, stream::BoxStream, StreamExt}; -use iroh_net::{ - discovery::{Discovery, DiscoveryItem}, - key::SecretKey, - AddrInfo, MagicEndpoint, NodeId, -}; -use tracing::warn; - -use crate::publish::{self, Publisher}; -use crate::resolve::{self, Resolver}; - -#[derive(Debug)] -pub struct DnsDiscovery { - publisher: Option>, - resolver: Resolver, -} - -impl DnsDiscovery { - pub fn new(resolver: Resolver, publisher: Option>) -> Self { - Self { - resolver, - publisher, - } - } - pub fn with_iroh_test(secret_key: Option) -> Result { - let publisher = - secret_key.map(|k| Arc::new(Publisher::new(publish::Config::with_iroh_test(k)))); - let resolver = Resolver::new(resolve::Config::with_cloudflare_and_iroh_test())?; - Ok(Self::new(resolver, publisher)) - } - pub fn localhost_dev(secret_key: Option) -> Result { - let publisher = - secret_key.map(|k| Arc::new(Publisher::new(publish::Config::localhost_dev(k)))); - let resolver = Resolver::new(resolve::Config::localhost_dev())?; - Ok(Self::new(resolver, publisher)) - } -} - -impl Discovery for DnsDiscovery { - fn publish(&self, info: &AddrInfo) { - if let Some(publisher) = self.publisher.clone() { - let info = info.clone(); - tokio::task::spawn(async move { - if let Err(err) = publisher.publish_addr_info(&info).await { - warn!("failed to publish address update: {err:?}"); - } - }); - } - } - - fn resolve<'a>( - &'a self, - _ep: MagicEndpoint, - node_id: NodeId, - ) -> Option>> { - let fut = async move { - let addr_info = self.resolver.resolve_node_by_id(node_id).await?; - Ok(DiscoveryItem { - provenance: "iroh-dns", - last_updated: None, - addr_info, - }) - }; - Some(fut.into_stream().boxed()) - } -} diff --git a/iroh-dns/src/lib.rs b/iroh-dns/src/lib.rs deleted file mode 100644 index 915a69e2864..00000000000 --- a/iroh-dns/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use anyhow::{anyhow, Result}; -use iroh_net::NodeId; - -pub mod discovery; -pub mod packet; -pub mod publish; -pub mod resolve; - -pub fn to_z32(node_id: &NodeId) -> String { - z32::encode(node_id.as_bytes()) -} - -pub fn from_z32(s: &str) -> Result { - let bytes = z32::decode(s.as_bytes()).map_err(|_| anyhow!("invalid z32"))?; - let bytes: &[u8; 32] = &bytes.try_into().map_err(|_| anyhow!("not 32 bytes long"))?; - let node_id = NodeId::from_bytes(bytes)?; - Ok(node_id) -} diff --git a/iroh-dns/src/packet.rs b/iroh-dns/src/packet.rs deleted file mode 100644 index aae1aa66ea3..00000000000 --- a/iroh-dns/src/packet.rs +++ /dev/null @@ -1,364 +0,0 @@ -use std::{collections::HashMap, fmt::Display, str::FromStr}; - -// use hickory_proto::rr::Name; -use anyhow::{anyhow, bail, Result}; -use hickory_proto::error::ProtoError; -use iroh_net::{AddrInfo, NodeAddr, NodeId}; -use url::Url; - -use crate::from_z32; - -pub const IROH_ROOT_ZONE: &str = "iroh"; -pub const IROH_NODE_TXT_LABEL: &str = "_iroh_node"; -pub const DEFAULT_TTL: u32 = 30; - -pub const ATTR_DERP: &str = "derp"; -pub const ATTR_NODE_ID: &str = "node"; -pub const ATTR_DNS: &str = "dns"; - -#[derive(derive_more::Debug, Clone, Eq, PartialEq)] -pub struct NodeAnnounce { - pub node_id: NodeId, - #[debug("{:?}", self.home_derp.as_ref().map(|s| s.to_string()))] - pub home_derp: Option, - pub home_dns: Vec, -} - -impl From for NodeAddr { - fn from(value: NodeAnnounce) -> Self { - NodeAddr { - node_id: value.node_id, - info: value.into(), - } - } -} - -impl From for AddrInfo { - fn from(value: NodeAnnounce) -> Self { - AddrInfo { - derp_url: value.home_derp.map(|u| u.into()), - direct_addresses: Default::default(), - } - } -} - -impl NodeAnnounce { - pub fn new(node_id: NodeId, derp: Option, dns: Vec) -> Self { - Self { - node_id, - home_derp: derp, - home_dns: dns, - } - } - - pub fn to_attr_string(&self) -> String { - let mut attrs = vec![]; - attrs.push(fmt_attr(ATTR_NODE_ID, self.node_id)); - if let Some(derp) = &self.home_derp { - attrs.push(fmt_attr(ATTR_DERP, derp)); - } - for dns in &self.home_dns { - attrs.push(fmt_attr(ATTR_DNS, dns)); - } - attrs.join(" ") - } - - pub fn zone(&self, absolute: bool) -> String { - match absolute { - true => format!("{}.{}.", self.node_id, IROH_ROOT_ZONE), - false => format!("{}.{}", self.node_id, IROH_ROOT_ZONE), - } - } - - pub fn hickory_zone(&self, absolute: bool) -> Result { - hickory_proto::rr::Name::from_str(&self.zone(absolute)) - } - - pub fn into_hickory_answers_message(&self) -> Result { - use hickory_proto::op; - let record = self.into_hickory_dns_record()?; - let mut packet = op::Message::new(); - packet.answers_mut().push(record); - Ok(packet) - } - - pub fn into_hickory_update_message(&self) -> Result { - use hickory_proto::{op, rr}; - let record = self.into_hickory_dns_record()?; - let zone = rr::Name::from_str(&self.zone(true))?; - let message = op::update_message::create(record.into(), zone, false); - Ok(message) - } - - pub fn into_hickory_dns_record(&self) -> Result { - use hickory_proto::rr; - let origin = rr::Name::from_str(IROH_ROOT_ZONE)?; - self.into_hickory_dns_record_with_origin(&origin) - } - - pub fn into_hickory_dns_record_with_origin( - &self, - origin: &hickory_proto::rr::Name, - ) -> Result { - use hickory_proto::rr; - let zone = rr::Name::from_str(&self.node_id.to_string())?; - let zone = zone.append_domain(origin)?; - let name = rr::Name::parse(IROH_NODE_TXT_LABEL, Some(&zone))?; - let txt_value = self.to_attr_string(); - let txt_data = rr::rdata::TXT::new(vec![txt_value]); - let rdata = rr::RData::TXT(txt_data); - let record = rr::Record::from_rdata(name, DEFAULT_TTL, rdata); - Ok(record) - } - - pub fn into_pkarr_dns_packet(&self) -> Result> { - use pkarr::dns::{self, rdata}; - let mut packet = dns::Packet::new_reply(0); - // let name = format!("{}.{}", IROH_NODE_TXT_NAME, self.zone()); - let name = IROH_NODE_TXT_LABEL; - let name = dns::Name::new(name)?.into_owned(); - let txt_value = self.to_attr_string(); - let txt_data = rdata::TXT::new().with_string(&txt_value)?.into_owned(); - let rdata = rdata::RData::TXT(txt_data); - packet.answers.push(dns::ResourceRecord::new( - name, - dns::CLASS::IN, - DEFAULT_TTL, - rdata, - )); - Ok(packet) - } - - pub fn into_pkarr_signed_packet( - &self, - signing_key: &ed25519_dalek::SigningKey, - ) -> Result { - // TODO: PR to pkarr for impl From for pkarr::Keypair - let keypair = pkarr::Keypair::from_secret_key(&signing_key.to_bytes()); - let packet = self.into_pkarr_dns_packet()?; - let signed_packet = pkarr::SignedPacket::from_packet(&keypair, &packet)?; - Ok(signed_packet) - } - - pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result { - use pkarr::dns::{self, rdata::RData}; - let pubkey = packet.public_key(); - let pubkey_z32 = pubkey.to_z32(); - let node_id = NodeId::from(*pubkey.verifying_key()); - let zone = dns::Name::new(&pubkey_z32)?; - let inner = packet.packet(); - let txt_record = inner - .answers - .iter() - .find_map(|rr| match &rr.rdata { - RData::TXT(txt) => match rr.name.without(&zone) { - Some(name) if name.to_string() == IROH_NODE_TXT_LABEL => Some(txt), - Some(_) | None => None, - }, - _ => None, - }) - .ok_or_else(|| anyhow!("missing _iroh_node txt record"))?; - - let txt_record = txt_record.to_owned(); - let txt = String::try_from(txt_record)?; - let an = Self::parse_from_attributes(&txt)?; - if an.node_id != node_id { - bail!("node id mismatch between record name and TXT value"); - } - Ok(an) - } - - pub fn from_hickory_answers_message(message: &hickory_proto::op::Message) -> Result { - Self::from_hickory_records(message.answers()) - } - - pub fn from_hickory_lookup(lookup: &hickory_resolver::lookup::Lookup) -> Result { - Self::from_hickory_records(lookup.records()) - } - - pub fn from_hickory_records(records: &[hickory_proto::rr::Record]) -> Result { - use hickory_proto::rr; - let (node_id, txt) = records - .iter() - .find_map(|rr| match rr.data() { - Some(rr::RData::TXT(txt)) => { - is_hickory_node_info_name(rr.name()).map(|node_id| (node_id, txt)) - } - _ => None, - }) - .ok_or_else(|| anyhow!("no TXT record with name _iroh_node.b32encodedpubkey found"))?; - let attr_str = txt.to_string(); - let an = Self::parse_from_attributes(&attr_str)?; - if an.node_id != node_id { - bail!("node id mismatch between record name and TXT value"); - } - Ok(an) - } - - pub fn parse_from_attributes(attrs: &str) -> Result { - let attrs = parse_attrs(attrs); - let Some(node) = attrs.get(ATTR_NODE_ID) else { - bail!("missing required node attr"); - }; - if node.len() != 1 { - bail!("more than one node attr is not allowed"); - } - let node_id = NodeId::from_str(node[0])?; - let home_derp: Option = attrs - .get(ATTR_DERP) - .into_iter() - .flatten() - .find_map(|x| Url::parse(x).ok()); - let home_dns: Vec = attrs - .get(ATTR_DNS) - .into_iter() - .flat_map(|x| x.iter()) - .map(|s| s.to_string()) - .collect(); - Ok(Self { - node_id, - home_derp, - home_dns, - }) - } -} - -fn is_hickory_node_info_name(name: &hickory_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()?; - if label != IROH_NODE_TXT_LABEL { - return None; - } - let label = std::str::from_utf8(labels.next().expect("num_labels checked")).ok()?; - let node_id = from_z32(label).ok()?; - Some(node_id) -} - -fn parse_attrs<'a>(s: &'a str) -> HashMap<&'a str, Vec<&'a str>> { - let mut map: HashMap<&'a str, Vec<&'a str>> = HashMap::new(); - let parts = s.split(' '); - for part in parts { - if let Some((name, value)) = part.split_once('=') { - map.entry(name).or_default().push(value); - } - } - map -} - -fn fmt_attr(label: &str, value: impl Display) -> String { - format!("{label}={value}") -} - -// fn simple_dns_to_hickory( -// signed_packet: &pkarr::SignedPacket, -// ) -> anyhow::Result { -// let encoded = signed_packet.encoded_packet(); -// let parsed1 = pkarr::dns::Packet::parse(&encoded)?; -// println!("simple_dns {parsed1:#?}"); -// let parsed2 = hickory_proto::op::Message::from_bytes(&encoded)?; -// println!("hickory {parsed2:#?}"); -// Ok(parsed2) -// } - -#[cfg(test)] -mod tests { - // TODO: The tests are not comprehensive in any way, more like examples while getting things to - // work - - use std::str::FromStr; - - use hickory_proto::serialize::binary::{BinDecodable, BinEncodable}; - use url::Url; - - use super::*; - - #[test] - fn create_signed_packet() -> Result<()> { - let signing_key = iroh_net::key::SecretKey::generate(); - let node_id = signing_key.public(); - let home_derp: Url = "https://derp.example/".parse()?; - let an = NodeAnnounce { - node_id, - home_derp: Some(home_derp), - home_dns: vec![], - }; - let signing_key = ed25519_dalek::SigningKey::from_bytes(&signing_key.to_bytes()); - let sp = an.into_pkarr_signed_packet(&signing_key)?; - println!("sp {sp:#?}"); - println!("packet {:#?}", sp.packet()); - let an2 = NodeAnnounce::from_pkarr_signed_packet(&sp)?; - assert_eq!(an, an2); - let _p = an.into_hickory_answers_message()?; - Ok(()) - } - - #[test] - fn convert2() -> anyhow::Result<()> { - let key = iroh_net::key::SecretKey::generate(); - let node_id = key.public(); - let home_derp: Url = "https://derp.example".parse()?; - let a = NodeAnnounce { - node_id, - home_derp: Some(home_derp), - home_dns: Default::default(), - }; - let packet_simpdns = a.into_hickory_answers_message()?; - let packet_hickory = a.into_hickory_answers_message()?; - let buf_simpdns = packet_simpdns.to_bytes()?; - let buf_hickory = packet_hickory.to_bytes()?; - println!( - "simple_dns {} {}", - buf_simpdns.len(), - hex::encode(&buf_simpdns) - ); - println!( - "hickory {} {}", - buf_hickory.len(), - hex::encode(&buf_hickory) - ); - let _simpdns_from_hickory = pkarr::dns::Packet::parse(&buf_hickory)?; - let _hickory_form_simpdns = hickory_proto::op::Message::from_bytes(&buf_simpdns)?; - - Ok(()) - } - - #[test] - fn convert3() -> anyhow::Result<()> { - use hickory_proto as proto; - use pkarr::dns; - let ttl = 300; - let (packet1, bytes1) = { - use dns::rdata; - let mut packet = dns::Packet::new_reply(0); - let name = dns::Name::new("foo")?; - let rdata = rdata::RData::TXT(rdata::TXT::new().with_string("bar")?); - let record = dns::ResourceRecord::new(name, dns::CLASS::IN, ttl, rdata); - packet.answers.push(record); - let bytes = packet.build_bytes_vec()?; - (packet, bytes) - }; - let (packet2, bytes2) = { - use proto::rr; - use proto::serialize::binary::BinEncodable; - let mut packet = proto::op::Message::new(); - let name = rr::Name::from_str("foo")?; - let rdata = rr::RData::TXT(rr::rdata::TXT::new(vec!["bar".to_string()])); - let mut record = rr::Record::with(name, rr::RecordType::TXT, ttl); - record.set_data(Some(rdata)); - packet.answers_mut().push(record); - let bytes = packet.to_bytes()?; - (packet, bytes) - }; - println!("simple_dns deb {:#?}", packet1); - println!("hickory deb {:#?}", packet2); - println!("simple_dns len {}", bytes1.len()); - println!("hickory len {}", bytes2.len()); - println!("simple_dns hex {}", hex::encode(&bytes1)); - println!("hickory hex {}", hex::encode(&bytes2)); - - Ok(()) - } -} diff --git a/iroh-dns/src/publish.rs b/iroh-dns/src/publish.rs deleted file mode 100644 index 3e19b7cb1e3..00000000000 --- a/iroh-dns/src/publish.rs +++ /dev/null @@ -1,86 +0,0 @@ -use anyhow::Result; -use ed25519_dalek::SigningKey; -use iroh_net::{key::SecretKey, AddrInfo, NodeId}; -use parking_lot::RwLock; -use pkarr::PkarrClient; -use url::Url; - -use crate::packet::NodeAnnounce; - -pub const IROH_TEST_PKARR_RELAY: &str = "https://testdns.iroh.link/pkarr"; -pub const LOCALHOST_PKARR_RELAY: &str = "http://localhost:8080/pkarr"; - -/// Publisher config -pub struct Config { - pub secret_key: SecretKey, - pub pkarr_relay: Url, -} - -impl Config { - pub fn new(secret_key: SecretKey, pkarr_relay: Url) -> Self { - Self { - secret_key, - pkarr_relay, - } - } - - pub fn with_iroh_test(secret_key: SecretKey) -> Self { - let pkarr_relay: Url = IROH_TEST_PKARR_RELAY.parse().expect("url is valid"); - Self::new(secret_key, pkarr_relay) - } - - pub fn localhost_dev(secret_key: SecretKey) -> Self { - let pkarr_relay: Url = LOCALHOST_PKARR_RELAY.parse().expect("url is valid"); - Self::new(secret_key, pkarr_relay) - } -} - -/// Publish node announces to a pkarr relay. -#[derive(derive_more::Debug)] -pub struct Publisher { - node_id: NodeId, - #[debug("SigningKey")] - signing_key: SigningKey, - #[debug("{}", self.pkarr_relay)] - pkarr_relay: Url, - #[debug("PkarrClient")] - pkarr_client: PkarrClient, - #[debug(skip)] - last_announce: RwLock>, -} - -impl Publisher { - pub fn new(config: Config) -> Self { - let pkarr_client = PkarrClient::builder().build(); - let node_id = config.secret_key.public(); - let signing_key = ed25519_dalek::SigningKey::from_bytes(&config.secret_key.to_bytes()); - Self { - node_id, - signing_key, - pkarr_relay: config.pkarr_relay, - pkarr_client, - last_announce: Default::default(), - } - } - - pub async fn publish_addr_info(&self, info: &AddrInfo) -> Result<()> { - let an = NodeAnnounce::new( - self.node_id, - info.derp_url.as_ref().map(|u| u.clone().into()), - Default::default(), - ); - if self.last_announce.read().as_ref() == Some(&an) { - return Ok(()); - } - let _ = self.last_announce.write().insert(an.clone()); - let signed_packet = an.into_pkarr_signed_packet(&self.signing_key)?; - self.pkarr_client - .relay_put(&self.pkarr_relay, &signed_packet) - .await?; - Ok(()) - } - - pub fn pkarr_relay(&self) -> &Url { - &self.pkarr_relay - } -} diff --git a/iroh-dns/src/resolve.rs b/iroh-dns/src/resolve.rs deleted file mode 100644 index 4f0ee9ac650..00000000000 --- a/iroh-dns/src/resolve.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::{net::Ipv4Addr, str::FromStr}; - -use anyhow::Result; -use hickory_proto::error::ProtoError; -use hickory_resolver::{ - config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, - AsyncResolver, Name, TokioAsyncResolver, -}; -use iroh_net::{AddrInfo, NodeAddr, NodeId}; -use tracing::debug; - -use crate::{ - packet::{NodeAnnounce, IROH_NODE_TXT_LABEL}, - to_z32, -}; - -pub const IROH_TEST_DNS_IPV4: Ipv4Addr = Ipv4Addr::new(5, 75, 181, 3); -pub const IROH_TEST_DOMAIN: &str = "testdns.iroh.link."; -pub const EXAMPLE_DOMAIN: &str = "irohdns.example."; - -/// Resolver config -pub struct Config { - name_servers: NameServerConfigGroup, - default_node_origin: String, -} - -impl Config { - // TODO: Add with_system_and_iroh_test() - - pub fn with_cloudflare_and_iroh_test() -> Self { - let cloudflare_dns = NameServerConfigGroup::cloudflare(); - let cloudflare_https = NameServerConfigGroup::cloudflare_https(); - let iroh_test_https = NameServerConfigGroup::from_ips_https( - &[IROH_TEST_DNS_IPV4.into()], - 443, - IROH_TEST_DOMAIN.to_string(), - true, - ); - let iroh_test_dns = - NameServerConfigGroup::from_ips_clear(&[IROH_TEST_DNS_IPV4.into()], 53, false); - - let mut name_servers = NameServerConfigGroup::new(); - name_servers.merge(cloudflare_https); - name_servers.merge(cloudflare_dns); - name_servers.merge(iroh_test_https); - name_servers.merge(iroh_test_dns); - Self { - name_servers, - default_node_origin: IROH_TEST_DOMAIN.to_string(), - } - } - - pub fn localhost_dev() -> Self { - let name_servers = - NameServerConfigGroup::from_ips_clear(&[Ipv4Addr::LOCALHOST.into()], 5353, true); - Self { - name_servers, - default_node_origin: EXAMPLE_DOMAIN.to_string(), - } - } -} - -/// Resolve iroh nodes through DNS -#[derive(derive_more::Debug, Clone)] -pub struct Resolver { - default_node_origin: Name, - #[debug("TokioAsyncResolver")] - dns_resolver: TokioAsyncResolver, -} - -impl Resolver { - pub fn new(config: Config) -> Result { - let default_node_origin = Name::from_str(&config.default_node_origin)?; - // TODO: If we add our default node origin as search domain, we can resolve just node IDs! - // let domain = Some(config.default_node_origin); - let domain = None; - let resolv_conf = ResolverConfig::from_parts(domain, vec![], config.name_servers); - let dns_resolver = AsyncResolver::tokio(resolv_conf, ResolverOpts::default()); - Ok(Self { - dns_resolver, - default_node_origin, - }) - } - - pub fn resolver(&self) -> &TokioAsyncResolver { - &self.dns_resolver - } - - pub async fn resolve_node_by_domain(&self, domain: &str) -> Result { - let name = Name::from_str(domain)?; - self.resolve_node(name).await - } - - pub async fn resolve_node_by_id(&self, node_id: NodeId) -> Result { - debug!(?node_id, "resolve node by id"); - let name = Name::parse(&to_z32(&node_id), Some(&self.default_node_origin))?; - let addr = self.resolve_node(name).await; - debug!(?node_id, ?addr, "resolved"); - let addr = addr?; - Ok(addr.info) - } - - async fn resolve_node(&self, name: Name) -> Result { - let name = with_iroh_node_txt_label(name)?; - let lookup = self.dns_resolver.txt_lookup(name).await?; - let an = NodeAnnounce::from_hickory_lookup(lookup.as_lookup())?; - Ok(an.into()) - } -} - -fn with_iroh_node_txt_label(name: Name) -> Result { - if name.iter().next() == Some(IROH_NODE_TXT_LABEL.as_bytes()) { - Ok(name) - } else { - Name::parse(IROH_NODE_TXT_LABEL, Some(&name)) - } -} diff --git a/iroh-net/Cargo.toml b/iroh-net/Cargo.toml index d4edebf5946..ad95ce0ce83 100644 --- a/iroh-net/Cargo.toml +++ b/iroh-net/Cargo.toml @@ -25,6 +25,7 @@ data-encoding = "2.3.3" default-net = "0.20" der = { version = "0.7", features = ["alloc", "derive"] } derive_more = { version = "1.0.0-beta.1", features = ["debug", "display", "from", "try_into", "deref"] } +ed25519-dalek = { version = "2.0.0", features = ["serde", "rand_core"], optional = true } flume = "0.11" futures = "0.3.25" governor = "0.6.0" @@ -82,6 +83,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = tr # metrics iroh-metrics = { version = "0.12.0", path = "../iroh-metrics", default-features = false } +z32 = "1.0.3" +hickory-proto = "0.24.0" +pkarr = { version = "1.1.3", default-features = false, features = ["async", "relay"] } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] netlink-packet-core = "0.7.0" diff --git a/iroh-net/src/discovery.rs b/iroh-net/src/discovery.rs index b6991eb06a0..ec608a5bd02 100644 --- a/iroh-net/src/discovery.rs +++ b/iroh-net/src/discovery.rs @@ -10,6 +10,9 @@ use tracing::{debug, error_span, warn, Instrument}; use crate::{AddrInfo, MagicEndpoint, NodeId}; +pub mod dns; +pub mod pkarr_relay_publish; + /// Node discovery for [`super::MagicEndpoint`]. /// /// The purpose of this trait is to hook up a node discovery mechanism that diff --git a/iroh-net/src/discovery/dns.rs b/iroh-net/src/discovery/dns.rs new file mode 100644 index 00000000000..afee40ae8ba --- /dev/null +++ b/iroh-net/src/discovery/dns.rs @@ -0,0 +1,58 @@ +//! DNS node discovery for iroh-net + +use crate::{ + discovery::{Discovery, DiscoveryItem}, + MagicEndpoint, NodeId, +}; +use anyhow::Result; +use futures::{future::FutureExt, stream::BoxStream, StreamExt}; + +use crate::dns; + +/// DNS node discovery. +/// +/// The DNS discovery looks up node addressing information over the Domain Name System. +/// Node information is resolved via an _iroh_node.z32encodednodeid TXT record. +/// +/// The content of this record is expected to be a DNS attribute string, with a required +/// `node=` attribute containing the base32 encoded node id and a derp_url attribute containing the +/// node's home Derp server. +/// +/// The discovery has to be configured with a `node_origin`, which is the domain name under which +/// lookups for nodes will be made. +/// With a origin of mydns.example, a node info record would be searched at +/// _iroh_node.z32encodednodeid.mydns.example TXT +#[derive(Debug)] +pub struct DnsDiscovery { + node_origin: String, +} + +impl DnsDiscovery { + /// Create a new DNS discovery with `node_origin` appended to all lookups. + pub fn new(node_origin: String) -> Self { + Self { node_origin } + } + + /// Create a new DNS discovery which uses the n0 testdns origin. + pub fn with_n0_testdns() -> Self { + Self::new("testdns.iroh.link".to_string()) + } +} + +impl Discovery for DnsDiscovery { + fn resolve<'a>( + &'a self, + _ep: MagicEndpoint, + node_id: NodeId, + ) -> Option>> { + let fut = async move { + let node_addr = dns::node_info::lookup_by_id(&node_id, &self.node_origin).await?; + Ok(DiscoveryItem { + provenance: "iroh-dns", + last_updated: None, + addr_info: node_addr.info, + }) + }; + Some(fut.into_stream().boxed()) + } +} diff --git a/iroh-net/src/discovery/pkarr_relay_publish.rs b/iroh-net/src/discovery/pkarr_relay_publish.rs new file mode 100644 index 00000000000..fc969bf51f2 --- /dev/null +++ b/iroh-net/src/discovery/pkarr_relay_publish.rs @@ -0,0 +1,113 @@ +//! A discovery service which publishes node information to a [Pkarr] relay. +//! +//! This service only implements the [`Discovery::publish`] method and does not provide discovery. +//! It encodes the node information into a DNS packet in the format resolvable by the +//! [`super::dns::DnsDiscovery`], which means a single _iroh_node TXT record, under the z32 encoded +//! node id as origin domain. +//! +//! [pkarr]: https://pkarr.org + +// TODO: Decide what to do with this module once publishing over Derpers land. Either remove, or +// leave in the repo but do not enable it by default in the iroh node. + +use std::sync::Arc; + +use anyhow::Result; +use parking_lot::RwLock; +use pkarr::PkarrClient; +use tracing::warn; +use url::Url; + +use crate::{discovery::Discovery, dns::node_info::NodeInfo, key::SecretKey, AddrInfo}; + +/// URL of the n0 testdns server +pub const IROH_TEST_PKARR_RELAY: &str = "https://testdns.iroh.link/pkarr"; + +/// Default TTL for the _iroh_node TXT record in the pkarr signed packet +const DEFAULT_PKARR_TTL: u32 = 30; + +/// Publish node info to a pkarr relay. +#[derive(derive_more::Debug, Clone)] +pub struct Publisher { + config: Config, + #[debug("PkarrClient")] + pkarr_client: PkarrClient, + last_published: Arc>>, +} + +/// Publisher config +#[derive(derive_more::Debug, Clone)] +pub struct Config { + #[debug("SecretKey")] + secret_key: SecretKey, + #[debug("{}", self.pkarr_relay)] + pkarr_relay: Url, + ttl: u32, +} + +impl Config { + /// Create a new config with a secret key and a pkarr relay URL. + pub fn new(secret_key: SecretKey, pkarr_relay: Url) -> Self { + Self { + secret_key, + pkarr_relay, + ttl: DEFAULT_PKARR_TTL, + } + } + + /// Create a config that publishes to the n0 testdns server. + pub fn n0_testdns(secret_key: SecretKey) -> Self { + let pkarr_relay: Url = IROH_TEST_PKARR_RELAY.parse().expect("url is valid"); + Self::new(secret_key, pkarr_relay) + } + + /// Set the TTL for pkarr packets, in seconds. + /// + /// Default value is 30 seconds. + pub fn ttl(mut self, ttl: u32) -> Self { + self.ttl = ttl; + self + } +} + +impl Publisher { + /// Create a new publisher with a [`Config`]. + pub fn new(config: Config) -> Self { + let pkarr_client = PkarrClient::builder().build(); + Self { + config, + pkarr_client, + last_published: Default::default(), + } + } + + /// Publish [`AddrInfo`] about this node to a pkarr relay. + pub async fn publish_addr_info(&self, info: &AddrInfo) -> Result<()> { + let info = NodeInfo::new( + self.config.secret_key.public(), + info.derp_url.clone().map(Url::from), + ); + if self.last_published.read().as_ref() == Some(&info) { + return Ok(()); + } + let _ = self.last_published.write().insert(info.clone()); + let signed_packet = + info.into_pkarr_signed_packet(&self.config.secret_key, self.config.ttl)?; + self.pkarr_client + .relay_put(&self.config.pkarr_relay, &signed_packet) + .await?; + Ok(()) + } +} + +impl Discovery for Publisher { + fn publish(&self, info: &AddrInfo) { + let this = self.clone(); + let info = info.clone(); + tokio::task::spawn(async move { + if let Err(err) = this.publish_addr_info(&info).await { + warn!("failed to publish address update: {err:?}"); + } + }); + } +} diff --git a/iroh-net/src/dns.rs b/iroh-net/src/dns.rs index 441fb364dec..c15c3be0ed9 100644 --- a/iroh-net/src/dns.rs +++ b/iroh-net/src/dns.rs @@ -1,3 +1,5 @@ +//! DNS resolver and discovery for iroh-net + use std::net::IpAddr; use std::time::Duration; @@ -5,9 +7,16 @@ use anyhow::Result; use hickory_resolver::{AsyncResolver, IntoName, TokioAsyncResolver, TryParseIp}; use once_cell::sync::Lazy; -pub static DNS_RESOLVER: Lazy = +pub mod node_info; + +pub(crate) static DNS_RESOLVER: Lazy = Lazy::new(|| get_resolver().expect("unable to create DNS resolver")); +/// Get the DNS resolver used within iroh-net. +pub fn resolver() -> &'static TokioAsyncResolver { + Lazy::force(&DNS_RESOLVER) +} + /// Get resolver to query MX records. /// /// We first try to read the system's resolver from `/etc/resolv.conf`. diff --git a/iroh-net/src/dns/node_info.rs b/iroh-net/src/dns/node_info.rs new file mode 100644 index 00000000000..76d1851fbe6 --- /dev/null +++ b/iroh-net/src/dns/node_info.rs @@ -0,0 +1,249 @@ +//! todo + +use std::{collections::HashMap, fmt, str::FromStr}; + +use anyhow::{anyhow, bail, Result}; +use hickory_proto::error::ProtoError; +use hickory_resolver::Name; +use url::Url; + +use crate::{key::SecretKey, AddrInfo, NodeAddr, NodeId}; + +/// +pub const ATTR_DERP: &str = "derp"; +/// +pub const ATTR_NODE_ID: &str = "node"; +/// +pub const ATTR_ADDR: &str = "addr"; +/// +pub const IROH_NODE_TXT_LABEL: &str = "_iroh_node"; + +/// +pub async fn lookup_by_domain(domain: &str) -> Result { + let name = Name::from_str(domain)?; + let info = lookup_node_info(name).await?; + Ok(info.into()) +} + +/// +pub async fn lookup_by_id(node_id: &NodeId, origin: &str) -> Result { + let domain = format!("{}.{}", to_z32(node_id), origin); + lookup_by_domain(&domain).await +} + +async fn lookup_node_info(name: Name) -> Result { + let name = ensure_iroh_node_txt_label(name)?; + let lookup = super::resolver().txt_lookup(name).await?; + NodeInfo::from_hickory_lookup(lookup.as_lookup()) +} + +fn ensure_iroh_node_txt_label(name: Name) -> Result { + if name.iter().next() == Some(IROH_NODE_TXT_LABEL.as_bytes()) { + Ok(name) + } else { + Name::parse(IROH_NODE_TXT_LABEL, Some(&name)) + } +} + +/// +#[derive(derive_more::Debug, Clone, Eq, PartialEq)] +pub struct NodeInfo { + /// + pub node_id: NodeId, + /// + #[debug("{:?}", self.derp_url.as_ref().map(|s| s.to_string()))] + pub derp_url: Option, +} + +impl From for NodeAddr { + fn from(value: NodeInfo) -> Self { + NodeAddr { + node_id: value.node_id, + info: value.into(), + } + } +} + +impl From for AddrInfo { + fn from(value: NodeInfo) -> Self { + AddrInfo { + derp_url: value.derp_url.map(|u| u.into()), + direct_addresses: Default::default(), + } + } +} + +impl NodeInfo { + /// + pub fn new(node_id: NodeId, derp_url: Option) -> Self { + Self { node_id, derp_url } + } + /// + pub fn node_domain(&self, origin: &str) -> String { + format!("{}.{}", to_z32(&self.node_id), origin) + } + + /// + pub fn node_info_domain(&self, origin: &str) -> String { + format!("{}.{}", IROH_NODE_TXT_LABEL, self.node_domain(origin)) + } + + /// + pub fn to_attr_string(&self) -> String { + let mut attrs = vec![]; + attrs.push(fmt_attr(ATTR_NODE_ID, self.node_id)); + if let Some(derp) = &self.derp_url { + attrs.push(fmt_attr(ATTR_DERP, derp)); + } + attrs.join(" ") + } + + /// + pub fn from_hickory_lookup(lookup: &hickory_resolver::lookup::Lookup) -> Result { + Self::from_hickory_records(lookup.records()) + } + + /// + pub fn from_hickory_records(records: &[hickory_proto::rr::Record]) -> Result { + use hickory_proto::rr; + let (node_id, txt) = records + .iter() + .find_map(|rr| match rr.data() { + Some(rr::RData::TXT(txt)) => { + parse_hickory_node_info_name(rr.name()).map(|node_id| (node_id, txt)) + } + _ => None, + }) + .ok_or_else(|| anyhow!("no TXT record with name _iroh_node.b32encodedpubkey found"))?; + let node_info = Self::parse_from_attributes(&txt.to_string())?; + if node_info.node_id != node_id { + bail!("node id mismatch between record name and TXT value"); + } + Ok(node_info) + } + + /// + pub fn parse_from_attributes(attrs: &str) -> Result { + let attrs = parse_attrs(attrs); + let Some(node) = attrs.get(ATTR_NODE_ID) else { + bail!("missing required node attribute"); + }; + if node.len() != 1 { + bail!("more than one node attribute is not allowed"); + } + let node_id = NodeId::from_str(node[0])?; + let home_derp: Option = attrs + .get(ATTR_DERP) + .into_iter() + .flatten() + .find_map(|x| Url::parse(x).ok()); + Ok(Self { + node_id, + derp_url: home_derp, + }) + } + + /// + pub fn into_pkarr_signed_packet( + &self, + secret_key: &SecretKey, + ttl: u32, + ) -> Result { + let packet = self.into_pkarr_dns_packet(ttl)?; + let keypair = pkarr::Keypair::from_secret_key(&secret_key.to_bytes()); + let signed_packet = pkarr::SignedPacket::from_packet(&keypair, &packet)?; + Ok(signed_packet) + } + + /// + pub fn into_pkarr_dns_packet(&self, ttl: u32) -> Result> { + use pkarr::dns::{self, rdata}; + let name = dns::Name::new(IROH_NODE_TXT_LABEL)?.into_owned(); + let rdata = { + let value = self.to_attr_string(); + let txt = rdata::TXT::new().with_string(&value)?.into_owned(); + rdata::RData::TXT(txt) + }; + + let mut packet = dns::Packet::new_reply(0); + packet + .answers + .push(dns::ResourceRecord::new(name, dns::CLASS::IN, ttl, rdata)); + Ok(packet) + } + + /// + pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result { + use pkarr::dns::{self, rdata::RData}; + let pubkey = packet.public_key(); + let pubkey_z32 = pubkey.to_z32(); + let node_id = NodeId::from(*pubkey.verifying_key()); + let zone = dns::Name::new(&pubkey_z32)?; + let inner = packet.packet(); + let txt_record = inner + .answers + .iter() + .find_map(|rr| match &rr.rdata { + RData::TXT(txt) => match rr.name.without(&zone) { + Some(name) if name.to_string() == IROH_NODE_TXT_LABEL => Some(txt), + Some(_) | None => None, + }, + _ => None, + }) + .ok_or_else(|| anyhow!("missing _iroh_node txt record"))?; + + let txt_record = txt_record.to_owned(); + let txt = String::try_from(txt_record)?; + let an = Self::parse_from_attributes(&txt)?; + if an.node_id != node_id { + bail!("node id mismatch between record name and TXT value"); + } + Ok(an) + } +} + +fn parse_hickory_node_info_name(name: &hickory_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()?; + if label != IROH_NODE_TXT_LABEL { + return None; + } + let label = std::str::from_utf8(labels.next().expect("num_labels checked")).ok()?; + let node_id = from_z32(label).ok()?; + Some(node_id) +} + +fn fmt_attr(label: &str, value: impl fmt::Display) -> String { + format!("{label}={value}") +} + +fn parse_attrs<'a>(s: &'a str) -> HashMap<&'a str, Vec<&'a str>> { + let mut map: HashMap<&'a str, Vec<&'a str>> = HashMap::new(); + let parts = s.split(' '); + for part in parts { + if let Some((name, value)) = part.split_once('=') { + map.entry(name).or_default().push(value); + } + } + map +} + +/// Encode a [`NodeId`] in [`z-base-32`] encoding. +/// +/// [z-base-32]: https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt +pub fn to_z32(node_id: &NodeId) -> String { + z32::encode(node_id.as_bytes()) +} + +/// Parse a [`NodeId`] from [`z-base-32`] encoding. +/// +/// [z-base-32]: https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt +pub fn from_z32(s: &str) -> Result { + let bytes = z32::decode(s.as_bytes()).map_err(|_| anyhow!("invalid z32"))?; + let bytes: &[u8; 32] = &bytes.try_into().map_err(|_| anyhow!("not 32 bytes long"))?; + let node_id = NodeId::from_bytes(bytes)?; + Ok(node_id) +} diff --git a/iroh-net/src/lib.rs b/iroh-net/src/lib.rs index 95362a2ca18..5db44541bb2 100644 --- a/iroh-net/src/lib.rs +++ b/iroh-net/src/lib.rs @@ -16,7 +16,7 @@ pub mod derp; pub mod dialer; mod disco; pub mod discovery; -mod dns; +pub mod dns; pub mod magic_endpoint; pub mod magicsock; pub mod metrics; diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index b359351fb3a..e7f92eddda2 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -29,7 +29,6 @@ hashlink = "0.8.4" hex = { version = "0.4.3" } iroh-bytes = { version = "0.12.0", path = "../iroh-bytes", features = ["downloader"] } iroh-base = { version = "0.12.0", path = "../iroh-base", features = ["key"] } -iroh-dns = { version = "0.12.0", path = "../iroh-dns" } iroh-io = { version = "0.4.0", features = ["stats"] } iroh-metrics = { version = "0.12.0", path = "../iroh-metrics", optional = true } iroh-net = { version = "0.12.0", path = "../iroh-net" } diff --git a/iroh/src/commands/start.rs b/iroh/src/commands/start.rs index fd26a554698..642b6409e6b 100644 --- a/iroh/src/commands/start.rs +++ b/iroh/src/commands/start.rs @@ -14,9 +14,9 @@ use iroh::{ rpc_protocol::{ProviderRequest, ProviderResponse, ProviderService}, util::{fs::load_secret_key, path::IrohPaths}, }; -use iroh_dns::discovery::DnsDiscovery; use iroh_net::{ derp::{DerpMap, DerpMode}, + discovery::{dns::DnsDiscovery, pkarr_relay_publish, CombinedDiscovery}, key::SecretKey, }; use quic_rpc::{transport::quinn::QuinnServerEndpoint, ServiceEndpoint}; @@ -220,7 +220,14 @@ pub(crate) async fn start_node( Some(derp_map) => DerpMode::Custom(derp_map), }; - let discovery = DnsDiscovery::with_iroh_test(Some(secret_key.clone()))?; + let mut discovery = CombinedDiscovery::new(); + let dns_discovery = DnsDiscovery::with_n0_testdns(); + discovery.add(dns_discovery); + // TODO: We don't want nodes to self-publish. Remove once publishing over derpers lands. + let pkarr_publish = pkarr_relay_publish::Publisher::new( + pkarr_relay_publish::Config::n0_testdns(secret_key.clone()), + ); + discovery.add(pkarr_publish); Node::builder(bao_store, doc_store) .derp_mode(derp_mode)