Add catalog-zone-gen tool

Generate RFC 9432 DNS catalog zone files from a declarative input file.
Parses zone-to-catalog assignments with optional group and coo properties,
produces deterministic BIND-format output with automatic SOA serial
management and change detection.
This commit is contained in:
2026-02-28 16:13:58 -08:00
parent 5f230676d7
commit 1f2f39f40c
14 changed files with 1944 additions and 0 deletions

166
README.md Normal file
View File

@@ -0,0 +1,166 @@
# 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] <input-file>
```
**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)
## 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.<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>]
```
### 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. |
A zone can appear in multiple catalogs (for distributing to different server groups).
### Example
```
# Production zones
zone.example.org catalog1, catalog2
zone.example.com catalog2, coo=old-catalog.example.com.
test.example.net catalog1, group=internal
app.example.org catalog1, group=external, coo=migrated.example.com.
```
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
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` and `coo`) — error