Skip to main content

iroh_dns_server/
config.rs

1//! Configuration for the [`Server`].
2//!
3//! [`Config`] is the entry point. It is usually loaded from a TOML file via [`Config::load`].
4//!
5//! [`Server`]: crate::Server
6
7use std::{
8    env,
9    net::{IpAddr, Ipv4Addr, SocketAddr},
10    path::{Path, PathBuf},
11    time::Duration,
12};
13
14use n0_error::{Result, StdResultExt};
15use serde::{Deserialize, Serialize};
16use tracing::info;
17
18use crate::store::Options;
19pub use crate::{
20    dns::DnsConfig,
21    http::{CertMode, HttpConfig, HttpsConfig, RateLimitConfig},
22};
23
24const DEFAULT_METRICS_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9117);
25
26/// Top-level configuration for the server.
27///
28/// Usually loaded from a TOML file via [`Self::load`]. The [`Default`] impl
29/// produces a config suitable for local development and testing.
30#[derive(Debug, Serialize, Deserialize)]
31#[non_exhaustive]
32pub struct Config {
33    /// Configuration for the HTTP listener.
34    ///
35    /// When `None`, no HTTP listener is started.
36    pub http: Option<HttpConfig>,
37    /// Configuration for the HTTPS listener.
38    ///
39    /// When `None`, no HTTPS listener is started.
40    pub https: Option<HttpsConfig>,
41    /// Configuration for the DNS listener.
42    pub dns: DnsConfig,
43    /// Configuration for the metrics server.
44    ///
45    /// When `None`, the metrics server binds to a default address. To disable
46    /// the metrics server entirely, use [`MetricsConfig::disabled`].
47    pub metrics: Option<MetricsConfig>,
48
49    /// Configuration for the mainline DHT fallback.
50    ///
51    /// When `None` or disabled, packets that are not present in the local store
52    /// are not looked up on the mainline DHT.
53    pub mainline: Option<MainlineConfig>,
54
55    /// Configuration for the signed-packet zone store.
56    ///
57    /// When `None`, the defaults from [`StoreConfig::default`] are used.
58    pub zone_store: Option<StoreConfig>,
59
60    /// Rate limit applied to `PUT /pkarr` requests.
61    #[serde(default)]
62    pub pkarr_put_rate_limit: RateLimitConfig,
63
64    /// Location where the server stores its data.
65    ///
66    /// When `None`, [`Self::data_dir`] falls back to the `IROH_DNS_DATA_DIR`
67    /// environment variable, then to the platform's standard data directory.
68    pub data_dir: Option<PathBuf>,
69}
70
71/// Configuration for the signed-packet store.
72#[derive(Debug, Serialize, Deserialize, Clone)]
73#[non_exhaustive]
74pub struct StoreConfig {
75    /// Maximum number of packets processed in a single write transaction.
76    pub max_batch_size: usize,
77
78    /// Maximum time a write transaction stays open before it is committed.
79    ///
80    /// Bounds how much data can be lost on a crash.
81    #[serde(with = "humantime_serde")]
82    pub max_batch_time: Duration,
83
84    /// Time a packet is retained in the store before it becomes eligible for eviction.
85    #[serde(with = "humantime_serde")]
86    pub eviction: Duration,
87
88    /// Interval between runs of the eviction task.
89    #[serde(with = "humantime_serde")]
90    pub eviction_interval: Duration,
91}
92
93impl Default for StoreConfig {
94    fn default() -> Self {
95        Options::default().into()
96    }
97}
98
99impl From<Options> for StoreConfig {
100    fn from(value: Options) -> Self {
101        Self {
102            max_batch_size: value.max_batch_size,
103            max_batch_time: value.max_batch_time,
104            eviction: value.eviction,
105            eviction_interval: value.eviction_interval,
106        }
107    }
108}
109
110impl From<StoreConfig> for Options {
111    fn from(value: StoreConfig) -> Self {
112        Self {
113            max_batch_size: value.max_batch_size,
114            max_batch_time: value.max_batch_time,
115            eviction: value.eviction,
116            eviction_interval: value.eviction_interval,
117        }
118    }
119}
120
121/// Configuration for the metrics server.
122///
123/// The metrics server exposes [`Metrics`] as [Prometheus]-format counters over a
124/// plain HTTP endpoint. It carries no authentication, so the bind address should
125/// be kept on a trusted network.
126///
127/// [`Metrics`]: crate::Metrics
128/// [Prometheus]: https://prometheus.io/docs/instrumenting/exposition_formats/
129#[derive(Debug, Serialize, Deserialize)]
130#[non_exhaustive]
131pub struct MetricsConfig {
132    /// Disables the metrics server when set to `true`.
133    pub disabled: bool,
134    /// Address to bind the metrics server to.
135    ///
136    /// When `None` and the server is enabled, binds to `127.0.0.1:9117`.
137    pub bind_addr: Option<SocketAddr>,
138}
139
140impl MetricsConfig {
141    /// Returns a [`MetricsConfig`] with the metrics server disabled.
142    pub fn disabled() -> Self {
143        Self {
144            disabled: true,
145            bind_addr: None,
146        }
147    }
148}
149
150/// Configuration for the mainline DHT fallback.
151///
152/// When enabled, the server looks up signed packets on the BitTorrent mainline
153/// DHT for keys that are not present in the local store.
154#[derive(Debug, Serialize, Deserialize)]
155#[non_exhaustive]
156pub struct MainlineConfig {
157    /// Enables the mainline DHT fallback when set to `true`.
158    pub enabled: bool,
159    /// Custom bootstrap nodes for the mainline DHT.
160    ///
161    /// Addresses must be formatted as `domain:port` or `ipv4:port`. When `None`
162    /// or empty, the default BitTorrent mainline bootstrap nodes defined by
163    /// pkarr are used.
164    pub bootstrap: Option<Vec<String>>,
165}
166
167/// Bootstrap nodes for mainline DHT resolution.
168#[derive(Debug, Serialize, Deserialize, Default)]
169pub(crate) enum BootstrapOption {
170    /// The default bootstrap nodes defined by pkarr.
171    #[default]
172    Default,
173    /// A custom set of bootstrap addresses (`domain:port` or `ipv4:port`).
174    Custom(Vec<String>),
175}
176
177#[allow(clippy::derivable_impls)]
178impl Default for MainlineConfig {
179    fn default() -> Self {
180        Self {
181            enabled: false,
182            bootstrap: None,
183        }
184    }
185}
186
187impl Config {
188    /// Loads a [`Config`] from a TOML file at `path`.
189    pub async fn load(path: impl AsRef<Path>) -> Result<Config> {
190        info!(
191            "loading config file from {}",
192            path.as_ref().to_string_lossy()
193        );
194        let s = tokio::fs::read_to_string(path.as_ref())
195            .await
196            .with_std_context(|_| format!("failed to read {}", path.as_ref().to_string_lossy()))?;
197        let config: Config = toml::from_str(&s).anyerr()?;
198        Ok(config)
199    }
200
201    /// Returns the data directory where the server stores its state.
202    ///
203    /// Resolution order:
204    /// 1. The [`Self::data_dir`] field, if set.
205    /// 2. The `IROH_DNS_DATA_DIR` environment variable.
206    /// 3. An `iroh-dns` subdirectory of the platform's standard data directory,
207    ///    as reported by `dirs_next::data_dir`.
208    pub fn data_dir(&self) -> Result<PathBuf> {
209        let dir = if let Some(dir) = &self.data_dir {
210            dir.clone()
211        } else if let Some(val) = env::var_os("IROH_DNS_DATA_DIR") {
212            PathBuf::from(val)
213        } else {
214            let path = dirs_next::data_dir()
215                .std_context("operating environment provides no directory for application data")?;
216
217            path.join("iroh-dns")
218        };
219        Ok(dir)
220    }
221
222    /// Returns the path to the signed-packet store database file.
223    ///
224    /// The path is `<data_dir>/signed-packets-1.db`, where `<data_dir>` is
225    /// resolved by [`Self::data_dir`].
226    pub fn signed_packet_store_path(&self) -> Result<PathBuf> {
227        Ok(self.data_dir()?.join("signed-packets-1.db"))
228    }
229
230    /// Get the address where the metrics server should be bound, if set.
231    pub(crate) fn metrics_addr(&self) -> Option<SocketAddr> {
232        match &self.metrics {
233            None => Some(DEFAULT_METRICS_ADDR),
234            Some(conf) => match conf.disabled {
235                true => None,
236                false => Some(conf.bind_addr.unwrap_or(DEFAULT_METRICS_ADDR)),
237            },
238        }
239    }
240
241    pub(crate) fn mainline_enabled(&self) -> Option<BootstrapOption> {
242        match self.mainline.as_ref() {
243            None => None,
244            Some(MainlineConfig { enabled: false, .. }) => None,
245            Some(MainlineConfig {
246                bootstrap: Some(bootstrap),
247                ..
248            }) => Some(BootstrapOption::Custom(bootstrap.clone())),
249            Some(MainlineConfig {
250                bootstrap: None, ..
251            }) => Some(BootstrapOption::Default),
252        }
253    }
254}
255
256impl Default for Config {
257    fn default() -> Self {
258        Self {
259            http: Some(HttpConfig {
260                port: 8080,
261                bind_addr: None,
262            }),
263            https: Some(HttpsConfig {
264                port: 8443,
265                bind_addr: None,
266                domains: vec!["localhost".to_string()],
267                cert_mode: CertMode::SelfSigned,
268                letsencrypt_contact: None,
269                letsencrypt_prod: None,
270            }),
271            dns: DnsConfig {
272                port: 5300,
273                bind_addr: None,
274                origins: vec!["irohdns.example.".to_string(), ".".to_string()],
275
276                default_soa: "irohdns.example hostmaster.irohdns.example 0 10800 3600 604800 3600"
277                    .to_string(),
278                default_ttl: 900,
279
280                rr_a: Some(Ipv4Addr::LOCALHOST),
281                rr_aaaa: None,
282                rr_ns: Some("ns1.irohdns.example.".to_string()),
283            },
284            zone_store: None,
285            metrics: None,
286            mainline: None,
287            pkarr_put_rate_limit: RateLimitConfig::default(),
288            data_dir: None,
289        }
290    }
291}