Skip to main content

mhost/resolver/
mod.rs

1// Copyright 2017-2021 Lukas Pustina <lukas@pustina.de>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! DNS resolver abstractions for concurrent multi-server lookups.
9//!
10//! This module provides [`Resolver`] for querying a single nameserver and
11//! [`ResolverGroup`] for fanning out queries across multiple nameservers concurrently.
12//! Use [`ResolverGroupBuilder`] for ergonomic construction, or build resolvers manually
13//! via [`ResolverGroup::from_configs`] and [`ResolverGroup::from_system_config`].
14//!
15//! Queries are expressed as [`UniQuery`] (single name + record type) or [`MultiQuery`]
16//! (multiple names and/or record types). Results are returned as [`Lookups`], which
17//! provides typed accessors (`.a()`, `.mx()`, `.txt()`, etc.) and deduplication via
18//! the [`Uniquify`](lookup::Uniquify) trait.
19
20use std::path::Path;
21use std::sync::Arc;
22use std::time::Duration;
23
24use futures::future::join_all;
25use futures::stream::{self, StreamExt};
26use futures::Future;
27use rand::seq::IndexedRandom;
28use tokio::task;
29use tracing::{debug, instrument};
30
31pub use builder::ResolverGroupBuilder;
32pub use error::Error;
33pub use lookup::{Lookup, Lookups};
34pub use query::{MultiQuery, UniQuery};
35
36use crate::nameserver::{NameServerConfig, NameServerConfigGroup};
37use crate::system_config;
38use crate::Result;
39use std::fmt::Display;
40use std::fmt::Formatter;
41use std::str::FromStr;
42
43pub mod builder;
44pub mod delegation;
45pub mod error;
46pub mod lookup;
47pub mod predefined;
48pub mod query;
49pub mod raw;
50
51pub type ResolverResult<T> = std::result::Result<T, Error>;
52
53/// Configuration for a single [`Resolver`], wrapping a [`NameServerConfig`].
54#[derive(Debug)]
55pub struct ResolverConfig {
56    name_server_config: NameServerConfig,
57}
58
59impl ResolverConfig {
60    pub fn new(name_server_config: NameServerConfig) -> Self {
61        ResolverConfig { name_server_config }
62    }
63}
64
65impl From<NameServerConfig> for ResolverConfig {
66    fn from(ns_config: NameServerConfig) -> Self {
67        ResolverConfig {
68            name_server_config: ns_config,
69        }
70    }
71}
72
73/// Per-resolver options controlling timeout, retries, and query behavior.
74///
75/// Defaults are suitable for most use cases. Use [`from_system_config`](Self::from_system_config)
76/// to derive options from the operating system's resolver configuration.
77#[derive(Debug, Clone)]
78pub struct ResolverOpts {
79    pub retries: usize,
80    /// Maximum number of concurrent queries sent with this resolver.
81    pub max_concurrent_requests: usize,
82    pub ndots: usize,
83    pub preserve_intermediates: bool,
84    /// cf. `hickory_resolver::proto::xfer::DnsRequestOptions`
85    pub expects_multiple_responses: bool,
86    pub timeout: Duration,
87    pub abort_on_error: bool,
88    pub abort_on_timeout: bool,
89}
90
91impl ResolverOpts {
92    /// Creates `ResolverOpts` from local system configuration.
93    ///
94    /// Unix: Parses `/etc/resolv.conf`.
95    pub fn from_system_config() -> Result<ResolverOpts> {
96        let opts = system_config::load_from_system_config()?;
97        Ok(opts)
98    }
99
100    pub fn from_system_config_path<P: AsRef<Path>>(path: P) -> Result<ResolverOpts> {
101        let opts = system_config::load_from_system_config_path(path)?;
102        Ok(opts)
103    }
104}
105
106impl Default for ResolverOpts {
107    fn default() -> Self {
108        ResolverOpts {
109            retries: 1,
110            max_concurrent_requests: 5,
111            ndots: 1,
112            preserve_intermediates: false,
113            expects_multiple_responses: false,
114            timeout: Duration::from_secs(5),
115            abort_on_error: true,
116            abort_on_timeout: true,
117        }
118    }
119}
120
121/// A collection of [`ResolverConfig`]s, typically used to create a [`ResolverGroup`].
122#[derive(Debug)]
123pub struct ResolverConfigGroup {
124    configs: Vec<ResolverConfig>,
125}
126
127impl ResolverConfigGroup {
128    pub fn new(resolver_configs: Vec<ResolverConfig>) -> ResolverConfigGroup {
129        ResolverConfigGroup {
130            configs: resolver_configs,
131        }
132    }
133
134    /// Merges this `ResolverConfigGroup` with another
135    pub fn merge(&mut self, other: Self) {
136        self.configs.extend(other.configs)
137    }
138}
139
140impl From<NameServerConfigGroup> for ResolverConfigGroup {
141    fn from(configs: NameServerConfigGroup) -> Self {
142        let resolver_confings: Vec<_> = configs.into_iter().map(From::from).collect();
143        ResolverConfigGroup::new(resolver_confings)
144    }
145}
146
147impl IntoIterator for ResolverConfigGroup {
148    type Item = ResolverConfig;
149    type IntoIter = std::vec::IntoIter<Self::Item>;
150
151    fn into_iter(self) -> Self::IntoIter {
152        self.configs.into_iter()
153    }
154}
155
156/// A DNS resolver bound to a single nameserver.
157///
158/// Wraps a [`hickory_resolver::TokioResolver`] and tracks the associated
159/// [`NameServerConfig`] and [`ResolverOpts`]. Cloning is cheap (inner state is `Arc`-wrapped).
160#[derive(Debug, Clone)]
161pub struct Resolver {
162    pub(crate) inner: Arc<hickory_resolver::TokioResolver>,
163    pub(crate) opts: Arc<ResolverOpts>,
164    pub(crate) name_server: Arc<NameServerConfig>,
165}
166
167impl Resolver {
168    /// Creates a new Resolver.
169    ///
170    /// May panic if underlying tasks panic
171    #[instrument(name =  "create resolver", level = "info", skip(config, opts), fields(server = %config.name_server_config))]
172    pub async fn new(config: ResolverConfig, opts: ResolverOpts) -> ResolverResult<Self> {
173        let name_server = config.name_server_config.clone();
174        let tr_opts = opts.clone().into();
175        let tr_config: hickory_resolver::config::ResolverConfig = config.into();
176        let tr_resolver = hickory_resolver::Resolver::builder_with_config(
177            tr_config,
178            hickory_resolver::name_server::TokioConnectionProvider::default(),
179        )
180        .with_options(tr_opts)
181        .build();
182
183        Ok(Resolver {
184            inner: Arc::new(tr_resolver),
185            opts: Arc::new(opts),
186            name_server: Arc::new(name_server),
187        })
188    }
189
190    pub async fn lookup<T: Into<MultiQuery>>(&self, query: T) -> ResolverResult<Lookups> {
191        lookup::lookup(self.clone(), query).await
192    }
193
194    pub fn name(&self) -> String {
195        self.name_server.to_string()
196    }
197}
198
199/// Group-level options for a [`ResolverGroup`].
200#[derive(Debug, Clone)]
201pub struct ResolverGroupOpts {
202    /// Maximum number of resolvers queried concurrently.
203    pub max_concurrent: usize,
204    /// Optional limit on the number of resolvers used per query.
205    pub limit: Option<usize>,
206    /// Lookup mode: [`Multi`](Mode::Multi) fans out to all resolvers,
207    /// [`Uni`](Mode::Uni) picks one at random per query.
208    pub mode: Mode,
209}
210
211/// Lookup mode controlling how queries are distributed across resolvers.
212#[derive(Debug, Eq, PartialEq, Clone, Copy)]
213pub enum Mode {
214    /// Send each query to all available resolvers and aggregate results.
215    Multi,
216    /// Send each query to a single randomly chosen resolver.
217    Uni,
218}
219
220impl Display for Mode {
221    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
222        match self {
223            Mode::Multi => write!(f, "multi"),
224            Mode::Uni => write!(f, "uni"),
225        }
226    }
227}
228
229impl FromStr for Mode {
230    type Err = crate::error::Error;
231
232    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
233        match s {
234            "multi" => Ok(Mode::Multi),
235            "uni" => Ok(Mode::Uni),
236            _ => Err(crate::error::Error::ParserError {
237                what: s.to_string(),
238                to: "Mode",
239                why: "no such mode".to_string(),
240            }),
241        }
242    }
243}
244
245impl Default for ResolverGroupOpts {
246    fn default() -> Self {
247        ResolverGroupOpts {
248            max_concurrent: 10,
249            limit: None,
250            mode: Mode::Multi,
251        }
252    }
253}
254
255/// A group of DNS [`Resolver`]s that fans out queries concurrently.
256///
257/// A `ResolverGroup` queries multiple nameservers in parallel and collects
258/// the results into [`Lookups`]. Use [`ResolverGroupBuilder`] for ergonomic
259/// construction, or create one directly via [`from_configs`](Self::from_configs)
260/// or [`from_system_config`](Self::from_system_config).
261#[derive(Debug)]
262pub struct ResolverGroup {
263    pub(crate) resolvers: Vec<Resolver>,
264    pub(crate) opts: ResolverGroupOpts,
265}
266
267impl ResolverGroup {
268    pub fn new<T: Into<Vec<Resolver>>>(resolvers: T, opts: ResolverGroupOpts) -> Self {
269        ResolverGroup {
270            resolvers: resolvers.into(),
271            opts,
272        }
273    }
274
275    pub async fn from_configs<T: IntoIterator<Item = ResolverConfig>>(
276        configs: T,
277        resolver_opts: ResolverOpts,
278        opts: ResolverGroupOpts,
279    ) -> Result<Self> {
280        // Create resolver futures
281        let futures: Vec<_> = configs
282            .into_iter()
283            .map(|config| Resolver::new(config, resolver_opts.clone()))
284            .collect();
285
286        // Wait all futures
287        let resolvers: ResolverResult<Vec<_>> = join_all(futures).await.drain(..).collect();
288
289        // Check for Err
290        let resolvers = resolvers?;
291
292        Ok(Self::new(resolvers, opts))
293    }
294
295    pub async fn from_system_config(opts: ResolverGroupOpts) -> Result<Self> {
296        let resolver_opts = ResolverOpts::from_system_config()?;
297        let configs: ResolverConfigGroup = NameServerConfigGroup::from_system_config()?.into();
298        ResolverGroup::from_configs(configs, resolver_opts, opts).await
299    }
300
301    pub async fn lookup<T: Into<MultiQuery>>(&self, query: T) -> ResolverResult<Lookups> {
302        match self.opts.mode {
303            Mode::Multi => self.multi_lookup(query).await,
304            Mode::Uni => self.uni_lookup(query).await,
305        }
306    }
307
308    async fn multi_lookup<T: Into<MultiQuery>>(&self, query: T) -> ResolverResult<Lookups> {
309        let multi_query = query.into();
310        let mut resolvers = self.resolvers.clone();
311
312        let lookup_futures: Vec<_> = resolvers
313            .drain(..)
314            .take(self.opts.limit.unwrap_or(self.resolvers.len()))
315            .map(|resolver| lookup::lookup(resolver, multi_query.clone()))
316            .collect();
317        let lookups = sliding_window_lookups(lookup_futures, self.opts.max_concurrent);
318        let lookups = task::spawn(lookups).await?;
319
320        Ok(lookups)
321    }
322
323    async fn uni_lookup<T: Into<MultiQuery>>(&self, query: T) -> ResolverResult<Lookups> {
324        if self.resolvers.is_empty() {
325            return Err(Error::ResolveError {
326                reason: "no resolvers available".to_string(),
327            });
328        }
329
330        let mut rng = rand::rng();
331        let multi_query = query.into();
332        let resolvers = self.resolvers.as_slice();
333
334        let lookup_futures: Vec<_> = multi_query
335            .into_uni_queries()
336            .drain(..)
337            .map(|q| {
338                let resolver = resolvers.choose(&mut rng).expect("resolvers is non-empty");
339                lookup::lookup(resolver.clone(), q)
340            })
341            .collect();
342        let lookups = sliding_window_lookups(lookup_futures, self.opts.max_concurrent);
343        let lookups = task::spawn(lookups).await?;
344
345        Ok(lookups)
346    }
347
348    /// Merges this `ResolverGroup` with another
349    ///
350    /// Attention: the `ResolverGroupOpts` of this `ResolverGroup` will apply
351    pub fn merge(&mut self, other: Self) {
352        self.resolvers.extend(other.resolvers)
353    }
354
355    pub fn add(&mut self, resolver: Resolver) {
356        self.resolvers.push(resolver)
357    }
358
359    pub fn set_opts(&mut self, opts: ResolverGroupOpts) {
360        self.opts = opts
361    }
362
363    pub fn opts(&self) -> &ResolverGroupOpts {
364        &self.opts
365    }
366
367    pub fn resolvers(&self) -> &[Resolver] {
368        self.resolvers.as_slice()
369    }
370
371    pub fn len(&self) -> usize {
372        self.resolvers.len()
373    }
374
375    pub fn is_empty(&self) -> bool {
376        self.resolvers.is_empty()
377    }
378}
379
380async fn sliding_window_lookups(
381    futures: Vec<impl Future<Output = ResolverResult<Lookups>>>,
382    max_concurrent: usize,
383) -> Lookups {
384    let results: Vec<_> = stream::iter(futures)
385        .buffer_unordered(max_concurrent)
386        .collect::<Vec<_>>()
387        .await;
388
389    let mut error_count = 0usize;
390    let lookups: Vec<Lookup> = results
391        .into_iter()
392        .filter_map(|r| match r {
393            Ok(lookups) => Some(lookups),
394            Err(e) => {
395                error_count += 1;
396                debug!("Resolver lookup failed: {}", e);
397                None
398            }
399        })
400        .flatten()
401        .collect();
402
403    if error_count > 0 && lookups.is_empty() {
404        debug!("All {} resolver lookups failed with no results", error_count);
405    }
406
407    lookups.into()
408}
409
410#[doc(hidden)]
411impl From<resolv_conf::Config> for ResolverOpts {
412    fn from(config: resolv_conf::Config) -> Self {
413        ResolverOpts {
414            retries: config.attempts as usize,
415            ndots: config.ndots as usize,
416            timeout: Duration::from_secs(config.timeout as u64),
417            ..Default::default()
418        }
419    }
420}
421
422#[doc(hidden)]
423impl From<ResolverOpts> for hickory_resolver::config::ResolverOpts {
424    fn from(opts: ResolverOpts) -> Self {
425        let mut resolver_opts = hickory_resolver::config::ResolverOpts::default();
426        resolver_opts.attempts = opts.retries;
427        resolver_opts.ndots = opts.ndots;
428        resolver_opts.num_concurrent_reqs = opts.max_concurrent_requests;
429        resolver_opts.preserve_intermediates = opts.preserve_intermediates;
430        resolver_opts.timeout = opts.timeout;
431        resolver_opts
432    }
433}
434
435#[cfg(test)]
436impl Resolver {
437    pub fn new_for_test(opts: ResolverOpts, name_server: NameServerConfig) -> Self {
438        let config = ResolverConfig::new(name_server.clone());
439        let tr_opts: hickory_resolver::config::ResolverOpts = opts.clone().into();
440        let tr_config: hickory_resolver::config::ResolverConfig = config.into();
441        let tr_resolver = hickory_resolver::Resolver::builder_with_config(
442            tr_config,
443            hickory_resolver::name_server::TokioConnectionProvider::default(),
444        )
445        .with_options(tr_opts)
446        .build();
447
448        Resolver {
449            inner: Arc::new(tr_resolver),
450            opts: Arc::new(opts),
451            name_server: Arc::new(name_server),
452        }
453    }
454}
455
456#[doc(hidden)]
457impl From<ResolverConfig> for hickory_resolver::config::ResolverConfig {
458    fn from(rc: ResolverConfig) -> Self {
459        let mut config = Self::new();
460        config.add_name_server(rc.name_server_config.into());
461
462        config
463    }
464}