1use std::{
36 collections::{BTreeMap, BTreeSet},
37 fmt::{self, Display},
38 hash::Hash,
39 net::SocketAddr,
40 str::{FromStr, Utf8Error},
41 sync::Arc,
42};
43
44use iroh_base::{EndpointAddr, EndpointId, KeyParsingError, RelayUrl, SecretKey, TransportAddr};
45use n0_error::{e, ensure, stack_error};
46use url::Url;
47
48pub const IROH_TXT_NAME: &str = "_iroh";
50
51#[allow(missing_docs)]
52#[stack_error(derive, add_meta)]
53#[non_exhaustive]
54pub enum EncodingError {
55 #[error(transparent)]
56 FailedBuildingPacket {
57 #[error(std_err)]
58 source: pkarr::errors::SignedPacketBuildError,
59 },
60 #[error("invalid TXT entry")]
61 InvalidTxtEntry {
62 #[error(std_err)]
63 source: pkarr::dns::SimpleDnsError,
64 },
65}
66
67#[allow(missing_docs)]
68#[stack_error(derive, add_meta)]
69#[non_exhaustive]
70pub enum DecodingError {
71 #[error("endpoint id was not encoded in valid z32")]
72 InvalidEncodingZ32 {
73 #[error(std_err)]
74 source: z32::Z32Error,
75 },
76 #[error("length must be 32 bytes, but got {len} byte(s)")]
77 InvalidLength { len: usize },
78 #[error("endpoint id is not a valid public key")]
79 InvalidKey { source: KeyParsingError },
80}
81
82pub trait EndpointIdExt {
85 fn to_z32(&self) -> String;
89
90 fn from_z32(s: &str) -> Result<EndpointId, DecodingError>;
94}
95
96impl EndpointIdExt for EndpointId {
97 fn to_z32(&self) -> String {
98 z32::encode(self.as_bytes())
99 }
100
101 fn from_z32(s: &str) -> Result<EndpointId, DecodingError> {
102 let bytes =
103 z32::decode(s.as_bytes()).map_err(|err| e!(DecodingError::InvalidEncodingZ32, err))?;
104 let bytes: &[u8; 32] = &bytes
105 .try_into()
106 .map_err(|_| e!(DecodingError::InvalidLength { len: s.len() }))?;
107 let endpoint_id =
108 EndpointId::from_bytes(bytes).map_err(|err| e!(DecodingError::InvalidKey, err))?;
109 Ok(endpoint_id)
110 }
111}
112
113#[derive(Debug, Clone, Default, Eq, PartialEq)]
122pub struct EndpointData {
123 addrs: BTreeSet<TransportAddr>,
125 user_data: Option<UserData>,
127}
128
129impl EndpointData {
130 pub fn new(addrs: impl IntoIterator<Item = TransportAddr>) -> Self {
132 Self {
133 addrs: addrs.into_iter().collect(),
134 user_data: None,
135 }
136 }
137
138 pub fn with_relay_url(mut self, relay_url: Option<RelayUrl>) -> Self {
140 if let Some(url) = relay_url {
141 self.addrs.insert(TransportAddr::Relay(url));
142 }
143 self
144 }
145
146 pub fn with_ip_addrs(mut self, addresses: BTreeSet<SocketAddr>) -> Self {
148 for addr in addresses.into_iter() {
149 self.addrs.insert(TransportAddr::Ip(addr));
150 }
151 self
152 }
153
154 pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
156 self.user_data = user_data;
157 self
158 }
159
160 pub fn relay_urls(&self) -> impl Iterator<Item = &RelayUrl> {
162 self.addrs.iter().filter_map(|addr| match addr {
163 TransportAddr::Relay(url) => Some(url),
164 _ => None,
165 })
166 }
167
168 pub fn user_data(&self) -> Option<&UserData> {
170 self.user_data.as_ref()
171 }
172
173 pub fn ip_addrs(&self) -> impl Iterator<Item = &SocketAddr> {
175 self.addrs.iter().filter_map(|addr| match addr {
176 TransportAddr::Ip(addr) => Some(addr),
177 _ => None,
178 })
179 }
180
181 pub fn clear_ip_addrs(&mut self) {
183 self.addrs
184 .retain(|addr| !matches!(addr, TransportAddr::Ip(_)));
185 }
186
187 pub fn clear_relay_urls(&mut self) {
189 self.addrs
190 .retain(|addr| !matches!(addr, TransportAddr::Relay(_)));
191 }
192
193 pub fn add_addrs(&mut self, addrs: impl IntoIterator<Item = TransportAddr>) {
195 for addr in addrs.into_iter() {
196 self.addrs.insert(addr);
197 }
198 }
199
200 pub fn set_user_data(&mut self, user_data: Option<UserData>) {
202 self.user_data = user_data;
203 }
204
205 pub fn addrs(&self) -> impl Iterator<Item = &TransportAddr> {
207 self.addrs.iter()
208 }
209
210 pub fn has_addrs(&self) -> bool {
212 !self.addrs.is_empty()
213 }
214
215 pub fn filtered_addrs(&self, filter: &AddrFilter) -> Vec<TransportAddr> {
219 filter.apply(&self.addrs)
220 }
221}
222
223type AddrFilterFn = dyn Fn(&BTreeSet<TransportAddr>) -> Vec<TransportAddr> + Send + Sync + 'static;
225
226#[derive(Clone, Default)]
237pub struct AddrFilter(Option<Arc<AddrFilterFn>>);
238
239impl std::fmt::Debug for AddrFilter {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 if self.0.is_some() {
242 f.debug_struct("AddrFilter").finish_non_exhaustive()
243 } else {
244 write!(f, "identity")
245 }
246 }
247}
248
249impl AddrFilter {
250 pub fn new(
252 f: impl Fn(&BTreeSet<TransportAddr>) -> Vec<TransportAddr> + Send + Sync + 'static,
253 ) -> Self {
254 Self(Some(Arc::new(f)))
255 }
256
257 pub fn unfiltered() -> Self {
259 Self::new(|addrs| addrs.iter().cloned().collect())
260 }
261
262 pub fn relay_only() -> Self {
264 Self::new(|addrs| addrs.iter().filter(|a| a.is_relay()).cloned().collect())
265 }
266
267 pub fn ip_only() -> Self {
269 Self::new(|addrs| addrs.iter().filter(|a| !a.is_relay()).cloned().collect())
270 }
271
272 pub fn apply(&self, addrs: &BTreeSet<TransportAddr>) -> Vec<TransportAddr> {
274 match &self.0 {
275 Some(f) => f(addrs),
276 None => addrs.iter().cloned().collect(),
277 }
278 }
279}
280
281impl From<EndpointAddr> for EndpointData {
282 fn from(endpoint_addr: EndpointAddr) -> Self {
283 Self {
284 addrs: endpoint_addr.addrs,
285 user_data: None,
286 }
287 }
288}
289
290#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
299pub struct UserData(String);
300
301impl UserData {
302 pub const MAX_LENGTH: usize = 245;
308}
309
310#[allow(missing_docs)]
312#[stack_error(derive, add_meta)]
313#[error("max length exceeded")]
314pub struct MaxLengthExceededError {}
315
316impl TryFrom<String> for UserData {
317 type Error = MaxLengthExceededError;
318
319 fn try_from(value: String) -> Result<Self, Self::Error> {
320 ensure!(value.len() <= Self::MAX_LENGTH, MaxLengthExceededError);
321 Ok(Self(value))
322 }
323}
324
325impl FromStr for UserData {
326 type Err = MaxLengthExceededError;
327
328 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
329 ensure!(s.len() <= Self::MAX_LENGTH, MaxLengthExceededError);
330 Ok(Self(s.to_string()))
331 }
332}
333
334impl fmt::Display for UserData {
335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 write!(f, "{}", self.0)
337 }
338}
339
340impl AsRef<str> for UserData {
341 fn as_ref(&self) -> &str {
342 &self.0
343 }
344}
345
346#[derive(derive_more::Debug, Clone, Eq, PartialEq)]
350pub struct EndpointInfo {
351 pub endpoint_id: EndpointId,
353 pub data: EndpointData,
355}
356
357impl From<TxtAttrs<IrohAttr>> for EndpointInfo {
358 fn from(attrs: TxtAttrs<IrohAttr>) -> Self {
359 (&attrs).into()
360 }
361}
362
363impl From<&TxtAttrs<IrohAttr>> for EndpointInfo {
364 fn from(attrs: &TxtAttrs<IrohAttr>) -> Self {
365 use iroh_base::CustomAddr;
366
367 let endpoint_id = attrs.endpoint_id();
368 let attrs = attrs.attrs();
369 let relay_urls = attrs
370 .get(&IrohAttr::Relay)
371 .into_iter()
372 .flatten()
373 .filter_map(|s| Url::parse(s).ok())
374 .map(|url| TransportAddr::Relay(url.into()));
375 let addrs = attrs
377 .get(&IrohAttr::Addr)
378 .into_iter()
379 .flatten()
380 .filter_map(|s| {
381 if let Ok(addr) = SocketAddr::from_str(s) {
382 Some(TransportAddr::Ip(addr))
383 } else if let Ok(addr) = CustomAddr::from_str(s) {
384 Some(TransportAddr::Custom(addr))
385 } else {
386 None
387 }
388 });
389
390 let user_data = attrs
391 .get(&IrohAttr::UserData)
392 .into_iter()
393 .flatten()
394 .next()
395 .and_then(|s| UserData::from_str(s).ok());
396 let mut data = EndpointData::default();
397 data.set_user_data(user_data);
398 data.add_addrs(relay_urls.chain(addrs));
399
400 Self { endpoint_id, data }
401 }
402}
403
404impl From<EndpointInfo> for EndpointAddr {
405 fn from(value: EndpointInfo) -> Self {
406 value.into_endpoint_addr()
407 }
408}
409
410impl From<EndpointAddr> for EndpointInfo {
411 fn from(addr: EndpointAddr) -> Self {
412 let mut info = Self::new(addr.id);
413 info.add_addrs(addr.addrs);
414 info
415 }
416}
417
418impl EndpointInfo {
419 pub fn new(endpoint_id: EndpointId) -> Self {
421 Self::from_parts(endpoint_id, Default::default())
422 }
423
424 pub fn from_parts(endpoint_id: EndpointId, data: EndpointData) -> Self {
426 Self { endpoint_id, data }
427 }
428
429 pub fn with_relay_url(mut self, relay_url: Option<RelayUrl>) -> Self {
431 self.data = self.data.with_relay_url(relay_url);
432 self
433 }
434
435 pub fn with_ip_addrs(mut self, addrs: BTreeSet<SocketAddr>) -> Self {
437 self.data = self.data.with_ip_addrs(addrs);
438 self
439 }
440
441 pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
443 self.data = self.data.with_user_data(user_data);
444 self
445 }
446
447 pub fn to_endpoint_addr(&self) -> EndpointAddr {
449 EndpointAddr {
450 id: self.endpoint_id,
451 addrs: self.addrs.clone(),
452 }
453 }
454
455 pub fn into_endpoint_addr(self) -> EndpointAddr {
457 let Self { endpoint_id, data } = self;
458 EndpointAddr {
459 id: endpoint_id,
460 addrs: data.addrs,
461 }
462 }
463
464 fn to_attrs(&self) -> TxtAttrs<IrohAttr> {
465 self.into()
466 }
467
468 #[cfg(not(wasm_browser))]
469 pub fn from_txt_lookup(
471 domain_name: String,
472 lookup: impl Iterator<Item = crate::dns::TxtRecordData>,
473 ) -> Result<Self, ParseError> {
474 let attrs = TxtAttrs::from_txt_lookup(domain_name, lookup)?;
475 Ok(Self::from(attrs))
476 }
477
478 pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result<Self, ParseError> {
480 let attrs = TxtAttrs::from_pkarr_signed_packet(packet)?;
481 Ok(attrs.into())
482 }
483
484 pub fn to_pkarr_signed_packet(
488 &self,
489 secret_key: &SecretKey,
490 ttl: u32,
491 ) -> Result<pkarr::SignedPacket, EncodingError> {
492 self.to_attrs().to_pkarr_signed_packet(secret_key, ttl)
493 }
494
495 pub fn to_txt_strings(&self) -> Vec<String> {
497 self.to_attrs().to_txt_strings().collect()
498 }
499}
500
501#[allow(missing_docs)]
502#[stack_error(derive, add_meta, from_sources)]
503#[non_exhaustive]
504pub enum ParseError {
505 #[error("Expected format `key=value`, received `{s}`")]
506 UnexpectedFormat { s: String },
507 #[error("Could not convert key to Attr")]
508 AttrFromString { key: String },
509 #[error("Expected 2 labels, received {num_labels}")]
510 NumLabels { num_labels: usize },
511 #[error("Could not parse labels")]
512 Utf8 {
513 #[error(std_err)]
514 source: Utf8Error,
515 },
516 #[error("Record is not an `iroh` record, expected `_iroh`, got `{label}`")]
517 NotAnIrohRecord { label: String },
518 #[error(transparent)]
519 DecodingError { source: DecodingError },
520}
521
522impl std::ops::Deref for EndpointInfo {
523 type Target = EndpointData;
524 fn deref(&self) -> &Self::Target {
525 &self.data
526 }
527}
528
529impl std::ops::DerefMut for EndpointInfo {
530 fn deref_mut(&mut self) -> &mut Self::Target {
531 &mut self.data
532 }
533}
534
535#[cfg(not(wasm_browser))]
541fn endpoint_id_from_txt_name(name: &str) -> Result<EndpointId, ParseError> {
542 let num_labels = name.split(".").count();
543 if num_labels < 2 {
544 return Err(e!(ParseError::NumLabels { num_labels }));
545 }
546 let mut labels = name.split(".");
547 let label = labels.next().expect("checked above");
548 if label != IROH_TXT_NAME {
549 return Err(e!(ParseError::NotAnIrohRecord {
550 label: label.to_string()
551 }));
552 }
553 let label = labels.next().expect("checked above");
554 let endpoint_id = EndpointId::from_z32(label)?;
555 Ok(endpoint_id)
556}
557
558#[derive(
562 Debug, strum::Display, strum::AsRefStr, strum::EnumString, Hash, Eq, PartialEq, Ord, PartialOrd,
563)]
564#[strum(serialize_all = "kebab-case")]
565pub(crate) enum IrohAttr {
566 Relay,
568 Addr,
570 UserData,
572}
573
574#[derive(Debug)]
580pub(crate) struct TxtAttrs<T> {
581 endpoint_id: EndpointId,
582 attrs: BTreeMap<T, Vec<String>>,
583}
584
585impl From<&EndpointInfo> for TxtAttrs<IrohAttr> {
586 fn from(info: &EndpointInfo) -> Self {
587 let mut attrs = vec![];
588 for addr in &info.data.addrs {
589 match addr {
590 TransportAddr::Relay(url) => attrs.push((IrohAttr::Relay, url.to_string())),
591 TransportAddr::Ip(addr) => attrs.push((IrohAttr::Addr, addr.to_string())),
592 TransportAddr::Custom(addr) => attrs.push((IrohAttr::Addr, addr.to_string())),
593 _ => {}
594 }
595 }
596
597 if let Some(user_data) = &info.data.user_data {
598 attrs.push((IrohAttr::UserData, user_data.to_string()));
599 }
600 Self::from_parts(info.endpoint_id, attrs.into_iter())
601 }
602}
603
604impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
605 pub(crate) fn from_parts(
607 endpoint_id: EndpointId,
608 pairs: impl Iterator<Item = (T, String)>,
609 ) -> Self {
610 let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
611 for (k, v) in pairs {
612 attrs.entry(k).or_default().push(v);
613 }
614 Self { attrs, endpoint_id }
615 }
616
617 pub(crate) fn from_strings(
619 endpoint_id: EndpointId,
620 strings: impl Iterator<Item = String>,
621 ) -> Result<Self, ParseError> {
622 let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
623 for s in strings {
624 let mut parts = s.split('=');
625 let (Some(key), Some(value)) = (parts.next(), parts.next()) else {
626 return Err(e!(ParseError::UnexpectedFormat { s }));
627 };
628 let attr = T::from_str(key).map_err(|_| {
629 e!(ParseError::AttrFromString {
630 key: key.to_string()
631 })
632 })?;
633 attrs.entry(attr).or_default().push(value.to_string());
634 }
635 Ok(Self { attrs, endpoint_id })
636 }
637
638 pub(crate) fn attrs(&self) -> &BTreeMap<T, Vec<String>> {
640 &self.attrs
641 }
642
643 pub(crate) fn endpoint_id(&self) -> EndpointId {
645 self.endpoint_id
646 }
647
648 pub(crate) fn from_pkarr_signed_packet(
650 packet: &pkarr::SignedPacket,
651 ) -> Result<Self, ParseError> {
652 use pkarr::dns::{
653 rdata::RData,
654 {self},
655 };
656 let pubkey = packet.public_key();
657 let pubkey_z32 = pubkey.to_z32();
658 let endpoint_id =
659 EndpointId::from_bytes(&pubkey.verifying_key().to_bytes()).expect("valid key");
660 let zone = dns::Name::new(&pubkey_z32).expect("z32 encoding is valid");
661 let txt_data = packet
662 .all_resource_records()
663 .filter_map(|rr| match &rr.rdata {
664 RData::TXT(txt) => match rr.name.without(&zone) {
665 Some(name) if name.to_string() == IROH_TXT_NAME => Some(txt),
666 Some(_) | None => None,
667 },
668 _ => None,
669 });
670
671 let txt_strs = txt_data.filter_map(|s| String::try_from(s.clone()).ok());
672 Self::from_strings(endpoint_id, txt_strs)
673 }
674
675 #[cfg(not(wasm_browser))]
677 pub(crate) fn from_txt_lookup(
678 name: String,
679 lookup: impl Iterator<Item = crate::dns::TxtRecordData>,
680 ) -> Result<Self, ParseError> {
681 let queried_endpoint_id = endpoint_id_from_txt_name(&name)?;
682
683 let strings = lookup.map(|record| record.to_string());
684 Self::from_strings(queried_endpoint_id, strings)
685 }
686
687 fn to_txt_strings(&self) -> impl Iterator<Item = String> + '_ {
688 self.attrs
689 .iter()
690 .flat_map(move |(k, vs)| vs.iter().map(move |v| format!("{k}={v}")))
691 }
692
693 pub(crate) fn to_pkarr_signed_packet(
697 &self,
698 secret_key: &SecretKey,
699 ttl: u32,
700 ) -> Result<pkarr::SignedPacket, EncodingError> {
701 use pkarr::dns::{self, rdata};
702 let keypair = pkarr::Keypair::from_secret_key(&secret_key.to_bytes());
703 let name = dns::Name::new(IROH_TXT_NAME).expect("constant");
704
705 let mut builder = pkarr::SignedPacket::builder();
706 for s in self.to_txt_strings() {
707 let mut txt = rdata::TXT::new();
708 txt.add_string(&s)
709 .map_err(|err| e!(EncodingError::InvalidTxtEntry, err))?;
710 builder = builder.txt(name.clone(), txt.into_owned(), ttl);
711 }
712 let signed_packet = builder
713 .build(&keypair)
714 .map_err(|err| e!(EncodingError::FailedBuildingPacket, err))?;
715 Ok(signed_packet)
716 }
717}
718
719#[cfg(not(wasm_browser))]
720pub(crate) fn ensure_iroh_txt_label(name: String) -> String {
721 let mut parts = name.split(".");
722 if parts.next() == Some(IROH_TXT_NAME) {
723 name
724 } else {
725 format!("{IROH_TXT_NAME}.{name}")
726 }
727}
728
729#[cfg(not(wasm_browser))]
730pub(crate) fn endpoint_domain(endpoint_id: &EndpointId, origin: &str) -> String {
731 format!("{}.{}", EndpointId::to_z32(endpoint_id), origin)
732}
733
734#[cfg(test)]
735mod tests {
736 use std::{collections::BTreeSet, str::FromStr, sync::Arc};
737
738 use hickory_resolver::{
739 Name,
740 lookup::Lookup,
741 proto::{
742 op::Query,
743 rr::{
744 RData, Record, RecordType,
745 rdata::{A, TXT},
746 },
747 },
748 };
749 use iroh_base::{EndpointId, SecretKey, TransportAddr};
750 use n0_error::{Result, StdResultExt};
751
752 use super::{EndpointData, EndpointIdExt, EndpointInfo};
753 use crate::dns::TxtRecordData;
754
755 #[test]
756 fn txt_attr_roundtrip() {
757 let endpoint_data = EndpointData::new([
758 TransportAddr::Relay("https://example.com".parse().unwrap()),
759 TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
760 ])
761 .with_user_data(Some("foobar".parse().unwrap()));
762 let endpoint_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
763 .parse()
764 .unwrap();
765 let expected = EndpointInfo::from_parts(endpoint_id, endpoint_data);
766 let attrs = expected.to_attrs();
767 let actual = EndpointInfo::from(&attrs);
768 assert_eq!(expected, actual);
769 }
770
771 #[test]
772 fn signed_packet_roundtrip() {
773 let secret_key =
774 SecretKey::from_str("vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia").unwrap();
775 let endpoint_data = EndpointData::new([
776 TransportAddr::Relay("https://example.com".parse().unwrap()),
777 TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
778 ])
779 .with_user_data(Some("foobar".parse().unwrap()));
780 let expected = EndpointInfo::from_parts(secret_key.public(), endpoint_data);
781 let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
782 let actual = EndpointInfo::from_pkarr_signed_packet(&packet).unwrap();
783 assert_eq!(expected, actual);
784 }
785
786 #[test]
787 fn txt_attr_roundtrip_with_custom_addr() {
788 use iroh_base::CustomAddr;
789
790 let bt_addr = CustomAddr::from_parts(1, &[0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6]);
792 let tor_addr = CustomAddr::from_parts(42, &[0xab; 32]);
794
795 let endpoint_data = EndpointData::new([
796 TransportAddr::Relay("https://example.com".parse().unwrap()),
797 TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
798 TransportAddr::Custom(bt_addr),
799 TransportAddr::Custom(tor_addr),
800 ]);
801 let endpoint_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
802 .parse()
803 .unwrap();
804 let expected = EndpointInfo::from_parts(endpoint_id, endpoint_data);
805 let attrs = expected.to_attrs();
806 let actual = EndpointInfo::from(&attrs);
807 assert_eq!(expected, actual);
808 }
809
810 #[test]
811 fn signed_packet_roundtrip_with_custom_addr() {
812 use iroh_base::CustomAddr;
813
814 let secret_key =
815 SecretKey::from_str("vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia").unwrap();
816
817 let bt_addr = CustomAddr::from_parts(1, &[0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6]);
819 let tor_addr = CustomAddr::from_parts(42, &[0xab; 32]);
821
822 let endpoint_data = EndpointData::new([
823 TransportAddr::Relay("https://example.com".parse().unwrap()),
824 TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
825 TransportAddr::Custom(bt_addr),
826 TransportAddr::Custom(tor_addr),
827 ])
828 .with_user_data(Some("foobar".parse().unwrap()));
829
830 let expected = EndpointInfo::from_parts(secret_key.public(), endpoint_data);
831 let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
832 let actual = EndpointInfo::from_pkarr_signed_packet(&packet).unwrap();
833 assert_eq!(expected, actual);
834 }
835
836 #[test]
843 fn test_from_hickory_lookup() -> Result {
844 let name = Name::from_utf8(
845 "_iroh.dgjpkxyn3zyrk3zfads5duwdgbqpkwbjxfj4yt7rezidr3fijccy.dns.iroh.link.",
846 )
847 .std_context("dns name")?;
848 let query = Query::query(name.clone(), RecordType::TXT);
849 let records = [
850 Record::from_rdata(
851 name.clone(),
852 30,
853 RData::TXT(TXT::new(vec!["addr=192.168.96.145:60165".to_string()])),
854 ),
855 Record::from_rdata(
856 name.clone(),
857 30,
858 RData::TXT(TXT::new(vec!["addr=213.208.157.87:60165".to_string()])),
859 ),
860 Record::from_rdata(name.clone(), 30, RData::A(A::new(127, 0, 0, 1))),
862 Record::from_rdata(
864 Name::from_utf8(format!(
865 "_iroh.{}.dns.iroh.link.",
866 EndpointId::from_str(
867 "a55f26132e5e43de834d534332f66a20d480c3e50a13a312a071adea6569981e"
869 )?
870 .to_z32()
871 ))
872 .std_context("name")?,
873 30,
874 RData::TXT(TXT::new(vec![
875 "relay=https://euw1-1.relay.iroh.network./".to_string(),
876 ])),
877 ),
878 Record::from_rdata(
880 Name::from_utf8("dns.iroh.link.").std_context("name")?,
881 30,
882 RData::TXT(TXT::new(vec![
883 "relay=https://euw1-1.relay.iroh.network./".to_string(),
884 ])),
885 ),
886 Record::from_rdata(
887 name.clone(),
888 30,
889 RData::TXT(TXT::new(vec![
890 "relay=https://euw1-1.relay.iroh.network./".to_string(),
891 ])),
892 ),
893 ];
894 let lookup = Lookup::new_with_max_ttl(query, Arc::new(records));
895 let lookup = hickory_resolver::lookup::TxtLookup::from(lookup);
896 let lookup = lookup
897 .into_iter()
898 .map(|txt| TxtRecordData::from_iter(txt.iter().cloned()));
899
900 let endpoint_info = EndpointInfo::from_txt_lookup(name.to_string(), lookup)?;
901
902 let expected_endpoint_info = EndpointInfo::new(EndpointId::from_str(
903 "1992d53c02cdc04566e5c0edb1ce83305cd550297953a047a445ea3264b54b18",
904 )?)
905 .with_relay_url(Some("https://euw1-1.relay.iroh.network./".parse()?))
906 .with_ip_addrs(BTreeSet::from([
907 "192.168.96.145:60165".parse().unwrap(),
908 "213.208.157.87:60165".parse().unwrap(),
909 ]));
910
911 assert_eq!(endpoint_info, expected_endpoint_info);
912
913 Ok(())
914 }
915}