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    collections::BTreeMap,
12    fs,
13    ops::{Deref, DerefMut},
14    path::{Path, PathBuf},
15};
16
17use serde::Deserialize;
18use tracing::{debug, info};
19
20#[cfg(feature = "metrics")]
21use crate::store::metrics::StoreMetrics;
22use crate::{
23    authority::{
24        Authority, LookupControlFlow, LookupOptions, MessageRequest, UpdateResult, ZoneType,
25    },
26    proto::rr::{LowerName, Name, RecordSet, RecordType, RrKey},
27    proto::serialize::txt::Parser,
28    server::RequestInfo,
29    store::in_memory::InMemoryAuthority,
30};
31#[cfg(feature = "__dnssec")]
32use crate::{
33    authority::{DnssecAuthority, Nsec3QueryInfo},
34    dnssec::NxProofKind,
35    proto::dnssec::{DnsSecResult, SigSigner, rdata::key::KEY},
36};
37
38/// FileAuthority is responsible for storing the resource records for a particular zone.
39///
40/// Authorities default to DNSClass IN. The ZoneType specifies if this should be treated as the
41/// start of authority for the zone, is a Secondary, or a cached zone.
42pub struct FileAuthority {
43    in_memory: InMemoryAuthority,
44    #[cfg(feature = "metrics")]
45    metrics: StoreMetrics,
46}
47
48impl FileAuthority {
49    /// Creates a new Authority.
50    ///
51    /// # Arguments
52    ///
53    /// * `origin` - The zone `Name` being created, this should match that of the `RecordType::SOA`
54    ///   record.
55    /// * `records` - The map of the initial set of records in the zone.
56    /// * `zone_type` - The type of zone, i.e. is this authoritative?
57    /// * `allow_axfr` - Whether AXFR is allowed.
58    /// * `nx_proof_kind` - The kind of non-existence proof to be used by the server.
59    ///
60    /// # Return value
61    ///
62    /// The new `Authority`.
63    pub fn new(
64        origin: Name,
65        records: BTreeMap<RrKey, RecordSet>,
66        zone_type: ZoneType,
67        allow_axfr: bool,
68        #[cfg(feature = "__dnssec")] nx_proof_kind: Option<NxProofKind>,
69    ) -> Result<Self, String> {
70        Ok(Self {
71            #[cfg(feature = "metrics")]
72            metrics: {
73                let new = StoreMetrics::new("file");
74                new.persistent.zone_records.increment(records.len() as f64);
75                new
76            },
77            in_memory: InMemoryAuthority::new(
78                origin,
79                records,
80                zone_type,
81                allow_axfr,
82                #[cfg(feature = "__dnssec")]
83                nx_proof_kind,
84            )?,
85        })
86    }
87
88    /// Read the Authority for the origin from the specified configuration
89    pub fn try_from_config(
90        origin: Name,
91        zone_type: ZoneType,
92        allow_axfr: bool,
93        root_dir: Option<&Path>,
94        config: &FileConfig,
95        #[cfg(feature = "__dnssec")] nx_proof_kind: Option<NxProofKind>,
96    ) -> Result<Self, String> {
97        Self::try_from_config_internal(
98            origin,
99            zone_type,
100            allow_axfr,
101            root_dir,
102            config,
103            #[cfg(feature = "__dnssec")]
104            nx_proof_kind,
105            #[cfg(feature = "metrics")]
106            false,
107        )
108    }
109
110    // internal load for e.g. sqlite db creation
111    pub(crate) fn try_from_config_internal(
112        origin: Name,
113        zone_type: ZoneType,
114        allow_axfr: bool,
115        root_dir: Option<&Path>,
116        config: &FileConfig,
117        #[cfg(feature = "__dnssec")] nx_proof_kind: Option<NxProofKind>,
118        #[cfg(feature = "metrics")] is_internal_load: bool,
119    ) -> Result<Self, String> {
120        let root_dir_path = root_dir.map(PathBuf::from).unwrap_or_default();
121        let zone_path = root_dir_path.join(&config.zone_file_path);
122
123        info!("loading zone file: {:?}", zone_path);
124
125        // TODO: this should really use something to read line by line or some other method to
126        //  keep the usage down. and be a custom lexer...
127        let buf = fs::read_to_string(&zone_path).map_err(|e| {
128            format!(
129                "failed to read {}: {:?}",
130                config.zone_file_path.display(),
131                e
132            )
133        })?;
134
135        let (origin, records) = Parser::new(buf, Some(zone_path), Some(origin))
136            .parse()
137            .map_err(|e| {
138                format!(
139                    "failed to parse {}: {:?}",
140                    config.zone_file_path.display(),
141                    e
142                )
143            })?;
144
145        info!(
146            "zone file loaded: {} with {} records",
147            origin,
148            records.len()
149        );
150        debug!("zone: {:#?}", records);
151
152        Ok(Self {
153            #[cfg(feature = "metrics")]
154            metrics: {
155                let new = StoreMetrics::new("file");
156                if !is_internal_load {
157                    new.persistent.zone_records.increment(records.len() as f64);
158                }
159                new
160            },
161            in_memory: InMemoryAuthority::new(
162                origin,
163                records,
164                zone_type,
165                allow_axfr,
166                #[cfg(feature = "__dnssec")]
167                nx_proof_kind,
168            )?,
169        })
170    }
171
172    /// Unwrap the InMemoryAuthority
173    pub fn unwrap(self) -> InMemoryAuthority {
174        self.in_memory
175    }
176}
177
178impl Deref for FileAuthority {
179    type Target = InMemoryAuthority;
180
181    fn deref(&self) -> &Self::Target {
182        &self.in_memory
183    }
184}
185
186impl DerefMut for FileAuthority {
187    fn deref_mut(&mut self) -> &mut Self::Target {
188        &mut self.in_memory
189    }
190}
191
192#[async_trait::async_trait]
193impl Authority for FileAuthority {
194    type Lookup = <InMemoryAuthority as Authority>::Lookup;
195
196    /// What type is this zone
197    fn zone_type(&self) -> ZoneType {
198        self.in_memory.zone_type()
199    }
200
201    /// Return true if AXFR is allowed
202    fn is_axfr_allowed(&self) -> bool {
203        self.in_memory.is_axfr_allowed()
204    }
205
206    /// Perform a dynamic update of a zone
207    async fn update(&self, _update: &MessageRequest) -> UpdateResult<bool> {
208        use crate::proto::op::ResponseCode;
209        Err(ResponseCode::NotImp)
210    }
211
212    /// Get the origin of this zone, i.e. example.com is the origin for www.example.com
213    fn origin(&self) -> &LowerName {
214        self.in_memory.origin()
215    }
216
217    /// Looks up all Resource Records matching the given `Name` and `RecordType`.
218    ///
219    /// # Arguments
220    ///
221    /// * `name` - The name to look up.
222    /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching
223    ///   `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA`
224    ///   due to the requirements that on zone transfers the `RecordType::SOA` must both
225    ///   precede and follow all other records.
226    /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash
227    ///   algorithms, etc.)
228    ///
229    /// # Return value
230    ///
231    /// A LookupControlFlow containing the lookup that should be returned to the client.
232    async fn lookup(
233        &self,
234        name: &LowerName,
235        rtype: RecordType,
236        lookup_options: LookupOptions,
237    ) -> LookupControlFlow<Self::Lookup> {
238        let lookup = self.in_memory.lookup(name, rtype, lookup_options).await;
239
240        #[cfg(feature = "metrics")]
241        self.metrics.query.increment_lookup(&lookup);
242
243        lookup
244    }
245
246    /// Using the specified query, perform a lookup against this zone.
247    ///
248    /// # Arguments
249    ///
250    /// * `request_info` - the query to perform the lookup with.
251    /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash
252    ///   algorithms, etc.)
253    ///
254    /// # Return value
255    ///
256    /// A LookupControlFlow containing the lookup that should be returned to the client.
257    async fn search(
258        &self,
259        request_info: RequestInfo<'_>,
260        lookup_options: LookupOptions,
261    ) -> LookupControlFlow<Self::Lookup> {
262        let search = self.in_memory.search(request_info, lookup_options).await;
263
264        #[cfg(feature = "metrics")]
265        self.metrics.query.increment_lookup(&search);
266
267        search
268    }
269
270    /// Get the NS, NameServer, record for the zone
271    async fn ns(&self, lookup_options: LookupOptions) -> LookupControlFlow<Self::Lookup> {
272        self.in_memory.ns(lookup_options).await
273    }
274
275    /// Return the NSEC records based on the given name
276    ///
277    /// # Arguments
278    ///
279    /// * `name` - given this name (i.e. the lookup name), return the NSEC record that is less than
280    ///   this
281    /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash
282    ///   algorithms, etc.)
283    async fn get_nsec_records(
284        &self,
285        name: &LowerName,
286        lookup_options: LookupOptions,
287    ) -> LookupControlFlow<Self::Lookup> {
288        self.in_memory.get_nsec_records(name, lookup_options).await
289    }
290
291    #[cfg(feature = "__dnssec")]
292    async fn get_nsec3_records(
293        &self,
294        info: Nsec3QueryInfo<'_>,
295        lookup_options: LookupOptions,
296    ) -> LookupControlFlow<Self::Lookup> {
297        self.in_memory.get_nsec3_records(info, lookup_options).await
298    }
299
300    /// Returns the SOA of the authority.
301    ///
302    /// *Note*: This will only return the SOA, if this is fulfilling a request, a standard lookup
303    ///  should be used, see `soa_secure()`, which will optionally return RRSIGs.
304    async fn soa(&self) -> LookupControlFlow<Self::Lookup> {
305        self.in_memory.soa().await
306    }
307
308    /// Returns the SOA record for the zone
309    async fn soa_secure(&self, lookup_options: LookupOptions) -> LookupControlFlow<Self::Lookup> {
310        self.in_memory.soa_secure(lookup_options).await
311    }
312
313    #[cfg(feature = "__dnssec")]
314    fn nx_proof_kind(&self) -> Option<&NxProofKind> {
315        self.in_memory.nx_proof_kind()
316    }
317}
318
319#[cfg(feature = "__dnssec")]
320#[async_trait::async_trait]
321impl DnssecAuthority for FileAuthority {
322    /// Add a (Sig0) key that is authorized to perform updates against this authority
323    async fn add_update_auth_key(&self, name: Name, key: KEY) -> DnsSecResult<()> {
324        self.in_memory.add_update_auth_key(name, key).await
325    }
326
327    /// Add Signer
328    async fn add_zone_signing_key(&self, signer: SigSigner) -> DnsSecResult<()> {
329        self.in_memory.add_zone_signing_key(signer).await
330    }
331
332    /// Sign the zone for DNSSEC
333    async fn secure_zone(&self) -> DnsSecResult<()> {
334        DnssecAuthority::secure_zone(&self.in_memory).await
335    }
336}
337
338/// Configuration for file based zones
339#[derive(Deserialize, PartialEq, Eq, Debug)]
340#[serde(deny_unknown_fields)]
341pub struct FileConfig {
342    /// path to the zone file
343    pub zone_file_path: PathBuf,
344}
345
346#[cfg(test)]
347mod tests {
348    use std::str::FromStr;
349
350    use crate::proto::rr::{RData, rdata::A};
351    use futures_executor::block_on;
352    use test_support::subscribe;
353
354    use super::*;
355    use crate::authority::ZoneType;
356
357    #[test]
358    fn test_load_zone() {
359        subscribe();
360
361        #[cfg(feature = "__dnssec")]
362        let config = FileConfig {
363            zone_file_path: PathBuf::from(
364                "../../tests/test-data/test_configs/dnssec/example.com.zone",
365            ),
366        };
367        #[cfg(not(feature = "__dnssec"))]
368        let config = FileConfig {
369            zone_file_path: PathBuf::from("../../tests/test-data/test_configs/example.com.zone"),
370        };
371        let authority = FileAuthority::try_from_config(
372            Name::from_str("example.com.").unwrap(),
373            ZoneType::Primary,
374            false,
375            None,
376            &config,
377            #[cfg(feature = "__dnssec")]
378            Some(NxProofKind::Nsec),
379        )
380        .expect("failed to load file");
381
382        let lookup = block_on(Authority::lookup(
383            &authority,
384            &LowerName::from_str("www.example.com.").unwrap(),
385            RecordType::A,
386            LookupOptions::default(),
387        ))
388        .expect("lookup failed");
389
390        match lookup
391            .into_iter()
392            .next()
393            .expect("A record not found in authority")
394            .data()
395        {
396            RData::A(ip) => assert_eq!(A::new(127, 0, 0, 1), *ip),
397            _ => panic!("wrong rdata type returned"),
398        }
399
400        let include_lookup = block_on(Authority::lookup(
401            &authority,
402            &LowerName::from_str("include.alias.example.com.").unwrap(),
403            RecordType::A,
404            LookupOptions::default(),
405        ))
406        .expect("INCLUDE lookup failed");
407
408        match include_lookup
409            .into_iter()
410            .next()
411            .expect("A record not found in authority")
412            .data()
413        {
414            RData::A(ip) => assert_eq!(A::new(127, 0, 0, 5), *ip),
415            _ => panic!("wrong rdata type returned"),
416        }
417    }
418}