Skip to main content

iroh_dns_server/
server.rs

1//! The main server which combines the DNS and HTTP(S) servers.
2#[cfg(test)]
3use std::path::Path;
4use std::{net::SocketAddr, sync::Arc};
5
6use n0_error::Result;
7use tracing::info;
8#[cfg(test)]
9use url::Url;
10
11#[cfg(test)]
12use crate::http::HttpsConfig;
13use crate::{
14    config::Config,
15    dns::{DnsHandler, DnsServer},
16    http::HttpServer,
17    metrics::Metrics,
18    state::AppState,
19    store::ZoneStore,
20};
21
22/// A running iroh-dns server.
23///
24/// Combines a DNS listener and an HTTP/HTTPS listener into a single handle.
25/// Construct with [`Self::bind`] and drive to completion with [`Self::join`], or
26/// stop the tasks with [`Self::shutdown`].
27#[derive(Debug)]
28pub struct Server {
29    http_server: HttpServer,
30    dns_server: DnsServer,
31    metrics_server: Option<iroh_metrics::service::MetricsServer>,
32    metrics: Arc<Metrics>,
33}
34
35impl Server {
36    /// Binds and spawns the server from a [`Config`].
37    ///
38    /// Opens the persistent signed-packet store, enables the mainline DHT
39    /// fallback when configured, and spawns the DNS, HTTP(S), and metrics tasks.
40    /// Returns once all listeners are bound.
41    pub async fn bind(config: Config) -> Result<Self> {
42        let metrics = Arc::new(Metrics::default());
43        let mut store = ZoneStore::persistent(
44            config.signed_packet_store_path()?,
45            config.zone_store.clone().unwrap_or_default().into(),
46            metrics.clone(),
47        )?;
48        if let Some(bootstrap) = config.mainline_enabled() {
49            info!("mainline fallback enabled");
50            store = store.with_mainline_fallback(bootstrap);
51        };
52        Self::bind_with_store(config, store, metrics).await
53    }
54
55    /// Spawn the server.
56    ///
57    /// This will spawn several background tasks:
58    /// * A DNS server task
59    /// * A HTTP server task, if `config.http` is not empty
60    /// * A HTTPS server task, if `config.https` is not empty
61    async fn bind_with_store(
62        config: Config,
63        store: ZoneStore,
64        metrics: Arc<Metrics>,
65    ) -> Result<Self> {
66        let cert_cache_dir = config.data_dir()?.join("cert_cache");
67        let dns_handler = DnsHandler::new(store.clone(), &config.dns, metrics.clone())?;
68
69        let state = AppState {
70            store,
71            dns_handler,
72            metrics: metrics.clone(),
73        };
74
75        let metrics_server = if let Some(addr) = config.metrics_addr() {
76            let mut registry = iroh_metrics::Registry::default();
77            registry.register(metrics.clone());
78            let server =
79                iroh_metrics::service::MetricsServer::spawn(addr, Arc::new(registry)).await?;
80            Some(server)
81        } else {
82            None
83        };
84
85        let http_server = HttpServer::spawn(
86            config.http,
87            config.https,
88            config.pkarr_put_rate_limit,
89            state.clone(),
90            cert_cache_dir,
91        )
92        .await?;
93        let dns_server = DnsServer::spawn(config.dns, state.dns_handler.clone()).await?;
94
95        Ok(Self {
96            http_server,
97            dns_server,
98            metrics_server,
99            metrics,
100        })
101    }
102
103    /// Cancels the server tasks and waits for them to complete.
104    pub async fn shutdown(mut self) -> Result<()> {
105        if let Some(server) = self.metrics_server.take() {
106            server.shutdown().await;
107        }
108        let (res1, res2) = tokio::join!(self.dns_server.shutdown(), self.http_server.shutdown(),);
109        res1?;
110        res2?;
111        Ok(())
112    }
113
114    /// Waits for the server tasks to complete.
115    ///
116    /// Returns when a listener task finishes, either with success or an error.
117    pub async fn join(mut self) -> Result<()> {
118        tokio::select! {
119            res = self.dns_server.run_until_done() => res?,
120            res = self.http_server.run_until_done() => res?,
121        }
122        if let Some(server) = self.metrics_server.take() {
123            server.shutdown().await;
124        }
125
126        Ok(())
127    }
128
129    /// Returns the [`Metrics`] for this server.
130    pub fn metrics(&self) -> &Arc<Metrics> {
131        &self.metrics
132    }
133
134    /// Spawn a server suitable for testing.
135    ///
136    /// This will run the DNS and HTTP servers, but not the HTTPS server.
137    ///
138    /// It returns the server handle, the [`SocketAddr`] of the DNS server and the [`Url`] of the
139    /// HTTP server.
140    #[cfg(test)]
141    pub(crate) async fn spawn_for_tests(dir: impl AsRef<Path>) -> Result<Self> {
142        Self::spawn_for_tests_with_options(dir, None, None, None).await
143    }
144
145    /// Spawn a server suitable for testing, while optionally enabling mainline with custom
146    /// bootstrap addresses.
147    #[cfg(test)]
148    pub(crate) async fn spawn_for_tests_with_options(
149        dir: impl AsRef<Path>,
150        mainline: Option<crate::config::BootstrapOption>,
151        options: Option<crate::store::Options>,
152        https: Option<HttpsConfig>,
153    ) -> Result<Self> {
154        use std::net::{IpAddr, Ipv4Addr};
155
156        use crate::config::MetricsConfig;
157
158        let mut config = Config::default();
159        config.dns.port = 0;
160        config.dns.bind_addr = Some(IpAddr::V4(Ipv4Addr::LOCALHOST));
161        config.http.as_mut().unwrap().port = 0;
162        config.http.as_mut().unwrap().bind_addr = Some(IpAddr::V4(Ipv4Addr::LOCALHOST));
163        config.https = https;
164        config.metrics = Some(MetricsConfig::disabled());
165        config.data_dir = Some(dir.as_ref().to_owned());
166
167        let mut store = ZoneStore::in_memory(options.unwrap_or_default(), Default::default())?;
168        if let Some(bootstrap) = mainline {
169            info!("mainline fallback enabled");
170            store = store.with_mainline_fallback(bootstrap);
171        }
172        let server = Self::bind_with_store(config, store, Default::default()).await?;
173        Ok(server)
174    }
175
176    /// Returns the local address that the DNS listener is bound to.
177    pub fn dns_addr(&self) -> SocketAddr {
178        self.dns_server.local_addr()
179    }
180
181    /// Returns the local address of the HTTP listener, or `None` if no HTTP
182    /// listener was configured.
183    pub fn http_addr(&self) -> Option<SocketAddr> {
184        self.http_server.http_addr()
185    }
186
187    /// Returns the local address of the HTTPS listener, or `None` if no HTTPS
188    /// listener was configured.
189    pub fn https_addr(&self) -> Option<SocketAddr> {
190        self.http_server.https_addr()
191    }
192
193    #[cfg(test)]
194    pub(crate) fn http_url(&self) -> Option<Url> {
195        let http_addr = self.http_server.http_addr()?;
196        Some(
197            format!("http://{http_addr}")
198                .parse::<url::Url>()
199                .expect("valid url"),
200        )
201    }
202
203    #[cfg(test)]
204    pub(crate) fn https_url(&self) -> Option<Url> {
205        let https_addr = self.https_addr()?;
206        Some(
207            format!("https://{https_addr}")
208                .parse::<url::Url>()
209                .expect("valid url"),
210        )
211    }
212}