Skip to main content

hickory_resolver/recursor/
error.rs

1// Copyright 2015-2020 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//! Error types for the crate
9
10#![deny(missing_docs)]
11
12use std::io;
13use std::sync::Arc;
14
15use thiserror::Error;
16use tracing::warn;
17
18use crate::{
19    net::{DnsError, ForwardNSData, NetError, NoRecords},
20    proto::{
21        ProtoError,
22        op::Query,
23        op::ResponseCode,
24        rr::{Name, Record, RecordType, rdata::SOA},
25    },
26};
27
28/// The error kind for errors that get returned in the crate
29#[derive(Debug, Error)]
30#[non_exhaustive]
31pub enum RecursorError {
32    /// Maximum record limit was exceeded
33    #[error("maximum record limit for {record_type} exceeded: {count} records")]
34    MaxRecordLimitExceeded {
35        /// number of records
36        count: usize,
37        /// The record type that triggered the error.
38        record_type: RecordType,
39    },
40
41    /// An error with an arbitrary message, referenced as &'static str
42    #[error("{0}")]
43    Message(&'static str),
44
45    /// An error with an arbitrary message, stored as String
46    #[error("{0}")]
47    Msg(String),
48
49    /// Upstream DNS authority returned an empty RRset
50    #[error("negative response")]
51    Negative(AuthorityData),
52
53    /// Upstream DNS authority returned a referral to another set of nameservers in the form of
54    /// additional NS records.
55    #[error("forward NS Response")]
56    ForwardNS(Arc<[ForwardNSData]>),
57
58    /// An error got returned from IO
59    #[error("io error: {0}")]
60    Io(#[from] io::Error),
61
62    /// An error got returned by the hickory-proto crate
63    #[error("net error: {0}")]
64    Net(NetError),
65
66    /// A request timed out
67    #[error("request timed out")]
68    Timeout,
69
70    /// Could not fetch all records because a recursion limit was exceeded
71    #[error("maximum recursion limit exceeded: {count} queries")]
72    RecursionLimitExceeded {
73        /// Number of queries that were made
74        count: usize,
75    },
76
77    /// The per-request upstream-query budget was exhausted before resolution completed.
78    #[error("per-request query budget exceeded")]
79    QueryBudgetExceeded,
80}
81
82impl RecursorError {
83    /// Test if the recursion depth has been exceeded, and return an error if it has.
84    pub fn recursion_exceeded(limit: u8, depth: u8, name: &Name) -> Result<(), Self> {
85        if depth < limit {
86            return Ok(());
87        }
88
89        warn!("recursion depth exceeded for {name}");
90        Err(Self::RecursionLimitExceeded {
91            count: depth as usize,
92        })
93    }
94
95    /// Returns the SOA record, if the error contains one
96    pub fn into_soa(self) -> Option<Box<Record<SOA>>> {
97        match self {
98            Self::Net(net) => net.into_soa(),
99            Self::Negative(fwd) => fwd.soa,
100            _ => None,
101        }
102    }
103
104    /// Returns true if no records were returned
105    pub fn is_no_records_found(&self) -> bool {
106        match self {
107            Self::Net(net) => net.is_no_records_found(),
108            Self::Negative(fwd) => fwd.is_no_records_found(),
109            _ => false,
110        }
111    }
112
113    /// Returns true if the domain does not exist
114    pub fn is_nx_domain(&self) -> bool {
115        match self {
116            Self::Net(net) => net.is_nx_domain(),
117            Self::Negative(fwd) => fwd.is_nx_domain(),
118            _ => false,
119        }
120    }
121
122    /// Returns true if a query timed out
123    pub fn is_timeout(&self) -> bool {
124        match self {
125            Self::Net(net) => matches!(net, NetError::Timeout),
126            _ => false,
127        }
128    }
129}
130
131impl From<NetError> for RecursorError {
132    fn from(e: NetError) -> Self {
133        let NetError::Dns(DnsError::NoRecordsFound(no_records)) = e else {
134            return Self::Net(e);
135        };
136
137        if let Some(ns) = no_records.ns {
138            Self::ForwardNS(ns)
139        } else {
140            Self::Negative(AuthorityData::new(
141                no_records.query,
142                no_records.soa,
143                true,
144                matches!(no_records.response_code, ResponseCode::NXDomain),
145                no_records.authorities,
146            ))
147        }
148    }
149}
150
151impl From<RecursorError> for NetError {
152    fn from(e: RecursorError) -> Self {
153        match e {
154            RecursorError::Negative(fwd) => DnsError::NoRecordsFound(fwd.into()).into(),
155            _ => Self::from(e.to_string()),
156        }
157    }
158}
159
160impl From<ProtoError> for RecursorError {
161    fn from(e: ProtoError) -> Self {
162        NetError::from(e).into()
163    }
164}
165
166impl From<String> for RecursorError {
167    fn from(msg: String) -> Self {
168        Self::Msg(msg)
169    }
170}
171
172impl From<&'static str> for RecursorError {
173    fn from(msg: &'static str) -> Self {
174        Self::Message(msg)
175    }
176}
177
178impl Clone for RecursorError {
179    fn clone(&self) -> Self {
180        use self::RecursorError::*;
181        match self {
182            MaxRecordLimitExceeded { count, record_type } => MaxRecordLimitExceeded {
183                count: *count,
184                record_type: *record_type,
185            },
186            Message(msg) => Message(msg),
187            Msg(msg) => Msg(msg.clone()),
188            Negative(ns) => Negative(ns.clone()),
189            ForwardNS(ns) => ForwardNS(ns.clone()),
190            Io(io) => Io(io::Error::from(io.kind())),
191            Net(net) => Net(net.clone()),
192            Timeout => Self::Timeout,
193            RecursionLimitExceeded { count } => RecursionLimitExceeded { count: *count },
194            QueryBudgetExceeded => QueryBudgetExceeded,
195        }
196    }
197}
198
199/// Data from the authority section of a response.
200#[derive(Clone, Debug)]
201pub struct AuthorityData {
202    /// Query
203    pub query: Box<Query>,
204    /// SOA
205    pub soa: Option<Box<Record<SOA>>>,
206    /// No records found?
207    no_records_found: bool,
208    /// IS nx domain?
209    nx_domain: bool,
210    /// Authority records
211    pub authorities: Option<Arc<[Record]>>,
212}
213
214impl AuthorityData {
215    /// Construct a new AuthorityData
216    pub fn new(
217        query: Box<Query>,
218        soa: Option<Box<Record<SOA>>>,
219        no_records_found: bool,
220        nx_domain: bool,
221        authorities: Option<Arc<[Record]>>,
222    ) -> Self {
223        Self {
224            query,
225            soa,
226            no_records_found,
227            nx_domain,
228            authorities,
229        }
230    }
231
232    /// are there records?
233    pub fn is_no_records_found(&self) -> bool {
234        self.no_records_found
235    }
236
237    /// is this nxdomain?
238    pub fn is_nx_domain(&self) -> bool {
239        self.nx_domain
240    }
241}
242
243impl From<AuthorityData> for NoRecords {
244    fn from(data: AuthorityData) -> Self {
245        let response_code = match data.is_nx_domain() {
246            true => ResponseCode::NXDomain,
247            false => ResponseCode::NoError,
248        };
249
250        let mut new = Self::new(data.query, response_code);
251        new.soa = data.soa;
252        new.authorities = data.authorities;
253        new
254    }
255}