hickory_server/store/
recursor.rs

1// Copyright 2015-2022 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#![cfg(feature = "recursor")]
9
10//! Recursive resolver related types
11
12#[cfg(feature = "__dnssec")]
13use std::sync::Arc;
14use std::{
15    borrow::Cow,
16    collections::HashSet,
17    fs::File,
18    io::{self, Read},
19    net::SocketAddr,
20    path::{Path, PathBuf},
21    time::Instant,
22};
23
24use ipnet::IpNet;
25use serde::Deserialize;
26use tracing::{debug, info};
27
28#[cfg(feature = "__dnssec")]
29use crate::{authority::Nsec3QueryInfo, dnssec::NxProofKind, proto::dnssec::TrustAnchors};
30use crate::{
31    authority::{
32        Authority, LookupControlFlow, LookupError, LookupObject, LookupOptions, MessageRequest,
33        UpdateResult, ZoneType,
34    },
35    error::ConfigError,
36    proto::{
37        op::{Query, ResponseCode},
38        rr::{LowerName, Name, RData, Record, RecordSet, RecordType},
39        serialize::txt::{ParseError, Parser},
40        xfer::Protocol,
41    },
42    recursor::{DnssecPolicy, Recursor},
43    resolver::{
44        config::{NameServerConfig, NameServerConfigGroup},
45        dns_lru::TtlConfig,
46        lookup::Lookup,
47    },
48    server::RequestInfo,
49};
50
51/// An authority that performs recursive resolutions.
52///
53/// This uses the hickory-recursor crate for resolving requests.
54pub struct RecursiveAuthority {
55    origin: LowerName,
56    recursor: Recursor,
57}
58
59impl RecursiveAuthority {
60    /// Read the Authority for the origin from the specified configuration
61    pub async fn try_from_config(
62        origin: Name,
63        _zone_type: ZoneType,
64        config: &RecursiveConfig,
65        root_dir: Option<&Path>,
66    ) -> Result<Self, String> {
67        info!("loading recursor config: {}", origin);
68
69        // read the roots
70        let root_addrs = config
71            .read_roots(root_dir)
72            .map_err(|e| format!("failed to read roots {}: {}", config.roots.display(), e))?;
73
74        // Configure all the name servers
75        let mut roots = NameServerConfigGroup::new();
76        for socket_addr in root_addrs {
77            roots.push(NameServerConfig {
78                socket_addr,
79                protocol: Protocol::Tcp,
80                tls_dns_name: None,
81                http_endpoint: None,
82                trust_negative_responses: false,
83                bind_addr: None, // TODO: need to support bind addresses
84            });
85
86            roots.push(NameServerConfig {
87                socket_addr,
88                protocol: Protocol::Udp,
89                tls_dns_name: None,
90                http_endpoint: None,
91                trust_negative_responses: false,
92                bind_addr: None,
93            });
94        }
95
96        let mut builder = Recursor::builder();
97        if let Some(ns_cache_size) = config.ns_cache_size {
98            builder = builder.ns_cache_size(ns_cache_size);
99        }
100        if let Some(record_cache_size) = config.record_cache_size {
101            builder = builder.record_cache_size(record_cache_size);
102        }
103
104        let recursor = builder
105            .dnssec_policy(config.dnssec_policy.load().map_err(|e| e.to_string())?)
106            .nameserver_filter(config.allow_server.iter(), config.deny_server.iter())
107            .recursion_limit(match config.recursion_limit {
108                0 => None,
109                limit => Some(limit),
110            })
111            .ns_recursion_limit(match config.ns_recursion_limit {
112                0 => None,
113                limit => Some(limit),
114            })
115            .avoid_local_udp_ports(config.avoid_local_udp_ports.clone())
116            .ttl_config(config.cache_policy.clone())
117            .case_randomization(config.case_randomization)
118            .build(roots)
119            .map_err(|e| format!("failed to initialize recursor: {e}"))?;
120
121        Ok(Self {
122            origin: origin.into(),
123            recursor,
124        })
125    }
126}
127
128#[async_trait::async_trait]
129impl Authority for RecursiveAuthority {
130    type Lookup = RecursiveLookup;
131
132    /// Always External
133    fn zone_type(&self) -> ZoneType {
134        ZoneType::External
135    }
136
137    /// Always false for Forward zones
138    fn is_axfr_allowed(&self) -> bool {
139        false
140    }
141
142    fn can_validate_dnssec(&self) -> bool {
143        self.recursor.is_validating()
144    }
145
146    async fn update(&self, _update: &MessageRequest) -> UpdateResult<bool> {
147        Err(ResponseCode::NotImp)
148    }
149
150    /// Get the origin of this zone, i.e. example.com is the origin for www.example.com
151    ///
152    /// In the context of a forwarder, this is either a zone which this forwarder is associated,
153    ///   or `.`, the root zone for all zones. If this is not the root zone, then it will only forward
154    ///   for lookups which match the given zone name.
155    fn origin(&self) -> &LowerName {
156        &self.origin
157    }
158
159    /// Forwards a lookup given the resolver configuration for this Forwarded zone
160    async fn lookup(
161        &self,
162        name: &LowerName,
163        rtype: RecordType,
164        lookup_options: LookupOptions,
165    ) -> LookupControlFlow<Self::Lookup> {
166        debug!("recursive lookup: {} {}", name, rtype);
167
168        let query = Query::query(name.into(), rtype);
169        let now = Instant::now();
170
171        let result = self
172            .recursor
173            .resolve(query, now, lookup_options.dnssec_ok())
174            .await;
175
176        use LookupControlFlow::*;
177        match result {
178            Ok(lookup) => Continue(Ok(RecursiveLookup(lookup))),
179            Err(error) => Continue(Err(LookupError::from(error))),
180        }
181    }
182
183    async fn search(
184        &self,
185        request_info: RequestInfo<'_>,
186        lookup_options: LookupOptions,
187    ) -> LookupControlFlow<Self::Lookup> {
188        self.lookup(
189            request_info.query.name(),
190            request_info.query.query_type(),
191            lookup_options,
192        )
193        .await
194    }
195
196    async fn get_nsec_records(
197        &self,
198        _name: &LowerName,
199        _lookup_options: LookupOptions,
200    ) -> LookupControlFlow<Self::Lookup> {
201        LookupControlFlow::Continue(Err(LookupError::from(io::Error::new(
202            io::ErrorKind::Other,
203            "Getting NSEC records is unimplemented for the recursor",
204        ))))
205    }
206
207    #[cfg(feature = "__dnssec")]
208    async fn get_nsec3_records(
209        &self,
210        _info: Nsec3QueryInfo<'_>,
211        _lookup_options: LookupOptions,
212    ) -> LookupControlFlow<Self::Lookup> {
213        LookupControlFlow::Continue(Err(LookupError::from(io::Error::new(
214            io::ErrorKind::Other,
215            "getting NSEC3 records is unimplemented for the recursor",
216        ))))
217    }
218
219    #[cfg(feature = "__dnssec")]
220    fn nx_proof_kind(&self) -> Option<&NxProofKind> {
221        None
222    }
223}
224
225/// A Lookup object for the recursive resolver
226pub struct RecursiveLookup(Lookup);
227
228impl LookupObject for RecursiveLookup {
229    fn is_empty(&self) -> bool {
230        self.0.is_empty()
231    }
232
233    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Record> + Send + 'a> {
234        Box::new(self.0.record_iter())
235    }
236
237    fn take_additionals(&mut self) -> Option<Box<dyn LookupObject>> {
238        None
239    }
240}
241
242/// Configuration for file based zones
243#[derive(Clone, Deserialize, Eq, PartialEq, Debug)]
244#[serde(deny_unknown_fields)]
245pub struct RecursiveConfig {
246    /// File with roots, aka hints
247    pub roots: PathBuf,
248
249    /// Maximum nameserver cache size
250    pub ns_cache_size: Option<usize>,
251
252    /// Maximum DNS record cache size
253    pub record_cache_size: Option<usize>,
254
255    /// Maximum recursion depth for queries. Set to 0 for unlimited recursion depth.
256    #[serde(default = "recursion_limit_default")]
257    pub recursion_limit: u8,
258
259    /// Maximum recursion depth for building NS pools. Set to 0 for unlimited recursion depth.
260    #[serde(default = "ns_recursion_limit_default")]
261    pub ns_recursion_limit: u8,
262
263    /// DNSSEC policy
264    #[serde(default)]
265    pub dnssec_policy: DnssecPolicyConfig,
266
267    /// Networks that will be queried during resolution
268    #[serde(default)]
269    pub allow_server: Vec<IpNet>,
270
271    /// Networks that will not be queried during resolution
272    #[serde(default)]
273    pub deny_server: Vec<IpNet>,
274
275    /// Local UDP ports to avoid when making outgoing queries
276    #[serde(default)]
277    pub avoid_local_udp_ports: HashSet<u16>,
278
279    /// Caching policy, setting minimum and maximum TTLs
280    #[serde(default)]
281    pub cache_policy: TtlConfig,
282
283    /// Enable case randomization.
284    ///
285    /// Randomize the case of letters in query names, and require that responses preserve the case
286    /// of the query name, in order to mitigate spoofing attacks. This is only applied over UDP.
287    ///
288    /// This implements the mechanism described in
289    /// [draft-vixie-dnsext-dns0x20-00](https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00).
290    #[serde(default)]
291    pub case_randomization: bool,
292}
293
294impl RecursiveConfig {
295    pub(crate) fn read_roots(
296        &self,
297        root_dir: Option<&Path>,
298    ) -> Result<Vec<SocketAddr>, ConfigError> {
299        let path = if let Some(root_dir) = root_dir {
300            Cow::Owned(root_dir.join(&self.roots))
301        } else {
302            Cow::Borrowed(&self.roots)
303        };
304
305        let mut roots = File::open(path.as_ref())?;
306        let mut roots_str = String::new();
307        roots.read_to_string(&mut roots_str)?;
308
309        let (_zone, roots_zone) =
310            Parser::new(roots_str, Some(path.into_owned()), Some(Name::root())).parse()?;
311
312        // TODO: we may want to deny some of the root nameservers, for reasons...
313        Ok(roots_zone
314            .values()
315            .flat_map(RecordSet::records_without_rrsigs)
316            .map(Record::data)
317            .filter_map(RData::ip_addr) // we only want IPs
318            .map(|ip| SocketAddr::from((ip, 53))) // all the roots only have tradition DNS ports
319            .collect())
320    }
321}
322
323fn recursion_limit_default() -> u8 {
324    12
325}
326
327fn ns_recursion_limit_default() -> u8 {
328    16
329}
330
331/// DNSSEC policy configuration
332#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
333#[serde(deny_unknown_fields)]
334#[allow(missing_copy_implementations)]
335pub enum DnssecPolicyConfig {
336    /// security unaware; DNSSEC records will not be requested nor processed
337    #[default]
338    SecurityUnaware,
339
340    /// DNSSEC validation is disabled; DNSSEC records will be requested and processed
341    #[cfg(feature = "__dnssec")]
342    ValidationDisabled,
343
344    /// DNSSEC validation is enabled and will use the chosen `trust_anchor` set of keys
345    #[cfg(feature = "__dnssec")]
346    ValidateWithStaticKey {
347        /// set to `None` to use built-in trust anchor
348        path: Option<PathBuf>,
349    },
350}
351
352impl DnssecPolicyConfig {
353    pub(crate) fn load(&self) -> Result<DnssecPolicy, ParseError> {
354        Ok(match self {
355            Self::SecurityUnaware => DnssecPolicy::SecurityUnaware,
356            #[cfg(feature = "__dnssec")]
357            Self::ValidationDisabled => DnssecPolicy::ValidationDisabled,
358            #[cfg(feature = "__dnssec")]
359            Self::ValidateWithStaticKey { path } => DnssecPolicy::ValidateWithStaticKey {
360                trust_anchor: path
361                    .as_ref()
362                    .map(|path| TrustAnchors::from_file(path))
363                    .transpose()?
364                    .map(Arc::new),
365            },
366        })
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    #[cfg(all(feature = "__dnssec", feature = "toml"))]
373    use super::*;
374
375    #[cfg(all(feature = "__dnssec", feature = "toml"))]
376    #[test]
377    fn can_parse_recursive_config() {
378        let input = r#"roots = "/etc/root.hints"
379dnssec_policy.ValidateWithStaticKey.path = "/etc/trusted-key.key""#;
380
381        let config: RecursiveConfig = toml::from_str(input).unwrap();
382
383        if let DnssecPolicyConfig::ValidateWithStaticKey { path } = config.dnssec_policy {
384            assert_eq!(Some(Path::new("/etc/trusted-key.key")), path.as_deref());
385        } else {
386            unreachable!()
387        }
388    }
389
390    #[cfg(all(feature = "recursor", feature = "toml"))]
391    #[test]
392    fn can_parse_recursor_cache_policy() {
393        use std::time::Duration;
394
395        use hickory_proto::rr::RecordType;
396
397        let input = r#"roots = "/etc/root.hints"
398
399[cache_policy.default]
400positive_max_ttl = 14400
401
402[cache_policy.A]
403positive_max_ttl = 3600"#;
404
405        let config: RecursiveConfig = toml::from_str(input).unwrap();
406
407        assert_eq!(
408            *config
409                .cache_policy
410                .positive_response_ttl_bounds(RecordType::MX)
411                .end(),
412            Duration::from_secs(14400)
413        );
414
415        assert_eq!(
416            *config
417                .cache_policy
418                .positive_response_ttl_bounds(RecordType::A)
419                .end(),
420            Duration::from_secs(3600)
421        )
422    }
423}