Skip to main content

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