#![deny(missing_docs)]
use std::cmp::Ordering;
use std::sync::Arc;
use std::{fmt, io, sync};
#[cfg(feature = "backtrace")]
#[cfg_attr(docsrs, doc(cfg(feature = "backtrace")))]
pub use backtrace::Backtrace as ExtBacktrace;
use enum_as_inner::EnumAsInner;
#[cfg(feature = "backtrace")]
use once_cell::sync::Lazy;
use thiserror::Error;
use tracing::debug;
use crate::op::{Header, Query, ResponseCode};
#[cfg(feature = "dnssec")]
use crate::rr::dnssec::{rdata::tsig::TsigAlgorithm, Proof};
use crate::rr::{rdata::SOA, resource::RecordRef, Record, RecordType};
use crate::serialize::binary::DecodeError;
use crate::xfer::DnsResponse;
#[cfg(feature = "backtrace")]
#[cfg_attr(docsrs, doc(cfg(feature = "backtrace")))]
pub static ENABLE_BACKTRACE: Lazy<bool> = Lazy::new(|| {
use std::env;
let bt = env::var("RUST_BACKTRACE");
matches!(bt.as_ref().map(|s| s as &str), Ok("full") | Ok("1"))
});
#[cfg(feature = "backtrace")]
#[cfg_attr(docsrs, doc(cfg(feature = "backtrace")))]
#[macro_export]
macro_rules! trace {
() => {{
use $crate::error::ExtBacktrace as Backtrace;
if *$crate::error::ENABLE_BACKTRACE {
Some(Backtrace::new())
} else {
None
}
}};
}
pub type ProtoResult<T> = ::std::result::Result<T, ProtoError>;
#[derive(Debug, EnumAsInner, Error)]
#[non_exhaustive]
pub enum ProtoErrorKind {
#[error("there should only be one query per request, got: {0}")]
BadQueryCount(usize),
#[error("resource too busy")]
Busy,
#[error("future was canceled: {0:?}")]
Canceled(futures_channel::oneshot::Canceled),
#[error("char data length exceeds {max}: {len}")]
CharacterDataTooLong {
max: usize,
len: usize,
},
#[error("overlapping labels name {label} other {other}")]
LabelOverlapsWithOther {
label: usize,
other: usize,
},
#[cfg(feature = "dnssec")]
#[error("DNSSEC Negative Record Response for {query}, {proof}")]
Nsec {
query: crate::op::Query,
proof: Proof,
},
#[error("dns key value unknown, must be 3: {0}")]
DnsKeyProtocolNot3(u8),
#[error("name label data exceed 255: {0}")]
DomainNameTooLong(usize),
#[error("edns resource record label must be the root label (.): {0}")]
EdnsNameNotRoot(crate::rr::Name),
#[error("message format error: {error}")]
FormError {
header: Header,
error: Box<ProtoError>,
},
#[error("hmac validation failure")]
HmacInvalid(),
#[error("incorrect rdata length read: {read} expected: {len}")]
IncorrectRDataLengthRead {
read: usize,
len: usize,
},
#[error("label bytes exceed 63: {0}")]
LabelBytesTooLong(usize),
#[error("label points to data not prior to idx: {idx} ptr: {ptr}")]
PointerNotPriorToLabel {
idx: usize,
ptr: u16,
},
#[error("maximum buffer size exceeded: {0}")]
MaxBufferSizeExceeded(usize),
#[error("{0}")]
Message(&'static str),
#[error("{0}")]
Msg(String),
#[error("no connections available")]
NoConnections,
#[error("no error specified")]
NoError,
#[error("not all records could be written, wrote: {count}")]
NotAllRecordsWritten {
count: usize,
},
#[error("no records found for {:?}", query)]
NoRecordsFound {
query: Box<Query>,
soa: Option<Box<Record<SOA>>>,
ns: Option<Vec<ForwardNSData>>,
negative_ttl: Option<u32>,
response_code: ResponseCode,
trusted: bool,
},
#[error("algorithm type value unknown: {0}")]
UnknownAlgorithmTypeValue(u8),
#[error("dns class string unknown: {0}")]
UnknownDnsClassStr(String),
#[error("dns class value unknown: {0}")]
UnknownDnsClassValue(u16),
#[error("record type string unknown: {0}")]
UnknownRecordTypeStr(String),
#[error("record type value unknown: {0}")]
UnknownRecordTypeValue(u16),
#[error("unrecognized label code: {0:b}")]
UnrecognizedLabelCode(u8),
#[error("nsec3 flags should be 0b0000000*: {0:b}")]
UnrecognizedNsec3Flags(u8),
#[error("csync flags should be 0b000000**: {0:b}")]
UnrecognizedCsyncFlags(u16),
#[error("io error: {0}")]
Io(Arc<io::Error>),
#[error("lock poisoned error")]
Poisoned,
#[error("request refused")]
RequestRefused,
#[error("ring error: {0}")]
Ring(#[from] Unspecified),
#[error("ssl error: {0}")]
SSL(#[from] SslErrorStack),
#[error("timer error")]
Timer,
#[error("request timed out")]
Timeout,
#[error("Tsig key wrong key error")]
TsigWrongKey,
#[cfg(feature = "dnssec")]
#[error("Tsig unsupported mac algorithm")]
TsigUnsupportedMacAlgorithm(TsigAlgorithm),
#[error("url parsing error")]
UrlParsing(#[from] url::ParseError),
#[error("error parsing utf8 string")]
Utf8(#[from] std::str::Utf8Error),
#[error("error parsing utf8 string")]
FromUtf8(#[from] std::string::FromUtf8Error),
#[error("error parsing int")]
ParseInt(#[from] std::num::ParseIntError),
#[cfg(feature = "dns-over-quic")]
#[error("error creating quic connection: {0}")]
QuinnConnect(#[from] quinn::ConnectError),
#[cfg(feature = "dns-over-quic")]
#[error("error with quic connection: {0}")]
QuinnConnection(#[from] quinn::ConnectionError),
#[cfg(feature = "dns-over-quic")]
#[error("error writing to quic connection: {0}")]
QuinnWriteError(#[from] quinn::WriteError),
#[cfg(feature = "dns-over-quic")]
#[error("error writing to quic read: {0}")]
QuinnReadError(#[from] quinn::ReadExactError),
#[cfg(feature = "dns-over-quic")]
#[error("referenced a closed QUIC stream: {0}")]
QuinnStreamError(#[from] quinn::ClosedStream),
#[cfg(feature = "dns-over-quic")]
#[error("error constructing quic configuration: {0}")]
QuinnConfigError(#[from] quinn::ConfigError),
#[cfg(feature = "dns-over-quic")]
#[error("QUIC TLS config must include an AES-128-GCM cipher suite")]
QuinnTlsConfigError(#[from] quinn::crypto::rustls::NoInitialCipherSuite),
#[cfg(feature = "dns-over-quic")]
#[error("an unknown quic stream was used")]
QuinnUnknownStreamError,
#[cfg(feature = "dns-over-quic")]
#[error("quic messages should always be 0, got: {0}")]
QuicMessageIdNot0(u16),
#[cfg(feature = "rustls")]
#[error("rustls construction error: {0}")]
RustlsError(#[from] rustls::Error),
#[cfg(all(feature = "native-certs", not(feature = "webpki-roots")))]
#[error("no valid certificates found in the native root store")]
NativeCerts,
}
#[derive(Clone, Debug)]
pub struct ForwardNSData {
pub ns: Record,
pub glue: Vec<Record>,
}
#[derive(Error, Clone, Debug)]
#[non_exhaustive]
pub struct ProtoError {
pub kind: Box<ProtoErrorKind>,
#[cfg(feature = "backtrace")]
pub backtrack: Option<ExtBacktrace>,
}
impl ProtoError {
#[inline]
pub fn nx_error(
query: Query,
soa: Option<Record<SOA>>,
ns: Option<Vec<ForwardNSData>>,
negative_ttl: Option<u32>,
response_code: ResponseCode,
trusted: bool,
) -> Self {
ProtoErrorKind::NoRecordsFound {
query: Box::new(query),
soa: soa.map(Box::new),
ns,
negative_ttl,
response_code,
trusted,
}
.into()
}
#[inline]
pub fn kind(&self) -> &ProtoErrorKind {
&self.kind
}
#[inline]
pub fn is_busy(&self) -> bool {
matches!(*self.kind, ProtoErrorKind::Busy)
}
#[inline]
pub fn is_no_connections(&self) -> bool {
matches!(*self.kind, ProtoErrorKind::NoConnections)
}
#[inline]
pub fn is_io(&self) -> bool {
matches!(*self.kind, ProtoErrorKind::Io(..))
}
pub(crate) fn as_dyn(&self) -> &(dyn std::error::Error + 'static) {
self
}
pub fn from_response(response: DnsResponse, trust_nx: bool) -> Result<DnsResponse, Self> {
use ResponseCode::*;
debug!("Response:{}", *response);
match response.response_code() {
code @ ServFail
| code @ Refused
| 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 => {
let response = response;
let soa = response.soa().as_ref().map(RecordRef::to_owned);
let query = response.queries().iter().next().cloned().unwrap_or_default();
let error_kind = ProtoErrorKind::NoRecordsFound {
query: Box::new(query),
ns: None,
soa: soa.map(Box::new),
negative_ttl: None,
response_code: code,
trusted: false,
};
Err(Self::from(error_kind))
}
code @ NXDomain |
code @ NoError
if !response.contains_answer() && !response.truncated() => {
let response = response;
let soa = response.soa().as_ref().map(RecordRef::to_owned);
let mut referral_name_servers = vec![];
for ns in response.name_servers().iter().filter(|ns| ns.record_type() == RecordType::NS) {
let glue = response
.additionals()
.iter()
.filter_map(|record| {
if let Some(ns_data) = ns.data().as_ns() {
if *record.name() == **ns_data &&
(record.data().as_a().is_some() || record.data().as_aaaa().is_some()) {
return Some(Record::to_owned(record));
}
}
None
})
.collect::<Vec<Record>>();
referral_name_servers.push(ForwardNSData { ns: Record::to_owned(ns), glue })
}
let option_ns = if !referral_name_servers.is_empty() {
Some(referral_name_servers)
} else {
None
};
let negative_ttl = response.negative_ttl();
let trusted = trust_nx && soa.is_some();
let query = response.into_message().take_queries().drain(..).next().unwrap_or_default();
let error_kind = ProtoErrorKind::NoRecordsFound {
query: Box::new(query),
soa: soa.map(Box::new),
ns: option_ns,
negative_ttl,
response_code: code,
trusted,
};
Err(Self::from(error_kind))
}
NXDomain
| NoError
| Unknown(_) => Ok(response),
}
}
pub fn cmp_specificity(&self, other: &Self) -> Ordering {
let kind = self.kind();
let other = other.kind();
match (kind, other) {
(ProtoErrorKind::NoRecordsFound { .. }, ProtoErrorKind::NoRecordsFound { .. }) => {
return Ordering::Equal
}
(ProtoErrorKind::NoRecordsFound { .. }, _) => return Ordering::Greater,
(_, ProtoErrorKind::NoRecordsFound { .. }) => return Ordering::Less,
_ => (),
}
match (kind, other) {
(ProtoErrorKind::Io { .. }, ProtoErrorKind::Io { .. }) => return Ordering::Equal,
(ProtoErrorKind::Io { .. }, _) => return Ordering::Greater,
(_, ProtoErrorKind::Io { .. }) => return Ordering::Less,
_ => (),
}
match (kind, other) {
(ProtoErrorKind::Timeout, ProtoErrorKind::Timeout) => return Ordering::Equal,
(ProtoErrorKind::Timeout, _) => return Ordering::Greater,
(_, ProtoErrorKind::Timeout) => return Ordering::Less,
_ => (),
}
Ordering::Equal
}
}
impl fmt::Display for ProtoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
cfg_if::cfg_if! {
if #[cfg(feature = "backtrace")] {
if let Some(ref backtrace) = self.backtrack {
fmt::Display::fmt(&self.kind, f)?;
fmt::Debug::fmt(backtrace, f)
} else {
fmt::Display::fmt(&self.kind, f)
}
} else {
fmt::Display::fmt(&self.kind, f)
}
}
}
}
impl<E> From<E> for ProtoError
where
E: Into<ProtoErrorKind>,
{
fn from(error: E) -> Self {
let kind: ProtoErrorKind = error.into();
Self {
kind: Box::new(kind),
#[cfg(feature = "backtrace")]
backtrack: trace!(),
}
}
}
impl From<DecodeError> for ProtoError {
fn from(err: DecodeError) -> Self {
match err {
DecodeError::PointerNotPriorToLabel { idx, ptr } => {
ProtoErrorKind::PointerNotPriorToLabel { idx, ptr }
}
DecodeError::LabelBytesTooLong(len) => ProtoErrorKind::LabelBytesTooLong(len),
DecodeError::UnrecognizedLabelCode(code) => ProtoErrorKind::UnrecognizedLabelCode(code),
DecodeError::DomainNameTooLong(len) => ProtoErrorKind::DomainNameTooLong(len),
DecodeError::LabelOverlapsWithOther { label, other } => {
ProtoErrorKind::LabelOverlapsWithOther { label, other }
}
_ => ProtoErrorKind::Msg(err.to_string()),
}
.into()
}
}
impl From<&'static str> for ProtoError {
fn from(msg: &'static str) -> Self {
ProtoErrorKind::Message(msg).into()
}
}
impl From<String> for ProtoError {
fn from(msg: String) -> Self {
ProtoErrorKind::Msg(msg).into()
}
}
impl From<io::Error> for ProtoErrorKind {
fn from(e: io::Error) -> Self {
match e.kind() {
io::ErrorKind::TimedOut => Self::Timeout,
_ => Self::Io(e.into()),
}
}
}
impl<T> From<sync::PoisonError<T>> for ProtoError {
fn from(_e: sync::PoisonError<T>) -> Self {
ProtoErrorKind::Poisoned.into()
}
}
impl From<ProtoError> for io::Error {
fn from(e: ProtoError) -> Self {
match *e.kind() {
ProtoErrorKind::Timeout => Self::new(io::ErrorKind::TimedOut, e),
_ => Self::new(io::ErrorKind::Other, e),
}
}
}
impl From<ProtoError> for String {
fn from(e: ProtoError) -> Self {
e.to_string()
}
}
#[cfg(feature = "wasm-bindgen")]
#[cfg_attr(docsrs, doc(cfg(feature = "wasm-bindgen")))]
impl From<ProtoError> for wasm_bindgen_crate::JsValue {
fn from(e: ProtoError) -> Self {
js_sys::Error::new(&e.to_string()).into()
}
}
impl Clone for ProtoErrorKind {
fn clone(&self) -> Self {
use self::ProtoErrorKind::*;
match *self {
BadQueryCount(count) => BadQueryCount(count),
Busy => Busy,
Canceled(ref c) => Canceled(*c),
CharacterDataTooLong { max, len } => CharacterDataTooLong { max, len },
LabelOverlapsWithOther { label, other } => LabelOverlapsWithOther { label, other },
DnsKeyProtocolNot3(protocol) => DnsKeyProtocolNot3(protocol),
DomainNameTooLong(len) => DomainNameTooLong(len),
EdnsNameNotRoot(ref found) => EdnsNameNotRoot(found.clone()),
FormError { header, ref error } => FormError {
header,
error: error.clone(),
},
HmacInvalid() => HmacInvalid(),
IncorrectRDataLengthRead { read, len } => IncorrectRDataLengthRead { read, len },
LabelBytesTooLong(len) => LabelBytesTooLong(len),
PointerNotPriorToLabel { idx, ptr } => PointerNotPriorToLabel { idx, ptr },
MaxBufferSizeExceeded(max) => MaxBufferSizeExceeded(max),
Message(msg) => Message(msg),
Msg(ref msg) => Msg(msg.clone()),
NoConnections => NoConnections,
NoError => NoError,
NotAllRecordsWritten { count } => NotAllRecordsWritten { count },
NoRecordsFound {
ref query,
ref soa,
ref ns,
negative_ttl,
response_code,
trusted,
} => NoRecordsFound {
query: query.clone(),
soa: soa.clone(),
ns: ns.clone(),
negative_ttl,
response_code,
trusted,
},
RequestRefused => RequestRefused,
#[cfg(feature = "dnssec")]
Nsec { ref query, proof } => Nsec {
query: query.clone(),
proof,
},
UnknownAlgorithmTypeValue(value) => UnknownAlgorithmTypeValue(value),
UnknownDnsClassStr(ref value) => UnknownDnsClassStr(value.clone()),
UnknownDnsClassValue(value) => UnknownDnsClassValue(value),
UnknownRecordTypeStr(ref value) => UnknownRecordTypeStr(value.clone()),
UnknownRecordTypeValue(value) => UnknownRecordTypeValue(value),
UnrecognizedLabelCode(value) => UnrecognizedLabelCode(value),
UnrecognizedNsec3Flags(flags) => UnrecognizedNsec3Flags(flags),
UnrecognizedCsyncFlags(flags) => UnrecognizedCsyncFlags(flags),
Io(ref e) => Io(e.clone()),
Poisoned => Poisoned,
Ring(ref _e) => Ring(Unspecified),
SSL(ref e) => Msg(format!("there was an SSL error: {e}")),
Timeout => Timeout,
Timer => Timer,
#[cfg(feature = "dnssec")]
TsigUnsupportedMacAlgorithm(ref alg) => TsigUnsupportedMacAlgorithm(alg.clone()),
TsigWrongKey => TsigWrongKey,
UrlParsing(ref e) => UrlParsing(*e),
Utf8(ref e) => Utf8(*e),
FromUtf8(ref e) => FromUtf8(e.clone()),
ParseInt(ref e) => ParseInt(e.clone()),
#[cfg(feature = "dns-over-quic")]
QuinnConnect(ref e) => QuinnConnect(e.clone()),
#[cfg(feature = "dns-over-quic")]
QuinnConnection(ref e) => QuinnConnection(e.clone()),
#[cfg(feature = "dns-over-quic")]
QuinnWriteError(ref e) => QuinnWriteError(e.clone()),
#[cfg(feature = "dns-over-quic")]
QuicMessageIdNot0(val) => QuicMessageIdNot0(val),
#[cfg(feature = "dns-over-quic")]
QuinnReadError(ref e) => QuinnReadError(e.clone()),
#[cfg(feature = "dns-over-quic")]
QuinnStreamError(ref e) => QuinnStreamError(e.clone()),
#[cfg(feature = "dns-over-quic")]
QuinnConfigError(ref e) => QuinnConfigError(e.clone()),
#[cfg(feature = "dns-over-quic")]
QuinnTlsConfigError(ref e) => QuinnTlsConfigError(e.clone()),
#[cfg(feature = "dns-over-quic")]
QuinnUnknownStreamError => QuinnUnknownStreamError,
#[cfg(feature = "rustls")]
RustlsError(ref e) => RustlsError(e.clone()),
#[cfg(all(feature = "native-certs", not(feature = "webpki-roots")))]
NativeCerts => NativeCerts,
}
}
}
pub trait FromProtoError: From<ProtoError> + std::error::Error + Clone {}
impl<E> FromProtoError for E where E: From<ProtoError> + std::error::Error + Clone {}
#[cfg(not(any(feature = "dns-over-openssl", feature = "dnssec-openssl")))]
use self::not_openssl::SslErrorStack;
#[cfg(not(feature = "dnssec-ring"))]
use self::not_ring::{KeyRejected, Unspecified};
#[cfg(any(feature = "dns-over-openssl", feature = "dnssec-openssl"))]
use openssl::error::ErrorStack as SslErrorStack;
#[cfg(feature = "dnssec-ring")]
use ring::error::{KeyRejected, Unspecified};
pub type DnsSecResult<T> = ::std::result::Result<T, DnsSecError>;
#[allow(unreachable_pub)]
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DnsSecErrorKind {
#[error("{0}")]
Message(&'static str),
#[error("{0}")]
Msg(String),
#[error("proto error: {0}")]
Proto(#[from] ProtoError),
#[error("ring error: {0}")]
RingKeyRejected(#[from] KeyRejected),
#[error("ring error: {0}")]
RingUnspecified(#[from] Unspecified),
#[error("ssl error: {0}")]
SSL(#[from] SslErrorStack),
#[error("request timed out")]
Timeout,
}
impl Clone for DnsSecErrorKind {
fn clone(&self) -> Self {
use DnsSecErrorKind::*;
match self {
Message(msg) => Message(msg),
Msg(ref msg) => Msg(msg.clone()),
Proto(proto) => Proto(proto.clone()),
RingKeyRejected(r) => Msg(format!("Ring rejected key: {r}")),
RingUnspecified(_r) => RingUnspecified(Unspecified),
SSL(ssl) => Msg(format!("SSL had an error: {ssl}")),
Timeout => Timeout,
}
}
}
#[derive(Debug, Clone, Error)]
pub struct DnsSecError {
kind: DnsSecErrorKind,
#[cfg(feature = "backtrace")]
backtrack: Option<ExtBacktrace>,
}
impl DnsSecError {
pub fn kind(&self) -> &DnsSecErrorKind {
&self.kind
}
}
impl fmt::Display for DnsSecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
cfg_if::cfg_if! {
if #[cfg(feature = "backtrace")] {
if let Some(ref backtrace) = self.backtrack {
fmt::Display::fmt(&self.kind, f)?;
fmt::Debug::fmt(backtrace, f)
} else {
fmt::Display::fmt(&self.kind, f)
}
} else {
fmt::Display::fmt(&self.kind, f)
}
}
}
}
impl From<DnsSecErrorKind> for DnsSecError {
fn from(kind: DnsSecErrorKind) -> Self {
Self {
kind,
#[cfg(feature = "backtrace")]
backtrack: trace!(),
}
}
}
impl From<&'static str> for DnsSecError {
fn from(msg: &'static str) -> Self {
DnsSecErrorKind::Message(msg).into()
}
}
impl From<String> for DnsSecError {
fn from(msg: String) -> Self {
DnsSecErrorKind::Msg(msg).into()
}
}
impl From<ProtoError> for DnsSecError {
fn from(e: ProtoError) -> Self {
match *e.kind() {
ProtoErrorKind::Timeout => DnsSecErrorKind::Timeout.into(),
_ => DnsSecErrorKind::from(e).into(),
}
}
}
impl From<KeyRejected> for DnsSecError {
fn from(e: KeyRejected) -> Self {
DnsSecErrorKind::from(e).into()
}
}
impl From<Unspecified> for DnsSecError {
fn from(e: Unspecified) -> Self {
DnsSecErrorKind::from(e).into()
}
}
impl From<SslErrorStack> for DnsSecError {
fn from(e: SslErrorStack) -> Self {
DnsSecErrorKind::from(e).into()
}
}
#[doc(hidden)]
#[allow(unreachable_pub)]
#[cfg(not(any(feature = "dns-over-openssl", feature = "dnssec-openssl")))]
#[cfg_attr(
docsrs,
doc(cfg(not(any(feature = "dns-over-openssl", feature = "dnssec-openssl"))))
)]
pub mod not_openssl {
use std;
#[derive(Clone, Copy, Debug)]
pub struct SslErrorStack;
impl std::fmt::Display for SslErrorStack {
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
Ok(())
}
}
impl std::error::Error for SslErrorStack {
fn description(&self) -> &str {
"openssl feature not enabled"
}
}
}
#[doc(hidden)]
#[allow(unreachable_pub)]
#[cfg(not(feature = "dnssec-ring"))]
#[cfg_attr(docsrs, doc(cfg(feature = "dnssec-ring")))]
pub mod not_ring {
use std;
#[derive(Clone, Copy, Debug)]
pub struct KeyRejected;
#[derive(Clone, Copy, Debug)]
pub struct Unspecified;
impl std::fmt::Display for KeyRejected {
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
Ok(())
}
}
impl std::error::Error for KeyRejected {
fn description(&self) -> &str {
"ring feature not enabled"
}
}
impl std::fmt::Display for Unspecified {
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
Ok(())
}
}
impl std::error::Error for Unspecified {
fn description(&self) -> &str {
"ring feature not enabled"
}
}
}