Added DHT peer resolver, fixed MTU

This commit is contained in:
ab
2026-09-22 15:44:33 +03:00
parent 72eaf9a228
commit 9698f21d55
27 changed files with 2041 additions and 161 deletions
+117
View File
@@ -0,0 +1,117 @@
//! Discovery is a cancellable bootstrap job, never the network's event loop.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use iroh::{EndpointAddr, EndpointId};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use std::time::Duration;
use tsunagi::config::DiscoveryPolicy;
use tsunagi::discovery::{BoxFuture, Candidate, NetworkDiscovery, SharedMemoryDiscovery};
use tsunagi::identity::DiscoveryKey;
use tsunagi::testing::{local_config, network, wait_for_peers, wait_until};
use tsunagi::{Agent, Result};
#[derive(Debug, Default)]
struct Counted {
table: SharedMemoryDiscovery,
reads: AtomicUsize,
writes: AtomicUsize,
}
impl NetworkDiscovery for Counted {
fn name(&self) -> &str {
"counted"
}
fn publish<'a>(&'a self, key: DiscoveryKey, addr: EndpointAddr) -> BoxFuture<'a, Result<()>> {
self.writes.fetch_add(1, Ordering::SeqCst);
self.table.publish(key, addr)
}
fn unpublish<'a>(&'a self, key: DiscoveryKey, id: EndpointId) -> BoxFuture<'a, Result<()>> {
self.table.unpublish(key, id)
}
fn resolve<'a>(&'a self, key: DiscoveryKey) -> BoxFuture<'a, Result<Vec<Candidate>>> {
self.reads.fetch_add(1, Ordering::SeqCst);
self.table.resolve(key)
}
}
fn policy() -> DiscoveryPolicy {
DiscoveryPolicy {
publish_interval: Duration::from_millis(100),
lookup_interval: Duration::from_millis(50),
max_lookup_interval: Duration::from_millis(100),
reconnect_delay: Duration::from_millis(300),
request_timeout: Duration::from_secs(2),
}
}
#[tokio::test]
async fn connected_peers_only_publish_and_isolated_peers_resume_search() {
let discovery = Arc::new(Counted::default());
let first_dir = tempfile::tempdir().unwrap();
let second_dir = tempfile::tempdir().unwrap();
let config = |path: &std::path::Path| {
local_config(path)
.with_discovery(discovery.clone())
.with_discovery_policy(policy())
};
let first = Agent::spawn(config(first_dir.path())).await.unwrap();
let second = Agent::spawn(config(second_dir.path())).await.unwrap();
let (name, secret) = network("discovery-lifecycle");
let id = first.join_network(&name, &secret).await.unwrap();
second.join_network(&name, &secret).await.unwrap();
wait_for_peers(&first, id, 1).await;
wait_for_peers(&second, id, 1).await;
let reads = discovery.reads.load(Ordering::SeqCst);
let writes = discovery.writes.load(Ordering::SeqCst);
wait_until("publications continue while connected", || async {
(discovery.writes.load(Ordering::SeqCst) >= writes + 6).then_some(())
})
.await;
assert_eq!(discovery.reads.load(Ordering::SeqCst), reads);
second.shutdown().await;
wait_for_peers(&first, id, 0).await;
wait_until("isolation resumes discovery", || async {
(discovery.reads.load(Ordering::SeqCst) > reads).then_some(())
})
.await;
first.shutdown().await;
}
#[derive(Debug)]
struct Stalled;
impl NetworkDiscovery for Stalled {
fn name(&self) -> &str {
"stalled"
}
fn publish<'a>(&'a self, _: DiscoveryKey, _: EndpointAddr) -> BoxFuture<'a, Result<()>> {
Box::pin(std::future::pending())
}
fn unpublish<'a>(&'a self, _: DiscoveryKey, _: EndpointId) -> BoxFuture<'a, Result<()>> {
Box::pin(std::future::pending())
}
fn resolve<'a>(&'a self, _: DiscoveryKey) -> BoxFuture<'a, Result<Vec<Candidate>>> {
Box::pin(std::future::pending())
}
}
#[tokio::test]
async fn stalled_discovery_does_not_block_status_or_shutdown() {
let dir = tempfile::tempdir().unwrap();
let agent = Agent::spawn(local_config(dir.path()).with_discovery(Arc::new(Stalled)))
.await
.unwrap();
let (name, secret) = network("stalled-discovery");
let id = agent.join_network(&name, &secret).await.unwrap();
// A recheck schedules work; it must not await the unreachable backend.
agent.recheck_network(id).await.unwrap();
tokio::time::timeout(Duration::from_secs(2), agent.network_status(id))
.await
.unwrap()
.unwrap();
tokio::time::timeout(Duration::from_secs(2), agent.shutdown())
.await
.unwrap();
}
+92
View File
@@ -0,0 +1,92 @@
//! Local Mainline Testnet, real iroh authentication, and durable agent state.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::time::Duration;
use tsunagi::testing::{local_config, network, wait_for_peers};
use tsunagi::{Agent, config::DiscoveryPolicy, discovery::MainlineDiscovery};
#[tokio::test]
#[ignore = "uses public Mainline DHT and iroh relay services"]
async fn public_dht_finds_and_authenticates_two_agents() {
tsunagi::testing::init_tracing();
let a_dir = tempfile::tempdir().unwrap();
let b_dir = tempfile::tempdir().unwrap();
let config = |path: &std::path::Path| {
tsunagi::config::AgentConfig::new(tsunagi::config::StoragePaths::under(path))
.with_transport(tsunagi::config::TransportPolicy::N0Defaults)
.with_dht(MainlineDiscovery::default())
};
let a = Agent::spawn(config(a_dir.path())).await.unwrap();
let b = Agent::spawn(config(b_dir.path())).await.unwrap();
let (name, secret) = network("mainline-public-smoke");
let id = a.join_network(&name, &secret).await.unwrap();
b.join_network(&name, &secret).await.unwrap();
let found = tokio::time::timeout(Duration::from_secs(180), async {
loop {
if !a.network_status(id).await.unwrap().peers.is_empty()
&& !b.network_status(id).await.unwrap().peers.is_empty()
{
break;
}
tokio::time::sleep(Duration::from_millis(250)).await;
}
})
.await;
a.shutdown().await;
b.shutdown().await;
assert!(
found.is_ok(),
"public DHT/relay discovery did not connect within three minutes"
);
}
#[tokio::test]
async fn two_agents_restore_after_all_dht_records_and_addresses_are_gone() {
let first_dir = tempfile::tempdir().unwrap();
let second_dir = tempfile::tempdir().unwrap();
let (name, secret) = network("dht-restart");
let mut identities = None;
for _ in 0..2 {
if identities.is_some() {
for path in [first_dir.path(), second_dir.path()] {
let cache = tsunagi::config::StoragePaths::under(path).cache_dir;
if cache.exists() {
std::fs::remove_dir_all(cache).unwrap();
}
}
}
// A fresh Testnet means no old rendezvous record survives. The second
// pass restores the networks from SQLite without another join command.
let net = mainline::Testnet::builder(5).build().unwrap();
let config = |path: &std::path::Path| {
local_config(path)
.with_dht(MainlineDiscovery::local_testnet(&net.bootstrap).unwrap())
.with_discovery_policy(DiscoveryPolicy {
lookup_interval: Duration::from_millis(100),
max_lookup_interval: Duration::from_millis(300),
..Default::default()
})
};
let (a, b) = tokio::join!(
Agent::spawn(config(first_dir.path())),
Agent::spawn(config(second_dir.path()))
);
let a = a.unwrap();
let b = b.unwrap();
let id = tsunagi::identity::NetworkKeys::derive(&name, &secret).network_id();
if let Some(old) = identities {
assert_eq!((a.endpoint_id(), b.endpoint_id()), old);
} else {
identities = Some((a.endpoint_id(), b.endpoint_id()));
let (ra, rb) = tokio::join!(
a.join_network(&name, &secret),
b.join_network(&name, &secret)
);
assert_eq!(ra.unwrap(), id);
assert_eq!(rb.unwrap(), id);
}
wait_for_peers(&a, id, 1).await;
wait_for_peers(&b, id, 1).await;
a.shutdown().await;
b.shutdown().await;
}
}