iroh_net/discovery/pkarr.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
//! A discovery service which publishes and resolves node information using a [pkarr] relay.
//!
//! Public-Key Addressable Resource Records, [pkarr], is a system which allows publishing
//! [DNS Resource Records] owned by a particular [`SecretKey`] under a name derived from its
//! corresponding [`PublicKey`], also known as the [`NodeId`]. Additionally this pkarr
//! Resource Record is signed using the same [`SecretKey`], ensuring authenticity of the
//! record.
//!
//! Pkarr normally stores these records on the [Mainline DHT], but also provides two bridges
//! that do not require clients to directly interact with the DHT:
//!
//! - Resolvers are servers which expose the pkarr Resource Record under a domain name,
//! e.g. `o3dks..6uyy.dns.iroh.link`. This allows looking up the pkarr Resource Records
//! using normal DNS clients. These resolvers would normally perform lookups on the
//! Mainline DHT augmented with a local cache to improve performance.
//!
//! - Relays are servers which allow both publishing and looking up of the pkarr Resource
//! Records using HTTP PUT and GET requests. They will usually perform the publishing to
//! the Mainline DHT on behalf on the client as well as cache lookups performed on the DHT
//! to improve performance.
//!
//! For node discovery in iroh-net the pkarr Resource Records contain the [`AddrInfo`]
//! information, providing nodes which retrieve the pkarr Resource Record with enough detail
//! to contact the iroh-net node.
//!
//! There are several node discovery services built on top of pkarr, which can be composed
//! to the application's needs:
//!
//! - [`PkarrPublisher`], which publishes to a pkarr relay server using HTTP.
//!
//! - [`PkarrResolver`], which resolves from a pkarr relay server using HTTP.
//!
//! - [`DnsDiscovery`], which resolves from a DNS server.
//!
//! - [`DhtDiscovery`], which resolves and publishes from both pkarr relay servers and well
//! as the Mainline DHT.
//!
//! [pkarr]: https://pkarr.org
//! [DNS Resource Records]: https://en.wikipedia.org/wiki/Domain_Name_System#Resource_records
//! [Mainline DHT]: https://en.wikipedia.org/wiki/Mainline_DHT
//! [`SecretKey`]: crate::key::SecretKey
//! [`PublicKey`]: crate::key::PublicKey
//! [`NodeId`]: crate::key::NodeId
//! [`DnsDiscovery`]: crate::discovery::dns::DnsDiscovery
//! [`DhtDiscovery`]: dht::DhtDiscovery
use std::sync::Arc;
use anyhow::{anyhow, bail, Result};
use futures_util::stream::BoxStream;
use pkarr::SignedPacket;
use tokio::{
task::JoinHandle,
time::{Duration, Instant},
};
use tracing::{debug, error_span, info, warn, Instrument};
use url::Url;
use watchable::{Watchable, Watcher};
use crate::{
discovery::{Discovery, DiscoveryItem},
dns::node_info::NodeInfo,
key::SecretKey,
relay::force_staging_infra,
AddrInfo, Endpoint, NodeId,
};
#[cfg(feature = "discovery-pkarr-dht")]
#[cfg_attr(iroh_docsrs, doc(cfg(feature = "discovery-pkarr-dht")))]
pub mod dht;
/// The production pkarr relay run by [number 0].
///
/// This server is both a pkarr relay server as well as a DNS resolver, see the [module
/// documentation]. However it does not interact with the Mainline DHT, so is a more
/// central service. It is a reliable service to use for node discovery.
///
/// [number 0]: https://n0.computer
/// [module documentation]: crate::discovery::pkarr
pub const N0_DNS_PKARR_RELAY_PROD: &str = "https://dns.iroh.link/pkarr";
/// The testing pkarr relay run by [number 0].
///
/// This server operates similarly to [`N0_DNS_PKARR_RELAY_PROD`] but is not as reliable.
/// It is meant for more experimental use and testing purposes.
///
/// [number 0]: https://n0.computer
pub const N0_DNS_PKARR_RELAY_STAGING: &str = "https://staging-dns.iroh.link/pkarr";
/// Default TTL for the records in the pkarr signed packet.
///
/// The Time To Live (TTL) tells DNS caches how long to store a record. It is ignored by the
/// `iroh-dns-server`, e.g. as running on [`N0_DNS_PKARR_RELAY_PROD`], as the home server
/// keeps the records for the domain. When using the pkarr relay no DNS is involved and the
/// setting is ignored.
// TODO(flub): huh?
pub const DEFAULT_PKARR_TTL: u32 = 30;
/// Interval in which to republish the node info even if unchanged: 5 minutes.
pub const DEFAULT_REPUBLISH_INTERVAL: Duration = Duration::from_secs(60 * 5);
/// Publisher of node discovery information to a [pkarr] relay.
///
/// This publisher uses HTTP to publish node discovery information to a pkarr relay
/// server, see the [module docs] for details.
///
/// This implements the [`Discovery`] trait to be used as a node discovery service. Note
/// that it only publishes node discovery information, for the corresponding resolver use
/// the [`PkarrResolver`] together with [`ConcurrentDiscovery`].
///
/// This publisher will **only** publish the [`RelayUrl`] if the [`AddrInfo`] contains a
/// [`RelayUrl`]. If the [`AddrInfo`] does not contain a [`RelayUrl`] the *direct
/// addresses* are published instead.
///
/// [pkarr]: https://pkarr.org
/// [module docs]: crate::discovery::pkarr
/// [`RelayUrl`]: crate::relay::RelayUrl
/// [`ConcurrentDiscovery`]: super::ConcurrentDiscovery
#[derive(derive_more::Debug, Clone)]
pub struct PkarrPublisher {
node_id: NodeId,
watchable: Watchable<Option<NodeInfo>>,
join_handle: Arc<JoinHandle<()>>,
}
impl PkarrPublisher {
/// Creates a new publisher for the [`SecretKey`].
///
/// This publisher will be able to publish [pkarr] records for [`SecretKey`]. It will
/// use [`DEFAULT_PKARR_TTL`] as the time-to-live value for the published packets. Will
/// republish discovery information every [`DEFAULT_REPUBLISH_INTERVAL`], even if the
/// information is unchanged.
///
/// [pkarr]: https://pkarr.org
pub fn new(secret_key: SecretKey, pkarr_relay: Url) -> Self {
Self::with_options(
secret_key,
pkarr_relay,
DEFAULT_PKARR_TTL,
DEFAULT_REPUBLISH_INTERVAL,
)
}
/// Creates a new [`PkarrPublisher`] with a custom TTL and republish intervals.
///
/// This allows creating the publisher with custom time-to-live values of the
/// [`pkarr::SignedPacket`]s and well as a custom republish interval.
pub fn with_options(
secret_key: SecretKey,
pkarr_relay: Url,
ttl: u32,
republish_interval: std::time::Duration,
) -> Self {
debug!("creating pkarr publisher that publishes to {pkarr_relay}");
let node_id = secret_key.public();
let pkarr_client = PkarrRelayClient::new(pkarr_relay);
let watchable = Watchable::default();
let service = PublisherService {
ttl,
watcher: watchable.watch(),
secret_key,
pkarr_client,
republish_interval,
};
let join_handle = tokio::task::spawn(
service
.run()
.instrument(error_span!("pkarr_publish", me=%node_id.fmt_short())),
);
Self {
watchable,
node_id,
join_handle: Arc::new(join_handle),
}
}
/// Creates a pkarr publisher which uses the [number 0] pkarr relay server.
///
/// This uses the pkarr relay server operated by [number 0], at
/// [`N0_DNS_PKARR_RELAY_PROD`].
///
/// When running with the environment variable
/// `IROH_FORCE_STAGING_RELAYS` set to any non empty value [`N0_DNS_PKARR_RELAY_STAGING`]
/// server is used instead.
///
/// [number 0]: https://n0.computer
pub fn n0_dns(secret_key: SecretKey) -> Self {
let pkarr_relay = match force_staging_infra() {
true => N0_DNS_PKARR_RELAY_STAGING,
false => N0_DNS_PKARR_RELAY_PROD,
};
let pkarr_relay: Url = pkarr_relay.parse().expect("url is valid");
Self::new(secret_key, pkarr_relay)
}
/// Publishes [`AddrInfo`] about this node to a pkarr relay.
///
/// This is a nonblocking function, the actual update is performed in the background.
pub fn update_addr_info(&self, info: &AddrInfo) {
let (relay_url, direct_addresses) = if let Some(relay_url) = info.relay_url.as_ref() {
(Some(relay_url.clone().into()), Default::default())
} else {
(None, info.direct_addresses.clone())
};
let info = NodeInfo::new(self.node_id, relay_url, direct_addresses);
self.watchable.update(Some(info)).ok();
}
}
impl Discovery for PkarrPublisher {
fn publish(&self, info: &AddrInfo) {
self.update_addr_info(info);
}
}
impl Drop for PkarrPublisher {
fn drop(&mut self) {
// this means we're dropping the last reference
if let Some(handle) = Arc::get_mut(&mut self.join_handle) {
handle.abort();
}
}
}
/// Publish node info to a pkarr relay.
#[derive(derive_more::Debug, Clone)]
struct PublisherService {
#[debug("SecretKey")]
secret_key: SecretKey,
#[debug("PkarrClient")]
pkarr_client: PkarrRelayClient,
watcher: Watcher<Option<NodeInfo>>,
ttl: u32,
republish_interval: Duration,
}
impl PublisherService {
async fn run(self) {
let mut failed_attempts = 0;
let republish = tokio::time::sleep(Duration::MAX);
tokio::pin!(republish);
loop {
if let Some(info) = self.watcher.get() {
if let Err(err) = self.publish_current(info).await {
failed_attempts += 1;
// Retry after increasing timeout
let retry_after = Duration::from_secs(failed_attempts);
republish.as_mut().reset(Instant::now() + retry_after);
warn!(
err = %format!("{err:#}"),
url = %self.pkarr_client.pkarr_relay_url ,
?retry_after,
%failed_attempts,
"Failed to publish to pkarr",
);
} else {
failed_attempts = 0;
// Republish after fixed interval
republish
.as_mut()
.reset(Instant::now() + self.republish_interval);
}
}
// Wait until either the retry/republish timeout is reached, or the node info changed.
tokio::select! {
res = self.watcher.watch_async() => match res {
Ok(()) => debug!("Publish node info to pkarr (info changed)"),
Err(_disconnected) => break,
},
_ = &mut republish => debug!("Publish node info to pkarr (interval elapsed)"),
}
}
}
async fn publish_current(&self, info: NodeInfo) -> Result<()> {
info!(
relay_url = ?info
.relay_url
.as_ref()
.map(|s| s.as_str()),
pkarr_relay = %self.pkarr_client.pkarr_relay_url,
"Publish node info to pkarr"
);
let signed_packet = info.to_pkarr_signed_packet(&self.secret_key, self.ttl)?;
self.pkarr_client.publish(&signed_packet).await?;
Ok(())
}
}
/// Resolver of node discovery information from a [pkarr] relay.
///
/// The resolver uses HTTP to query node discovery information from a pkarr relay server,
/// see the [module docs] for details.
///
/// This implements the [`Discovery`] trait to be used as a node discovery service. Note
/// that it only resolves node discovery information, for the corresponding publisher use
/// the [`PkarrPublisher`] together with [`ConcurrentDiscovery`].
///
/// [pkarr]: https://pkarr.org
/// [module docs]: crate::discovery::pkarr
/// [`ConcurrentDiscovery`]: super::ConcurrentDiscovery
#[derive(derive_more::Debug, Clone)]
pub struct PkarrResolver {
pkarr_client: PkarrRelayClient,
}
impl PkarrResolver {
/// Creates a new publisher using the pkarr relay server at the URL.
pub fn new(pkarr_relay: Url) -> Self {
Self {
pkarr_client: PkarrRelayClient::new(pkarr_relay),
}
}
/// Creates a pkarr resolver which uses the [number 0] pkarr relay server.
///
/// This uses the pkarr relay server operated by [number 0] at
/// [`N0_DNS_PKARR_RELAY_PROD`].
///
/// When running with the environment variable `IROH_FORCE_STAGING_RELAYS`
/// set to any non empty value [`N0_DNS_PKARR_RELAY_STAGING`]
/// server is used instead.
///
/// [number 0]: https://n0.computer
pub fn n0_dns() -> Self {
let pkarr_relay = match force_staging_infra() {
true => N0_DNS_PKARR_RELAY_STAGING,
false => N0_DNS_PKARR_RELAY_PROD,
};
let pkarr_relay: Url = pkarr_relay.parse().expect("url is valid");
Self::new(pkarr_relay)
}
}
impl Discovery for PkarrResolver {
fn resolve(
&self,
_ep: Endpoint,
node_id: NodeId,
) -> Option<BoxStream<'static, Result<DiscoveryItem>>> {
let pkarr_client = self.pkarr_client.clone();
let fut = async move {
let signed_packet = pkarr_client.resolve(node_id).await?;
let info = NodeInfo::from_pkarr_signed_packet(&signed_packet)?;
let item = DiscoveryItem {
node_id,
provenance: "pkarr",
last_updated: None,
addr_info: info.into(),
};
Ok(item)
};
let stream = futures_lite::stream::once_future(fut);
Some(Box::pin(stream))
}
}
/// A [pkarr] client to publish [`pkarr::SignedPacket`]s to a pkarr relay.
///
/// [pkarr]: https://pkarr.org
#[derive(Debug, Clone)]
pub struct PkarrRelayClient {
http_client: reqwest::Client,
pkarr_relay_url: Url,
}
impl PkarrRelayClient {
/// Creates a new client.
pub fn new(pkarr_relay_url: Url) -> Self {
Self {
http_client: reqwest::Client::new(),
pkarr_relay_url,
}
}
/// Resolves a [`SignedPacket`] for the given [`NodeId`].
pub async fn resolve(&self, node_id: NodeId) -> anyhow::Result<SignedPacket> {
let public_key = pkarr::PublicKey::try_from(node_id.as_bytes())?;
let mut url = self.pkarr_relay_url.clone();
url.path_segments_mut()
.map_err(|_| anyhow!("Failed to resolve: Invalid relay URL"))?
.push(&public_key.to_z32());
let response = self.http_client.get(url).send().await?;
if !response.status().is_success() {
bail!(format!(
"Resolve request failed with status {}",
response.status()
))
}
let payload = response.bytes().await?;
Ok(SignedPacket::from_relay_payload(&public_key, &payload)?)
}
/// Publishes a [`SignedPacket`].
pub async fn publish(&self, signed_packet: &SignedPacket) -> anyhow::Result<()> {
let mut url = self.pkarr_relay_url.clone();
url.path_segments_mut()
.map_err(|_| anyhow!("Failed to publish: Invalid relay URL"))?
.push(&signed_packet.public_key().to_z32());
let response = self
.http_client
.put(url)
.body(signed_packet.to_relay_payload())
.send()
.await?;
if !response.status().is_success() {
bail!(format!(
"Publish request failed with status {}",
response.status()
))
}
Ok(())
}
}