Skip to main content

iroh_dns_server/
lib.rs

1//! A DNS server and [pkarr] relay.
2//!
3//! [`Server`] combines a DNS server (UDP and TCP) with an HTTP/HTTPS server
4//! into a single process. Clients publish self-signed DNS records as [pkarr]
5//! signed packets at `PUT /pkarr`; the server persists them and answers DNS
6//! queries for the published names, including DNS-over-HTTPS at `/dns-query`.
7//!
8//! With the mainline fallback enabled, keys missing from the local store are
9//! looked up on the BitTorrent mainline DHT.
10//!
11//! # Example
12//!
13//! ```no_run
14//! use iroh_dns_server::{Server, config::Config};
15//! # async fn run() -> n0_error::Result<()> {
16//! let config = Config::load("config.toml").await?;
17//! let server = Server::bind(config).await?;
18//! server.join().await?;
19//! # Ok(())
20//! # }
21//! ```
22//!
23//! [pkarr]: https://github.com/Nuhvi/pkarr/
24
25#![deny(missing_docs, rustdoc::broken_intra_doc_links, unreachable_pub)]
26
27pub mod config;
28mod dns;
29mod http;
30mod metrics;
31mod server;
32mod state;
33mod store;
34mod util;
35
36pub use crate::{metrics::Metrics, server::Server};
37
38#[cfg(test)]
39mod tests {
40    use std::{
41        net::{Ipv4Addr, Ipv6Addr, SocketAddr},
42        time::Duration,
43    };
44
45    use iroh::{
46        RelayUrl, SecretKey,
47        address_lookup::PkarrRelayClient,
48        dns::DnsResolver,
49        endpoint_info::EndpointInfo,
50        tls::{CaTlsConfig, default_provider},
51    };
52    use iroh_dns::pkarr::SignedPacket;
53    use mainline::{DhtBuilder, MutableItem, Testnet};
54    use n0_error::{Result, StdResultExt};
55    use n0_tracing_test::traced_test;
56    use rand::{CryptoRng, RngExt, SeedableRng};
57
58    use crate::{
59        config::BootstrapOption,
60        server::Server,
61        store::{Options, PacketSource, ZoneStore},
62        util::PublicKeyBytes,
63    };
64
65    const DNS_TIMEOUT: Duration = Duration::from_secs(2);
66
67    #[tokio::test]
68    #[traced_test]
69    async fn pkarr_publish_dns_resolve() -> Result {
70        use simple_dns::{CLASS, Name as DnsName, Packet, ResourceRecord, rdata};
71
72        let dir = tempfile::tempdir()?;
73        let server = Server::spawn_for_tests(dir.path()).await?;
74        let pkarr_relay_url = {
75            let mut url = server.http_url().expect("http is bound");
76            url.set_path("/pkarr");
77            url
78        };
79
80        // Build a DNS packet with various record types using simple_dns directly
81        let secret_key = SecretKey::generate();
82        let origin = secret_key.public().to_z32();
83
84        let mut packet = Packet::new_reply(0);
85        // record at root
86        packet.answers.push(ResourceRecord::new(
87            DnsName::new_unchecked(&origin).into_owned(),
88            CLASS::IN,
89            30,
90            rdata::RData::TXT("hi0".try_into().unwrap()),
91        ));
92        // record at level one
93        packet.answers.push(ResourceRecord::new(
94            DnsName::new_unchecked(&format!("_hello.{origin}")).into_owned(),
95            CLASS::IN,
96            30,
97            rdata::RData::TXT("hi1".try_into().unwrap()),
98        ));
99        // record at level two
100        packet.answers.push(ResourceRecord::new(
101            DnsName::new_unchecked(&format!("_hello.world.{origin}")).into_owned(),
102            CLASS::IN,
103            30,
104            rdata::RData::TXT("hi2".try_into().unwrap()),
105        ));
106        // multiple records for same name
107        packet.answers.push(ResourceRecord::new(
108            DnsName::new_unchecked(&format!("multiple.{origin}")).into_owned(),
109            CLASS::IN,
110            30,
111            rdata::RData::TXT("hi3".try_into().unwrap()),
112        ));
113        packet.answers.push(ResourceRecord::new(
114            DnsName::new_unchecked(&format!("multiple.{origin}")).into_owned(),
115            CLASS::IN,
116            30,
117            rdata::RData::TXT("hi4".try_into().unwrap()),
118        ));
119        // record of type A
120        packet.answers.push(ResourceRecord::new(
121            DnsName::new_unchecked(&origin).into_owned(),
122            CLASS::IN,
123            30,
124            rdata::RData::A(Ipv4Addr::LOCALHOST.into()),
125        ));
126        // record of type AAAA
127        packet.answers.push(ResourceRecord::new(
128            DnsName::new_unchecked(&format!("foo.bar.baz.{origin}")).into_owned(),
129            CLASS::IN,
130            30,
131            rdata::RData::AAAA(Ipv6Addr::LOCALHOST.into()),
132        ));
133
134        // Encode and sign manually (same as pkarr format)
135        let encoded = packet.build_bytes_vec_compressed().anyerr()?;
136        let timestamp = std::time::SystemTime::now()
137            .duration_since(std::time::UNIX_EPOCH)
138            .unwrap()
139            .as_micros() as u64;
140        let signable = {
141            let mut s = format!("3:seqi{}e1:v{}:", timestamp, encoded.len()).into_bytes();
142            s.extend(&encoded);
143            s
144        };
145        let signature = secret_key.sign(&signable);
146        let mut raw = Vec::with_capacity(104 + encoded.len());
147        raw.extend_from_slice(secret_key.public().as_bytes());
148        raw.extend_from_slice(&signature.to_bytes());
149        raw.extend_from_slice(&timestamp.to_be_bytes());
150        raw.extend_from_slice(&encoded);
151        let signed_packet = SignedPacket::from_bytes(&raw).anyerr()?;
152
153        // Publish via relay
154        let tls_config = CaTlsConfig::default()
155            .client_config(default_provider())
156            .expect("infallible");
157        let pkarr_client =
158            PkarrRelayClient::new(pkarr_relay_url, tls_config, DnsResolver::default());
159        pkarr_client.publish(&signed_packet).await?;
160
161        use hickory_server::proto::rr::Name;
162        let pubkey = origin;
163        let resolver = test_resolver(server.dns_addr());
164
165        // resolve root record
166        let name = Name::from_utf8(format!("{pubkey}.")).anyerr()?;
167        let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
168        let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
169        assert_eq!(records, vec!["hi0".to_string()]);
170
171        // resolve level one record
172        let name = Name::from_utf8(format!("_hello.{pubkey}.")).anyerr()?;
173        let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
174        let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
175        assert_eq!(records, vec!["hi1".to_string()]);
176
177        // resolve level two record
178        let name = Name::from_utf8(format!("_hello.world.{pubkey}.")).anyerr()?;
179        let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
180        let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
181        assert_eq!(records, vec!["hi2".to_string()]);
182
183        // resolve multiple records for same name
184        let name = Name::from_utf8(format!("multiple.{pubkey}.")).anyerr()?;
185        let res = resolver.lookup_txt(name, DNS_TIMEOUT).await?;
186        let records = res.into_iter().map(|t| t.to_string()).collect::<Vec<_>>();
187        assert_eq!(records, vec!["hi3".to_string(), "hi4".to_string()]);
188
189        // resolve A record
190        let name = Name::from_utf8(format!("{pubkey}.")).anyerr()?;
191        let res = resolver.lookup_ipv4(name, DNS_TIMEOUT).await?;
192        let records = res.collect::<Vec<_>>();
193        assert_eq!(records, vec![Ipv4Addr::LOCALHOST]);
194
195        // resolve AAAA record
196        let name = Name::from_utf8(format!("foo.bar.baz.{pubkey}.")).anyerr()?;
197        let res = resolver.lookup_ipv6(name, DNS_TIMEOUT).await?;
198        let records = res.collect::<Vec<_>>();
199        assert_eq!(records, vec![Ipv6Addr::LOCALHOST]);
200
201        server.shutdown().await?;
202        Ok(())
203    }
204
205    #[tokio::test]
206    #[traced_test]
207    async fn integration_smoke() -> Result {
208        let dir = tempfile::tempdir()?;
209        let server = Server::spawn_for_tests(dir.path()).await?;
210
211        let pkarr_relay = {
212            let mut url = server.http_url().expect("http is bound");
213            url.set_path("/pkarr");
214            url
215        };
216
217        let origin = "irohdns.example.";
218
219        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
220
221        let secret_key = SecretKey::from_bytes(&rng.random());
222        let endpoint_id = secret_key.public();
223        let tls_config = CaTlsConfig::default()
224            .client_config(default_provider())
225            .expect("infallible");
226        let pkarr = PkarrRelayClient::new(pkarr_relay, tls_config, DnsResolver::default());
227        let relay_url: RelayUrl = "https://relay.example.".parse()?;
228        let endpoint_info = EndpointInfo::new(endpoint_id).with_relay_url(relay_url.clone());
229        let signed_packet = endpoint_info.to_pkarr_signed_packet(&secret_key, 30)?;
230
231        pkarr.publish(&signed_packet).await?;
232
233        let resolver = test_resolver(server.dns_addr());
234        let res = resolver.lookup_endpoint_by_id(&endpoint_id, origin).await?;
235
236        assert_eq!(res.endpoint_id, endpoint_id);
237        assert_eq!(res.relay_urls().next(), Some(&relay_url));
238
239        server.shutdown().await?;
240        Ok(())
241    }
242
243    #[tokio::test]
244    #[traced_test]
245    async fn store_eviction() -> Result {
246        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
247
248        let options = Options {
249            eviction: Duration::from_millis(100),
250            eviction_interval: Duration::from_millis(100),
251            max_batch_time: Duration::from_millis(100),
252            ..Default::default()
253        };
254        let store = ZoneStore::in_memory(options, Default::default())?;
255
256        // create a signed packet
257        let signed_packet = random_signed_packet(&mut rng)?;
258        let key = PublicKeyBytes::from_signed_packet(&signed_packet);
259
260        store
261            .insert(signed_packet, PacketSource::PkarrPublish)
262            .await?;
263
264        tokio::time::sleep(Duration::from_secs(1)).await;
265        for _ in 0..10 {
266            let entry = store.get_signed_packet(&key).await?;
267            if entry.is_none() {
268                return Ok(());
269            }
270            tokio::time::sleep(Duration::from_secs(1)).await;
271        }
272        panic!("store did not evict packet");
273    }
274
275    #[tokio::test]
276    #[traced_test]
277    #[ignore = "flaky"]
278    async fn integration_mainline() -> Result {
279        let dir = tempfile::tempdir()?;
280        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
281
282        // run a mainline testnet
283        let testnet = Testnet::new_async(5).await.anyerr()?;
284        let bootstrap = testnet.bootstrap.clone();
285
286        // spawn our server with mainline support
287        let server = Server::spawn_for_tests_with_options(
288            dir.path(),
289            Some(BootstrapOption::Custom(bootstrap.clone())),
290            None,
291            None,
292        )
293        .await?;
294
295        let origin = "irohdns.example.";
296
297        // create a signed packet
298        let secret_key = SecretKey::from_bytes(&rng.random());
299        let endpoint_id = secret_key.public();
300        let relay_url: RelayUrl = "https://relay.example.".parse()?;
301        let endpoint_info = EndpointInfo::new(endpoint_id).with_relay_url(relay_url.clone());
302        let signed_packet = endpoint_info.to_pkarr_signed_packet(&secret_key, 30)?;
303
304        // publish to DHT using mainline directly
305        let mut dht_builder = DhtBuilder::default();
306        dht_builder.bootstrap(&bootstrap);
307        let dht = dht_builder.build().anyerr()?;
308        let item = MutableItem::new_signed_unchecked(
309            *secret_key.public().as_bytes(),
310            signed_packet.signature().to_bytes(),
311            signed_packet.encoded_packet(),
312            signed_packet.timestamp().as_micros() as i64,
313            None,
314        );
315        dht.clone()
316            .as_async()
317            .put_mutable(item, None)
318            .await
319            .anyerr()?;
320
321        // resolve via DNS from our server, which will lookup from our DHT
322        let resolver = test_resolver(server.dns_addr());
323        let res = resolver.lookup_endpoint_by_id(&endpoint_id, origin).await?;
324
325        assert_eq!(res.endpoint_id, endpoint_id);
326        assert_eq!(res.relay_urls().next(), Some(&relay_url));
327
328        server.shutdown().await?;
329        Ok(())
330    }
331
332    fn test_resolver(nameserver: SocketAddr) -> DnsResolver {
333        DnsResolver::with_nameserver(nameserver)
334    }
335
336    fn random_signed_packet<R: CryptoRng + ?Sized>(rng: &mut R) -> Result<SignedPacket> {
337        let secret_key = SecretKey::from_bytes(&rng.random());
338        let endpoint_id = secret_key.public();
339        let relay_url: RelayUrl = "https://relay.example.".parse()?;
340        let endpoint_info = EndpointInfo::new(endpoint_id).with_relay_url(relay_url.clone());
341        let packet = endpoint_info.to_pkarr_signed_packet(&secret_key, 30)?;
342        Ok(packet)
343    }
344}