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:
anynonelocalhostlocalnets
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:
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 —
ValidationErrorandSeveritytypes