iroh_relay/
node_info.rs

1//! Support for handling DNS resource records for dialing by [`NodeId`].
2//!
3//! Dialing by [`NodeId`] is supported by iroh nodes publishing [Pkarr] records to DNS
4//! servers or the Mainline DHT.  This module supports creating and parsing these records.
5//!
6//! DNS records are published under the following names:
7//!
8//! `_iroh.<z32-node-id>.<origin-domain> TXT`
9//!
10//! - `_iroh` is the record name as defined by [`IROH_TXT_NAME`].
11//!
12//! - `<z32-node-id>` is the [z-base-32] encoding of the [`NodeId`].
13//!
14//! - `<origin-domain>` is the domain name of the publishing DNS server,
15//!   [`N0_DNS_NODE_ORIGIN_PROD`] is the server operated by number0 for production.
16//!   [`N0_DNS_NODE_ORIGIN_STAGING`] is the server operated by number0 for testing.
17//!
18//! - `TXT` is the DNS record type.
19//!
20//! The returned TXT records must contain a string value of the form `key=value` as defined
21//! in [RFC1464].  The following attributes are defined:
22//!
23//! - `relay=<url>`: The home [`RelayUrl`] of this node.
24//!
25//! - `addr=<addr> <addr>`: A space-separated list of sockets addresses for this iroh node.
26//!   Each address is an IPv4 or IPv6 address with a port.
27//!
28//! [Pkarr]: https://app.pkarr.org
29//! [z-base-32]: https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt
30//! [RFC1464]: https://www.rfc-editor.org/rfc/rfc1464
31//! [`RelayUrl`]: iroh_base::RelayUrl
32//! [`N0_DNS_NODE_ORIGIN_PROD`]: crate::dns::N0_DNS_NODE_ORIGIN_PROD
33//! [`N0_DNS_NODE_ORIGIN_STAGING`]: crate::dns::N0_DNS_NODE_ORIGIN_STAGING
34
35use std::{
36    collections::{BTreeMap, BTreeSet},
37    fmt::{self, Display},
38    hash::Hash,
39    net::SocketAddr,
40    str::{FromStr, Utf8Error},
41};
42
43use iroh_base::{NodeAddr, NodeId, RelayUrl, SecretKey, SignatureError};
44use nested_enum_utils::common_fields;
45use snafu::{Backtrace, ResultExt, Snafu};
46use url::Url;
47
48/// The DNS name for the iroh TXT record.
49pub const IROH_TXT_NAME: &str = "_iroh";
50
51#[common_fields({
52    backtrace: Option<Backtrace>,
53    #[snafu(implicit)]
54    span_trace: n0_snafu::SpanTrace,
55})]
56#[allow(missing_docs)]
57#[derive(Debug, Snafu)]
58#[non_exhaustive]
59#[snafu(visibility(pub(crate)))]
60pub enum EncodingError {
61    #[snafu(transparent)]
62    FailedBuildingPacket {
63        source: pkarr::errors::SignedPacketBuildError,
64    },
65    #[snafu(display("invalid TXT entry"))]
66    InvalidTxtEntry { source: pkarr::dns::SimpleDnsError },
67}
68
69#[common_fields({
70    backtrace: Option<Backtrace>,
71    #[snafu(implicit)]
72    span_trace: n0_snafu::SpanTrace,
73})]
74#[allow(missing_docs)]
75#[derive(Debug, Snafu)]
76#[non_exhaustive]
77#[snafu(visibility(pub(crate)))]
78pub enum DecodingError {
79    #[snafu(display("node id was not encoded in valid z32"))]
80    InvalidEncodingZ32 { source: z32::Z32Error },
81    #[snafu(display("length must be 32 bytes, but got {len} byte(s)"))]
82    InvalidLength { len: usize },
83    #[snafu(display("node id is not a valid public key"))]
84    InvalidSignature { source: SignatureError },
85}
86
87/// Extension methods for [`NodeId`] to encode to and decode from [`z32`],
88/// which is the encoding used in [`pkarr`] domain names.
89pub trait NodeIdExt {
90    /// Encodes a [`NodeId`] in [`z-base-32`] encoding.
91    ///
92    /// [`z-base-32`]: https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt
93    fn to_z32(&self) -> String;
94
95    /// Parses a [`NodeId`] from [`z-base-32`] encoding.
96    ///
97    /// [`z-base-32`]: https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt
98    fn from_z32(s: &str) -> Result<NodeId, DecodingError>;
99}
100
101impl NodeIdExt for NodeId {
102    fn to_z32(&self) -> String {
103        z32::encode(self.as_bytes())
104    }
105
106    fn from_z32(s: &str) -> Result<NodeId, DecodingError> {
107        let bytes = z32::decode(s.as_bytes()).context(InvalidEncodingZ32Snafu)?;
108        let bytes: &[u8; 32] = &bytes
109            .try_into()
110            .map_err(|_| InvalidLengthSnafu { len: s.len() }.build())?;
111        let node_id = NodeId::from_bytes(bytes).context(InvalidSignatureSnafu)?;
112        Ok(node_id)
113    }
114}
115
116/// Data about a node that may be published to and resolved from discovery services.
117///
118/// This includes an optional [`RelayUrl`], a set of direct addresses, and the optional
119/// [`UserData`], a string that can be set by applications and is not parsed or used by iroh
120/// itself.
121///
122/// This struct does not include the node's [`NodeId`], only the data *about* a certain
123/// node. See [`NodeInfo`] for a struct that contains a [`NodeId`] with associated [`NodeData`].
124#[derive(Debug, Clone, Default, Eq, PartialEq)]
125pub struct NodeData {
126    /// URL of the home relay of this node.
127    relay_url: Option<RelayUrl>,
128    /// Direct addresses where this node can be reached.
129    direct_addresses: BTreeSet<SocketAddr>,
130    /// Optional user-defined [`UserData`] for this node.
131    user_data: Option<UserData>,
132}
133
134impl NodeData {
135    /// Creates a new [`NodeData`] with a relay URL and a set of direct addresses.
136    pub fn new(relay_url: Option<RelayUrl>, direct_addresses: BTreeSet<SocketAddr>) -> Self {
137        Self {
138            relay_url,
139            direct_addresses,
140            user_data: None,
141        }
142    }
143
144    /// Sets the relay URL and returns the updated node data.
145    pub fn with_relay_url(mut self, relay_url: Option<RelayUrl>) -> Self {
146        self.relay_url = relay_url;
147        self
148    }
149
150    /// Sets the direct addresses and returns the updated node data.
151    pub fn with_direct_addresses(mut self, direct_addresses: BTreeSet<SocketAddr>) -> Self {
152        self.direct_addresses = direct_addresses;
153        self
154    }
155
156    /// Sets the user-defined data and returns the updated node data.
157    pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
158        self.user_data = user_data;
159        self
160    }
161
162    /// Returns the relay URL of the node.
163    pub fn relay_url(&self) -> Option<&RelayUrl> {
164        self.relay_url.as_ref()
165    }
166
167    /// Returns the optional user-defined data of the node.
168    pub fn user_data(&self) -> Option<&UserData> {
169        self.user_data.as_ref()
170    }
171
172    /// Returns the direct addresses of the node.
173    pub fn direct_addresses(&self) -> &BTreeSet<SocketAddr> {
174        &self.direct_addresses
175    }
176
177    /// Removes all direct addresses from the node data.
178    pub fn clear_direct_addresses(&mut self) {
179        self.direct_addresses = Default::default();
180    }
181
182    /// Adds direct addresses to the node data.
183    pub fn add_direct_addresses(&mut self, addrs: impl IntoIterator<Item = SocketAddr>) {
184        self.direct_addresses.extend(addrs)
185    }
186
187    /// Sets the relay URL of the node data.
188    pub fn set_relay_url(&mut self, relay_url: Option<RelayUrl>) {
189        self.relay_url = relay_url
190    }
191
192    /// Sets the user-defined data of the node data.
193    pub fn set_user_data(&mut self, user_data: Option<UserData>) {
194        self.user_data = user_data;
195    }
196}
197
198impl From<NodeAddr> for NodeData {
199    fn from(node_addr: NodeAddr) -> Self {
200        Self {
201            relay_url: node_addr.relay_url,
202            direct_addresses: node_addr.direct_addresses,
203            user_data: None,
204        }
205    }
206}
207
208// User-defined data that can be published and resolved through node discovery.
209///
210/// Under the hood this is a UTF-8 String is no longer than [`UserData::MAX_LENGTH`] bytes.
211///
212/// Iroh does not keep track of or examine the user-defined data.
213///
214/// `UserData` implements [`FromStr`] and [`TryFrom<String>`], so you can
215/// convert `&str` and `String` into `UserData` easily.
216#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
217pub struct UserData(String);
218
219impl UserData {
220    /// The max byte length allowed for user-defined data.
221    ///
222    /// In DNS discovery services, the user-defined data is stored in a TXT record character string,
223    /// which has a max length of 255 bytes. We need to subtract the `user-data=` prefix,
224    /// which leaves 245 bytes for the actual user-defined data.
225    pub const MAX_LENGTH: usize = 245;
226}
227
228/// Error returned when an input value is too long for [`UserData`].
229#[allow(missing_docs)]
230#[derive(Debug, Snafu)]
231pub struct MaxLengthExceededError {
232    backtrace: Option<Backtrace>,
233    #[snafu(implicit)]
234    span_trace: n0_snafu::SpanTrace,
235}
236
237impl TryFrom<String> for UserData {
238    type Error = MaxLengthExceededError;
239
240    fn try_from(value: String) -> Result<Self, Self::Error> {
241        snafu::ensure!(value.len() <= Self::MAX_LENGTH, MaxLengthExceededSnafu);
242        Ok(Self(value))
243    }
244}
245
246impl FromStr for UserData {
247    type Err = MaxLengthExceededError;
248
249    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
250        snafu::ensure!(s.len() <= Self::MAX_LENGTH, MaxLengthExceededSnafu);
251        Ok(Self(s.to_string()))
252    }
253}
254
255impl fmt::Display for UserData {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write!(f, "{}", self.0)
258    }
259}
260
261impl AsRef<str> for UserData {
262    fn as_ref(&self) -> &str {
263        &self.0
264    }
265}
266
267/// Information about a node that may be published to and resolved from discovery services.
268///
269/// This struct couples a [`NodeId`] with its associated [`NodeData`].
270#[derive(derive_more::Debug, Clone, Eq, PartialEq)]
271pub struct NodeInfo {
272    /// The [`NodeId`] of the node this is about.
273    pub node_id: NodeId,
274    /// The information published about the node.
275    pub data: NodeData,
276}
277
278impl From<TxtAttrs<IrohAttr>> for NodeInfo {
279    fn from(attrs: TxtAttrs<IrohAttr>) -> Self {
280        (&attrs).into()
281    }
282}
283
284impl From<&TxtAttrs<IrohAttr>> for NodeInfo {
285    fn from(attrs: &TxtAttrs<IrohAttr>) -> Self {
286        let node_id = attrs.node_id();
287        let attrs = attrs.attrs();
288        let relay_url = attrs
289            .get(&IrohAttr::Relay)
290            .into_iter()
291            .flatten()
292            .next()
293            .and_then(|s| Url::parse(s).ok());
294        let direct_addresses = attrs
295            .get(&IrohAttr::Addr)
296            .into_iter()
297            .flatten()
298            .filter_map(|s| SocketAddr::from_str(s).ok())
299            .collect();
300        let user_data = attrs
301            .get(&IrohAttr::UserData)
302            .into_iter()
303            .flatten()
304            .next()
305            .and_then(|s| UserData::from_str(s).ok());
306        let data = NodeData {
307            relay_url: relay_url.map(Into::into),
308            direct_addresses,
309            user_data,
310        };
311        Self { node_id, data }
312    }
313}
314
315impl From<NodeInfo> for NodeAddr {
316    fn from(value: NodeInfo) -> Self {
317        value.into_node_addr()
318    }
319}
320
321impl From<NodeAddr> for NodeInfo {
322    fn from(addr: NodeAddr) -> Self {
323        Self::new(addr.node_id)
324            .with_relay_url(addr.relay_url)
325            .with_direct_addresses(addr.direct_addresses)
326    }
327}
328
329impl NodeInfo {
330    /// Creates a new [`NodeInfo`] with an empty [`NodeData`].
331    pub fn new(node_id: NodeId) -> Self {
332        Self::from_parts(node_id, Default::default())
333    }
334
335    /// Creates a new [`NodeInfo`] from its parts.
336    pub fn from_parts(node_id: NodeId, data: NodeData) -> Self {
337        Self { node_id, data }
338    }
339
340    /// Sets the relay URL and returns the updated node info.
341    pub fn with_relay_url(mut self, relay_url: Option<RelayUrl>) -> Self {
342        self.data = self.data.with_relay_url(relay_url);
343        self
344    }
345
346    /// Sets the direct addresses and returns the updated node info.
347    pub fn with_direct_addresses(mut self, direct_addresses: BTreeSet<SocketAddr>) -> Self {
348        self.data = self.data.with_direct_addresses(direct_addresses);
349        self
350    }
351
352    /// Sets the user-defined data and returns the updated node info.
353    pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
354        self.data = self.data.with_user_data(user_data);
355        self
356    }
357
358    /// Converts into a [`NodeAddr`] by cloning the needed fields.
359    pub fn to_node_addr(&self) -> NodeAddr {
360        NodeAddr {
361            node_id: self.node_id,
362            relay_url: self.data.relay_url.clone(),
363            direct_addresses: self.data.direct_addresses.clone(),
364        }
365    }
366
367    /// Converts into a [`NodeAddr`] without cloning.
368    pub fn into_node_addr(self) -> NodeAddr {
369        NodeAddr {
370            node_id: self.node_id,
371            relay_url: self.data.relay_url,
372            direct_addresses: self.data.direct_addresses,
373        }
374    }
375
376    fn to_attrs(&self) -> TxtAttrs<IrohAttr> {
377        self.into()
378    }
379
380    #[cfg(not(wasm_browser))]
381    /// Parses a [`NodeInfo`] from DNS TXT lookup.
382    pub fn from_txt_lookup(
383        domain_name: String,
384        lookup: impl Iterator<Item = crate::dns::TxtRecordData>,
385    ) -> Result<Self, ParseError> {
386        let attrs = TxtAttrs::from_txt_lookup(domain_name, lookup)?;
387        Ok(Self::from(attrs))
388    }
389
390    /// Parses a [`NodeInfo`] from a [`pkarr::SignedPacket`].
391    pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result<Self, ParseError> {
392        let attrs = TxtAttrs::from_pkarr_signed_packet(packet)?;
393        Ok(attrs.into())
394    }
395
396    /// Creates a [`pkarr::SignedPacket`].
397    ///
398    /// This constructs a DNS packet and signs it with a [`SecretKey`].
399    pub fn to_pkarr_signed_packet(
400        &self,
401        secret_key: &SecretKey,
402        ttl: u32,
403    ) -> Result<pkarr::SignedPacket, EncodingError> {
404        self.to_attrs().to_pkarr_signed_packet(secret_key, ttl)
405    }
406
407    /// Converts into a list of `{key}={value}` strings.
408    pub fn to_txt_strings(&self) -> Vec<String> {
409        self.to_attrs().to_txt_strings().collect()
410    }
411}
412
413#[common_fields({
414    backtrace: Option<Backtrace>,
415    #[snafu(implicit)]
416    span_trace: n0_snafu::SpanTrace,
417})]
418#[allow(missing_docs)]
419#[derive(Debug, Snafu)]
420#[non_exhaustive]
421#[snafu(visibility(pub(crate)))]
422pub enum ParseError {
423    #[snafu(display("Expected format `key=value`, received `{s}`"))]
424    UnexpectedFormat { s: String },
425    #[snafu(display("Could not convert key to Attr"))]
426    AttrFromString { key: String },
427    #[snafu(display("Expected 2 labels, received {num_labels}"))]
428    NumLabels { num_labels: usize },
429    #[snafu(display("Could not parse labels"))]
430    Utf8 { source: Utf8Error },
431    #[snafu(display("Record is not an `iroh` record, expected `_iroh`, got `{label}`"))]
432    NotAnIrohRecord { label: String },
433    #[snafu(transparent)]
434    DecodingError {
435        #[snafu(source(from(DecodingError, Box::new)))]
436        source: Box<DecodingError>,
437    },
438}
439
440impl std::ops::Deref for NodeInfo {
441    type Target = NodeData;
442    fn deref(&self) -> &Self::Target {
443        &self.data
444    }
445}
446
447impl std::ops::DerefMut for NodeInfo {
448    fn deref_mut(&mut self) -> &mut Self::Target {
449        &mut self.data
450    }
451}
452
453/// Parses a [`NodeId`] from iroh DNS name.
454///
455/// Takes a [`hickory_resolver::proto::rr::Name`] DNS name and expects the first label to be
456/// [`IROH_TXT_NAME`] and the second label to be a z32 encoded [`NodeId`]. Ignores
457/// subsequent labels.
458#[cfg(not(wasm_browser))]
459fn node_id_from_txt_name(name: &str) -> Result<NodeId, ParseError> {
460    let num_labels = name.split(".").count();
461    if num_labels < 2 {
462        return Err(NumLabelsSnafu { num_labels }.build());
463    }
464    let mut labels = name.split(".");
465    let label = labels.next().expect("checked above");
466    if label != IROH_TXT_NAME {
467        return Err(NotAnIrohRecordSnafu { label }.build());
468    }
469    let label = labels.next().expect("checked above");
470    let node_id = NodeId::from_z32(label)?;
471    Ok(node_id)
472}
473
474/// The attributes supported by iroh for [`IROH_TXT_NAME`] DNS resource records.
475///
476/// The resource record uses the lower-case names.
477#[derive(
478    Debug, strum::Display, strum::AsRefStr, strum::EnumString, Hash, Eq, PartialEq, Ord, PartialOrd,
479)]
480#[strum(serialize_all = "kebab-case")]
481pub(crate) enum IrohAttr {
482    /// URL of home relay.
483    Relay,
484    /// Direct address.
485    Addr,
486    /// User-defined data
487    UserData,
488}
489
490/// Attributes parsed from [`IROH_TXT_NAME`] TXT records.
491///
492/// This struct is generic over the key type. When using with [`String`], this will parse
493/// all attributes. Can also be used with an enum, if it implements [`FromStr`] and
494/// [`Display`].
495#[derive(Debug)]
496pub(crate) struct TxtAttrs<T> {
497    node_id: NodeId,
498    attrs: BTreeMap<T, Vec<String>>,
499}
500
501impl From<&NodeInfo> for TxtAttrs<IrohAttr> {
502    fn from(info: &NodeInfo) -> Self {
503        let mut attrs = vec![];
504        if let Some(relay_url) = &info.data.relay_url {
505            attrs.push((IrohAttr::Relay, relay_url.to_string()));
506        }
507        for addr in &info.data.direct_addresses {
508            attrs.push((IrohAttr::Addr, addr.to_string()));
509        }
510        if let Some(user_data) = &info.data.user_data {
511            attrs.push((IrohAttr::UserData, user_data.to_string()));
512        }
513        Self::from_parts(info.node_id, attrs.into_iter())
514    }
515}
516
517impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
518    /// Creates [`TxtAttrs`] from a node id and an iterator of key-value pairs.
519    pub(crate) fn from_parts(node_id: NodeId, pairs: impl Iterator<Item = (T, String)>) -> Self {
520        let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
521        for (k, v) in pairs {
522            attrs.entry(k).or_default().push(v);
523        }
524        Self { attrs, node_id }
525    }
526
527    /// Creates [`TxtAttrs`] from a node id and an iterator of "{key}={value}" strings.
528    pub(crate) fn from_strings(
529        node_id: NodeId,
530        strings: impl Iterator<Item = String>,
531    ) -> Result<Self, ParseError> {
532        let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
533        for s in strings {
534            let mut parts = s.split('=');
535            let (Some(key), Some(value)) = (parts.next(), parts.next()) else {
536                return Err(UnexpectedFormatSnafu { s }.build());
537            };
538            let attr = T::from_str(key).map_err(|_| AttrFromStringSnafu { key }.build())?;
539            attrs.entry(attr).or_default().push(value.to_string());
540        }
541        Ok(Self { attrs, node_id })
542    }
543
544    /// Returns the parsed attributes.
545    pub(crate) fn attrs(&self) -> &BTreeMap<T, Vec<String>> {
546        &self.attrs
547    }
548
549    /// Returns the node id.
550    pub(crate) fn node_id(&self) -> NodeId {
551        self.node_id
552    }
553
554    /// Parses a [`pkarr::SignedPacket`].
555    pub(crate) fn from_pkarr_signed_packet(
556        packet: &pkarr::SignedPacket,
557    ) -> Result<Self, ParseError> {
558        use pkarr::dns::{
559            rdata::RData,
560            {self},
561        };
562        let pubkey = packet.public_key();
563        let pubkey_z32 = pubkey.to_z32();
564        let node_id = NodeId::from(*pubkey.verifying_key());
565        let zone = dns::Name::new(&pubkey_z32).expect("z32 encoding is valid");
566        let txt_data = packet
567            .all_resource_records()
568            .filter_map(|rr| match &rr.rdata {
569                RData::TXT(txt) => match rr.name.without(&zone) {
570                    Some(name) if name.to_string() == IROH_TXT_NAME => Some(txt),
571                    Some(_) | None => None,
572                },
573                _ => None,
574            });
575
576        let txt_strs = txt_data.filter_map(|s| String::try_from(s.clone()).ok());
577        Self::from_strings(node_id, txt_strs)
578    }
579
580    /// Parses a TXT records lookup.
581    #[cfg(not(wasm_browser))]
582    pub(crate) fn from_txt_lookup(
583        name: String,
584        lookup: impl Iterator<Item = crate::dns::TxtRecordData>,
585    ) -> Result<Self, ParseError> {
586        let queried_node_id = node_id_from_txt_name(&name)?;
587
588        let strings = lookup.map(|record| record.to_string());
589        Self::from_strings(queried_node_id, strings)
590    }
591
592    fn to_txt_strings(&self) -> impl Iterator<Item = String> + '_ {
593        self.attrs
594            .iter()
595            .flat_map(move |(k, vs)| vs.iter().map(move |v| format!("{k}={v}")))
596    }
597
598    /// Creates a [`pkarr::SignedPacket`]
599    ///
600    /// This constructs a DNS packet and signs it with a [`SecretKey`].
601    pub(crate) fn to_pkarr_signed_packet(
602        &self,
603        secret_key: &SecretKey,
604        ttl: u32,
605    ) -> Result<pkarr::SignedPacket, EncodingError> {
606        use pkarr::dns::{self, rdata};
607        let keypair = pkarr::Keypair::from_secret_key(&secret_key.to_bytes());
608        let name = dns::Name::new(IROH_TXT_NAME).expect("constant");
609
610        let mut builder = pkarr::SignedPacket::builder();
611        for s in self.to_txt_strings() {
612            let mut txt = rdata::TXT::new();
613            txt.add_string(&s).context(InvalidTxtEntrySnafu)?;
614            builder = builder.txt(name.clone(), txt.into_owned(), ttl);
615        }
616        let signed_packet = builder.build(&keypair)?;
617        Ok(signed_packet)
618    }
619}
620
621#[cfg(not(wasm_browser))]
622pub(crate) fn ensure_iroh_txt_label(name: String) -> String {
623    let mut parts = name.split(".");
624    if parts.next() == Some(IROH_TXT_NAME) {
625        name
626    } else {
627        format!("{IROH_TXT_NAME}.{name}")
628    }
629}
630
631#[cfg(not(wasm_browser))]
632pub(crate) fn node_domain(node_id: &NodeId, origin: &str) -> String {
633    format!("{}.{}", NodeId::to_z32(node_id), origin)
634}
635
636#[cfg(test)]
637mod tests {
638    use std::{collections::BTreeSet, str::FromStr, sync::Arc};
639
640    use hickory_resolver::{
641        Name,
642        lookup::Lookup,
643        proto::{
644            op::Query,
645            rr::{
646                RData, Record, RecordType,
647                rdata::{A, TXT},
648            },
649        },
650    };
651    use iroh_base::{NodeId, SecretKey};
652    use n0_snafu::{Result, ResultExt};
653
654    use super::{NodeData, NodeIdExt, NodeInfo};
655    use crate::dns::TxtRecordData;
656
657    #[test]
658    fn txt_attr_roundtrip() {
659        let node_data = NodeData::new(
660            Some("https://example.com".parse().unwrap()),
661            ["127.0.0.1:1234".parse().unwrap()].into_iter().collect(),
662        )
663        .with_user_data(Some("foobar".parse().unwrap()));
664        let node_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
665            .parse()
666            .unwrap();
667        let expected = NodeInfo::from_parts(node_id, node_data);
668        let attrs = expected.to_attrs();
669        let actual = NodeInfo::from(&attrs);
670        assert_eq!(expected, actual);
671    }
672
673    #[test]
674    fn signed_packet_roundtrip() {
675        let secret_key =
676            SecretKey::from_str("vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia").unwrap();
677        let node_data = NodeData::new(
678            Some("https://example.com".parse().unwrap()),
679            ["127.0.0.1:1234".parse().unwrap()].into_iter().collect(),
680        )
681        .with_user_data(Some("foobar".parse().unwrap()));
682        let expected = NodeInfo::from_parts(secret_key.public(), node_data);
683        let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
684        let actual = NodeInfo::from_pkarr_signed_packet(&packet).unwrap();
685        assert_eq!(expected, actual);
686    }
687
688    /// There used to be a bug where uploading a NodeAddr with more than only exactly
689    /// one relay URL or one publicly reachable IP addr would prevent connection
690    /// establishment.
691    ///
692    /// The reason was that only the first address was parsed (e.g. 192.168.96.145 in
693    /// this example), which could be a local, unreachable address.
694    #[test]
695    fn test_from_hickory_lookup() -> Result {
696        let name = Name::from_utf8(
697            "_iroh.dgjpkxyn3zyrk3zfads5duwdgbqpkwbjxfj4yt7rezidr3fijccy.dns.iroh.link.",
698        )
699        .context("dns name")?;
700        let query = Query::query(name.clone(), RecordType::TXT);
701        let records = [
702            Record::from_rdata(
703                name.clone(),
704                30,
705                RData::TXT(TXT::new(vec!["addr=192.168.96.145:60165".to_string()])),
706            ),
707            Record::from_rdata(
708                name.clone(),
709                30,
710                RData::TXT(TXT::new(vec!["addr=213.208.157.87:60165".to_string()])),
711            ),
712            // Test a record with mismatching record type (A instead of TXT). It should be filtered out.
713            Record::from_rdata(name.clone(), 30, RData::A(A::new(127, 0, 0, 1))),
714            // Test a record with a mismatching name
715            Record::from_rdata(
716                Name::from_utf8(format!(
717                    "_iroh.{}.dns.iroh.link.",
718                    NodeId::from_str(
719                        // Another NodeId
720                        "a55f26132e5e43de834d534332f66a20d480c3e50a13a312a071adea6569981e"
721                    )?
722                    .to_z32()
723                ))
724                .context("name")?,
725                30,
726                RData::TXT(TXT::new(vec![
727                    "relay=https://euw1-1.relay.iroh.network./".to_string(),
728                ])),
729            ),
730            // Test a record with a completely different name
731            Record::from_rdata(
732                Name::from_utf8("dns.iroh.link.").context("name")?,
733                30,
734                RData::TXT(TXT::new(vec![
735                    "relay=https://euw1-1.relay.iroh.network./".to_string(),
736                ])),
737            ),
738            Record::from_rdata(
739                name.clone(),
740                30,
741                RData::TXT(TXT::new(vec![
742                    "relay=https://euw1-1.relay.iroh.network./".to_string(),
743                ])),
744            ),
745        ];
746        let lookup = Lookup::new_with_max_ttl(query, Arc::new(records));
747        let lookup = hickory_resolver::lookup::TxtLookup::from(lookup);
748        let lookup = lookup
749            .into_iter()
750            .map(|txt| TxtRecordData::from_iter(txt.iter().cloned()));
751
752        let node_info = NodeInfo::from_txt_lookup(name.to_string(), lookup)?;
753
754        let expected_node_info = NodeInfo::new(NodeId::from_str(
755            "1992d53c02cdc04566e5c0edb1ce83305cd550297953a047a445ea3264b54b18",
756        )?)
757        .with_relay_url(Some("https://euw1-1.relay.iroh.network./".parse()?))
758        .with_direct_addresses(BTreeSet::from([
759            "192.168.96.145:60165".parse().unwrap(),
760            "213.208.157.87:60165".parse().unwrap(),
761        ]));
762
763        assert_eq!(node_info, expected_node_info);
764
765        Ok(())
766    }
767}