Skip to content

Validating

Hornet's validator performs semantic checks on a parsed AST and returns a list of ValidationError diagnostics. Validation never modifies the AST.


Basic usage

Validate a named.conf

use hornet_bind9::{parse_named_conf, validate_named_conf, Severity};

let conf = parse_named_conf(input)?;
let diags = validate_named_conf(&conf);

if diags.is_empty() {
    println!("No issues found.");
}

for d in &diags {
    let prefix = match d.severity {
        Severity::Error   => "error",
        Severity::Warning => "warning",
        Severity::Info    => "info",
    };
    eprintln!("{prefix}: {}", d.message);
}

Validate a zone file

use hornet_bind9::{parse_zone_file, validate_zone_file};

let zone = parse_zone_file(zone_text)?;
let diags = validate_zone_file(&zone);

for d in &diags {
    eprintln!("[{:?}] {}", d.severity, d.message);
}

Severity levels

Level Meaning
Severity::Error Definite misconfiguration; BIND9 will likely refuse to load
Severity::Warning Suspicious configuration; BIND9 will load but behaviour may be unintended
Severity::Info Informational note; best practice reminder

Severity implements PartialOrd — Error > Warning > Info.


Filtering by severity

use hornet_bind9::Severity;

let errors: Vec<_> = diags.iter()
    .filter(|d| d.severity == Severity::Error)
    .collect();

let warnings_and_above: Vec<_> = diags.iter()
    .filter(|d| d.severity >= Severity::Warning)
    .collect();

Exit code integration

A common pattern for CI pipelines is to fail on errors but pass on warnings:

use hornet_bind9::Severity;

let has_error   = diags.iter().any(|d| d.severity == Severity::Error);
let has_warning = diags.iter().any(|d| d.severity == Severity::Warning);

if has_error {
    std::process::exit(1);
}
if has_warning && !allow_warnings {
    std::process::exit(1);
}

Tip

The hornet check CLI command handles exit codes automatically. See check.


What named.conf validation checks

Check Severity
ACL reference to undefined name Error
CIDR prefix length out of range Error
Key with empty secret Error
Duplicate zone declaration Error
Logging category references undefined channel Error
Primary zone without file directive Warning
Secondary zone without primaries directive Warning
Forward zone without forwarders Warning
forwarders set without forward policy Warning
DNSSEC validation enabled with recursion disabled Warning
Unrecognised key algorithm Warning
View without match-clients or match-destinations Warning
Logging file channel without severity Info
Zone name > 253 characters Error
Zone label > 63 characters Error
Zone label starts or ends with - Warning
Duplicate dnssec-policy name Error
dnssec-policy named default, insecure or none Error
dnssec-policy key with an unknown role (kept verbatim) Error
Unrecognised DNSSEC algorithm (not a mnemonic BIND knows, or not 0-255) Error
Algorithm with a KSK but no ZSK, or a ZSK but no KSK (a CSK counts as both) Error
More than one KSK, or more than one ZSK, for one algorithm Error
Duration or key lifetime that is not a BIND duration (an AST built in code) Error
Zone whose effective dnssec-policy (its own, else its view's, else the global one) is not defined Error
dnssec-policy with no keys (no keys clause, or keys { };) Warning
Algorithm BIND 9.20 rejects or deprecates for signing (RSAMD5, DSA, RSASHA1, ...) Warning
nsec3param iterations other than 0 (BIND 9.20 rejects it) Warning
Other dnssec-policy clause kept verbatim because its value is outside hornet's grammar Warning

Every dnssec-policy Error above is one named-checkconf also rejects, on BIND 9.18 and 9.20 (ADR-0004), so a configuration BIND accepts never has one. A global dnssec-policy naming an undefined policy is only reported for zones that inherit it, as BIND does. Some checks BIND makes are left to it: signature refresh versus validity, key lifetime versus rollover time, NSEC3 salt length, and 9.18's rule that a signed zone needs inline-signing or dynamic updates.

Built-in ACLs

The validator recognises the following built-in ACL names and never reports them as undefined:

  • any
  • none
  • localhost
  • localnets

What zone file validation checks

Check Severity
Missing SOA record Error
Multiple SOA records Error
Missing NS records Error
TXT string chunk exceeds 255 bytes Warning
TXT record total exceeds 65535 bytes Error
MX exchange is . (null MX) Warning
Non-standard CAA tag Warning
Record of a modelled type whose data did not parse (kept verbatim) Warning

Malformed records kept verbatim

The zone-file parser never drops a record. When a record's type is one hornet models (see hornet_bind9::ast::zone_file::MODELLED_RTYPES) but its data does not match that type's syntax, or has trailing text after a valid value, the record is kept as RData::Unknown { rtype, data } with its real type name. The validator reports each one:

warning: MX record data `not-a-number mail.example.com.` is not valid MX syntax; kept verbatim

The record still round-trips through the writer unchanged, but BIND9 will most likely reject it. Records of types hornet does not model at all (for example TYPE65534 or OPENPGPKEY) are also stored as RData::Unknown, but produce no warning.


Next Steps

  • Parsing — Parse configs before validating
  • check CLI — Validate from the command line
  • Error Types — ValidationError and Severity types