initial layout of recursor

This commit is contained in:
Benjamin Fry 2022-03-16 20:03:25 -07:00
parent 47ac8b39f7
commit 28ffe0e8b6
7 changed files with 338 additions and 50 deletions

4
Cargo.lock generated
View File

@ -319,9 +319,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "3.2.15"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4"
checksum = "01d42c94ce7c2252681b5fed4d3627cc807b13dfc033246bd05d5b252399000e"
dependencies = [
"heck",
"proc-macro-error",

View File

@ -7,7 +7,9 @@ edition = "2021"
# A short blurb about the package. This is not rendered in any format when
# uploaded to crates.io (aka this is not markdown)
description = """
Trust-DNS is a safe and secure DNS recursive resolver with DNSSec support.
*WARNING* This library is experimental
Trust-DNS Recursor is a safe and secure DNS recursive resolver with DNSSec support.
Trust-DNS is based on the Tokio and Futures libraries, which means
it should be easily integrated into other software that also use those
libraries. This library can be used as in the server and binary for performing recursive lookups.

View File

@ -1,21 +1,45 @@
use std::{fmt, time::Instant};
use log::debug;
use trust_dns_proto::rr::{RecordSet, RecordType};
use trust_dns_resolver::{
config::{NameServerConfigGroup, ResolverConfig, ResolverOpts},
error::ResolveError,
lookup::Lookup,
name_server::NameServerPool,
IntoName, Name, TokioAsyncResolver, TokioConnection, TokioConnectionProvider,
use std::{
collections::BTreeSet,
fmt,
net::{IpAddr, SocketAddr},
time::Instant,
};
use crate::Error;
use futures_util::StreamExt;
use log::debug;
use lru_cache::LruCache;
use trust_dns_proto::{
op::Query,
rr::{RData, RecordSet, RecordType},
xfer::{DnsRequestOptions, DnsResponse},
DnsHandle,
};
use trust_dns_resolver::{
config::{NameServerConfigGroup, ResolverConfig, ResolverOpts},
error::{ResolveError, ResolveErrorKind},
lookup::Lookup,
name_server::{GenericConnectionProvider, NameServerPool, RuntimeProvider, TokioRuntime},
IntoName, Name, TokioAsyncResolver, TokioConnection, TokioConnectionProvider, TokioHandle,
};
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>>;
/// Records that have been found
type RecordCache = LruCache<CacheKey, RecordSet>;
/// A top down recursive resolver which operates off a list of "hints", this is often the root nodes.
pub struct Recursor {
hints: TokioAsyncResolver,
hints: NameServerPool<TokioConnection, TokioConnectionProvider>,
opts: ResolverOpts,
name_server_cache: NameServerCache,
record_cache: RecordCache,
}
impl Recursor {
@ -25,14 +49,18 @@ impl Recursor {
let mut config = ResolverConfig::new();
let hints: NameServerConfigGroup = hints.into();
for hint in hints.into_inner() {
config.add_name_server(hint);
}
let opts = recursor_opts();
let hints = TokioAsyncResolver::tokio(config, 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);
Ok(Self { hints, opts })
Ok(Self {
hints,
opts,
name_server_cache,
record_cache,
})
}
/// Permform a recursive resolution
@ -197,11 +225,11 @@ impl Recursor {
/// contiguous zone at ISI.EDU.
/// ```
pub async fn resolve<N: IntoName>(
&self,
&mut self,
domain: N,
ty: RecordType,
request_time: Instant,
) -> Result<Lookup, ResolveError> {
) -> Result<&RecordSet, ResolveError> {
let domain = domain.into_name()?;
// wild guess on number fo lookups needed
@ -209,43 +237,156 @@ impl Recursor {
lookups.push((domain.clone(), 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;
// 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;
}
lookups.push((ns.clone(), RecordType::NS).into());
next_ns = ns;
// 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!()
}
for to_resolve in lookups {
debug!("resolving: {}", to_resolve);
}
// 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!()
}
todo!();
fn check_cache(&mut self, query: RecursiveQuery) -> Option<&RecordSet> {
let key = CacheKey {
domain: query.domain,
ty: query.ty,
};
self.record_cache.get_mut(&key).map(|v| &*v)
}
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
}
async fn lookup(
&self,
domain: &Name,
ty: RecordType,
ns: &NameServerPool<TokioConnection, TokioConnectionProvider>,
) -> Result<RecordSet, Error> {
unimplemented!()
query: RecursiveQuery,
mut ns: NameServerPool<TokioConnection, TokioConnectionProvider>,
) -> Result<DnsResponse, Error> {
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);
// 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"
response.map_err(|e| ErrorKind::from(e).into())
} else {
Err("no responses from nameserver".into())
}
}
}
#[derive(Clone)]
struct RecursiveQuery {
name: Name,
domain: Name,
ty: RecordType,
}
impl From<(Name, RecordType)> for RecursiveQuery {
fn from(name_ty: (Name, RecordType)) -> Self {
Self {
name: name_ty.0,
domain: name_ty.0,
ty: name_ty.1,
}
}
@ -253,7 +394,7 @@ impl From<(Name, RecordType)> for RecursiveQuery {
impl fmt::Display for RecursiveQuery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "({},{})", self.name, self.ty)
write!(f, "({},{})", self.domain, self.ty)
}
}
@ -272,3 +413,24 @@ 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

@ -266,7 +266,7 @@ mod hosts;
mod https;
pub mod lookup;
pub mod lookup_ip;
#[doc(hidden)]
// TODO: consider #[doc(hidden)]
pub mod name_server;
#[cfg(feature = "dns-over-quic")]
mod quic;

View File

@ -19,7 +19,7 @@ use proto::xfer::{DnsHandle, DnsRequest, DnsResponse, FirstAnswer};
use proto::Time;
use tracing::debug;
use crate::config::{ResolverConfig, ResolverOpts, ServerOrderingStrategy};
use crate::config::{NameServerConfigGroup, ResolverConfig, ResolverOpts, ServerOrderingStrategy};
use crate::error::{ResolveError, ResolveErrorKind};
#[cfg(feature = "mdns")]
use crate::name_server;
@ -111,6 +111,33 @@ where
}
}
/// Construct a NameServerPool from a set of name server configs
pub fn from_config(
mut name_servers: NameServerConfigGroup,
options: &ResolverOpts,
conn_provider: P,
) -> Self {
let map_config_to_ns = |ns_config| {
NameServer::<C, P>::new_with_provider(ns_config, *options, conn_provider.clone())
};
let (datagram, stream): (Vec<_>, Vec<_>) = name_servers
.into_inner()
.into_iter()
.partition(|ns| ns.protocol.is_datagram());
let datagram_conns: Vec<_> = datagram.into_iter().map(map_config_to_ns).collect();
let stream_conns: Vec<_> = stream.into_iter().map(map_config_to_ns).collect();
Self {
datagram_conns: Arc::from(datagram_conns),
stream_conns: Arc::from(stream_conns),
#[cfg(feature = "mdns")]
mdns_conns: name_server::mdns_nameserver(*options, conn_provider.clone(), false),
options: *options,
}
}
#[doc(hidden)]
#[cfg(not(feature = "mdns"))]
pub fn from_nameservers(

View File

@ -0,0 +1,92 @@
; This file holds the information on root name servers needed to
; initialize cache of Internet domain name servers
; (e.g. reference this file in the "cache . <file>"
; configuration file of BIND domain name servers).
;
; This file is made available by InterNIC
; under anonymous FTP as
; file /domain/named.cache
; on server FTP.INTERNIC.NET
; -OR- RS.INTERNIC.NET
;
; last update: February 16, 2022
; related version of root zone: 2022021601
;
; FORMERLY NS.INTERNIC.NET
;
. 3600000 NS A.ROOT-SERVERS.NET.
A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4
A.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:ba3e::2:30
;
; FORMERLY NS1.ISI.EDU
;
. 3600000 NS B.ROOT-SERVERS.NET.
B.ROOT-SERVERS.NET. 3600000 A 199.9.14.201
B.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:200::b
;
; FORMERLY C.PSI.NET
;
. 3600000 NS C.ROOT-SERVERS.NET.
C.ROOT-SERVERS.NET. 3600000 A 192.33.4.12
C.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2::c
;
; FORMERLY TERP.UMD.EDU
;
. 3600000 NS D.ROOT-SERVERS.NET.
D.ROOT-SERVERS.NET. 3600000 A 199.7.91.13
D.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2d::d
;
; FORMERLY NS.NASA.GOV
;
. 3600000 NS E.ROOT-SERVERS.NET.
E.ROOT-SERVERS.NET. 3600000 A 192.203.230.10
E.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:a8::e
;
; FORMERLY NS.ISC.ORG
;
. 3600000 NS F.ROOT-SERVERS.NET.
F.ROOT-SERVERS.NET. 3600000 A 192.5.5.241
F.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2f::f
;
; FORMERLY NS.NIC.DDN.MIL
;
. 3600000 NS G.ROOT-SERVERS.NET.
G.ROOT-SERVERS.NET. 3600000 A 192.112.36.4
G.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:12::d0d
;
; FORMERLY AOS.ARL.ARMY.MIL
;
. 3600000 NS H.ROOT-SERVERS.NET.
H.ROOT-SERVERS.NET. 3600000 A 198.97.190.53
H.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:1::53
;
; FORMERLY NIC.NORDU.NET
;
. 3600000 NS I.ROOT-SERVERS.NET.
I.ROOT-SERVERS.NET. 3600000 A 192.36.148.17
I.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fe::53
;
; OPERATED BY VERISIGN, INC.
;
. 3600000 NS J.ROOT-SERVERS.NET.
J.ROOT-SERVERS.NET. 3600000 A 192.58.128.30
J.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:c27::2:30
;
; OPERATED BY RIPE NCC
;
. 3600000 NS K.ROOT-SERVERS.NET.
K.ROOT-SERVERS.NET. 3600000 A 193.0.14.129
K.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fd::1
;
; OPERATED BY ICANN
;
. 3600000 NS L.ROOT-SERVERS.NET.
L.ROOT-SERVERS.NET. 3600000 A 199.7.83.42
L.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:9f::42
;
; OPERATED BY WIDE
;
. 3600000 NS M.ROOT-SERVERS.NET.
M.ROOT-SERVERS.NET. 3600000 A 202.12.27.33
M.ROOT-SERVERS.NET. 3600000 AAAA 2001:dc3::35
; End of file

View File

@ -51,7 +51,12 @@ struct Opts {
ty: RecordType,
/// Specify a nameserver to use, ip and port e.g. 8.8.8.8:53 or \[2001:4860:4860::8888\]:53 (port required)
#[clap(short = 'n', long, require_value_delimiter = true)]
#[clap(
short = 'n',
long,
use_value_delimiter = true,
require_value_delimiter = true
)]
nameserver: Vec<SocketAddr>,
/// Specify the IP address to connect from.
@ -175,7 +180,7 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
let name = &opts.domainname;
let ty = opts.ty;
let recursor = Recursor::new(hints)?;
let mut recursor = Recursor::new(hints)?;
// execute query
println!(
@ -189,12 +194,12 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
// report response, TODO: better display of errors
println!(
"{} for query {}",
"{} for query {:?}",
style("Success").green(),
style(lookup.query()).blue()
style(lookup).blue()
);
for r in lookup.record_iter() {
for r in lookup.records_without_rrsigs() {
print!(
"\t{name} {ttl} {class} {ty}",
name = style(r.name()).blue(),