mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-11-10 09:12:47 +08:00
531 lines
15 KiB
Go
531 lines
15 KiB
Go
package prettyzone
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"testing"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
"github.com/miekg/dns"
|
|
"github.com/miekg/dns/dnsutil"
|
|
)
|
|
|
|
func parseAndRegen(t *testing.T, buf *bytes.Buffer, expected string) {
|
|
// Take a zonefile, parse it, then generate a zone. We should
|
|
// get back the same string.
|
|
// This is used after any WriteZoneFile test as an extra verification step.
|
|
|
|
// Parse the output:
|
|
var parsed []dns.RR
|
|
for x := range dns.ParseZone(buf, "bosun.org", "bosun.org.zone") {
|
|
if x.Error != nil {
|
|
log.Fatalf("Error in zonefile: %v", x.Error)
|
|
} else {
|
|
parsed = append(parsed, x.RR)
|
|
}
|
|
}
|
|
// Generate it back:
|
|
buf2 := &bytes.Buffer{}
|
|
WriteZoneFileRR(buf2, parsed, "bosun.org")
|
|
|
|
// Compare:
|
|
if buf2.String() != expected {
|
|
t.Fatalf("Regenerated zonefile does not match: got=(\n%v\n)\nexpected=(\n%v\n)\n", buf2.String(), expected)
|
|
}
|
|
}
|
|
|
|
func TestMostCommonTtl(t *testing.T) {
|
|
var records []dns.RR
|
|
var g, e uint32
|
|
r1, _ := dns.NewRR("bosun.org. 100 IN A 1.1.1.1")
|
|
r2, _ := dns.NewRR("bosun.org. 200 IN A 1.1.1.1")
|
|
r3, _ := dns.NewRR("bosun.org. 300 IN A 1.1.1.1")
|
|
r4, _ := dns.NewRR("bosun.org. 400 IN NS foo.bosun.org.")
|
|
r5, _ := dns.NewRR("bosun.org. 400 IN NS bar.bosun.org.")
|
|
|
|
// All records are TTL=100
|
|
records = nil
|
|
records, e = append(records, r1, r1, r1), 100
|
|
x := models.RRstoRCs(records, "bosun.org")
|
|
g = MostCommonTTL(x)
|
|
if e != g {
|
|
t.Fatalf("expected %d; got %d\n", e, g)
|
|
}
|
|
|
|
// Mixture of TTLs with an obvious winner.
|
|
records = nil
|
|
records, e = append(records, r1, r2, r2), 200
|
|
g = MostCommonTTL(models.RRstoRCs(records, "bosun.org"))
|
|
if e != g {
|
|
t.Fatalf("expected %d; got %d\n", e, g)
|
|
}
|
|
|
|
// 3-way tie. Largest TTL should be used.
|
|
records = nil
|
|
records, e = append(records, r1, r2, r3), 300
|
|
g = MostCommonTTL(models.RRstoRCs(records, "bosun.org"))
|
|
if e != g {
|
|
t.Fatalf("expected %d; got %d\n", e, g)
|
|
}
|
|
|
|
// NS records are ignored.
|
|
records = nil
|
|
records, e = append(records, r1, r4, r5), 100
|
|
g = MostCommonTTL(models.RRstoRCs(records, "bosun.org"))
|
|
if e != g {
|
|
t.Fatalf("expected %d; got %d\n", e, g)
|
|
}
|
|
|
|
}
|
|
|
|
// func WriteZoneFile
|
|
|
|
func TestWriteZoneFileSimple(t *testing.T) {
|
|
r1, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.153")
|
|
r2, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.154")
|
|
r3, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
|
|
buf := &bytes.Buffer{}
|
|
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org")
|
|
expected := `$TTL 300
|
|
@ IN A 192.30.252.153
|
|
IN A 192.30.252.154
|
|
www IN CNAME bosun.org.
|
|
`
|
|
if buf.String() != expected {
|
|
t.Log(buf.String())
|
|
t.Log(expected)
|
|
t.Fatalf("Zone file does not match.")
|
|
}
|
|
|
|
parseAndRegen(t, buf, expected)
|
|
}
|
|
|
|
func TestWriteZoneFileSimpleTtl(t *testing.T) {
|
|
r1, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.153")
|
|
r2, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.154")
|
|
r3, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.155")
|
|
r4, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
|
|
buf := &bytes.Buffer{}
|
|
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4}, "bosun.org")
|
|
expected := `$TTL 100
|
|
@ IN A 192.30.252.153
|
|
IN A 192.30.252.154
|
|
IN A 192.30.252.155
|
|
www 300 IN CNAME bosun.org.
|
|
`
|
|
if buf.String() != expected {
|
|
t.Log(buf.String())
|
|
t.Log(expected)
|
|
t.Fatalf("Zone file does not match")
|
|
}
|
|
|
|
parseAndRegen(t, buf, expected)
|
|
}
|
|
|
|
func TestWriteZoneFileMx(t *testing.T) {
|
|
// sort by priority
|
|
r1, _ := dns.NewRR("aaa.bosun.org. IN MX 1 aaa.example.com.")
|
|
r2, _ := dns.NewRR("aaa.bosun.org. IN MX 5 aaa.example.com.")
|
|
r3, _ := dns.NewRR("aaa.bosun.org. IN MX 10 aaa.example.com.")
|
|
// same priority? sort by name
|
|
r4, _ := dns.NewRR("bbb.bosun.org. IN MX 10 ccc.example.com.")
|
|
r5, _ := dns.NewRR("bbb.bosun.org. IN MX 10 bbb.example.com.")
|
|
r6, _ := dns.NewRR("bbb.bosun.org. IN MX 10 aaa.example.com.")
|
|
// a mix
|
|
r7, _ := dns.NewRR("ccc.bosun.org. IN MX 40 zzz.example.com.")
|
|
r8, _ := dns.NewRR("ccc.bosun.org. IN MX 40 aaa.example.com.")
|
|
r9, _ := dns.NewRR("ccc.bosun.org. IN MX 1 ttt.example.com.")
|
|
buf := &bytes.Buffer{}
|
|
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6, r7, r8, r9}, "bosun.org")
|
|
if buf.String() != testdataZFMX {
|
|
t.Log(buf.String())
|
|
t.Log(testdataZFMX)
|
|
t.Fatalf("Zone file does not match.")
|
|
}
|
|
parseAndRegen(t, buf, testdataZFMX)
|
|
}
|
|
|
|
var testdataZFMX = `$TTL 3600
|
|
aaa IN MX 1 aaa.example.com.
|
|
IN MX 5 aaa.example.com.
|
|
IN MX 10 aaa.example.com.
|
|
bbb IN MX 10 aaa.example.com.
|
|
IN MX 10 bbb.example.com.
|
|
IN MX 10 ccc.example.com.
|
|
ccc IN MX 1 ttt.example.com.
|
|
IN MX 40 aaa.example.com.
|
|
IN MX 40 zzz.example.com.
|
|
`
|
|
|
|
func TestWriteZoneFileSrv(t *testing.T) {
|
|
// exhibits explicit ttls and long name
|
|
r1, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 9999 foo.com.`)
|
|
r2, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 20 5050 foo.com.`)
|
|
r3, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 5050 foo.com.`)
|
|
r4, _ := dns.NewRR(`bosun.org. 300 IN SRV 20 10 5050 foo.com.`)
|
|
r5, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 5050 foo.com.`)
|
|
buf := &bytes.Buffer{}
|
|
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5}, "bosun.org")
|
|
if buf.String() != testdataZFSRV {
|
|
t.Log(buf.String())
|
|
t.Log(testdataZFSRV)
|
|
t.Fatalf("Zone file does not match.")
|
|
}
|
|
parseAndRegen(t, buf, testdataZFSRV)
|
|
}
|
|
|
|
var testdataZFSRV = `$TTL 300
|
|
@ IN SRV 10 10 5050 foo.com.
|
|
IN SRV 10 10 5050 foo.com.
|
|
IN SRV 10 20 5050 foo.com.
|
|
IN SRV 20 10 5050 foo.com.
|
|
IN SRV 10 10 9999 foo.com.
|
|
`
|
|
|
|
func TestWriteZoneFilePtr(t *testing.T) {
|
|
// exhibits explicit ttls and long name
|
|
r1, _ := dns.NewRR(`bosun.org. 300 IN PTR chell.bosun.org`)
|
|
r2, _ := dns.NewRR(`bosun.org. 300 IN PTR barney.bosun.org.`)
|
|
r3, _ := dns.NewRR(`bosun.org. 300 IN PTR alex.bosun.org.`)
|
|
buf := &bytes.Buffer{}
|
|
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org")
|
|
if buf.String() != testdataZFPTR {
|
|
t.Log(buf.String())
|
|
t.Log(testdataZFPTR)
|
|
t.Fatalf("Zone file does not match.")
|
|
}
|
|
parseAndRegen(t, buf, testdataZFPTR)
|
|
}
|
|
|
|
var testdataZFPTR = `$TTL 300
|
|
@ IN PTR alex.bosun.org.
|
|
IN PTR barney.bosun.org.
|
|
IN PTR chell.bosun.org.
|
|
`
|
|
|
|
func TestWriteZoneFileCaa(t *testing.T) {
|
|
// exhibits explicit ttls and long name
|
|
r1, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 issuewild ";"`)
|
|
r2, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 issue "letsencrypt.org"`)
|
|
r3, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "http://example.com"`)
|
|
r4, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.com"`)
|
|
r5, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.net"`)
|
|
r6, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "mailto:example.com"`)
|
|
buf := &bytes.Buffer{}
|
|
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6}, "bosun.org")
|
|
if buf.String() != testdataZFCAA {
|
|
t.Log(buf.String())
|
|
t.Log(testdataZFCAA)
|
|
t.Fatalf("Zone file does not match.")
|
|
}
|
|
parseAndRegen(t, buf, testdataZFCAA)
|
|
}
|
|
|
|
var testdataZFCAA = `$TTL 300
|
|
@ IN CAA 1 iodef "http://example.com"
|
|
IN CAA 1 iodef "mailto:example.com"
|
|
IN CAA 0 iodef "https://example.com"
|
|
IN CAA 0 iodef "https://example.net"
|
|
IN CAA 0 issue "letsencrypt.org"
|
|
IN CAA 0 issuewild ";"
|
|
`
|
|
|
|
// Test 1 of each record type
|
|
|
|
func mustNewRR(s string) dns.RR {
|
|
r, err := dns.NewRR(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func TestWriteZoneFileEach(t *testing.T) {
|
|
// Each rtype should be listed in this test exactly once.
|
|
// If an rtype has more than one variations, add a test like TestWriteZoneFileCaa to test each.
|
|
var d []dns.RR
|
|
// #rtype_variations
|
|
d = append(d, mustNewRR(`4.5 300 IN PTR y.bosun.org.`)) // Wouldn't actually be in this domain.
|
|
d = append(d, mustNewRR(`bosun.org. 300 IN A 1.2.3.4`))
|
|
d = append(d, mustNewRR(`bosun.org. 300 IN MX 1 bosun.org.`))
|
|
d = append(d, mustNewRR(`bosun.org. 300 IN TXT "my text"`))
|
|
d = append(d, mustNewRR(`bosun.org. 300 IN AAAA 4500:fe::1`))
|
|
d = append(d, mustNewRR(`bosun.org. 300 IN SRV 10 10 9999 foo.com.`))
|
|
d = append(d, mustNewRR(`bosun.org. 300 IN CAA 0 issue "letsencrypt.org"`))
|
|
d = append(d, mustNewRR(`_443._tcp.bosun.org. 300 IN TLSA 3 1 1 abcdef0`)) // Label must be _port._proto
|
|
d = append(d, mustNewRR(`sub.bosun.org. 300 IN NS bosun.org.`)) // Must be a label with no other records.
|
|
d = append(d, mustNewRR(`x.bosun.org. 300 IN CNAME bosun.org.`)) // Must be a label with no other records.
|
|
buf := &bytes.Buffer{}
|
|
WriteZoneFileRR(buf, d, "bosun.org")
|
|
if buf.String() != testdataZFEach {
|
|
t.Log(buf.String())
|
|
t.Log(testdataZFEach)
|
|
t.Fatalf("Zone file does not match.")
|
|
}
|
|
parseAndRegen(t, buf, testdataZFEach)
|
|
}
|
|
|
|
var testdataZFEach = `$TTL 300
|
|
@ IN A 1.2.3.4
|
|
IN AAAA 4500:fe::1
|
|
IN MX 1 bosun.org.
|
|
IN SRV 10 10 9999 foo.com.
|
|
IN TXT "my text"
|
|
IN CAA 0 issue "letsencrypt.org"
|
|
4.5 IN PTR y.bosun.org.
|
|
_443._tcp IN TLSA 3 1 1 abcdef0
|
|
sub IN NS bosun.org.
|
|
x IN CNAME bosun.org.
|
|
`
|
|
|
|
func TestWriteZoneFileSynth(t *testing.T) {
|
|
r1, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.153")
|
|
r2, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.154")
|
|
r3, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
|
|
rsynm := &models.RecordConfig{Type: "R53_ALIAS", TTL: 300}
|
|
rsynm.SetLabel("myalias", "bosun.org")
|
|
rsynz := &models.RecordConfig{Type: "R53_ALIAS", TTL: 300}
|
|
rsynz.SetLabel("zalias", "bosun.org")
|
|
|
|
recs := models.RRstoRCs([]dns.RR{r1, r2, r3}, "bosun.org")
|
|
recs = append(recs, rsynm)
|
|
recs = append(recs, rsynm)
|
|
recs = append(recs, rsynz)
|
|
|
|
buf := &bytes.Buffer{}
|
|
WriteZoneFileRC(buf, recs, "bosun.org", 0, []string{"c1", "c2", "c3\nc4"})
|
|
expected := `$TTL 300
|
|
; c1
|
|
; c2
|
|
; c3
|
|
; c4
|
|
@ IN A 192.30.252.153
|
|
IN A 192.30.252.154
|
|
;myalias IN R53_ALIAS atype= zone_id=
|
|
;myalias IN R53_ALIAS atype= zone_id=
|
|
www IN CNAME bosun.org.
|
|
;zalias IN R53_ALIAS atype= zone_id=
|
|
`
|
|
if buf.String() != expected {
|
|
t.Log(buf.String())
|
|
t.Log(expected)
|
|
t.Fatalf("Zone file does not match.")
|
|
}
|
|
}
|
|
|
|
// Test sorting
|
|
|
|
func TestWriteZoneFileOrder(t *testing.T) {
|
|
var records []dns.RR
|
|
for i, td := range []string{
|
|
"@",
|
|
"@",
|
|
"@",
|
|
"stackoverflow.com.",
|
|
"*",
|
|
"foo",
|
|
"bar.foo",
|
|
"hip.foo",
|
|
"mup",
|
|
"a.mup",
|
|
"bzt.mup",
|
|
"aaa.bzt.mup",
|
|
"zzz.bzt.mup",
|
|
"nnn.mup",
|
|
"zt.mup",
|
|
"zap",
|
|
} {
|
|
name := dnsutil.AddOrigin(td, "stackoverflow.com.")
|
|
r, _ := dns.NewRR(fmt.Sprintf("%s 300 IN A 1.2.3.%d", name, i))
|
|
records = append(records, r)
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
WriteZoneFileRR(buf, records, "stackoverflow.com")
|
|
// Compare
|
|
if buf.String() != testdataOrder {
|
|
t.Log("Found:")
|
|
t.Log(buf.String())
|
|
t.Log("Expected:")
|
|
t.Log(testdataOrder)
|
|
t.Fatalf("Zone file does not match.")
|
|
}
|
|
parseAndRegen(t, buf, testdataOrder)
|
|
|
|
// Now shuffle the list many times and make sure it still works:
|
|
for iteration := 5; iteration > 0; iteration-- {
|
|
// Randomize the list:
|
|
perm := rand.Perm(len(records))
|
|
for i, v := range perm {
|
|
records[i], records[v] = records[v], records[i]
|
|
}
|
|
// Generate
|
|
buf := &bytes.Buffer{}
|
|
WriteZoneFileRR(buf, records, "stackoverflow.com")
|
|
// Compare
|
|
if buf.String() != testdataOrder {
|
|
t.Log(buf.String())
|
|
t.Log(testdataOrder)
|
|
t.Fatalf("Zone file does not match.")
|
|
}
|
|
parseAndRegen(t, buf, testdataOrder)
|
|
}
|
|
}
|
|
|
|
var testdataOrder = `$TTL 300
|
|
@ IN A 1.2.3.0
|
|
IN A 1.2.3.1
|
|
IN A 1.2.3.2
|
|
IN A 1.2.3.3
|
|
* IN A 1.2.3.4
|
|
foo IN A 1.2.3.5
|
|
bar.foo IN A 1.2.3.6
|
|
hip.foo IN A 1.2.3.7
|
|
mup IN A 1.2.3.8
|
|
a.mup IN A 1.2.3.9
|
|
bzt.mup IN A 1.2.3.10
|
|
aaa.bzt.mup IN A 1.2.3.11
|
|
zzz.bzt.mup IN A 1.2.3.12
|
|
nnn.mup IN A 1.2.3.13
|
|
zt.mup IN A 1.2.3.14
|
|
zap IN A 1.2.3.15
|
|
`
|
|
|
|
// func formatLine
|
|
|
|
func TestFormatLine(t *testing.T) {
|
|
tests := []struct {
|
|
lengths []int
|
|
fields []string
|
|
expected string
|
|
}{
|
|
{[]int{2, 2, 0}, []string{"a", "b", "c"}, "a b c"},
|
|
{[]int{2, 2, 0}, []string{"aaaaa", "b", "c"}, "aaaaa b c"},
|
|
}
|
|
for _, ts := range tests {
|
|
actual := formatLine(ts.lengths, ts.fields)
|
|
if actual != ts.expected {
|
|
t.Errorf("\"%s\" != \"%s\"", actual, ts.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
// func zoneLabelLess
|
|
|
|
func TestZoneLabelLess(t *testing.T) {
|
|
/*
|
|
The zone should sort in prefix traversal order:
|
|
|
|
@
|
|
*
|
|
foo
|
|
bar.foo
|
|
hip.foo
|
|
mup
|
|
a.mup
|
|
bzt.mup
|
|
*.bzt.mup
|
|
1.bzt.mup
|
|
2.bzt.mup
|
|
10.bzt.mup
|
|
aaa.bzt.mup
|
|
zzz.bzt.mup
|
|
nnn.mup
|
|
zt.mup
|
|
zap
|
|
*/
|
|
|
|
var tests = []struct {
|
|
e1, e2 string
|
|
expected bool
|
|
}{
|
|
{"@", "@", false},
|
|
{"@", "*", true},
|
|
{"@", "b", true},
|
|
{"*", "@", false},
|
|
{"*", "*", false},
|
|
{"*", "b", true},
|
|
{"foo", "foo", false},
|
|
{"foo", "bar", false},
|
|
{"bar", "foo", true},
|
|
{"a.mup", "mup", false},
|
|
{"mup", "a.mup", true},
|
|
{"a.mup", "a.mup", false},
|
|
{"a.mup", "bzt.mup", true},
|
|
{"a.mup", "aa.mup", true},
|
|
{"zt.mup", "aaa.bzt.mup", false},
|
|
{"aaa.bzt.mup", "mup", false},
|
|
{"*.bzt.mup", "aaa.bzt.mup", true},
|
|
{"1.bzt.mup", "aaa.bzt.mup", true},
|
|
{"1.bzt.mup", "2.bzt.mup", true},
|
|
{"10.bzt.mup", "2.bzt.mup", false},
|
|
{"nnn.mup", "aaa.bzt.mup", false},
|
|
{`www\.miek.nl`, `www.miek.nl`, false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
actual := zoneLabelLess(test.e1, test.e2)
|
|
if test.expected != actual {
|
|
t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
|
|
}
|
|
actual = zoneLabelLess(test.e2, test.e1)
|
|
// The reverse should work too:
|
|
var expected bool
|
|
if test.e1 == test.e2 {
|
|
expected = false
|
|
} else {
|
|
expected = !test.expected
|
|
}
|
|
if expected != actual {
|
|
t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestZoneRrtypeLess(t *testing.T) {
|
|
/*
|
|
In zonefiles we want to list SOAs, then NSs, then all others.
|
|
*/
|
|
|
|
var tests = []struct {
|
|
e1, e2 string
|
|
expected bool
|
|
}{
|
|
{"SOA", "SOA", false},
|
|
{"SOA", "A", true},
|
|
{"SOA", "TXT", true},
|
|
{"SOA", "NS", true},
|
|
{"NS", "SOA", false},
|
|
{"NS", "A", true},
|
|
{"NS", "TXT", true},
|
|
{"NS", "NS", false},
|
|
{"A", "SOA", false},
|
|
{"A", "A", false},
|
|
{"A", "TXT", true},
|
|
{"A", "NS", false},
|
|
{"MX", "SOA", false},
|
|
{"MX", "A", false},
|
|
{"MX", "TXT", true},
|
|
{"MX", "NS", false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
actual := zoneRrtypeLess(test.e1, test.e2)
|
|
if test.expected != actual {
|
|
t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
|
|
}
|
|
actual = zoneRrtypeLess(test.e2, test.e1)
|
|
// The reverse should work too:
|
|
var expected bool
|
|
if test.e1 == test.e2 {
|
|
expected = false
|
|
} else {
|
|
expected = !test.expected
|
|
}
|
|
if expected != actual {
|
|
t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
|
|
}
|
|
}
|
|
}
|