///|
fn is_null_mx(record : ResourceRecord) -> Bool {
record.record_type == "MX" &&
record.rdata.length() == 2 &&
record.rdata[1] == "."
}
///|
fn check_null_mx(zone : Zone, findings : Array[Diagnostic]) -> Unit {
let mx_counts : Map[String, Int] = Map([])
for record in zone.records {
if record.record_type == "MX" {
let previous = match mx_counts.get(record.owner) {
Some(n) => n
None => 0
}
mx_counts.set(record.owner, previous + 1)
}
}
for record in zone.records {
if !is_null_mx(record) {
continue
}
if record.rdata[0] != "0" {
add_record_note(
findings, "Z240", "error", "null MX must have preference 0", record,
)
}
match mx_counts.get(record.owner) {
Some(count) =>
if count > 1 {
add_record_note(
findings, "Z241", "error", "null MX must be the only MX record at its owner",
record,
)
}
None => ()
}
}
}
///|
fn known_caa_tag(tag : String) -> Bool {
let canonical = lower(tag)
canonical == "issue" || canonical == "issuewild" || canonical == "iodef"
}
///|
fn valid_caa_tag(tag : String) -> Bool {
let chars = tag.to_array()
if chars.length() == 0 || chars.length() > 15 {
return false
}
for c in chars {
if !ascii_alnum(c) {
return false
}
}
true
}
///|
fn check_caa_semantics(zone : Zone, findings : Array[Diagnostic]) -> Unit {
for record in zone.records {
if record.record_type != "CAA" || record.rdata.length() != 3 {
continue
}
let flag = decimal_number(record.rdata[0])
match flag {
Some(value) => {
if value != 0 && value != 128 {
add_record_note(
findings, "Z242", "warning", "CAA reserved flag bits are set; RFC 8659 defines 0 and 128",
record,
)
}
if value == 128 && !known_caa_tag(record.rdata[1]) {
add_record_note(
findings, "Z243", "warning", "unknown critical CAA property may prevent issuance",
record,
)
}
}
None => ()
}
if !valid_caa_tag(record.rdata[1]) {
add_record_note(
findings, "Z244", "error", "CAA property tag must contain ASCII letters and digits",
record,
)
}
let value = record.rdata[2]
if value.length() < 2 || !value.has_prefix("\"") || !value.has_suffix("\"") {
add_record_note(
findings, "Z245", "warning", "quote CAA property value for portable zone-file syntax",
record,
)
}
}
}
///|
fn starts_spf_text(record : ResourceRecord) -> Bool {
if record.record_type != "TXT" || record.rdata.length() == 0 {
return false
}
let first = lower(record.rdata[0])
first.has_prefix("\"v=spf1") || first.has_prefix("v=spf1")
}
///|
fn check_spf_records(zone : Zone, findings : Array[Diagnostic]) -> Unit {
let spf_counts : Map[String, Int] = Map([])
for record in zone.records {
if record.record_type == "SPF" {
add_record_note(
findings, "Z246", "warning", "SPF RR type is obsolete; publish SPF policy in TXT",
record,
)
}
if starts_spf_text(record) {
let count = match spf_counts.get(record.owner) {
Some(n) => n
None => 0
}
if count > 0 {
add_record_note(
findings, "Z247", "error", "multiple SPF TXT policies at one owner", record,
)
}
spf_counts.set(record.owner, count + 1)
}
}
}
///|
fn check_srv_aliases(zone : Zone, findings : Array[Diagnostic]) -> Unit {
let aliases = collect_aliases(zone)
for record in zone.records {
if record.record_type != "SRV" || record.rdata.length() != 4 {
continue
}
let exchange = absolute_rdata_name(record, 3, zone.origin)
if aliases.get(exchange) is Some(_) {
add_record_note(
findings, "Z248", "warning", "SRV target is an alias; use its canonical host name",
record,
)
}
}
}
///|
fn check_mail_security(zone : Zone, findings : Array[Diagnostic]) -> Unit {
check_null_mx(zone, findings)
check_caa_semantics(zone, findings)
check_spf_records(zone, findings)
check_srv_aliases(zone, findings)
}