use std::sync::Arc;
use iroh_base::{EndpointId, RelayUrl, SecretKey};
use iroh_dns::pkarr::{SignedPacket, SignedPacketVerifyError};
use iroh_relay::endpoint_info::{AddrFilter, EncodingError, EndpointInfo};
use n0_error::{e, stack_error};
use n0_future::{
boxed::BoxStream,
task::{self, AbortOnDropHandle},
time::{self, Duration, Instant},
};
use n0_watcher::{Disconnected, Watchable, Watcher as _};
use tracing::{Instrument, debug, error_span, trace, warn};
use url::Url;
#[cfg(not(wasm_browser))]
use crate::dns::DnsResolver;
use crate::{
Endpoint,
address_lookup::{
AddressLookup, AddressLookupBuilder, AddressLookupBuilderError, EndpointData,
Error as AddressLookupError, Item as AddressLookupItem,
},
endpoint::force_staging_infra,
util::reqwest_client_builder,
};
#[cfg(feature = "address-lookup-pkarr-dht")]
pub mod dht;
#[allow(missing_docs)]
#[stack_error(derive, add_meta)]
#[non_exhaustive]
pub enum PkarrError {
#[error("Invalid public key")]
PublicKey {
#[error(std_err)]
source: iroh_base::KeyParsingError,
},
#[error("Packet failed to verify")]
Verify {
#[error(std_err)]
source: SignedPacketVerifyError,
},
#[error("Invalid relay URL")]
InvalidRelayUrl { url: RelayUrl },
#[error("Error sending http request")]
HttpSend {
#[error(std_err)]
source: reqwest::Error,
},
#[error("Error resolving http request")]
HttpRequest { status: reqwest::StatusCode },
#[error("Http payload error")]
HttpPayload {
#[error(std_err)]
source: reqwest::Error,
},
#[error("EncodingError")]
Encoding { source: EncodingError },
}
impl From<PkarrError> for AddressLookupError {
fn from(err: PkarrError) -> Self {
AddressLookupError::from_err_any("pkarr", err)
}
}
pub const N0_DNS_PKARR_RELAY_PROD: &str = "https://dns.iroh.link/pkarr";
pub const N0_DNS_PKARR_RELAY_STAGING: &str = "https://staging-dns.iroh.link/pkarr";
pub const DEFAULT_PKARR_TTL: u32 = 30;
pub const DEFAULT_REPUBLISH_INTERVAL: Duration = Duration::from_secs(60 * 5);
#[derive(Debug)]
pub struct PkarrPublisherBuilder {
pkarr_relay: Url,
ttl: u32,
republish_interval: Duration,
filter: AddrFilter,
#[cfg(not(wasm_browser))]
dns_resolver: Option<DnsResolver>,
}
impl PkarrPublisherBuilder {
fn new(pkarr_relay: Url) -> Self {
Self {
pkarr_relay,
ttl: DEFAULT_PKARR_TTL,
republish_interval: DEFAULT_REPUBLISH_INTERVAL,
filter: AddrFilter::relay_only(),
#[cfg(not(wasm_browser))]
dns_resolver: None,
}
}
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)
}
pub fn ttl(mut self, ttl: u32) -> Self {
self.ttl = ttl;
self
}
pub fn republish_interval(mut self, republish_interval: Duration) -> Self {
self.republish_interval = republish_interval;
self
}
#[cfg(not(wasm_browser))]
pub fn dns_resolver(mut self, dns_resolver: DnsResolver) -> Self {
self.dns_resolver = Some(dns_resolver);
self
}
pub fn addr_filter(mut self, filter: AddrFilter) -> Self {
self.filter = filter;
self
}
pub fn build(self, secret_key: SecretKey, tls_config: rustls::ClientConfig) -> PkarrPublisher {
PkarrPublisher::new(
secret_key,
self.pkarr_relay,
self.ttl,
self.republish_interval,
#[cfg(not(wasm_browser))]
self.dns_resolver,
tls_config,
self.filter,
)
}
}
impl AddressLookupBuilder for PkarrPublisherBuilder {
fn into_address_lookup(
mut self,
endpoint: &Endpoint,
) -> Result<impl AddressLookup, AddressLookupBuilderError> {
#[cfg(not(wasm_browser))]
if self.dns_resolver.is_none() {
self.dns_resolver = Some(endpoint.dns_resolver()?.clone());
}
let tls_config = endpoint.tls_config().clone();
Ok(self.build(endpoint.secret_key().clone(), tls_config))
}
}
#[derive(derive_more::Debug, Clone)]
pub struct PkarrPublisher {
endpoint_id: EndpointId,
watchable: Watchable<Option<EndpointInfo>>,
addr_filter: AddrFilter,
_drop_guard: Arc<AbortOnDropHandle<()>>,
}
impl PkarrPublisher {
pub fn builder(pkarr_relay: Url) -> PkarrPublisherBuilder {
PkarrPublisherBuilder::new(pkarr_relay)
}
fn new(
secret_key: SecretKey,
pkarr_relay: Url,
ttl: u32,
republish_interval: Duration,
#[cfg(not(wasm_browser))] dns_resolver: Option<DnsResolver>,
tls_config: rustls::ClientConfig,
addr_filter: AddrFilter,
) -> Self {
debug!("creating pkarr publisher that publishes to {pkarr_relay}");
let endpoint_id = secret_key.public();
#[cfg(wasm_browser)]
let pkarr_client = PkarrRelayClient::new(pkarr_relay, tls_config);
#[cfg(not(wasm_browser))]
let pkarr_client = PkarrRelayClient::builder(pkarr_relay, tls_config)
.set_dns_resolver(dns_resolver)
.build();
let watchable = Watchable::default();
let service = PublisherService {
ttl,
watcher: watchable.watch(),
secret_key,
pkarr_client,
republish_interval,
};
let join_handle = task::spawn(
service
.run()
.instrument(error_span!("pkarr_publish", me=%endpoint_id.fmt_short())),
);
Self {
watchable,
endpoint_id,
addr_filter,
_drop_guard: Arc::new(AbortOnDropHandle::new(join_handle)),
}
}
pub fn n0_dns() -> PkarrPublisherBuilder {
PkarrPublisherBuilder::n0_dns()
}
pub fn update_endpoint_data(&self, data: &EndpointData) {
let data = data.apply_filter(&self.addr_filter).into_owned();
let info = EndpointInfo::from_parts(self.endpoint_id, data);
self.watchable.set(Some(info)).ok();
}
}
impl AddressLookup for PkarrPublisher {
fn publish(&self, data: &EndpointData) {
self.update_endpoint_data(data);
}
}
#[derive(derive_more::Debug, Clone)]
struct PublisherService {
#[debug("SecretKey")]
secret_key: SecretKey,
#[debug("PkarrClient")]
pkarr_client: PkarrRelayClient,
watcher: n0_watcher::Direct<Option<EndpointInfo>>,
ttl: u32,
republish_interval: Duration,
}
impl PublisherService {
async fn run(mut self) {
let mut failed_attempts = 0;
let republish = time::sleep(Duration::MAX);
tokio::pin!(republish);
loop {
if !self.watcher.is_connected() {
break;
}
if let Some(info) = self.watcher.get() {
match self.publish_current(info).await {
Err(err) => {
failed_attempts += 1;
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",
);
}
Ok(()) => {
failed_attempts = 0;
republish
.as_mut()
.reset(Instant::now() + self.republish_interval);
}
}
}
tokio::select! {
res = self.watcher.updated() => match res {
Ok(_) => debug!("Publish endpoint info to pkarr (info changed)"),
Err(Disconnected { .. }) => break,
},
_ = &mut republish => debug!("Publish endpoint info to pkarr (interval elapsed)"),
}
}
}
async fn publish_current(&self, info: EndpointInfo) -> Result<(), PkarrError> {
debug!(
data = ?info.data,
pkarr_relay = %self.pkarr_client.pkarr_relay_url,
"Publishing endpoint info to pkarr"
);
let signed_packet = info
.to_pkarr_signed_packet(&self.secret_key, self.ttl)
.map_err(|err| e!(PkarrError::Encoding, err))?;
self.pkarr_client.publish(&signed_packet).await?;
trace!(
data = ?info.data,
pkarr_relay = %self.pkarr_client.pkarr_relay_url,
"Published endpoint info to pkarr"
);
Ok(())
}
}
#[derive(Debug)]
pub struct PkarrResolverBuilder {
pkarr_relay: Url,
#[cfg(not(wasm_browser))]
dns_resolver: Option<DnsResolver>,
}
impl PkarrResolverBuilder {
#[cfg(not(wasm_browser))]
pub fn dns_resolver(mut self, dns_resolver: DnsResolver) -> Self {
self.dns_resolver = Some(dns_resolver);
self
}
pub fn build(self, tls_config: rustls::ClientConfig) -> PkarrResolver {
#[cfg(wasm_browser)]
let pkarr_client = PkarrRelayClient::new(self.pkarr_relay, tls_config);
#[cfg(not(wasm_browser))]
let pkarr_client = PkarrRelayClient::builder(self.pkarr_relay, tls_config)
.set_dns_resolver(self.dns_resolver)
.build();
PkarrResolver { pkarr_client }
}
}
impl AddressLookupBuilder for PkarrResolverBuilder {
fn into_address_lookup(
mut self,
endpoint: &Endpoint,
) -> Result<impl AddressLookup, AddressLookupBuilderError> {
#[cfg(not(wasm_browser))]
if self.dns_resolver.is_none() {
self.dns_resolver = Some(endpoint.dns_resolver()?.clone());
}
let tls_config = endpoint.tls_config().clone();
Ok(self.build(tls_config))
}
}
#[derive(derive_more::Debug, Clone)]
pub struct PkarrResolver {
pkarr_client: PkarrRelayClient,
}
impl PkarrResolver {
pub fn builder(pkarr_relay: Url) -> PkarrResolverBuilder {
PkarrResolverBuilder {
pkarr_relay,
#[cfg(not(wasm_browser))]
dns_resolver: None,
}
}
pub fn n0_dns() -> PkarrResolverBuilder {
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::builder(pkarr_relay)
}
}
impl AddressLookup for PkarrResolver {
fn resolve(
&self,
endpoint_id: EndpointId,
) -> Option<BoxStream<Result<AddressLookupItem, AddressLookupError>>> {
let pkarr_client = self.pkarr_client.clone();
let fut = async move {
let signed_packet = pkarr_client.resolve(endpoint_id).await?;
let info = EndpointInfo::from_pkarr_signed_packet(&signed_packet)
.map_err(|err| AddressLookupError::from_err_any("pkarr", err))?;
let item = AddressLookupItem::new(info, "pkarr", None);
Ok(item)
};
let stream = n0_future::stream::once_future(fut);
Some(Box::pin(stream))
}
}
#[derive(Debug, Clone)]
pub struct PkarrRelayClient {
http_client: reqwest::Client,
pkarr_relay_url: Url,
}
#[derive(Debug, Clone)]
pub struct PkarrRelayClientBuilder {
pkarr_relay_url: Url,
#[cfg(not(wasm_browser))]
dns_relay_resolver: Option<DnsResolver>,
tls_config: rustls::ClientConfig,
}
impl PkarrRelayClientBuilder {
fn new(pkarr_relay_url: Url, tls_config: rustls::ClientConfig) -> Self {
Self {
pkarr_relay_url,
#[cfg(not(wasm_browser))]
dns_relay_resolver: None,
tls_config,
}
}
#[cfg(not(wasm_browser))]
pub fn set_dns_resolver(mut self, dns_resolver: Option<DnsResolver>) -> Self {
self.dns_relay_resolver = dns_resolver;
self
}
pub fn build(self) -> PkarrRelayClient {
let mut http_client = reqwest_client_builder(Some(self.tls_config));
#[cfg(not(wasm_browser))]
if let Some(dns_resolver) = self.dns_relay_resolver {
http_client = http_client.dns_resolver(Arc::new(dns_resolver));
};
let http_client = http_client
.build()
.expect("failed to create request client");
PkarrRelayClient {
http_client,
pkarr_relay_url: self.pkarr_relay_url,
}
}
}
impl PkarrRelayClient {
pub fn new(pkarr_relay_url: Url, tls_config: rustls::ClientConfig) -> Self {
Self {
http_client: reqwest_client_builder(Some(tls_config))
.build()
.expect("failed to create request client"),
pkarr_relay_url,
}
}
pub fn builder(
pkarr_relay_url: Url,
tls_config: rustls::ClientConfig,
) -> PkarrRelayClientBuilder {
PkarrRelayClientBuilder::new(pkarr_relay_url, tls_config)
}
pub async fn resolve(
&self,
endpoint_id: EndpointId,
) -> Result<SignedPacket, AddressLookupError> {
let mut url = self.pkarr_relay_url.clone();
url.path_segments_mut()
.map_err(|_| {
e!(PkarrError::InvalidRelayUrl {
url: self.pkarr_relay_url.clone().into()
})
})?
.push(&endpoint_id.to_z32());
let response = self
.http_client
.get(url)
.send()
.await
.map_err(|err| e!(PkarrError::HttpSend, err))?;
if !response.status().is_success() {
return Err(e!(PkarrError::HttpRequest {
status: response.status()
})
.into());
}
let payload = response
.bytes()
.await
.map_err(|source| e!(PkarrError::HttpPayload { source }))?;
let packet = SignedPacket::from_relay_payload(&endpoint_id, &payload)
.map_err(|err| e!(PkarrError::Verify, err))?;
Ok(packet)
}
pub async fn publish(&self, signed_packet: &SignedPacket) -> Result<(), PkarrError> {
let mut url = self.pkarr_relay_url.clone();
url.path_segments_mut()
.map_err(|_| {
e!(PkarrError::InvalidRelayUrl {
url: self.pkarr_relay_url.clone().into()
})
})?
.push(&signed_packet.public_key().to_z32());
let response = self
.http_client
.put(url)
.body(signed_packet.to_relay_payload())
.send()
.await
.map_err(|source| e!(PkarrError::HttpSend { source }))?;
if !response.status().is_success() {
return Err(e!(PkarrError::HttpRequest {
status: response.status()
}));
}
Ok(())
}
}