#[cfg(feature = "serde")]
use std::{
borrow::Cow,
fs,
path::{Path, PathBuf},
};
use std::{
collections::HashSet,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::{Arc, atomic::AtomicU8},
time::Instant,
};
use ipnet::IpNet;
#[cfg(feature = "serde")]
use serde::Deserialize;
use tracing::warn;
#[cfg(all(feature = "__dnssec", feature = "metrics"))]
use crate::metrics::recursor::RecursorMetrics;
#[cfg(feature = "tokio")]
use crate::net::runtime::TokioRuntimeProvider;
#[cfg(feature = "serde")]
use crate::proto::{
rr::{RData, RecordSet},
serialize::txt::{ParseError, Parser},
};
use crate::{
ConnectionProvider, NameServerTransportState, PoolContext, TlsConfig, TtlConfig,
config::OpportunisticEncryption,
proto::{
op::{DEFAULT_MAX_PAYLOAD_LEN, Message, Query},
rr::Name,
},
};
#[cfg(feature = "__dnssec")]
use crate::{
ResponseCache,
net::{
DnsError, NetError, NoRecords,
dnssec::DnssecDnsHandle,
xfer::{DnsHandle as _, FirstAnswer as _},
},
proto::{
dnssec::TrustAnchors,
op::{DnsRequestOptions, ResponseCode},
rr::RecordType,
},
};
mod error;
pub use error::{AuthorityData, RecursorError};
mod handle;
use handle::RecursorDnsHandle;
#[cfg(test)]
mod tests;
pub struct Recursor<P: ConnectionProvider> {
pub(super) mode: RecursorMode<P>,
}
#[cfg(feature = "tokio")]
impl Recursor<TokioRuntimeProvider> {}
impl<P: ConnectionProvider> Recursor<P> {
#[cfg(feature = "serde")]
pub fn from_config(
config: RecursiveConfig,
root_dir: Option<&Path>,
conn_provider: P,
) -> Result<Self, RecursorError> {
let dnssec_policy =
DnssecPolicy::from_config(&config.dnssec_policy).map_err(|e| e.to_string())?;
#[allow(unused_mut, unused_assignments)]
let mut encrypted_transport_state = None;
#[cfg(all(feature = "toml", any(feature = "__tls", feature = "__quic")))]
{
encrypted_transport_state =
config.options.opportunistic_encryption.persisted_state()?;
}
let path = match root_dir {
Some(root_dir) => Cow::Owned(root_dir.join(&config.roots)),
None => Cow::Borrowed(&config.roots),
};
let roots_str = fs::read_to_string(path.as_ref()).map_err(|e| {
format!(
"failed to read roots file '{path}': {e}",
path = path.display()
)
})?;
let (_zone, roots_zone) =
Parser::new(roots_str, Some(path.into_owned()), Some(Name::root()))
.parse()
.map_err(|e| format!("failed to read roots {}: {e}", config.roots.display()))?;
let root_addrs = roots_zone
.values()
.flat_map(RecordSet::records_without_rrsigs)
.map(|r| &r.data)
.filter_map(RData::ip_addr) .collect::<Vec<_>>();
Self::new(
&root_addrs,
dnssec_policy,
encrypted_transport_state,
config.options.clone(),
conn_provider,
)
}
pub fn with_options(
roots: &[IpAddr],
options: RecursorOptions,
conn_provider: P,
) -> Result<Self, RecursorError> {
Self::new(roots, DnssecPolicy::default(), None, options, conn_provider)
}
pub fn new(
roots: &[IpAddr],
dnssec_policy: DnssecPolicy,
encrypted_transport_state: Option<NameServerTransportState>,
options: RecursorOptions,
conn_provider: P,
) -> Result<Self, RecursorError> {
let mut tls_config = TlsConfig::new()?;
if options.opportunistic_encryption.is_enabled() {
warn!("disabling TLS peer verification for opportunistic encryption mode");
tls_config.insecure_skip_verify();
}
#[cfg(feature = "__dnssec")]
let response_cache_size = options.response_cache_size;
#[cfg(feature = "__dnssec")]
let ttl_config = options.cache_policy.clone();
let handle = RecursorDnsHandle::new(
roots,
dnssec_policy.clone(),
encrypted_transport_state,
options,
tls_config,
conn_provider,
)?;
Ok(Self {
mode: match dnssec_policy {
DnssecPolicy::SecurityUnaware => RecursorMode::NonValidating { handle },
#[cfg(feature = "__dnssec")]
DnssecPolicy::ValidationDisabled => RecursorMode::NonValidating { handle },
#[cfg(feature = "__dnssec")]
DnssecPolicy::ValidateWithStaticKey(config) => RecursorMode::Validating(
ValidatingRecursor::new(handle, config, response_cache_size, ttl_config)?,
),
},
})
}
pub async fn resolve(
&self,
query: Query,
request_time: Instant,
query_has_dnssec_ok: bool,
) -> Result<Message, RecursorError> {
if !query.name().is_fqdn() {
return Err(RecursorError::from(
"query's domain name must be fully qualified",
));
}
match &self.mode {
RecursorMode::NonValidating { handle } => {
handle
.resolve(
query,
request_time,
query_has_dnssec_ok,
0,
Arc::new(AtomicU8::new(0)),
)
.await
}
#[cfg(feature = "__dnssec")]
RecursorMode::Validating(validating) => {
validating
.resolve(query, request_time, query_has_dnssec_ok)
.await
}
}
}
pub fn pool_context(&self) -> &Arc<PoolContext> {
match &self.mode {
RecursorMode::NonValidating { handle, .. } => handle.pool_context(),
#[cfg(feature = "__dnssec")]
RecursorMode::Validating(validating) => validating.handle.inner().pool_context(),
}
}
pub fn is_validating(&self) -> bool {
!matches!(self.mode, RecursorMode::NonValidating { .. })
}
}
#[allow(clippy::large_enum_variant)]
pub(super) enum RecursorMode<P: ConnectionProvider> {
NonValidating {
handle: RecursorDnsHandle<P>,
},
#[cfg(feature = "__dnssec")]
Validating(ValidatingRecursor<P>),
}
#[cfg(feature = "__dnssec")]
pub(crate) struct ValidatingRecursor<P: ConnectionProvider> {
pub(crate) handle: DnssecDnsHandle<RecursorDnsHandle<P>>,
pub(crate) validated_response_cache: ResponseCache,
#[cfg(feature = "metrics")]
metrics: RecursorMetrics,
}
#[cfg(feature = "__dnssec")]
impl<P: ConnectionProvider> ValidatingRecursor<P> {
pub(crate) fn new(
handle: RecursorDnsHandle<P>,
config: DnssecConfig,
response_cache_size: u64,
ttl_config: TtlConfig,
) -> Result<Self, RecursorError> {
let validated_response_cache = ResponseCache::new(response_cache_size, ttl_config.clone());
let trust_anchor = match config.trust_anchor {
Some(anchor) if anchor.is_empty() => {
return Err(RecursorError::from("trust anchor must not be empty"));
}
Some(anchor) => anchor,
None => Arc::new(TrustAnchors::default()),
};
#[cfg(feature = "metrics")]
let metrics = handle.metrics.clone();
let mut handle = DnssecDnsHandle::with_trust_anchor(handle, trust_anchor)
.nsec3_iteration_limits(
config.nsec3_soft_iteration_limit,
config.nsec3_hard_iteration_limit,
)
.negative_validation_ttl(ttl_config.negative_response_ttl_bounds(RecordType::RRSIG))
.positive_validation_ttl(ttl_config.positive_response_ttl_bounds(RecordType::RRSIG));
if let Some(validation_cache_size) = config.validation_cache_size {
handle = handle.validation_cache_size(validation_cache_size);
}
Ok(Self {
validated_response_cache,
#[cfg(feature = "metrics")]
metrics,
handle,
})
}
async fn resolve(
&self,
query: Query,
request_time: Instant,
query_has_dnssec_ok: bool,
) -> Result<Message, RecursorError> {
if let Some(Ok(response)) = self.validated_response_cache.get(&query, request_time) {
#[cfg(feature = "metrics")]
{
self.metrics.cache_hit_counter.increment(1);
self.metrics
.dnssec_metrics
.increment_proof_counter(&response);
self.metrics
.validated_cache_size
.set(self.validated_response_cache.entry_count() as f64);
}
let none_indeterminate = response
.all_sections()
.all(|record| !record.proof.is_indeterminate());
if response.authoritative && none_indeterminate {
let result = response.maybe_strip_dnssec_records(query_has_dnssec_ok);
#[cfg(feature = "metrics")]
self.metrics
.cache_hit_duration
.record(request_time.elapsed());
return Ok(result);
}
}
let mut options = DnsRequestOptions::default();
options.use_edns = true;
options.edns_set_dnssec_ok = true;
let response = self
.handle
.lookup(query.clone(), options)
.first_answer()
.await?;
if response.response_code == ResponseCode::NXDomain {
use crate::recursor::RecursorError;
let Err(dns_error) = DnsError::from_response(response) else {
return Err(RecursorError::from(
"unable to build ProtoError from response {response:?}",
));
};
Err(RecursorError::Net(NetError::from(dns_error)))
} else if response.answers.is_empty()
&& !response.authorities.is_empty()
&& response.response_code == ResponseCode::NoError
{
let mut no_records = NoRecords::new(query.clone(), ResponseCode::NoError);
no_records.soa = response
.soa()
.as_ref()
.map(|record| Box::new(record.to_owned()));
no_records.authorities = Some(
response
.authorities
.iter()
.filter_map(|x| match x.record_type() {
RecordType::SOA => None,
_ => Some(x.clone()),
})
.collect(),
);
Err(RecursorError::from(NetError::from(no_records)))
} else {
let message = response.into_message();
#[cfg(feature = "metrics")]
self.metrics
.dnssec_metrics
.increment_proof_counter(&message);
self.validated_response_cache
.insert(query.clone(), Ok(message.clone()), request_time);
#[cfg(feature = "metrics")]
self.metrics
.validated_cache_size
.set(self.validated_response_cache.entry_count() as f64);
Ok(message.maybe_strip_dnssec_records(query_has_dnssec_ok))
}
}
}
#[cfg(feature = "serde")]
#[derive(Clone, Deserialize, Eq, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct RecursiveConfig {
pub roots: PathBuf,
#[serde(default)]
pub dnssec_policy: DnssecPolicyConfig,
#[serde(flatten)]
pub options: RecursorOptions,
}
#[cfg_attr(feature = "serde", derive(Deserialize))]
#[derive(Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct RecursorOptions {
#[cfg_attr(feature = "serde", serde(default = "default_ns_cache_size"))]
pub ns_cache_size: usize,
#[cfg_attr(
feature = "serde",
serde(default = "default_response_cache_size", alias = "record_cache_size")
)]
pub response_cache_size: u64,
#[cfg_attr(feature = "serde", serde(default = "recursion_limit_default"))]
pub recursion_limit: u8,
#[cfg_attr(feature = "serde", serde(default = "ns_recursion_limit_default"))]
pub ns_recursion_limit: u8,
#[cfg_attr(feature = "serde", serde(default))]
pub allow_answers: Vec<IpNet>,
#[cfg_attr(feature = "serde", serde(default))]
pub deny_answers: Vec<IpNet>,
#[cfg_attr(feature = "serde", serde(default))]
pub allow_server: Vec<IpNet>,
#[cfg_attr(feature = "serde", serde(default = "deny_server_default"))]
pub deny_server: Vec<IpNet>,
#[cfg_attr(feature = "serde", serde(default))]
pub avoid_local_udp_ports: HashSet<u16>,
#[cfg_attr(feature = "serde", serde(default))]
pub cache_policy: TtlConfig,
#[cfg_attr(feature = "serde", serde(default))]
pub case_randomization: bool,
#[cfg_attr(feature = "serde", serde(default))]
pub opportunistic_encryption: OpportunisticEncryption,
#[cfg_attr(feature = "serde", serde(default = "default_edns_payload_len"))]
pub edns_payload_len: u16,
}
impl Default for RecursorOptions {
fn default() -> Self {
Self {
ns_cache_size: 1_024,
response_cache_size: 1_048_576,
recursion_limit: 24,
ns_recursion_limit: 24,
allow_answers: Vec::new(),
deny_answers: Vec::new(),
allow_server: Vec::new(),
deny_server: RECOMMENDED_SERVER_FILTERS.to_vec(),
avoid_local_udp_ports: HashSet::new(),
cache_policy: TtlConfig::default(),
case_randomization: false,
opportunistic_encryption: OpportunisticEncryption::default(),
edns_payload_len: default_edns_payload_len(),
}
}
}
#[cfg(feature = "serde")]
fn default_ns_cache_size() -> usize {
1_024
}
#[cfg(feature = "serde")]
fn default_response_cache_size() -> u64 {
1_048_576
}
#[cfg(feature = "serde")]
fn recursion_limit_default() -> u8 {
24
}
#[cfg(feature = "serde")]
fn ns_recursion_limit_default() -> u8 {
24
}
#[cfg(feature = "serde")]
fn deny_server_default() -> Vec<IpNet> {
RECOMMENDED_SERVER_FILTERS.to_vec()
}
fn default_edns_payload_len() -> u16 {
DEFAULT_MAX_PAYLOAD_LEN
}
#[derive(Clone, Default)]
pub enum DnssecPolicy {
#[default]
SecurityUnaware,
#[cfg(feature = "__dnssec")]
ValidationDisabled,
#[cfg(feature = "__dnssec")]
ValidateWithStaticKey(DnssecConfig),
}
impl DnssecPolicy {
#[cfg(feature = "serde")]
fn from_config(config: &DnssecPolicyConfig) -> Result<Self, ParseError> {
Ok(match config {
DnssecPolicyConfig::SecurityUnaware => Self::SecurityUnaware,
#[cfg(feature = "__dnssec")]
DnssecPolicyConfig::ValidationDisabled => Self::ValidationDisabled,
#[cfg(feature = "__dnssec")]
DnssecPolicyConfig::ValidateWithStaticKey {
path,
nsec3_soft_iteration_limit,
nsec3_hard_iteration_limit,
validation_cache_size,
} => Self::ValidateWithStaticKey(DnssecConfig {
trust_anchor: path
.as_ref()
.map(|path| TrustAnchors::from_file(path))
.transpose()?
.map(Arc::new),
nsec3_soft_iteration_limit: *nsec3_soft_iteration_limit,
nsec3_hard_iteration_limit: *nsec3_hard_iteration_limit,
validation_cache_size: *validation_cache_size,
}),
})
}
pub(crate) fn is_security_aware(&self) -> bool {
!matches!(self, Self::SecurityUnaware)
}
}
#[cfg(feature = "__dnssec")]
#[non_exhaustive]
#[derive(Clone, Default)]
pub struct DnssecConfig {
pub trust_anchor: Option<Arc<TrustAnchors>>,
pub nsec3_soft_iteration_limit: Option<u16>,
pub nsec3_hard_iteration_limit: Option<u16>,
pub validation_cache_size: Option<usize>,
}
#[cfg(feature = "serde")]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub enum DnssecPolicyConfig {
#[default]
SecurityUnaware,
#[cfg(feature = "__dnssec")]
ValidationDisabled,
#[cfg(feature = "__dnssec")]
ValidateWithStaticKey {
path: Option<PathBuf>,
nsec3_soft_iteration_limit: Option<u16>,
nsec3_hard_iteration_limit: Option<u16>,
validation_cache_size: Option<usize>,
},
}
fn is_subzone(parent: &Name, child: &Name) -> bool {
if parent.is_empty() {
return false;
}
if (parent.is_fqdn() && !child.is_fqdn()) || (!parent.is_fqdn() && child.is_fqdn()) {
return false;
}
parent.zone_of(child)
}
const RECOMMENDED_SERVER_FILTERS: [IpNet; 22] = [
IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 0)), 8), IpNet::new_assert(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8), IpNet::new_assert(IpAddr::V4(Ipv4Addr::BROADCAST), 32), IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)), 8), IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 0)), 12), IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 0)), 16), IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(100, 64, 0, 0)), 10), IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(169, 254, 0, 0)), 16), IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(192, 0, 0, 0)), 24), IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 0)), 24), IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 0)), 24), IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 0)), 24), IpNet::new_assert(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 0)), 4), IpNet::new_assert(IpAddr::V6(Ipv6Addr::LOCALHOST), 128), IpNet::new_assert(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 128), IpNet::new_assert(IpAddr::V6(Ipv6Addr::new(0x100, 0, 0, 0, 0, 0, 0, 0)), 64), IpNet::new_assert(
IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)),
32,
), IpNet::new_assert(IpAddr::V6(Ipv6Addr::new(0x3fff, 0, 0, 0, 0, 0, 0, 0)), 20), IpNet::new_assert(IpAddr::V6(Ipv6Addr::new(0x5f00, 0, 0, 0, 0, 0, 0, 0)), 16), IpNet::new_assert(IpAddr::V6(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 0)), 7), IpNet::new_assert(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0)), 64), IpNet::new_assert(IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 0)), 8), ];