#![deny(missing_docs)]
use core::num::ParseIntError;
use std::io;
use std::sync::Arc;
#[cfg(any(feature = "__https", feature = "__h3"))]
use http::header::ToStrError;
use thiserror::Error;
use tracing::debug;
use crate::proto::ProtoError;
#[cfg(feature = "__dnssec")]
use crate::proto::dnssec::Proof;
use crate::proto::op::{DnsResponse, Query, ResponseCode};
use crate::proto::rr::RData;
use crate::proto::rr::{DNSClass, Name, Record, RecordRef, RecordType, rdata::SOA};
use crate::proto::serialize::binary::DecodeError;
#[non_exhaustive]
#[derive(Error, Clone, Debug)]
pub enum NetError {
#[error("resource too busy")]
Busy,
#[cfg(any(feature = "__https", feature = "__h3"))]
#[error("header decode error: {0}")]
Decode(Arc<ToStrError>),
#[error("DNS error: {0}")]
Dns(#[from] DnsError),
#[error("H2 error: {0}")]
#[cfg(feature = "__https")]
H2(Arc<h2::Error>),
#[error("H3 error: {0}")]
#[cfg(feature = "__h3")]
H3(Arc<h3::error::StreamError>),
#[error("{0}")]
Message(&'static str),
#[error("{0}")]
Msg(String),
#[error("unable to parse number: {0}")]
ParseInt(#[from] ParseIntError),
#[error("no connections available")]
NoConnections,
#[error("protocol error: {0}")]
Proto(#[from] ProtoError),
#[error("io error: {0}")]
Io(Arc<io::Error>),
#[cfg(any(feature = "__https", feature = "__h3"))]
#[error("request too large")]
RequestTooLarge,
#[error("request timed out")]
Timeout,
#[cfg(feature = "__quic")]
#[error("error creating quic connection: {0}")]
QuinnConnect(#[from] quinn::ConnectError),
#[cfg(feature = "__quic")]
#[error("error with quic connection: {0}")]
QuinnConnection(#[from] quinn::ConnectionError),
#[cfg(feature = "__quic")]
#[error("error writing to quic connection: {0}")]
QuinnWriteError(#[from] quinn::WriteError),
#[cfg(feature = "__quic")]
#[error("error writing to quic read: {0}")]
QuinnReadError(#[from] quinn::ReadExactError),
#[cfg(feature = "__quic")]
#[error("referenced a closed QUIC stream: {0}")]
QuinnStreamError(#[from] quinn::ClosedStream),
#[cfg(feature = "__quic")]
#[error("error constructing quic configuration: {0}")]
QuinnConfigError(#[from] quinn::ConfigError),
#[cfg(feature = "__quic")]
#[error("QUIC TLS config must include an AES-128-GCM cipher suite")]
QuinnTlsConfigError(#[from] quinn::crypto::rustls::NoInitialCipherSuite),
#[cfg(feature = "__quic")]
#[error("an unknown quic stream was used")]
QuinnUnknownStreamError,
#[cfg(feature = "__quic")]
#[error("quic messages should always be 0, got: {0}")]
QuicMessageIdNot0(u16),
#[cfg(feature = "__tls")]
#[error("rustls construction error: {0}")]
RustlsError(#[from] rustls::Error),
#[error("case of query name in response did not match")]
QueryCaseMismatch,
#[error("foreign class record in response: {record_name} {record_class} {record_type}")]
ForeignClassRecord {
record_name: Name,
record_class: DNSClass,
record_type: RecordType,
},
#[error("response was truncated; TCP already tried or no other transport available")]
Truncated,
}
impl NetError {
#[inline]
pub fn is_nx_domain(&self) -> bool {
matches!(
self,
Self::Dns(DnsError::NoRecordsFound(NoRecords {
response_code: ResponseCode::NXDomain,
..
}))
)
}
#[inline]
pub fn is_no_records_found(&self) -> bool {
matches!(self, Self::Dns(DnsError::NoRecordsFound { .. }))
}
pub fn is_connection_closed(&self) -> bool {
match self {
#[cfg(feature = "__https")]
Self::H2(err) => {
err.is_io() || err.is_go_away() || err.reason() == Some(h2::Reason::REFUSED_STREAM)
}
#[cfg(feature = "__h3")]
Self::H3(err) => err.is_h3_no_error(),
Self::Io(err) => matches!(
err.kind(),
io::ErrorKind::ConnectionReset
| io::ErrorKind::ConnectionAborted
| io::ErrorKind::BrokenPipe
| io::ErrorKind::NotConnected
| io::ErrorKind::UnexpectedEof
),
#[cfg(feature = "__quic")]
Self::QuinnConnection(err) => matches!(
err,
quinn::ConnectionError::ConnectionClosed(_)
| quinn::ConnectionError::ApplicationClosed(_)
| quinn::ConnectionError::Reset
),
_ => false,
}
}
#[inline]
pub fn into_soa(self) -> Option<Box<Record<SOA>>> {
match self {
Self::Dns(DnsError::NoRecordsFound(NoRecords { soa, .. })) => soa,
_ => None,
}
}
pub fn as_metrics_label(&self) -> &'static str {
match self {
Self::Busy => "busy",
#[cfg(any(feature = "__https", feature = "__h3"))]
Self::Decode(_) => "http_header_decode",
Self::Dns(dns_err) => dns_err.as_metrics_label(),
#[cfg(feature = "__https")]
Self::H2(_) => "http2",
#[cfg(feature = "__h3")]
Self::H3(_) => "http3",
Self::ParseInt(_) => "parse_header_value",
Self::NoConnections => "no_connections",
Self::Proto(_) => "proto",
Self::Io(e) => {
use std::io::ErrorKind::*;
match e.kind() {
NotFound => "io_not_found",
PermissionDenied => "io_permission_denied",
ConnectionRefused => "io_connection_refused",
ConnectionReset => "io_connection_reset",
HostUnreachable => "io_host_unreachable",
NetworkUnreachable => "io_network_unreachable",
ConnectionAborted => "io_connection_aborted",
NotConnected => "io_not_connected",
AddrInUse => "io_addr_in_use",
AddrNotAvailable => "io_addr_not_available",
NetworkDown => "io_network_down",
BrokenPipe => "io_broken_pipe",
AlreadyExists => "io_already_exists",
WouldBlock => "io_would_block",
InvalidInput => "io_invalid_input",
InvalidData => "io_invalid_data",
TimedOut => "io_timed_out",
WriteZero => "io_write_zero",
QuotaExceeded => "io_quota_exceeded",
ResourceBusy => "io_resource_busy",
Deadlock => "io_deadlock",
Interrupted => "io_interrupted",
Unsupported => "io_unsupported",
OutOfMemory => "io_out_of_memory",
Other => "io_other",
NotADirectory => "io_not_a_directory",
IsADirectory => "io_is_a_directory",
DirectoryNotEmpty => "io_directory_not_empty",
ReadOnlyFilesystem => "io_read_only_filesystem",
StaleNetworkFileHandle => "io_stale_network_file_handle",
StorageFull => "io_storage_full",
NotSeekable => "io_not_seekable",
FileTooLarge => "io_file_too_large",
ExecutableFileBusy => "io_executable_file_busy",
CrossesDevices => "io_crosses_devices",
TooManyLinks => "io_too_many_links",
InvalidFilename => "io_invalid_filename",
ArgumentListTooLong => "io_argument_list_too_long",
UnexpectedEof => "io_unexpected_eof",
_ => "io_unknown",
}
}
#[cfg(any(feature = "__https", feature = "__h3"))]
Self::RequestTooLarge => "request_too_large",
Self::Timeout => "timeout",
#[cfg(feature = "__quic")]
Self::QuinnConnect(_) => "quic_connect",
#[cfg(feature = "__quic")]
Self::QuinnConnection(_) => "quic_connection",
#[cfg(feature = "__quic")]
Self::QuinnWriteError(_) => "quic_write",
#[cfg(feature = "__quic")]
Self::QuinnReadError(_) => "quic_read",
#[cfg(feature = "__quic")]
Self::QuinnStreamError(_) => "quic_stream",
#[cfg(feature = "__quic")]
Self::QuinnConfigError(_) => "quic_config",
#[cfg(feature = "__quic")]
Self::QuinnTlsConfigError(_) => "quic_tls_config_error",
#[cfg(feature = "__quic")]
Self::QuinnUnknownStreamError => "quic_unknown_stream",
#[cfg(feature = "__quic")]
Self::QuicMessageIdNot0(_) => "quic_message_id_not_0",
#[cfg(feature = "__tls")]
Self::RustlsError(_) => "tls",
Self::QueryCaseMismatch => "query_case_mismatch",
Self::ForeignClassRecord { .. } => "foreign_class_record",
Self::Truncated => "truncated",
Self::Message(_) | Self::Msg(_) => "message",
}
}
}
impl From<NoRecords> for NetError {
fn from(no_records: NoRecords) -> Self {
Self::Dns(DnsError::NoRecordsFound(no_records))
}
}
impl From<DecodeError> for NetError {
fn from(e: DecodeError) -> Self {
Self::Proto(e.into())
}
}
#[cfg(feature = "__h3")]
impl From<h3::error::StreamError> for NetError {
fn from(e: h3::error::StreamError) -> Self {
Self::H3(Arc::new(e))
}
}
#[cfg(feature = "__https")]
impl From<h2::Error> for NetError {
fn from(e: h2::Error) -> Self {
Self::H2(Arc::new(e))
}
}
#[cfg(any(feature = "__https", feature = "__h3"))]
impl From<ToStrError> for NetError {
fn from(e: ToStrError) -> Self {
Self::Decode(Arc::new(e))
}
}
impl From<io::Error> for NetError {
fn from(e: io::Error) -> Self {
match e.kind() {
io::ErrorKind::TimedOut => Self::Timeout,
_ => Self::Io(Arc::new(e)),
}
}
}
impl From<String> for NetError {
fn from(msg: String) -> Self {
Self::Msg(msg)
}
}
impl From<&'static str> for NetError {
fn from(msg: &'static str) -> Self {
Self::Message(msg)
}
}
#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum DnsError {
#[error("error response: {0}")]
ResponseCode(ResponseCode),
#[error("no records found for {:?}", .0.query)]
NoRecordsFound(NoRecords),
#[cfg(feature = "__dnssec")]
#[non_exhaustive]
#[error("DNSSEC Negative Record Response for {query}, {proof}")]
Nsec {
query: Box<Query>,
response: Box<DnsResponse>,
proof: Proof,
},
#[error("DNSSEC validation failed")]
DnssecBogus,
}
impl DnsError {
pub fn from_response(response: DnsResponse) -> Result<DnsResponse, Self> {
use ResponseCode::*;
debug!("response: {}", *response);
match response.response_code {
Refused => Err(Self::ResponseCode(Refused)),
code @ ServFail
| code @ FormErr
| code @ NotImp
| code @ YXDomain
| code @ YXRRSet
| code @ NXRRSet
| code @ NotAuth
| code @ NotZone
| code @ BADVERS
| code @ BADSIG
| code @ BADKEY
| code @ BADTIME
| code @ BADMODE
| code @ BADNAME
| code @ BADALG
| code @ BADTRUNC
| code @ BADCOOKIE => Err(Self::ResponseCode(code)),
code @ NXDomain |
code @ NoError
if !response.contains_answer() && !response.truncation => {
let soa = response.soa().as_ref().map(RecordRef::to_owned);
let mut referral_name_servers = vec![];
for ns in response.authorities.iter().filter(|ns| ns.record_type() == RecordType::NS) {
let glue = response
.additionals
.iter()
.filter_map(|record| {
if let RData::NS(ns_data) = &ns.data {
if record.name == **ns_data && matches!(&record.data, RData::A(_) | RData::AAAA(_)) {
return Some(Record::to_owned(record));
}
}
None
})
.collect::<Vec<Record>>();
referral_name_servers.push(ForwardNSData { ns: Record::to_owned(ns), glue: glue.into() })
}
let option_ns = if !referral_name_servers.is_empty() {
Some(referral_name_servers.into())
} else {
None
};
let authorities = if !response.authorities.is_empty() {
Some(response.authorities.to_owned().into())
} else {
None
};
let negative_ttl = response.negative_ttl();
let query = response.into_message().queries.drain(..).next().unwrap_or_default();
Err(Self::NoRecordsFound(NoRecords {
query: Box::new(query),
soa: soa.map(Box::new),
ns: option_ns,
negative_ttl,
response_code: code,
authorities,
}))
}
NXDomain
| NoError
| Unknown(_) => Ok(response),
}
}
pub fn as_metrics_label(&self) -> &'static str {
use hickory_proto::op::ResponseCode::*;
match self {
Self::ResponseCode(NoError) => "dns_response_code_noerror",
Self::ResponseCode(FormErr) => "dns_response_code_formerror",
Self::ResponseCode(ServFail) => "dns_response_code_servfail",
Self::ResponseCode(NXDomain) => "dns_response_code_nxdomain",
Self::ResponseCode(NotImp) => "dns_response_code_notimp",
Self::ResponseCode(Refused) => "dns_response_code_refused",
Self::ResponseCode(YXDomain) => "dns_response_code_yxdomain",
Self::ResponseCode(YXRRSet) => "dns_response_code_yxrrset",
Self::ResponseCode(NXRRSet) => "dns_response_code_nxrrset",
Self::ResponseCode(NotAuth) => "dns_response_code_notauth",
Self::ResponseCode(NotZone) => "dns_response_code_notzone",
Self::ResponseCode(BADVERS) => "dns_response_code_badvers",
Self::ResponseCode(BADSIG) => "dns_response_code_badsig",
Self::ResponseCode(BADKEY) => "dns_response_code_badkey",
Self::ResponseCode(BADTIME) => "dns_response_code_badtime",
Self::ResponseCode(BADMODE) => "dns_response_code_badmode",
Self::ResponseCode(BADNAME) => "dns_response_code_badname",
Self::ResponseCode(BADALG) => "dns_response_code_badalg",
Self::ResponseCode(BADTRUNC) => "dns_response_code_badtrunc",
Self::ResponseCode(BADCOOKIE) => "dns_response_code_badcookie",
Self::ResponseCode(Unknown(_)) => "dns_response_code_unknown",
Self::NoRecordsFound(_) => "dns_no_records",
#[cfg(feature = "__dnssec")]
Self::Nsec { .. } => "dns_nsec",
Self::DnssecBogus => "dns_dnssec_bogus",
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct NoRecords {
pub query: Box<Query>,
pub soa: Option<Box<Record<SOA>>>,
pub ns: Option<Arc<[ForwardNSData]>>,
pub negative_ttl: Option<u32>,
pub response_code: ResponseCode,
pub authorities: Option<Arc<[Record]>>,
}
impl NoRecords {
pub fn new(query: impl Into<Box<Query>>, response_code: ResponseCode) -> Self {
Self {
query: query.into(),
soa: None,
ns: None,
negative_ttl: None,
response_code,
authorities: None,
}
}
}
#[derive(Clone, Debug)]
pub struct ForwardNSData {
pub ns: Record,
pub glue: Arc<[Record]>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_connection_closed_io_kinds() {
for kind in [
io::ErrorKind::ConnectionReset,
io::ErrorKind::ConnectionAborted,
io::ErrorKind::BrokenPipe,
io::ErrorKind::NotConnected,
io::ErrorKind::UnexpectedEof,
] {
assert!(
NetError::from(io::Error::from(kind)).is_connection_closed(),
"{kind:?}"
);
}
assert!(
!NetError::from(io::Error::from(io::ErrorKind::PermissionDenied))
.is_connection_closed()
);
assert!(!NetError::Timeout.is_connection_closed());
assert!(!NetError::Message("server error").is_connection_closed());
}
#[cfg(feature = "__https")]
#[test]
fn is_connection_closed_h2() {
assert!(NetError::from(h2::Error::from(h2::Reason::REFUSED_STREAM)).is_connection_closed());
assert!(
!NetError::from(h2::Error::from(h2::Reason::INTERNAL_ERROR)).is_connection_closed()
);
}
#[cfg(feature = "__quic")]
#[test]
fn is_connection_closed_quic_excludes_timeout() {
assert!(NetError::QuinnConnection(quinn::ConnectionError::Reset).is_connection_closed());
assert!(
!NetError::QuinnConnection(quinn::ConnectionError::TimedOut).is_connection_closed()
);
}
}