add NS search method

This commit is contained in:
Benjamin Fry 2022-05-17 19:59:41 -07:00
parent 28ffe0e8b6
commit 25861da44e
7 changed files with 255 additions and 197 deletions

14
Cargo.lock generated
View File

@ -142,6 +142,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "async-recursion"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-std"
version = "1.12.0"
@ -1860,6 +1871,7 @@ dependencies = [
name = "trust-dns-recursor"
version = "0.21.1"
dependencies = [
"async-recursion",
"async-trait",
"backtrace",
"bytes",
@ -1869,7 +1881,9 @@ dependencies = [
"futures-util",
"h2",
"http",
"lru-cache",
"openssl",
"parking_lot",
"rusqlite",
"rustls",
"serde",

View File

@ -67,6 +67,7 @@ path = "src/lib.rs"
[dependencies]
async-trait = "0.1.43"
async-recursion = "1.0.0"
backtrace = { version = "0.3.50", optional = true }
bytes = "1"
cfg-if = "1"
@ -75,7 +76,9 @@ futures-executor = { version = "0.3.5", default-features = false, features = ["s
futures-util = { version = "0.3.5", default-features = false, features = ["std"] }
h2 = { version = "0.3.0", features = ["stream"], optional = true }
http = { version = "0.2", optional = true }
lru-cache = "0.1.2"
openssl = { version = "0.10", features = ["v102", "v110"], optional = true }
parking_lot = "0.12"
rusqlite = { version = "0.27.0", features = ["bundled", "time"], optional = true }
rustls = { version = "0.20", optional = true }
serde = { version = "1.0.114", features = ["derive"] }

View File

@ -85,8 +85,13 @@ impl fmt::Display for Error {
}
}
impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Self {
impl<E> From<E> for Error
where
E: Into<ErrorKind>,
{
fn from(error: E) -> Self {
let kind: ErrorKind = error.into();
Self {
kind: Box::new(kind),
#[cfg(feature = "backtrace")]
@ -107,15 +112,6 @@ impl From<String> for Error {
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
match e.kind() {
io::ErrorKind::TimedOut => ErrorKind::Timeout.into(),
_ => ErrorKind::from(e).into(),
}
}
}
impl From<Error> for io::Error {
fn from(e: Error) -> Self {
match *e.kind() {

View File

@ -1,22 +1,25 @@
use std::{
collections::BTreeSet,
collections::{BTreeSet, HashMap},
fmt,
net::{IpAddr, SocketAddr},
time::Instant,
};
use futures_util::StreamExt;
use log::debug;
use async_recursion::async_recursion;
use futures_util::{future::select_all, Future, FutureExt, StreamExt};
use lru_cache::LruCache;
use parking_lot::Mutex;
use tracing::{debug, dispatcher::SetGlobalDefaultError, info, warn};
use trust_dns_proto::{
op::Query,
rr::{RData, RecordSet, RecordType},
op::{Message, Query},
rr::{RData, Record, RecordSet, RecordType},
xfer::{DnsRequestOptions, DnsResponse},
DnsHandle,
};
use trust_dns_resolver::{
config::{NameServerConfigGroup, ResolverConfig, ResolverOpts},
config::{NameServerConfig, NameServerConfigGroup, Protocol, ResolverConfig, ResolverOpts},
error::{ResolveError, ResolveErrorKind},
lookup::Lookup,
name_server::{GenericConnectionProvider, NameServerPool, RuntimeProvider, TokioRuntime},
@ -25,41 +28,52 @@ use trust_dns_resolver::{
use crate::{Error, ErrorKind};
/// A socket_addr based via of known nameservers.
///
/// The SocketAddr is matched to the Glue records in the GlueCache
type NameServerCache =
LruCache<ConnectionKey, NameServerPool<TokioConnection, TokioConnectionProvider>>;
/// Set of nameservers by the zone name
type NameServerCache = LruCache<Name, NameServerPool<TokioConnection, TokioConnectionProvider>>;
/// Records that have been found
type RecordCache = LruCache<CacheKey, RecordSet>;
///
/// 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>>>>;
/// 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,
name_server_cache: NameServerCache,
record_cache: RecordCache,
name_server_cache: Mutex<NameServerCache>,
message_cache: Mutex<MessageCache>,
}
impl Recursor {
/// Construct a new recursor using the list of NameServerConfigs for the hint list
///
/// # Panics
///
/// 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");
let opts = recursor_opts();
let hints =
NameServerPool::from_config(hints, &opts, TokioConnectionProvider::new(TokioHandle));
let name_server_cache = NameServerCache::new(100); // TODO: make this configurable
let record_cache = RecordCache::new(100);
let name_server_cache = Mutex::new(NameServerCache::new(100)); // TODO: make this configurable
let message_cache = Mutex::new(MessageCache::new(100));
Ok(Self {
hints,
opts,
name_server_cache,
record_cache,
message_cache,
})
}
@ -224,134 +238,27 @@ 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<N: IntoName>(
&mut self,
domain: N,
pub async fn resolve(
&self,
domain: Name,
ty: RecordType,
request_time: Instant,
) -> Result<&RecordSet, ResolveError> {
let domain = domain.into_name()?;
) -> Result<DnsResponse, Error> {
// wild guess on number fo lookups needed
let mut lookups = Vec::<RecursiveQuery>::with_capacity(10);
lookups.push((domain.clone(), ty).into());
let lookup: RecursiveQuery = (domain, ty).into();
// collect all the nameservers we need.
// let mut next_ns = domain;
// loop {
// let ns = next_ns.base_name();
// if ns.is_root() {
// break;
// }
// lookups.push((ns.clone(), RecordType::NS).into());
// next_ns = ns;
// }
// peak the next thing... only pop if we've found it.
while let Some(lookup) = lookups.last() {
let cached = self.check_cache(lookup.clone());
if cached.is_some() {
lookups.pop();
continue;
}
// not in cache, let's look for an ns record for lookup
let ns_query = if lookup.ty == RecordType::NS {
// if the lookup was NS, then we need to bump up a level
RecursiveQuery::from((lookup.domain.base_name(), RecordType::NS))
} else {
// look for the NS records "inside" the zone
RecursiveQuery::from((lookup.domain.clone(), RecordType::NS))
};
let nameserver_pool = if ns_query.domain.is_root() {
&self.hints
} else {
// see if we have the nameserver records cached
let ns_cache = if let Some(ns) = self.check_cache(ns_query.clone()) {
// collect the NS records
let names = ns
.records_without_rrsigs()
.filter_map(|r| r.data().map(RData::as_ns).flatten());
let names = names.collect::<Vec<_>>();
if names.is_empty() {
None
} else {
Some(names)
}
} else {
None
};
let ns_cache = if let Some(ns_cache) = ns_cache {
ns_cache
} else {
// add to search list and continue
// we have more to find.
lookups.push(ns_query);
continue;
};
// now see if we have the glue for these
let mut glue = BTreeSet::<IpAddr>::new();
todo!()
// for ns in ns_cache {
// let glue = self.check_glue();
};
log::info!("sending request: {}", lookup);
// let response = self.lookup(lookup.clone(), nameserver_pool).await?;
// log::info!("got response: {}", response.header());
todo!()
}
// we've gone through the entire search list, so we either found the record or not
// self.check_cache((domain.clone(), ty).into())
// .ok_or_else(|| ResolveErrorKind::Message("No records found").into())
todo!()
}
fn check_cache(&mut self, query: RecursiveQuery) -> Option<&RecordSet> {
let key = CacheKey {
domain: query.domain,
ty: query.ty,
// not in cache, let's look for an ns record for lookup
let zone = if lookup.ty == RecordType::NS {
lookup.domain.base_name()
} else {
// look for the NS records "inside" the zone
lookup.domain.clone()
};
self.record_cache.get_mut(&key).map(|v| &*v)
}
let ns = self.get_ns_pool_for_zone(zone, request_time).await?;
fn check_glue(&mut self, domain: &Name) -> Vec<IpAddr> {
let mut ipv4s: Vec<IpAddr> = self
.check_cache(RecursiveQuery {
domain: domain.clone(),
ty: RecordType::A,
})
.map(|r| r.records_without_rrsigs())
.into_iter()
.flatten()
.filter_map(|r| r.data().map(RData::as_a).flatten())
.map(|i| IpAddr::from(*i))
.collect();
let ipv6s = self
.check_cache(RecursiveQuery {
domain: domain.clone(),
ty: RecordType::AAAA,
})
.map(|r| r.records_without_rrsigs())
.into_iter()
.flatten()
.filter_map(|r| r.data().map(RData::as_aaaa).flatten())
.map(|i| IpAddr::from(*i));
// combine the ips for glue...
ipv4s.extend(ipv6s);
ipv4s
let response = self.lookup(lookup, ns).await?;
Ok(response)
}
async fn lookup(
@ -359,6 +266,12 @@ impl Recursor {
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());
}
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
@ -370,14 +283,147 @@ impl Recursor {
// 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"
response.map_err(|e| ErrorKind::from(e).into())
match response {
Ok(r) => {
info!("response: {}", r.header());
debug!("response: {}", *r);
Ok(r)
}
Err(e) => {
warn!("lookup error: {}", e);
Err(ErrorKind::from(e).into())
}
}
} else {
Err("no responses from nameserver".into())
}
}
#[async_recursion]
async fn get_ns_pool_for_zone(
&self,
zone: Name,
request_time: Instant,
) -> Result<NameServerPool<TokioConnection, TokioConnectionProvider>, Error> {
// TODO: need to check TTLs here.
if let Some(ns) = self.name_server_cache.lock().get_mut(&zone) {
return Ok(ns.clone());
};
let parent_zone = zone.base_name();
let nameserver_pool = if parent_zone.is_root() {
debug!("using ROOTS for {zone} nameservers");
self.hints.clone()
} else {
self.get_ns_pool_for_zone(parent_zone, request_time).await?
};
// 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 zone_nameservers = response.name_servers();
let glue = response.additionals();
// TODO: grab TTL and use for cache
// get all the NS records and glue
let mut config_group = NameServerConfigGroup::new();
let mut need_ips_for_names = Vec::new();
// unpack all glued records
for zns in zone_nameservers {
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 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);
config_group.push(udp);
config_group.push(tcp);
had_glue = true;
}
if !had_glue {
debug!("glue not found for {ns_data}");
need_ips_for_names.push(ns_data);
}
}
}
// collect missing IP addresses, select over them all, get the addresses
if !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 aaaa_resolves = need_ips_for_names.iter().take(1).map(|name| {
self.resolve((*name).clone(), RecordType::AAAA, request_time)
.boxed()
});
let mut a_resolves: Vec<_> = a_resolves.chain(aaaa_resolves).collect();
while !a_resolves.is_empty() {
let (next, _, rest) = select_all(a_resolves).await;
a_resolves = rest;
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,
});
for ip in ips {
let udp =
NameServerConfig::new(SocketAddr::from((ip, 53)), Protocol::Udp);
let tcp =
NameServerConfig::new(SocketAddr::from((ip, 53)), Protocol::Tcp);
config_group.push(udp);
config_group.push(tcp);
}
}
Err(e) => {
warn!("resolve failed {e}");
}
}
}
}
// now construct a namesever pool based off the NS and glue records
let ns = NameServerPool::from_config(
config_group,
&recursor_opts(),
TokioConnectionProvider::new(TokioHandle),
);
// store in cache for future usage
self.name_server_cache.lock().insert(zone, ns.clone());
Ok(ns)
}
}
#[derive(Clone)]
#[derive(Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
struct RecursiveQuery {
domain: Name,
ty: RecordType,
@ -405,6 +451,7 @@ fn recursor_opts() -> ResolverOpts {
options.validate = false; // we'll need to do any dnssec validation differently in a recursor (top-down rather than bottom-up)
options.preserve_intermediates = true;
options.recursion_desired = false;
options.num_concurrent_reqs = 1;
options
}
@ -413,24 +460,3 @@ enum RecursiveLookup {
Found(RecordSet),
Forward(RecordSet),
}
// FIXME: this needs a borrowed variant
#[derive(Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
struct CacheKey {
domain: Name,
ty: RecordType,
}
// FIXME: this is inefficient, doing this to hash from list of addrs for a NameServer to connections.
#[derive(Hash, Eq, PartialEq, Ord, PartialOrd)]
struct ConnectionKey {
addrs: BTreeSet<IpAddr>,
}
impl ConnectionKey {
fn from_addresses(addrs: &[IpAddr]) -> Self {
let addrs = BTreeSet::from_iter(addrs.iter().map(|a| *a));
Self { addrs }
}
}

View File

@ -419,6 +419,21 @@ pub struct NameServerConfig {
pub bind_addr: Option<SocketAddr>,
}
impl NameServerConfig {
/// Constructs a Nameserver configuration with some basic defaults
pub fn new(socket_addr: SocketAddr, protocol: Protocol) -> Self {
Self {
socket_addr,
protocol,
trust_nx_responses: true,
tls_dns_name: None,
#[cfg(feature = "dns-over-rustls")]
tls_config: None,
bind_addr: None,
}
}
}
impl fmt::Display for NameServerConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:", self.protocol)?;
@ -827,6 +842,8 @@ pub struct ResolverOpts {
///
/// This is true by default, disabling this is useful for requesting single records, but may prevent successful resolution.
pub recursion_desired: bool,
/// This is true by default, disabling this is useful for requesting single records, but may prevent successful resolution.
pub authentic_data: bool,
}
impl Default for ResolverOpts {
@ -857,6 +874,7 @@ impl Default for ResolverOpts {
try_tcp_on_error: false,
server_ordering_strategy: ServerOrderingStrategy::default(),
recursion_desired: true,
authentic_data: false,
}
}
}

View File

@ -263,7 +263,7 @@ where
Ok(response)
}
Err(e) if opts.try_tcp_on_error || e.is_no_connections() => {
debug!("error received, retrying over TCP");
debug!("error from UDP, retrying over TCP: {}", e);
Err(e)
}
result => return result,

View File

@ -30,11 +30,11 @@ use clap::Parser;
use console::style;
use trust_dns_recursor::Recursor;
use trust_dns_resolver::config::{
NameServerConfig, NameServerConfigGroup, Protocol, ResolverConfig, ResolverOpts,
use trust_dns_resolver::{
config::{NameServerConfig, NameServerConfigGroup, Protocol, ResolverConfig, ResolverOpts},
proto::rr::RecordType,
Name, TokioAsyncResolver,
};
use trust_dns_resolver::proto::rr::RecordType;
use trust_dns_resolver::TokioAsyncResolver;
/// A CLI interface for the trust-dns-recursor.
///
@ -44,7 +44,7 @@ use trust_dns_resolver::TokioAsyncResolver;
#[clap(name = "recurse")]
struct Opts {
/// Name to attempt to resolve, this is assumed to be fully-qualified
domainname: String,
domainname: Name,
/// Type of query to issue, e.g. A, AAAA, NS, etc.
#[clap(short = 't', long = "type", default_value = "A")]
@ -97,7 +97,7 @@ struct Opts {
/// Path to a hints file
#[clap(short = 'f', long)]
hints: PathBuf,
hints: Option<PathBuf>,
}
/// Run the resolve programf
@ -107,25 +107,18 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
// enable logging early
let log_level = if opts.debug {
log::LevelFilter::Debug
Some(tracing::Level::DEBUG)
} else if opts.info {
log::LevelFilter::Info
Some(tracing::Level::INFO)
} else if opts.warn {
log::LevelFilter::Warn
Some(tracing::Level::WARN)
} else if opts.error {
log::LevelFilter::Error
Some(tracing::Level::ERROR)
} else {
log::LevelFilter::Off
None
};
// Get query term
env_logger::builder()
.filter_module("trust_dns_recursor", log_level)
.filter_module("trust_dns_resolver", log_level)
.filter_module("trust_dns_proto", log_level)
.write_style(env_logger::WriteStyle::Auto)
.format_indent(Some(4))
.init();
trust_dns_util::logger(env!("CARGO_BIN_NAME"), log_level);
// Configure all the name servers
let mut hints = NameServerConfigGroup::new();
@ -177,7 +170,7 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
});
// query parameters
let name = &opts.domainname;
let name = opts.domainname;
let ty = opts.ty;
let mut recursor = Recursor::new(hints)?;
@ -185,21 +178,29 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
// execute query
println!(
"Recursing for {name} {ty} from hints",
name = style(name).yellow(),
name = style(&name).yellow(),
ty = style(ty).yellow(),
);
let now = Instant::now();
let lookup = recursor.resolve(name.to_string(), ty, now).await?;
let lookup = recursor.resolve(name, ty, now).await?;
// report response, TODO: better display of errors
println!(
"{} for query {:?}",
style("Success").green(),
style(lookup).blue()
style(&lookup).blue()
);
for r in lookup.records_without_rrsigs() {
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) {
print!(
"\t{name} {ttl} {class} {ty}",
name = style(r.name()).blue(),