1use std::{
36 collections::{BTreeMap, BTreeSet},
37 fmt::{self, Display},
38 hash::Hash,
39 net::SocketAddr,
40 str::{FromStr, Utf8Error},
41};
42
43use iroh_base::{EndpointAddr, EndpointId, KeyParsingError, RelayUrl, SecretKey, TransportAddr};
44use n0_error::{e, ensure, stack_error};
45use url::Url;
46
47pub const IROH_TXT_NAME: &str = "_iroh";
49
50#[allow(missing_docs)]
51#[stack_error(derive, add_meta)]
52#[non_exhaustive]
53pub enum EncodingError {
54 #[error(transparent)]
55 FailedBuildingPacket {
56 #[error(std_err)]
57 source: pkarr::errors::SignedPacketBuildError,
58 },
59 #[error("invalid TXT entry")]
60 InvalidTxtEntry {
61 #[error(std_err)]
62 source: pkarr::dns::SimpleDnsError,
63 },
64}
65
66#[allow(missing_docs)]
67#[stack_error(derive, add_meta)]
68#[non_exhaustive]
69pub enum DecodingError {
70 #[error("endpoint id was not encoded in valid z32")]
71 InvalidEncodingZ32 {
72 #[error(std_err)]
73 source: z32::Z32Error,
74 },
75 #[error("length must be 32 bytes, but got {len} byte(s)")]
76 InvalidLength { len: usize },
77 #[error("endpoint id is not a valid public key")]
78 InvalidKey { source: KeyParsingError },
79}
80
81pub trait EndpointIdExt {
84 fn to_z32(&self) -> String;
88
89 fn from_z32(s: &str) -> Result<EndpointId, DecodingError>;
93}
94
95impl EndpointIdExt for EndpointId {
96 fn to_z32(&self) -> String {
97 z32::encode(self.as_bytes())
98 }
99
100 fn from_z32(s: &str) -> Result<EndpointId, DecodingError> {
101 let bytes =
102 z32::decode(s.as_bytes()).map_err(|err| e!(DecodingError::InvalidEncodingZ32, err))?;
103 let bytes: &[u8; 32] = &bytes
104 .try_into()
105 .map_err(|_| e!(DecodingError::InvalidLength { len: s.len() }))?;
106 let endpoint_id =
107 EndpointId::from_bytes(bytes).map_err(|err| e!(DecodingError::InvalidKey, err))?;
108 Ok(endpoint_id)
109 }
110}
111
112#[derive(Debug, Clone, Default, Eq, PartialEq)]
121pub struct EndpointData {
122 addrs: BTreeSet<TransportAddr>,
124 user_data: Option<UserData>,
126}
127
128impl EndpointData {
129 pub fn new(addrs: impl IntoIterator<Item = TransportAddr>) -> Self {
131 Self {
132 addrs: addrs.into_iter().collect(),
133 user_data: None,
134 }
135 }
136
137 pub fn with_relay_url(mut self, relay_url: Option<RelayUrl>) -> Self {
139 if let Some(url) = relay_url {
140 self.addrs.insert(TransportAddr::Relay(url));
141 }
142 self
143 }
144
145 pub fn with_ip_addrs(mut self, addresses: BTreeSet<SocketAddr>) -> Self {
147 for addr in addresses.into_iter() {
148 self.addrs.insert(TransportAddr::Ip(addr));
149 }
150 self
151 }
152
153 pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
155 self.user_data = user_data;
156 self
157 }
158
159 pub fn relay_urls(&self) -> impl Iterator<Item = &RelayUrl> {
161 self.addrs.iter().filter_map(|addr| match addr {
162 TransportAddr::Relay(url) => Some(url),
163 _ => None,
164 })
165 }
166
167 pub fn user_data(&self) -> Option<&UserData> {
169 self.user_data.as_ref()
170 }
171
172 pub fn ip_addrs(&self) -> impl Iterator<Item = &SocketAddr> {
174 self.addrs.iter().filter_map(|addr| match addr {
175 TransportAddr::Ip(addr) => Some(addr),
176 _ => None,
177 })
178 }
179
180 pub fn clear_ip_addrs(&mut self) {
182 self.addrs
183 .retain(|addr| !matches!(addr, TransportAddr::Ip(_)));
184 }
185
186 pub fn clear_relay_urls(&mut self) {
188 self.addrs
189 .retain(|addr| !matches!(addr, TransportAddr::Relay(_)));
190 }
191
192 pub fn add_addrs(&mut self, addrs: impl IntoIterator<Item = TransportAddr>) {
194 for addr in addrs.into_iter() {
195 self.addrs.insert(addr);
196 }
197 }
198
199 pub fn set_user_data(&mut self, user_data: Option<UserData>) {
201 self.user_data = user_data;
202 }
203
204 pub fn addrs(&self) -> impl Iterator<Item = &TransportAddr> {
206 self.addrs.iter()
207 }
208
209 pub fn has_addrs(&self) -> bool {
211 !self.addrs.is_empty()
212 }
213}
214
215impl From<EndpointAddr> for EndpointData {
216 fn from(endpoint_addr: EndpointAddr) -> Self {
217 Self {
218 addrs: endpoint_addr.addrs,
219 user_data: None,
220 }
221 }
222}
223
224#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
233pub struct UserData(String);
234
235impl UserData {
236 pub const MAX_LENGTH: usize = 245;
242}
243
244#[allow(missing_docs)]
246#[stack_error(derive, add_meta)]
247#[error("max length exceeded")]
248pub struct MaxLengthExceededError {}
249
250impl TryFrom<String> for UserData {
251 type Error = MaxLengthExceededError;
252
253 fn try_from(value: String) -> Result<Self, Self::Error> {
254 ensure!(value.len() <= Self::MAX_LENGTH, MaxLengthExceededError);
255 Ok(Self(value))
256 }
257}
258
259impl FromStr for UserData {
260 type Err = MaxLengthExceededError;
261
262 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
263 ensure!(s.len() <= Self::MAX_LENGTH, MaxLengthExceededError);
264 Ok(Self(s.to_string()))
265 }
266}
267
268impl fmt::Display for UserData {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 write!(f, "{}", self.0)
271 }
272}
273
274impl AsRef<str> for UserData {
275 fn as_ref(&self) -> &str {
276 &self.0
277 }
278}
279
280#[derive(derive_more::Debug, Clone, Eq, PartialEq)]
284pub struct EndpointInfo {
285 pub endpoint_id: EndpointId,
287 pub data: EndpointData,
289}
290
291impl From<TxtAttrs<IrohAttr>> for EndpointInfo {
292 fn from(attrs: TxtAttrs<IrohAttr>) -> Self {
293 (&attrs).into()
294 }
295}
296
297impl From<&TxtAttrs<IrohAttr>> for EndpointInfo {
298 fn from(attrs: &TxtAttrs<IrohAttr>) -> Self {
299 let endpoint_id = attrs.endpoint_id();
300 let attrs = attrs.attrs();
301 let relay_urls = attrs
302 .get(&IrohAttr::Relay)
303 .into_iter()
304 .flatten()
305 .filter_map(|s| Url::parse(s).ok())
306 .map(|url| TransportAddr::Relay(url.into()));
307 let ip_addrs = attrs
308 .get(&IrohAttr::Addr)
309 .into_iter()
310 .flatten()
311 .filter_map(|s| SocketAddr::from_str(s).ok())
312 .map(TransportAddr::Ip);
313
314 let user_data = attrs
315 .get(&IrohAttr::UserData)
316 .into_iter()
317 .flatten()
318 .next()
319 .and_then(|s| UserData::from_str(s).ok());
320 let mut data = EndpointData::default();
321 data.set_user_data(user_data);
322 data.add_addrs(relay_urls.chain(ip_addrs));
323
324 Self { endpoint_id, data }
325 }
326}
327
328impl From<EndpointInfo> for EndpointAddr {
329 fn from(value: EndpointInfo) -> Self {
330 value.into_endpoint_addr()
331 }
332}
333
334impl From<EndpointAddr> for EndpointInfo {
335 fn from(addr: EndpointAddr) -> Self {
336 let mut info = Self::new(addr.id);
337 info.add_addrs(addr.addrs);
338 info
339 }
340}
341
342impl EndpointInfo {
343 pub fn new(endpoint_id: EndpointId) -> Self {
345 Self::from_parts(endpoint_id, Default::default())
346 }
347
348 pub fn from_parts(endpoint_id: EndpointId, data: EndpointData) -> Self {
350 Self { endpoint_id, data }
351 }
352
353 pub fn with_relay_url(mut self, relay_url: Option<RelayUrl>) -> Self {
355 self.data = self.data.with_relay_url(relay_url);
356 self
357 }
358
359 pub fn with_ip_addrs(mut self, addrs: BTreeSet<SocketAddr>) -> Self {
361 self.data = self.data.with_ip_addrs(addrs);
362 self
363 }
364
365 pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
367 self.data = self.data.with_user_data(user_data);
368 self
369 }
370
371 pub fn to_endpoint_addr(&self) -> EndpointAddr {
373 EndpointAddr {
374 id: self.endpoint_id,
375 addrs: self.addrs.clone(),
376 }
377 }
378
379 pub fn into_endpoint_addr(self) -> EndpointAddr {
381 let Self { endpoint_id, data } = self;
382 EndpointAddr {
383 id: endpoint_id,
384 addrs: data.addrs,
385 }
386 }
387
388 fn to_attrs(&self) -> TxtAttrs<IrohAttr> {
389 self.into()
390 }
391
392 #[cfg(not(wasm_browser))]
393 pub fn from_txt_lookup(
395 domain_name: String,
396 lookup: impl Iterator<Item = crate::dns::TxtRecordData>,
397 ) -> Result<Self, ParseError> {
398 let attrs = TxtAttrs::from_txt_lookup(domain_name, lookup)?;
399 Ok(Self::from(attrs))
400 }
401
402 pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result<Self, ParseError> {
404 let attrs = TxtAttrs::from_pkarr_signed_packet(packet)?;
405 Ok(attrs.into())
406 }
407
408 pub fn to_pkarr_signed_packet(
412 &self,
413 secret_key: &SecretKey,
414 ttl: u32,
415 ) -> Result<pkarr::SignedPacket, EncodingError> {
416 self.to_attrs().to_pkarr_signed_packet(secret_key, ttl)
417 }
418
419 pub fn to_txt_strings(&self) -> Vec<String> {
421 self.to_attrs().to_txt_strings().collect()
422 }
423}
424
425#[allow(missing_docs)]
426#[stack_error(derive, add_meta, from_sources)]
427#[non_exhaustive]
428pub enum ParseError {
429 #[error("Expected format `key=value`, received `{s}`")]
430 UnexpectedFormat { s: String },
431 #[error("Could not convert key to Attr")]
432 AttrFromString { key: String },
433 #[error("Expected 2 labels, received {num_labels}")]
434 NumLabels { num_labels: usize },
435 #[error("Could not parse labels")]
436 Utf8 {
437 #[error(std_err)]
438 source: Utf8Error,
439 },
440 #[error("Record is not an `iroh` record, expected `_iroh`, got `{label}`")]
441 NotAnIrohRecord { label: String },
442 #[error(transparent)]
443 DecodingError { source: DecodingError },
444}
445
446impl std::ops::Deref for EndpointInfo {
447 type Target = EndpointData;
448 fn deref(&self) -> &Self::Target {
449 &self.data
450 }
451}
452
453impl std::ops::DerefMut for EndpointInfo {
454 fn deref_mut(&mut self) -> &mut Self::Target {
455 &mut self.data
456 }
457}
458
459#[cfg(not(wasm_browser))]
465fn endpoint_id_from_txt_name(name: &str) -> Result<EndpointId, ParseError> {
466 let num_labels = name.split(".").count();
467 if num_labels < 2 {
468 return Err(e!(ParseError::NumLabels { num_labels }));
469 }
470 let mut labels = name.split(".");
471 let label = labels.next().expect("checked above");
472 if label != IROH_TXT_NAME {
473 return Err(e!(ParseError::NotAnIrohRecord {
474 label: label.to_string()
475 }));
476 }
477 let label = labels.next().expect("checked above");
478 let endpoint_id = EndpointId::from_z32(label)?;
479 Ok(endpoint_id)
480}
481
482#[derive(
486 Debug, strum::Display, strum::AsRefStr, strum::EnumString, Hash, Eq, PartialEq, Ord, PartialOrd,
487)]
488#[strum(serialize_all = "kebab-case")]
489pub(crate) enum IrohAttr {
490 Relay,
492 Addr,
494 UserData,
496}
497
498#[derive(Debug)]
504pub(crate) struct TxtAttrs<T> {
505 endpoint_id: EndpointId,
506 attrs: BTreeMap<T, Vec<String>>,
507}
508
509impl From<&EndpointInfo> for TxtAttrs<IrohAttr> {
510 fn from(info: &EndpointInfo) -> Self {
511 let mut attrs = vec![];
512 for addr in &info.data.addrs {
513 match addr {
514 TransportAddr::Relay(relay_url) => {
515 attrs.push((IrohAttr::Relay, relay_url.to_string()))
516 }
517 TransportAddr::Ip(addr) => attrs.push((IrohAttr::Addr, addr.to_string())),
518 _ => {}
519 }
520 }
521
522 if let Some(user_data) = &info.data.user_data {
523 attrs.push((IrohAttr::UserData, user_data.to_string()));
524 }
525 Self::from_parts(info.endpoint_id, attrs.into_iter())
526 }
527}
528
529impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
530 pub(crate) fn from_parts(
532 endpoint_id: EndpointId,
533 pairs: impl Iterator<Item = (T, String)>,
534 ) -> Self {
535 let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
536 for (k, v) in pairs {
537 attrs.entry(k).or_default().push(v);
538 }
539 Self { attrs, endpoint_id }
540 }
541
542 pub(crate) fn from_strings(
544 endpoint_id: EndpointId,
545 strings: impl Iterator<Item = String>,
546 ) -> Result<Self, ParseError> {
547 let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
548 for s in strings {
549 let mut parts = s.split('=');
550 let (Some(key), Some(value)) = (parts.next(), parts.next()) else {
551 return Err(e!(ParseError::UnexpectedFormat { s }));
552 };
553 let attr = T::from_str(key).map_err(|_| {
554 e!(ParseError::AttrFromString {
555 key: key.to_string()
556 })
557 })?;
558 attrs.entry(attr).or_default().push(value.to_string());
559 }
560 Ok(Self { attrs, endpoint_id })
561 }
562
563 pub(crate) fn attrs(&self) -> &BTreeMap<T, Vec<String>> {
565 &self.attrs
566 }
567
568 pub(crate) fn endpoint_id(&self) -> EndpointId {
570 self.endpoint_id
571 }
572
573 pub(crate) fn from_pkarr_signed_packet(
575 packet: &pkarr::SignedPacket,
576 ) -> Result<Self, ParseError> {
577 use pkarr::dns::{
578 rdata::RData,
579 {self},
580 };
581 let pubkey = packet.public_key();
582 let pubkey_z32 = pubkey.to_z32();
583 let endpoint_id =
584 EndpointId::from_bytes(&pubkey.verifying_key().to_bytes()).expect("valid key");
585 let zone = dns::Name::new(&pubkey_z32).expect("z32 encoding is valid");
586 let txt_data = packet
587 .all_resource_records()
588 .filter_map(|rr| match &rr.rdata {
589 RData::TXT(txt) => match rr.name.without(&zone) {
590 Some(name) if name.to_string() == IROH_TXT_NAME => Some(txt),
591 Some(_) | None => None,
592 },
593 _ => None,
594 });
595
596 let txt_strs = txt_data.filter_map(|s| String::try_from(s.clone()).ok());
597 Self::from_strings(endpoint_id, txt_strs)
598 }
599
600 #[cfg(not(wasm_browser))]
602 pub(crate) fn from_txt_lookup(
603 name: String,
604 lookup: impl Iterator<Item = crate::dns::TxtRecordData>,
605 ) -> Result<Self, ParseError> {
606 let queried_endpoint_id = endpoint_id_from_txt_name(&name)?;
607
608 let strings = lookup.map(|record| record.to_string());
609 Self::from_strings(queried_endpoint_id, strings)
610 }
611
612 fn to_txt_strings(&self) -> impl Iterator<Item = String> + '_ {
613 self.attrs
614 .iter()
615 .flat_map(move |(k, vs)| vs.iter().map(move |v| format!("{k}={v}")))
616 }
617
618 pub(crate) fn to_pkarr_signed_packet(
622 &self,
623 secret_key: &SecretKey,
624 ttl: u32,
625 ) -> Result<pkarr::SignedPacket, EncodingError> {
626 use pkarr::dns::{self, rdata};
627 let keypair = pkarr::Keypair::from_secret_key(&secret_key.to_bytes());
628 let name = dns::Name::new(IROH_TXT_NAME).expect("constant");
629
630 let mut builder = pkarr::SignedPacket::builder();
631 for s in self.to_txt_strings() {
632 let mut txt = rdata::TXT::new();
633 txt.add_string(&s)
634 .map_err(|err| e!(EncodingError::InvalidTxtEntry, err))?;
635 builder = builder.txt(name.clone(), txt.into_owned(), ttl);
636 }
637 let signed_packet = builder
638 .build(&keypair)
639 .map_err(|err| e!(EncodingError::FailedBuildingPacket, err))?;
640 Ok(signed_packet)
641 }
642}
643
644#[cfg(not(wasm_browser))]
645pub(crate) fn ensure_iroh_txt_label(name: String) -> String {
646 let mut parts = name.split(".");
647 if parts.next() == Some(IROH_TXT_NAME) {
648 name
649 } else {
650 format!("{IROH_TXT_NAME}.{name}")
651 }
652}
653
654#[cfg(not(wasm_browser))]
655pub(crate) fn endpoint_domain(endpoint_id: &EndpointId, origin: &str) -> String {
656 format!("{}.{}", EndpointId::to_z32(endpoint_id), origin)
657}
658
659#[cfg(test)]
660mod tests {
661 use std::{collections::BTreeSet, str::FromStr, sync::Arc};
662
663 use hickory_resolver::{
664 Name,
665 lookup::Lookup,
666 proto::{
667 op::Query,
668 rr::{
669 RData, Record, RecordType,
670 rdata::{A, TXT},
671 },
672 },
673 };
674 use iroh_base::{EndpointId, SecretKey, TransportAddr};
675 use n0_error::{Result, StdResultExt};
676
677 use super::{EndpointData, EndpointIdExt, EndpointInfo};
678 use crate::dns::TxtRecordData;
679
680 #[test]
681 fn txt_attr_roundtrip() {
682 let endpoint_data = EndpointData::new([
683 TransportAddr::Relay("https://example.com".parse().unwrap()),
684 TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
685 ])
686 .with_user_data(Some("foobar".parse().unwrap()));
687 let endpoint_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
688 .parse()
689 .unwrap();
690 let expected = EndpointInfo::from_parts(endpoint_id, endpoint_data);
691 let attrs = expected.to_attrs();
692 let actual = EndpointInfo::from(&attrs);
693 assert_eq!(expected, actual);
694 }
695
696 #[test]
697 fn signed_packet_roundtrip() {
698 let secret_key =
699 SecretKey::from_str("vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia").unwrap();
700 let endpoint_data = EndpointData::new([
701 TransportAddr::Relay("https://example.com".parse().unwrap()),
702 TransportAddr::Ip("127.0.0.1:1234".parse().unwrap()),
703 ])
704 .with_user_data(Some("foobar".parse().unwrap()));
705 let expected = EndpointInfo::from_parts(secret_key.public(), endpoint_data);
706 let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
707 let actual = EndpointInfo::from_pkarr_signed_packet(&packet).unwrap();
708 assert_eq!(expected, actual);
709 }
710
711 #[test]
718 fn test_from_hickory_lookup() -> Result {
719 let name = Name::from_utf8(
720 "_iroh.dgjpkxyn3zyrk3zfads5duwdgbqpkwbjxfj4yt7rezidr3fijccy.dns.iroh.link.",
721 )
722 .std_context("dns name")?;
723 let query = Query::query(name.clone(), RecordType::TXT);
724 let records = [
725 Record::from_rdata(
726 name.clone(),
727 30,
728 RData::TXT(TXT::new(vec!["addr=192.168.96.145:60165".to_string()])),
729 ),
730 Record::from_rdata(
731 name.clone(),
732 30,
733 RData::TXT(TXT::new(vec!["addr=213.208.157.87:60165".to_string()])),
734 ),
735 Record::from_rdata(name.clone(), 30, RData::A(A::new(127, 0, 0, 1))),
737 Record::from_rdata(
739 Name::from_utf8(format!(
740 "_iroh.{}.dns.iroh.link.",
741 EndpointId::from_str(
742 "a55f26132e5e43de834d534332f66a20d480c3e50a13a312a071adea6569981e"
744 )?
745 .to_z32()
746 ))
747 .std_context("name")?,
748 30,
749 RData::TXT(TXT::new(vec![
750 "relay=https://euw1-1.relay.iroh.network./".to_string(),
751 ])),
752 ),
753 Record::from_rdata(
755 Name::from_utf8("dns.iroh.link.").std_context("name")?,
756 30,
757 RData::TXT(TXT::new(vec![
758 "relay=https://euw1-1.relay.iroh.network./".to_string(),
759 ])),
760 ),
761 Record::from_rdata(
762 name.clone(),
763 30,
764 RData::TXT(TXT::new(vec![
765 "relay=https://euw1-1.relay.iroh.network./".to_string(),
766 ])),
767 ),
768 ];
769 let lookup = Lookup::new_with_max_ttl(query, Arc::new(records));
770 let lookup = hickory_resolver::lookup::TxtLookup::from(lookup);
771 let lookup = lookup
772 .into_iter()
773 .map(|txt| TxtRecordData::from_iter(txt.iter().cloned()));
774
775 let endpoint_info = EndpointInfo::from_txt_lookup(name.to_string(), lookup)?;
776
777 let expected_endpoint_info = EndpointInfo::new(EndpointId::from_str(
778 "1992d53c02cdc04566e5c0edb1ce83305cd550297953a047a445ea3264b54b18",
779 )?)
780 .with_relay_url(Some("https://euw1-1.relay.iroh.network./".parse()?))
781 .with_ip_addrs(BTreeSet::from([
782 "192.168.96.145:60165".parse().unwrap(),
783 "213.208.157.87:60165".parse().unwrap(),
784 ]));
785
786 assert_eq!(endpoint_info, expected_endpoint_info);
787
788 Ok(())
789 }
790}