#![deny(missing_docs, rustdoc::broken_intra_doc_links)]
pub mod config;
pub mod dns;
pub mod http;
pub mod metrics;
pub mod server;
pub mod state;
mod store;
mod util;
pub use store::ZoneStore;
#[cfg(test)]
mod tests {
use std::{
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
time::Duration,
};
use iroh::{
RelayUrl, SecretKey, address_lookup::PkarrRelayClient, dns::DnsResolver,
endpoint_info::EndpointInfo,
};
use n0_error::{Result, StdResultExt};
use n0_tracing_test::traced_test;
use pkarr::{SignedPacket, Timestamp};
use rand::{CryptoRng, SeedableRng};
use crate::{
ZoneStore,
config::BootstrapOption,
server::Server,
store::{PacketSource, ZoneStoreOptions},
util::PublicKeyBytes,
};
const DNS_TIMEOUT: Duration = Duration::from_secs(2);
#[tokio::test]
#[traced_test]
async fn pkarr_publish_dns_resolve() -> Result {
let dir = tempfile::tempdir()?;
let server = Server::spawn_for_tests(dir.path()).await?;
let pkarr_relay_url = {
let mut url = server.http_url().expect("http is bound");
url.set_path("/pkarr");
url
};
let signed_packet = {
use pkarr::dns;
let keypair = pkarr::Keypair::random();
let mut packet = dns::Packet::new_reply(0);
packet.answers.push(dns::ResourceRecord::new(
dns::Name::new("").anyerr()?,
dns::CLASS::IN,
30,
dns::rdata::RData::TXT("hi0".try_into().unwrap()),
));
packet.answers.push(dns::ResourceRecord::new(
dns::Name::new("_hello").anyerr()?,
dns::CLASS::IN,
30,
dns::rdata::RData::TXT("hi1".try_into().unwrap()),
));
packet.answers.push(dns::ResourceRecord::new(
dns::Name::new("_hello.world").anyerr()?,
dns::CLASS::IN,
30,
dns::rdata::RData::TXT("hi2".try_into().unwrap()),
));
packet.answers.push(dns::ResourceRecord::new(
dns::Name::new("multiple").anyerr()?,
dns::CLASS::IN,
30,
dns::rdata::RData::TXT("hi3".try_into().unwrap()),
));
packet.answers.push(dns::ResourceRecord::new(
dns::Name::new("multiple").anyerr()?,
dns::CLASS::IN,
30,
dns::rdata::RData::TXT("hi4".try_into().unwrap()),
));
packet.answers.push(dns::ResourceRecord::new(
dns::Name::new("").anyerr()?,
dns::CLASS::IN,
30,
dns::rdata::RData::A(Ipv4Addr::LOCALHOST.into()),
));
packet.answers.push(dns::ResourceRecord::new(
dns::Name::new("foo.bar.baz").anyerr()?,
dns::CLASS::IN,
30,
dns::rdata::RData::AAAA(Ipv6Addr::LOCALHOST.into()),
));
SignedPacket::new(&keypair, &packet.answers, Timestamp::now()).anyerr()?
};
let pkarr_client = pkarr::Client::builder()
.no_default_network()
.relays(&[pkarr_relay_url])
.anyerr()?
.build()
.anyerr()?;
pkarr_client.publish(&signed_packet, None).await.anyerr()?;
use hickory_server::proto::rr::Name;
let pubkey = signed_packet.public_key().to_z32();
let resolver = test_resolver(server.dns_addr());
let name = Name::from_utf8(format!("{pubkey}.")).anyerr()?;
let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
assert_eq!(records, vec!["hi0".to_string()]);
let name = Name::from_utf8(format!("_hello.{pubkey}.")).anyerr()?;
let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
assert_eq!(records, vec!["hi1".to_string()]);
let name = Name::from_utf8(format!("_hello.world.{pubkey}.")).anyerr()?;
let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
assert_eq!(records, vec!["hi2".to_string()]);
let name = Name::from_utf8(format!("multiple.{pubkey}.")).anyerr()?;
let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
assert_eq!(records, vec!["hi3".to_string(), "hi4".to_string()]);
let name = Name::from_utf8(format!("{pubkey}.")).anyerr()?;
let res = resolver.lookup_ipv4(name, DNS_TIMEOUT).await?;
let records = res.collect::<Vec<_>>();
assert_eq!(records, vec![Ipv4Addr::LOCALHOST]);
let name = Name::from_utf8(format!("foo.bar.baz.{pubkey}.")).anyerr()?;
let res = resolver.lookup_ipv6(name, DNS_TIMEOUT).await?;
let records = res.collect::<Vec<_>>();
assert_eq!(records, vec![Ipv6Addr::LOCALHOST]);
server.shutdown().await?;
Ok(())
}
#[tokio::test]
#[traced_test]
async fn integration_smoke() -> Result {
let dir = tempfile::tempdir()?;
let server = Server::spawn_for_tests(dir.path()).await?;
let pkarr_relay = {
let mut url = server.http_url().expect("http is bound");
url.set_path("/pkarr");
url
};
let origin = "irohdns.example.";
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
let secret_key = SecretKey::generate(&mut rng);
let endpoint_id = secret_key.public();
let pkarr = PkarrRelayClient::new(pkarr_relay);
let relay_url: RelayUrl = "https://relay.example.".parse()?;
let endpoint_info = EndpointInfo::new(endpoint_id).with_relay_url(Some(relay_url.clone()));
let signed_packet = endpoint_info.to_pkarr_signed_packet(&secret_key, 30)?;
pkarr.publish(&signed_packet).await?;
let resolver = test_resolver(server.dns_addr());
let res = resolver.lookup_endpoint_by_id(&endpoint_id, origin).await?;
assert_eq!(res.endpoint_id, endpoint_id);
assert_eq!(res.relay_urls().next(), Some(&relay_url));
server.shutdown().await?;
Ok(())
}
#[tokio::test]
#[traced_test]
async fn store_eviction() -> Result {
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
let options = ZoneStoreOptions {
eviction: Duration::from_millis(100),
eviction_interval: Duration::from_millis(100),
max_batch_time: Duration::from_millis(100),
..Default::default()
};
let store = ZoneStore::in_memory(options, Default::default())?;
let signed_packet = random_signed_packet(&mut rng)?;
let key = PublicKeyBytes::from_signed_packet(&signed_packet);
store
.insert(signed_packet, PacketSource::PkarrPublish)
.await?;
tokio::time::sleep(Duration::from_secs(1)).await;
for _ in 0..10 {
let entry = store.get_signed_packet(&key).await?;
if entry.is_none() {
return Ok(());
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
panic!("store did not evict packet");
}
#[tokio::test]
#[traced_test]
#[ignore = "flaky"]
async fn integration_mainline() -> Result {
let dir = tempfile::tempdir()?;
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
let testnet = pkarr::mainline::Testnet::new_async(5).await.anyerr()?;
let bootstrap = testnet.bootstrap.clone();
let server = Server::spawn_for_tests_with_options(
dir.path(),
Some(BootstrapOption::Custom(bootstrap)),
None,
None,
)
.await?;
let origin = "irohdns.example.";
let secret_key = SecretKey::generate(&mut rng);
let endpoint_id = secret_key.public();
let relay_url: RelayUrl = "https://relay.example.".parse()?;
let endpoint_info = EndpointInfo::new(endpoint_id).with_relay_url(Some(relay_url.clone()));
let signed_packet = endpoint_info.to_pkarr_signed_packet(&secret_key, 30)?;
let pkarr = pkarr::Client::builder()
.no_default_network()
.dht(|builder| builder.bootstrap(&testnet.bootstrap))
.build()
.anyerr()?;
pkarr.publish(&signed_packet, None).await.anyerr()?;
let resolver = test_resolver(server.dns_addr());
let res = resolver.lookup_endpoint_by_id(&endpoint_id, origin).await?;
assert_eq!(res.endpoint_id, endpoint_id);
assert_eq!(res.relay_urls().next(), Some(&relay_url));
server.shutdown().await?;
Ok(())
}
fn test_resolver(nameserver: SocketAddr) -> DnsResolver {
DnsResolver::with_nameserver(nameserver)
}
fn random_signed_packet<R: CryptoRng + ?Sized>(rng: &mut R) -> Result<SignedPacket> {
let secret_key = SecretKey::generate(rng);
let endpoint_id = secret_key.public();
let relay_url: RelayUrl = "https://relay.example.".parse()?;
let endpoint_info = EndpointInfo::new(endpoint_id).with_relay_url(Some(relay_url.clone()));
let packet = endpoint_info.to_pkarr_signed_packet(&secret_key, 30)?;
Ok(packet)
}
}