converge apex NS per record, not by RRset replace
BIND ignores an RRset-wide delete of apex NS, so the previous replace only appended to the seed placeholder.
This commit is contained in:
@@ -23,6 +23,7 @@ const (
|
||||
NamedBin = "/usr/sbin/named"
|
||||
RndcBin = "/usr/sbin/rndc"
|
||||
NsupdateBin = "/usr/bin/nsupdate"
|
||||
DigBin = "/usr/bin/dig"
|
||||
)
|
||||
|
||||
// Config file paths derived from ConfigDir.
|
||||
|
||||
+37
-17
@@ -19,35 +19,55 @@ type RecordUpdate struct {
|
||||
Type string // RR type
|
||||
TTL int32 // record TTL
|
||||
Values []string // RDATA entries
|
||||
Delete bool // when true, delete the RRset instead of replacing it
|
||||
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 {
|
||||
var b strings.Builder
|
||||
b.WriteString("server 127.0.0.1\n")
|
||||
b.WriteString(fmt.Sprintf("zone %s\n", dot(zone)))
|
||||
for _, u := range updates {
|
||||
// Replace semantics: clear the RRset first, then add the desired values.
|
||||
b.WriteString(fmt.Sprintf("update delete %s %s\n", dot(u.FQDN), u.Type))
|
||||
if u.Delete {
|
||||
continue
|
||||
}
|
||||
for _, v := range u.Values {
|
||||
b.WriteString(fmt.Sprintf("update add %s %d %s %s\n", dot(u.FQDN), u.TTL, u.Type, v))
|
||||
}
|
||||
}
|
||||
b.WriteString("send\n")
|
||||
|
||||
cmd := []string{NsupdateBin, "-y", fmt.Sprintf("%s:%s:%s", creds.Algorithm, creds.Name, creds.Secret)}
|
||||
if out, err := e.Exec(ctx, namespace, pod, cmd, b.String()); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 == "@" {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package bind
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNSUpdateScriptReplaceSemantics(t *testing.T) {
|
||||
got := nsupdateScript("acme.unkin.net", []RecordUpdate{
|
||||
{FQDN: "www", Type: "A", TTL: 60, Values: []string{"10.0.0.1", "10.0.0.2"}},
|
||||
{FQDN: "old.acme.unkin.net.", Type: "TXT", Delete: true},
|
||||
})
|
||||
want := `server 127.0.0.1
|
||||
zone acme.unkin.net.
|
||||
update delete www. A
|
||||
update add www. 60 A 10.0.0.1
|
||||
update add www. 60 A 10.0.0.2
|
||||
update delete old.acme.unkin.net. TXT
|
||||
send
|
||||
`
|
||||
if got != want {
|
||||
t.Errorf("got:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// At the apex BIND ignores an RRset-wide delete of NS, so the apex sync must add
|
||||
// the new names and delete the old ones record by record, adds first: named
|
||||
// refuses to leave an apex with no NS record.
|
||||
func TestNSUpdateScriptPerValueApexNS(t *testing.T) {
|
||||
got := nsupdateScript("acme.unkin.net", []RecordUpdate{
|
||||
{FQDN: "acme.unkin.net.", Type: "NS", TTL: 60, Values: []string{"acme-ns1.unkin.net."}, PerValue: true},
|
||||
{FQDN: "acme.unkin.net.", Type: "NS", Values: []string{"ns1.acme.unkin.net."}, PerValue: true, Delete: true},
|
||||
{FQDN: "ns1.acme.unkin.net.", Type: "A", Delete: true},
|
||||
})
|
||||
want := `server 127.0.0.1
|
||||
zone acme.unkin.net.
|
||||
update add acme.unkin.net. 60 NS acme-ns1.unkin.net.
|
||||
update delete acme.unkin.net. NS ns1.acme.unkin.net.
|
||||
update delete ns1.acme.unkin.net. A
|
||||
send
|
||||
`
|
||||
if got != want {
|
||||
t.Errorf("got:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
@@ -84,3 +84,21 @@ func (e *Executor) ZoneSerial(ctx context.Context, namespace, pod, zone, view st
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// ApexNS returns the zone's currently published apex NS names, queried from the
|
||||
// local server so the operator can converge the RRset rather than append to it.
|
||||
func (e *Executor) ApexNS(ctx context.Context, namespace, pod, zone string) ([]string, error) {
|
||||
out, err := e.Exec(ctx, namespace, pod, []string{DigBin, "+short", "@127.0.0.1", dot(zone), "NS"}, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query apex NS of %s: %w (out: %s)", zone, err, out)
|
||||
}
|
||||
var ns []string
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
// dig +short prints one fully-qualified name per line; anything without
|
||||
// a trailing dot is not an answer.
|
||||
if line = strings.TrimSpace(line); strings.HasSuffix(line, ".") {
|
||||
ns = append(ns, line)
|
||||
}
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user