use std::sync::Arc;
use iroh_base::{NodeAddr, NodeId};
use n0_future::{
Stream, TryStreamExt,
boxed::BoxStream,
stream::StreamExt,
task::{self, AbortOnDropHandle},
time::{self, Duration},
};
use nested_enum_utils::common_fields;
use snafu::{IntoError, Snafu, ensure};
use tokio::sync::oneshot;
use tracing::{Instrument, debug, error_span, warn};
#[cfg(not(wasm_browser))]
use crate::dns::DnsResolver;
pub use crate::node_info::{NodeData, NodeInfo, ParseError, UserData};
use crate::{Endpoint, SecretKey};
#[cfg(not(wasm_browser))]
pub mod dns;
#[cfg(feature = "discovery-local-network")]
pub mod mdns;
pub mod pkarr;
pub mod static_provider;
pub trait IntoDiscovery: Send + Sync + std::fmt::Debug + 'static {
fn into_discovery(
self,
context: &DiscoveryContext,
) -> Result<impl Discovery, IntoDiscoveryError>;
}
impl<T: Discovery> IntoDiscovery for T {
fn into_discovery(
self,
_context: &DiscoveryContext,
) -> Result<impl Discovery, IntoDiscoveryError> {
Ok(self)
}
}
pub(crate) trait DynIntoDiscovery: Send + Sync + std::fmt::Debug + 'static {
fn into_discovery(
self: Box<Self>,
context: &DiscoveryContext,
) -> Result<Box<dyn Discovery>, IntoDiscoveryError>;
}
impl<T: IntoDiscovery> DynIntoDiscovery for T {
fn into_discovery(
self: Box<Self>,
context: &DiscoveryContext,
) -> Result<Box<dyn Discovery>, IntoDiscoveryError> {
let disco: Box<dyn Discovery> = Box::new(IntoDiscovery::into_discovery(*self, context)?);
Ok(disco)
}
}
#[derive(Debug)]
pub struct DiscoveryContext<'a> {
#[cfg(not(wasm_browser))]
pub(crate) dns_resolver: &'a DnsResolver,
pub(crate) secret_key: &'a SecretKey,
}
impl DiscoveryContext<'_> {
pub fn node_id(&self) -> NodeId {
self.secret_key.public()
}
pub fn secret_key(&self) -> &SecretKey {
self.secret_key
}
#[cfg(not(wasm_browser))]
pub fn dns_resolver(&self) -> &DnsResolver {
self.dns_resolver
}
}
#[common_fields({
backtrace: Option<snafu::Backtrace>,
#[snafu(implicit)]
span_trace: n0_snafu::SpanTrace,
})]
#[allow(missing_docs)]
#[derive(Debug, Snafu)]
#[non_exhaustive]
#[snafu(module)]
pub enum IntoDiscoveryError {
#[snafu(display("Service '{provenance}' error"))]
User {
provenance: &'static str,
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},
}
impl IntoDiscoveryError {
pub fn from_err<T: std::error::Error + Send + Sync + 'static>(
provenance: &'static str,
source: T,
) -> Self {
into_discovery_error::UserSnafu { provenance }.into_error(Box::new(source))
}
pub fn from_err_box(
provenance: &'static str,
source: Box<dyn std::error::Error + Send + Sync + 'static>,
) -> Self {
into_discovery_error::UserSnafu { provenance }.into_error(source)
}
}
#[common_fields({
backtrace: Option<snafu::Backtrace>,
#[snafu(implicit)]
span_trace: n0_snafu::SpanTrace,
})]
#[allow(missing_docs)]
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum DiscoveryError {
#[snafu(display("No discovery service configured"))]
NoServiceConfigured {},
#[snafu(display("Discovery produced no results for {}", node_id.fmt_short()))]
NoResults { node_id: NodeId },
#[snafu(display("Service '{provenance}' error"))]
User {
provenance: &'static str,
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},
}
impl DiscoveryError {
pub fn from_err<T: std::error::Error + Send + Sync + 'static>(
provenance: &'static str,
source: T,
) -> Self {
UserSnafu { provenance }.into_error(Box::new(source))
}
pub fn from_err_box(
provenance: &'static str,
source: Box<dyn std::error::Error + Send + Sync + 'static>,
) -> Self {
UserSnafu { provenance }.into_error(source)
}
}
pub trait Discovery: std::fmt::Debug + Send + Sync + 'static {
fn publish(&self, _data: &NodeData) {}
fn resolve(
&self,
_node_id: NodeId,
) -> Option<BoxStream<Result<DiscoveryItem, DiscoveryError>>> {
None
}
fn subscribe(&self) -> Option<BoxStream<DiscoveryEvent>> {
None
}
}
impl<T: Discovery> Discovery for Arc<T> {}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum DiscoveryEvent {
Discovered(DiscoveryItem),
Expired(NodeId),
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct DiscoveryItem {
node_info: NodeInfo,
provenance: &'static str,
last_updated: Option<u64>,
}
impl DiscoveryItem {
pub fn new(node_info: NodeInfo, provenance: &'static str, last_updated: Option<u64>) -> Self {
Self {
node_info,
provenance,
last_updated,
}
}
pub fn node_id(&self) -> NodeId {
self.node_info.node_id
}
pub fn node_info(&self) -> &NodeInfo {
&self.node_info
}
pub fn provenance(&self) -> &'static str {
self.provenance
}
pub fn last_updated(&self) -> Option<u64> {
self.last_updated
}
pub fn to_node_addr(&self) -> NodeAddr {
self.node_info.to_node_addr()
}
pub fn into_node_addr(self) -> NodeAddr {
self.node_info.into_node_addr()
}
pub fn user_data(&self) -> Option<UserData> {
self.node_info().data.user_data().cloned()
}
}
impl std::ops::Deref for DiscoveryItem {
type Target = NodeData;
fn deref(&self) -> &Self::Target {
&self.node_info.data
}
}
impl From<DiscoveryItem> for NodeInfo {
fn from(item: DiscoveryItem) -> Self {
item.node_info
}
}
#[derive(Debug, Default)]
pub struct ConcurrentDiscovery {
services: Vec<Box<dyn Discovery>>,
}
impl ConcurrentDiscovery {
pub fn empty() -> Self {
Self::default()
}
pub fn from_services(services: Vec<Box<dyn Discovery>>) -> Self {
Self { services }
}
pub fn add(&mut self, service: impl Discovery + 'static) {
self.services.push(Box::new(service));
}
}
impl<T> From<T> for ConcurrentDiscovery
where
T: IntoIterator<Item = Box<dyn Discovery>>,
{
fn from(iter: T) -> Self {
let services = iter.into_iter().collect::<Vec<_>>();
Self { services }
}
}
impl Discovery for ConcurrentDiscovery {
fn publish(&self, data: &NodeData) {
for service in &self.services {
service.publish(data);
}
}
fn resolve(&self, node_id: NodeId) -> Option<BoxStream<Result<DiscoveryItem, DiscoveryError>>> {
let streams = self
.services
.iter()
.filter_map(|service| service.resolve(node_id));
let streams = n0_future::MergeBounded::from_iter(streams);
Some(Box::pin(streams))
}
fn subscribe(&self) -> Option<BoxStream<DiscoveryEvent>> {
let mut streams = vec![];
for service in self.services.iter() {
if let Some(stream) = service.subscribe() {
streams.push(stream)
}
}
let streams = n0_future::MergeBounded::from_iter(streams);
Some(Box::pin(streams))
}
}
const MAX_AGE: Duration = Duration::from_secs(10);
pub(super) struct DiscoveryTask {
on_first_rx: oneshot::Receiver<Result<(), DiscoveryError>>,
_task: AbortOnDropHandle<()>,
}
impl DiscoveryTask {
pub(super) fn start(ep: Endpoint, node_id: NodeId) -> Result<Self, DiscoveryError> {
ensure!(ep.discovery().is_some(), NoServiceConfiguredSnafu);
let (on_first_tx, on_first_rx) = oneshot::channel();
let me = ep.node_id();
let task = task::spawn(
async move { Self::run(ep, node_id, on_first_tx).await }.instrument(
error_span!("discovery", me = %me.fmt_short(), node = %node_id.fmt_short()),
),
);
Ok(Self {
_task: AbortOnDropHandle::new(task),
on_first_rx,
})
}
pub(super) fn maybe_start_after_delay(
ep: &Endpoint,
node_id: NodeId,
delay: Option<Duration>,
) -> Result<Option<Self>, DiscoveryError> {
if !Self::needs_discovery(ep, node_id) {
return Ok(None);
}
ensure!(ep.discovery().is_some(), NoServiceConfiguredSnafu);
let (on_first_tx, on_first_rx) = oneshot::channel();
let ep = ep.clone();
let me = ep.node_id();
let task = task::spawn(
async move {
if let Some(delay) = delay {
time::sleep(delay).await;
if !Self::needs_discovery(&ep, node_id) {
debug!("no discovery needed, abort");
on_first_tx.send(Ok(())).ok();
return;
}
}
Self::run(ep, node_id, on_first_tx).await
}
.instrument(
error_span!("discovery", me = %me.fmt_short(), node = %node_id.fmt_short()),
),
);
Ok(Some(Self {
_task: AbortOnDropHandle::new(task),
on_first_rx,
}))
}
pub(super) async fn first_arrived(&mut self) -> Result<(), DiscoveryError> {
let fut = &mut self.on_first_rx;
fut.await.expect("sender dropped")?;
Ok(())
}
fn create_stream(
ep: &Endpoint,
node_id: NodeId,
) -> Result<BoxStream<Result<DiscoveryItem, DiscoveryError>>, DiscoveryError> {
let discovery = ep.discovery().ok_or(NoServiceConfiguredSnafu.build())?;
let stream = discovery
.resolve(node_id)
.ok_or(NoResultsSnafu { node_id }.build())?;
Ok(stream)
}
fn needs_discovery(ep: &Endpoint, node_id: NodeId) -> bool {
match ep.remote_info(node_id) {
None => true,
Some(info) => {
match (
info.last_received(),
info.relay_url.as_ref().and_then(|r| r.last_alive),
) {
(None, None) => true,
(Some(elapsed), Some(elapsed_relay)) => {
elapsed > MAX_AGE && elapsed_relay > MAX_AGE
}
(Some(elapsed), _) | (_, Some(elapsed)) => elapsed > MAX_AGE,
}
}
}
}
async fn run(
ep: Endpoint,
node_id: NodeId,
on_first_tx: oneshot::Sender<Result<(), DiscoveryError>>,
) {
let mut stream = match Self::create_stream(&ep, node_id) {
Ok(stream) => stream,
Err(err) => {
on_first_tx.send(Err(err)).ok();
return;
}
};
let mut on_first_tx = Some(on_first_tx);
debug!("starting");
loop {
match stream.next().await {
Some(Ok(r)) => {
let provenance = r.provenance;
let node_addr = r.to_node_addr();
if node_addr.is_empty() {
debug!(%provenance, "empty address found");
continue;
}
debug!(%provenance, addr = ?node_addr, "new address found");
ep.add_node_addr_with_source(node_addr, provenance).ok();
if let Some(tx) = on_first_tx.take() {
tx.send(Ok(())).ok();
}
ep.discovery_subscribers()
.send(DiscoveryEvent::Discovered(r));
}
Some(Err(err)) => {
warn!(?err, "discovery service produced error");
break;
}
None => break,
}
}
if let Some(tx) = on_first_tx.take() {
tx.send(Err(NoResultsSnafu { node_id }.build())).ok();
}
}
}
#[derive(Debug, Snafu)]
#[snafu(display("channel lagged by {val}"))]
pub struct Lagged {
pub val: u64,
}
#[derive(Clone, Debug)]
pub(super) struct DiscoverySubscribers {
inner: tokio::sync::broadcast::Sender<DiscoveryEvent>,
}
impl DiscoverySubscribers {
pub(crate) fn new() -> Self {
const CAPACITY: usize = 128;
Self {
inner: tokio::sync::broadcast::Sender::new(CAPACITY),
}
}
pub(crate) fn subscribe(&self) -> impl Stream<Item = Result<DiscoveryEvent, Lagged>> + use<> {
use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError};
let recv = self.inner.subscribe();
BroadcastStream::new(recv).map_err(|BroadcastStreamRecvError::Lagged(n)| Lagged { val: n })
}
pub(crate) fn send(&self, item: DiscoveryEvent) {
self.inner.send(item).ok();
}
}
#[cfg(test)]
mod tests {
use std::{
collections::{BTreeSet, HashMap},
net::SocketAddr,
sync::{Arc, Mutex},
time::SystemTime,
};
use iroh_base::{NodeAddr, SecretKey};
use n0_snafu::{Error, Result, ResultExt};
use n0_watcher::Watcher as _;
use quinn::{IdleTimeout, TransportConfig};
use rand::Rng;
use tokio_util::task::AbortOnDropHandle;
use tracing_test::traced_test;
use super::*;
use crate::{Endpoint, RelayMode, endpoint::ConnectOptions};
type InfoStore = HashMap<NodeId, (NodeData, u64)>;
#[derive(Debug, Clone)]
struct TestDiscoveryShared {
nodes: Arc<Mutex<InfoStore>>,
watchers: tokio::sync::broadcast::Sender<DiscoveryEvent>,
}
impl Default for TestDiscoveryShared {
fn default() -> Self {
Self {
nodes: Default::default(),
watchers: tokio::sync::broadcast::Sender::new(1024),
}
}
}
impl TestDiscoveryShared {
pub fn create_discovery(&self, node_id: NodeId) -> TestDiscovery {
TestDiscovery {
node_id,
shared: self.clone(),
publish: true,
resolve_wrong: false,
delay: Duration::from_millis(200),
}
}
pub fn create_lying_discovery(&self, node_id: NodeId) -> TestDiscovery {
TestDiscovery {
node_id,
shared: self.clone(),
publish: false,
resolve_wrong: true,
delay: Duration::from_millis(100),
}
}
pub fn send_passive(&self, item: DiscoveryEvent) {
self.watchers.send(item).ok();
}
}
#[derive(Debug)]
struct TestDiscovery {
node_id: NodeId,
shared: TestDiscoveryShared,
publish: bool,
resolve_wrong: bool,
delay: Duration,
}
impl Discovery for TestDiscovery {
fn publish(&self, data: &NodeData) {
if !self.publish {
return;
}
let now = system_time_now();
self.shared
.nodes
.lock()
.unwrap()
.insert(self.node_id, (data.clone(), now));
}
fn resolve(
&self,
node_id: NodeId,
) -> Option<BoxStream<Result<DiscoveryItem, DiscoveryError>>> {
let addr_info = if self.resolve_wrong {
let ts = system_time_now() - 100_000;
let port: u16 = rand::thread_rng().gen_range(10_000..20_000);
let addr: SocketAddr = format!("240.0.0.1:{port}").parse().unwrap();
let data = NodeData::new(None, BTreeSet::from([addr]));
Some((data, ts))
} else {
self.shared.nodes.lock().unwrap().get(&node_id).cloned()
};
let stream = match addr_info {
Some((data, ts)) => {
let item = DiscoveryItem::new(
NodeInfo::from_parts(node_id, data),
"test-disco",
Some(ts),
);
let delay = self.delay;
let fut = async move {
time::sleep(delay).await;
tracing::debug!("resolve: {} = {item:?}", node_id.fmt_short());
Ok(item)
};
n0_future::stream::once_future(fut).boxed()
}
None => n0_future::stream::empty().boxed(),
};
Some(stream)
}
fn subscribe(&self) -> Option<BoxStream<DiscoveryEvent>> {
let recv = self.shared.watchers.subscribe();
let stream =
tokio_stream::wrappers::BroadcastStream::new(recv).filter_map(|item| item.ok());
Some(Box::pin(stream))
}
}
#[derive(Debug, Clone)]
struct EmptyDiscovery;
impl Discovery for EmptyDiscovery {
fn publish(&self, _data: &NodeData) {}
fn resolve(
&self,
_node_id: NodeId,
) -> Option<BoxStream<Result<DiscoveryItem, DiscoveryError>>> {
Some(n0_future::stream::empty().boxed())
}
}
const TEST_ALPN: &[u8] = b"n0/iroh/test";
#[tokio::test]
#[traced_test]
async fn endpoint_discovery_simple_shared() -> Result {
let disco_shared = TestDiscoveryShared::default();
let (ep1, _guard1) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco = disco_shared.create_discovery(secret.public());
new_endpoint(secret, disco).await
};
let (ep2, _guard2) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco = disco_shared.create_discovery(secret.public());
new_endpoint(secret, disco).await
};
let ep1_addr = NodeAddr::new(ep1.node_id());
ep1.node_addr().initialized().await;
let _conn = ep2.connect(ep1_addr, TEST_ALPN).await?;
Ok(())
}
#[tokio::test]
#[traced_test]
async fn endpoint_discovery_combined_with_empty() -> Result {
let disco_shared = TestDiscoveryShared::default();
let (ep1, _guard1) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco = disco_shared.create_discovery(secret.public());
new_endpoint(secret, disco).await
};
let (ep2, _guard2) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco1 = EmptyDiscovery;
let disco2 = disco_shared.create_discovery(secret.public());
let mut disco = ConcurrentDiscovery::empty();
disco.add(disco1);
disco.add(disco2);
new_endpoint(secret, disco).await
};
let ep1_addr = NodeAddr::new(ep1.node_id());
ep1.node_addr().initialized().await;
let _conn = ep2
.connect(ep1_addr, TEST_ALPN)
.await
.context("connecting")?;
Ok(())
}
#[tokio::test]
#[traced_test]
async fn endpoint_discovery_combined_with_empty_and_wrong() -> Result {
let disco_shared = TestDiscoveryShared::default();
let (ep1, _guard1) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco = disco_shared.create_discovery(secret.public());
new_endpoint(secret, disco).await
};
let (ep2, _guard2) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco1 = EmptyDiscovery;
let disco2 = disco_shared.create_lying_discovery(secret.public());
let disco3 = disco_shared.create_discovery(secret.public());
let mut disco = ConcurrentDiscovery::empty();
disco.add(disco1);
disco.add(disco2);
disco.add(disco3);
new_endpoint(secret, disco).await
};
ep1.node_addr().initialized().await;
let _conn = ep2.connect(ep1.node_id(), TEST_ALPN).await?;
Ok(())
}
#[tokio::test]
#[traced_test]
async fn endpoint_discovery_combined_wrong_only() -> Result {
let disco_shared = TestDiscoveryShared::default();
let (ep1, _guard1) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco = disco_shared.create_discovery(secret.public());
new_endpoint(secret, disco).await
};
let (ep2, _guard2) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco1 = disco_shared.create_lying_discovery(secret.public());
let disco = ConcurrentDiscovery::from_services(vec![Box::new(disco1)]);
new_endpoint(secret, disco).await
};
ep1.node_addr().initialized().await;
let mut config = TransportConfig::default();
config.keep_alive_interval(Some(Duration::from_secs(1)));
config.max_idle_timeout(Some(IdleTimeout::try_from(Duration::from_secs(3)).unwrap()));
let opts = ConnectOptions::new().with_transport_config(Arc::new(config));
let res = ep2
.connect_with_opts(ep1.node_id(), TEST_ALPN, opts)
.await? .await; assert!(res.is_err());
Ok(())
}
#[tokio::test]
#[traced_test]
async fn endpoint_discovery_with_wrong_existing_addr() -> Result {
let disco_shared = TestDiscoveryShared::default();
let (ep1, _guard1) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco = disco_shared.create_discovery(secret.public());
new_endpoint(secret, disco).await
};
let (ep2, _guard2) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco = disco_shared.create_discovery(secret.public());
new_endpoint(secret, disco).await
};
ep1.node_addr().initialized().await;
let ep1_wrong_addr = NodeAddr {
node_id: ep1.node_id(),
relay_url: None,
direct_addresses: BTreeSet::from(["240.0.0.1:1000".parse().unwrap()]),
};
let _conn = ep2.connect(ep1_wrong_addr, TEST_ALPN).await?;
Ok(())
}
#[tokio::test]
#[traced_test]
async fn endpoint_discovery_watch() -> Result {
let disco_shared = TestDiscoveryShared::default();
let (ep1, _guard1) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco = disco_shared.create_discovery(secret.public());
new_endpoint(secret, disco).await
};
let (ep2, _guard2) = {
let secret = SecretKey::generate(rand::thread_rng());
let disco = disco_shared.create_discovery(secret.public());
new_endpoint(secret, disco).await
};
let mut stream = ep1.discovery_stream();
ep2.node_addr().initialized().await;
let _ = ep1.connect(ep2.node_id(), TEST_ALPN).await?;
let DiscoveryEvent::Discovered(item) =
tokio::time::timeout(Duration::from_secs(1), stream.next())
.await
.expect("timeout")
.expect("stream closed")
.expect("stream lagged")
else {
panic!("Returned unexpected discovery event!");
};
assert_eq!(item.node_id(), ep2.node_id());
assert_eq!(item.provenance(), "test-disco");
let passive_node_id = SecretKey::generate(rand::thread_rng()).public();
let node_info = NodeInfo::new(passive_node_id);
let passive_item = DiscoveryItem::new(node_info, "test-disco-passive", None);
disco_shared.send_passive(DiscoveryEvent::Discovered(passive_item.clone()));
let DiscoveryEvent::Discovered(item) =
tokio::time::timeout(Duration::from_secs(1), stream.next())
.await
.expect("timeout")
.expect("stream closed")
.expect("stream lagged")
else {
panic!("Returned unexpected discovery event!");
};
assert_eq!(item.node_id(), passive_node_id);
assert_eq!(item.provenance(), "test-disco-passive");
Ok(())
}
async fn new_endpoint(
secret: SecretKey,
disco: impl IntoDiscovery + 'static,
) -> (Endpoint, AbortOnDropHandle<Result<()>>) {
let ep = Endpoint::builder()
.secret_key(secret)
.discovery(disco)
.relay_mode(RelayMode::Disabled)
.alpns(vec![TEST_ALPN.to_vec()])
.bind()
.await
.unwrap();
let handle = tokio::spawn({
let ep = ep.clone();
async move {
let mut connections = Vec::new();
while let Some(connecting) = ep.accept().await.and_then(|inc| inc.accept().ok()) {
let conn = connecting.await.context("connecting")?;
connections.push(conn);
}
Ok::<_, Error>(())
}
});
(ep, AbortOnDropHandle::new(handle))
}
fn system_time_now() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("time drift")
.as_micros() as u64
}
}
#[cfg(test)]
mod test_dns_pkarr {
use iroh_base::{NodeAddr, SecretKey};
use iroh_relay::{RelayMap, node_info::UserData};
use n0_future::time::Duration;
use n0_snafu::{Error, Result, ResultExt};
use tokio_util::task::AbortOnDropHandle;
use tracing_test::traced_test;
use crate::{
Endpoint, RelayMode,
discovery::{NodeData, pkarr::PkarrPublisher},
dns::DnsResolver,
node_info::NodeInfo,
test_utils::{
DnsPkarrServer, dns_server::run_dns_server, pkarr_dns_state::State, run_relay_server,
},
};
const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10);
#[tokio::test]
#[traced_test]
async fn dns_resolve() -> Result<()> {
let origin = "testdns.example".to_string();
let state = State::new(origin.clone());
let (nameserver, _dns_drop_guard) = run_dns_server(state.clone())
.await
.context("Running DNS server")?;
let secret_key = SecretKey::generate(rand::thread_rng());
let node_info = NodeInfo::new(secret_key.public())
.with_relay_url(Some("https://relay.example".parse().unwrap()));
let signed_packet = node_info.to_pkarr_signed_packet(&secret_key, 30)?;
state
.upsert(signed_packet)
.context("update and insert signed packet")?;
let resolver = DnsResolver::with_nameserver(nameserver);
let resolved = resolver
.lookup_node_by_id(&node_info.node_id, &origin)
.await?;
assert_eq!(resolved, node_info);
Ok(())
}
#[tokio::test]
#[traced_test]
async fn pkarr_publish_dns_resolve() -> Result<()> {
let origin = "testdns.example".to_string();
let dns_pkarr_server = DnsPkarrServer::run_with_origin(origin.clone())
.await
.context("DnsPkarrServer")?;
let secret_key = SecretKey::generate(rand::thread_rng());
let node_id = secret_key.public();
let relay_url = Some("https://relay.example".parse().unwrap());
let resolver = DnsResolver::with_nameserver(dns_pkarr_server.nameserver);
let publisher =
PkarrPublisher::builder(dns_pkarr_server.pkarr_url.clone()).build(secret_key);
let user_data: UserData = "foobar".parse().unwrap();
let data = NodeData::new(relay_url.clone(), Default::default())
.with_user_data(Some(user_data.clone()));
publisher.update_node_data(&data);
dns_pkarr_server
.on_node(&node_id, PUBLISH_TIMEOUT)
.await
.context("wait for on node update")?;
let resolved = resolver.lookup_node_by_id(&node_id, &origin).await?;
println!("resolved {resolved:?}");
let expected_addr = NodeAddr {
node_id,
relay_url,
direct_addresses: Default::default(),
};
assert_eq!(resolved.to_node_addr(), expected_addr);
assert_eq!(resolved.user_data(), Some(&user_data));
Ok(())
}
const TEST_ALPN: &[u8] = b"TEST";
#[tokio::test]
#[traced_test]
async fn pkarr_publish_dns_discover() -> Result<()> {
let dns_pkarr_server = DnsPkarrServer::run().await.context("DnsPkarrServer run")?;
let (relay_map, _relay_url, _relay_guard) = run_relay_server().await?;
let (ep1, _guard1) = ep_with_discovery(&relay_map, &dns_pkarr_server).await?;
let (ep2, _guard2) = ep_with_discovery(&relay_map, &dns_pkarr_server).await?;
dns_pkarr_server
.on_node(&ep1.node_id(), PUBLISH_TIMEOUT)
.await
.context("wait for on node update")?;
let _conn = ep2.connect(ep1.node_id(), TEST_ALPN).await?;
Ok(())
}
async fn ep_with_discovery(
relay_map: &RelayMap,
dns_pkarr_server: &DnsPkarrServer,
) -> Result<(Endpoint, AbortOnDropHandle<Result<()>>)> {
let secret_key = SecretKey::generate(rand::thread_rng());
let ep = Endpoint::builder()
.relay_mode(RelayMode::Custom(relay_map.clone()))
.insecure_skip_relay_cert_verify(true)
.secret_key(secret_key.clone())
.alpns(vec![TEST_ALPN.to_vec()])
.dns_resolver(dns_pkarr_server.dns_resolver())
.discovery(dns_pkarr_server.discovery(secret_key))
.bind()
.await?;
let handle = tokio::spawn({
let ep = ep.clone();
async move {
while let Some(connecting) = ep.accept().await.and_then(|inc| inc.accept().ok()) {
let _conn = connecting.await.context("connecting")?;
}
Ok::<_, Error>(())
}
});
Ok((ep, AbortOnDropHandle::new(handle)))
}
}