declare initial recursive lookup
This commit is contained in:
parent
a89cde5393
commit
5b929d71d7
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1954,6 +1954,7 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"trust-dns-client",
|
||||
"trust-dns-proto",
|
||||
"trust-dns-recursor",
|
||||
"trust-dns-resolver",
|
||||
"webpki",
|
||||
"webpki-roots",
|
||||
|
@ -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"] }
|
||||
|
@ -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;
|
||||
|
256
crates/recursor/src/recursor.rs
Normal file
256
crates/recursor/src/recursor.rs
Normal 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
|
||||
}
|
@ -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.
|
||||
|
@ -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
205
util/src/recurse.rs
Normal 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(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user