package localzone import ( "os" "path/filepath" "strings" "testing" "github.com/miekg/dns" ) // loadManaged compiles a managed directory the way the app does, so a test // asserts on what the query path would actually serve rather than on the // writer's own idea of it. func loadManaged(t *testing.T, dir string) *Store { t.Helper() store, err := Load([]string{dir}, Options{}) if err != nil { t.Fatalf("load managed dir: %v", err) } return store } func mustAdd(t *testing.T, m *Managed, origin string, rec Record, def ZoneDefaults) uint32 { t.Helper() serial, err := m.Add(origin, rec, def) if err != nil { t.Fatalf("add %s %s: %v", rec.Name, rec.Type, err) } return serial } func zoneDef() ZoneDefaults { return ZoneDefaults{NS: []string{"ns1.example.com."}} } func TestManagedAddWritesAParsableZoneFile(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) first := mustAdd(t, m, "example.com", Record{Name: "www.example.com", Type: "A", TTL: 60, Value: "10.0.0.1"}, ZoneDefaults{}) raw, err := os.ReadFile(filepath.Join(dir, "example.com.zone")) if err != nil { t.Fatalf("read zone file: %v", err) } text := string(raw) for _, want := range []string{ "$ORIGIN example.com.", "$TTL 5m", "IN SOA", "IN NS", "IN A", "10.0.0.1", } { if !strings.Contains(text, want) { t.Errorf("zone file is missing %q:\n%s", want, text) } } // The second write bumps the serial: a zone file that does not move its // serial tells nobody the data changed. second := mustAdd(t, m, "example.com", Record{Name: "db.example.com", Type: "A", TTL: 60, Value: "10.0.0.2"}, ZoneDefaults{}) if second != first+1 { t.Errorf("serial after second write = %d, want %d", second, first+1) } store := loadManaged(t, dir) if got := store.Zones(); len(got) != 1 || got[0] != "example.com." { t.Fatalf("zones = %v, want [example.com.]", got) } res := store.Lookup(dns.Question{Name: "www.example.com.", Qtype: dns.TypeA}) if !res.Found || len(res.Records) != 1 { t.Fatalf("lookup www = %+v, want one A record", res) } if a, ok := res.Records[0].(*dns.A); !ok || a.A.String() != "10.0.0.1" { t.Errorf("www A = %v, want 10.0.0.1", res.Records[0]) } // A record written without a TTL inherits the file's $TTL, not the // library default of 3600. if ttl := res.Records[0].Header().Ttl; ttl != 60 { t.Errorf("www TTL = %d, want 60", ttl) } } func TestManagedSnapshotMarksTheConsoleAsTheSource(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) mustAdd(t, m, "example.com", Record{Name: "www.example.com", Type: "A", TTL: 60, Value: "10.0.0.1"}, ZoneDefaults{}) zone := loadManaged(t, dir).Snapshot("example.com.", dir) if zone == nil { t.Fatal("snapshot is nil") } if !zone.Managed { t.Error("zone is not marked managed, so the console would refuse to edit it") } if len(zone.Records) != 2 || zone.RecordCount != 2 { t.Fatalf("records = %d, want 2", zone.RecordCount) } if zone.Serial == 0 { t.Error("snapshot has no SOA serial") } for _, rec := range zone.Records { if !rec.Managed { t.Errorf("record %s %s is not marked managed", rec.Name, rec.Type) } if !strings.HasPrefix(rec.Source, dir) { t.Errorf("record %s %s source = %q, want a path under %s", rec.Name, rec.Type, rec.Source, dir) } if rec.Delegation { t.Errorf("the apex NS set is reported as a delegation: %+v", rec) } } if len(zone.Delegations) != 0 { t.Errorf("delegations = %+v, want none", zone.Delegations) } } // An operator's own master file is not the console's to rewrite, so its // records have to be visible with Managed false. func TestManagedSnapshotOfAnOperatorFileIsNotManaged(t *testing.T) { dir := t.TempDir() managed := NewManaged(dir) mustAdd(t, managed, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) op := filepath.Join(t.TempDir(), "example.com") if err := os.WriteFile(op, []byte("$ORIGIN example.com.\n$TTL 300\n@ 300 IN SOA ns1 hostmaster 1 3600 600 604800 300\n@ 300 IN NS ns1\nhost 300 IN A 192.0.2.7\n"), 0o644); err != nil { t.Fatal(err) } store, err := Load([]string{dir, op}, Options{}) if err != nil { t.Fatalf("load: %v", err) } zone := store.Snapshot("example.com.", dir) if zone.Managed { t.Error("a zone with an operator-managed record is reported as wholly managed") } var found bool for _, rec := range zone.Records { if rec.Name == "host.example.com." { found = true if rec.Managed { t.Error("the operator's record is marked managed") } if rec.Value != "192.0.2.7" { t.Errorf("operator record value = %q", rec.Value) } } } if !found { t.Errorf("the operator's record is missing from the snapshot: %+v", zone.Records) } } func TestManagedDelegationIsACutBelowTheApex(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) if _, err := m.SetDelegation("example.com", "sub.example.com", []string{"ns1.sub.example.com.", "ns2.sub.example.com."}, 300, ZoneDefaults{}); err != nil { t.Fatalf("set delegation: %v", err) } store := loadManaged(t, dir) zone := store.Snapshot("example.com.", dir) if len(zone.Delegations) != 1 { t.Fatalf("delegations = %+v, want one", zone.Delegations) } cut := zone.Delegations[0] if cut.Name != "sub.example.com." { t.Errorf("delegated name = %q", cut.Name) } if strings.Join(cut.Targets, ",") != "ns1.sub.example.com.,ns2.sub.example.com." { t.Errorf("delegation targets = %v", cut.Targets) } if !cut.Managed { t.Error("delegation is not marked managed") } // A cut means the parent answers nothing below it, and the store is what // decides that. res := store.Lookup(dns.Question{Name: "host.sub.example.com.", Qtype: dns.TypeA}) if !res.Delegated { t.Errorf("lookup below the cut = %+v, want Delegated", res) } // The NS RRset at the cut itself stays answerable, or a resolver can // never learn the delegation (RFC 1034 §4.3.3). res = store.Lookup(dns.Question{Name: "sub.example.com.", Qtype: dns.TypeNS}) if !res.Found || len(res.Records) != 2 { t.Errorf("NS at the cut = %+v, want two records", res) } // Above the cut the parent still answers for itself. if res := store.Lookup(dns.Question{Name: "example.com.", Qtype: dns.TypeNS}); !res.Found { t.Error("apex NS lookup stopped working after a delegation was added") } } func TestManagedDelegationRejectsTheApexAndEmptyTargets(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) if _, err := m.SetDelegation("example.com", "example.com", []string{"ns1.example.com."}, 300, ZoneDefaults{}); err == nil { t.Error("delegating the apex was accepted") } if _, err := m.SetDelegation("example.com", "sub.example.com", nil, 300, ZoneDefaults{}); err == nil { t.Error("a delegation with no targets was accepted") } if _, err := m.SetDelegation("example.com", "elsewhere.test", []string{"ns1.test."}, 300, ZoneDefaults{}); err == nil { t.Error("a delegation outside the zone was accepted") } } func TestManagedRemoveDelegationDropsTheWholeCut(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) if _, err := m.SetDelegation("example.com", "sub.example.com", []string{"ns1.sub.example.com.", "ns2.sub.example.com."}, 300, ZoneDefaults{}); err != nil { t.Fatal(err) } if _, removed, err := m.RemoveDelegation("example.com", "sub.example.com."); err != nil || !removed { t.Fatalf("remove delegation: removed=%v err=%v", removed, err) } zone := loadManaged(t, dir).Snapshot("example.com.", dir) if len(zone.Delegations) != 0 { t.Errorf("delegations = %+v, want none left", zone.Delegations) } // The parent answers for the name again once the cut is gone. if res := loadManaged(t, dir).Lookup(dns.Question{Name: "host.sub.example.com.", Qtype: dns.TypeA}); res.Delegated { t.Error("the name is still delegated after the cut was removed") } // A second removal is a stale console, not a server error. if _, _, err := m.RemoveDelegation("example.com", "sub.example.com."); err == nil { t.Error("removing a delegation twice was accepted") } } func TestManagedPutReplacesTheRRset(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) for _, target := range []string{"10 mx1.example.com.", "20 mx2.example.com."} { mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "MX", TTL: 300, Value: target}, ZoneDefaults{}) } if _, err := m.Put("example.com", Record{Name: "example.com", Type: "MX", TTL: 300, Value: "10 mx1.example.com."}, ZoneDefaults{}); err != nil { t.Fatalf("put: %v", err) } zone := loadManaged(t, dir).Snapshot("example.com.", dir) var mx int for _, rec := range zone.Records { if rec.Type == "MX" { mx++ } } if mx != 1 { t.Errorf("MX records = %d, want the whole RRset replaced by one", mx) } } func TestManagedDeleteIsScopedToTheRRsetOrTheRecord(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "TXT", TTL: 300, Value: `"one"`}, ZoneDefaults{}) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "TXT", TTL: 300, Value: `"two"`}, ZoneDefaults{}) if _, removed, err := m.Delete("example.com", "example.com", "TXT", `"two"`); err != nil || !removed { t.Fatalf("delete one TXT: removed=%v err=%v", removed, err) } zone := loadManaged(t, dir).Snapshot("example.com.", dir) var txt int for _, rec := range zone.Records { if rec.Type == "TXT" { txt++ } } if txt != 1 { t.Errorf("TXT records = %d, want the one that was not deleted", txt) } if _, _, err := m.Delete("example.com", "example.com", "TXT", ""); err != nil { t.Fatalf("delete the RRset: %v", err) } if _, removed, err := m.Delete("example.com", "example.com", "TXT", ""); err == nil || removed { t.Error("deleting an absent record succeeded") } } func TestManagedRefusesWhatItCannotRoundTrip(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) seed := Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."} mustAdd(t, m, "example.com", seed, ZoneDefaults{}) cases := []struct { what string rec Record }{ {"out of zone", Record{Name: "host.elsewhere.test.", Type: "A", Value: "10.0.0.1"}}, {"unknown type", Record{Name: "example.com", Type: "BOGUS", Value: "x"}}, {"SOA", Record{Name: "example.com", Type: "SOA", Value: "a. b. 1 2 3 4 5"}}, {"empty value", Record{Name: "example.com", Type: "A", Value: ""}}, {"malformed rdata", Record{Name: "example.com", Type: "A", Value: "not-an-address"}}, {"empty name", Record{Name: "", Type: "A", Value: "10.0.0.1"}}, } for _, tc := range cases { if _, err := m.Add("example.com", tc.rec, ZoneDefaults{}); err == nil { t.Errorf("%s was accepted", tc.what) } } // A duplicate is refused rather than written twice, and neither attempt // touched the file. if _, err := m.Add("example.com", seed, ZoneDefaults{}); err == nil { t.Error("a duplicate record was accepted") } zone := loadManaged(t, dir).Snapshot("example.com.", dir) if len(zone.Records) != 1 { t.Errorf("records = %d, want only the seed record", len(zone.Records)) } } func TestManagedRefusesABadZoneApex(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) for _, origin := range []string{"", ".", "example", "example..com."} { if _, err := m.Add(origin, Record{Name: "example.com.", Type: "A", Value: "10.0.0.1"}, ZoneDefaults{}); err == nil { t.Errorf("origin %q was accepted", origin) } } // The one shape that is a zone: an apex with at least two labels. if _, err := m.Add("example.com", Record{Name: "www.example.com", Type: "A", Value: "10.0.0.1"}, ZoneDefaults{NS: []string{"ns1.example.com."}}); err != nil { t.Fatalf("valid origin refused: %v", err) } } // A zone with no SOA cannot say NXDOMAIN, and one with no apex NS has no // server to name, so the writer refuses to create either. func TestManagedRefusesAZoneWithNoServerList(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) _, err := m.Add("example.com", Record{Name: "www.example.com", Type: "A", Value: "10.0.0.1"}, ZoneDefaults{}) if err == nil { t.Fatal("a brand new zone with no NS record was created") } if !strings.Contains(err.Error(), "NS") { t.Errorf("error = %v, want it to say what is missing", err) } if _, statErr := os.Stat(filepath.Join(dir, "example.com.zone")); statErr == nil { t.Error("a refused write left a zone file behind") } } func TestManagedWildcardAndRelativeTargetsSurviveTheRoundTrip(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) // A wildcard owner and a relative RDATA name: both are the kind of thing // an operator types and a writer has to preserve. mustAdd(t, m, "example.com", Record{Name: "*.example.com", Type: "A", TTL: 60, Value: "10.0.0.9"}, ZoneDefaults{}) mustAdd(t, m, "example.com", Record{Name: "alias.example.com", Type: "CNAME", TTL: 60, Value: "target"}, ZoneDefaults{}) zone := loadManaged(t, dir).Snapshot("example.com.", dir) byKey := map[string]Record{} for _, rec := range zone.Records { byKey[rec.Key()] = rec } wild, ok := byKey["*.example.com.|A"] if !ok { t.Fatalf("the wildcard record was lost: %+v", zone.Records) } if !wild.Wildcard { t.Error("the wildcard record is not flagged as one") } if wild.Value != "10.0.0.9" { t.Errorf("wildcard value = %q", wild.Value) } cname, ok := byKey["alias.example.com.|CNAME"] if !ok { t.Fatalf("the CNAME was lost: %+v", zone.Records) } if cname.Value != "target.example.com." { t.Errorf("a relative CNAME target became %q, want it qualified by the origin", cname.Value) } // And the store still answers the wildcard, which is the point of it. res := loadManaged(t, dir).Lookup(dns.Question{Name: "anything.example.com.", Qtype: dns.TypeA}) if !res.Found || len(res.Records) != 1 { t.Errorf("wildcard lookup = %+v, want an answer", res) } } func TestManagedSeedsFromAnExistingZone(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) // The apex and SOA come from a zone that already exists elsewhere, so // the console can add a record to it without the caller restating them. _, err := m.Add("example.com", Record{Name: "new.example.com", Type: "A", Value: "10.0.0.5"}, ZoneDefaults{ TTL: 120, Soa: "ns1.example.com. hostmaster.example.com. 2024010101 3600 600 604800 300", NS: []string{"ns1.example.com.", "ns2.example.com."}, }) if err != nil { t.Fatalf("add with seeded zone: %v", err) } zone := loadManaged(t, dir).Snapshot("example.com.", dir) if zone.Serial != 2024010102 { t.Errorf("serial = %d, want the seeded SOA's serial bumped once by the write", zone.Serial) } if zone.DefaultTTL != 120 { t.Errorf("default TTL = %d, want the seeded 120", zone.DefaultTTL) } var ns int for _, rec := range zone.Records { if rec.Name == "example.com." && rec.Type == "NS" { ns++ } } if ns != 2 { t.Errorf("apex NS records = %d, want the two seeded", ns) } } // Every type the API accepts has to survive being written and read back. A // type in the whitelist that the master-file assembler does not recognise // would render a line no parse accepts, and the writer's round-trip check // would refuse the write for every record of that type. func TestEveryEditableTypeSurvivesTheFile(t *testing.T) { samples := map[string]string{ "A": "10.0.0.1", "AAAA": "2001:db8::1", "CAA": `0 issue "letsencrypt.org"`, "CNAME": "target.example.com.", "DHCID": "AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=", "HINFO": `"cpu" "os"`, "HTTPS": "1 . alpn=h2", "LOC": "42 21 54 N 71 06 18 W -24m 30m 0m 10m", // NAPTR's flags, service and regexp are all character-strings in // miekg's presentation format, so all three are quoted; a bare // regexp (as in RFC 3403 §4.1) does not parse. "MX": "10 mail.example.com.", "NAPTR": `100 50 "S" "SIP+D2U" "!^.*$!sip:info@example.com!" _sip._udp.example.com.`, "NS": "ns1.example.com.", "PTR": "host.example.com.", "SRV": "0 1 443 target.example.com.", "SPF": `"v=spf1 -all"`, "SSHFP": "1 1 aa11223344556666778899aabbccddeeff00112233445566778899aabbccddeeff", "SVCB": "1 target.example.com. alpn=h2", "TXT": `"hello" "world"`, // miekg presents URI's target as a character-string, so it has to be // quoted; the samples above pin that. "URI": `10 1 "https://example.com/"`, } for _, rrtype := range EditableTypes() { sample, ok := samples[rrtype] if !ok { t.Errorf("editable type %s has no sample in this test; the writer is untested for it", rrtype) continue } dir := t.TempDir() m := NewManaged(dir) if _, err := m.Add("example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}); err != nil { t.Fatalf("%s: seed zone: %v", rrtype, err) } if _, err := m.Add("example.com", Record{Name: "rec.example.com", Type: rrtype, TTL: 300, Value: sample}, ZoneDefaults{}); err != nil { t.Errorf("%s: add: %v", rrtype, err) continue } zone := loadManaged(t, dir).Snapshot("example.com.", dir) var found *Record for i := range zone.Records { if zone.Records[i].Name == "rec.example.com." && zone.Records[i].Type == rrtype { found = &zone.Records[i] } } if found == nil { t.Errorf("%s: the record did not survive the file: %+v", rrtype, zone.Records) continue } // The canonical value is what the API echoed, so the console shows // the same text the file holds. if found.TTL != 300 { t.Errorf("%s: TTL = %d, want 300", rrtype, found.TTL) } } } func TestManagedWritableAndPath(t *testing.T) { dir := filepath.Join(t.TempDir(), "nested", "zones") m := NewManaged(dir) if !m.Writable() { t.Error("a directory under a writable parent is not writable") } // Asking must not touch the disk: the console calls this on every read. if _, err := os.Stat(dir); !os.IsNotExist(err) { t.Errorf("checking writability created the directory (stat err = %v)", err) } if _, err := m.Add("example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}); err != nil { t.Fatalf("add: %v", err) } if !m.Writable() { t.Error("a directory the process just wrote into is not writable") } if got := m.Path("example.com."); got != filepath.Join(dir, "example.com.zone") { t.Errorf("Path = %q", got) } if got := NewManaged("").Writable(); got { t.Error("an unconfigured managed directory is writable") } // The probe must not leave files behind. entries, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } if len(entries) != 1 || entries[0].Name() != "example.com.zone" { names := make([]string, 0, len(entries)) for _, e := range entries { names = append(names, e.Name()) } t.Errorf("managed directory holds %v, want only the zone file", names) } } // The console's form asks for a short name, because a zone is already chosen. // A name without a trailing dot therefore means "in this zone", the same way // it does in a zone file. func TestManagedQualifiesARelativeOwnerName(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) mustAdd(t, m, "example.com", Record{Name: "www", Type: "A", TTL: 60, Value: "10.0.0.1"}, ZoneDefaults{}) mustAdd(t, m, "example.com", Record{Name: "*.cdn", Type: "A", TTL: 60, Value: "10.0.0.2"}, ZoneDefaults{}) zone := loadManaged(t, dir).Snapshot("example.com.", dir) got := map[string]string{} for _, rec := range zone.Records { got[rec.Name] = rec.Value } if got["www.example.com."] != "10.0.0.1" { t.Errorf("relative owner became %v, want www.example.com.", got) } if got["*.cdn.example.com."] != "10.0.0.2" { t.Errorf("relative wildcard owner became %v, want *.cdn.example.com.", got) } // And the store serves them under the qualified names. if res := loadManaged(t, dir).Lookup(dns.Question{Name: "cdn.example.com.", Qtype: dns.TypeA}); !res.Found { t.Error("the wildcard does not answer under its qualified name") } // Deleting by the same short name finds the same record. if _, removed, err := m.Delete("example.com", "www", "A", ""); err != nil || !removed { t.Errorf("delete by relative name: removed=%v err=%v", removed, err) } // An absolute name outside the zone is still refused. if _, err := m.Add("example.com", Record{Name: "host.elsewhere.test.", Type: "A", Value: "10.0.0.9"}, ZoneDefaults{}); err == nil { t.Error("an absolute name outside the zone was accepted") } } // A zone whose SOA names a server that is not in its own NS set hands out a // referral no resolver can follow, and the console is what changes that set. func TestManagedSOAPrimaryFollowsTheApexNSSet(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) zone := loadManaged(t, dir).Snapshot("example.com.", dir) if !strings.HasPrefix(zone.Soa, "ns1.example.com. ") { t.Fatalf("soa = %q, want ns1.example.com. as the primary", zone.Soa) } if _, err := m.Put("example.com", Record{Name: "example.com", Type: "NS", TTL: 300, Value: "ns2.example.com."}, ZoneDefaults{}); err != nil { t.Fatal(err) } zone = loadManaged(t, dir).Snapshot("example.com.", dir) if !strings.HasPrefix(zone.Soa, "ns2.example.com. ") { t.Errorf("soa = %q after replacing the apex NS set, want ns2.example.com.", zone.Soa) } } // A zone file that is not a zone file must be reported, not silently // rewritten. func TestManagedRefusesAFileWhoseOriginMoved(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) if err := os.Rename(m.Path("example.com."), m.Path("other.com.")); err != nil { t.Fatal(err) } if _, err := m.Add("example.com", Record{Name: "www.example.com", Type: "A", Value: "10.0.0.1"}, ZoneDefaults{}); err == nil { t.Error("a file holding another zone was overwritten") } } // A cut means the names under it belong to another server, so a record there // is dead data: the store answers them as delegated without ever looking at // what the zone file says. func TestManagedRefusesDataBelowADelegation(t *testing.T) { dir := t.TempDir() m := NewManaged(dir) mustAdd(t, m, "example.com", Record{Name: "example.com", Type: "NS", Value: "ns1.example.com."}, ZoneDefaults{}) if _, err := m.SetDelegation("example.com", "sub.example.com", []string{"ns1.sub.example.com."}, 300, ZoneDefaults{}); err != nil { t.Fatal(err) } below := Record{Name: "host.sub.example.com", Type: "A", TTL: 60, Value: "10.0.0.3"} if _, err := m.Add("example.com", below, ZoneDefaults{}); err == nil { t.Error("a record below a delegation was accepted") } for _, rec := range loadManaged(t, dir).Snapshot("example.com.", dir).Records { if rec.Name == below.Name { t.Errorf("the refused record was written anyway: %+v", rec) } } // The delegation itself is still there and still a cut. if res := loadManaged(t, dir).Lookup(dns.Question{Name: "host.sub.example.com.", Qtype: dns.TypeA}); !res.Delegated { t.Error("the cut stopped being a cut") } // And the rule follows the cut, not the record: removing the delegation // makes the same record legal again. if _, _, err := m.RemoveDelegation("example.com", "sub.example.com."); err != nil { t.Fatal(err) } if _, err := m.Add("example.com", below, ZoneDefaults{}); err != nil { t.Errorf("a record under a name that is no longer delegated was refused: %v", err) } } func TestZoneFileName(t *testing.T) { for origin, want := range map[string]string{ "example.com.": "example.com.zone", "EXAMPLE.com.": "example.com.zone", "example.com": "example.com.zone", "a.b.example.": "a.b.example.zone", "example.com...": "example.com...zone", } { if got := zoneFileName(origin); got != want { t.Errorf("zoneFileName(%q) = %q, want %q", origin, got, want) } } } func TestNormalizeOriginAndName(t *testing.T) { if got, err := NormalizeOrigin(" Example.COM "); err != nil || got != "example.com." { t.Errorf("NormalizeOrigin = %q, %v", got, err) } if got, err := NormalizeName("*.WWW.Example.com"); err != nil || got != "*.www.example.com." { t.Errorf("NormalizeName = %q, %v", got, err) } if _, err := NormalizeName("."); err == nil { t.Error("the root was accepted as an owner name") } // A name that is only a wildcard is not a zone the store can hold. if _, err := NormalizeOrigin("*."); err == nil { t.Error("a wildcard was accepted as a zone apex") } } func TestZoneFileIsHumanReadable(t *testing.T) { f := &fileState{ origin: "example.com.", ttl: 300, soa: mustSOA(t, "example.com. 300 IN SOA ns1.example.com. hostmaster.example.com. 2024010101 3600 600 604800 300"), recs: []Record{ {Name: "www.example.com.", Type: "A", TTL: 60, Value: "10.0.0.1"}, {Name: "example.com.", Type: "NS", TTL: 300, Value: "ns1.example.com."}, }, } text := f.render() if !strings.Contains(text, "@") || !strings.Contains(text, "IN SOA") { t.Errorf("the apex SOA is not written with @:\n%s", text) } // One record per line, with the owner relative to the origin: that is the // form this project's master-file parser accepts, so what the writer // emits is what the loader can read. A bare owner line would parse as a // record with no type. for _, line := range strings.Split(text, "\n") { trimmed := strings.TrimSpace(line) if trimmed == "" || strings.HasPrefix(trimmed, ";") || strings.HasPrefix(trimmed, "$") { continue } if !strings.Contains(line, " IN ") { t.Errorf("record line without an owner, TTL, class and type: %q\n%s", line, text) } } if !strings.Contains(text, "1m IN A") || !strings.Contains(text, "10.0.0.1") { t.Errorf("the A record is missing:\n%s", text) } // And it is a zone file: it parses, and it parses to the same records. parsed, err := parseFileState("example.com.", text) if err != nil { t.Fatalf("rendered file does not parse: %v\n%s", err, text) } if err := sameRecords(parsed.recs, sortCopy(f.recs)); err != nil { t.Errorf("rendered file differs from the records: %v\n%s", err, text) } } func sortCopy(in []Record) []Record { out := append([]Record(nil), in...) sortRecords(out) return out } func mustSOA(t *testing.T, line string) *dns.SOA { t.Helper() rr, err := dns.NewRR(line) if err != nil { t.Fatal(err) } soa, ok := rr.(*dns.SOA) if !ok { t.Fatalf("%q is not an SOA", line) } return soa }