cc714dc2b4
named rejects an apex NS pointing at an in-zone name with no address record, so the glue has to land in an earlier transaction.
113 lines
3.9 KiB
Go
113 lines
3.9 KiB
Go
package bind
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// TSIGCreds carries the material needed to authenticate a dynamic update.
|
|
type TSIGCreds struct {
|
|
Name string // TSIG key name
|
|
Algorithm string // e.g. hmac-sha256
|
|
Secret string // base64-encoded key
|
|
}
|
|
|
|
// RecordUpdate describes a desired record set to apply to a zone.
|
|
type RecordUpdate struct {
|
|
FQDN string // fully-qualified owner name, trailing dot recommended
|
|
Type string // RR type
|
|
TTL int32 // record TTL
|
|
Values []string // RDATA entries
|
|
Delete bool // when true, delete instead of add
|
|
// PerValue operates on individual records rather than the whole RRset: adds
|
|
// leave existing records in place, deletes remove only the listed Values.
|
|
// Required at a zone apex, where BIND silently ignores an RRset-wide delete
|
|
// of NS or SOA and would turn a replace into an append.
|
|
PerValue bool
|
|
}
|
|
|
|
// NSUpdate applies a set of record changes to zone by executing nsupdate on the
|
|
// primary pod, targeting the local server and authenticating with creds. All
|
|
// changes are sent in a single atomic transaction.
|
|
func (e *Executor) NSUpdate(ctx context.Context, namespace, pod, zone string, creds TSIGCreds, updates []RecordUpdate) error {
|
|
cmd := []string{NsupdateBin, "-y", fmt.Sprintf("%s:%s:%s", creds.Algorithm, creds.Name, creds.Secret)}
|
|
if out, err := e.Exec(ctx, namespace, pod, cmd, nsupdateScript(zone, updates)); err != nil {
|
|
return fmt.Errorf("nsupdate zone %s: %w (out: %s)", zone, err, out)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ApexNS returns the zone's currently published apex NS names, so the operator
|
|
// can converge the RRset rather than append to it. The query is TSIG-signed with
|
|
// the same creds as an update: a zone behind a view whose match-clients is a key
|
|
// is unreachable to an unsigned query, which named answers REFUSED (with an empty
|
|
// body and a zero exit status), and the caller must not read that as "no NS".
|
|
func (e *Executor) ApexNS(ctx context.Context, namespace, pod, zone string, creds TSIGCreds) ([]string, error) {
|
|
cmd := []string{
|
|
DigBin, "-y", fmt.Sprintf("%s:%s:%s", creds.Algorithm, creds.Name, creds.Secret),
|
|
"+short", "+time=5", "+tries=1", "@127.0.0.1", dot(zone), "NS",
|
|
}
|
|
out, err := e.Exec(ctx, namespace, pod, cmd, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query apex NS of %s: %w (out: %s)", zone, err, out)
|
|
}
|
|
return parseDigNames(out), nil
|
|
}
|
|
|
|
// parseDigNames picks the answers out of `dig +short` output: one fully-qualified
|
|
// name per line. Anything without a trailing dot is not a name, and dig prefixes
|
|
// its diagnostics (a missing or mismatched TSIG key among them) with ';'.
|
|
func parseDigNames(out string) []string {
|
|
var names []string
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, ";") || !strings.HasSuffix(line, ".") {
|
|
continue
|
|
}
|
|
names = append(names, line)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// nsupdateScript renders the nsupdate input for a set of changes.
|
|
func nsupdateScript(zone string, updates []RecordUpdate) string {
|
|
var b strings.Builder
|
|
b.WriteString("server 127.0.0.1\n")
|
|
fmt.Fprintf(&b, "zone %s\n", dot(zone))
|
|
for _, u := range updates {
|
|
switch {
|
|
case u.PerValue && u.Delete:
|
|
for _, v := range u.Values {
|
|
fmt.Fprintf(&b, "update delete %s %s %s\n", dot(u.FQDN), u.Type, v)
|
|
}
|
|
case u.PerValue:
|
|
for _, v := range u.Values {
|
|
fmt.Fprintf(&b, "update add %s %d %s %s\n", dot(u.FQDN), u.TTL, u.Type, v)
|
|
}
|
|
default:
|
|
// Replace semantics: clear the RRset first, then add the values.
|
|
fmt.Fprintf(&b, "update delete %s %s\n", dot(u.FQDN), u.Type)
|
|
if u.Delete {
|
|
continue
|
|
}
|
|
for _, v := range u.Values {
|
|
fmt.Fprintf(&b, "update add %s %d %s %s\n", dot(u.FQDN), u.TTL, u.Type, v)
|
|
}
|
|
}
|
|
}
|
|
b.WriteString("send\n")
|
|
return b.String()
|
|
}
|
|
|
|
// dot ensures a name is fully qualified with a trailing dot.
|
|
func dot(name string) string {
|
|
if name == "" || name == "@" {
|
|
return "@"
|
|
}
|
|
if strings.HasSuffix(name, ".") {
|
|
return name
|
|
}
|
|
return name + "."
|
|
}
|