Skip to content

Commit a2cd248

Browse files
committed
feat: add user data to discovery
1 parent 33f3a8a commit a2cd248

File tree

10 files changed

+268
-40
lines changed

10 files changed

+268
-40
lines changed

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ unexpected_cfgs = { level = "warn", check-cfg = ["cfg(iroh_docsrs)", "cfg(iroh_l
4141

4242
[workspace.lints.clippy]
4343
unused-async = "warn"
44+
45+
[patch.crates-io]
46+
swarm-discovery = { git = "https://github.com/Frando/swarm-discovery.git", branch = "feat/txt" }

iroh-dns-server/examples/publish.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +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-
NodeData,
9+
NodeData, UserData,
1010
},
1111
dns::node_info::{NodeIdExt, NodeInfo, IROH_TXT_NAME},
1212
NodeId, SecretKey,
@@ -40,7 +40,14 @@ struct Cli {
4040
#[clap(long, conflicts_with = "env")]
4141
pkarr_relay: Option<Url>,
4242
/// Home relay server to publish for this node
43-
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>,
4451
/// Create a new node secret if IROH_SECRET is unset. Only for development / debugging.
4552
#[clap(short, long)]
4653
create: bool,
@@ -73,12 +80,24 @@ async fn main() -> Result<()> {
7380
};
7481

7582
println!("announce {node_id}:");
76-
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+
}
7792
println!();
7893
println!("publish to {pkarr_relay} ...");
7994

8095
let pkarr = PkarrRelayClient::new(pkarr_relay);
81-
let data = NodeData::default().with_relay_url(args.relay_url);
96+
let data = NodeData::new(
97+
args.relay_url.map(Into::into),
98+
args.addr.into_iter().collect(),
99+
)
100+
.with_user_data(args.user_data);
82101
let node_info = NodeInfo::new(node_id, data);
83102
let signed_packet = node_info.to_pkarr_signed_packet(&secret_key, 30)?;
84103
pkarr.publish(&signed_packet).await?;

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: 121 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 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,91 @@ 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 data of the node data.
158+
pub fn set_user_data(&mut self, user_data: Option<UserData>) {
159+
self.user_data = user_data;
160+
}
161+
162+
/// Converts into a [`NodeAddr`] and [`UserData`].
163+
pub fn into_node_addr_and_user_data(self, node_id: NodeId) -> (NodeAddr, Option<UserData>) {
164+
(
165+
NodeAddr {
166+
node_id,
167+
relay_url: self.relay_url,
168+
direct_addresses: self.direct_addresses,
169+
},
170+
self.user_data,
171+
)
172+
}
140173
}
141174

142175
impl From<NodeAddr> for NodeData {
143176
fn from(node_addr: NodeAddr) -> Self {
144177
Self {
145178
relay_url: node_addr.relay_url,
146179
direct_addresses: node_addr.direct_addresses,
180+
user_data: None,
181+
}
182+
}
183+
}
184+
185+
// User defined data that can be published and resolved through node discovery.
186+
///
187+
/// Under the hood this is a UTF-8 String that is less than or equal to
188+
/// [`USER_DATA_MAX_LENGTH`] bytes.
189+
///
190+
/// Iroh does not keep track of or examine user defined data.
191+
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
192+
pub struct UserData(String);
193+
194+
/// The max byte length allowed for user defined data.
195+
///
196+
/// In DNS discovery services, the user data is stored in a TXT record character string,
197+
/// which has a max length of 255 bytes. We need to subtract "user-data=", which leaves
198+
/// 245 bytes for the actual user data.
199+
pub const USER_DATA_MAX_LENGTH: usize = 245;
200+
201+
/// Error returned when an input value is too long for [`UserData`].
202+
#[derive(Debug, thiserror::Error)]
203+
#[error("User-defined data exceeds max length")]
204+
pub struct MaxLengthExceededError;
205+
206+
impl TryFrom<String> for UserData {
207+
type Error = MaxLengthExceededError;
208+
fn try_from(value: String) -> Result<Self, Self::Error> {
209+
if value.len() > USER_DATA_MAX_LENGTH {
210+
Err(MaxLengthExceededError)
211+
} else {
212+
Ok(Self(value))
213+
}
214+
}
215+
}
216+
217+
impl FromStr for UserData {
218+
type Err = MaxLengthExceededError;
219+
220+
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
221+
if s.len() > USER_DATA_MAX_LENGTH {
222+
Err(MaxLengthExceededError)
223+
} else {
224+
Ok(Self(s.to_string()))
147225
}
148226
}
149227
}
150228

229+
impl fmt::Display for UserData {
230+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231+
write!(f, "{}", self.0)
232+
}
233+
}
234+
235+
impl AsRef<str> for UserData {
236+
fn as_ref(&self) -> &str {
237+
&self.0
238+
}
239+
}
240+
151241
/// Information about a node that may be published to and resolved from discovery services.
152242
///
153243
/// This struct couples a [`NodeId`] with its associated [`NodeData`].
@@ -181,9 +271,16 @@ impl From<&TxtAttrs<IrohAttr>> for NodeInfo {
181271
.flatten()
182272
.filter_map(|s| SocketAddr::from_str(s).ok())
183273
.collect();
274+
let user_data = attrs
275+
.get(&IrohAttr::UserData)
276+
.into_iter()
277+
.flatten()
278+
.next()
279+
.and_then(|s| UserData::from_str(s).ok());
184280
let data = NodeData {
185281
relay_url: relay_url.map(Into::into),
186282
direct_addresses,
283+
user_data,
187284
};
188285
Self { node_id, data }
189286
}
@@ -200,6 +297,7 @@ impl From<NodeAddr> for NodeInfo {
200297
let data = NodeData {
201298
direct_addresses: value.direct_addresses,
202299
relay_url: value.relay_url,
300+
user_data: None,
203301
};
204302
Self::new(value.node_id, data)
205303
}
@@ -260,6 +358,18 @@ impl NodeInfo {
260358
pub fn to_txt_strings(&self) -> Vec<String> {
261359
self.to_attrs().to_txt_strings().collect()
262360
}
361+
362+
/// Converts into a [`NodeAddr`] and optional [`UserData`].
363+
pub fn into_node_addr_and_user_data(self) -> (NodeAddr, Option<UserData>) {
364+
(
365+
NodeAddr {
366+
node_id: self.node_id,
367+
relay_url: self.data.relay_url,
368+
direct_addresses: self.data.direct_addresses,
369+
},
370+
self.data.user_data,
371+
)
372+
}
263373
}
264374

265375
impl std::ops::Deref for NodeInfo {
@@ -300,6 +410,8 @@ pub(super) enum IrohAttr {
300410
Relay,
301411
/// Direct address.
302412
Addr,
413+
/// User-defined data
414+
UserData,
303415
}
304416

305417
/// Attributes parsed from [`IROH_TXT_NAME`] TXT records.
@@ -322,6 +434,9 @@ impl From<&NodeInfo> for TxtAttrs<IrohAttr> {
322434
for addr in &info.data.direct_addresses {
323435
attrs.push((IrohAttr::Addr, addr.to_string()));
324436
}
437+
if let Some(user_data) = &info.data.user_data {
438+
attrs.push((IrohAttr::UserData, user_data.to_string()));
439+
}
325440
Self::from_parts(info.node_id, attrs.into_iter())
326441
}
327442
}
@@ -531,7 +646,8 @@ mod tests {
531646
let node_data = NodeData::new(
532647
Some("https://example.com".parse().unwrap()),
533648
["127.0.0.1:1234".parse().unwrap()].into_iter().collect(),
534-
);
649+
)
650+
.with_user_data(Some("foobar".parse().unwrap()));
535651
let node_id = "vpnk377obfvzlipnsfbqba7ywkkenc4xlpmovt5tsfujoa75zqia"
536652
.parse()
537653
.unwrap();
@@ -548,7 +664,8 @@ mod tests {
548664
let node_data = NodeData::new(
549665
Some("https://example.com".parse().unwrap()),
550666
["127.0.0.1:1234".parse().unwrap()].into_iter().collect(),
551-
);
667+
)
668+
.with_user_data(Some("foobar".parse().unwrap()));
552669
let expected = NodeInfo::new(secret_key.public(), node_data);
553670
let packet = expected.to_pkarr_signed_packet(&secret_key, 30).unwrap();
554671
let actual = NodeInfo::from_pkarr_signed_packet(&packet).unwrap();

iroh/src/discovery.rs

Lines changed: 11 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},
@@ -203,6 +203,7 @@ impl<T: Discovery> Discovery for Arc<T> {}
203203
/// This struct derefs to [`NodeData`], so you can access the methods from [`NodeData`]
204204
/// directly from [`DiscoveryItem`].
205205
#[derive(Debug, Clone)]
206+
#[non_exhaustive]
206207
pub struct DiscoveryItem {
207208
/// The node info for the node, as discovered by the the discovery service.
208209
node_info: NodeInfo,
@@ -814,7 +815,7 @@ mod tests {
814815
mod test_dns_pkarr {
815816
use anyhow::Result;
816817
use iroh_base::{NodeAddr, SecretKey};
817-
use iroh_relay::RelayMap;
818+
use iroh_relay::{dns::node_info::UserData, RelayMap};
818819
use n0_future::time::Duration;
819820
use tokio_util::task::AbortOnDropHandle;
820821
use tracing_test::traced_test;
@@ -853,7 +854,7 @@ mod test_dns_pkarr {
853854
.lookup_node_by_id(&node_info.node_id, &origin)
854855
.await?;
855856

856-
assert_eq!(resolved, node_info.into());
857+
assert_eq!(resolved, node_info);
857858

858859
Ok(())
859860
}
@@ -872,20 +873,25 @@ mod test_dns_pkarr {
872873

873874
let resolver = DnsResolver::with_nameserver(dns_pkarr_server.nameserver);
874875
let publisher = PkarrPublisher::new(secret_key, dns_pkarr_server.pkarr_url.clone());
875-
let data = NodeData::new(relay_url.clone(), Default::default());
876+
let user_data: UserData = "foobar".parse().unwrap();
877+
let data = NodeData::new(relay_url.clone(), Default::default())
878+
.with_user_data(Some(user_data.clone()));
876879
// does not block, update happens in background task
877880
publisher.update_addr_info(&data);
878881
// wait until our shared state received the update from pkarr publishing
879882
dns_pkarr_server.on_node(&node_id, PUBLISH_TIMEOUT).await?;
880883
let resolved = resolver.lookup_node_by_id(&node_id, &origin).await?;
884+
println!("resolved {resolved:?}");
881885

882886
let expected = NodeAddr {
883887
node_id,
884888
relay_url,
885889
direct_addresses: Default::default(),
886890
};
891+
let (resolved_addr, resolved_user_data) = resolved.into_node_addr_and_user_data();
887892

888-
assert_eq!(resolved.node_addr(), expected);
893+
assert_eq!(resolved_addr, expected);
894+
assert_eq!(resolved_user_data, Some(user_data));
889895
Ok(())
890896
}
891897

0 commit comments

Comments
 (0)