hickory_dns/
lib.rs

1// Copyright 2015-2018 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! Configuration module for the server binary, `named`.
9
10#[cfg(feature = "__dnssec")]
11pub mod dnssec;
12
13#[cfg(feature = "prometheus-metrics")]
14use std::net::SocketAddr;
15#[cfg(feature = "__tls")]
16use std::{ffi::OsStr, fs};
17use std::{
18    fmt,
19    fs::File,
20    io::Read,
21    net::{AddrParseError, Ipv4Addr, Ipv6Addr},
22    path::{Path, PathBuf},
23    str::FromStr,
24    sync::Arc,
25    time::Duration,
26};
27
28use cfg_if::cfg_if;
29use ipnet::IpNet;
30#[cfg(feature = "__tls")]
31use rustls::{
32    pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
33    server::ResolvesServerCert,
34    sign::{CertifiedKey, SingleCertAndKey},
35};
36use serde::de::{self, MapAccess, SeqAccess, Visitor};
37use serde::{self, Deserialize, Deserializer};
38
39#[cfg(feature = "__tls")]
40use hickory_proto::rustls::default_provider;
41use hickory_proto::{ProtoError, rr::Name};
42#[cfg(feature = "__dnssec")]
43use hickory_server::authority::DnssecAuthority;
44#[cfg(feature = "__dnssec")]
45use hickory_server::dnssec::NxProofKind;
46#[cfg(feature = "blocklist")]
47use hickory_server::store::blocklist::BlocklistAuthority;
48#[cfg(feature = "blocklist")]
49use hickory_server::store::blocklist::BlocklistConfig;
50use hickory_server::store::file::FileConfig;
51#[cfg(feature = "resolver")]
52use hickory_server::store::forwarder::ForwardAuthority;
53#[cfg(feature = "resolver")]
54use hickory_server::store::forwarder::ForwardConfig;
55#[cfg(feature = "recursor")]
56use hickory_server::store::recursor::RecursiveAuthority;
57#[cfg(feature = "recursor")]
58use hickory_server::store::recursor::RecursiveConfig;
59#[cfg(feature = "sqlite")]
60use hickory_server::store::sqlite::{SqliteAuthority, SqliteConfig};
61use hickory_server::{
62    ConfigError,
63    authority::{AuthorityObject, ZoneType},
64    store::file::FileAuthority,
65};
66use tracing::{debug, info, warn};
67
68#[cfg(feature = "prometheus-metrics")]
69mod prometheus_server;
70
71#[cfg(feature = "prometheus-metrics")]
72pub use prometheus_server::PrometheusServer;
73
74static DEFAULT_PATH: &str = "/var/named"; // TODO what about windows (do I care? ;)
75static DEFAULT_PORT: u16 = 53;
76static DEFAULT_TLS_PORT: u16 = 853;
77static DEFAULT_HTTPS_PORT: u16 = 443;
78static DEFAULT_QUIC_PORT: u16 = 853; // https://www.rfc-editor.org/rfc/rfc9250.html#name-reservation-of-a-dedicated-
79static DEFAULT_H3_PORT: u16 = 443;
80static DEFAULT_TCP_REQUEST_TIMEOUT: u64 = 5;
81
82/// Server configuration
83#[derive(Deserialize, Debug)]
84#[serde(deny_unknown_fields)]
85pub struct Config {
86    /// The list of IPv4 addresses to listen on
87    #[serde(default)]
88    listen_addrs_ipv4: Vec<String>,
89    /// This list of IPv6 addresses to listen on
90    #[serde(default)]
91    listen_addrs_ipv6: Vec<String>,
92    /// Port on which to listen (associated to all IPs)
93    listen_port: Option<u16>,
94    /// Secure port to listen on
95    tls_listen_port: Option<u16>,
96    /// HTTPS port to listen on
97    https_listen_port: Option<u16>,
98    /// QUIC port to listen on
99    quic_listen_port: Option<u16>,
100    /// HTTP/3 port to listen on
101    h3_listen_port: Option<u16>,
102    /// Prometheus listen address
103    #[cfg(feature = "prometheus-metrics")]
104    prometheus_listen_addr: Option<SocketAddr>,
105    /// Disable TCP protocol
106    disable_tcp: Option<bool>,
107    /// Disable UDP protocol
108    disable_udp: Option<bool>,
109    /// Disable TLS protocol
110    disable_tls: Option<bool>,
111    /// Disable HTTPS protocol
112    disable_https: Option<bool>,
113    /// Disable QUIC protocol
114    disable_quic: Option<bool>,
115    /// Disable Prometheus metrics
116    #[cfg(feature = "prometheus-metrics")]
117    disable_prometheus: Option<bool>,
118    /// Timeout associated to a request before it is closed.
119    tcp_request_timeout: Option<u64>,
120    /// Level at which to log, default is INFO
121    log_level: Option<String>,
122    /// Base configuration directory, i.e. root path for zones
123    directory: Option<String>,
124    /// User to run the server as.
125    ///
126    /// Only supported on Unix-like platforms. If the real or effective UID of the hickory process
127    /// is root, we will attempt to change to this user (or to nobody if no user is specified here.)
128    pub user: Option<String>,
129    /// Group to run the server as.
130    ///
131    /// Only supported on Unix-like platforms. If the real or effective UID of the hickory process
132    /// is root, we will attempt to change to this group (or to nobody if no group is specified here.)
133    pub group: Option<String>,
134    /// List of configurations for zones
135    #[serde(default)]
136    #[serde(deserialize_with = "deserialize_with_file")]
137    zones: Vec<ZoneConfig>,
138    /// Certificate to associate to TLS connections (currently the same is used for HTTPS and TLS)
139    #[cfg(feature = "__tls")]
140    tls_cert: Option<TlsCertConfig>,
141    /// The HTTP endpoint where the DNS-over-HTTPS server provides service. Applicable
142    /// to both HTTP/2 and HTTP/3 servers. Typically `/dns-query`.
143    #[cfg(any(feature = "__https", feature = "__h3"))]
144    http_endpoint: Option<String>,
145    /// Networks denied to access the server
146    #[serde(default)]
147    deny_networks: Vec<IpNet>,
148    /// Networks allowed to access the server
149    #[serde(default)]
150    allow_networks: Vec<IpNet>,
151}
152
153impl Config {
154    /// read a Config file from the file specified at path.
155    pub fn read_config(path: &Path) -> Result<Self, ConfigError> {
156        let mut file = File::open(path)?;
157        let mut toml = String::new();
158        file.read_to_string(&mut toml)?;
159        Self::from_toml(&toml)
160    }
161
162    /// Read a [`Config`] from the given TOML string.
163    pub fn from_toml(toml: &str) -> Result<Self, ConfigError> {
164        Ok(toml::from_str(toml)?)
165    }
166
167    /// set of listening ipv4 addresses (for TCP and UDP)
168    pub fn listen_addrs_ipv4(&self) -> Result<Vec<Ipv4Addr>, AddrParseError> {
169        self.listen_addrs_ipv4.iter().map(|s| s.parse()).collect()
170    }
171
172    /// set of listening ipv6 addresses (for TCP and UDP)
173    pub fn listen_addrs_ipv6(&self) -> Result<Vec<Ipv6Addr>, AddrParseError> {
174        self.listen_addrs_ipv6.iter().map(|s| s.parse()).collect()
175    }
176
177    /// port on which to listen for connections on specified addresses
178    pub fn listen_port(&self) -> u16 {
179        self.listen_port.unwrap_or(DEFAULT_PORT)
180    }
181
182    /// port on which to listen for TLS connections
183    pub fn tls_listen_port(&self) -> u16 {
184        self.tls_listen_port.unwrap_or(DEFAULT_TLS_PORT)
185    }
186
187    /// port on which to listen for HTTPS connections
188    pub fn https_listen_port(&self) -> u16 {
189        self.https_listen_port.unwrap_or(DEFAULT_HTTPS_PORT)
190    }
191
192    /// port on which to listen for QUIC connections
193    pub fn quic_listen_port(&self) -> u16 {
194        self.quic_listen_port.unwrap_or(DEFAULT_QUIC_PORT)
195    }
196
197    /// port on which to listen for HTTP/3 connections
198    pub fn h3_listen_port(&self) -> u16 {
199        self.h3_listen_port.unwrap_or(DEFAULT_H3_PORT)
200    }
201
202    /// prometheus metric endpoint listen address
203    #[cfg(feature = "prometheus-metrics")]
204    pub fn prometheus_listen_addr(&self) -> SocketAddr {
205        self.prometheus_listen_addr
206            .unwrap_or(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 9000))
207    }
208
209    /// get if TCP protocol should be disabled
210    pub fn disable_tcp(&self) -> bool {
211        self.disable_tcp.unwrap_or_default()
212    }
213
214    /// get if UDP protocol should be disabled
215    pub fn disable_udp(&self) -> bool {
216        self.disable_udp.unwrap_or_default()
217    }
218
219    /// get if TLS protocol should be disabled
220    pub fn disable_tls(&self) -> bool {
221        self.disable_tls.unwrap_or_default()
222    }
223
224    /// get if HTTPS protocol should be disabled
225    pub fn disable_https(&self) -> bool {
226        self.disable_https.unwrap_or_default()
227    }
228
229    /// get if QUIC protocol should be disabled
230    pub fn disable_quic(&self) -> bool {
231        self.disable_quic.unwrap_or_default()
232    }
233
234    /// get if Prometheus metrics endpoint should be disabled
235    #[cfg(feature = "prometheus-metrics")]
236    pub fn disable_prometheus(&self) -> bool {
237        self.disable_prometheus.unwrap_or_default()
238    }
239
240    /// default timeout for all TCP connections before forcibly shutdown
241    pub fn tcp_request_timeout(&self) -> Duration {
242        Duration::from_secs(
243            self.tcp_request_timeout
244                .unwrap_or(DEFAULT_TCP_REQUEST_TIMEOUT),
245        )
246    }
247
248    /// specify the log level which should be used, ["Trace", "Debug", "Info", "Warn", "Error"]
249    pub fn log_level(&self) -> tracing::Level {
250        if let Some(level_str) = &self.log_level {
251            tracing::Level::from_str(level_str).unwrap_or(tracing::Level::INFO)
252        } else {
253            tracing::Level::INFO
254        }
255    }
256
257    /// the path for all zone configurations, defaults to `/var/named`
258    pub fn directory(&self) -> &Path {
259        self.directory
260            .as_ref()
261            .map_or(Path::new(DEFAULT_PATH), Path::new)
262    }
263
264    /// the set of zones which should be loaded
265    pub fn zones(&self) -> &[ZoneConfig] {
266        &self.zones
267    }
268
269    /// the tls certificate to use for accepting tls connections
270    pub fn tls_cert(&self) -> Option<&TlsCertConfig> {
271        cfg_if! {
272            if #[cfg(feature = "__tls")] {
273                self.tls_cert.as_ref()
274            } else {
275                None
276            }
277        }
278    }
279
280    /// the HTTP endpoint from where requests are received
281    #[cfg(any(feature = "__https", feature = "__h3"))]
282    pub fn http_endpoint(&self) -> &str {
283        self.http_endpoint
284            .as_deref()
285            .unwrap_or(hickory_proto::http::DEFAULT_DNS_QUERY_PATH)
286    }
287
288    /// get the networks denied access to this server
289    pub fn deny_networks(&self) -> &[IpNet] {
290        &self.deny_networks
291    }
292
293    /// get the networks allowed to connect to this server
294    pub fn allow_networks(&self) -> &[IpNet] {
295        &self.allow_networks
296    }
297}
298
299#[derive(Deserialize, Debug)]
300struct ZoneConfigWithFile {
301    file: Option<PathBuf>,
302    #[serde(flatten)]
303    config: ZoneConfig,
304}
305
306fn deserialize_with_file<'de, D>(deserializer: D) -> Result<Vec<ZoneConfig>, D::Error>
307where
308    D: Deserializer<'de>,
309    D::Error: serde::de::Error,
310{
311    Vec::<ZoneConfigWithFile>::deserialize(deserializer)?
312        .into_iter()
313        .map(|ZoneConfigWithFile { file, mut config }| match file {
314            Some(file) => match &mut config.zone_type_config {
315                ZoneTypeConfig::Primary(server_config)
316                | ZoneTypeConfig::Secondary(server_config) => {
317                    if server_config
318                        .stores
319                        .iter()
320                        .any(|store| matches!(store, ServerStoreConfig::File(_)))
321                    {
322                        Err(<D::Error as serde::de::Error>::custom(
323                            "having `file` and `[zones.store]` item with type `file` is ambiguous",
324                        ))
325                    } else {
326                        let store = ServerStoreConfig::File(FileConfig {
327                            zone_file_path: file,
328                        });
329
330                        if server_config.stores.len() == 1
331                            && matches!(&server_config.stores[0], ServerStoreConfig::Default)
332                        {
333                            server_config.stores[0] = store;
334                        } else {
335                            server_config.stores.push(store);
336                        }
337                        Ok(config)
338                    }
339                }
340                _ => Err(<D::Error as serde::de::Error>::custom(
341                    "cannot use `file` on a zone that is not primary or secondary",
342                )),
343            },
344
345            _ => Ok(config),
346        })
347        .collect::<Result<Vec<_>, _>>()
348}
349
350/// Configuration for a zone
351#[derive(Deserialize, Debug)]
352pub struct ZoneConfig {
353    /// name of the zone
354    pub zone: String, // TODO: make Domain::Name decodable
355    /// type of the zone
356    #[serde(flatten)]
357    pub zone_type_config: ZoneTypeConfig,
358}
359
360impl ZoneConfig {
361    #[warn(clippy::wildcard_enum_match_arm)] // make sure all cases are handled despite of non_exhaustive
362    pub async fn load(&self, zone_dir: &Path) -> Result<Vec<Arc<dyn AuthorityObject>>, String> {
363        debug!("loading zone with config: {self:#?}");
364
365        let zone_name = self
366            .zone()
367            .map_err(|err| format!("failed to read zone name: {err}"))?;
368        let zone_type = self.zone_type();
369
370        // load the zone and insert any configured authorities in the catalog.
371
372        let mut authorities: Vec<Arc<dyn AuthorityObject>> = vec![];
373
374        #[cfg(feature = "blocklist")]
375        let handle_blocklist_store = |config| {
376            let zone_name = zone_name.clone();
377
378            async move {
379                Result::<Arc<dyn AuthorityObject>, String>::Ok(Arc::new(
380                    BlocklistAuthority::try_from_config(
381                        zone_name.clone(),
382                        zone_type,
383                        config,
384                        Some(zone_dir),
385                    )
386                    .await?,
387                ))
388            }
389        };
390
391        match &self.zone_type_config {
392            ZoneTypeConfig::Primary(server_config) | ZoneTypeConfig::Secondary(server_config) => {
393                debug!(
394                    "loading authorities for {zone_name} with stores {:?}",
395                    server_config.stores
396                );
397
398                let is_axfr_allowed = server_config.is_axfr_allowed();
399                for store in &server_config.stores {
400                    let authority: Arc<dyn AuthorityObject> = match store {
401                        #[cfg(feature = "sqlite")]
402                        ServerStoreConfig::Sqlite(config) => {
403                            #[cfg_attr(not(feature = "__dnssec"), allow(unused_mut))]
404                            let mut authority = SqliteAuthority::try_from_config(
405                                zone_name.clone(),
406                                zone_type,
407                                is_axfr_allowed,
408                                server_config.is_dnssec_enabled(),
409                                Some(zone_dir),
410                                config,
411                                #[cfg(feature = "__dnssec")]
412                                server_config.nx_proof_kind.clone(),
413                            )
414                            .await?;
415
416                            #[cfg(feature = "__dnssec")]
417                            server_config.load_keys(&mut authority, &zone_name).await?;
418                            Arc::new(authority)
419                        }
420
421                        ServerStoreConfig::File(config) => {
422                            #[cfg_attr(not(feature = "__dnssec"), allow(unused_mut))]
423                            let mut authority = FileAuthority::try_from_config(
424                                zone_name.clone(),
425                                zone_type,
426                                is_axfr_allowed,
427                                Some(zone_dir),
428                                config,
429                                #[cfg(feature = "__dnssec")]
430                                server_config.nx_proof_kind.clone(),
431                            )?;
432
433                            #[cfg(feature = "__dnssec")]
434                            server_config.load_keys(&mut authority, &zone_name).await?;
435                            Arc::new(authority)
436                        }
437                        _ => return empty_stores_error(),
438                    };
439
440                    authorities.push(authority);
441                }
442            }
443            ZoneTypeConfig::External { stores } => {
444                debug!(
445                    "loading authorities for {zone_name} with stores {:?}",
446                    stores
447                );
448
449                #[cfg_attr(
450                    not(any(feature = "blocklist", feature = "resolver")),
451                    allow(unreachable_code, unused_variables, clippy::never_loop)
452                )]
453                for store in stores {
454                    let authority: Arc<dyn AuthorityObject> = match store {
455                        #[cfg(feature = "blocklist")]
456                        ExternalStoreConfig::Blocklist(config) => {
457                            handle_blocklist_store(config).await?
458                        }
459                        #[cfg(feature = "resolver")]
460                        ExternalStoreConfig::Forward(config) => {
461                            let forwarder = ForwardAuthority::builder_tokio(config.clone())
462                                .with_origin(zone_name.clone())
463                                .build()?;
464
465                            Arc::new(forwarder)
466                        }
467                        #[cfg(feature = "recursor")]
468                        ExternalStoreConfig::Recursor(config) => {
469                            let recursor = RecursiveAuthority::try_from_config(
470                                zone_name.clone(),
471                                zone_type,
472                                config,
473                                Some(zone_dir),
474                            )
475                            .await?;
476
477                            Arc::new(recursor)
478                        }
479                        _ => return empty_stores_error(),
480                    };
481
482                    authorities.push(authority);
483                }
484            }
485        }
486
487        info!("zone successfully loaded: {}", self.zone()?);
488        Ok(authorities)
489    }
490
491    // TODO this is a little ugly for the parse, b/c there is no terminal char
492    /// returns the name of the Zone, i.e. the `example.com` of `www.example.com.`
493    pub fn zone(&self) -> Result<Name, ProtoError> {
494        Name::parse(&self.zone, Some(&Name::new()))
495    }
496
497    /// the type of the zone
498    pub fn zone_type(&self) -> ZoneType {
499        match &self.zone_type_config {
500            ZoneTypeConfig::Primary { .. } => ZoneType::Primary,
501            ZoneTypeConfig::Secondary { .. } => ZoneType::Secondary,
502            ZoneTypeConfig::External { .. } => ZoneType::External,
503        }
504    }
505}
506
507fn empty_stores_error<T>() -> Result<T, String> {
508    Result::Err("empty [[zones.stores]] in config".to_owned())
509}
510
511#[derive(Deserialize, Debug)]
512#[serde(tag = "zone_type")]
513#[serde(deny_unknown_fields)]
514/// Enumeration over each zone type's configuration.
515pub enum ZoneTypeConfig {
516    Primary(ServerZoneConfig),
517    Secondary(ServerZoneConfig),
518    External {
519        /// Store configurations.  Note: we specify a default handler to get a Vec containing a
520        /// StoreConfig::Default, which is used for authoritative file-based zones and legacy sqlite
521        /// configurations. #[serde(default)] cannot be used, because it will invoke Default for Vec,
522        /// i.e., an empty Vec and we cannot implement Default for StoreConfig and return a Vec.  The
523        /// custom visitor is used to handle map (single store) or sequence (chained store) configurations.
524        #[serde(default = "store_config_default")]
525        #[serde(deserialize_with = "store_config_visitor")]
526        stores: Vec<ExternalStoreConfig>,
527    },
528}
529
530impl ZoneTypeConfig {
531    pub fn as_server(&self) -> Option<&ServerZoneConfig> {
532        match self {
533            Self::Primary(c) | Self::Secondary(c) => Some(c),
534            _ => None,
535        }
536    }
537}
538
539#[derive(Deserialize, Debug)]
540#[serde(deny_unknown_fields)]
541pub struct ServerZoneConfig {
542    /// Allow AXFR (TODO: need auth)
543    pub allow_axfr: Option<bool>,
544    /// Keys for use by the zone
545    #[cfg(feature = "__dnssec")]
546    #[serde(default)]
547    pub keys: Vec<dnssec::KeyConfig>,
548    /// The kind of non-existence proof provided by the nameserver
549    #[cfg(feature = "__dnssec")]
550    pub nx_proof_kind: Option<NxProofKind>,
551    /// Store configurations.  Note: we specify a default handler to get a Vec containing a
552    /// StoreConfig::Default, which is used for authoritative file-based zones and legacy sqlite
553    /// configurations. #[serde(default)] cannot be used, because it will invoke Default for Vec,
554    /// i.e., an empty Vec and we cannot implement Default for StoreConfig and return a Vec.  The
555    /// custom visitor is used to handle map (single store) or sequence (chained store) configurations.
556    #[serde(default = "store_config_default")]
557    #[serde(deserialize_with = "store_config_visitor")]
558    pub stores: Vec<ServerStoreConfig>,
559}
560
561impl ServerZoneConfig {
562    #[cfg(feature = "__dnssec")]
563    async fn load_keys(
564        &self,
565        authority: &mut impl DnssecAuthority<Lookup = impl Send + Sync + Sized + 'static>,
566        zone_name: &Name,
567    ) -> Result<(), String> {
568        if !self.is_dnssec_enabled() {
569            return Ok(());
570        }
571
572        for key_config in &self.keys {
573            key_config.load(authority, zone_name.clone()).await?;
574        }
575
576        info!("signing zone: {zone_name}");
577        authority
578            .secure_zone()
579            .await
580            .map_err(|err| format!("failed to sign zone {zone_name}: {err}"))?;
581
582        Ok(())
583    }
584
585    /// path to the zone file, i.e. the base set of original records in the zone
586    ///
587    /// this is only used on first load, if dynamic update is enabled for the zone, then the journal
588    /// file is the actual source of truth for the zone.
589    pub fn file(&self) -> Option<&Path> {
590        self.stores.iter().find_map(|store| match store {
591            ServerStoreConfig::File(file_config) => Some(&*file_config.zone_file_path),
592            #[cfg(feature = "sqlite")]
593            ServerStoreConfig::Sqlite(sqlite_config) => Some(&*sqlite_config.zone_file_path),
594            ServerStoreConfig::Default => None,
595        })
596    }
597
598    /// enable AXFR transfers
599    pub fn is_axfr_allowed(&self) -> bool {
600        self.allow_axfr.unwrap_or(false)
601    }
602
603    /// declare that this zone should be signed, see keys for configuration of the keys for signing
604    pub fn is_dnssec_enabled(&self) -> bool {
605        cfg_if! {
606            if #[cfg(feature = "__dnssec")] {
607                !self.keys.is_empty()
608            } else {
609                false
610            }
611        }
612    }
613}
614
615/// Enumeration over store types for secondary nameservers.
616#[derive(Deserialize, Debug, Default)]
617#[serde(tag = "type")]
618#[serde(rename_all = "lowercase")]
619#[non_exhaustive]
620pub enum ServerStoreConfig {
621    /// File based configuration
622    File(FileConfig),
623    /// Sqlite based configuration file
624    #[cfg(feature = "sqlite")]
625    Sqlite(SqliteConfig),
626    /// This is used by the configuration processing code to represent a deprecated or main-block config without an associated store.
627    #[default]
628    Default,
629}
630
631/// Enumeration over store types for external nameservers.
632#[allow(clippy::large_enum_variant)]
633#[derive(Deserialize, Debug, Default)]
634#[serde(rename_all = "lowercase", tag = "type")]
635#[non_exhaustive]
636pub enum ExternalStoreConfig {
637    /// Blocklist configuration
638    #[cfg(feature = "blocklist")]
639    Blocklist(BlocklistConfig),
640    /// Forwarding Resolver
641    #[cfg(feature = "resolver")]
642    Forward(ForwardConfig),
643    /// Recursive Resolver
644    #[cfg(feature = "recursor")]
645    Recursor(Box<RecursiveConfig>),
646    /// This is used by the configuration processing code to represent a deprecated or main-block config without an associated store.
647    #[default]
648    Default,
649}
650
651/// Create a default value for serde for store config enums.
652fn store_config_default<S: Default>() -> Vec<S> {
653    vec![Default::default()]
654}
655
656/// Custom serde visitor that can deserialize a map (single configuration store, expressed as a TOML
657/// table) or sequence (chained configuration stores, expressed as a TOML array of tables.)
658/// This is used instead of an untagged enum because serde cannot provide variant-specific error
659/// messages when using an untagged enum.
660fn store_config_visitor<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
661where
662    D: Deserializer<'de>,
663    T: Deserialize<'de>,
664{
665    struct MapOrSequence<T>(std::marker::PhantomData<T>);
666
667    impl<'de, T: Deserialize<'de>> Visitor<'de> for MapOrSequence<T> {
668        type Value = Vec<T>;
669
670        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
671            formatter.write_str("map or sequence")
672        }
673
674        fn visit_seq<S>(self, seq: S) -> Result<Vec<T>, S::Error>
675        where
676            S: SeqAccess<'de>,
677        {
678            Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
679        }
680
681        fn visit_map<M>(self, map: M) -> Result<Vec<T>, M::Error>
682        where
683            M: MapAccess<'de>,
684        {
685            match Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) {
686                Ok(seq) => Ok(vec![seq]),
687                Err(e) => Err(e),
688            }
689        }
690    }
691
692    deserializer.deserialize_any(MapOrSequence::<T>(Default::default()))
693}
694
695/// Configuration for a TLS certificate
696#[derive(Deserialize, PartialEq, Eq, Debug)]
697#[serde(deny_unknown_fields)]
698#[non_exhaustive]
699pub struct TlsCertConfig {
700    pub path: PathBuf,
701    pub endpoint_name: Option<String>,
702    pub private_key: PathBuf,
703}
704
705#[cfg(feature = "__tls")]
706impl TlsCertConfig {
707    /// Load a Certificate from the path (with rustls)
708    pub fn load(&self, zone_dir: &Path) -> Result<Arc<dyn ResolvesServerCert>, String> {
709        if self.path.extension().and_then(OsStr::to_str) != Some("pem") {
710            return Err(format!(
711                "unsupported certificate file format (expected `.pem` extension): {}",
712                self.path.display()
713            ));
714        }
715
716        let cert_path = zone_dir.join(&self.path);
717        info!(
718            "loading TLS PEM certificate chain from: {}",
719            cert_path.display()
720        );
721
722        let cert_chain = CertificateDer::pem_file_iter(&cert_path)
723            .map_err(|e| {
724                format!(
725                    "failed to read cert chain from {}: {e}",
726                    cert_path.display()
727                )
728            })?
729            .collect::<Result<Vec<_>, _>>()
730            .map_err(|e| {
731                format!(
732                    "failed to parse cert chain from {}: {e}",
733                    cert_path.display()
734                )
735            })?;
736
737        let key_extension = self.private_key.extension();
738        let key = if key_extension.is_some_and(|ext| ext == "pem") {
739            let key_path = zone_dir.join(&self.private_key);
740            info!("loading TLS PKCS8 key from PEM: {}", key_path.display());
741            PrivateKeyDer::from_pem_file(&key_path)
742                .map_err(|e| format!("failed to read key from {}: {e}", key_path.display()))?
743        } else if key_extension.is_some_and(|ext| ext == "der" || ext == "key") {
744            let key_path = zone_dir.join(&self.private_key);
745            info!("loading TLS PKCS8 key from DER: {}", key_path.display());
746
747            let buf =
748                fs::read(&key_path).map_err(|e| format!("error reading key from file: {e}"))?;
749            PrivateKeyDer::try_from(buf).map_err(|e| format!("error parsing key DER: {e}"))?
750        } else {
751            return Err(format!(
752                "unsupported private key file format (expected `.pem` or `.der` extension): {}",
753                self.private_key.display()
754            ));
755        };
756
757        let certified_key = CertifiedKey::from_der(cert_chain, key, &default_provider())
758            .map_err(|err| format!("failed to read certificate and keys: {err:?}"))?;
759
760        Ok(Arc::new(SingleCertAndKey::from(certified_key)))
761    }
762}
763
764#[cfg(all(test, any(feature = "resolver", feature = "recursor")))]
765mod tests {
766    use super::*;
767
768    #[cfg(feature = "recursor")]
769    #[test]
770    fn example_recursor_config() {
771        toml::from_str::<Config>(include_str!(
772            "../../tests/test-data/test_configs/example_recursor.toml"
773        ))
774        .unwrap();
775    }
776
777    #[cfg(feature = "resolver")]
778    #[test]
779    fn single_store_config_error_message() {
780        match toml::from_str::<Config>(
781            r#"[[zones]]
782               zone = "."
783               zone_type = "External"
784
785               [zones.stores]
786               ype = "forward""#,
787        ) {
788            Ok(val) => panic!("expected error value; got ok: {val:?}"),
789            Err(e) => assert!(e.to_string().contains("missing field `type`")),
790        }
791    }
792
793    #[cfg(feature = "resolver")]
794    #[test]
795    fn chained_store_config_error_message() {
796        match toml::from_str::<Config>(
797            r#"[[zones]]
798               zone = "."
799               zone_type = "External"
800
801               [[zones.stores]]
802               type = "forward"
803
804               [[zones.stores.name_servers]]
805               socket_addr = "8.8.8.8:53"
806               protocol = "udp"
807               trust_negative_responses = false
808
809               [[zones.stores]]
810               type = "forward"
811
812               [[zones.stores.name_servers]]
813               socket_addr = "1.1.1.1:53"
814               rotocol = "udp"
815               trust_negative_responses = false"#,
816        ) {
817            Ok(val) => panic!("expected error value; got ok: {val:?}"),
818            Err(e) => assert!(dbg!(e).to_string().contains("unknown field `rotocol`")),
819        }
820    }
821
822    #[cfg(feature = "resolver")]
823    #[test]
824    fn file_store_zone_file_path() {
825        match toml::from_str::<Config>(
826            r#"[[zones]]
827               zone = "localhost"
828               zone_type = "Primary"
829
830               [zones.stores]
831               type = "file"
832               zone_file_path = "default/localhost.zone""#,
833        ) {
834            Ok(val) => {
835                let ZoneTypeConfig::Primary(config) = &val.zones[0].zone_type_config else {
836                    panic!("expected primary zone type");
837                };
838
839                assert_eq!(config.stores.len(), 1);
840                assert!(matches!(
841                        &config.stores[0],
842                    ServerStoreConfig::File(FileConfig { zone_file_path }) if zone_file_path == Path::new("default/localhost.zone"),
843                ));
844            }
845            Err(e) => panic!("expected successful parse: {e:?}"),
846        }
847    }
848}