//! A DNS server and pkarr relay

#![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;

// Re-export to be able to construct your own dns-server
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);
            // record at root
            packet.answers.push(dns::ResourceRecord::new(
                dns::Name::new("").anyerr()?,
                dns::CLASS::IN,
                30,
                dns::rdata::RData::TXT("hi0".try_into().unwrap()),
            ));
            // record at level one
            packet.answers.push(dns::ResourceRecord::new(
                dns::Name::new("_hello").anyerr()?,
                dns::CLASS::IN,
                30,
                dns::rdata::RData::TXT("hi1".try_into().unwrap()),
            ));
            // record at level two
            packet.answers.push(dns::ResourceRecord::new(
                dns::Name::new("_hello.world").anyerr()?,
                dns::CLASS::IN,
                30,
                dns::rdata::RData::TXT("hi2".try_into().unwrap()),
            ));
            // multiple records for same name
            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()),
            ));
            // record of type A
            packet.answers.push(dns::ResourceRecord::new(
                dns::Name::new("").anyerr()?,
                dns::CLASS::IN,
                30,
                dns::rdata::RData::A(Ipv4Addr::LOCALHOST.into()),
            ));
            // record of type AAAA
            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());

        // resolve root record
        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()]);

        // resolve level one record
        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()]);

        // resolve level two record
        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()]);

        // resolve multiple records for same name
        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()]);

        // resolve A record
        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]);

        // resolve AAAA record
        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())?;

        // create a signed packet
        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);

        // run a mainline testnet
        let testnet = pkarr::mainline::Testnet::new_async(5).await.anyerr()?;
        let bootstrap = testnet.bootstrap.clone();

        // spawn our server with mainline support
        let server = Server::spawn_for_tests_with_options(
            dir.path(),
            Some(BootstrapOption::Custom(bootstrap)),
            None,
            None,
        )
        .await?;

        let origin = "irohdns.example.";

        // create a signed packet
        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)?;

        // publish the signed packet to our DHT
        let pkarr = pkarr::Client::builder()
            .no_default_network()
            .dht(|builder| builder.bootstrap(&testnet.bootstrap))
            .build()
            .anyerr()?;
        pkarr.publish(&signed_packet, None).await.anyerr()?;

        // resolve via DNS from our server, which will lookup from our DHT
        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)
    }
}

Synonyms

bbg/src/lib.rs
optica/src/lib.rs
zheng/src/lib.rs
nox/rs/lib.rs
honeycrisp/src/lib.rs
trident/src/lib.rs
lens/src/lib.rs
strata/src/lib.rs
rs/macros/src/lib.rs
strata/nebu/rs/lib.rs
honeycrisp/rane/src/lib.rs
honeycrisp/acpu/src/lib.rs
lens/core/src/lib.rs
rs/mir-format/src/lib.rs
rs/core/src/lib.rs
hemera/wgsl/src/lib.rs
strata/kuro/rs/lib.rs
radio/iroh-ffi/src/lib.rs
cyb/src-tauri/src/lib.rs
strata/core/src/lib.rs
radio/iroh-docs/src/lib.rs
strata/compute/src/lib.rs
lens/porphyry/src/lib.rs
radio/cyber-bao/src/lib.rs
radio/iroh-relay/src/lib.rs
lens/assayer/src/lib.rs
lens/brakedown/src/lib.rs
radio/iroh-car/src/lib.rs
honeycrisp/unimem/src/lib.rs
honeycrisp/aruminium/src/lib.rs
lens/binius/src/lib.rs
hemera/rs/src/lib.rs
strata/ext/src/lib.rs
radio/iroh/src/lib.rs
radio/iroh-gossip/src/lib.rs
strata/proof/src/lib.rs
radio/iroh-blobs/src/lib.rs
radio/iroh-base/src/lib.rs
radio/iroh-willow/src/lib.rs
lens/ikat/src/lib.rs
rs/tests/macro-integration/src/lib.rs
cw-cyber/contracts/hub-networks/src/lib.rs
radio/tests/integration/src/lib.rs
cw-cyber/contracts/litium-core/src/lib.rs
strata/trop/wgsl/src/lib.rs
strata/kuro/wgsl/src/lib.rs
cw-cyber/contracts/hub-protocols/src/lib.rs
cw-cyber/contracts/cw-cyber-gift/src/lib.rs
strata/trop/rs/src/lib.rs
cw-cyber/contracts/cybernet/src/lib.rs
cw-cyber/contracts/hub-channels/src/lib.rs
strata/nebu/wgsl/src/lib.rs
cw-cyber/contracts/graph-filter/src/lib.rs
cw-cyber/contracts/litium-stake/src/lib.rs
trident/editor/zed/src/lib.rs
radio/iroh-ffi/iroh-js/src/lib.rs
cw-cyber/contracts/hub-tokens/src/lib.rs
cyb/cyb/cyb-services/src/lib.rs
cw-cyber/packages/hub-base/src/lib.rs
strata/genies/rs/src/lib.rs
cw-cyber/contracts/std-test/src/lib.rs
cw-cyber/packages/cyber-std-test/src/lib.rs
cw-cyber/contracts/litium-refer/src/lib.rs
strata/jali/rs/src/lib.rs
cw-cyber/contracts/hub-libs/src/lib.rs
cw-cyber/contracts/litium-wrap/src/lib.rs
cw-cyber/packages/cyber-std/src/lib.rs
strata/genies/wgsl/src/lib.rs
cw-cyber/contracts/hub-skills/src/lib.rs
strata/jali/wgsl/src/lib.rs
cw-cyber/contracts/cw-cyber-subgraph/src/lib.rs
radio/iroh/bench/src/lib.rs
cw-cyber/contracts/litium-mine/src/lib.rs
cw-cyber/contracts/cw-cyber-passport/src/lib.rs

Neighbours