///|
/// A finding in one parent/child deployment snapshot. `source` identifies
/// the input file, and `line` is the original zone-file line.
pub(all) struct DelegationFinding {
code : String
severity : String
source : String
owner : String
line : Int
message : String
} derive(Debug)
///|
pub(all) struct DelegationState {
name : String
parent_version : String
child_version : String
blocked : Bool
findings : Array[DelegationFinding]
} derive(Debug)
///|
/// The four snapshots assume that each zone file is published atomically.
/// A safe order means no local error was found in its three snapshots.
pub(all) struct DelegationRollout {
states : Array[DelegationState]
preflight : Array[DelegationFinding]
parent_first : Bool
child_first : Bool
recommendation : String
} derive(Debug)
///|
fn delegation_finding(
findings : Array[DelegationFinding],
code : String,
severity : String,
source : String,
owner : String,
line : Int,
message : String,
) -> Unit {
findings.push({ code, severity, source, owner, line, message, })
}
///|
fn append_zone_findings(
findings : Array[DelegationFinding],
zone : Zone,
source : String,
) -> Unit {
for item in validate_zone(zone) {
delegation_finding(
findings,
item.code,
item.severity,
source,
item.owner,
item.line,
item.message,
)
}
}
///|
fn records_at(
zone : Zone,
owner : String,
record_type : String,
) -> Array[ResourceRecord] {
let result : Array[ResourceRecord] = []
for record in zone.records {
if record.owner == owner && record.record_type == record_type {
result.push(record)
}
}
result
}
///|
fn ns_targets(records : Array[ResourceRecord]) -> Map[String, Bool] {
let result : Map[String, Bool] = Map([])
for record in records {
if record.rdata.length() > 0 {
result.set(absolute_rdata_name(record, 0, record.origin), true)
}
}
result
}
///|
fn same_ns_targets(
parent_records : Array[ResourceRecord],
child_records : Array[ResourceRecord],
) -> Bool {
let parent_targets = ns_targets(parent_records)
let child_targets = ns_targets(child_records)
if parent_targets.length() != child_targets.length() {
return false
}
for target in parent_targets.keys() {
if child_targets.get(target) is None {
return false
}
}
true
}
///|
fn address_values(zone : Zone, owner : String) -> Map[String, Bool] {
let values : Map[String, Bool] = Map([])
for record in zone.records {
if record.owner == owner &&
(record.record_type == "A" || record.record_type == "AAAA") &&
record.rdata.length() > 0 {
values.set(record.record_type + ":" + lower(record.rdata[0]), true)
}
}
values
}
///|
fn addresses_overlap(
parent : Map[String, Bool],
child : Map[String, Bool],
) -> Bool {
for address in parent.keys() {
if child.get(address) is Some(_) {
return true
}
}
false
}
///|
fn audit_delegation_pair(
parent : Zone,
child : Zone,
parent_source : String,
child_source : String,
include_validation : Bool,
) -> Array[DelegationFinding] {
let findings : Array[DelegationFinding] = []
if include_validation {
append_zone_findings(findings, parent, parent_source)
append_zone_findings(findings, child, child_source)
}
if parent.origin == child.origin ||
!is_within_zone(child.origin, parent.origin) {
delegation_finding(
findings,
"G001",
"error",
child_source,
child.origin,
0,
"child origin must be strictly below parent origin",
)
return findings
}
let parent_ns = records_at(parent, child.origin, "NS")
let child_ns = records_at(child, child.origin, "NS")
if parent_ns.length() == 0 {
delegation_finding(
findings,
"G002",
"error",
parent_source,
child.origin,
0,
"parent zone has no NS delegation for child origin",
)
}
if child_ns.length() == 0 {
delegation_finding(
findings,
"G003",
"error",
child_source,
child.origin,
0,
"child zone has no apex NS record",
)
}
if parent_ns.length() > 0 &&
child_ns.length() > 0 &&
!same_ns_targets(parent_ns, child_ns) {
delegation_finding(
findings,
"G004",
"warning",
child_source,
child.origin,
child_ns[0].line,
"parent delegation and child apex NS sets differ during this snapshot",
)
}
let seen : Map[String, Bool] = Map([])
for record in parent_ns {
if record.rdata.length() == 0 {
continue
}
let target = absolute_rdata_name(record, 0, record.origin)
if !is_within_zone(target, child.origin) || seen.get(target) is Some(_) {
continue
}
seen.set(target, true)
let glue = address_values(parent, target)
let authoritative = address_values(child, target)
if glue.length() == 0 {
delegation_finding(
findings,
"G005",
"error",
parent_source,
target,
record.line,
"in-domain nameserver has no A/AAAA glue in parent zone",
)
}
if authoritative.length() == 0 {
delegation_finding(
findings, "G006", "error", child_source, target, 0, "in-domain nameserver has no A/AAAA address in child snapshot",
)
} else if glue.length() > 0 && !addresses_overlap(glue, authoritative) {
delegation_finding(
findings,
"G007",
"error",
parent_source,
target,
record.line,
"parent glue and child authoritative addresses have no common value under conservative rollout policy",
)
}
}
audit_dnssec_link(parent, child, parent_source, child_source, findings)
findings
}
///|
fn delegation_state(
name : String,
parent_version : String,
child_version : String,
parent : Zone,
child : Zone,
) -> DelegationState {
let findings = audit_delegation_pair(
parent,
child,
"parent-" + parent_version,
"child-" + child_version,
true,
)
let mut blocked = false
for item in findings {
if item.severity == "error" {
blocked = true
}
}
{ name, parent_version, child_version, blocked, findings, }
}
///|
/// Check both publication orders of one parent and one child zone.
/// This is a bounded offline model: it does not simulate DNS caches,
/// live servers, DNSSEC validation, or non-atomic publication.
pub fn analyze_delegation_rollout(
parent_before : Zone,
parent_after : Zone,
child_before : Zone,
child_after : Zone,
) -> DelegationRollout {
let preflight : Array[DelegationFinding] = []
let parent_diff = compare_zones(parent_before, parent_after)
let child_diff = compare_zones(child_before, child_after)
audit_dnssec_transition(
parent_before,
parent_after,
child_before.origin,
"parent-after",
preflight,
)
for item in parent_diff.diagnostics {
delegation_finding(
preflight,
item.code,
item.severity,
"parent-after",
item.owner,
item.line,
item.message,
)
}
for item in child_diff.diagnostics {
delegation_finding(
preflight,
item.code,
item.severity,
"child-after",
item.owner,
item.line,
item.message,
)
}
let states = [
delegation_state(
"baseline", "before", "before", parent_before, child_before,
),
delegation_state(
"parent-updated", "after", "before", parent_after, child_before,
),
delegation_state(
"child-updated", "before", "after", parent_before, child_after,
),
delegation_state("final", "after", "after", parent_after, child_after),
]
let mut preflight_ok = true
for item in preflight {
if item.severity == "error" {
preflight_ok = false
}
}
let endpoints_ok = preflight_ok && !states[0].blocked && !states[3].blocked
let parent_first = endpoints_ok && !states[1].blocked
let child_first = endpoints_ok && !states[2].blocked
let recommendation = if parent_first && child_first {
"either-order"
} else if parent_first {
"parent-first"
} else if child_first {
"child-first"
} else {
"no-safe-order"
}
{ states, preflight, parent_first, child_first, recommendation, }
}
///|
pub fn render_delegation_rollout_text(result : DelegationRollout) -> String {
let out = StringBuilder()
out.write_string("recommendation: ")
out.write_string(result.recommendation)
out.write_char('\n')
for item in result.preflight {
out.write_string("preflight: ")
out.write_string(item.severity)
out.write_char(' ')
out.write_string(item.code)
out.write_string(" [")
out.write_string(item.source)
out.write_string("] ")
out.write_string(item.message)
out.write_char('\n')
}
for state in result.states {
out.write_string(state.name)
out.write_string(" (parent=")
out.write_string(state.parent_version)
out.write_string(", child=")
out.write_string(state.child_version)
out.write_string("): ")
out.write_string(if state.blocked { "blocked" } else { "pass" })
out.write_char('\n')
for item in state.findings {
out.write_string(" ")
out.write_string(item.severity)
out.write_char(' ')
out.write_string(item.code)
out.write_string(" [")
out.write_string(item.source)
out.write_char(':')
out.write_string(item.line.to_string())
out.write_string("] ")
out.write_string(item.owner)
out.write_string(": ")
out.write_string(item.message)
out.write_char('\n')
}
}
out.to_string()
}
///|
pub fn render_delegation_rollout_json(result : DelegationRollout) -> String {
let out = StringBuilder()
out.write_char('{')
write_json_field(out, "recommendation", result.recommendation, false)
out.write_string(",\"parent_first\":")
out.write_string(if result.parent_first { "true" } else { "false" })
out.write_string(",\"child_first\":")
out.write_string(if result.child_first { "true" } else { "false" })
out.write_string(",\"preflight\":[")
for i in 0.. 0 {
out.write_char(',')
}
let item = result.preflight[i]
out.write_char('{')
write_json_field(out, "code", item.code, false)
write_json_field(out, "severity", item.severity, true)
write_json_field(out, "source", item.source, true)
write_json_field(out, "owner", item.owner, true)
write_json_number(out, "line", item.line, true)
write_json_field(out, "message", item.message, true)
out.write_char('}')
}
out.write_char(']')
out.write_string(",\"states\":[")
for i in 0.. 0 {
out.write_char(',')
}
let state = result.states[i]
out.write_char('{')
write_json_field(out, "name", state.name, false)
write_json_field(out, "parent_version", state.parent_version, true)
write_json_field(out, "child_version", state.child_version, true)
out.write_string(",\"blocked\":")
out.write_string(if state.blocked { "true" } else { "false" })
out.write_string(",\"findings\":[")
for j in 0.. 0 {
out.write_char(',')
}
let item = state.findings[j]
out.write_char('{')
write_json_field(out, "code", item.code, false)
write_json_field(out, "severity", item.severity, true)
write_json_field(out, "source", item.source, true)
write_json_field(out, "owner", item.owner, true)
write_json_number(out, "line", item.line, true)
write_json_field(out, "message", item.message, true)
out.write_char('}')
}
out.write_string("]}")
}
out.write_string("]}")
out.to_string()
}