Missing reverse zones are suggested for the private views' A and AAAA records (/24 for private IPv4, /32 for a public address, /48 for IPv6) and created filled with PTR records (jarl zone suggest-reverse, the Zones page). zone create --fill and zone fill-ptr add the PTRs a reverse zone lacks; a reverse zone created without a view goes to the private view using its addresses. Per zone, auto_ptr keeps the PTR records of A and AAAA records in step on every change (database version 12). Zone commands take a network. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
6.7 KiB
Go
139 lines
6.7 KiB
Go
package engine
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"jarl/internal/model"
|
|
)
|
|
|
|
// ptrs returns a reverse zone's PTR records in a view as "owner target",
|
|
// with " disabled" when so.
|
|
func (env *testEnv) ptrs(view, zone string) []string {
|
|
env.t.Helper()
|
|
s := env.state()
|
|
z := s.FindZone(zone, s.ViewByName(view).ID)
|
|
if z == nil {
|
|
env.t.Fatalf("no zone %s in %s", zone, view)
|
|
}
|
|
var out []string
|
|
for _, r := range s.ZoneRecords(z.ID) {
|
|
if r.Type == "PTR" {
|
|
d := r.Name + " " + r.Data
|
|
if r.Disabled {
|
|
d += " disabled"
|
|
}
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestReverseFillAndSuggest(t *testing.T) {
|
|
env := linkedEnv(t)
|
|
env.submit(env.admin,
|
|
recIn("internal", "@", "A", "192.0.2.25"),
|
|
recIn("internal", "nas", "A", "10.0.10.20"),
|
|
recIn("internal", "files", "A", "10.0.10.20"),
|
|
recIn("internal", "nas", "AAAA", "fdab:abab:abab:10::20"),
|
|
recIn("internal", "*.dev", "A", "10.0.10.99"),
|
|
recIn("internal", "lo", "A", "127.0.0.1"))
|
|
|
|
sugg := ReverseSuggestions(env.state(), env.e.Exposures(env.state()))
|
|
var got []string
|
|
for _, s := range sugg {
|
|
got = append(got, s.View+" "+s.Prefix+" "+s.Zone)
|
|
}
|
|
want(t, "suggestions", got,
|
|
"internal 10.0.10.0/24 10.0.10.in-addr.arpa",
|
|
"internal 192.0.2.10/32 10.2.0.192.in-addr.arpa",
|
|
"internal 192.0.2.25/32 25.2.0.192.in-addr.arpa",
|
|
"internal fdab:abab:abab::/48 b.a.b.a.b.a.b.a.b.a.d.f.ip6.arpa")
|
|
if sugg[0].Fill != 1 || sugg[0].Samples[0] != "10.0.10.20 files.example.com" {
|
|
t.Errorf("10.0.10.0/24: %+v", sugg[0])
|
|
}
|
|
|
|
// No view given: the private view whose records use the addresses.
|
|
res := env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("10.0.10.0/24"), Fill: true}))
|
|
want(t, "10.0.10/24", env.ptrs("internal", "10.0.10.in-addr.arpa."), "20.10.0.10.in-addr.arpa. files.example.com.")
|
|
if !slices.ContainsFunc(res.Warnings, func(w string) bool { return strings.Contains(w, "10.0.10.20 has several names") }) {
|
|
t.Errorf("warnings %q", res.Warnings)
|
|
}
|
|
// The mail server's name beats the domain's own.
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("192.0.2.25/32"), Fill: true}))
|
|
want(t, "192.0.2.25", env.ptrs("internal", "25.2.0.192.in-addr.arpa."), "25.2.0.192.in-addr.arpa. mail.example.com.")
|
|
|
|
// Filling an existing zone; then there is nothing left to add.
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("fdab:abab:abab::/48")}))
|
|
st := env.state()
|
|
z6 := st.FindZone("b.a.b.a.b.a.b.a.b.a.d.f.ip6.arpa.", st.ViewByName("internal").ID)
|
|
env.submit(env.admin, NewRequest(OpZoneFillPTR, z6.ID, LinkInput{}))
|
|
if p := env.ptrs("internal", z6.Name); len(p) != 1 || !strings.HasSuffix(p[0], " nas.example.com.") {
|
|
t.Errorf("ip6 fill: %q", p)
|
|
}
|
|
if e := env.submitErr(env.admin, NewRequest(OpZoneFillPTR, z6.ID, LinkInput{})); !strings.Contains(e.Message, "nothing to add") {
|
|
t.Errorf("second fill: %v", e)
|
|
}
|
|
|
|
got = got[:0]
|
|
for _, s := range ReverseSuggestions(env.state(), env.e.Exposures(env.state())) {
|
|
got = append(got, s.Prefix)
|
|
}
|
|
want(t, "suggestions left", got, "192.0.2.10/32")
|
|
|
|
// Addresses that two private views use: the view must be given.
|
|
env.submit(env.admin, NewRequest(OpViewCreate, 0, ViewInput{Name: model.Ptr("lab"), MatchClients: &[]string{"172.16.0.0/12"}}))
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("lab.test"), View: model.Ptr("lab")}),
|
|
NewRequest(OpRecordCreate, 0, RecordInput{Zone: "lab.test", View: "lab", Name: model.Ptr("x"), Type: model.Ptr("A"), Data: model.Ptr("192.0.2.10")}))
|
|
if e := env.submitErr(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("192.0.2.10/32")})); !strings.Contains(e.Message, "views internal and lab all have addresses") {
|
|
t.Errorf("two views: %v", e)
|
|
}
|
|
}
|
|
|
|
func TestAutoPTR(t *testing.T) {
|
|
env := linkedEnv(t)
|
|
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("10.0.10.0/24"), View: model.Ptr("internal")}))
|
|
st := env.state()
|
|
zid := st.FindZone("example.com.", st.ViewByName("internal").ID).ID
|
|
env.submit(env.admin, NewRequest(OpZoneUpdate, zid, ZoneInput{AutoPTR: model.Ptr(true)}))
|
|
rz := "10.0.10.in-addr.arpa."
|
|
|
|
res := env.submit(env.admin, recIn("internal", "host", "A", "10.0.10.30"), recIn("internal", "pub", "A", "203.0.113.5"))
|
|
want(t, "added", env.ptrs("internal", rz), "30.10.0.10.in-addr.arpa. host.example.com.")
|
|
if len(res.Warnings) > 0 {
|
|
t.Errorf("no reverse zone for 203.0.113.5 isn't worth a warning: %q", res.Warnings)
|
|
}
|
|
// A second name for the address: its PTR stays.
|
|
res = env.submit(env.admin, recIn("internal", "alias", "A", "10.0.10.30"))
|
|
want(t, "second name", env.ptrs("internal", rz), "30.10.0.10.in-addr.arpa. host.example.com.")
|
|
if len(res.Warnings) > 0 {
|
|
t.Errorf("warnings %q", res.Warnings)
|
|
}
|
|
env.submit(env.admin, NewRequest(OpRecordDelete, env.recordID("internal", "alias", "A"), nil))
|
|
|
|
// Moved, disabled, enabled, deleted.
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "host", "A"), RecordInput{Data: model.Ptr("10.0.10.31")}))
|
|
want(t, "moved", env.ptrs("internal", rz), "31.10.0.10.in-addr.arpa. host.example.com.")
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "host", "A"), RecordInput{Disabled: model.Ptr(true)}))
|
|
want(t, "disabled", env.ptrs("internal", rz), "31.10.0.10.in-addr.arpa. host.example.com. disabled")
|
|
env.submit(env.admin, NewRequest(OpRecordUpdate, env.recordID("internal", "host", "A"), RecordInput{Disabled: model.Ptr(false)}))
|
|
want(t, "enabled", env.ptrs("internal", rz), "31.10.0.10.in-addr.arpa. host.example.com.")
|
|
env.submit(env.admin, NewRequest(OpRecordDelete, env.recordID("internal", "host", "A"), nil))
|
|
want(t, "deleted", env.ptrs("internal", rz))
|
|
|
|
// A whole set replaced.
|
|
env.submit(env.admin, NewRequest(OpRRSetReplace, 0, RRSetInput{Zone: "example.com", View: "internal", Name: "multi", Type: "A", Data: []string{"10.0.10.40", "10.0.10.41"}}))
|
|
want(t, "set", env.ptrs("internal", rz), "40.10.0.10.in-addr.arpa. multi.example.com.", "41.10.0.10.in-addr.arpa. multi.example.com.")
|
|
env.submit(env.admin, NewRequest(OpRRSetReplace, 0, RRSetInput{Zone: "example.com", View: "internal", Name: "multi", Type: "A", Data: []string{"10.0.10.41"}}))
|
|
want(t, "set shrunk", env.ptrs("internal", rz), "41.10.0.10.in-addr.arpa. multi.example.com.")
|
|
// Deleting by name and type.
|
|
env.submit(env.admin, NewRequest(OpRecordDelete, 0, RecordInput{Zone: "example.com", View: "internal", Name: model.Ptr("multi"), Type: model.Ptr("A")}))
|
|
want(t, "deleted by match", env.ptrs("internal", rz))
|
|
|
|
// Off again: records come and go alone.
|
|
env.submit(env.admin, NewRequest(OpZoneUpdate, zid, ZoneInput{AutoPTR: model.Ptr(false)}))
|
|
env.submit(env.admin, recIn("internal", "quiet", "A", "10.0.10.50"))
|
|
want(t, "off", env.ptrs("internal", rz))
|
|
}
|