Skip to main content

iroh_dns/
endpoint_info.rs

1//! Support for handling DNS resource records for dialing by [`EndpointId`].
2//!
3//! Dialing by [`EndpointId`] is supported by iroh endpoints publishing [Pkarr] records to DNS
4//! servers or the Mainline DHT.  This module supports creating and parsing these records.
5//!
6//! [`EndpointInfo`] combines an [`iroh_base::EndpointId`] with [`EndpointData`]:
7//! the addressing and metadata that discovery services publish and resolve.
8//! Discovery services use [`AddrFilter`] to control which addresses are published.
9//!
10//! This module also provides serialization to and from pkarr signed packets and
11//! DNS TXT records.
12//!
13//! DNS records are published under the following names:
14//!
15//! `_iroh.<z32-endpoint-id>.<origin-domain> TXT`
16//!
17//! - `_iroh` is the record name as defined by [`IROH_TXT_NAME`].
18//!
19//! - `<z32-endpoint-id>` is the [z-base-32] encoding of the [`EndpointId`].
20//!
21//! - `<origin-domain>` is the domain name of the publishing DNS server,
22//!   [`N0_DNS_ENDPOINT_ORIGIN_PROD`] is the server operated by number0 for production.
23//!   [`N0_DNS_ENDPOINT_ORIGIN_STAGING`] is the server operated by number0 for testing.
24//!
25//! - `TXT` is the DNS record type.
26//!
27//! The returned TXT records must contain a string value of the form `key=value` as defined
28//! in [RFC1464].  The following attributes are defined:
29//!
30//! - `relay=<url>`: The home [`RelayUrl`] of this endpoint.
31//!
32//! - `addr=<addr> <addr>`: A space-separated list of sockets addresses for this iroh endpoint.
33//!   Each address is an IPv4 or IPv6 address with a port.
34//!
35//! [Pkarr]: https://app.pkarr.org
36//! [z-base-32]: https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt
37//! [RFC1464]: https://www.rfc-editor.org/rfc/rfc1464
38//! [`RelayUrl`]: iroh_base::RelayUrl
39//! [`IROH_TXT_NAME`]: crate::IROH_TXT_NAME
40//! [`N0_DNS_ENDPOINT_ORIGIN_PROD`]: crate::dns::N0_DNS_ENDPOINT_ORIGIN_PROD
41//! [`N0_DNS_ENDPOINT_ORIGIN_STAGING`]: crate::dns::N0_DNS_ENDPOINT_ORIGIN_STAGING
42
43use std::{
44    borrow::Cow,
45    collections::{BTreeSet, HashSet},
46    fmt::{self, Display},
47    hash::Hash,
48    net::SocketAddr,
49    str::FromStr,
50    sync::Arc,
51};
52
53use iroh_base::{EndpointAddr, EndpointId, RelayUrl, SecretKey, TransportAddr};
54use n0_error::{ensure, stack_error};
55use url::Url;
56
57use crate::{
58    attrs::{EncodingError, IrohAttr, ParseError, TxtAttrs},
59    pkarr,
60};
61
62/// Data about an endpoint that may be published to and resolved from discovery services.
63///
64/// This includes an optional [`RelayUrl`], a set of direct addresses, and the optional
65/// [`UserData`], a string that can be set by applications and is not parsed or used by iroh
66/// itself.
67///
68/// This struct does not include the endpoint's [`EndpointId`], only the data *about* a certain
69/// endpoint. See [`EndpointInfo`] for a struct that contains a [`EndpointId`] with associated [`EndpointData`].
70#[derive(Debug, Clone, Default, Eq, PartialEq)]
71pub struct EndpointData {
72    /// addresses where this endpoint can be reached.
73    addrs: Vec<TransportAddr>,
74    /// Optional user-defined [`UserData`] for this endpoint.
75    user_data: Option<UserData>,
76}
77
78fn dedup<T: Eq + Hash + Clone>(items: &mut Vec<T>) -> HashSet<T> {
79    // Remove all duplicate entries, but keep the array order.
80    let mut seen = HashSet::new();
81    items.retain(|item| seen.insert(item.clone()));
82    seen
83}
84
85impl EndpointData {
86    /// Creates a new [`EndpointData`] with given list of transport addresses.
87    ///
88    /// The address order is preserved, so it can encode priority for address lookup
89    /// services, should they not fit into e.g. a single DNS packet otherwise.
90    ///
91    /// If the addresses contain duplicate entries, those entries are removed.
92    pub fn new(mut addrs: Vec<TransportAddr>) -> Self {
93        dedup(&mut addrs);
94        Self {
95            addrs,
96            user_data: None,
97        }
98    }
99
100    /// Sets the user-defined data and returns the updated endpoint info.
101    ///
102    /// Useful for calling on construction after [`EndpointData::new`] or [`EndpointData::from_iter`].
103    ///
104    /// See also [`Self::set_user_data`].
105    pub fn with_user_data(mut self, user_data: UserData) -> Self {
106        self.user_data = Some(user_data);
107        self
108    }
109
110    /// Adds the relay URL to the end of the endpoint data, unless it already existed.
111    pub fn add_relay_url(&mut self, relay_url: RelayUrl) {
112        let addr = TransportAddr::Relay(relay_url);
113        if !self.addrs.contains(&addr) {
114            self.addrs.push(addr);
115        }
116    }
117
118    /// Adds addresses in order with duplicates or already existing addresses filtered out.
119    pub fn add_ip_addrs(&mut self, addresses: Vec<SocketAddr>) {
120        self.add_addrs(addresses.into_iter().map(TransportAddr::Ip))
121    }
122
123    /// Adds addresses to the endpoint data in the given order, but with duplicates filtered.
124    pub fn add_addrs(&mut self, addrs: impl IntoIterator<Item = TransportAddr>) {
125        let mut addr_set = dedup(&mut self.addrs);
126        for addr in addrs.into_iter() {
127            if !addr_set.contains(&addr) {
128                self.addrs.push(addr.clone());
129                addr_set.insert(addr);
130            }
131        }
132    }
133
134    /// Sets the user-defined data.
135    pub fn set_user_data(&mut self, user_data: Option<UserData>) {
136        self.user_data = user_data;
137    }
138
139    /// Removes all direct addresses from the endpoint data.
140    pub fn clear_ip_addrs(&mut self) {
141        self.addrs
142            .retain(|addr| !matches!(addr, TransportAddr::Ip(_)));
143    }
144
145    /// Removes all relay URLs from the endpoint data.
146    pub fn clear_relay_urls(&mut self) {
147        self.addrs
148            .retain(|addr| !matches!(addr, TransportAddr::Relay(_)));
149    }
150
151    /// Returns the relay URL of the endpoint.
152    pub fn relay_urls(&self) -> impl Iterator<Item = &RelayUrl> {
153        self.addrs.iter().filter_map(|addr| match addr {
154            TransportAddr::Relay(url) => Some(url),
155            _ => None,
156        })
157    }
158
159    /// Returns the optional user-defined data of the endpoint.
160    pub fn user_data(&self) -> Option<&UserData> {
161        self.user_data.as_ref()
162    }
163
164    /// Returns the direct addresses of the endpoint.
165    pub fn ip_addrs(&self) -> impl Iterator<Item = &SocketAddr> {
166        self.addrs.iter().filter_map(|addr| match addr {
167            TransportAddr::Ip(addr) => Some(addr),
168            _ => None,
169        })
170    }
171
172    /// Returns the full list of all known addresses.
173    pub fn addrs(&self) -> impl Iterator<Item = &TransportAddr> {
174        self.addrs.iter()
175    }
176
177    /// Returns whether this has any addresses.
178    pub fn has_addrs(&self) -> bool {
179        !self.addrs.is_empty()
180    }
181
182    /// Apply the given filter to the current addresses.
183    ///
184    /// Returns a vec to allow re-ordering of addresses.
185    pub fn filtered_addrs(&self, filter: &AddrFilter) -> Cow<'_, Vec<TransportAddr>> {
186        filter.apply(&self.addrs)
187    }
188
189    /// Returns the `EndpointData` with given filter applied.
190    pub fn apply_filter(&self, filter: &AddrFilter) -> Cow<'_, Self> {
191        match self.filtered_addrs(filter) {
192            Cow::Borrowed(_) => Cow::Borrowed(self),
193            Cow::Owned(addrs) => {
194                let mut data = EndpointData::new(addrs);
195                data.set_user_data(self.user_data.clone());
196                Cow::Owned(data)
197            }
198        }
199    }
200}
201
202// These From instances are faster than `EndpointData::new`, as they don't require deduplication.
203
204impl From<BTreeSet<TransportAddr>> for EndpointData {
205    fn from(addrs: BTreeSet<TransportAddr>) -> Self {
206        Self {
207            addrs: addrs.into_iter().collect(),
208            user_data: None,
209        }
210    }
211}
212
213impl From<BTreeSet<SocketAddr>> for EndpointData {
214    fn from(addrs: BTreeSet<SocketAddr>) -> Self {
215        Self {
216            addrs: addrs.into_iter().map(TransportAddr::Ip).collect(),
217            user_data: None,
218        }
219    }
220}
221
222impl FromIterator<TransportAddr> for EndpointData {
223    fn from_iter<T: IntoIterator<Item = TransportAddr>>(iter: T) -> Self {
224        Self::new(iter.into_iter().collect())
225    }
226}
227
228/// The function type inside [`AddrFilter`].
229type AddrFilterFn =
230    dyn Fn(&Vec<TransportAddr>) -> Cow<'_, Vec<TransportAddr>> + Send + Sync + 'static;
231
232/// A filter and/or reordering function applied to transport addresses,
233/// typically used by AddressLookup services in iroh before publishing.
234///
235/// Takes the full set of transport addresses and returns them as an ordered `Vec`,
236/// allowing both filtering (by omitting addresses) and reordering (by controlling
237/// the output order). A `BTreeSet` cannot preserve a custom order, so the return
238/// type is `Vec` to make reordering possible.
239///
240/// See the documentation for each address lookup implementation for details on
241/// what additional filtering the implementation may perform on top.
242#[derive(Clone, Default)]
243pub struct AddrFilter(Option<Arc<AddrFilterFn>>);
244
245impl std::fmt::Debug for AddrFilter {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        if self.0.is_some() {
248            f.debug_struct("AddrFilter").finish_non_exhaustive()
249        } else {
250            write!(f, "identity")
251        }
252    }
253}
254
255impl AddrFilter {
256    /// Create a new [`AddrFilter`]
257    pub fn new(
258        f: impl Fn(&Vec<TransportAddr>) -> Cow<'_, Vec<TransportAddr>> + Send + Sync + 'static,
259    ) -> Self {
260        Self(Some(Arc::new(f)))
261    }
262
263    /// Constructs a filter that doesn't filter addresses and passes all through.
264    pub fn unfiltered() -> Self {
265        Self::new(|addrs| Cow::Borrowed(addrs))
266    }
267
268    /// Only keep relay addresses.
269    pub fn relay_only() -> Self {
270        Self::new(|addrs| Cow::Owned(addrs.iter().filter(|a| a.is_relay()).cloned().collect()))
271    }
272
273    /// Only keep direct IP addresses.
274    pub fn ip_only() -> Self {
275        Self::new(|addrs| Cow::Owned(addrs.iter().filter(|a| !a.is_relay()).cloned().collect()))
276    }
277
278    /// Apply the address filter function to a set of addresses.
279    pub fn apply<'a>(&self, addrs: &'a Vec<TransportAddr>) -> Cow<'a, Vec<TransportAddr>> {
280        match &self.0 {
281            Some(f) => f(addrs),
282            None => Cow::Borrowed(addrs),
283        }
284    }
285}
286
287impl From<EndpointAddr> for EndpointData {
288    fn from(endpoint_addr: EndpointAddr) -> Self {
289        Self {
290            // No need to check for duplicates - we already know they can't have duplicates
291            addrs: endpoint_addr.addrs.into_iter().collect(),
292            user_data: None,
293        }
294    }
295}
296
297/// User-defined data that can be published and resolved through endpoint discovery.
298///
299/// Under the hood this is a UTF-8 string no longer than [`UserData::MAX_LENGTH`] bytes.
300///
301/// Iroh does not keep track of or examine the user-defined data.
302///
303/// `UserData` implements [`FromStr`] and [`TryFrom<String>`], so you can
304/// convert `&str` and `String` into `UserData` easily.
305#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
306pub struct UserData(String);
307
308impl UserData {
309    /// The max byte length allowed for user-defined data.
310    ///
311    /// In DNS discovery services, the user-defined data is stored in a TXT record character string,
312    /// which has a max length of 255 bytes. We need to subtract the `user-data=` prefix,
313    /// which leaves 245 bytes for the actual user-defined data.
314    pub const MAX_LENGTH: usize = 245;
315}
316
317/// Error returned when an input value is too long for [`UserData`].
318#[allow(missing_docs)]
319#[stack_error(derive, add_meta)]
320#[error("max length exceeded")]
321pub struct MaxLengthExceededError {}
322
323impl TryFrom<String> for UserData {
324    type Error = MaxLengthExceededError;
325
326    fn try_from(value: String) -> Result<Self, Self::Error> {
327        ensure!(value.len() <= Self::MAX_LENGTH, MaxLengthExceededError);
328        Ok(Self(value))
329    }
330}
331
332impl FromStr for UserData {
333    type Err = MaxLengthExceededError;
334
335    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
336        ensure!(s.len() <= Self::MAX_LENGTH, MaxLengthExceededError);
337        Ok(Self(s.to_string()))
338    }
339}
340
341impl fmt::Display for UserData {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        write!(f, "{}", self.0)
344    }
345}
346
347impl AsRef<str> for UserData {
348    fn as_ref(&self) -> &str {
349        &self.0
350    }
351}
352
353/// Information about an endpoint that may be published to and resolved from discovery services.
354///
355/// This struct couples a [`EndpointId`] with its associated [`EndpointData`].
356#[derive(derive_more::Debug, Clone, Eq, PartialEq)]
357pub struct EndpointInfo {
358    /// The [`EndpointId`] of the endpoint this is about.
359    pub endpoint_id: EndpointId,
360    /// The information published about the endpoint.
361    pub data: EndpointData,
362}
363
364impl From<EndpointInfo> for EndpointAddr {
365    fn from(value: EndpointInfo) -> Self {
366        value.into_endpoint_addr()
367    }
368}
369
370impl From<EndpointAddr> for EndpointInfo {
371    fn from(addr: EndpointAddr) -> Self {
372        Self {
373            endpoint_id: addr.id,
374            data: EndpointData::from(addr.addrs),
375        }
376    }
377}
378
379impl EndpointInfo {
380    /// Creates a new [`EndpointInfo`] with an empty [`EndpointData`].
381    pub fn new(endpoint_id: EndpointId) -> Self {
382        Self::from_parts(endpoint_id, Default::default())
383    }
384
385    /// Creates a new [`EndpointInfo`] from its parts.
386    pub fn from_parts(endpoint_id: EndpointId, data: EndpointData) -> Self {
387        Self { endpoint_id, data }
388    }
389
390    /// Adds the relay URL and returns the updated endpoint info.
391    pub fn with_relay_url(mut self, relay_url: RelayUrl) -> Self {
392        self.data.add_relay_url(relay_url);
393        self
394    }
395
396    /// Sets the IP based addresses and returns the updated endpoint info.
397    pub fn with_ip_addrs(mut self, addrs: Vec<SocketAddr>) -> Self {
398        self.data.add_ip_addrs(addrs);
399        self
400    }
401
402    /// Sets the user-defined data and returns the updated endpoint info.
403    pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
404        self.data.set_user_data(user_data);
405        self
406    }
407
408    /// Converts into a [`EndpointAddr`] by cloning the needed fields.
409    pub fn to_endpoint_addr(&self) -> EndpointAddr {
410        EndpointAddr {
411            id: self.endpoint_id,
412            addrs: self.data.addrs.iter().cloned().collect(),
413        }
414    }
415
416    /// Converts into a [`EndpointAddr`].
417    pub fn into_endpoint_addr(self) -> EndpointAddr {
418        let Self { endpoint_id, data } = self;
419        EndpointAddr {
420            id: endpoint_id,
421            addrs: data.addrs.into_iter().collect(),
422        }
423    }
424
425    /// Converts to TXT attributes.
426    pub(crate) fn to_attrs(&self) -> TxtAttrs<IrohAttr> {
427        endpoint_info_to_attrs(self)
428    }
429
430    /// Returns the transport addr information.
431    pub fn addrs(&self) -> impl Iterator<Item = &TransportAddr> {
432        self.data.addrs()
433    }
434
435    /// Returns the relay URL of the endpoint.
436    pub fn relay_urls(&self) -> impl Iterator<Item = &RelayUrl> {
437        self.data.relay_urls()
438    }
439
440    /// Returns user data information, if set.
441    pub fn user_data(&self) -> Option<&UserData> {
442        self.data.user_data()
443    }
444
445    /// Returns the direct addresses of the endpoint.
446    pub fn ip_addrs(&self) -> impl Iterator<Item = &SocketAddr> {
447        self.data.ip_addrs()
448    }
449
450    /// Parses a [`EndpointInfo`] from DNS TXT lookup results.
451    ///
452    /// The `domain_name` is the queried DNS name (e.g. `_iroh.<z32>.<origin>`).
453    /// The `lookup` iterator yields TXT record values that implement [`Display`].
454    pub fn from_txt_lookup(
455        domain_name: String,
456        lookup: impl Iterator<Item = impl Display>,
457    ) -> Result<Self, ParseError> {
458        let attrs: TxtAttrs<IrohAttr> = TxtAttrs::from_txt_lookup(domain_name, lookup)?;
459        Ok(endpoint_info_from_attrs(&attrs))
460    }
461
462    /// Parses a [`EndpointInfo`] from a [`pkarr::SignedPacket`].
463    pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result<Self, ParseError> {
464        let attrs: TxtAttrs<IrohAttr> = TxtAttrs::from_pkarr_signed_packet(packet)?;
465        Ok(endpoint_info_from_attrs(&attrs))
466    }
467
468    /// Creates a [`pkarr::SignedPacket`].
469    ///
470    /// This constructs a DNS packet and signs it with a [`SecretKey`].
471    pub fn to_pkarr_signed_packet(
472        &self,
473        secret_key: &SecretKey,
474        ttl: u32,
475    ) -> Result<pkarr::SignedPacket, EncodingError> {
476        self.to_attrs().to_pkarr_signed_packet(secret_key, ttl)
477    }
478
479    /// Converts into a list of `{key}={value}` strings.
480    pub fn to_txt_strings(&self) -> Vec<String> {
481        self.to_attrs().to_txt_strings().collect()
482    }
483}
484
485/// Convert [`EndpointInfo`] to [`TxtAttrs`].
486fn endpoint_info_to_attrs(info: &EndpointInfo) -> TxtAttrs<IrohAttr> {
487    let mut attrs = vec![];
488    for addr in &info.data.addrs {
489        match addr {
490            TransportAddr::Relay(url) => attrs.push((IrohAttr::Relay, url.to_string())),
491            TransportAddr::Ip(addr) => attrs.push((IrohAttr::Addr, addr.to_string())),
492            TransportAddr::Custom(addr) => attrs.push((IrohAttr::Addr, addr.to_string())),
493            _ => {}
494        }
495    }
496
497    if let Some(user_data) = &info.data.user_data {
498        attrs.push((IrohAttr::UserData, user_data.to_string()));
499    }
500    TxtAttrs::from_parts(info.endpoint_id, attrs.into_iter())
501}
502
503/// Parse [`EndpointInfo`] from [`TxtAttrs`].
504fn endpoint_info_from_attrs(attrs: &TxtAttrs<IrohAttr>) -> EndpointInfo {
505    use iroh_base::CustomAddr;
506
507    let endpoint_id = attrs.endpoint_id();
508    let a = attrs.attrs();
509    let relay_urls = a
510        .get(&IrohAttr::Relay)
511        .into_iter()
512        .flatten()
513        .filter_map(|s| Url::parse(s).ok())
514        .map(|url| TransportAddr::Relay(url.into()));
515    let addrs = a
516        .get(&IrohAttr::Addr)
517        .into_iter()
518        .flatten()
519        .filter_map(|s| {
520            if let Ok(addr) = SocketAddr::from_str(s) {
521                Some(TransportAddr::Ip(addr))
522            } else if let Ok(addr) = CustomAddr::from_str(s) {
523                Some(TransportAddr::Custom(addr))
524            } else {
525                None
526            }
527        });
528
529    let user_data = a
530        .get(&IrohAttr::UserData)
531        .into_iter()
532        .flatten()
533        .next()
534        .and_then(|s| UserData::from_str(s).ok());
535    let mut data = EndpointData::default();
536    data.set_user_data(user_data);
537    data.add_addrs(relay_urls.chain(addrs));
538
539    EndpointInfo { endpoint_id, data }
540}
541
542#[cfg(test)]
543mod tests {
544    use std::str::FromStr;
545
546    use hickory_resolver::{
547        lookup::Lookup,
548        proto::{
549            op::Query,
550            rr::{
551                Name, RData, Record, RecordType,
552                rdata::{A, TXT},
553            },
554        },
555    };
556    use iroh_base::{EndpointId, SecretKey, TransportAddr};
557    use n0_error::{Result, StdResultExt};
558
559    use super::{EndpointData, EndpointInfo};
560    use crate::dns::TxtRecordData;
561
562    #[test]
563    fn txt_attr_roundtrip() {
564        let endpoint_data = EndpointData::from_iter([
565            TransportAddr::Relay("https://example.com".parse().unwrap()),
566            TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
567        ])
568        .with_user_data("foobar".parse().unwrap());
569        let endpoint_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
570            .parse()
571            .unwrap();
572        let expected = EndpointInfo::from_parts(endpoint_id, endpoint_data);
573        let attrs = expected.to_attrs();
574        let actual = super::endpoint_info_from_attrs(&attrs);
575        assert_eq!(expected, actual);
576    }
577
578    #[test]
579    fn signed_packet_roundtrip() {
580        let secret_key =
581            SecretKey::from_str("vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia").unwrap();
582        let endpoint_data = EndpointData::from_iter([
583            TransportAddr::Relay("https://example.com".parse().unwrap()),
584            TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
585        ])
586        .with_user_data("foobar".parse().unwrap());
587        let expected = EndpointInfo::from_parts(secret_key.public(), endpoint_data);
588        let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
589        let actual = EndpointInfo::from_pkarr_signed_packet(&packet).unwrap();
590        assert_eq!(expected, actual);
591    }
592
593    #[test]
594    fn txt_attr_roundtrip_with_custom_addr() {
595        use iroh_base::CustomAddr;
596
597        let bt_addr = CustomAddr::from_parts(1, &[0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6]);
598        let tor_addr = CustomAddr::from_parts(42, &[0xab; 32]);
599
600        let endpoint_data = EndpointData::from_iter([
601            TransportAddr::Relay("https://example.com".parse().unwrap()),
602            TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
603            TransportAddr::Custom(bt_addr),
604            TransportAddr::Custom(tor_addr),
605        ]);
606        let endpoint_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
607            .parse()
608            .unwrap();
609        let expected = EndpointInfo::from_parts(endpoint_id, endpoint_data);
610        let attrs = expected.to_attrs();
611        let actual = super::endpoint_info_from_attrs(&attrs);
612        assert_eq!(expected, actual);
613    }
614
615    #[test]
616    fn signed_packet_roundtrip_with_custom_addr() {
617        use iroh_base::CustomAddr;
618
619        let secret_key =
620            SecretKey::from_str("vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia").unwrap();
621
622        let bt_addr = CustomAddr::from_parts(1, &[0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6]);
623        let tor_addr = CustomAddr::from_parts(42, &[0xab; 32]);
624
625        let endpoint_data = EndpointData::from_iter([
626            TransportAddr::Relay("https://example.com".parse().unwrap()),
627            TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
628            TransportAddr::Custom(bt_addr),
629            TransportAddr::Custom(tor_addr),
630        ])
631        .with_user_data("foobar".parse().unwrap());
632
633        let expected = EndpointInfo::from_parts(secret_key.public(), endpoint_data);
634        let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
635        let actual = EndpointInfo::from_pkarr_signed_packet(&packet).unwrap();
636        assert_eq!(expected, actual);
637    }
638
639    /// There used to be a bug where uploading an EndpointAddr with more than only exactly
640    /// one relay URL or one publicly reachable IP addr would prevent connection
641    /// establishment.
642    ///
643    /// The reason was that only the first address was parsed (e.g. 192.168.96.145 in
644    /// this example), which could be a local, unreachable address.
645    #[test]
646    fn test_from_hickory_lookup() -> Result {
647        let name = Name::from_utf8(
648            "_iroh.dgjpkxyn3zyrk3zfads5duwdgbqpkwbjxfj4yt7rezidr3fijccy.dns.iroh.link.",
649        )
650        .std_context("dns name")?;
651        let query = Query::query(name.clone(), RecordType::TXT);
652        let records = [
653            Record::from_rdata(
654                name.clone(),
655                30,
656                RData::TXT(TXT::new(vec!["addr=192.168.96.145:60165".to_string()])),
657            ),
658            Record::from_rdata(
659                name.clone(),
660                30,
661                RData::TXT(TXT::new(vec!["addr=213.208.157.87:60165".to_string()])),
662            ),
663            // Test a record with mismatching record type (A instead of TXT). It should be filtered out.
664            Record::from_rdata(name.clone(), 30, RData::A(A::new(127, 0, 0, 1))),
665            // Test a record with a mismatching name
666            Record::from_rdata(
667                {
668                    // Another EndpointId
669                    let other_id = EndpointId::from_str(
670                        "a55f26132e5e43de834d534332f66a20d480c3e50a13a312a071adea6569981e",
671                    )?;
672                    Name::from_utf8(format!("_iroh.{}.dns.iroh.link.", other_id.to_z32()))
673                }
674                .std_context("name")?,
675                30,
676                RData::TXT(TXT::new(vec![
677                    "relay=https://euw1-1.relay.iroh.network./".to_string(),
678                ])),
679            ),
680            // Test a record with a completely different name
681            Record::from_rdata(
682                Name::from_utf8("dns.iroh.link.").std_context("name")?,
683                30,
684                RData::TXT(TXT::new(vec![
685                    "relay=https://euw1-1.relay.iroh.network./".to_string(),
686                ])),
687            ),
688            Record::from_rdata(
689                name.clone(),
690                30,
691                RData::TXT(TXT::new(vec![
692                    "relay=https://euw1-1.relay.iroh.network./".to_string(),
693                ])),
694            ),
695        ];
696        let lookup = Lookup::new_with_max_ttl(query, records);
697        let lookup = lookup
698            .answers()
699            .iter()
700            .filter_map(|record| match &record.data {
701                RData::TXT(txt) => Some(TxtRecordData::from(txt.txt_data.to_vec())),
702                _ => None,
703            });
704
705        let endpoint_info = EndpointInfo::from_txt_lookup(name.to_string(), lookup)?;
706
707        let expected_endpoint_info = EndpointInfo::new(EndpointId::from_str(
708            "1992d53c02cdc04566e5c0edb1ce83305cd550297953a047a445ea3264b54b18",
709        )?)
710        .with_relay_url("https://euw1-1.relay.iroh.network./".parse()?)
711        .with_ip_addrs(vec![
712            "192.168.96.145:60165".parse().unwrap(),
713            "213.208.157.87:60165".parse().unwrap(),
714        ]);
715
716        assert_eq!(endpoint_info, expected_endpoint_info);
717
718        Ok(())
719    }
720}