Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Zones

Each zone is one JSON file in the zones directory (<zones_dir>/*.json), the source of truth for that zone.

File name

The file name is derived from the zone name: lowercase, no trailing dot, only [a-z0-9.-], for example example.com.json. The root zone is _root.json. Names that cannot be written this way (underscores, escapes, non-ASCII; use punycode for IDN) are rejected. A file whose name does not match its origin is rejected.

Format

{
  "schema": 1,
  "origin": "example.com.",
  "default_ttl": 3600,
  "soa": {"mname": "ns1.example.com.", "rname": "hostmaster.example.com.",
          "serial": 1, "refresh": 7200, "retry": 3600, "expire": 1209600, "minimum": 300},
  "records": [
    {"name": "www", "type": "A", "ttl": 300, "value": "192.0.2.1"}
  ],
  "dnssec": {"enabled": false, "algorithm": 13, "key_layout": "csk", "denial": "nsec3",
             "nsec3": {"iterations": 0, "salt": "", "opt_out": false},
             "signature_validity_days": 14, "refresh_days": 5, "dnskey_ttl": 3600,
             "propagation_delay": 3600}
}
  • name is relative to the origin, @ is the origin, and a trailing dot makes it absolute. It must lie inside the zone.
  • value is RFC 1035 presentation format. Names inside values are absolute.
  • ttl is optional and defaults to default_ttl. The SOA takes an optional ttl too.
  • Supported types: A, AAAA, NS, CNAME, PTR, MX, TXT, SRV, CAA and DS. The SOA lives in the soa block only.
  • DS is only allowed at a delegation point (a name below the apex with NS records): it is the parent’s half of a secure delegation and is signed with the zone.
  • A CNAME cannot share a name with any other record (including the apex), nor repeat.
  • DNSKEY, RRSIG, NSEC, NSEC3, NSEC3PARAM, CDS and CDNSKEY are generated by signing and are rejected when written by hand.

DNSSEC policy

The optional dnssec block is validated even when enabled is false: algorithm 13 or 15, key_layout csk or ksk_zsk, denial nsec or nsec3, refresh_days below signature_validity_days, NSEC3 salt as hex. NSEC3 iterations must be 0 (RFC 9276) unless allow_nonzero_iterations is set, which is accepted with a warning (at most 100, the validator’s own cap: a zone above it would be insecure to validators).

Writes and hot reload

Writes are atomic (temp file, fsync, rename). The server watches the directory and reloads 250 ms after the last change. An invalid file never replaces a working zone: the previous version keeps serving and the error is kept as the zone’s load_error, visible in the logs and through the API.

A reload reads every file but parses only the ones whose content changed since the last reload: an unchanged zone keeps its parsed Arc<Zone> and its lookup index (and an unchanged invalid file keeps its recorded error), so editing one of many zone files costs one parse and one index build.

Answering queries

The server answers authoritatively for every name inside a loaded zone, following RFC 1034 section 4.3.2 and RFC 4592. Queries outside all zones, or for a class other than IN (or ANY), are not answered from zones.

  • Closest enclosing zone. When zones nest (example.com. and dev.example.com.), the most specific zone answers.
  • Exact match. The RRset of the asked type, plus A/AAAA records from the zone for the names that NS, MX and SRV answers point at (additional section). ANY returns every RRset of the name, including the SOA at the apex.
  • CNAME. A CNAME is followed while its target stays in the same zone, for at most 8 links; a loop stops at the first repeated name. The answer holds every CNAME followed and then the target’s data. A target outside the zone (or in another zone) ends the chain and the client resolves the rest. A chain that ends in a missing name or type carries the SOA in the authority section. Asking for CNAME (or ANY) returns the CNAME itself without following it.
  • Wildcards. A name that does not exist is answered from *.<closest encloser>, with the owner rewritten to the query name. A wildcard never applies to a name that exists, and empty non-terminals exist: with only _ssh._tcp.host2.example. present, host2.example. and _tcp.host2.example. are NODATA, not wildcard matches. A * label asked for directly is matched literally, and a wildcard CNAME is synthesised and then followed.
  • Delegation. An NS RRset below the apex is a zone cut. Any query at or below it gets a referral: the cut’s NS records plus glue, the A/AAAA records the zone holds for in-zone name servers. Records below a cut are only served as glue. DS at the cut itself is answered by the parent side (NODATA while the zone is unsigned).
  • NODATA and NXDOMAIN. A name that exists without the asked type is NODATA; a name that does not exist and has no wildcard is NXDOMAIN. Both carry the zone SOA in the authority section with TTL = min(SOA TTL, SOA minimum) (RFC 2308).
  • Rotation. Every query rotates the order of each RRset by one, so clients that pick the first address spread across all of them. The counter is kept per thread (one cache line each), so each worker rotates independently and cores do not contend on it.
  • Signed answers. A zone served signed keeps its RRsets as shared slices and, for NSEC3, the hash of every name in the zone, computed once when the index is built; only names that do not exist (the next closer name, the wildcard) are hashed per query.

BIND export

dnsy_zone::export_bind(&zone, signed) renders a zone as a BIND master file: $ORIGIN, $TTL, a multi-line SOA first, then the records grouped by owner with owner names relative to the origin (@ for the apex). A record’s TTL is written only when it differs from $TTL. With a signed zone (a SignedZone from the zone signer, see DNSSEC) the DNSKEY, CDS, CDNSKEY, RRSIG, NSEC/NSEC3 and NSEC3PARAM records follow in canonical order.

The output is safe to hand to other tools. Owner names, rdata names and character-strings are escaped: . \ ; ( ) " $ @ get a backslash and non-printable bytes become \DDD, so a record cannot end its line, start a comment or inject a directive such as $INCLUDE. TXT strings longer than 255 octets are split into several strings, and a CAA record whose tag is not alphanumeric is written in the RFC 3597 generic form (\# len hex).

The export is checked with named-checkzone when it is installed (the test is skipped otherwise).

Managing zones over the API

Zones, records and the BIND export are available under /api/v1/zones (see REST API). The API writes the same JSON files described above, atomically, bumps the SOA serial on every change and keeps the dnssec block of the file. Records are addressed by a stable id derived from name, type and value. GET /zones/{name} shows load_error when the file on disk is currently invalid (the previous version keeps serving). Authoritative answers are not cached, so a change is visible to queries as soon as the write returns.

Serial semantics and request bodies

Every edit goes through one place in dnsy-zone (ZoneStore::update): it takes the store’s write lock, applies the change to a copy of the stored zone, validates it exactly like a file, bumps the SOA serial by one (wrapping at 2^32) and writes the file atomically. Any successful write bumps the serial, including DNSSEC policy changes; a failed edit writes nothing and keeps the serial. A serial supplied in a PUT body is ignored; POST /zones uses it as the initial serial (default 1). An If-Match that no longer matches is answered 412 and nothing is written, even when another writer got in between the check and the write.

POST /zones and PUT /zones/{name} reject unknown top-level fields (and unknown fields in soa and in records) with 422 instead of ignoring them, so a misspelled default_tll is an error rather than a silently missing setting.