// CNAME chain resolution helpers. A tracker is created once per top-level
// resolution and passed through every recursive lookup so loops spanning more
// than one DNS response cannot evade detection.
///|
fn dns_name_equal(left : String, right : String) -> Bool {
canonical_dns_name(left) == canonical_dns_name(right)
}
// Check if a name already appears in a CNAME chain (case-insensitive and with
// trailing-dot equivalence, as required by DNS name comparison rules).
///|
fn has_cname_loop(chain : Array[String], name : String) -> Bool {
for candidate in chain {
if dns_name_equal(candidate, name) {
return true
}
}
false
}
///|
pub struct CnameTracker {
max_depth : Int
chain : Array[String]
}
///|
pub fn CnameTracker::new(
origin : String,
max_depth? : Int = 16,
) -> CnameTracker {
{ max_depth: if max_depth < 0 { 0 } else { max_depth }, chain: [origin] }
}
// Record a new CNAME hop. `max_depth` counts aliases, not the origin.
///|
pub fn CnameTracker::follow(
self : CnameTracker,
target : String,
) -> Result[Unit, String] {
if target == "" {
return Err("CNAME target is empty")
}
if has_cname_loop(self.chain, target) {
return Err("CNAME loop detected: " + target)
}
if self.depth() >= self.max_depth {
return Err("CNAME chain exceeds max depth: " + self.max_depth.to_string())
}
self.chain.push(target)
Ok(())
}
///|
pub fn CnameTracker::get_chain(self : CnameTracker) -> Array[String] {
self.chain.copy()
}
///|
pub fn CnameTracker::depth(self : CnameTracker) -> Int {
if self.chain.length() == 0 {
0
} else {
self.chain.length() - 1
}
}
///|
pub fn CnameTracker::is_at_max(self : CnameTracker) -> Bool {
self.depth() >= self.max_depth
}
///|
pub fn CnameTracker::contains(self : CnameTracker, name : String) -> Bool {
has_cname_loop(self.chain, name)
}
// Extract CNAME targets from a response. This is intentionally an extraction
// helper only: the resolver follows a target only when the RR owner matches the
// current query name.
///|
pub fn extract_cname_targets(answers : Array[RR]) -> Array[String] {
let targets : Array[String] = Array::new(capacity=answers.length())
for rr in answers {
match rr.rdata {
CNAME(target) => targets.push(target)
_ => ()
}
}
targets
}
// Find the CNAME target directly owned by `original_name`, if present. This
// avoids accidentally following an unrelated CNAME included in the same
// answer section.
///|
pub fn find_cname_final_target(
answers : Array[RR],
original_name : String,
) -> String {
let current = Ref(original_name)
let tracker = CnameTracker::new(original_name)
for _ in 0.. return current.val
Some(target) =>
match tracker.follow(target) {
Ok(_) => current.val = target
// This helper cannot report a resolve error, so stop at the last
// safe name. The resolver itself propagates CnameLoop instead.
Err(_) => return current.val
}
}
}
current.val
}
///|
pub fn response_has_cname(msg : Message) -> Bool {
for rr in msg.answers {
if rr.rtype == qtype_cname {
return true
}
}
false
}
///|
pub fn filter_terminal_records(answers : Array[RR]) -> Array[RR] {
let result : Array[RR] = Array::new(capacity=answers.length())
for rr in answers {
if rr.rtype != qtype_cname {
result.push(rr)
}
}
result
}