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::{NodeAddr, NodeId, RelayUrl, SecretKey, SignatureError};
44use nested_enum_utils::common_fields;
45use snafu::{Backtrace, ResultExt, Snafu};
46use url::Url;
47
48pub const IROH_TXT_NAME: &str = "_iroh";
50
51#[common_fields({
52 backtrace: Option<Backtrace>,
53 #[snafu(implicit)]
54 span_trace: n0_snafu::SpanTrace,
55})]
56#[allow(missing_docs)]
57#[derive(Debug, Snafu)]
58#[non_exhaustive]
59#[snafu(visibility(pub(crate)))]
60pub enum EncodingError {
61 #[snafu(transparent)]
62 FailedBuildingPacket {
63 source: pkarr::errors::SignedPacketBuildError,
64 },
65 #[snafu(display("invalid TXT entry"))]
66 InvalidTxtEntry { source: pkarr::dns::SimpleDnsError },
67}
68
69#[common_fields({
70 backtrace: Option<Backtrace>,
71 #[snafu(implicit)]
72 span_trace: n0_snafu::SpanTrace,
73})]
74#[allow(missing_docs)]
75#[derive(Debug, Snafu)]
76#[non_exhaustive]
77#[snafu(visibility(pub(crate)))]
78pub enum DecodingError {
79 #[snafu(display("node id was not encoded in valid z32"))]
80 InvalidEncodingZ32 { source: z32::Z32Error },
81 #[snafu(display("length must be 32 bytes, but got {len} byte(s)"))]
82 InvalidLength { len: usize },
83 #[snafu(display("node id is not a valid public key"))]
84 InvalidSignature { source: SignatureError },
85}
86
87pub trait NodeIdExt {
90 fn to_z32(&self) -> String;
94
95 fn from_z32(s: &str) -> Result<NodeId, DecodingError>;
99}
100
101impl NodeIdExt for NodeId {
102 fn to_z32(&self) -> String {
103 z32::encode(self.as_bytes())
104 }
105
106 fn from_z32(s: &str) -> Result<NodeId, DecodingError> {
107 let bytes = z32::decode(s.as_bytes()).context(InvalidEncodingZ32Snafu)?;
108 let bytes: &[u8; 32] = &bytes
109 .try_into()
110 .map_err(|_| InvalidLengthSnafu { len: s.len() }.build())?;
111 let node_id = NodeId::from_bytes(bytes).context(InvalidSignatureSnafu)?;
112 Ok(node_id)
113 }
114}
115
116#[derive(Debug, Clone, Default, Eq, PartialEq)]
125pub struct NodeData {
126 relay_url: Option<RelayUrl>,
128 direct_addresses: BTreeSet<SocketAddr>,
130 user_data: Option<UserData>,
132}
133
134impl NodeData {
135 pub fn new(relay_url: Option<RelayUrl>, direct_addresses: BTreeSet<SocketAddr>) -> Self {
137 Self {
138 relay_url,
139 direct_addresses,
140 user_data: None,
141 }
142 }
143
144 pub fn with_relay_url(mut self, relay_url: Option<RelayUrl>) -> Self {
146 self.relay_url = relay_url;
147 self
148 }
149
150 pub fn with_direct_addresses(mut self, direct_addresses: BTreeSet<SocketAddr>) -> Self {
152 self.direct_addresses = direct_addresses;
153 self
154 }
155
156 pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
158 self.user_data = user_data;
159 self
160 }
161
162 pub fn relay_url(&self) -> Option<&RelayUrl> {
164 self.relay_url.as_ref()
165 }
166
167 pub fn user_data(&self) -> Option<&UserData> {
169 self.user_data.as_ref()
170 }
171
172 pub fn direct_addresses(&self) -> &BTreeSet<SocketAddr> {
174 &self.direct_addresses
175 }
176
177 pub fn clear_direct_addresses(&mut self) {
179 self.direct_addresses = Default::default();
180 }
181
182 pub fn add_direct_addresses(&mut self, addrs: impl IntoIterator<Item = SocketAddr>) {
184 self.direct_addresses.extend(addrs)
185 }
186
187 pub fn set_relay_url(&mut self, relay_url: Option<RelayUrl>) {
189 self.relay_url = relay_url
190 }
191
192 pub fn set_user_data(&mut self, user_data: Option<UserData>) {
194 self.user_data = user_data;
195 }
196}
197
198impl From<NodeAddr> for NodeData {
199 fn from(node_addr: NodeAddr) -> Self {
200 Self {
201 relay_url: node_addr.relay_url,
202 direct_addresses: node_addr.direct_addresses,
203 user_data: None,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
217pub struct UserData(String);
218
219impl UserData {
220 pub const MAX_LENGTH: usize = 245;
226}
227
228#[allow(missing_docs)]
230#[derive(Debug, Snafu)]
231pub struct MaxLengthExceededError {
232 backtrace: Option<Backtrace>,
233 #[snafu(implicit)]
234 span_trace: n0_snafu::SpanTrace,
235}
236
237impl TryFrom<String> for UserData {
238 type Error = MaxLengthExceededError;
239
240 fn try_from(value: String) -> Result<Self, Self::Error> {
241 snafu::ensure!(value.len() <= Self::MAX_LENGTH, MaxLengthExceededSnafu);
242 Ok(Self(value))
243 }
244}
245
246impl FromStr for UserData {
247 type Err = MaxLengthExceededError;
248
249 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
250 snafu::ensure!(s.len() <= Self::MAX_LENGTH, MaxLengthExceededSnafu);
251 Ok(Self(s.to_string()))
252 }
253}
254
255impl fmt::Display for UserData {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 write!(f, "{}", self.0)
258 }
259}
260
261impl AsRef<str> for UserData {
262 fn as_ref(&self) -> &str {
263 &self.0
264 }
265}
266
267#[derive(derive_more::Debug, Clone, Eq, PartialEq)]
271pub struct NodeInfo {
272 pub node_id: NodeId,
274 pub data: NodeData,
276}
277
278impl From<TxtAttrs<IrohAttr>> for NodeInfo {
279 fn from(attrs: TxtAttrs<IrohAttr>) -> Self {
280 (&attrs).into()
281 }
282}
283
284impl From<&TxtAttrs<IrohAttr>> for NodeInfo {
285 fn from(attrs: &TxtAttrs<IrohAttr>) -> Self {
286 let node_id = attrs.node_id();
287 let attrs = attrs.attrs();
288 let relay_url = attrs
289 .get(&IrohAttr::Relay)
290 .into_iter()
291 .flatten()
292 .next()
293 .and_then(|s| Url::parse(s).ok());
294 let direct_addresses = attrs
295 .get(&IrohAttr::Addr)
296 .into_iter()
297 .flatten()
298 .filter_map(|s| SocketAddr::from_str(s).ok())
299 .collect();
300 let user_data = attrs
301 .get(&IrohAttr::UserData)
302 .into_iter()
303 .flatten()
304 .next()
305 .and_then(|s| UserData::from_str(s).ok());
306 let data = NodeData {
307 relay_url: relay_url.map(Into::into),
308 direct_addresses,
309 user_data,
310 };
311 Self { node_id, data }
312 }
313}
314
315impl From<NodeInfo> for NodeAddr {
316 fn from(value: NodeInfo) -> Self {
317 value.into_node_addr()
318 }
319}
320
321impl From<NodeAddr> for NodeInfo {
322 fn from(addr: NodeAddr) -> Self {
323 Self::new(addr.node_id)
324 .with_relay_url(addr.relay_url)
325 .with_direct_addresses(addr.direct_addresses)
326 }
327}
328
329impl NodeInfo {
330 pub fn new(node_id: NodeId) -> Self {
332 Self::from_parts(node_id, Default::default())
333 }
334
335 pub fn from_parts(node_id: NodeId, data: NodeData) -> Self {
337 Self { node_id, data }
338 }
339
340 pub fn with_relay_url(mut self, relay_url: Option<RelayUrl>) -> Self {
342 self.data = self.data.with_relay_url(relay_url);
343 self
344 }
345
346 pub fn with_direct_addresses(mut self, direct_addresses: BTreeSet<SocketAddr>) -> Self {
348 self.data = self.data.with_direct_addresses(direct_addresses);
349 self
350 }
351
352 pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
354 self.data = self.data.with_user_data(user_data);
355 self
356 }
357
358 pub fn to_node_addr(&self) -> NodeAddr {
360 NodeAddr {
361 node_id: self.node_id,
362 relay_url: self.data.relay_url.clone(),
363 direct_addresses: self.data.direct_addresses.clone(),
364 }
365 }
366
367 pub fn into_node_addr(self) -> NodeAddr {
369 NodeAddr {
370 node_id: self.node_id,
371 relay_url: self.data.relay_url,
372 direct_addresses: self.data.direct_addresses,
373 }
374 }
375
376 fn to_attrs(&self) -> TxtAttrs<IrohAttr> {
377 self.into()
378 }
379
380 #[cfg(not(wasm_browser))]
381 pub fn from_txt_lookup(
383 domain_name: String,
384 lookup: impl Iterator<Item = crate::dns::TxtRecordData>,
385 ) -> Result<Self, ParseError> {
386 let attrs = TxtAttrs::from_txt_lookup(domain_name, lookup)?;
387 Ok(Self::from(attrs))
388 }
389
390 pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result<Self, ParseError> {
392 let attrs = TxtAttrs::from_pkarr_signed_packet(packet)?;
393 Ok(attrs.into())
394 }
395
396 pub fn to_pkarr_signed_packet(
400 &self,
401 secret_key: &SecretKey,
402 ttl: u32,
403 ) -> Result<pkarr::SignedPacket, EncodingError> {
404 self.to_attrs().to_pkarr_signed_packet(secret_key, ttl)
405 }
406
407 pub fn to_txt_strings(&self) -> Vec<String> {
409 self.to_attrs().to_txt_strings().collect()
410 }
411}
412
413#[common_fields({
414 backtrace: Option<Backtrace>,
415 #[snafu(implicit)]
416 span_trace: n0_snafu::SpanTrace,
417})]
418#[allow(missing_docs)]
419#[derive(Debug, Snafu)]
420#[non_exhaustive]
421#[snafu(visibility(pub(crate)))]
422pub enum ParseError {
423 #[snafu(display("Expected format `key=value`, received `{s}`"))]
424 UnexpectedFormat { s: String },
425 #[snafu(display("Could not convert key to Attr"))]
426 AttrFromString { key: String },
427 #[snafu(display("Expected 2 labels, received {num_labels}"))]
428 NumLabels { num_labels: usize },
429 #[snafu(display("Could not parse labels"))]
430 Utf8 { source: Utf8Error },
431 #[snafu(display("Record is not an `iroh` record, expected `_iroh`, got `{label}`"))]
432 NotAnIrohRecord { label: String },
433 #[snafu(transparent)]
434 DecodingError {
435 #[snafu(source(from(DecodingError, Box::new)))]
436 source: Box<DecodingError>,
437 },
438}
439
440impl std::ops::Deref for NodeInfo {
441 type Target = NodeData;
442 fn deref(&self) -> &Self::Target {
443 &self.data
444 }
445}
446
447impl std::ops::DerefMut for NodeInfo {
448 fn deref_mut(&mut self) -> &mut Self::Target {
449 &mut self.data
450 }
451}
452
453#[cfg(not(wasm_browser))]
459fn node_id_from_txt_name(name: &str) -> Result<NodeId, ParseError> {
460 let num_labels = name.split(".").count();
461 if num_labels < 2 {
462 return Err(NumLabelsSnafu { num_labels }.build());
463 }
464 let mut labels = name.split(".");
465 let label = labels.next().expect("checked above");
466 if label != IROH_TXT_NAME {
467 return Err(NotAnIrohRecordSnafu { label }.build());
468 }
469 let label = labels.next().expect("checked above");
470 let node_id = NodeId::from_z32(label)?;
471 Ok(node_id)
472}
473
474#[derive(
478 Debug, strum::Display, strum::AsRefStr, strum::EnumString, Hash, Eq, PartialEq, Ord, PartialOrd,
479)]
480#[strum(serialize_all = "kebab-case")]
481pub(crate) enum IrohAttr {
482 Relay,
484 Addr,
486 UserData,
488}
489
490#[derive(Debug)]
496pub(crate) struct TxtAttrs<T> {
497 node_id: NodeId,
498 attrs: BTreeMap<T, Vec<String>>,
499}
500
501impl From<&NodeInfo> for TxtAttrs<IrohAttr> {
502 fn from(info: &NodeInfo) -> Self {
503 let mut attrs = vec![];
504 if let Some(relay_url) = &info.data.relay_url {
505 attrs.push((IrohAttr::Relay, relay_url.to_string()));
506 }
507 for addr in &info.data.direct_addresses {
508 attrs.push((IrohAttr::Addr, addr.to_string()));
509 }
510 if let Some(user_data) = &info.data.user_data {
511 attrs.push((IrohAttr::UserData, user_data.to_string()));
512 }
513 Self::from_parts(info.node_id, attrs.into_iter())
514 }
515}
516
517impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
518 pub(crate) fn from_parts(node_id: NodeId, pairs: impl Iterator<Item = (T, String)>) -> Self {
520 let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
521 for (k, v) in pairs {
522 attrs.entry(k).or_default().push(v);
523 }
524 Self { attrs, node_id }
525 }
526
527 pub(crate) fn from_strings(
529 node_id: NodeId,
530 strings: impl Iterator<Item = String>,
531 ) -> Result<Self, ParseError> {
532 let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
533 for s in strings {
534 let mut parts = s.split('=');
535 let (Some(key), Some(value)) = (parts.next(), parts.next()) else {
536 return Err(UnexpectedFormatSnafu { s }.build());
537 };
538 let attr = T::from_str(key).map_err(|_| AttrFromStringSnafu { key }.build())?;
539 attrs.entry(attr).or_default().push(value.to_string());
540 }
541 Ok(Self { attrs, node_id })
542 }
543
544 pub(crate) fn attrs(&self) -> &BTreeMap<T, Vec<String>> {
546 &self.attrs
547 }
548
549 pub(crate) fn node_id(&self) -> NodeId {
551 self.node_id
552 }
553
554 pub(crate) fn from_pkarr_signed_packet(
556 packet: &pkarr::SignedPacket,
557 ) -> Result<Self, ParseError> {
558 use pkarr::dns::{
559 rdata::RData,
560 {self},
561 };
562 let pubkey = packet.public_key();
563 let pubkey_z32 = pubkey.to_z32();
564 let node_id = NodeId::from(*pubkey.verifying_key());
565 let zone = dns::Name::new(&pubkey_z32).expect("z32 encoding is valid");
566 let txt_data = packet
567 .all_resource_records()
568 .filter_map(|rr| match &rr.rdata {
569 RData::TXT(txt) => match rr.name.without(&zone) {
570 Some(name) if name.to_string() == IROH_TXT_NAME => Some(txt),
571 Some(_) | None => None,
572 },
573 _ => None,
574 });
575
576 let txt_strs = txt_data.filter_map(|s| String::try_from(s.clone()).ok());
577 Self::from_strings(node_id, txt_strs)
578 }
579
580 #[cfg(not(wasm_browser))]
582 pub(crate) fn from_txt_lookup(
583 name: String,
584 lookup: impl Iterator<Item = crate::dns::TxtRecordData>,
585 ) -> Result<Self, ParseError> {
586 let queried_node_id = node_id_from_txt_name(&name)?;
587
588 let strings = lookup.map(|record| record.to_string());
589 Self::from_strings(queried_node_id, strings)
590 }
591
592 fn to_txt_strings(&self) -> impl Iterator<Item = String> + '_ {
593 self.attrs
594 .iter()
595 .flat_map(move |(k, vs)| vs.iter().map(move |v| format!("{k}={v}")))
596 }
597
598 pub(crate) fn to_pkarr_signed_packet(
602 &self,
603 secret_key: &SecretKey,
604 ttl: u32,
605 ) -> Result<pkarr::SignedPacket, EncodingError> {
606 use pkarr::dns::{self, rdata};
607 let keypair = pkarr::Keypair::from_secret_key(&secret_key.to_bytes());
608 let name = dns::Name::new(IROH_TXT_NAME).expect("constant");
609
610 let mut builder = pkarr::SignedPacket::builder();
611 for s in self.to_txt_strings() {
612 let mut txt = rdata::TXT::new();
613 txt.add_string(&s).context(InvalidTxtEntrySnafu)?;
614 builder = builder.txt(name.clone(), txt.into_owned(), ttl);
615 }
616 let signed_packet = builder.build(&keypair)?;
617 Ok(signed_packet)
618 }
619}
620
621#[cfg(not(wasm_browser))]
622pub(crate) fn ensure_iroh_txt_label(name: String) -> String {
623 let mut parts = name.split(".");
624 if parts.next() == Some(IROH_TXT_NAME) {
625 name
626 } else {
627 format!("{IROH_TXT_NAME}.{name}")
628 }
629}
630
631#[cfg(not(wasm_browser))]
632pub(crate) fn node_domain(node_id: &NodeId, origin: &str) -> String {
633 format!("{}.{}", NodeId::to_z32(node_id), origin)
634}
635
636#[cfg(test)]
637mod tests {
638 use std::{collections::BTreeSet, str::FromStr, sync::Arc};
639
640 use hickory_resolver::{
641 Name,
642 lookup::Lookup,
643 proto::{
644 op::Query,
645 rr::{
646 RData, Record, RecordType,
647 rdata::{A, TXT},
648 },
649 },
650 };
651 use iroh_base::{NodeId, SecretKey};
652 use n0_snafu::{Result, ResultExt};
653
654 use super::{NodeData, NodeIdExt, NodeInfo};
655 use crate::dns::TxtRecordData;
656
657 #[test]
658 fn txt_attr_roundtrip() {
659 let node_data = NodeData::new(
660 Some("https://example.com".parse().unwrap()),
661 ["127.0.0.1:1234".parse().unwrap()].into_iter().collect(),
662 )
663 .with_user_data(Some("foobar".parse().unwrap()));
664 let node_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
665 .parse()
666 .unwrap();
667 let expected = NodeInfo::from_parts(node_id, node_data);
668 let attrs = expected.to_attrs();
669 let actual = NodeInfo::from(&attrs);
670 assert_eq!(expected, actual);
671 }
672
673 #[test]
674 fn signed_packet_roundtrip() {
675 let secret_key =
676 SecretKey::from_str("vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia").unwrap();
677 let node_data = NodeData::new(
678 Some("https://example.com".parse().unwrap()),
679 ["127.0.0.1:1234".parse().unwrap()].into_iter().collect(),
680 )
681 .with_user_data(Some("foobar".parse().unwrap()));
682 let expected = NodeInfo::from_parts(secret_key.public(), node_data);
683 let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
684 let actual = NodeInfo::from_pkarr_signed_packet(&packet).unwrap();
685 assert_eq!(expected, actual);
686 }
687
688 #[test]
695 fn test_from_hickory_lookup() -> Result {
696 let name = Name::from_utf8(
697 "_iroh.dgjpkxyn3zyrk3zfads5duwdgbqpkwbjxfj4yt7rezidr3fijccy.dns.iroh.link.",
698 )
699 .context("dns name")?;
700 let query = Query::query(name.clone(), RecordType::TXT);
701 let records = [
702 Record::from_rdata(
703 name.clone(),
704 30,
705 RData::TXT(TXT::new(vec!["addr=192.168.96.145:60165".to_string()])),
706 ),
707 Record::from_rdata(
708 name.clone(),
709 30,
710 RData::TXT(TXT::new(vec!["addr=213.208.157.87:60165".to_string()])),
711 ),
712 Record::from_rdata(name.clone(), 30, RData::A(A::new(127, 0, 0, 1))),
714 Record::from_rdata(
716 Name::from_utf8(format!(
717 "_iroh.{}.dns.iroh.link.",
718 NodeId::from_str(
719 "a55f26132e5e43de834d534332f66a20d480c3e50a13a312a071adea6569981e"
721 )?
722 .to_z32()
723 ))
724 .context("name")?,
725 30,
726 RData::TXT(TXT::new(vec![
727 "relay=https://euw1-1.relay.iroh.network./".to_string(),
728 ])),
729 ),
730 Record::from_rdata(
732 Name::from_utf8("dns.iroh.link.").context("name")?,
733 30,
734 RData::TXT(TXT::new(vec![
735 "relay=https://euw1-1.relay.iroh.network./".to_string(),
736 ])),
737 ),
738 Record::from_rdata(
739 name.clone(),
740 30,
741 RData::TXT(TXT::new(vec![
742 "relay=https://euw1-1.relay.iroh.network./".to_string(),
743 ])),
744 ),
745 ];
746 let lookup = Lookup::new_with_max_ttl(query, Arc::new(records));
747 let lookup = hickory_resolver::lookup::TxtLookup::from(lookup);
748 let lookup = lookup
749 .into_iter()
750 .map(|txt| TxtRecordData::from_iter(txt.iter().cloned()));
751
752 let node_info = NodeInfo::from_txt_lookup(name.to_string(), lookup)?;
753
754 let expected_node_info = NodeInfo::new(NodeId::from_str(
755 "1992d53c02cdc04566e5c0edb1ce83305cd550297953a047a445ea3264b54b18",
756 )?)
757 .with_relay_url(Some("https://euw1-1.relay.iroh.network./".parse()?))
758 .with_direct_addresses(BTreeSet::from([
759 "192.168.96.145:60165".parse().unwrap(),
760 "213.208.157.87:60165".parse().unwrap(),
761 ]));
762
763 assert_eq!(node_info, expected_node_info);
764
765 Ok(())
766 }
767}