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.
167 lines
5.2 KiB
Markdown
167 lines
5.2 KiB
Markdown
# 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
|