/commands/dns/validate-dns-zone-file
Validate a DNS zone file before loading it (named-checkzone, dnspython)
- tool
- DNS
- versions
- BIND 9, dnspython 2.x
- os
- any
To check a zone file, run named-checkzone ZONE FILE (from BIND 9, packaged as bind9-utils or bind-utils): it performs the same checks named does when loading the zone and returns exit status 1 if it found errors, 0 otherwise. Without BIND, dns.zone.from_file() in dnspython parses the file and raises an exception that names the problem and the line, which works on any OS with Python.
named-checkzone
named-checkzone example.com /etc/bind/db.example.com
named-checkzone -q example.com db.example.com && echo valid # exit code only
named-checkzone -o - example.com db.example.com # print the zone as parsedZONE is the origin the file is loaded under, so relative names are expanded against it. -q prints nothing and only sets the exit code, which suits CI and pre-reload hooks. -o - dumps the parsed zone, which shows exactly what each relative name expanded to.
The same check with dnspython
cat > check_zone.py <<'EOF'
import sys
import dns.exception, dns.zone
for path in sys.argv[1:]:
try:
z = dns.zone.from_file(path, origin="example.test.", relativize=False)
z.check_origin()
print(f"{path}: OK, {len(z.nodes)} names, serial {z.get_soa().serial}")
except dns.exception.DNSException as e:
print(f"{path}: {type(e).__name__}: {e}")
EOF
cat > good.zone <<'EOF'
$ORIGIN example.test.
$TTL 3600
@ IN SOA ns1 hostmaster 2026102301 7200 900 1209600 300
@ IN NS ns1
ns1 IN A 192.0.2.53
@ IN A 192.0.2.10
www IN CNAME @
EOF
cat > nosoa.zone <<'EOF'
$TTL 3600
@ IN NS ns1
ns1 IN A 192.0.2.53
EOF
cat > badip.zone <<'EOF'
$TTL 3600
@ IN SOA ns1 hostmaster 1 7200 900 1209600 300
@ IN NS ns1
ns1 IN A 192.0.2.300
EOF
cat > extra.zone <<'EOF'
$TTL 3600
@ IN SOA ns1 hostmaster 1 7200 900 1209600 300
www IN AAAA 2001:db8::1 extra
EOF
cat > cname.zone <<'EOF'
$TTL 3600
@ IN SOA ns1 hostmaster 1 7200 900 1209600 300
@ IN NS ns1
ns1 IN A 192.0.2.53
www IN CNAME @
www IN A 192.0.2.11
EOF
uv run -q --no-project --python 3.13 --with dnspython==2.8.0 python check_zone.py good.zone nosoa.zone badip.zone extra.zone cname.zoneOutput
good.zone: OK, 3 names, serial 2026102301 nosoa.zone: NoSOA: The DNS zone has no SOA RR at its origin. badip.zone: SyntaxError: badip.zone:5: Text input is malformed. extra.zone: SyntaxError: extra.zone:4: expected EOL or EOF, got 3 "extra" cname.zone: CNAMEAndOtherData: rdataset type is not compatible with a CNAME node
In this run the line number dnspython reported was one past the faulty record (it had already read the line break), so look at that line and the one before it.
What each tool catches
| Problem | named-checkzone | dnspython from_file |
|---|---|---|
| Syntax error, bad IP address, extra field | Error, exit 1 | dns.exception.SyntaxError with file and line |
| No SOA at the origin | Error | NoSOA (from_file checks the origin by default) |
| No NS at the origin | Error | NoNS (raised by check_origin) |
| MX or NS pointing to a CNAME | Warning by default (-M, -n modes) | Not checked |
| MX/NS/SRV target without an address record | Checked by -i full (the default) | Not checked |
| CNAME next to other data at the same name | Error | CNAMEAndOtherData while parsing |
Pitfalls
- dnspython accepted a test file without
$TTLwhose records had no TTL; other software may warn or choose a different default, so set$TTLat the top of every file. @means the current origin. Inside a file loaded asexample.com,@ IN MX 10 mailtargetsmail.example.com..- Running the check under the wrong zone name makes relative names expand wrongly while the file still "passes". Use the exact zone name from
named.conf.
A file that passes both checks can still serve wrong data (for example a typo in an IP address that is still valid). After reloading, query the authoritative server for the changed names, as described in checking DNS propagation.
FAQ
Does named-checkzone catch a forgotten serial increment?
No. It checks one file on its own, so it cannot know the previous serial. Compare the SOA serial with the one the servers publish (dig SOA +short) before you reload, because secondaries ignore a zone whose serial did not increase.
Why does a name without a trailing dot end up as www.example.com.example.com?
Names without a final dot are relative to $ORIGIN. A CNAME or MX target written as www.example.com (no dot) becomes www.example.com.example.com.; write fully qualified targets with the trailing dot, or use the relative form on purpose.
Is it safe to run named-checkzone on a file someone sent me?
The BIND manual warns against it: $INCLUDE directives can make the parser read other files you can access, and parts of them can show up in error messages. Run it only on trusted zone text or as an unprivileged user.