Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(iroh-net): Minor tweaks in the public iroh_net::dns module #2289

Merged
merged 3 commits into from
May 14, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 74 additions & 41 deletions iroh-net/src/dns/node_info.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
//! This module contains functions and structs to lookup node information from DNS
//! and to encode node information in Pkarr signed packets.
//! Support for handling DNS resource records for dialing by [`NodeId`].
//!
//! Dialing by [`NodeId`] is supported by iroh nodes publishing [Pkarr] records to DNS
//! servers or the Mainline DHT. This module supports creating and parsing these records.
//!
//! DNS records are published under the following names:
//!
//! `_iroh.<z32-node-id>.<origin-domain> TXT`
//!
//! - `_iroh` is the record name as defined by [`IROH_TXT_NAME`].
//!
//! - `<z32-node-id>` is the [z-base-32] encoding of the [`NodeId`].
//!
//! - `<origin-domain>` is the domain name of the publishing DNS server, `dns.iroh.link` is
//! operated by number0.
flub marked this conversation as resolved.
Show resolved Hide resolved
//!
//! - `TXT` is the DNS record type.
//!
//! The returned TXT records must contain a string value of the form `key=value` as defined
//! in [RFC1464]. The following attributes are defined:
//!
//! - `relay=<url>`: The home [`RelayUrl`] of this node.
//!
//! - `addr=<addr> <addr>`: A space-separated list of sockets addresses for this iroh node.
//! Each address is an IPv4 or IPv6 address with a port.
//!
//! [Pkarr]: https://app.pkarr.org
//! [z-base-32]: https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt
//! [RFC1464]: https://www.rfc-editor.org/rfc/rfc1464
//! [`RelayUrl`]: iroh_base::node_addr::RelayUrl

use std::{
collections::{BTreeMap, BTreeSet},
Expand All @@ -16,32 +44,34 @@ use url::Url;

use crate::{key::SecretKey, AddrInfo, NodeAddr, NodeId};

/// The DNS name for the iroh TXT record
/// The DNS name for the iroh TXT record.
pub const IROH_TXT_NAME: &str = "_iroh";

/// The attributes supported by iroh for `_iroh` DNS records
/// The attributes supported by iroh for `_iroh` DNS resource records.
///
/// The resource record uses the lower-case names.
#[derive(
Debug, strum::Display, strum::AsRefStr, strum::EnumString, Hash, Eq, PartialEq, Ord, PartialOrd,
)]
#[strum(serialize_all = "kebab-case")]
pub enum IrohAttr {
/// `relay`: URL of home relay
/// URL of home relay.
Relay,
/// `addr`: Direct address
/// Direct address.
Addr,
}

/// Lookup node info by domain name
/// Looks up node info by DNS name.
///
/// The domain name must either contain an _iroh TXT record or be a CNAME record that leads to
/// an _iroh TXT record.
pub async fn lookup_by_domain(resolver: &TokioAsyncResolver, domain: &str) -> Result<NodeAddr> {
let attrs = TxtAttrs::<IrohAttr>::lookup_by_domain(resolver, domain).await?;
/// The resource records returned for `name` must either contain an `_iroh` TXT record or be
/// a CNAME record that leads to an `_iroh` TXT record.
pub async fn lookup_by_domain(resolver: &TokioAsyncResolver, name: &str) -> Result<NodeAddr> {
flub marked this conversation as resolved.
Show resolved Hide resolved
let attrs = TxtAttrs::<IrohAttr>::lookup_by_domain(resolver, name).await?;
let info: NodeInfo = attrs.into();
Ok(info.into())
}

/// Lookup node info by node id and origin domain name.
/// Looks up node info by [`NodeId`] and origin domain name.
pub async fn lookup_by_id(
resolver: &TokioAsyncResolver,
node_id: &NodeId,
Expand All @@ -52,14 +82,14 @@ pub async fn lookup_by_id(
Ok(info.into())
}

/// Encode a [`NodeId`] in [`z-base-32`] encoding.
/// Encodes 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.
/// Parses 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<NodeId> {
Expand All @@ -69,15 +99,15 @@ pub fn from_z32(s: &str) -> Result<NodeId> {
Ok(node_id)
}

/// Node info contained in a DNS _iroh TXT record.
/// Information about and iroh node which is contained in an `_iroh` TXT resource record.
#[derive(derive_more::Debug, Clone, Eq, PartialEq)]
flub marked this conversation as resolved.
Show resolved Hide resolved
pub struct NodeInfo {
/// The node id
/// The [`NodeId`].
pub node_id: NodeId,
/// Home relay server for this node
/// The advertised home relay server.
#[debug("{:?}", self.relay_url.as_ref().map(|s| s.to_string()))]
pub relay_url: Option<Url>,
/// Direct addresses
/// Any direct addresses.
pub direct_addresses: BTreeSet<SocketAddr>,
}

Expand Down Expand Up @@ -143,7 +173,7 @@ impl From<NodeInfo> for AddrInfo {
}

impl NodeInfo {
/// Create a new [`NodeInfo`] from its parts.
/// Creates a new [`NodeInfo`] from its parts.
pub fn new(
node_id: NodeId,
relay_url: Option<Url>,
Expand All @@ -160,20 +190,21 @@ impl NodeInfo {
self.into()
}

/// Try to parse a [`NodeInfo`] from a set of DNS records.
/// Parses a [`NodeInfo`] from a set of DNS records.
pub fn from_hickory_records(records: &[hickory_proto::rr::Record]) -> Result<Self> {
let attrs = TxtAttrs::from_hickory_records(records)?;
Ok(attrs.into())
}

/// Try to parse a [`NodeInfo`] from a [`pkarr::SignedPacket`].
/// Parses a [`NodeInfo`] from a [`pkarr::SignedPacket`].
pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result<Self> {
let attrs = TxtAttrs::from_pkarr_signed_packet(packet)?;
Ok(attrs.into())
}

/// Create a [`pkarr::SignedPacket`] by constructing a DNS packet and
/// signing it with a [`SecretKey`].
/// Creates a [`pkarr::SignedPacket`].
///
/// This constructs a DNS packet and signs it with a [`SecretKey`].
pub fn to_pkarr_signed_packet(
&self,
secret_key: &SecretKey,
Expand All @@ -182,7 +213,7 @@ impl NodeInfo {
self.to_attrs().to_pkarr_signed_packet(secret_key, ttl)
}

/// Convert into a [`hickory_proto::rr::Record`] DNS record.
/// Converts into a [`hickory_proto::rr::Record`] DNS record.
pub fn to_hickory_records(
&self,
origin: &str,
Expand All @@ -194,10 +225,10 @@ impl NodeInfo {
}
}

/// Parse a [`NodeId`] from iroh DNS name.
/// Parses a [`NodeId`] from iroh DNS name.
///
/// Takes a [`hickory_proto::rr::Name`] DNS name and expects the first label to be `_iroh`
/// and the second label to be a z32 encoded [`NodeId`]. Does not care about subsequent labels.
/// and the second label to be a z32 encoded [`NodeId`]. Ignores subsequent labels.
pub(crate) fn node_id_from_hickory_name(name: &hickory_proto::rr::Name) -> Option<NodeId> {
if name.num_labels() < 2 {
return None;
Expand All @@ -214,16 +245,17 @@ pub(crate) fn node_id_from_hickory_name(name: &hickory_proto::rr::Name) -> Optio

/// Attributes parsed from `_iroh` TXT records.
///
/// This struct is generic over the key type. When using with String, this will parse all
/// attributes. Can also be used with an enum, if it implements [`FromStr`] and [`Display`].
/// This struct is generic over the key type. When using with [`String`], this will parse
/// all attributes. Can also be used with an enum, if it implements [`FromStr`] and
/// [`Display`].
#[derive(Debug)]
pub struct TxtAttrs<T> {
node_id: NodeId,
attrs: BTreeMap<T, Vec<String>>,
}

impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
/// Create from a node id and an iterator of key-value pairs.
/// Creates [`TxtAttrs`] from a node id and an iterator of key-value pairs.
pub fn from_parts(node_id: NodeId, pairs: impl Iterator<Item = (T, String)>) -> Self {
let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
for (k, v) in pairs {
Expand All @@ -232,7 +264,7 @@ impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
Self { attrs, node_id }
}

/// Create from a node id and an iterator of "{key}={value}" strings.
/// Creates [`TxtAttrs`] from a node id and an iterator of "{key}={value}" strings.
pub fn from_strings(node_id: NodeId, strings: impl Iterator<Item = String>) -> Result<Self> {
let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
for s in strings {
Expand All @@ -255,7 +287,7 @@ impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
Ok(attrs)
}

/// Lookup attributes for a node id and origin domain.
/// Looks up attributes by [`NodeId`] and origin domain.
pub async fn lookup_by_id(
resolver: &TokioAsyncResolver,
node_id: &NodeId,
Expand All @@ -265,23 +297,23 @@ impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
TxtAttrs::lookup(resolver, name).await
}

/// Lookup attributes for a domain.
pub async fn lookup_by_domain(resolver: &TokioAsyncResolver, domain: &str) -> Result<Self> {
let name = Name::from_str(domain)?;
/// Looks up attributes by DNS name.
pub async fn lookup_by_domain(resolver: &TokioAsyncResolver, name: &str) -> Result<Self> {
let name = Name::from_str(name)?;
TxtAttrs::lookup(resolver, name).await
}

/// Get a reference to the parsed attributes.
/// Returns the parsed attributes.
pub fn attrs(&self) -> &BTreeMap<T, Vec<String>> {
&self.attrs
}

/// Get the node id.
/// Returns the node id.
pub fn node_id(&self) -> NodeId {
self.node_id
}

/// Try to parse a from a [`pkarr::SignedPacket`].
/// Parses a [`pkarr::SignedPacket`].
pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result<Self> {
use pkarr::dns::{self, rdata::RData};
let pubkey = packet.public_key();
Expand All @@ -301,7 +333,7 @@ impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
Self::from_strings(node_id, txt_strs)
}

/// Try to parse a from a set of DNS records.
/// Parses a set of DNS resource records.
pub fn from_hickory_records(records: &[hickory_proto::rr::Record]) -> Result<Self> {
use hickory_proto::rr;
let mut records = records.iter().filter_map(|rr| match rr.data() {
Expand All @@ -328,7 +360,7 @@ impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
.flat_map(move |(k, vs)| vs.iter().map(move |v| format!("{k}={v}")))
}

/// Convert into list of [`hickory_proto::rr::Record`].
/// Converts to a list of [`hickory_proto::rr::Record`] resource records.
pub fn to_hickory_records(
&self,
origin: &str,
Expand All @@ -345,8 +377,9 @@ impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
Ok(records)
}

/// Create a [`pkarr::SignedPacket`] by constructing a DNS packet and
/// signing it with a [`SecretKey`].
/// Creates a [`pkarr::SignedPacket`]
///
/// This constructs a DNS packet and signs it with a [`SecretKey`].
pub fn to_pkarr_signed_packet(
&self,
secret_key: &SecretKey,
Expand Down
Loading