declare initial recursive lookup

This commit is contained in:
Benjamin Fry 2022-02-24 11:50:38 -08:00
parent a89cde5393
commit 5b929d71d7
7 changed files with 498 additions and 2 deletions

1
Cargo.lock generated
View File

@ -1954,6 +1954,7 @@ dependencies = [
"tracing-subscriber",
"trust-dns-client",
"trust-dns-proto",
"trust-dns-recursor",
"trust-dns-resolver",
"webpki",
"webpki-roots",

View File

@ -84,8 +84,8 @@ tokio = { version = "1.0", features = ["net"] }
tokio-openssl = { version = "0.6.0", optional = true }
tokio-rustls = { version = "0.23.0", optional = true }
toml = "0.5"
trust-dns-proto = { version = "0.21.1-alpha.5", path = "../proto" }
trust-dns-resolver = { version = "0.21.1-alpha.5", path = "../resolver", features = ["serde-config"]}
trust-dns-proto = { version = "0.21.1", path = "../proto" }
trust-dns-resolver = { version = "0.21.1", path = "../resolver", features = ["serde-config"]}
[dev-dependencies]
tokio = { version="1.0", features = ["macros", "rt"] }

View File

@ -1 +1,25 @@
//! A recursive DNS resolver based on the Trust-DNS (stub) resolver
#![warn(
clippy::default_trait_access,
clippy::dbg_macro,
clippy::print_stdout,
clippy::unimplemented,
missing_copy_implementations,
missing_docs,
non_snake_case,
non_upper_case_globals,
rust_2018_idioms,
unreachable_pub
)]
#![allow(
clippy::single_component_path_imports,
clippy::upper_case_acronyms, // can be removed on a major release boundary
)]
#![recursion_limit = "2048"]
#![cfg_attr(docsrs, feature(doc_cfg))]
mod recursor;
pub use recursor::Recursor;
pub use trust_dns_resolver::config::NameServerConfig;

View File

@ -0,0 +1,256 @@
use std::fmt;
use log::debug;
use trust_dns_proto::rr::RecordType;
use trust_dns_resolver::{
config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
error::ResolveError,
lookup::Lookup,
IntoName, Name, TokioAsyncResolver,
};
/// A top down recursive resolver which operates off a list of "hints", this is often the root nodes.
pub struct Recursor {
hints: TokioAsyncResolver,
opts: ResolverOpts,
}
impl Recursor {
/// Construct a new recursor using the list of NameServerConfigs for the hint list
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();
for hint in hints.into_inner() {
config.add_name_server(hint);
}
let opts = recursor_opts();
let hints = TokioAsyncResolver::tokio(config, opts)?;
Ok(Self { hints, opts })
}
/// Permform a recursive resolution
///
/// [https://datatracker.ietf.org/doc/html/rfc1034#section-5.3.3](RFC 1034), Domain Concepts and Facilities, November 1987
/// ```text
/// 5.3.3. Algorithm
///
/// The top level algorithm has four steps:
///
/// 1. See if the answer is in local information, and if so return
/// it to the client.
///
/// 2. Find the best servers to ask.
///
/// 3. Send them queries until one returns a response.
///
/// 4. Analyze the response, either:
///
/// a. if the response answers the question or contains a name
/// error, cache the data as well as returning it back to
/// the client.
///
/// b. if the response contains a better delegation to other
/// servers, cache the delegation information, and go to
/// step 2.
///
/// c. if the response shows a CNAME and that is not the
/// answer itself, cache the CNAME, change the SNAME to the
/// canonical name in the CNAME RR and go to step 1.
///
/// d. if the response shows a servers failure or other
/// bizarre contents, delete the server from the SLIST and
/// go back to step 3.
///
/// Step 1 searches the cache for the desired data. If the data is in the
/// cache, it is assumed to be good enough for normal use. Some resolvers
/// have an option at the user interface which will force the resolver to
/// ignore the cached data and consult with an authoritative server. This
/// is not recommended as the default. If the resolver has direct access to
/// a name server's zones, it should check to see if the desired data is
/// present in authoritative form, and if so, use the authoritative data in
/// preference to cached data.
///
/// Step 2 looks for a name server to ask for the required data. The
/// general strategy is to look for locally-available name server RRs,
/// starting at SNAME, then the parent domain name of SNAME, the
/// grandparent, and so on toward the root. Thus if SNAME were
/// Mockapetris.ISI.EDU, this step would look for NS RRs for
/// Mockapetris.ISI.EDU, then ISI.EDU, then EDU, and then . (the root).
/// These NS RRs list the names of hosts for a zone at or above SNAME. Copy
/// the names into SLIST. Set up their addresses using local data. It may
/// be the case that the addresses are not available. The resolver has many
/// choices here; the best is to start parallel resolver processes looking
/// for the addresses while continuing onward with the addresses which are
/// available. Obviously, the design choices and options are complicated
/// and a function of the local host's capabilities. The recommended
/// priorities for the resolver designer are:
///
/// 1. Bound the amount of work (packets sent, parallel processes
/// started) so that a request can't get into an infinite loop or
/// start off a chain reaction of requests or queries with other
/// implementations EVEN IF SOMEONE HAS INCORRECTLY CONFIGURED
/// SOME DATA.
///
/// 2. Get back an answer if at all possible.
///
/// 3. Avoid unnecessary transmissions.
///
/// 4. Get the answer as quickly as possible.
///
/// If the search for NS RRs fails, then the resolver initializes SLIST from
/// the safety belt SBELT. The basic idea is that when the resolver has no
/// idea what servers to ask, it should use information from a configuration
/// file that lists several servers which are expected to be helpful.
/// Although there are special situations, the usual choice is two of the
/// root servers and two of the servers for the host's domain. The reason
/// for two of each is for redundancy. The root servers will provide
/// eventual access to all of the domain space. The two local servers will
/// allow the resolver to continue to resolve local names if the local
/// network becomes isolated from the internet due to gateway or link
/// failure.
///
/// In addition to the names and addresses of the servers, the SLIST data
/// structure can be sorted to use the best servers first, and to insure
/// that all addresses of all servers are used in a round-robin manner. The
/// sorting can be a simple function of preferring addresses on the local
/// network over others, or may involve statistics from past events, such as
/// previous response times and batting averages.
///
/// Step 3 sends out queries until a response is received. The strategy is
/// to cycle around all of the addresses for all of the servers with a
/// timeout between each transmission. In practice it is important to use
/// all addresses of a multihomed host, and too aggressive a retransmission
/// policy actually slows response when used by multiple resolvers
/// contending for the same name server and even occasionally for a single
/// resolver. SLIST typically contains data values to control the timeouts
/// and keep track of previous transmissions.
///
/// Step 4 involves analyzing responses. The resolver should be highly
/// paranoid in its parsing of responses. It should also check that the
/// response matches the query it sent using the ID field in the response.
///
/// The ideal answer is one from a server authoritative for the query which
/// either gives the required data or a name error. The data is passed back
/// to the user and entered in the cache for future use if its TTL is
/// greater than zero.
///
/// If the response shows a delegation, the resolver should check to see
/// that the delegation is "closer" to the answer than the servers in SLIST
/// are. This can be done by comparing the match count in SLIST with that
/// computed from SNAME and the NS RRs in the delegation. If not, the reply
/// is bogus and should be ignored. If the delegation is valid the NS
/// delegation RRs and any address RRs for the servers should be cached.
/// The name servers are entered in the SLIST, and the search is restarted.
///
/// If the response contains a CNAME, the search is restarted at the CNAME
/// unless the response has the data for the canonical name or if the CNAME
/// is the answer itself.
///
/// Details and implementation hints can be found in [RFC-1035].
///
/// 6. A SCENARIO
///
/// In our sample domain space, suppose we wanted separate administrative
/// control for the root, MIL, EDU, MIT.EDU and ISI.EDU zones. We might
/// allocate name servers as follows:
///
///
/// |(C.ISI.EDU,SRI-NIC.ARPA
/// | A.ISI.EDU)
/// +---------------------+------------------+
/// | | |
/// MIL EDU ARPA
/// |(SRI-NIC.ARPA, |(SRI-NIC.ARPA, |
/// | A.ISI.EDU | C.ISI.EDU) |
/// +-----+-----+ | +------+-----+-----+
/// | | | | | | |
/// BRL NOSC DARPA | IN-ADDR SRI-NIC ACC
/// |
/// +--------+------------------+---------------+--------+
/// | | | | |
/// UCI MIT | UDEL YALE
/// |(XX.LCS.MIT.EDU, ISI
/// |ACHILLES.MIT.EDU) |(VAXA.ISI.EDU,VENERA.ISI.EDU,
/// +---+---+ | A.ISI.EDU)
/// | | |
/// LCS ACHILLES +--+-----+-----+--------+
/// | | | | | |
/// XX A C VAXA VENERA Mockapetris
///
/// In this example, the authoritative name server is shown in parentheses
/// at the point in the domain tree at which is assumes control.
///
/// Thus the root name servers are on C.ISI.EDU, SRI-NIC.ARPA, and
/// A.ISI.EDU. The MIL domain is served by SRI-NIC.ARPA and A.ISI.EDU. The
/// EDU domain is served by SRI-NIC.ARPA. and C.ISI.EDU. Note that servers
/// may have zones which are contiguous or disjoint. In this scenario,
/// C.ISI.EDU has contiguous zones at the root and EDU domains. A.ISI.EDU
/// 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>(
&self,
domain: N,
ty: RecordType,
) -> Result<Lookup, ResolveError> {
let domain = domain.into_name()?;
// wild guess on number fo lookups needed
let mut lookups = Vec::<RecursiveLookup>::with_capacity(10);
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;
}
lookups.push((ns.clone(), RecordType::NS).into());
next_ns = ns;
}
for to_resolve in lookups {
debug!("resolving: {}", to_resolve);
}
todo!();
}
}
struct RecursiveLookup {
name: Name,
ty: RecordType,
}
impl From<(Name, RecordType)> for RecursiveLookup {
fn from(name_ty: (Name, RecordType)) -> Self {
Self {
name: name_ty.0,
ty: name_ty.1,
}
}
}
impl fmt::Display for RecursiveLookup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "({},{})", self.name, self.ty)
}
}
fn recursor_opts() -> ResolverOpts {
let mut options = ResolverOpts::default();
options.ndots = 0;
options.edns0 = true;
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
}

View File

@ -479,6 +479,11 @@ impl NameServerConfigGroup {
)
}
/// Returns the inner vec of configs
pub fn into_inner(self) -> Vec<NameServerConfig> {
self.0
}
/// Configure a NameServer address and port
///
/// This will create UDP and TCP connections, using the same port.

View File

@ -69,6 +69,10 @@ required-features = ["dnssec-openssl"]
name = "resolve"
path = "src/resolve.rs"
[[bin]]
name = "recurse"
path = "src/recurse.rs"
[dependencies]
clap = { version = "3.1", default-features = false, features = ["std", "cargo", "derive", "color", "suggestions"] }
console = "0.15.0"
@ -79,6 +83,7 @@ tracing = "0.1.30"
tracing-subscriber = { version = "0.3", features = ["std", "fmt", "env-filter"] }
trust-dns-client = { version = "0.21.1", path = "../crates/client" }
trust-dns-proto = { version = "0.21.1", path = "../crates/proto" }
trust-dns-recursor = { version = "0.21.1", path = "../crates/recursor" }
trust-dns-resolver = { version = "0.21.1", path = "../crates/resolver" }
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }
webpki = { version = "0.22.0", optional = true }

205
util/src/recurse.rs Normal file
View File

@ -0,0 +1,205 @@
// Copyright 2015-2020 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.
//! The resolve program
// BINARY WARNINGS
#![warn(
clippy::default_trait_access,
clippy::dbg_macro,
clippy::unimplemented,
missing_copy_implementations,
missing_docs,
non_snake_case,
non_upper_case_globals,
rust_2018_idioms,
unreachable_pub
)]
use std::net::{IpAddr, SocketAddr};
use console::style;
use structopt::StructOpt;
use trust_dns_recursor::Recursor;
use trust_dns_resolver::config::{
NameServerConfig, NameServerConfigGroup, Protocol, ResolverConfig, ResolverOpts,
};
use trust_dns_resolver::proto::rr::RecordType;
use trust_dns_resolver::TokioAsyncResolver;
/// A CLI interface for the trust-dns-recursor.
///
/// This utility directly uses the trust-dns-recursor to perform a recursive lookup
/// starting with a sent of hints or root dns servers.
#[derive(Debug, StructOpt)]
#[structopt(name = "recurse")]
struct Opts {
/// Name to attempt to resolve, this is assumed to be fully-qualified
domainname: String,
/// Type of query to issue, e.g. A, AAAA, NS, etc.
#[structopt(short = "t", long = "type", default_value = "A")]
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)
#[structopt(short = "n", long, require_delimiter = true)]
nameserver: Vec<SocketAddr>,
/// Specify the IP address to connect from.
#[structopt(long)]
bind: Option<IpAddr>,
/// Use ipv4 addresses only, default is both ipv4 and ipv6
#[structopt(long)]
ipv4: bool,
/// Use ipv6 addresses only, default is both ipv4 and ipv6
#[structopt(long)]
ipv6: bool,
/// Use only UDP, default to UDP and TCP
#[structopt(long)]
udp: bool,
/// Use only TCP, default to UDP and TCP
#[structopt(long)]
tcp: bool,
/// Enable debug and all logging
#[structopt(long)]
debug: bool,
/// Enable info + warning + error logging
#[structopt(long)]
info: bool,
/// Enable warning + error logging
#[structopt(long)]
warn: bool,
/// Enable error logging
#[structopt(long)]
error: bool,
}
/// Run the resolve program
#[tokio::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
let opts: Opts = Opts::from_args();
// enable logging early
let log_level = if opts.debug {
log::LevelFilter::Debug
} else if opts.info {
log::LevelFilter::Info
} else if opts.warn {
log::LevelFilter::Warn
} else if opts.error {
log::LevelFilter::Error
} else {
log::LevelFilter::Off
};
// 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();
// Configure all the name servers
let mut hints = NameServerConfigGroup::new();
for socket_addr in &opts.nameserver {
hints.push(NameServerConfig {
socket_addr: *socket_addr,
protocol: Protocol::Tcp,
tls_dns_name: None,
trust_nx_responses: false,
#[cfg(feature = "dns-over-rustls")]
tls_config: None,
bind_addr: opts.bind.map(|ip| SocketAddr::new(ip, 0)),
});
hints.push(NameServerConfig {
socket_addr: *socket_addr,
protocol: Protocol::Udp,
tls_dns_name: None,
trust_nx_responses: false,
#[cfg(feature = "dns-over-rustls")]
tls_config: None,
bind_addr: opts.bind.map(|ip| SocketAddr::new(ip, 0)),
});
}
let ipv4 = opts.ipv4 || !opts.ipv6;
let ipv6 = opts.ipv6 || !opts.ipv4;
let udp = opts.udp || !opts.tcp;
let tcp = opts.tcp || !opts.udp;
hints.retain(|ns| (ipv4 && ns.socket_addr.is_ipv4()) || (ipv6 && ns.socket_addr.is_ipv6()));
hints.retain(|ns| {
(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 recursor = Recursor::new(hints)?;
// execute query
println!(
"Recursing for {name} {ty} from hints",
name = style(name).yellow(),
ty = style(ty).yellow(),
);
let lookup = recursor.resolve(name.to_string(), ty).await?;
// report response, TODO: better display of errors
println!(
"{} for query {}",
style("Success").green(),
style(lookup.query()).blue()
);
for r in lookup.record_iter() {
print!(
"\t{name} {ttl} {class} {ty}",
name = style(r.name()).blue(),
ttl = style(r.ttl()).blue(),
class = style(r.dns_class()).blue(),
ty = style(r.record_type()).blue(),
);
if let Some(rdata) = r.data() {
println!(" {rdata}", rdata = rdata);
} else {
println!("NULL")
}
}
Ok(())
}