Basic bailiwick checking for the recursor

This commit is contained in:
Marcus Butler 2023-12-27 16:23:56 -06:00 committed by Benjamin Fry
parent ea8ba5857f
commit b4cd946eca

View File

@ -14,6 +14,9 @@ use lru_cache::LruCache;
use parking_lot::Mutex;
use tracing::{debug, info, warn};
#[cfg(test)]
use std::str::FromStr;
use crate::{
proto::{
op::Query,
@ -305,7 +308,19 @@ impl Recursor {
.take_answers()
.into_iter()
.chain(r.take_name_servers())
.chain(r.take_additionals());
.chain(r.take_additionals())
.filter(|x| {
if !in_bailiwick(ns.zone().clone(), x.name().clone()) {
warn!(
"Dropping out of bailiwick record {x} for zone {}",
ns.zone().clone()
);
false
} else {
true
}
});
let lookup = self.record_cache.insert_records(query, records, now);
lookup.ok_or_else(|| Error::from("no records found"))
@ -361,6 +376,15 @@ impl Recursor {
// .filter_map(Record::data)
// .filter_map(RData::to_ip_addr);
if !in_bailiwick(zone.base_name().clone(), zns.name().clone()) {
warn!(
"Dropping out of bailiwick record for {:?} with parent {:?}",
zns.name().clone(),
zone.base_name().clone()
);
continue;
}
let cached_a = self.record_cache.get(
&Query::query(ns_data.0.clone(), RecordType::A),
request_time,
@ -466,3 +490,102 @@ fn recursor_opts() -> ResolverOpts {
options
}
//
// Bailiwick Checking.
// The basic idea is to split the supplied parent and child string by '.' and verify each token in the
// parent string is present in the same position in the child string.
//
// Parent Child Expected Result
// ================ ==================== ========================
// Examples: . com. In-bailiwick (true)
// com. example.net. Out-of-bailiwick (false)
// example.com. www.example.com. In-bailiwick (true)
// example.com. www.otherdomain.com. Out-of-bailiwick (false)
// example.com www.example.com. Out-of-bailiwick (false, note the parent is not fully qualified)
//
// If the caller doesn't provide a parent at all, we'll return false.
//
// ".".split('.') will evaluate to ["", ""], which for our purposes doesn't work -- that's the reason for the ends_with tests and
// manually pushing the root entry onto the parent/child vectors, as well as skipping the first list element if the name was already
// fully qualified.
//
fn in_bailiwick(parent: Name, child: Name) -> bool {
let parent_str = parent.to_string();
let child_str = child.to_string();
if parent_str.is_empty() {
return false;
}
let mut rev_parent: Vec<&str> = vec![];
let mut rev_child: Vec<&str> = vec![];
let mut parent_offset = 0;
let mut child_offset = 0;
if child_str.ends_with('.') {
rev_child.push(".");
child_offset = 1;
}
child_str
.rsplit('.')
.skip(child_offset)
.collect::<Vec<_>>()
.iter()
.filter(|x| !x.is_empty())
.for_each(|x| rev_child.push(x));
if parent_str.ends_with('.') {
rev_parent.push(".");
parent_offset = 1;
}
parent_str
.rsplit('.')
.skip(parent_offset)
.collect::<Vec<_>>()
.iter()
.filter(|x| !x.is_empty())
.for_each(|x| rev_parent.push(x));
rev_parent
.into_iter()
.enumerate()
.all(|(i, elem)| rev_child[i] == elem)
}
#[test]
fn in_bailiwick_test() {
assert!(in_bailiwick(
Name::from_str(".").unwrap(),
Name::from_str("com.").unwrap()
));
assert!(in_bailiwick(
Name::from_str("com.").unwrap(),
Name::from_str("example.com.").unwrap()
));
assert!(in_bailiwick(
Name::from_str("example.com.").unwrap(),
Name::from_str("host.example.com.").unwrap()
));
assert!(in_bailiwick(
Name::from_str("example.com.").unwrap(),
Name::from_str("host.multilevel.example.com.").unwrap()
));
assert!(!in_bailiwick(
Name::from_str("").unwrap(),
Name::from_str("example.com.").unwrap()
));
assert!(!in_bailiwick(
Name::from_str("com.").unwrap(),
Name::from_str("example.net.").unwrap()
));
assert!(!in_bailiwick(
Name::from_str("example.com.").unwrap(),
Name::from_str("otherdomain.com.").unwrap()
));
assert!(!in_bailiwick(
Name::from_str("com").unwrap(),
Name::from_str("example.com.").unwrap()
));
}