recursor: fix to resolve most CNAMEs
This commit is contained in:
parent
c43bef87f9
commit
fd265a9ae4
|
@ -12,6 +12,7 @@ use futures_util::{future::select_all, FutureExt};
|
||||||
use hickory_resolver::name_server::TokioConnectionProvider;
|
use hickory_resolver::name_server::TokioConnectionProvider;
|
||||||
use lru_cache::LruCache;
|
use lru_cache::LruCache;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use std::sync::Arc;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -252,8 +253,45 @@ impl<P: ConnectionProvider + Default> Recursor<P> {
|
||||||
/// contiguous zone at ISI.EDU.
|
/// contiguous zone at ISI.EDU.
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn resolve(&self, query: Query, request_time: Instant) -> Result<Lookup, Error> {
|
pub async fn resolve(&self, query: Query, request_time: Instant) -> Result<Lookup, Error> {
|
||||||
if let Some(lookup) = self.record_cache.get(&query, request_time) {
|
let mut all_found = Lookup::new_with_max_ttl(query.clone(), Arc::new([]));
|
||||||
return lookup.map_err(Into::into);
|
let mut active_query = query.clone();
|
||||||
|
// max number of CNAMEs to traverse
|
||||||
|
const MAX_FOLLOWS: usize = 20;
|
||||||
|
for iter in 0..MAX_FOLLOWS {
|
||||||
|
let lookup = self.resolve_once(active_query.clone(), request_time).await?;
|
||||||
|
|
||||||
|
if lookup.query() == &query {
|
||||||
|
// Resolved as expected: done!
|
||||||
|
all_found = all_found.append(lookup);
|
||||||
|
debug!("resolve complete in iteration {}", iter);
|
||||||
|
return Ok(all_found);
|
||||||
|
}
|
||||||
|
// Extract whichever CNAME the server told us of
|
||||||
|
let mut cnames = lookup.records().iter().filter_map(|r| match r.data() {
|
||||||
|
Some(RData::CNAME(target)) if r.name() == active_query.name() => Some(target),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
if let Some(cname_data) = cnames.next() {
|
||||||
|
debug!("resolve to CNAME target: {}", *cname_data);
|
||||||
|
active_query.set_name((**cname_data).clone());
|
||||||
|
all_found = all_found.append(lookup);
|
||||||
|
} else {
|
||||||
|
// The server gave us neither the expected record, nor any CNAMEs to follow.
|
||||||
|
// This is probably an error.
|
||||||
|
all_found = all_found.append(lookup);
|
||||||
|
debug!("resolve complete in iteration {}: no more CNAMEs to follow", iter);
|
||||||
|
return Ok(all_found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("giving up after following {} CNAMEs", MAX_FOLLOWS);
|
||||||
|
Err(ErrorKind::Timeout.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively resolve a query until we find the record, or reach a CNAME that must be
|
||||||
|
/// manually dereferenced.
|
||||||
|
async fn resolve_once(&self, query: Query, request_time: Instant) -> Result<Lookup, Error> {
|
||||||
|
if let Some(lookup) = self.get_cached_query_or_cname(&query, request_time) {
|
||||||
|
return lookup;
|
||||||
}
|
}
|
||||||
|
|
||||||
// not in cache, let's look for an ns record for lookup
|
// not in cache, let's look for an ns record for lookup
|
||||||
|
@ -297,15 +335,17 @@ impl<P: ConnectionProvider + Default> Recursor<P> {
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform a single DNS query, checking/updating the cache.
|
||||||
|
/// In case that the nameserver answers to a non-CNAME query with a CNAME, this function will
|
||||||
|
/// return the CNAME. It's up to the caller to expand the answer further.
|
||||||
async fn lookup(
|
async fn lookup(
|
||||||
&self,
|
&self,
|
||||||
query: Query,
|
query: Query,
|
||||||
ns: RecursorPool<P>,
|
ns: RecursorPool<P>,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
) -> Result<Lookup, Error> {
|
) -> Result<Lookup, Error> {
|
||||||
if let Some(lookup) = self.record_cache.get(&query, now) {
|
if let Some(lookup) = self.get_cached_query_or_cname(&query, now) {
|
||||||
debug!("cached data {lookup:?}");
|
return lookup;
|
||||||
return lookup.map_err(Into::into);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = ns.lookup(query.clone());
|
let response = ns.lookup(query.clone());
|
||||||
|
@ -334,7 +374,17 @@ impl<P: ConnectionProvider + Default> Recursor<P> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let lookup = self.record_cache.insert_records(query, records, now);
|
let lookup = self.record_cache.insert_records(query.clone(), records, now);
|
||||||
|
let lookup = lookup.or_else(|| {
|
||||||
|
if query.query_type().is_cname() || query.query_type().is_any() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
debug!("no records for {}: checking for cached CNAME", query.name());
|
||||||
|
let mut cname_query = query;
|
||||||
|
cname_query.set_query_type(RecordType::CNAME);
|
||||||
|
self.record_cache.get(&cname_query, now).and_then(Result::ok)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
lookup.ok_or_else(|| Error::from("no records found"))
|
lookup.ok_or_else(|| Error::from("no records found"))
|
||||||
}
|
}
|
||||||
|
@ -490,6 +540,28 @@ impl<P: ConnectionProvider + Default> Recursor<P> {
|
||||||
self.name_server_cache.lock().insert(zone, ns.clone());
|
self.name_server_cache.lock().insert(zone, ns.clone());
|
||||||
Ok(ns)
|
Ok(ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return cached data for the precise query, if present,
|
||||||
|
/// falling back to aliased data if an answer compatible with the given query exists.
|
||||||
|
///
|
||||||
|
/// For example, a query for `www.example.com. A` could yield `www.example.com. A 1.2.3.4`
|
||||||
|
/// OR it could yield `www.example.com. CNAME other.example.com.`.
|
||||||
|
fn get_cached_query_or_cname(&self, query: &Query, request_time: Instant) -> Option<Result<Lookup, Error>> {
|
||||||
|
if let Some(lookup) = self.record_cache.get(query, request_time) {
|
||||||
|
debug!("cached data {lookup:?}");
|
||||||
|
return Some(lookup.map_err(Into::into));
|
||||||
|
}
|
||||||
|
if !query.query_type().is_cname() && !query.query_type().is_any() {
|
||||||
|
// If trying to lookup e.g. an A record, check if it's actually a record we've looked
|
||||||
|
// up before, behind a CNAME.
|
||||||
|
let mut cname_query = query.clone();
|
||||||
|
cname_query.set_query_type(RecordType::CNAME);
|
||||||
|
if let Some(lookup) = self.record_cache.get(&cname_query, request_time) {
|
||||||
|
return Some(lookup.map_err(Into::into));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recursor_opts() -> ResolverOpts {
|
fn recursor_opts() -> ResolverOpts {
|
||||||
|
|
|
@ -129,7 +129,8 @@ impl Lookup {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clones the inner vec, appends the other vec
|
/// Clones the inner vec, appends the other vec
|
||||||
pub(crate) fn append(&self, other: Self) -> Self {
|
#[doc(hidden)]
|
||||||
|
pub fn append(&self, other: Self) -> Self {
|
||||||
let mut records = Vec::with_capacity(self.len() + other.len());
|
let mut records = Vec::with_capacity(self.len() + other.len());
|
||||||
records.extend_from_slice(&self.records);
|
records.extend_from_slice(&self.records);
|
||||||
records.extend_from_slice(&other.records);
|
records.extend_from_slice(&other.records);
|
||||||
|
|
|
@ -95,6 +95,7 @@ const ZONE_EXAMPLE_COM: &str = r#"
|
||||||
example.com. A 10.0.100.1
|
example.com. A 10.0.100.1
|
||||||
www.example.com. A 10.0.100.1
|
www.example.com. A 10.0.100.1
|
||||||
cname.sub.example.com. CNAME www.example.com.
|
cname.sub.example.com. CNAME www.example.com.
|
||||||
|
cname.example.com. CNAME www.example.com.
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
type HardcodedNameServer = NameServer<HardcodedConnProvider>;
|
type HardcodedNameServer = NameServer<HardcodedConnProvider>;
|
||||||
|
@ -398,3 +399,60 @@ fn test_cname_below_nonexistent_parent() {
|
||||||
|
|
||||||
assert_eq!(&*lookup.records().to_vec(), &[expected_record]);
|
assert_eq!(&*lookup.records().to_vec(), &[expected_record]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `.` (NS) -> `com.` (NS) -> `example.com.` (NS) -> `cname.example.com.`.
|
||||||
|
/// This results in a lookup for `cname.example.com. NS`, even though the record is actually CNAME.
|
||||||
|
#[test]
|
||||||
|
fn test_cname_under_ns() {
|
||||||
|
logger("DEBUG");
|
||||||
|
|
||||||
|
let query = Query::query(Name::from_str("cname.example.com.").unwrap(), RecordType::CNAME);
|
||||||
|
let expected_record = cname_record(
|
||||||
|
Name::from_str("cname.example.com.").unwrap(),
|
||||||
|
Name::from_str("www.example.com.").unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let roots = NameServerPool::from_nameservers(
|
||||||
|
Default::default(),
|
||||||
|
vec![mock_nameserver(NS_ROOT)],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(&*lookup.records().to_vec(), &[expected_record]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query for an A record which is actually a CNAME record.
|
||||||
|
/// The server should answer with CNAME, and (since the target is in the same zone) an additional
|
||||||
|
/// A record.
|
||||||
|
#[test]
|
||||||
|
fn test_cname_queried_as_v4() {
|
||||||
|
logger("DEBUG");
|
||||||
|
|
||||||
|
let query = Query::query(Name::from_str("cname.example.com.").unwrap(), RecordType::A);
|
||||||
|
let expected_records = [
|
||||||
|
cname_record(
|
||||||
|
Name::from_str("cname.example.com.").unwrap(),
|
||||||
|
Name::from_str("www.example.com.").unwrap(),
|
||||||
|
),
|
||||||
|
v4_record(
|
||||||
|
Name::from_str("www.example.com.").unwrap(),
|
||||||
|
Ipv4Addr::new(10, 0, 100, 1),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let roots = NameServerPool::from_nameservers(
|
||||||
|
Default::default(),
|
||||||
|
vec![mock_nameserver(NS_ROOT)],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(&*lookup.records().to_vec(), &expected_records);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user