# catalog-zone-gen 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 catalog-zone-gen@latest ``` Or build from source: ``` go build -o catalog-zone-gen . ``` ## Usage ``` catalog-zone-gen [--config path] [--output-dir path] [--bind-conf path] ``` **Flags:** - `--config` — path to YAML config file (default: `catz.yaml` next to the input file) - `--output-dir` — directory for output zone files (default: same directory as the input file) - `--bind-conf` — path to write a BIND `domains.conf` file (optional; see [BIND Config Output](#bind-config-output)) ## Configuration File The config file (default `catz.yaml`) defines catalog zone names and SOA parameters. ```yaml 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..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. ``` [, ...] [, group=] [, coo=] [, file=] [, 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=` | key=value | RFC 9432 group property. Tells consumers to apply shared configuration to grouped zones. | | `coo=` | key=value | RFC 9432 change-of-ownership property. Points to the old catalog zone during migration. | | `file=` | 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:** `.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 1. SOA record 2. NS record (`invalid.`) 3. Version TXT record (`"2"`) 4. 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=` (dnssec is a bare flag, not a key=value) — error - When `--bind-conf` is used: zones without `file=` are silently skipped (included in catalog output only) ## BIND Config Output When `--bind-conf ` 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 catalog-zone-gen #============================================= # 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 `dnssec` in the input) get `dnssec-policy standard; inline-signing yes;` on the same line as `file` - Zones without a `file=` property are skipped (they appear in catalog zones only)