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
201 lines
6.8 KiB
Markdown
201 lines
6.8 KiB
Markdown
# 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.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 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 `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)
|