Skip to content

Commit 51319ec

Browse files
committed
feat: add user data to discovery
1 parent 3e3798f commit 51319ec

File tree

8 files changed

+232
-27
lines changed

8 files changed

+232
-27
lines changed

iroh-dns-server/examples/publish.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
use std::str::FromStr;
1+
use std::{net::SocketAddr, str::FromStr};
22

33
use anyhow::{bail, Result};
44
use clap::{Parser, ValueEnum};
55
use iroh::{
66
discovery::{
77
dns::{N0_DNS_NODE_ORIGIN_PROD, N0_DNS_NODE_ORIGIN_STAGING},
88
pkarr::{PkarrRelayClient, N0_DNS_PKARR_RELAY_PROD, N0_DNS_PKARR_RELAY_STAGING},
9+
UserData,
910
},
1011
dns::node_info::{NodeIdExt, NodeInfo, IROH_TXT_NAME},
1112
NodeId, SecretKey,
@@ -39,7 +40,14 @@ struct Cli {
3940
#[clap(long, conflicts_with = "env")]
4041
pkarr_relay: Option<Url>,
4142
/// Home relay server to publish for this node
42-
relay_url: Url,
43+
#[clap(short, long)]
44+
relay_url: Option<Url>,
45+
/// Direct addresses to publish for this node
46+
#[clap(short, long)]
47+
addr: Vec<SocketAddr>,
48+
/// User data to publish for this node
49+
#[clap(short, long)]
50+
user_data: Option<UserData>,
4351
/// Create a new node secret if IROH_SECRET is unset. Only for development / debugging.
4452
#[clap(short, long)]
4553
create: bool,
@@ -72,12 +80,23 @@ async fn main() -> Result<()> {
7280
};
7381

7482
println!("announce {node_id}:");
75-
println!(" relay={}", args.relay_url);
83+
if let Some(relay_url) = &args.relay_url {
84+
println!(" relay={relay_url}");
85+
}
86+
for addr in &args.addr {
87+
println!(" addr={addr}");
88+
}
89+
if let Some(user_data) = &args.user_data {
90+
println!(" user-data={user_data}");
91+
}
7692
println!();
7793
println!("publish to {pkarr_relay} ...");
7894

7995
let pkarr = PkarrRelayClient::new(pkarr_relay);
80-
let node_info = NodeInfo::new(node_id).with_relay_url(Some(args.relay_url.into()));
96+
let node_info = NodeInfo::new(node_id)
97+
.with_relay_url(args.relay_url.map(Into::into))
98+
.with_direct_addresses(args.addr.into_iter().collect())
99+
.with_user_data(args.user_data);
81100
let signed_packet = node_info.to_pkarr_signed_packet(&secret_key, 30)?;
82101
pkarr.publish(&signed_packet).await?;
83102

iroh-dns-server/examples/resolve.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,8 @@ async fn main() -> anyhow::Result<()> {
5757
for addr in resolved.direct_addresses() {
5858
println!(" addr={addr}")
5959
}
60+
if let Some(user_data) = resolved.user_data() {
61+
println!(" user-data={user_data}")
62+
}
6063
Ok(())
6164
}

iroh-relay/src/dns/node_info.rs

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
3535
use std::{
3636
collections::{BTreeMap, BTreeSet},
37-
fmt::Display,
37+
fmt::{self, Display},
3838
hash::Hash,
3939
net::SocketAddr,
4040
str::FromStr,
@@ -80,7 +80,9 @@ impl NodeIdExt for NodeId {
8080

8181
/// Data about a node that may be published to and resolved from discovery services.
8282
///
83-
/// This includes an optional [`RelayUrl`] and a set of direct addresses.
83+
/// This includes an optional [`RelayUrl`], a set of direct addresses, and the optional
84+
/// [`UserData`], a string that can be set by applications and is not parsed or used by iroh
85+
/// itself.
8486
///
8587
/// This struct does not include the node's [`NodeId`], only the data *about* a certain
8688
/// node. See [`NodeInfo`] for a struct that contains a [`NodeId`] with associated [`NodeData`].
@@ -90,6 +92,8 @@ pub struct NodeData {
9092
relay_url: Option<RelayUrl>,
9193
/// Direct addresses where this node can be reached.
9294
direct_addresses: BTreeSet<SocketAddr>,
95+
/// Optional user-defined [`UserData`] for this node.
96+
user_data: Option<UserData>,
9397
}
9498

9599
impl NodeData {
@@ -98,6 +102,7 @@ impl NodeData {
98102
Self {
99103
relay_url,
100104
direct_addresses,
105+
user_data: None,
101106
}
102107
}
103108

@@ -113,11 +118,22 @@ impl NodeData {
113118
self
114119
}
115120

121+
/// Sets the user-defined data and returns the updated node data.
122+
pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
123+
self.user_data = user_data;
124+
self
125+
}
126+
116127
/// Returns the relay URL of the node.
117128
pub fn relay_url(&self) -> Option<&RelayUrl> {
118129
self.relay_url.as_ref()
119130
}
120131

132+
/// Returns the optional user-defined data of the node.
133+
pub fn user_data(&self) -> Option<&UserData> {
134+
self.user_data.as_ref()
135+
}
136+
121137
/// Returns the direct addresses of the node.
122138
pub fn direct_addresses(&self) -> &BTreeSet<SocketAddr> {
123139
&self.direct_addresses
@@ -137,17 +153,84 @@ impl NodeData {
137153
pub fn set_relay_url(&mut self, relay_url: Option<RelayUrl>) {
138154
self.relay_url = relay_url
139155
}
156+
157+
/// Sets the user-defined data of the node data.
158+
pub fn set_user_data(&mut self, user_data: Option<UserData>) {
159+
self.user_data = user_data;
160+
}
140161
}
141162

142163
impl From<NodeAddr> for NodeData {
143164
fn from(node_addr: NodeAddr) -> Self {
144165
Self {
145166
relay_url: node_addr.relay_url,
146167
direct_addresses: node_addr.direct_addresses,
168+
user_data: None,
169+
}
170+
}
171+
}
172+
173+
// User-defined data that can be published and resolved through node discovery.
174+
///
175+
/// Under the hood this is a UTF-8 String is no longer than [`UserData::MAX_LENGTH`] bytes.
176+
///
177+
/// Iroh does not keep track of or examine the user-defined data.
178+
///
179+
/// `UserData` implements [`FromStr`] and [`TryFrom<String>`], so you can
180+
/// convert `&str` and `String` into `UserData` easily.
181+
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
182+
pub struct UserData(String);
183+
184+
impl UserData {
185+
/// The max byte length allowed for user-defined data.
186+
///
187+
/// In DNS discovery services, the user-defined data is stored in a TXT record character string,
188+
/// which has a max length of 255 bytes. We need to subtract the `user-data=` prefix,
189+
/// which leaves 245 bytes for the actual user-defined data.
190+
pub const MAX_LENGTH: usize = 245;
191+
}
192+
193+
/// Error returned when an input value is too long for [`UserData`].
194+
#[derive(Debug, thiserror::Error)]
195+
#[error("User-defined data exceeds max length")]
196+
pub struct MaxLengthExceededError;
197+
198+
impl TryFrom<String> for UserData {
199+
type Error = MaxLengthExceededError;
200+
201+
fn try_from(value: String) -> Result<Self, Self::Error> {
202+
if value.len() > Self::MAX_LENGTH {
203+
Err(MaxLengthExceededError)
204+
} else {
205+
Ok(Self(value))
206+
}
207+
}
208+
}
209+
210+
impl FromStr for UserData {
211+
type Err = MaxLengthExceededError;
212+
213+
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
214+
if s.len() > Self::MAX_LENGTH {
215+
Err(MaxLengthExceededError)
216+
} else {
217+
Ok(Self(s.to_string()))
147218
}
148219
}
149220
}
150221

222+
impl fmt::Display for UserData {
223+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224+
write!(f, "{}", self.0)
225+
}
226+
}
227+
228+
impl AsRef<str> for UserData {
229+
fn as_ref(&self) -> &str {
230+
&self.0
231+
}
232+
}
233+
151234
/// Information about a node that may be published to and resolved from discovery services.
152235
///
153236
/// This struct couples a [`NodeId`] with its associated [`NodeData`].
@@ -181,9 +264,16 @@ impl From<&TxtAttrs<IrohAttr>> for NodeInfo {
181264
.flatten()
182265
.filter_map(|s| SocketAddr::from_str(s).ok())
183266
.collect();
267+
let user_data = attrs
268+
.get(&IrohAttr::UserData)
269+
.into_iter()
270+
.flatten()
271+
.next()
272+
.and_then(|s| UserData::from_str(s).ok());
184273
let data = NodeData {
185274
relay_url: relay_url.map(Into::into),
186275
direct_addresses,
276+
user_data,
187277
};
188278
Self { node_id, data }
189279
}
@@ -226,6 +316,12 @@ impl NodeInfo {
226316
self
227317
}
228318

319+
/// Sets the user-defined data and returns the updated node info.
320+
pub fn with_user_data(mut self, user_data: Option<UserData>) -> Self {
321+
self.data = self.data.with_user_data(user_data);
322+
self
323+
}
324+
229325
/// Converts into a [`NodeAddr`] by cloning the needed fields.
230326
pub fn to_node_addr(&self) -> NodeAddr {
231327
NodeAddr {
@@ -321,6 +417,8 @@ pub(super) enum IrohAttr {
321417
Relay,
322418
/// Direct address.
323419
Addr,
420+
/// User-defined data
421+
UserData,
324422
}
325423

326424
/// Attributes parsed from [`IROH_TXT_NAME`] TXT records.
@@ -343,6 +441,9 @@ impl From<&NodeInfo> for TxtAttrs<IrohAttr> {
343441
for addr in &info.data.direct_addresses {
344442
attrs.push((IrohAttr::Addr, addr.to_string()));
345443
}
444+
if let Some(user_data) = &info.data.user_data {
445+
attrs.push((IrohAttr::UserData, user_data.to_string()));
446+
}
346447
Self::from_parts(info.node_id, attrs.into_iter())
347448
}
348449
}
@@ -552,7 +653,8 @@ mod tests {
552653
let node_data = NodeData::new(
553654
Some("https://example.com".parse().unwrap()),
554655
["127.0.0.1:1234".parse().unwrap()].into_iter().collect(),
555-
);
656+
)
657+
.with_user_data(Some("foobar".parse().unwrap()));
556658
let node_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
557659
.parse()
558660
.unwrap();
@@ -569,7 +671,8 @@ mod tests {
569671
let node_data = NodeData::new(
570672
Some("https://example.com".parse().unwrap()),
571673
["127.0.0.1:1234".parse().unwrap()].into_iter().collect(),
572-
);
674+
)
675+
.with_user_data(Some("foobar".parse().unwrap()));
573676
let expected = NodeInfo::from_parts(secret_key.public(), node_data);
574677
let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
575678
let actual = NodeInfo::from_pkarr_signed_packet(&packet).unwrap();

iroh/src/discovery.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ use std::sync::Arc;
109109

110110
use anyhow::{anyhow, ensure, Result};
111111
use iroh_base::{NodeAddr, NodeId};
112-
pub use iroh_relay::dns::node_info::{NodeData, NodeInfo};
112+
pub use iroh_relay::dns::node_info::{NodeData, NodeInfo, UserData};
113113
use n0_future::{
114114
stream::{Boxed as BoxStream, StreamExt},
115115
task::{self, AbortOnDropHandle},
@@ -832,7 +832,7 @@ mod tests {
832832
mod test_dns_pkarr {
833833
use anyhow::Result;
834834
use iroh_base::{NodeAddr, SecretKey};
835-
use iroh_relay::RelayMap;
835+
use iroh_relay::{dns::node_info::UserData, RelayMap};
836836
use n0_future::time::Duration;
837837
use tokio_util::task::AbortOnDropHandle;
838838
use tracing_test::traced_test;
@@ -885,20 +885,24 @@ mod test_dns_pkarr {
885885

886886
let resolver = DnsResolver::with_nameserver(dns_pkarr_server.nameserver);
887887
let publisher = PkarrPublisher::new(secret_key, dns_pkarr_server.pkarr_url.clone());
888-
let data = NodeData::new(relay_url.clone(), Default::default());
888+
let user_data: UserData = "foobar".parse().unwrap();
889+
let data = NodeData::new(relay_url.clone(), Default::default())
890+
.with_user_data(Some(user_data.clone()));
889891
// does not block, update happens in background task
890892
publisher.update_node_data(&data);
891893
// wait until our shared state received the update from pkarr publishing
892894
dns_pkarr_server.on_node(&node_id, PUBLISH_TIMEOUT).await?;
893895
let resolved = resolver.lookup_node_by_id(&node_id, &origin).await?;
896+
println!("resolved {resolved:?}");
894897

895-
let expected = NodeAddr {
898+
let expected_addr = NodeAddr {
896899
node_id,
897900
relay_url,
898901
direct_addresses: Default::default(),
899902
};
900903

901-
assert_eq!(resolved.to_node_addr(), expected);
904+
assert_eq!(resolved.to_node_addr(), expected_addr);
905+
assert_eq!(resolved.user_data(), Some(&user_data));
902906
Ok(())
903907
}
904908

0 commit comments

Comments
 (0)