Linked zones: PTR records follow each view, deleting an override stays deleted

Auto-PTR (and --ptr) changed PTR records in every linked view, whatever
view the address record was in. ptrSync now compares each touched A/AAAA
set before and after, per view, and changes only that view's reverse
zone. PTR records in linked reverse zones are local (fill too); a shared
PTR set is made local in each view before one view changes it.

Deleting a linked zone's local records no longer brings the shared ones
back into the view: the name and type are gone from that view and the
other views keep theirs, local to each. Share still restores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-18 15:29:12 +01:00
1 parent e38390df7d
commit 65086e314a
9 files changed
+453 -238

No files matched your search

+99
View File
@@ -303,3 +303,102 @@ func TestReverseFromOtherView(t *testing.T) {
t.Errorf("warnings %q", res.Warnings)
}
}
// ptrsMarked is ptrs with S (shared) or L (local) after each record.
func (env *testEnv) ptrsMarked(view, zone string) []string {
env.t.Helper()
s := env.state()
z := s.FindZone(zone, s.ViewByName(view).ID)
var out []string
for _, r := range s.ZoneRecords(z.ID) {
if r.Type == "PTR" {
mark := " L"
if r.SharedID != 0 {
mark = " S"
}
out = append(out, strings.TrimSuffix(r.Name, ".10.0.10.in-addr.arpa.")+" "+strings.TrimSuffix(r.Data, ".example.com.")+mark)
}
}
return out
}
// Auto-PTR with linked forward and reverse zones: PTR records follow each
// view's own address records, and are local to their view.
func TestAutoPTRLinked(t *testing.T) {
env := linkedEnv(t)
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("external"), LinkWith: "internal"}))
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("10.0.10.0/24"), View: model.Ptr("internal")}))
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("10.0.10.0/24"), View: model.Ptr("external"), LinkWith: "internal"}))
st := env.state()
for _, v := range []string{"internal", "external"} {
env.submit(env.admin, NewRequest(OpZoneUpdate, st.FindZone("example.com.", st.ViewByName(v).ID).ID, ZoneInput{AutoPTR: model.Ptr(true)}))
}
rz := "10.0.10.in-addr.arpa."
local := func(view, name, data string) Request {
r := recIn(view, name, "A", data)
var in RecordInput
_ = decode(r.Data, &in)
in.Local = model.Ptr(true)
return NewRequest(OpRecordCreate, 0, in)
}
// A local record: a local PTR record in its own view only.
env.submit(env.admin, local("internal", "a", "10.0.10.1"))
want(t, "local a internal", env.ptrsMarked("internal", rz), "1 a L")
want(t, "local a external", env.ptrsMarked("external", rz))
// The other view adds the same name (shared, as it has no local
// one): its own PTR record; deleting it in one view leaves the other.
env.submit(env.admin, recIn("external", "a", "A", "10.0.10.1"))
want(t, "both internal", env.ptrsMarked("internal", rz), "1 a L")
want(t, "both external", env.ptrsMarked("external", rz), "1 a L")
env.submit(env.admin, NewRequest(OpRecordDelete, env.recordID("external", "a", "A"), nil))
want(t, "deleted in external: internal", env.ptrsMarked("internal", rz), "1 a L")
want(t, "deleted in external: external", env.ptrsMarked("external", rz))
// Both local; one changes its address, the other keeps its PTR.
env.submit(env.admin, local("external", "a", "10.0.10.1"))
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "a", "A"), RecordInput{Data: model.Ptr("10.0.10.2")}))
want(t, "moved internal", env.ptrsMarked("internal", rz), "2 a L")
want(t, "moved external", env.ptrsMarked("external", rz), "1 a L")
// Disabling in one view disables there only.
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("external", "a", "A"), RecordInput{Disabled: model.Ptr(true)}))
want(t, "disabled external", env.ptrs("external", rz), "1.10.0.10.in-addr.arpa. a.example.com. disabled")
want(t, "disabled: internal", env.ptrsMarked("internal", rz), "2 a L")
env.submit(env.admin, NewRequest(OpRecordDelete, env.recordID("external", "a", "A"), nil))
env.submit(env.admin, NewRequest(OpRecordDelete, env.recordID("internal", "a", "A"), nil))
want(t, "gone internal", env.ptrsMarked("internal", rz))
want(t, "gone external", env.ptrsMarked("external", rz))
// A shared record: a local PTR in each view; deleting it removes both.
env.submit(env.admin, recIn("internal", "b", "A", "10.0.10.3"))
want(t, "shared internal", env.ptrsMarked("internal", rz), "3 b L")
want(t, "shared external", env.ptrsMarked("external", rz), "3 b L")
// Overridden in one view with another address: that view's PTR moves.
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("external", "b", "A"), RecordInput{Local: model.Ptr(true), Data: model.Ptr("10.0.10.4")}))
want(t, "override internal", env.ptrsMarked("internal", rz), "3 b L")
want(t, "override external", env.ptrsMarked("external", rz), "4 b L")
// Sharing internal's version: external gets it back, and its PTR.
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "b", "A"), RecordInput{Local: model.Ptr(false)}))
want(t, "shared again internal", env.ptrsMarked("internal", rz), "3 b L")
want(t, "shared again external", env.ptrsMarked("external", rz), "3 b L")
// The override deleted: external goes without b (and its PTR);
// internal keeps it, local now.
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("external", "b", "A"), RecordInput{Local: model.Ptr(true), Data: model.Ptr("10.0.10.4")}))
env.submit(env.admin, NewRequest(OpRecordDelete, env.recordID("external", "b", "A"), nil))
want(t, "override gone internal", env.ptrsMarked("internal", rz), "3 b L")
want(t, "override gone external", env.ptrsMarked("external", rz))
env.submit(env.admin, NewRequest(OpRecordDelete, env.recordID("internal", "b", "A"), nil))
want(t, "b gone internal", env.ptrsMarked("internal", rz))
// A shared PTR record from before (shared by hand) becomes local in
// each view when one view's address goes.
env.submit(env.admin, recIn("internal", "c", "A", "10.0.10.5"))
env.submit(env.admin, NewRequest(OpRecordDelete, 0, RecordInput{Zone: rz, View: "internal", Name: model.Ptr("5"), Type: model.Ptr("PTR")}))
env.submit(env.admin, NewRequest(OpRecordDelete, 0, RecordInput{Zone: rz, View: "external", Name: model.Ptr("5"), Type: model.Ptr("PTR")}))
env.submit(env.admin, NewRequest(OpRecordCreate, 0, RecordInput{Zone: rz, View: "internal", Name: model.Ptr("5"), Type: model.Ptr("PTR"), Data: model.Ptr("c.example.com."), Local: model.Ptr(false)}))
want(t, "shared ptr external", env.ptrsMarked("external", rz), "5 c S")
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("external", "c", "A"), RecordInput{Local: model.Ptr(true), Data: model.Ptr("10.0.10.6")}))
want(t, "unshared internal", env.ptrsMarked("internal", rz), "5 c L")
want(t, "unshared external", env.ptrsMarked("external", rz), "6 c L")
}