Update binary name, usage strings, generated file headers, tests, and README. The generated BIND config header now includes the install path: go install go.askask.com/catz@latest
6.8 KiB
catz
Generate RFC 9432 DNS catalog zone files from a declarative input file.
Given a list of zones with their catalog assignments and optional properties, this tool produces one BIND-format zone file per catalog. Output is deterministic: re-running with unchanged input produces unchanged output (no serial bump, no file write).
Installation
go install go.askask.com/catz@latest
Or build from source:
go build -o catz .
Usage
catz [--config path] [--output-dir path] [--bind-conf path] <input-file>
Flags:
--config— path to YAML config file (default:catz.yamlnext to the input file)--output-dir— directory for output zone files (default: same directory as the input file)--bind-conf— path to write a BINDdomains.conffile (optional; see BIND Config Output)
Configuration File
The config file (default catz.yaml) defines catalog zone names and SOA parameters.
catalogs:
catalog1:
zone: catalog1.example.com.
catalog2:
zone: catalog2.example.com.
soa:
mname: ns1.example.com.
rname: hostmaster.example.com.
Fields
| Field | Required | Description |
|---|---|---|
catalogs |
yes | Map of catalog names to their zone FQDNs. Names are used as references in the input file. |
catalogs.<name>.zone |
yes | The FQDN of the catalog zone (trailing dot optional, will be normalized). |
soa.mname |
yes | Primary nameserver for the SOA record. |
soa.rname |
yes | Responsible person email (in DNS format: hostmaster.example.com.). |
SOA timing values are hardcoded: refresh=900, retry=600, expire=2147483646, minimum=0.
The NS record is hardcoded to invalid. per RFC 9432.
Input File Format
Whitespace and comma delimited. Lines starting with # are comments.
Blank lines are ignored.
<zone-name> <catalog>[, <catalog>...] [, group=<value>] [, coo=<fqdn>] [, file=<path>] [, dnssec]
Fields
| Position | Format | Description |
|---|---|---|
| First token | FQDN | Zone name (trailing dot optional, normalized internally). |
| Bare names | identifier | Catalog assignments — must match a key in the config catalogs map. At least one required. |
group=<value> |
key=value | RFC 9432 group property. Tells consumers to apply shared configuration to grouped zones. |
coo=<fqdn> |
key=value | RFC 9432 change-of-ownership property. Points to the old catalog zone during migration. |
file=<path> |
key=value | Zone data file path for BIND config output. Zones without file= are included in catalog zones but skipped in --bind-conf output. |
dnssec |
bare flag | Adds dnssec-policy standard; inline-signing yes; to the BIND config for this zone. |
A zone can appear in multiple catalogs (for distributing to different server groups).
Example
# Production zones
zone.example.org catalog1, catalog2, file=data/zones/example.org
zone.example.com catalog2, coo=old-catalog.example.com., file=data/zones/example.com
test.example.net catalog1, group=internal, file=data/zones/example.net, dnssec
app.example.org catalog1, group=external, coo=migrated.example.com., file=data/zones/app.example.org
Whitespace and comma placement is flexible. These are all equivalent:
zone.example.org catalog1,catalog2
zone.example.org catalog1 , catalog2
zone.example.org catalog1, catalog2
Output
One BIND-format zone file per catalog, written to the output directory.
Filename: <catalog-zone-fqdn>.zone (e.g., catalog1.example.com.zone)
Example output:
catalog1.example.com. 0 IN SOA ns1.example.com. hostmaster.example.com. 2026030201 900 600 2147483646 0
catalog1.example.com. 0 IN NS invalid.
version.catalog1.example.com. 0 IN TXT "2"
grfen8g.zones.catalog1.example.com. 0 IN PTR app.example.org.
group.grfen8g.zones.catalog1.example.com. 0 IN TXT "external"
coo.grfen8g.zones.catalog1.example.com. 0 IN PTR migrated.example.com.
2qvgcfg.zones.catalog1.example.com. 0 IN PTR test.example.net.
group.2qvgcfg.zones.catalog1.example.com. 0 IN TXT "internal"
1860l9o.zones.catalog1.example.com. 0 IN PTR zone.example.org.
Record order
- SOA record
- NS record (
invalid.) - Version TXT record (
"2") - Member zones sorted alphabetically by zone name, each with:
- PTR record (member zone)
- Group TXT record (if
group=set) - COO PTR record (if
coo=set)
All records use TTL 0 and class IN. Fully-qualified owner names; no $ORIGIN or $TTL directives.
Member zone hashing
Member zone labels are generated by FNV-1a 32-bit hashing the normalized zone
name, then encoding as lowercase base32hex without padding. This produces
compact labels like grfen8g.
Changing tools will likely produce different hash labels, which is intentional per RFC 9432 Section 5.4 — it triggers a reconfig event on consumers.
SOA serial
Format: YYYYMMDDNN where NN is a sequence number (01-99).
- New zone files start at
YYYYMMDD01. - On subsequent runs, the tool generates output with the existing serial and compares bytes. If unchanged, no write occurs.
- If content differs, the serial is incremented (same date bumps NN; new date
resets to
YYYYMMDD01). - If NN reaches 99 on the same date: the tool errors.
Validation
The tool validates input and reports errors with file location:
error: zones.txt:3: unknown catalog "bogus"
error: zones.txt:5: zone example.com. already assigned to catalog "catalog1" at line 2
Checked conditions:
- Unknown catalog name (not in config) — error
- Same zone assigned to the same catalog more than once — error
- Hash collision (two zone names produce the same hash within a catalog) — error
- Missing required config fields — error
- Unknown properties (anything other than
group,coo,file) — error - Empty
file=value — error dnssec=<anything>(dnssec is a bare flag, not a key=value) — error- When
--bind-confis used: zones withoutfile=are silently skipped (included in catalog output only)
BIND Config Output
When --bind-conf <path> is specified, a BIND domains.conf file is written
in addition to the catalog zone files. This file defines all zones as type master with their file paths from the file= input property.
Example output:
# THIS FILE IS GENERATED BY catz (go install go.askask.com/catz@latest)
#=============================================
#
zone "askask.com" {
type master;
file "data/ask/askask.com";
};
zone "bitcard.org" {
type master;
file "data/misc/bitcard.org"; dnssec-policy standard; inline-signing yes;
};
- Zones are sorted alphabetically by name
- 8-space indentation
- DNSSEC zones (marked with
dnssecin the input) getdnssec-policy standard; inline-signing yes;on the same line asfile - Zones without a
file=property are skipped (they appear in catalog zones only)