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