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::{DNSClass, Name, 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 was too large
86    #[cfg(any(feature = "__https", feature = "__h3"))]
87    #[error("request too large")]
88    RequestTooLarge,
89
90    /// A request timed out
91    #[error("request timed out")]
92    Timeout,
93
94    /// A Quinn (Quic) connection error occurred
95    #[cfg(feature = "__quic")]
96    #[error("error creating quic connection: {0}")]
97    QuinnConnect(#[from] quinn::ConnectError),
98
99    /// A Quinn (QUIC) connection error occurred
100    #[cfg(feature = "__quic")]
101    #[error("error with quic connection: {0}")]
102    QuinnConnection(#[from] quinn::ConnectionError),
103
104    /// A Quinn (QUIC) write error occurred
105    #[cfg(feature = "__quic")]
106    #[error("error writing to quic connection: {0}")]
107    QuinnWriteError(#[from] quinn::WriteError),
108
109    /// A Quinn (QUIC) read error occurred
110    #[cfg(feature = "__quic")]
111    #[error("error writing to quic read: {0}")]
112    QuinnReadError(#[from] quinn::ReadExactError),
113
114    /// A Quinn (QUIC) stream error occurred
115    #[cfg(feature = "__quic")]
116    #[error("referenced a closed QUIC stream: {0}")]
117    QuinnStreamError(#[from] quinn::ClosedStream),
118
119    /// A Quinn (QUIC) configuration error occurred
120    #[cfg(feature = "__quic")]
121    #[error("error constructing quic configuration: {0}")]
122    QuinnConfigError(#[from] quinn::ConfigError),
123
124    /// QUIC TLS config must include an AES-128-GCM cipher suite
125    #[cfg(feature = "__quic")]
126    #[error("QUIC TLS config must include an AES-128-GCM cipher suite")]
127    QuinnTlsConfigError(#[from] quinn::crypto::rustls::NoInitialCipherSuite),
128
129    /// Unknown QUIC stream used
130    #[cfg(feature = "__quic")]
131    #[error("an unknown quic stream was used")]
132    QuinnUnknownStreamError,
133
134    /// A quic message id should always be 0
135    #[cfg(feature = "__quic")]
136    #[error("quic messages should always be 0, got: {0}")]
137    QuicMessageIdNot0(u16),
138
139    /// A Rustls error occurred
140    #[cfg(feature = "__tls")]
141    #[error("rustls construction error: {0}")]
142    RustlsError(#[from] rustls::Error),
143
144    /// Case randomization is enabled, and a server did not echo a query name back with the same
145    /// case.
146    #[error("case of query name in response did not match")]
147    QueryCaseMismatch,
148
149    /// An upstream response contained a record whose class did not match the IN query class.
150    #[error("foreign class record in response: {record_name} {record_class} {record_type}")]
151    ForeignClassRecord {
152        /// The name of the offending record.
153        record_name: Name,
154        /// The class of the offending record.
155        record_class: DNSClass,
156        /// The type of the offending record.
157        record_type: RecordType,
158    },
159
160    /// Received a truncated response
161    #[error("response was truncated; TCP already tried or no other transport available")]
162    Truncated,
163}
164
165impl NetError {
166    /// Returns true if the domain does not exist
167    #[inline]
168    pub fn is_nx_domain(&self) -> bool {
169        matches!(
170            self,
171            Self::Dns(DnsError::NoRecordsFound(NoRecords {
172                response_code: ResponseCode::NXDomain,
173                ..
174            }))
175        )
176    }
177
178    /// Returns true if the error represents NoRecordsFound
179    #[inline]
180    pub fn is_no_records_found(&self) -> bool {
181        matches!(self, Self::Dns(DnsError::NoRecordsFound { .. }))
182    }
183
184    /// Returns true for a transport-level connection error: the connection was
185    /// closed, reset, or refused, as opposed to a timeout or a server-side (DNS)
186    /// error.
187    pub fn is_connection_closed(&self) -> bool {
188        match self {
189            #[cfg(feature = "__https")]
190            Self::H2(err) => {
191                err.is_io() || err.is_go_away() || err.reason() == Some(h2::Reason::REFUSED_STREAM)
192            }
193            #[cfg(feature = "__h3")]
194            Self::H3(err) => err.is_h3_no_error(),
195            Self::Io(err) => matches!(
196                err.kind(),
197                io::ErrorKind::ConnectionReset
198                    | io::ErrorKind::ConnectionAborted
199                    | io::ErrorKind::BrokenPipe
200                    | io::ErrorKind::NotConnected
201                    | io::ErrorKind::UnexpectedEof
202            ),
203            #[cfg(feature = "__quic")]
204            Self::QuinnConnection(err) => matches!(
205                err,
206                quinn::ConnectionError::ConnectionClosed(_)
207                    | quinn::ConnectionError::ApplicationClosed(_)
208                    | quinn::ConnectionError::Reset
209            ),
210            _ => false,
211        }
212    }
213
214    /// Returns the SOA record, if the error contains one
215    #[inline]
216    pub fn into_soa(self) -> Option<Box<Record<SOA>>> {
217        match self {
218            Self::Dns(DnsError::NoRecordsFound(NoRecords { soa, .. })) => soa,
219            _ => None,
220        }
221    }
222
223    /// Returns a representative string of the error for use as a metrics label.
224    pub fn as_metrics_label(&self) -> &'static str {
225        match self {
226            Self::Busy => "busy",
227            #[cfg(any(feature = "__https", feature = "__h3"))]
228            Self::Decode(_) => "http_header_decode",
229            Self::Dns(dns_err) => dns_err.as_metrics_label(),
230            #[cfg(feature = "__https")]
231            Self::H2(_) => "http2",
232            #[cfg(feature = "__h3")]
233            Self::H3(_) => "http3",
234            Self::ParseInt(_) => "parse_header_value",
235            Self::NoConnections => "no_connections",
236            Self::Proto(_) => "proto",
237            Self::Io(e) => {
238                use std::io::ErrorKind::*;
239                match e.kind() {
240                    NotFound => "io_not_found",
241                    PermissionDenied => "io_permission_denied",
242                    ConnectionRefused => "io_connection_refused",
243                    ConnectionReset => "io_connection_reset",
244                    HostUnreachable => "io_host_unreachable",
245                    NetworkUnreachable => "io_network_unreachable",
246                    ConnectionAborted => "io_connection_aborted",
247                    NotConnected => "io_not_connected",
248                    AddrInUse => "io_addr_in_use",
249                    AddrNotAvailable => "io_addr_not_available",
250                    NetworkDown => "io_network_down",
251                    BrokenPipe => "io_broken_pipe",
252                    AlreadyExists => "io_already_exists",
253                    WouldBlock => "io_would_block",
254                    InvalidInput => "io_invalid_input",
255                    InvalidData => "io_invalid_data",
256                    TimedOut => "io_timed_out",
257                    WriteZero => "io_write_zero",
258                    QuotaExceeded => "io_quota_exceeded",
259                    ResourceBusy => "io_resource_busy",
260                    Deadlock => "io_deadlock",
261                    Interrupted => "io_interrupted",
262                    Unsupported => "io_unsupported",
263                    OutOfMemory => "io_out_of_memory",
264                    Other => "io_other",
265
266                    // It's highly unlikely that any of these would actually be returned.
267                    NotADirectory => "io_not_a_directory",
268                    IsADirectory => "io_is_a_directory",
269                    DirectoryNotEmpty => "io_directory_not_empty",
270                    ReadOnlyFilesystem => "io_read_only_filesystem",
271                    StaleNetworkFileHandle => "io_stale_network_file_handle",
272                    StorageFull => "io_storage_full",
273                    NotSeekable => "io_not_seekable",
274                    FileTooLarge => "io_file_too_large",
275                    ExecutableFileBusy => "io_executable_file_busy",
276                    CrossesDevices => "io_crosses_devices",
277                    TooManyLinks => "io_too_many_links",
278                    InvalidFilename => "io_invalid_filename",
279                    ArgumentListTooLong => "io_argument_list_too_long",
280                    UnexpectedEof => "io_unexpected_eof",
281
282                    _ => "io_unknown",
283                }
284            }
285            #[cfg(any(feature = "__https", feature = "__h3"))]
286            Self::RequestTooLarge => "request_too_large",
287            Self::Timeout => "timeout",
288            #[cfg(feature = "__quic")]
289            Self::QuinnConnect(_) => "quic_connect",
290            #[cfg(feature = "__quic")]
291            Self::QuinnConnection(_) => "quic_connection",
292            #[cfg(feature = "__quic")]
293            Self::QuinnWriteError(_) => "quic_write",
294            #[cfg(feature = "__quic")]
295            Self::QuinnReadError(_) => "quic_read",
296            #[cfg(feature = "__quic")]
297            Self::QuinnStreamError(_) => "quic_stream",
298            #[cfg(feature = "__quic")]
299            Self::QuinnConfigError(_) => "quic_config",
300            #[cfg(feature = "__quic")]
301            Self::QuinnTlsConfigError(_) => "quic_tls_config_error",
302            #[cfg(feature = "__quic")]
303            Self::QuinnUnknownStreamError => "quic_unknown_stream",
304            #[cfg(feature = "__quic")]
305            Self::QuicMessageIdNot0(_) => "quic_message_id_not_0",
306            #[cfg(feature = "__tls")]
307            Self::RustlsError(_) => "tls",
308            Self::QueryCaseMismatch => "query_case_mismatch",
309            Self::ForeignClassRecord { .. } => "foreign_class_record",
310            Self::Truncated => "truncated",
311
312            // Don't report these because the format is arbitrary, and in the case of Msg, dynamic.
313            Self::Message(_) | Self::Msg(_) => "message",
314        }
315    }
316}
317
318impl From<NoRecords> for NetError {
319    fn from(no_records: NoRecords) -> Self {
320        Self::Dns(DnsError::NoRecordsFound(no_records))
321    }
322}
323
324impl From<DecodeError> for NetError {
325    fn from(e: DecodeError) -> Self {
326        Self::Proto(e.into())
327    }
328}
329
330#[cfg(feature = "__h3")]
331impl From<h3::error::StreamError> for NetError {
332    fn from(e: h3::error::StreamError) -> Self {
333        Self::H3(Arc::new(e))
334    }
335}
336
337#[cfg(feature = "__https")]
338impl From<h2::Error> for NetError {
339    fn from(e: h2::Error) -> Self {
340        Self::H2(Arc::new(e))
341    }
342}
343
344#[cfg(any(feature = "__https", feature = "__h3"))]
345impl From<ToStrError> for NetError {
346    fn from(e: ToStrError) -> Self {
347        Self::Decode(Arc::new(e))
348    }
349}
350
351impl From<io::Error> for NetError {
352    fn from(e: io::Error) -> Self {
353        match e.kind() {
354            io::ErrorKind::TimedOut => Self::Timeout,
355            _ => Self::Io(Arc::new(e)),
356        }
357    }
358}
359
360impl From<String> for NetError {
361    fn from(msg: String) -> Self {
362        Self::Msg(msg)
363    }
364}
365
366impl From<&'static str> for NetError {
367    fn from(msg: &'static str) -> Self {
368        Self::Message(msg)
369    }
370}
371
372/// Semantic DNS errors
373#[derive(Clone, Debug, Error)]
374#[non_exhaustive]
375pub enum DnsError {
376    /// Received an error response code from the server
377    #[error("error response: {0}")]
378    ResponseCode(ResponseCode),
379    /// No records were found for a query
380    #[error("no records found for {:?}", .0.query)]
381    NoRecordsFound(NoRecords),
382    /// No Records and there is a corresponding DNSSEC Proof for NSEC
383    #[cfg(feature = "__dnssec")]
384    #[non_exhaustive]
385    #[error("DNSSEC Negative Record Response for {query}, {proof}")]
386    Nsec {
387        /// Query for which the NSEC was returned
388        query: Box<Query>,
389        /// Response for which the NSEC was returned
390        response: Box<DnsResponse>,
391        /// DNSSEC proof of the record
392        proof: Proof,
393    },
394    /// DNSSEC validation failure
395    #[error("DNSSEC validation failed")]
396    DnssecBogus,
397}
398
399impl DnsError {
400    /// A conversion to determine if the response is an error
401    pub fn from_response(response: DnsResponse) -> Result<DnsResponse, Self> {
402        use ResponseCode::*;
403        debug!("response: {}", *response);
404
405        match response.response_code {
406                Refused => Err(Self::ResponseCode(Refused)),
407                code @ ServFail
408                | code @ FormErr
409                | code @ NotImp
410                | code @ YXDomain
411                | code @ YXRRSet
412                | code @ NXRRSet
413                | code @ NotAuth
414                | code @ NotZone
415                | code @ BADVERS
416                | code @ BADSIG
417                | code @ BADKEY
418                | code @ BADTIME
419                | code @ BADMODE
420                | code @ BADNAME
421                | code @ BADALG
422                | code @ BADTRUNC
423                | code @ BADCOOKIE => Err(Self::ResponseCode(code)),
424                // Some NXDOMAIN responses contain CNAME referrals, that will not be an error
425                code @ NXDomain |
426                // No answers are available, CNAME referrals are not failures
427                code @ NoError
428                if !response.contains_answer() && !response.truncation => {
429                    // TODO: if authoritative, this is cacheable, store a TTL (currently that requires time, need a "now" here)
430                    // let valid_until = if response.authoritative() { now + response.negative_ttl() };
431                    let soa = response.soa().as_ref().map(RecordRef::to_owned);
432
433                    // Collect any referral nameservers and associated glue records
434                    let mut referral_name_servers = vec![];
435                    for ns in response.authorities.iter().filter(|ns| ns.record_type() == RecordType::NS) {
436                        let glue = response
437                            .additionals
438                            .iter()
439                            .filter_map(|record| {
440                                if let RData::NS(ns_data) = &ns.data {
441                                    if record.name == **ns_data && matches!(&record.data, RData::A(_) | RData::AAAA(_)) {
442                                        return Some(Record::to_owned(record));
443                                    }
444                                }
445
446                                None
447                            })
448                            .collect::<Vec<Record>>();
449                        referral_name_servers.push(ForwardNSData { ns: Record::to_owned(ns), glue: glue.into() })
450                    }
451
452                    let option_ns = if !referral_name_servers.is_empty() {
453                        Some(referral_name_servers.into())
454                    } else {
455                        None
456                    };
457
458                    let authorities = if !response.authorities.is_empty() {
459                        Some(response.authorities.to_owned().into())
460                    } else {
461                        None
462                    };
463
464                    let negative_ttl = response.negative_ttl();
465                    let query = response.into_message().queries.drain(..).next().unwrap_or_default();
466
467                    Err(Self::NoRecordsFound(NoRecords {
468                        query: Box::new(query),
469                        soa: soa.map(Box::new),
470                        ns: option_ns,
471                        negative_ttl,
472                        response_code: code,
473                        authorities,
474                    }))
475                }
476                NXDomain
477                | NoError
478                | Unknown(_) => Ok(response),
479            }
480    }
481
482    /// Returns the DNS error as a representative string for use as a metrics label.
483    pub fn as_metrics_label(&self) -> &'static str {
484        use hickory_proto::op::ResponseCode::*;
485        match self {
486            Self::ResponseCode(NoError) => "dns_response_code_noerror",
487            Self::ResponseCode(FormErr) => "dns_response_code_formerror",
488            Self::ResponseCode(ServFail) => "dns_response_code_servfail",
489            Self::ResponseCode(NXDomain) => "dns_response_code_nxdomain",
490            Self::ResponseCode(NotImp) => "dns_response_code_notimp",
491            Self::ResponseCode(Refused) => "dns_response_code_refused",
492            Self::ResponseCode(YXDomain) => "dns_response_code_yxdomain",
493            Self::ResponseCode(YXRRSet) => "dns_response_code_yxrrset",
494            Self::ResponseCode(NXRRSet) => "dns_response_code_nxrrset",
495            Self::ResponseCode(NotAuth) => "dns_response_code_notauth",
496            Self::ResponseCode(NotZone) => "dns_response_code_notzone",
497            Self::ResponseCode(BADVERS) => "dns_response_code_badvers",
498            Self::ResponseCode(BADSIG) => "dns_response_code_badsig",
499            Self::ResponseCode(BADKEY) => "dns_response_code_badkey",
500            Self::ResponseCode(BADTIME) => "dns_response_code_badtime",
501            Self::ResponseCode(BADMODE) => "dns_response_code_badmode",
502            Self::ResponseCode(BADNAME) => "dns_response_code_badname",
503            Self::ResponseCode(BADALG) => "dns_response_code_badalg",
504            Self::ResponseCode(BADTRUNC) => "dns_response_code_badtrunc",
505            Self::ResponseCode(BADCOOKIE) => "dns_response_code_badcookie",
506            Self::ResponseCode(Unknown(_)) => "dns_response_code_unknown",
507            Self::NoRecordsFound(_) => "dns_no_records",
508            #[cfg(feature = "__dnssec")]
509            Self::Nsec { .. } => "dns_nsec",
510            Self::DnssecBogus => "dns_dnssec_bogus",
511        }
512    }
513}
514
515/// Response where no records were found
516#[derive(Clone, Debug)]
517#[non_exhaustive]
518pub struct NoRecords {
519    /// The query for which no records were found.
520    pub query: Box<Query>,
521    /// If an SOA is present, then this is an authoritative response or a referral to another nameserver, see the negative_type field.
522    pub soa: Option<Box<Record<SOA>>>,
523    /// Nameservers may be present in addition to or in lieu of an SOA for a referral
524    /// The tuple struct layout is vec[(Nameserver, [vec of glue records])]
525    pub ns: Option<Arc<[ForwardNSData]>>,
526    /// negative ttl, as determined from DnsResponse::negative_ttl
527    ///  this will only be present if the SOA was also present.
528    pub negative_ttl: Option<u32>,
529    /// ResponseCode, if `NXDOMAIN`, the domain does not exist (and no other types).
530    ///   If `NoError`, then the domain exists but there exist either other types at the same label, or subzones of that label.
531    pub response_code: ResponseCode,
532    /// Authority records from the query. These are important to preserve for DNSSEC validation.
533    pub authorities: Option<Arc<[Record]>>,
534}
535
536impl NoRecords {
537    /// Construct a new [`NoRecords`] from a query and a response code
538    pub fn new(query: impl Into<Box<Query>>, response_code: ResponseCode) -> Self {
539        Self {
540            query: query.into(),
541            soa: None,
542            ns: None,
543            negative_ttl: None,
544            response_code,
545            authorities: None,
546        }
547    }
548}
549
550/// Data needed to process a NS-record-based referral.
551#[derive(Clone, Debug)]
552pub struct ForwardNSData {
553    /// The referant NS record
554    pub ns: Record,
555    /// Any glue records associated with the referant NS record.
556    pub glue: Arc<[Record]>,
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[test]
564    fn is_connection_closed_io_kinds() {
565        for kind in [
566            io::ErrorKind::ConnectionReset,
567            io::ErrorKind::ConnectionAborted,
568            io::ErrorKind::BrokenPipe,
569            io::ErrorKind::NotConnected,
570            io::ErrorKind::UnexpectedEof,
571        ] {
572            assert!(
573                NetError::from(io::Error::from(kind)).is_connection_closed(),
574                "{kind:?}"
575            );
576        }
577
578        assert!(
579            !NetError::from(io::Error::from(io::ErrorKind::PermissionDenied))
580                .is_connection_closed()
581        );
582        assert!(!NetError::Timeout.is_connection_closed());
583        assert!(!NetError::Message("server error").is_connection_closed());
584    }
585
586    #[cfg(feature = "__https")]
587    #[test]
588    fn is_connection_closed_h2() {
589        // REFUSED_STREAM: the server never processed the request, so it's safe to retry.
590        assert!(NetError::from(h2::Error::from(h2::Reason::REFUSED_STREAM)).is_connection_closed());
591        // A protocol error mid-exchange is not a clean connection-closed signal.
592        assert!(
593            !NetError::from(h2::Error::from(h2::Reason::INTERNAL_ERROR)).is_connection_closed()
594        );
595    }
596
597    #[cfg(feature = "__quic")]
598    #[test]
599    fn is_connection_closed_quic_excludes_timeout() {
600        assert!(NetError::QuinnConnection(quinn::ConnectionError::Reset).is_connection_closed());
601        assert!(
602            !NetError::QuinnConnection(quinn::ConnectionError::TimedOut).is_connection_closed()
603        );
604    }
605}