Ask Bjørn Hansen 0a460b975d Clean up code reuse, consistency, and efficiency issues
Merge readExistingSerial and readExistingContent into a single
readExisting function to eliminate duplicate file I/O. Extract dateBase
helper to deduplicate serial formula between defaultSerial and
bumpSerial. Cache hash results during collision check to avoid
recomputing per member. Normalize error prefixes (remove "error:" from
fmt.Errorf, add uniformly at print sites). Use filepath.Join instead of
manual "/" concatenation. Replace trivial containsStr wrapper with
strings.Contains. Simplify tokenize to a single return. Use writeTestFile
and fixedTime helpers consistently in tests.
2026-03-01 17:38:26 -08:00
2026-02-28 16:13:58 -08:00
2026-02-28 16:13:02 -08:00
2026-02-28 16:13:58 -08:00
2026-02-28 16:13:58 -08:00
2026-02-28 16:13:58 -08:00
2026-02-28 16:13:58 -08:00
2026-02-28 16:13:58 -08:00

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.

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
Description
DNS catalog zones helper
Readme 101 KiB
Languages
Go 100%