initial recursive resolution working

This commit is contained in:
Benjamin Fry 2022-05-21 18:30:05 -07:00
parent 25861da44e
commit a84deaca12
19 changed files with 401 additions and 214 deletions

View File

@ -69,8 +69,9 @@ impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"{id}:{flags}:{code:?}:{op_code}:{answers}/{authorities}/{additionals}",
"{id}:{message_type}:{flags}:{code:?}:{op_code}:{answers}/{authorities}/{additionals}",
id = self.id,
message_type = self.message_type,
flags = self.flags(),
code = self.response_code,
op_code = self.op_code,

View File

@ -413,6 +413,14 @@ impl Message {
self.header.response_code()
}
/// Returns the query from this Message.
///
/// In almost all cases, a Message will only contain one query. This is a convenience function to get the single query.
/// See the alternative `queries*` methods for the raw set of queries in the Message
pub fn query(&self) -> Option<&Query> {
self.queries.first()
}
/// ```text
/// Question Carries the query name and other query parameters.
/// ```
@ -1078,12 +1086,17 @@ impl fmt::Display for Message {
writeln!(f, "; query")?;
write_query(self.queries(), f)?;
writeln!(f, "; answers {}", self.answer_count())?;
write_slice(self.answers(), f)?;
writeln!(f, "; nameservers {}", self.name_server_count())?;
write_slice(self.name_servers(), f)?;
writeln!(f, "; additionals {}", self.additional_count())?;
write_slice(self.additionals(), f)?;
if self.header().message_type() == MessageType::Response
|| self.header().op_code() == OpCode::Update
{
writeln!(f, "; answers {}", self.answer_count())?;
write_slice(self.answers(), f)?;
writeln!(f, "; nameservers {}", self.name_server_count())?;
write_slice(self.name_servers(), f)?;
writeln!(f, "; additionals {}", self.additional_count())?;
write_slice(self.additionals(), f)?;
}
Ok(())
}

View File

@ -989,6 +989,11 @@ impl RData {
_ => None,
}
}
/// Returns true if
pub fn is_soa(&self) -> bool {
matches!(self, RData::SOA(..))
}
}
impl fmt::Display for RData {

View File

@ -32,8 +32,7 @@ pub struct DnsRequestOptions {
impl Default for DnsRequestOptions {
fn default() -> Self {
#[allow(deprecated)]
DnsRequestOptions {
max_request_depth: 26,
Self {
expects_multiple_responses: false,
use_edns: false,
recursion_desired: true,

View File

@ -19,8 +19,7 @@ use futures_util::stream::Stream;
use crate::error::{ProtoError, ProtoErrorKind, ProtoResult};
use crate::op::{Message, ResponseCode};
use crate::rr::rdata::SOA;
use crate::rr::{RData, RecordType};
use crate::rr::{RData, Record, RecordType};
/// A stream returning DNS responses
pub struct DnsResponseStream {
@ -133,12 +132,10 @@ pub struct DnsResponse(Message);
// TODO: when `impl Trait` lands in stable, remove this, and expose FlatMap over answers, et al.
impl DnsResponse {
/// Retrieves the SOA from the response. This will only exist if it was an authoritative response.
pub fn soa(&self) -> Option<SOA> {
pub fn soa(&self) -> Option<&Record> {
self.name_servers()
.iter()
.filter_map(|record| record.data().and_then(RData::as_soa))
.next()
.cloned()
.find(|record| record.data().map(|d| d.is_soa()).unwrap_or(false))
}
/// Looks in the authority section for an SOA record from the response, and returns the negative_ttl, None if not available.

View File

@ -9,15 +9,16 @@
#![deny(missing_docs)]
use std::{fmt, io, sync};
use std::{fmt, io};
use enum_as_inner::EnumAsInner;
use thiserror::Error;
use trust_dns_resolver::Name;
#[cfg(feature = "backtrace")]
use crate::proto::{trace, ExtBacktrace};
use crate::{
proto::error::{ProtoError, ProtoErrorKind},
proto::error::ProtoError,
resolver::error::{ResolveError, ResolveErrorKind},
};
@ -33,6 +34,10 @@ pub enum ErrorKind {
#[error("{0}")]
Msg(String),
/// An name error of some kind happened
#[error("forward response: {0}")]
Forward(Name),
/// An error got returned from IO
#[error("io error: {0}")]
Io(#[from] std::io::Error),
@ -43,7 +48,7 @@ pub enum ErrorKind {
/// An error got returned by the trust-dns-proto crate
#[error("proto error: {0}")]
Resolve(#[from] ResolveError),
Resolve(ResolveError),
/// A request timed out
#[error("request timed out")]
@ -127,11 +132,16 @@ impl From<Error> for String {
}
}
#[cfg(feature = "wasm-bindgen")]
#[cfg_attr(docsrs, doc(cfg(feature = "wasm-bindgen")))]
impl From<Error> for wasm_bindgen_crate::JsValue {
fn from(e: Error) -> Self {
js_sys::Error::new(&e.to_string()).into()
impl From<ResolveError> for Error {
fn from(e: ResolveError) -> Self {
if let ResolveErrorKind::NoRecordsFound { soa, .. } = e.kind() {
match soa {
Some(soa) => ErrorKind::Forward(soa.name().clone()).into(),
_ => ErrorKind::Resolve(e).into(),
}
} else {
ErrorKind::Resolve(e).into()
}
}
}
@ -141,9 +151,10 @@ impl Clone for ErrorKind {
match *self {
Message(msg) => Message(msg),
Msg(ref msg) => Msg(msg.clone()),
Io(ref io) => Self::from(std::io::Error::from(io.kind())),
Proto(ref proto) => Self::from(proto.clone()),
Resolve(ref resolve) => Self::from(resolve.clone()),
Forward(ref ns) => Forward(ns.clone()),
Io(ref io) => Io(std::io::Error::from(io.kind())),
Proto(ref proto) => Proto(proto.clone()),
Resolve(ref resolve) => Resolve(resolve.clone()),
Timeout => Self::Timeout,
}
}

View File

@ -1,3 +1,10 @@
// Copyright 2015-2022 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
//! A recursive DNS resolver based on the Trust-DNS (stub) resolver
#![warn(
@ -21,6 +28,7 @@
pub mod error;
mod recursor;
pub(crate) mod recursor_pool;
pub use error::{Error, ErrorKind};
pub use recursor::Recursor;

View File

@ -1,53 +1,42 @@
use std::{
collections::{BTreeSet, HashMap},
fmt,
net::{IpAddr, SocketAddr},
time::Instant,
};
// Copyright 2015-2022 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use std::{net::SocketAddr, time::Instant};
use async_recursion::async_recursion;
use futures_util::{future::select_all, Future, FutureExt, StreamExt};
use futures_util::{future::select_all, FutureExt};
use lru_cache::LruCache;
use parking_lot::Mutex;
use tracing::{debug, dispatcher::SetGlobalDefaultError, info, warn};
use tracing::{debug, info, warn};
use trust_dns_proto::{
op::{Message, Query},
rr::{RData, Record, RecordSet, RecordType},
xfer::{DnsRequestOptions, DnsResponse},
DnsHandle,
op::Query,
rr::{RData, RecordType},
};
use trust_dns_resolver::{
config::{NameServerConfig, NameServerConfigGroup, Protocol, ResolverConfig, ResolverOpts},
error::{ResolveError, ResolveErrorKind},
config::{NameServerConfig, NameServerConfigGroup, Protocol, ResolverOpts},
dns_lru::{DnsLru, TtlConfig},
error::ResolveError,
lookup::Lookup,
name_server::{GenericConnectionProvider, NameServerPool, RuntimeProvider, TokioRuntime},
IntoName, Name, TokioAsyncResolver, TokioConnection, TokioConnectionProvider, TokioHandle,
name_server::NameServerPool,
Name, TokioConnectionProvider, TokioHandle,
};
use crate::{Error, ErrorKind};
use crate::{recursor_pool::RecursorPool, Error, ErrorKind};
/// Set of nameservers by the zone name
type NameServerCache = LruCache<Name, NameServerPool<TokioConnection, TokioConnectionProvider>>;
/// Records that have been found
///
/// We will cache Message responses for simplicity
type MessageCache = LruCache<RecursiveQuery, DnsResponse>;
/// Active request cache
///
/// The futures are Shared so any waiting on these results will resolve to the same result
type ActiveRequests =
HashMap<RecursiveQuery, Box<dyn Future<Output = Result<Message, ResolveError>>>>;
type NameServerCache = LruCache<Name, RecursorPool>;
/// A top down recursive resolver which operates off a list of "hints", this is often the root nodes.
pub struct Recursor {
hints: NameServerPool<TokioConnection, TokioConnectionProvider>,
opts: ResolverOpts,
hints: RecursorPool,
name_server_cache: Mutex<NameServerCache>,
message_cache: Mutex<MessageCache>,
record_cache: DnsLru,
}
impl Recursor {
@ -58,7 +47,6 @@ impl Recursor {
/// This will panic if the hints are empty.
pub fn new(hints: impl Into<NameServerConfigGroup>) -> Result<Self, ResolveError> {
// configure the trust-dns-resolver
let mut config = ResolverConfig::new();
let hints: NameServerConfigGroup = hints.into();
assert!(!hints.is_empty(), "hints must not be empty");
@ -66,14 +54,14 @@ impl Recursor {
let opts = recursor_opts();
let hints =
NameServerPool::from_config(hints, &opts, TokioConnectionProvider::new(TokioHandle));
let hints = RecursorPool::from(Name::root(), hints);
let name_server_cache = Mutex::new(NameServerCache::new(100)); // TODO: make this configurable
let message_cache = Mutex::new(MessageCache::new(100));
let record_cache = DnsLru::new(100, TtlConfig::default());
Ok(Self {
hints,
opts,
name_server_cache,
message_cache,
record_cache,
})
}
@ -238,64 +226,81 @@ impl Recursor {
/// has contiguous zones at the root and MIL domains, but also has a non-
/// contiguous zone at ISI.EDU.
/// ```
pub async fn resolve(
&self,
domain: Name,
ty: RecordType,
request_time: Instant,
) -> Result<DnsResponse, Error> {
// wild guess on number fo lookups needed
let lookup: RecursiveQuery = (domain, ty).into();
pub async fn resolve(&self, query: Query, request_time: Instant) -> Result<Lookup, Error> {
if let Some(lookup) = self.record_cache.get(&query, request_time) {
return lookup.map_err(Into::into);
}
// not in cache, let's look for an ns record for lookup
let zone = if lookup.ty == RecordType::NS {
lookup.domain.base_name()
let zone = if query.query_type() == RecordType::NS {
query.name().base_name()
} else {
// look for the NS records "inside" the zone
lookup.domain.clone()
query.name().clone()
};
let ns = self.get_ns_pool_for_zone(zone, request_time).await?;
let mut zone = zone;
let mut ns = None;
let response = self.lookup(lookup, ns).await?;
// max number of forwarding processes
'max_forward: for _ in 0..20 {
match self.get_ns_pool_for_zone(zone.clone(), request_time).await {
Ok(found) => {
// found the nameserver
ns = Some(found);
break 'max_forward;
}
Err(e) => match e.kind() {
ErrorKind::Forward(name) => {
// if we already had this name, don't try again
if &zone == name {
debug!("zone previously searched for {name}");
break 'max_forward;
};
debug!("ns forwarded to {name}");
zone = name.clone();
}
_ => return Err(e),
},
}
}
let ns = ns.ok_or_else(|| Error::from(format!("no nameserver found for {}", zone)))?;
debug!("found zone {} for {}", ns.zone(), query);
let response = self.lookup(query, ns, request_time).await?;
Ok(response)
}
async fn lookup(
&self,
query: RecursiveQuery,
mut ns: NameServerPool<TokioConnection, TokioConnectionProvider>,
) -> Result<DnsResponse, Error> {
if let Some(cached) = self.message_cache.lock().get_mut(&query) {
return Ok(cached.clone());
async fn lookup(&self, query: Query, ns: RecursorPool, now: Instant) -> Result<Lookup, Error> {
if let Some(lookup) = self.record_cache.get(&query, now) {
debug!("cached data {:?}", lookup);
return lookup.map_err(Into::into);
}
info!("querying: {}", query);
let query = Query::query(query.domain, query.ty);
let mut options = DnsRequestOptions::default();
options.use_edns = false; // TODO: this should be configurable
options.recursion_desired = false;
let mut response = ns.lookup(query, options);
let response = ns.lookup(query.clone());
// TODO: we are only expecting one response
// TODO: should we change DnsHandle to always be a single response? And build a totally custom handler for other situations?
if let Some(response) = response.next().await {
// TODO: check if data is "authentic"
match response {
Ok(r) => {
info!("response: {}", r.header());
debug!("response: {}", *r);
Ok(r)
}
Err(e) => {
warn!("lookup error: {}", e);
Err(ErrorKind::from(e).into())
}
// TODO: check if data is "authentic"
match response.await {
Ok(mut r) => {
info!("response: {}", r.header());
let records = r
.take_answers()
.into_iter()
.chain(r.take_name_servers())
.chain(r.take_additionals());
let lookup = self.record_cache.insert_records(query, records, now);
lookup.ok_or_else(|| Error::from("no records found"))
}
Err(e) => {
warn!("lookup error: {}", e);
Err(Error::from(e))
}
} else {
Err("no responses from nameserver".into())
}
}
@ -304,7 +309,7 @@ impl Recursor {
&self,
zone: Name,
request_time: Instant,
) -> Result<NameServerPool<TokioConnection, TokioConnectionProvider>, Error> {
) -> Result<RecursorPool, Error> {
// TODO: need to check TTLs here.
if let Some(ns) = self.name_server_cache.lock().get_mut(&zone) {
return Ok(ns.clone());
@ -313,7 +318,7 @@ impl Recursor {
let parent_zone = zone.base_name();
let nameserver_pool = if parent_zone.is_root() {
debug!("using ROOTS for {zone} nameservers");
debug!("using hints for {zone} nameservers");
self.hints.clone()
} else {
self.get_ns_pool_for_zone(parent_zone, request_time).await?
@ -321,11 +326,13 @@ impl Recursor {
// TODO: check for cached ns pool for this zone
let lookup = RecursiveQuery::from((zone.clone(), RecordType::NS));
let response = self.lookup(lookup.clone(), nameserver_pool).await?;
let lookup = Query::query(zone.clone(), RecordType::NS);
let response = self
.lookup(lookup.clone(), nameserver_pool.clone(), request_time)
.await?;
let zone_nameservers = response.name_servers();
let glue = response.additionals();
// let zone_nameservers = response.name_servers();
// let glue = response.additionals();
// TODO: grab TTL and use for cache
// get all the NS records and glue
@ -333,22 +340,38 @@ impl Recursor {
let mut need_ips_for_names = Vec::new();
// unpack all glued records
for zns in zone_nameservers {
for zns in response.record_iter() {
if let Some(ns_data) = zns.data().and_then(RData::as_ns) {
let glue_ips = glue
.iter()
.filter(|g| g.name() == ns_data)
.map(Record::data)
.filter_map(|g| match g {
Some(RData::A(a)) => Some(IpAddr::from(*a)),
Some(RData::AAAA(aaaa)) => Some(IpAddr::from(*aaaa)),
_ => None,
});
// let glue_ips = glue
// .iter()
// .filter(|g| g.name() == ns_data)
// .filter_map(Record::data)
// .filter_map(RData::to_ip_addr);
let cached_a = self
.record_cache
.get(&Query::query(ns_data.clone(), RecordType::A), request_time);
let cached_aaaa = self.record_cache.get(
&Query::query(ns_data.clone(), RecordType::AAAA),
request_time,
);
let cached_a = cached_a.and_then(Result::ok).map(Lookup::into_iter);
let cached_aaaa = cached_aaaa.and_then(Result::ok).map(Lookup::into_iter);
let glue_ips = cached_a
.into_iter()
.flatten()
.chain(cached_aaaa.into_iter().flatten())
.filter_map(|r| RData::to_ip_addr(&r));
let mut had_glue = false;
for ip in glue_ips {
let udp = NameServerConfig::new(SocketAddr::from((ip, 53)), Protocol::Udp);
let tcp = NameServerConfig::new(SocketAddr::from((ip, 53)), Protocol::Tcp);
let mut udp = NameServerConfig::new(SocketAddr::from((ip, 53)), Protocol::Udp);
let mut tcp = NameServerConfig::new(SocketAddr::from((ip, 53)), Protocol::Tcp);
udp.trust_nx_responses = true;
tcp.trust_nx_responses = true;
config_group.push(udp);
config_group.push(tcp);
@ -363,15 +386,17 @@ impl Recursor {
}
// collect missing IP addresses, select over them all, get the addresses
if !need_ips_for_names.is_empty() {
// make it configurable to query for all records?
if config_group.is_empty() && !need_ips_for_names.is_empty() {
debug!("need glue for {zone}");
let a_resolves = need_ips_for_names.iter().take(1).map(|name| {
self.resolve((*name).clone(), RecordType::A, request_time)
.boxed()
let a_query = Query::query((*name).clone(), RecordType::A);
self.resolve(a_query, request_time).boxed()
});
let aaaa_resolves = need_ips_for_names.iter().take(1).map(|name| {
self.resolve((*name).clone(), RecordType::AAAA, request_time)
.boxed()
let aaaa_query = Query::query((*name).clone(), RecordType::AAAA);
self.resolve(aaaa_query, request_time).boxed()
});
let mut a_resolves: Vec<_> = a_resolves.chain(aaaa_resolves).collect();
@ -381,17 +406,8 @@ impl Recursor {
match next {
Ok(response) => {
debug!("A or AAAA response: {}", *response);
let ips =
response
.answers()
.iter()
.map(Record::data)
.filter_map(|d| match d {
Some(RData::A(a)) => Some(IpAddr::from(*a)),
Some(RData::AAAA(aaaa)) => Some(IpAddr::from(*aaaa)),
_ => None,
});
debug!("A or AAAA response: {:?}", response);
let ips = response.iter().filter_map(RData::to_ip_addr);
for ip in ips {
let udp =
@ -416,34 +432,15 @@ impl Recursor {
&recursor_opts(),
TokioConnectionProvider::new(TokioHandle),
);
let ns = RecursorPool::from(zone.clone(), ns);
// store in cache for future usage
debug!("found nameservers for {}", zone);
self.name_server_cache.lock().insert(zone, ns.clone());
Ok(ns)
}
}
#[derive(Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
struct RecursiveQuery {
domain: Name,
ty: RecordType,
}
impl From<(Name, RecordType)> for RecursiveQuery {
fn from(name_ty: (Name, RecordType)) -> Self {
Self {
domain: name_ty.0,
ty: name_ty.1,
}
}
}
impl fmt::Display for RecursiveQuery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "({},{})", self.domain, self.ty)
}
}
fn recursor_opts() -> ResolverOpts {
let mut options = ResolverOpts::default();
options.ndots = 0;
@ -455,8 +452,3 @@ fn recursor_opts() -> ResolverOpts {
options
}
enum RecursiveLookup {
Found(RecordSet),
Forward(RecordSet),
}

View File

@ -0,0 +1,112 @@
// Copyright 2015-2022 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use std::{
collections::HashMap,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use futures_util::{future::Shared, Future, FutureExt, StreamExt};
use parking_lot::Mutex;
use tracing::info;
use trust_dns_proto::{
op::Query,
xfer::{DnsRequestOptions, DnsResponse},
DnsHandle,
};
use trust_dns_resolver::{
error::{ResolveError, ResolveErrorKind},
name_server::NameServerPool,
Name, TokioConnection, TokioConnectionProvider,
};
/// Active request cache
///
/// The futures are Shared so any waiting on these results will resolve to the same result
type ActiveRequests = HashMap<Query, SharedLookup>;
type DnsResponseFuture =
Box<dyn Future<Output = Option<Result<DnsResponse, ResolveError>>> + Send + 'static>;
#[derive(Clone)]
pub(crate) struct SharedLookup(Shared<Pin<DnsResponseFuture>>);
impl Future for SharedLookup {
type Output = Result<DnsResponse, ResolveError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.0.poll_unpin(cx).map(|o| match o {
Some(r) => r,
None => Err(ResolveErrorKind::Message("no response from nameserver").into()),
})
}
}
#[derive(Clone)]
pub(crate) struct RecursorPool {
zone: Name,
ns: NameServerPool<TokioConnection, TokioConnectionProvider>,
active_requests: Arc<Mutex<ActiveRequests>>,
}
impl RecursorPool {
pub(crate) fn from(
zone: Name,
ns: NameServerPool<TokioConnection, TokioConnectionProvider>,
) -> Self {
let active_requests = Arc::new(Mutex::new(ActiveRequests::default()));
Self {
zone,
ns,
active_requests,
}
}
pub(crate) fn zone(&self) -> &Name {
&self.zone
}
pub(crate) async fn lookup(&self, query: Query) -> Result<DnsResponse, ResolveError> {
let mut ns = self.ns.clone();
let query_cpy = query.clone();
// block concurrent requests
let lookup = self
.active_requests
.lock()
.entry(query.clone())
.or_insert_with(move || {
info!("querying {} for {}", self.zone, query_cpy);
let mut options = DnsRequestOptions::default();
options.use_edns = false; // TODO: this should be configurable
options.recursion_desired = false;
// convert the lookup into a shared future
let lookup = ns
.lookup(query_cpy, options)
.into_future()
.map(|(next, _)| next)
.boxed()
.shared();
SharedLookup(lookup)
})
.clone();
let result = lookup.await;
// remove the concurrent request marker
self.active_requests.lock().remove(&query);
result
}
}

View File

@ -23,7 +23,6 @@ use proto::rr::domain::usage::{
ResolverUsage, DEFAULT, INVALID, IN_ADDR_ARPA_127, IP6_ARPA_1, LOCAL,
LOCALHOST as LOCALHOST_usage, ONION,
};
use proto::rr::rdata::SOA;
use proto::rr::{DNSClass, Name, RData, Record, RecordType};
use proto::xfer::{DnsHandle, DnsRequestOptions, DnsResponse, FirstAnswer};
@ -269,7 +268,7 @@ where
is_dnssec: bool,
valid_nsec: bool,
query: Query,
soa: Option<SOA>,
soa: Option<Record>,
negative_ttl: Option<u32>,
response_code: ResponseCode,
trusted: bool,
@ -310,7 +309,7 @@ where
const INITIAL_TTL: u32 = dns_lru::MAX_TTL;
// need to capture these before the subsequent and destructive record processing
let soa = response.soa();
let soa = response.soa().cloned();
let negative_ttl = response.negative_ttl();
let response_code = response.response_code();

View File

@ -7,6 +7,7 @@
//! An LRU cache designed for work with DNS lookups
use std::collections::HashMap;
use std::convert::TryFrom;
use std::sync::Arc;
use std::time::{Duration, Instant};
@ -44,8 +45,9 @@ impl LruValue {
}
}
/// And LRU eviction cache specifically for storing DNS records
#[derive(Clone, Debug)]
pub(crate) struct DnsLru {
pub struct DnsLru {
cache: Arc<Mutex<LruCache<Query, LruValue>>>,
/// A minimum TTL value for positive responses.
///
@ -93,7 +95,7 @@ pub(crate) struct DnsLru {
/// shouldn't cause any issue as this will never be used in serialization,
/// but understand that this would be outside the standard range.
#[derive(Copy, Clone, Debug, Default)]
pub(crate) struct TtlConfig {
pub struct TtlConfig {
/// An optional minimum TTL value for positive responses.
///
/// Positive responses with TTLs under `positive_min_ttl` will use
@ -117,7 +119,8 @@ pub(crate) struct TtlConfig {
}
impl TtlConfig {
pub(crate) fn from_opts(opts: &config::ResolverOpts) -> Self {
/// Construct the LRU based on the ResolverOpts configuration
pub fn from_opts(opts: &config::ResolverOpts) -> Self {
Self {
positive_min_ttl: opts.positive_min_ttl,
negative_min_ttl: opts.negative_min_ttl,
@ -128,7 +131,13 @@ impl TtlConfig {
}
impl DnsLru {
pub(crate) fn new(capacity: usize, ttl_cfg: TtlConfig) -> Self {
/// Construct a new cache
///
/// # Arguments
///
/// * `capacity` - size in number of records, this can be the max size of 2048 (record size) * `capacity`
/// * `ttl_cfg` - force minimums and maximums for cached records
pub fn new(capacity: usize, ttl_cfg: TtlConfig) -> Self {
let TtlConfig {
positive_min_ttl,
negative_min_ttl,
@ -187,6 +196,54 @@ impl DnsLru {
lookup
}
/// inserts a record based on the name and type.
///
/// # Arguments
///
/// * `original_query` - is used for matching the records that should be returned
/// * `records` - the records will be partitioned by type and name for storage in the cache
/// * `now` - current time for use in associating TTLs
///
/// # Return
///
/// This should always return some records, but will be None if there are no records or the original_query matches none
pub fn insert_records(
&self,
original_query: Query,
records: impl Iterator<Item = Record>,
now: Instant,
) -> Option<Lookup> {
// collect all records by name
let records = records.fold(
HashMap::<Query, Vec<(Record, u32)>>::new(),
|mut map, record| {
let mut query = Query::query(record.name().clone(), record.record_type());
query.set_query_class(record.dns_class());
let ttl = record.ttl();
map.entry(query)
.or_insert_with(Vec::default)
.push((record, ttl));
map
},
);
// now insert by record type and name
let mut lookup = None;
for (query, records_and_ttl) in records {
let is_query = original_query == query;
let inserted = self.insert(query, records_and_ttl, now);
if is_query {
lookup = Some(inserted)
}
}
lookup
}
/// Generally for inserting a set of records that have already been cached, but with a different Query.
pub(crate) fn duplicate(&self, query: Query, lookup: Lookup, ttl: u32, now: Instant) -> Lookup {
let ttl = Duration::from_secs(u64::from(ttl));
@ -260,8 +317,8 @@ impl DnsLru {
error
}
/// This needs to be mut b/c it's an LRU, meaning the ordering of elements will potentially change on retrieval...
pub(crate) fn get(&self, query: &Query, now: Instant) -> Option<Result<Lookup, ResolveError>> {
/// Based on the query, see if there are any records available
pub fn get(&self, query: &Query, now: Instant) -> Option<Result<Lookup, ResolveError>> {
let mut out_of_date = false;
let mut cache = self.cache.lock();
let lookup = cache.get_mut(query).and_then(|value| {

View File

@ -12,10 +12,10 @@ use std::{fmt, io, sync};
use thiserror::Error;
use tracing::debug;
use trust_dns_proto::rr::Record;
use crate::proto::error::{ProtoError, ProtoErrorKind};
use crate::proto::op::{Query, ResponseCode};
use crate::proto::rr::rdata::SOA;
use crate::proto::xfer::retry_dns_handle::RetryableError;
use crate::proto::xfer::DnsResponse;
#[cfg(feature = "backtrace")]
@ -42,12 +42,12 @@ pub enum ResolveErrorKind {
NoConnections,
/// No records were found for a query
#[error("no record found for {query}")]
#[error("no record found for {:?}", query)]
NoRecordsFound {
/// The query for which no records were found.
query: Box<Query>,
/// If an SOA is present, then this is an authoritative response.
soa: Option<Box<SOA>>,
/// If an SOA is present, then this is an authoritative response or a referral to another nameserver, see the negative_type field.
soa: Option<Box<Record>>,
/// negative ttl, as determined from DnsResponse::negative_ttl
/// this will only be present if the SOA was also present.
negative_ttl: Option<u32>,
@ -111,7 +111,7 @@ pub struct ResolveError {
impl ResolveError {
pub(crate) fn nx_error(
query: Query,
soa: Option<SOA>,
soa: Option<Record>,
negative_ttl: Option<u32>,
response_code: ResponseCode,
trusted: bool,
@ -145,7 +145,7 @@ impl ResolveError {
/// A conversion to determine if the response is an error
pub fn from_response(response: DnsResponse, trust_nx: bool) -> Result<DnsResponse, Self> {
debug!("Response:{}", response.header());
debug!("Response:{}", *response);
match response.response_code() {
response_code @ ResponseCode::ServFail
@ -167,7 +167,7 @@ impl ResolveError {
| response_code @ ResponseCode::BADTRUNC
| response_code @ ResponseCode::BADCOOKIE => {
let mut response = response;
let soa = response.soa();
let soa = response.soa().cloned();
let query = response.take_queries().drain(..).next().unwrap_or_default();
let error_kind = ResolveErrorKind::NoRecordsFound {
query: Box::new(query),
@ -188,7 +188,7 @@ impl ResolveError {
// let valid_until = if response.is_authoritative() { now + response.get_negative_ttl() };
let mut response = response;
let soa = response.soa();
let soa = response.soa().cloned();
let negative_ttl = response.negative_ttl();
let trusted = if response_code == ResponseCode::NoError { false } else { trust_nx };
let query = response.take_queries().drain(..).next().unwrap_or_default();

View File

@ -258,7 +258,7 @@ pub extern crate trust_dns_proto as proto;
mod async_resolver;
pub mod caching_client;
pub mod config;
mod dns_lru;
pub mod dns_lru;
pub mod dns_sd;
pub mod error;
mod hosts;

View File

@ -88,6 +88,7 @@ pub trait RuntimeProvider: Clone + 'static {
/// A type defines the Handle which can spawn future.
pub trait Spawn {
/// Spawn a future in the background
fn spawn_bg<F>(&mut self, future: F)
where
F: Future<Output = Result<(), ProtoError>> + Send + 'static;
@ -98,6 +99,7 @@ pub trait Spawn {
pub struct GenericConnectionProvider<R: RuntimeProvider>(R::Handle);
impl<R: RuntimeProvider> GenericConnectionProvider<R> {
/// construct a new Connection provider based on the Runtime Handle
pub fn new(handle: R::Handle) -> Self {
Self(handle)
}
@ -379,6 +381,7 @@ pub mod tokio_runtime {
use super::*;
use tokio::net::UdpSocket as TokioUdpSocket;
/// A handle to the Tokio runtime
#[derive(Clone, Copy)]
pub struct TokioHandle;
impl Spawn for TokioHandle {
@ -390,6 +393,7 @@ pub mod tokio_runtime {
}
}
/// The Tokio Runtime for async execution
#[derive(Clone, Copy)]
pub struct TokioRuntime;
impl RuntimeProvider for TokioRuntime {
@ -398,6 +402,10 @@ pub mod tokio_runtime {
type Timer = TokioTime;
type Udp = TokioUdpSocket;
}
/// An alias for Tokio use cases
pub type TokioConnection = GenericConnection;
/// An alias for Tokio use cases
pub type TokioConnectionProvider = GenericConnectionProvider<TokioRuntime>;
}

View File

@ -5,6 +5,8 @@
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
//! A module with associated items for working with nameservers
mod connection_provider;
#[allow(clippy::module_inception)]
mod name_server;

View File

@ -52,12 +52,14 @@ impl<C: DnsHandle<Error = ResolveError>, P: ConnectionProvider<Conn = C>> Debug
#[cfg(feature = "tokio-runtime")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio-runtime")))]
impl NameServer<TokioConnection, TokioConnectionProvider> {
/// A shortcut for constructing a nameserver usable in the Tokio runtime
pub fn new(config: NameServerConfig, options: ResolverOpts, runtime: TokioHandle) -> Self {
Self::new_with_provider(config, options, TokioConnectionProvider::new(runtime))
}
}
impl<C: DnsHandle<Error = ResolveError>, P: ConnectionProvider<Conn = C>> NameServer<C, P> {
/// Construct a new Nameserver with the configuration and options. The connection provider will create UDP and TCP sockets
pub fn new_with_provider(
config: NameServerConfig,
options: ResolverOpts,
@ -169,6 +171,7 @@ impl<C: DnsHandle<Error = ResolveError>, P: ConnectionProvider<Conn = C>> NameSe
}
}
/// Specifies that thie NameServer will treat negative responses as permanent failures and will not retry
pub fn trust_nx_responses(&self) -> bool {
self.config.trust_nx_responses
}

View File

@ -47,7 +47,7 @@ pub struct NameServerPool<
#[cfg(test)]
#[cfg(feature = "tokio-runtime")]
impl NameServerPool<TokioConnection, TokioConnectionProvider> {
pub(crate) fn from_config(
pub(crate) fn tokio_from_config(
config: &ResolverConfig,
options: &ResolverOpts,
runtime: TokioHandle,
@ -113,7 +113,7 @@ where
/// Construct a NameServerPool from a set of name server configs
pub fn from_config(
mut name_servers: NameServerConfigGroup,
name_servers: NameServerConfigGroup,
options: &ResolverOpts,
conn_provider: P,
) -> Self {
@ -496,7 +496,7 @@ mod tests {
resolver_config.add_name_server(config2);
let io_loop = Runtime::new().unwrap();
let mut pool = NameServerPool::<_, TokioConnectionProvider>::from_config(
let mut pool = NameServerPool::<_, TokioConnectionProvider>::tokio_from_config(
&resolver_config,
&ResolverOpts::default(),
TokioHandle,

View File

@ -8,7 +8,6 @@
use std::io;
use tracing::{debug, info};
use trust_dns_proto::xfer::DnsRequestOptions;
use crate::{
authority::{

View File

@ -1,4 +1,4 @@
// Copyright 2015-2020 Benjamin Fry <benjaminfry@me.com>
// Copyright 2015-2022 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
@ -22,18 +22,19 @@
use std::{
net::{IpAddr, SocketAddr},
path::{Path, PathBuf},
path::PathBuf,
time::Instant,
};
use clap::Parser;
use console::style;
use trust_dns_client::op::Query;
use trust_dns_recursor::Recursor;
use trust_dns_resolver::{
config::{NameServerConfig, NameServerConfigGroup, Protocol, ResolverConfig, ResolverOpts},
config::{NameServerConfig, NameServerConfigGroup, Protocol},
proto::rr::RecordType,
Name, TokioAsyncResolver,
Name,
};
/// A CLI interface for the trust-dns-recursor.
@ -156,24 +157,11 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
(udp && ns.protocol == Protocol::Udp) || (tcp && ns.protocol == Protocol::Tcp)
});
let name_servers =
hints
.iter()
.map(|n| format!("{}", n))
.fold(String::new(), |mut names, n| {
if !names.is_empty() {
names.push_str(", ")
}
names.push_str(&n);
names
});
// query parameters
let name = opts.domainname;
let ty = opts.ty;
let mut recursor = Recursor::new(hints)?;
let recursor = Recursor::new(hints)?;
// execute query
println!(
@ -183,7 +171,8 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
);
let now = Instant::now();
let lookup = recursor.resolve(name, ty, now).await?;
let query = Query::query(name, ty);
let lookup = recursor.resolve(query, now).await?;
// report response, TODO: better display of errors
println!(
@ -192,15 +181,7 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
style(&lookup).blue()
);
let answers = lookup.answers();
let nameservers = lookup.name_servers();
let additionals = lookup.additionals();
let records = answers
.iter()
.chain(nameservers.iter())
.chain(additionals.iter());
for r in records.filter(|r| r.record_type() == ty) {
for r in lookup.record_iter().filter(|r| r.record_type() == ty) {
print!(
"\t{name} {ttl} {class} {ty}",
name = style(r.name()).blue(),