Skip to main content

hickory_net/
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 core::num::ParseIntError;
13use std::io;
14use std::sync::Arc;
15
16#[cfg(any(feature = "__https", feature = "__h3"))]
17use http::header::ToStrError;
18use thiserror::Error;
19use tracing::debug;
20
21use crate::proto::ProtoError;
22#[cfg(feature = "__dnssec")]
23use crate::proto::dnssec::Proof;
24use crate::proto::op::{DnsResponse, Query, ResponseCode};
25use crate::proto::rr::RData;
26use crate::proto::rr::{Record, RecordRef, RecordType, rdata::SOA};
27use crate::proto::serialize::binary::DecodeError;
28
29/// The error type for network protocol errors (UDP, TCP, QUIC, H2, H3)
30#[non_exhaustive]
31#[derive(Error, Clone, Debug)]
32pub enum NetError {
33    /// The underlying resource is too busy
34    ///
35    /// This is a signal that an internal resource is too busy. The intended action should be tried
36    /// again, ideally after waiting for a little while for the situation to improve. Alternatively,
37    /// the action could be tried on another resource (for example, in a name server pool).
38    #[error("resource too busy")]
39    Busy,
40
41    /// Unable to decode HTTP header value to string
42    #[cfg(any(feature = "__https", feature = "__h3"))]
43    #[error("header decode error: {0}")]
44    Decode(Arc<ToStrError>),
45
46    /// Semantic DNS errors
47    #[error("DNS error: {0}")]
48    Dns(#[from] DnsError),
49
50    /// An HTTP/2 related error
51    #[error("H2 error: {0}")]
52    #[cfg(feature = "__https")]
53    H2(Arc<h2::Error>),
54
55    /// An HTTP/3 related error
56    #[error("H3 error: {0}")]
57    #[cfg(feature = "__h3")]
58    H3(Arc<h3::error::StreamError>),
59
60    /// An error with an arbitrary message, referenced as &'static str
61    #[error("{0}")]
62    Message(&'static str),
63
64    /// An error with an arbitrary message, stored as String
65    #[error("{0}")]
66    Msg(String),
67
68    /// Unable to parse header value as number
69    #[error("unable to parse number: {0}")]
70    ParseInt(#[from] ParseIntError),
71
72    /// No connections available
73    #[error("no connections available")]
74    NoConnections,
75
76    /// Protocol error from higher layers
77    #[error("protocol error: {0}")]
78    Proto(#[from] ProtoError),
79
80    // foreign
81    /// An error got returned from IO
82    #[error("io error: {0}")]
83    Io(Arc<io::Error>),
84
85    /// A request timed out
86    #[error("request timed out")]
87    Timeout,
88
89    /// A Quinn (Quic) connection error occurred
90    #[cfg(feature = "__quic")]
91    #[error("error creating quic connection: {0}")]
92    QuinnConnect(#[from] quinn::ConnectError),
93
94    /// A Quinn (QUIC) connection error occurred
95    #[cfg(feature = "__quic")]
96    #[error("error with quic connection: {0}")]
97    QuinnConnection(#[from] quinn::ConnectionError),
98
99    /// A Quinn (QUIC) write error occurred
100    #[cfg(feature = "__quic")]
101    #[error("error writing to quic connection: {0}")]
102    QuinnWriteError(#[from] quinn::WriteError),
103
104    /// A Quinn (QUIC) read error occurred
105    #[cfg(feature = "__quic")]
106    #[error("error writing to quic read: {0}")]
107    QuinnReadError(#[from] quinn::ReadExactError),
108
109    /// A Quinn (QUIC) stream error occurred
110    #[cfg(feature = "__quic")]
111    #[error("referenced a closed QUIC stream: {0}")]
112    QuinnStreamError(#[from] quinn::ClosedStream),
113
114    /// A Quinn (QUIC) configuration error occurred
115    #[cfg(feature = "__quic")]
116    #[error("error constructing quic configuration: {0}")]
117    QuinnConfigError(#[from] quinn::ConfigError),
118
119    /// QUIC TLS config must include an AES-128-GCM cipher suite
120    #[cfg(feature = "__quic")]
121    #[error("QUIC TLS config must include an AES-128-GCM cipher suite")]
122    QuinnTlsConfigError(#[from] quinn::crypto::rustls::NoInitialCipherSuite),
123
124    /// Unknown QUIC stream used
125    #[cfg(feature = "__quic")]
126    #[error("an unknown quic stream was used")]
127    QuinnUnknownStreamError,
128
129    /// A quic message id should always be 0
130    #[cfg(feature = "__quic")]
131    #[error("quic messages should always be 0, got: {0}")]
132    QuicMessageIdNot0(u16),
133
134    /// A Rustls error occurred
135    #[cfg(feature = "__tls")]
136    #[error("rustls construction error: {0}")]
137    RustlsError(#[from] rustls::Error),
138
139    /// Case randomization is enabled, and a server did not echo a query name back with the same
140    /// case.
141    #[error("case of query name in response did not match")]
142    QueryCaseMismatch,
143}
144
145impl NetError {
146    /// Returns true if the domain does not exist
147    #[inline]
148    pub fn is_nx_domain(&self) -> bool {
149        matches!(
150            self,
151            Self::Dns(DnsError::NoRecordsFound(NoRecords {
152                response_code: ResponseCode::NXDomain,
153                ..
154            }))
155        )
156    }
157
158    /// Returns true if the error represents NoRecordsFound
159    #[inline]
160    pub fn is_no_records_found(&self) -> bool {
161        matches!(self, Self::Dns(DnsError::NoRecordsFound { .. }))
162    }
163
164    /// Returns the SOA record, if the error contains one
165    #[inline]
166    pub fn into_soa(self) -> Option<Box<Record<SOA>>> {
167        match self {
168            Self::Dns(DnsError::NoRecordsFound(NoRecords { soa, .. })) => soa,
169            _ => None,
170        }
171    }
172}
173
174impl From<NoRecords> for NetError {
175    fn from(no_records: NoRecords) -> Self {
176        Self::Dns(DnsError::NoRecordsFound(no_records))
177    }
178}
179
180impl From<DecodeError> for NetError {
181    fn from(e: DecodeError) -> Self {
182        Self::Proto(e.into())
183    }
184}
185
186#[cfg(feature = "__h3")]
187impl From<h3::error::StreamError> for NetError {
188    fn from(e: h3::error::StreamError) -> Self {
189        Self::H3(Arc::new(e))
190    }
191}
192
193#[cfg(feature = "__https")]
194impl From<h2::Error> for NetError {
195    fn from(e: h2::Error) -> Self {
196        Self::H2(Arc::new(e))
197    }
198}
199
200#[cfg(any(feature = "__https", feature = "__h3"))]
201impl From<ToStrError> for NetError {
202    fn from(e: ToStrError) -> Self {
203        Self::Decode(Arc::new(e))
204    }
205}
206
207impl From<io::Error> for NetError {
208    fn from(e: io::Error) -> Self {
209        match e.kind() {
210            io::ErrorKind::TimedOut => Self::Timeout,
211            _ => Self::Io(Arc::new(e)),
212        }
213    }
214}
215
216impl From<String> for NetError {
217    fn from(msg: String) -> Self {
218        Self::Msg(msg)
219    }
220}
221
222impl From<&'static str> for NetError {
223    fn from(msg: &'static str) -> Self {
224        Self::Message(msg)
225    }
226}
227
228/// Semantic DNS errors
229#[derive(Clone, Debug, Error)]
230#[non_exhaustive]
231pub enum DnsError {
232    /// Received an error response code from the server
233    #[error("error response: {0}")]
234    ResponseCode(ResponseCode),
235    /// No records were found for a query
236    #[error("no records found for {:?}", .0.query)]
237    NoRecordsFound(NoRecords),
238    /// No Records and there is a corresponding DNSSEC Proof for NSEC
239    #[cfg(feature = "__dnssec")]
240    #[non_exhaustive]
241    #[error("DNSSEC Negative Record Response for {query}, {proof}")]
242    Nsec {
243        /// Query for which the NSEC was returned
244        query: Box<Query>,
245        /// Response for which the NSEC was returned
246        response: Box<DnsResponse>,
247        /// DNSSEC proof of the record
248        proof: Proof,
249    },
250}
251
252impl DnsError {
253    /// A conversion to determine if the response is an error
254    pub fn from_response(response: DnsResponse) -> Result<DnsResponse, Self> {
255        use ResponseCode::*;
256        debug!("response: {}", *response);
257
258        match response.response_code {
259                Refused => Err(Self::ResponseCode(Refused)),
260                code @ ServFail
261                | code @ FormErr
262                | code @ NotImp
263                | code @ YXDomain
264                | code @ YXRRSet
265                | code @ NXRRSet
266                | code @ NotAuth
267                | code @ NotZone
268                | code @ BADVERS
269                | code @ BADSIG
270                | code @ BADKEY
271                | code @ BADTIME
272                | code @ BADMODE
273                | code @ BADNAME
274                | code @ BADALG
275                | code @ BADTRUNC
276                | code @ BADCOOKIE => Err(Self::ResponseCode(code)),
277                // Some NXDOMAIN responses contain CNAME referrals, that will not be an error
278                code @ NXDomain |
279                // No answers are available, CNAME referrals are not failures
280                code @ NoError
281                if !response.contains_answer() && !response.truncation => {
282                    // TODO: if authoritative, this is cacheable, store a TTL (currently that requires time, need a "now" here)
283                    // let valid_until = if response.authoritative() { now + response.negative_ttl() };
284                    let soa = response.soa().as_ref().map(RecordRef::to_owned);
285
286                    // Collect any referral nameservers and associated glue records
287                    let mut referral_name_servers = vec![];
288                    for ns in response.authorities.iter().filter(|ns| ns.record_type() == RecordType::NS) {
289                        let glue = response
290                            .additionals
291                            .iter()
292                            .filter_map(|record| {
293                                if let RData::NS(ns_data) = &ns.data {
294                                    if record.name == **ns_data && matches!(&record.data, RData::A(_) | RData::AAAA(_)) {
295                                        return Some(Record::to_owned(record));
296                                    }
297                                }
298
299                                None
300                            })
301                            .collect::<Vec<Record>>();
302                        referral_name_servers.push(ForwardNSData { ns: Record::to_owned(ns), glue: glue.into() })
303                    }
304
305                    let option_ns = if !referral_name_servers.is_empty() {
306                        Some(referral_name_servers.into())
307                    } else {
308                        None
309                    };
310
311                    let authorities = if !response.authorities.is_empty() {
312                        Some(response.authorities.to_owned().into())
313                    } else {
314                        None
315                    };
316
317                    let negative_ttl = response.negative_ttl();
318                    let query = response.into_message().queries.drain(..).next().unwrap_or_default();
319
320                    Err(Self::NoRecordsFound(NoRecords {
321                        query: Box::new(query),
322                        soa: soa.map(Box::new),
323                        ns: option_ns,
324                        negative_ttl,
325                        response_code: code,
326                        authorities,
327                    }))
328                }
329                NXDomain
330                | NoError
331                | Unknown(_) => Ok(response),
332            }
333    }
334}
335
336/// Response where no records were found
337#[derive(Clone, Debug)]
338#[non_exhaustive]
339pub struct NoRecords {
340    /// The query for which no records were found.
341    pub query: Box<Query>,
342    /// If an SOA is present, then this is an authoritative response or a referral to another nameserver, see the negative_type field.
343    pub soa: Option<Box<Record<SOA>>>,
344    /// Nameservers may be present in addition to or in lieu of an SOA for a referral
345    /// The tuple struct layout is vec[(Nameserver, [vec of glue records])]
346    pub ns: Option<Arc<[ForwardNSData]>>,
347    /// negative ttl, as determined from DnsResponse::negative_ttl
348    ///  this will only be present if the SOA was also present.
349    pub negative_ttl: Option<u32>,
350    /// ResponseCode, if `NXDOMAIN`, the domain does not exist (and no other types).
351    ///   If `NoError`, then the domain exists but there exist either other types at the same label, or subzones of that label.
352    pub response_code: ResponseCode,
353    /// Authority records from the query. These are important to preserve for DNSSEC validation.
354    pub authorities: Option<Arc<[Record]>>,
355}
356
357impl NoRecords {
358    /// Construct a new [`NoRecords`] from a query and a response code
359    pub fn new(query: impl Into<Box<Query>>, response_code: ResponseCode) -> Self {
360        Self {
361            query: query.into(),
362            soa: None,
363            ns: None,
364            negative_ttl: None,
365            response_code,
366            authorities: None,
367        }
368    }
369}
370
371/// Data needed to process a NS-record-based referral.
372#[derive(Clone, Debug)]
373pub struct ForwardNSData {
374    /// The referant NS record
375    pub ns: Record,
376    /// Any glue records associated with the referant NS record.
377    pub glue: Arc<[Record]>,
378}