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>
252 lines
8.3 KiB
Go
252 lines
8.3 KiB
Go
package dnsname
|
|
|
|
import (
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestOwner(t *testing.T) {
|
|
zone := "example.com."
|
|
cases := []struct{ in, want string }{
|
|
{"", "example.com."},
|
|
{"@", "example.com."},
|
|
{"www", "www.example.com."},
|
|
{"WWW", "www.example.com."},
|
|
{"www.example.com", "www.example.com."},
|
|
{"www.example.com.", "www.example.com."},
|
|
{"example.com", "example.com."},
|
|
{"a.b", "a.b.example.com."},
|
|
{"other.org.", "other.org."},
|
|
{"_acme-challenge.www", "_acme-challenge.www.example.com."},
|
|
{"bücher", "xn--bcher-kva.example.com."},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := Owner(c.in, zone)
|
|
if err != nil || got != c.want {
|
|
t.Errorf("Owner(%q) = %q, %v; want %q", c.in, got, err, c.want)
|
|
}
|
|
}
|
|
// Not fooled by a zone name that's a suffix of a label.
|
|
got, _ := Owner("myexample.com", zone)
|
|
if got != "myexample.com.example.com." {
|
|
t.Errorf("Owner(myexample.com) = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestRelative(t *testing.T) {
|
|
cases := []struct{ name, zone, want string }{
|
|
{"example.com.", "example.com.", "@"},
|
|
{"www.example.com.", "example.com.", "www"},
|
|
{"a.b.example.com.", "example.com.", "a.b"},
|
|
{"other.org.", "example.com.", "other.org."},
|
|
{"x.", ".", "x."},
|
|
}
|
|
for _, c := range cases {
|
|
if got := Relative(c.name, c.zone); got != c.want {
|
|
t.Errorf("Relative(%q,%q) = %q want %q", c.name, c.zone, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckOwner(t *testing.T) {
|
|
ok := []string{".", "example.com.", "*.example.com.", "_dmarc.example.com.", "0/26.2.0.192.in-addr.arpa.", "a-b.c."}
|
|
for _, n := range ok {
|
|
if err := CheckOwner(n); err != nil {
|
|
t.Errorf("CheckOwner(%q) = %v", n, err)
|
|
}
|
|
}
|
|
bad := []string{"a..b.", "x.*.example.com.", "foo*.example.com.", "Upper.example.com.", "sp ace.com.", "example.com"}
|
|
for _, n := range bad {
|
|
if err := CheckOwner(n); err == nil {
|
|
t.Errorf("CheckOwner(%q) succeeded, want error", n)
|
|
}
|
|
}
|
|
long := ""
|
|
for i := 0; i < 64; i++ {
|
|
long += "a"
|
|
}
|
|
if CheckOwner(long+".com.") == nil {
|
|
t.Error("64 octet label accepted")
|
|
}
|
|
if CheckZoneName("*.example.com.") == nil {
|
|
t.Error("wildcard zone accepted")
|
|
}
|
|
}
|
|
|
|
func TestIsHostname(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
wildcard bool
|
|
want bool
|
|
}{
|
|
{"www.example.com.", false, true},
|
|
{"1host.example.com.", false, true},
|
|
{"foo_bar.example.com.", false, false},
|
|
{"-x.example.com.", false, false},
|
|
{"x-.example.com.", false, false},
|
|
{"*.example.com.", true, true},
|
|
{"*.example.com.", false, false},
|
|
{".", false, false},
|
|
{"a.b.", false, true},
|
|
}
|
|
for _, c := range cases {
|
|
if got := IsHostname(c.name, c.wildcard); got != c.want {
|
|
t.Errorf("IsHostname(%q,%v) = %v", c.name, c.wildcard, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMailbox(t *testing.T) {
|
|
mb, err := MailboxFromEmail("first.last@example.com")
|
|
if err != nil || mb != `first\.last.example.com.` {
|
|
t.Fatalf("MailboxFromEmail = %q %v", mb, err)
|
|
}
|
|
if !IsMailbox(mb) {
|
|
t.Error("IsMailbox false for escaped local part")
|
|
}
|
|
if got := EmailFromMailbox(mb); got != "first.last@example.com" {
|
|
t.Errorf("EmailFromMailbox = %q", got)
|
|
}
|
|
if mb, _ := MailboxFromEmail("hostmaster.example.com."); mb != "hostmaster.example.com." {
|
|
t.Errorf("domain form changed: %q", mb)
|
|
}
|
|
if IsMailbox("hostmaster.bad_domain.com.") {
|
|
t.Error("bad domain part accepted")
|
|
}
|
|
}
|
|
|
|
func TestReverse(t *testing.T) {
|
|
a := netip.MustParseAddr("192.0.2.10")
|
|
if got := ReverseName(a); got != "10.2.0.192.in-addr.arpa." {
|
|
t.Errorf("ReverseName v4 = %q", got)
|
|
}
|
|
a6 := netip.MustParseAddr("2001:db8::1")
|
|
r6 := ReverseName(a6)
|
|
if r6 != "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa." {
|
|
t.Errorf("ReverseName v6 = %q", r6)
|
|
}
|
|
for _, name := range []string{"10.2.0.192.in-addr.arpa.", r6} {
|
|
back, ok := AddrFromReverse(name)
|
|
if !ok {
|
|
t.Errorf("AddrFromReverse(%q) failed", name)
|
|
continue
|
|
}
|
|
if ReverseName(back) != name {
|
|
t.Errorf("round trip %q -> %v", name, back)
|
|
}
|
|
}
|
|
z, err := ReverseZone(netip.MustParsePrefix("192.168.1.0/24"))
|
|
if err != nil || z != "1.168.192.in-addr.arpa." {
|
|
t.Errorf("ReverseZone /24 = %q %v", z, err)
|
|
}
|
|
z, err = ReverseZone(netip.MustParsePrefix("10.0.0.0/8"))
|
|
if err != nil || z != "10.in-addr.arpa." {
|
|
t.Errorf("ReverseZone /8 = %q %v", z, err)
|
|
}
|
|
if _, err := ReverseZone(netip.MustParsePrefix("192.168.1.0/26")); err == nil {
|
|
t.Error("ReverseZone /26 accepted")
|
|
}
|
|
z, err = ReverseZone(netip.MustParsePrefix("2001:db8::/32"))
|
|
if err != nil || z != "8.b.d.0.1.0.0.2.ip6.arpa." {
|
|
t.Errorf("ReverseZone v6 = %q %v", z, err)
|
|
}
|
|
if !IsReverse("1.168.192.in-addr.arpa.") || IsReverse("example.com.") {
|
|
t.Error("IsReverse")
|
|
}
|
|
}
|
|
|
|
func TestMisc(t *testing.T) {
|
|
if Parent("www.example.com.") != "example.com." || Parent("com.") != "." || Parent(".") != "" {
|
|
t.Error("Parent")
|
|
}
|
|
if CountLabels(".") != 0 || CountLabels("a.b.c.") != 3 {
|
|
t.Error("CountLabels")
|
|
}
|
|
if BestZone("a.lab.example.com.", []string{"example.com.", "lab.example.com.", "other.com."}) != "lab.example.com." {
|
|
t.Error("BestZone")
|
|
}
|
|
if FileSafe("0/26.2.0.192.in-addr.arpa.") != "0%2f26.2.0.192.in-addr.arpa" || FileSafe(".") != "root" {
|
|
t.Errorf("FileSafe = %q", FileSafe("0/26.2.0.192.in-addr.arpa."))
|
|
}
|
|
if !IsSubdomain("a.example.com.", "example.com.") || IsSubdomain("aexample.com.", "example.com.") || !IsSubdomain("x.", ".") {
|
|
t.Error("IsSubdomain")
|
|
}
|
|
}
|
|
|
|
func TestOwnerAddressInReverseZone(t *testing.T) {
|
|
for _, tc := range []struct{ in, zone, want string }{
|
|
{"10.10.0.20", "0.10.10.in-addr.arpa.", "20.0.10.10.in-addr.arpa."},
|
|
{"20", "0.10.10.in-addr.arpa.", "20.0.10.10.in-addr.arpa."},
|
|
{"2001:db8::1", "8.b.d.0.1.0.0.2.ip6.arpa.", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa."},
|
|
{"10.10.0.20", "adbraun.lol.", "10.10.0.20.adbraun.lol."}, // not special in forward zones
|
|
} {
|
|
if got, err := Owner(tc.in, tc.zone); err != nil || got != tc.want {
|
|
t.Errorf("Owner(%q, %q) = %q, %v; want %q", tc.in, tc.zone, got, err, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReverseZones(t *testing.T) {
|
|
cases := []struct {
|
|
prefix string
|
|
want string // zones joined by spaces, or "error"
|
|
}{
|
|
{"10.0.10.0/24", "10.0.10.in-addr.arpa."},
|
|
{"10.0.10.100/32", "100.10.0.10.in-addr.arpa."},
|
|
{"10.0.8.0/22", "8.0.10.in-addr.arpa. 9.0.10.in-addr.arpa. 10.0.10.in-addr.arpa. 11.0.10.in-addr.arpa."},
|
|
{"10.0.9.0/22", "8.0.10.in-addr.arpa. 9.0.10.in-addr.arpa. 10.0.10.in-addr.arpa. 11.0.10.in-addr.arpa."},
|
|
{"172.16.0.0/12", "16.172.in-addr.arpa. 17.172.in-addr.arpa. 18.172.in-addr.arpa. 19.172.in-addr.arpa. 20.172.in-addr.arpa. 21.172.in-addr.arpa. 22.172.in-addr.arpa. 23.172.in-addr.arpa. 24.172.in-addr.arpa. 25.172.in-addr.arpa. 26.172.in-addr.arpa. 27.172.in-addr.arpa. 28.172.in-addr.arpa. 29.172.in-addr.arpa. 30.172.in-addr.arpa. 31.172.in-addr.arpa."},
|
|
{"10.0.0.0/17", "error"}, // 128 zones
|
|
{"10.0.10.64/26", "error"},
|
|
{"10.0.0.0/7", "error"},
|
|
{"2001:db8::/47", "0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. 1.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa."},
|
|
{"2001:db8::1/128", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa."},
|
|
}
|
|
for _, c := range cases {
|
|
zones, err := ReverseZones(netip.MustParsePrefix(c.prefix))
|
|
got := strings.Join(zones, " ")
|
|
if err != nil {
|
|
got = "error"
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("ReverseZones(%s) = %s (%v), want %s", c.prefix, got, err, c.want)
|
|
}
|
|
}
|
|
if _, err := ReverseZone(netip.MustParsePrefix("10.0.8.0/22")); err == nil || !strings.Contains(err.Error(), "spans 4 reverse zones") {
|
|
t.Errorf("ReverseZone /22: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPrefixFromReverse(t *testing.T) {
|
|
for zone, want := range map[string]string{
|
|
"10.0.10.in-addr.arpa.": "10.0.10.0/24",
|
|
"10.in-addr.arpa": "10.0.0.0/8",
|
|
"154.144.144.139.in-addr.arpa.": "139.144.144.154/32",
|
|
"d.3.f.7.3.1.0.8.2.0.a.2.ip6.arpa.": "2a02:8013:7f3d::/48",
|
|
"b.a.b.a.b.a.b.a.b.a.d.f.ip6.arpa.": "fdab:abab:abab::/48",
|
|
"0-63.1.0.10.in-addr.arpa.": "",
|
|
"example.com.": "",
|
|
"256.10.in-addr.arpa.": "",
|
|
} {
|
|
p, ok := PrefixFromReverse(zone)
|
|
got := ""
|
|
if ok {
|
|
got = p.String()
|
|
}
|
|
if got != want {
|
|
t.Errorf("PrefixFromReverse(%q) = %q, want %q", zone, got, want)
|
|
}
|
|
}
|
|
// Round trip with ReverseZone.
|
|
for _, s := range []string{"192.168.1.0/24", "2a02:8013:7f3d::/48", "fd00::/8", "10.0.0.5/32"} {
|
|
z, err := ReverseZone(netip.MustParsePrefix(s))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p, ok := PrefixFromReverse(z); !ok || p.String() != s {
|
|
t.Errorf("%s -> %s -> %v", s, z, p)
|
|
}
|
|
}
|
|
}
|