Files
DDI/server/internal/configgen/bindgen/bindgen.go
T
sir_melonchenandClaude Opus 5.5 5a6087c96e feat(configgen): update key and dynamic-records.json for dynamic zones
Targets that serve a dynamic zone as primary get the DNS agents' update
key (Source version 4, UpdateKeyID) in the update ACLs and two more
artifacts: ddi-suite-update.key (secret) and dynamic-records.json with
the records DDI owns in each dynamic zone (ADR 0026). Older sources and
targets without dynamic primary zones render byte-identically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 05:39:43 +02:00

102 lines
4.1 KiB
Go

// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
// Package bindgen renders a configgen.DNSConfig into BIND 9 (≥ 9.18)
// configuration: a named.conf include with the zone statements, a second
// include with statements for the options block, a third (secret) include
// with the TSIG keys, and one zone file per primary zone (ADR 0004,
// ADR 0014, ADR 0023).
//
// The distribution's named.conf includes the generated files like this:
//
// options {
// directory "/var/cache/bind";
// include "/etc/bind/ddi-suite/ddi-suite-options.conf";
// };
// include "/etc/bind/ddi-suite/ddi-suite-keys.conf";
// include "/etc/bind/ddi-suite/ddi-suite.conf";
//
// BIND accepts only one options block, so the generator cannot emit its own;
// BIND accepts "include" wherever a clause of a block may appear, so the
// options are included into the distribution's block instead. The keys come
// before the zones that refer to them.
//
// Primary zones in a cluster notify the secondaries explicitly
// ("notify explicit", "also-notify" with the transfer key per server) and
// allow transfers only with the transfer key; secondary zones transfer from
// their primaries with the key and are written by BIND to
// <ZoneDir>/secondary/db.<zone>, so no artifact is generated for them.
//
// With DNSConfig.UpdateKey every dynamic primary zone also accepts updates
// signed with that key, and two more files are written at the configuration
// root (ADR 0026): configgen.UpdateKeyFile with the key alone (for
// "nsupdate -k") and configgen.DynamicRecordsFile with the records DDI owns
// in the dynamic zones, which the DNS agent adds and removes with nsupdate.
//
// Every value is validated before anything is written. Names are restricted
// to letters, digits, '-' and (where DNS needs it) a leading '_', so no input
// can leave a quoted string or a statement; TSIG secrets must be canonical
// base64.
package bindgen
import (
"git.beugsiud.de/beug.k/DDI/server/internal/configgen"
)
// Paths of the generated files relative to the configuration root.
const (
// ConfFile holds the top-level statements (ACLs and zones).
ConfFile = "ddi-suite.conf"
// OptionsFile holds statements for the options block.
OptionsFile = "ddi-suite-options.conf"
// KeysFile holds the TSIG keys (a secret artifact, always generated).
KeysFile = "ddi-suite-keys.conf"
// ZonesDir holds the zone files; the agent maps it to DNSConfig.ZoneDir.
ZonesDir = "zones"
// SecondaryDir is the directory below DNSConfig.ZoneDir where BIND
// writes the files of secondary zones. The DNS agent creates it.
SecondaryDir = "secondary"
)
// fileMode is the mode of every generated file (readable by the named group).
const fileMode = 0o640
// Render validates cfg and returns the include files (ddi-suite.conf,
// ddi-suite-options.conf, ddi-suite-keys.conf), with cfg.UpdateKey the
// update key file and the records of the dynamic zones (ADR 0026), followed
// by one zone file per primary zone, sorted by zone name. Invalid input
// yields a *configgen.ValidationError and no artifacts.
func Render(cfg configgen.DNSConfig) ([]configgen.Artifact, error) {
m, err := validate(cfg)
if err != nil {
return nil, err
}
out := make([]configgen.Artifact, 0, 5+len(m.zones))
out = append(out,
configgen.Artifact{Path: ConfFile, Mode: fileMode, Content: renderConf(m)},
configgen.Artifact{Path: OptionsFile, Mode: fileMode, Content: renderOptions(m)},
configgen.Artifact{Path: KeysFile, Mode: fileMode, Content: renderKeys(m), Secret: true},
)
if m.updateKey != nil {
out = append(out,
configgen.Artifact{Path: configgen.UpdateKeyFile, Mode: fileMode, Content: renderUpdateKey(m), Secret: true},
configgen.Artifact{Path: configgen.DynamicRecordsFile, Mode: fileMode, Content: renderDynamicRecords(m)},
)
}
for i := range m.zones {
z := &m.zones[i]
if z.secondary {
continue
}
out = append(out, configgen.Artifact{
Path: ZonesDir + "/" + zoneFileName(z.name),
Mode: fileMode,
Content: renderZone(m.serial, z),
CreateOnly: z.dynamic,
})
}
return out, nil
}
// zoneFileName is the file name of a zone's file, e.g. "db.office.test".
func zoneFileName(zone string) string { return "db." + zone }