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>
102 lines
4.1 KiB
Go
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 }
|