Accept intra-cluster NOTIFY on secondaries via allow-notify
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

Secondaries transfer catalog member and plain secondary zones from the
primary Service ClusterIP (stable across primary pod restarts), and BIND
derives a zone's implicit allow-notify from its primaries list. But the
primary pod's NOTIFYs egress with its *pod* IP as source — k8s Services
NAT only the inbound direction — so BIND refuses them as "refused notify
from non-primary" and replication falls back to the SOA refresh timer, a
1-hour propagation delay on every dynamic zone (external-dns RFC2136 and
dns-updater nsupdates alike).

Render an options-scope allow-notify on secondaries covering the primary
pod IP (and the transfer address, since an explicit allow-notify replaces
the primaries-derived default). The cluster controller resolves the
primary pod IP the same way it already does for seeding/also-notify, and
the existing Pod watch re-renders the ConfigMap when the pod IP changes.
This commit is contained in:
2026-07-25 22:48:35 +10:00
parent 439aa9ea6b
commit 7771711682
3 changed files with 84 additions and 0 deletions
+48
View File
@@ -22,6 +22,15 @@ type RenderInput struct {
Forwards []bindv1alpha1.BindZone
// PrimaryAddress is the in-cluster address secondaries transfer from.
PrimaryAddress string
// PrimaryPodAddresses are the primary pod's own IP(s). Secondaries transfer
// from PrimaryAddress (the stable primary Service ClusterIP) but the primary
// pod's NOTIFYs egress with its *pod* IP as the source — k8s Services only
// NAT the inbound direction — so BIND, whose implicit allow-notify is the
// zone's primaries list (the ClusterIP), REFUSES them as "non-primary" and
// replication falls back to the SOA refresh timer. Secondaries render these
// into an options-scope allow-notify so intra-cluster NOTIFYs are accepted
// immediately. Empty leaves BIND's default behaviour unchanged.
PrimaryPodAddresses []string
}
// RenderNamedConf returns the primary and secondary named.conf contents for a
@@ -86,6 +95,12 @@ func render(in RenderInput, isPrimary bool) string {
b.WriteString(" allow-new-zones yes;\n")
}
b.WriteString(" dnssec-validation auto;\n")
// Secondaries accept NOTIFY from the primary's pod IP(s). Catalog member and
// plain secondary zones take their implicit allow-notify from their primaries
// (the primary Service ClusterIP), but the primary's NOTIFYs are sourced from
// its pod IP, so an options-scope allow-notify covering the pod IP(s) is
// needed or every NOTIFY is refused and replication waits for the SOA refresh.
b.WriteString(allowNotifyClause(in, isPrimary, " "))
for _, o := range c.Spec.ExtraOptions {
b.WriteString(" " + strings.TrimRight(o, ";") + ";\n")
}
@@ -277,6 +292,39 @@ func transferPrimaries(in RenderInput) []string {
return out
}
// allowNotifyClause renders an options-scope allow-notify on secondaries that
// permits the primary pod IP(s). Zones (catalog members and plain secondaries)
// point their primaries at the primary Service ClusterIP for stable AXFR, which
// also becomes their implicit allow-notify — but NOTIFYs leave the primary pod
// with its pod IP as source, so without this they are refused as "non-primary".
// Emitted only on secondaries and only when the primary pod IP(s) are known.
func allowNotifyClause(in RenderInput, isPrimary bool, indent string) string {
if isPrimary {
return ""
}
addrs := make([]string, 0, len(in.PrimaryPodAddresses)+1)
seen := map[string]bool{}
for _, a := range in.PrimaryPodAddresses {
a = strings.TrimSpace(strings.TrimRight(a, ";"))
if a == "" || seen[a] {
continue
}
seen[a] = true
addrs = append(addrs, a)
}
// Keep the transfer address (the Service ClusterIP, or the pod IP when no
// primary Service exists) in the set: an explicit allow-notify replaces the
// implicit primaries-derived default, so it must still cover that source.
if a := strings.TrimSpace(strings.TrimRight(in.PrimaryAddress, ";")); a != "" && !seen[a] {
addrs = append(addrs, a)
}
if len(addrs) == 0 {
return ""
}
sort.Strings(addrs)
return fmt.Sprintf("%sallow-notify { %s };\n", indent, terminate(addrs))
}
func catalogZonesClause(in RenderInput, isPrimary bool, indent string) string {
// Only secondaries consume the catalog to auto-provision member zones.
if in.Catalog == nil || isPrimary {