Files
catz/README.md
Ask Bjørn Hansen 49f7ad2987 Allow catalog-only zones without file= in --bind-conf mode
Zones without a file= property (e.g. ddns zones) are included in
catalog zone output for secondaries but skipped in domains.conf.
Previously --bind-conf required every zone to have file= set.
2026-03-28 11:56:49 -07:00

201 lines
6.8 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] [--bind-conf 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)
- `--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.<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
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=<anything>` (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 <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 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)