Skip to main content

hickory_server/store/
file.rs

1// Copyright 2015-2019 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//! Zone file based serving with Dynamic DNS and journaling support
9
10use std::{
11    ops::{Deref, DerefMut},
12    path::{Path, PathBuf},
13};
14
15#[cfg(feature = "metrics")]
16use crate::metrics::PersistentStoreMetrics;
17#[cfg(feature = "__dnssec")]
18use crate::{
19    dnssec::NxProofKind,
20    proto::dnssec::{DnsSecResult, DnssecSigner},
21    zone_handler::{DnssecZoneHandler, Nsec3QueryInfo},
22};
23use crate::{
24    proto::rr::{LowerName, Name, RecordType},
25    server::{Request, RequestInfo},
26    store::in_memory::{InMemoryZoneHandler, zone_from_path},
27    zone_handler::{
28        AuthLookup, AxfrPolicy, LookupControlFlow, LookupError, LookupOptions, ZoneHandler,
29        ZoneTransfer, ZoneType,
30    },
31};
32use hickory_proto::rr::TSigResponseContext;
33use serde::Deserialize;
34
35/// FileZoneHandler is responsible for storing the resource records for a particular zone.
36///
37/// Zone handlers default to DNSClass IN. The ZoneType specifies if this should be treated as the
38/// start of authority for the zone, is a Secondary, or a cached zone.
39pub struct FileZoneHandler {
40    in_memory: InMemoryZoneHandler,
41    #[cfg(feature = "metrics")]
42    #[allow(unused)]
43    metrics: PersistentStoreMetrics,
44}
45
46impl FileZoneHandler {
47    /// Creates a new ZoneHandler.
48    ///
49    /// # Arguments
50    ///
51    /// * `origin` - The zone `Name` being created, this should match that of the `RecordType::SOA`
52    ///   record.
53    /// * `records` - The map of the initial set of records in the zone.
54    /// * `zone_type` - The type of zone, i.e. is this authoritative?
55    /// * `axfr_policy` - A policy for determining if AXFR is allowed.
56    /// * `nx_proof_kind` - The kind of non-existence proof to be used by the server.
57    ///
58    /// # Return value
59    ///
60    /// The new `ZoneHandler`.
61    pub async fn new(in_memory: InMemoryZoneHandler) -> Self {
62        Self {
63            #[cfg(feature = "metrics")]
64            metrics: {
65                let new = PersistentStoreMetrics::new("file");
66                let records = in_memory.records().await;
67                new.zone_records.increment(records.len() as f64);
68                new
69            },
70            in_memory,
71        }
72    }
73
74    /// Read the ZoneHandler for the origin from the specified configuration
75    pub fn try_from_config(
76        origin: Name,
77        zone_type: ZoneType,
78        axfr_policy: AxfrPolicy,
79        root_dir: Option<&Path>,
80        config: &FileConfig,
81        #[cfg(feature = "__dnssec")] nx_proof_kind: Option<NxProofKind>,
82    ) -> Result<Self, String> {
83        let zone_path = rooted(&config.zone_path, root_dir);
84        let records = zone_from_path(&zone_path, origin.clone())
85            .map_err(|e| format!("failed to load zone file: {e}"))?;
86
87        // Don't call `new()`, since it needs to be async to get the number of records to initialize metrics
88        Ok(Self {
89            #[cfg(feature = "metrics")]
90            metrics: {
91                let new = PersistentStoreMetrics::new("file");
92                new.zone_records.increment(records.len() as f64);
93                new
94            },
95            in_memory: InMemoryZoneHandler::new(
96                origin,
97                records,
98                zone_type,
99                axfr_policy,
100                #[cfg(feature = "__dnssec")]
101                nx_proof_kind,
102            )?,
103        })
104    }
105}
106
107impl Deref for FileZoneHandler {
108    type Target = InMemoryZoneHandler;
109
110    fn deref(&self) -> &Self::Target {
111        &self.in_memory
112    }
113}
114
115impl DerefMut for FileZoneHandler {
116    fn deref_mut(&mut self) -> &mut Self::Target {
117        &mut self.in_memory
118    }
119}
120
121#[async_trait::async_trait]
122impl ZoneHandler for FileZoneHandler {
123    /// What type is this zone
124    fn zone_type(&self) -> ZoneType {
125        self.in_memory.zone_type()
126    }
127
128    /// Return the policy for determining if AXFR requests are allowed
129    fn axfr_policy(&self) -> AxfrPolicy {
130        self.in_memory.axfr_policy()
131    }
132
133    /// Get the origin of this zone, i.e. example.com is the origin for www.example.com
134    fn origin(&self) -> &LowerName {
135        self.in_memory.origin()
136    }
137
138    /// Looks up all Resource Records matching the given `Name` and `RecordType`.
139    ///
140    /// # Arguments
141    ///
142    /// * `name` - The name to look up.
143    /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching
144    ///   `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA`
145    ///   due to the requirements that on zone transfers the `RecordType::SOA` must both
146    ///   precede and follow all other records.
147    /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash
148    ///   algorithms, etc.)
149    ///
150    /// # Return value
151    ///
152    /// A LookupControlFlow containing the lookup that should be returned to the client.
153    async fn lookup(
154        &self,
155        name: &LowerName,
156        rtype: RecordType,
157        request_info: Option<&RequestInfo<'_>>,
158        lookup_options: LookupOptions,
159    ) -> LookupControlFlow<AuthLookup> {
160        self.in_memory
161            .lookup(name, rtype, request_info, lookup_options)
162            .await
163    }
164
165    /// Using the specified query, perform a lookup against this zone.
166    ///
167    /// # Arguments
168    ///
169    /// * `request` - the query to perform the lookup with.
170    /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash
171    ///   algorithms, etc.)
172    ///
173    /// # Return value
174    ///
175    /// A LookupControlFlow containing the lookup that should be returned to the client.
176    async fn search(
177        &self,
178        request: &Request,
179        lookup_options: LookupOptions,
180    ) -> (LookupControlFlow<AuthLookup>, Option<TSigResponseContext>) {
181        self.in_memory.search(request, lookup_options).await
182    }
183
184    async fn zone_transfer(
185        &self,
186        request: &Request,
187        lookup_options: LookupOptions,
188        now: u64,
189    ) -> Option<(
190        Result<ZoneTransfer, LookupError>,
191        Option<TSigResponseContext>,
192    )> {
193        self.in_memory
194            .zone_transfer(request, lookup_options, now)
195            .await
196    }
197
198    /// Return the NSEC records based on the given name
199    ///
200    /// # Arguments
201    ///
202    /// * `name` - given this name (i.e. the lookup name), return the NSEC record that is less than
203    ///   this
204    /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash
205    ///   algorithms, etc.)
206    async fn nsec_records(
207        &self,
208        name: &LowerName,
209        lookup_options: LookupOptions,
210    ) -> LookupControlFlow<AuthLookup> {
211        self.in_memory.nsec_records(name, lookup_options).await
212    }
213
214    #[cfg(feature = "__dnssec")]
215    async fn nsec3_records(
216        &self,
217        info: Nsec3QueryInfo<'_>,
218        lookup_options: LookupOptions,
219    ) -> LookupControlFlow<AuthLookup> {
220        self.in_memory.nsec3_records(info, lookup_options).await
221    }
222
223    #[cfg(feature = "__dnssec")]
224    fn nx_proof_kind(&self) -> Option<&NxProofKind> {
225        self.in_memory.nx_proof_kind()
226    }
227
228    #[cfg(feature = "metrics")]
229    fn metrics_label(&self) -> &'static str {
230        "file"
231    }
232}
233
234#[cfg(feature = "__dnssec")]
235#[async_trait::async_trait]
236impl DnssecZoneHandler for FileZoneHandler {
237    /// Add Signer
238    async fn add_zone_signing_key(&self, signer: DnssecSigner) -> DnsSecResult<()> {
239        self.in_memory.add_zone_signing_key(signer).await
240    }
241
242    /// Sign the zone for DNSSEC
243    async fn secure_zone(&self) -> DnsSecResult<()> {
244        DnssecZoneHandler::secure_zone(&self.in_memory).await
245    }
246}
247
248/// Configuration for file based zones
249#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
250#[serde(deny_unknown_fields)]
251pub struct FileConfig {
252    /// path to the zone file
253    pub zone_path: PathBuf,
254}
255
256pub(crate) fn rooted(zone_file: &Path, root_dir: Option<&Path>) -> PathBuf {
257    match root_dir {
258        Some(root) => root.join(zone_file),
259        None => zone_file.to_owned(),
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use std::str::FromStr;
266
267    use crate::proto::rr::{RData, rdata::A};
268
269    use futures_executor::block_on;
270    use test_support::subscribe;
271
272    use super::*;
273    use crate::zone_handler::ZoneType;
274
275    #[test]
276    fn test_load_zone() {
277        subscribe();
278
279        #[cfg(feature = "__dnssec")]
280        let config = FileConfig {
281            zone_path: PathBuf::from("../../tests/test-data/test_configs/dnssec/example.com.zone"),
282        };
283        #[cfg(not(feature = "__dnssec"))]
284        let config = FileConfig {
285            zone_path: PathBuf::from("../../tests/test-data/test_configs/example.com.zone"),
286        };
287        let handler = FileZoneHandler::try_from_config(
288            Name::from_str("example.com.").unwrap(),
289            ZoneType::Primary,
290            AxfrPolicy::Deny,
291            None,
292            &config,
293            #[cfg(feature = "__dnssec")]
294            Some(NxProofKind::Nsec),
295        )
296        .expect("failed to load file");
297
298        let lookup = block_on(ZoneHandler::lookup(
299            &handler,
300            &LowerName::from_str("www.example.com.").unwrap(),
301            RecordType::A,
302            None,
303            LookupOptions::default(),
304        ))
305        .expect("lookup failed");
306
307        match lookup
308            .into_iter()
309            .next()
310            .expect("A record not found in zone handler")
311            .data
312        {
313            RData::A(ip) => assert_eq!(A::new(127, 0, 0, 1), ip),
314            _ => panic!("wrong rdata type returned"),
315        }
316
317        let include_lookup = block_on(ZoneHandler::lookup(
318            &handler,
319            &LowerName::from_str("include.alias.example.com.").unwrap(),
320            RecordType::A,
321            None,
322            LookupOptions::default(),
323        ))
324        .expect("INCLUDE lookup failed");
325
326        match include_lookup
327            .into_iter()
328            .next()
329            .expect("A record not found in zone handler")
330            .data
331        {
332            RData::A(ip) => assert_eq!(A::new(127, 0, 0, 5), ip),
333            _ => panic!("wrong rdata type returned"),
334        }
335    }
336}