dnscontrol/pkg/prettyzone/prettyzone_test.go
Phil Pennock 3c41a39252
BIND: Implement AutoDNSSEC (#648)
There's a philosophy issue here around what is the Bind output meant to
do.  Since AFAIK we're not integrating into Bind's catalog zones or the
like, we're just targeting the zonefiles, we're not in a position to do
_anything_ relating to registrar options such as setting up DS glue.

So at one level, enabling AutoDNSSEC for Bind is a lie. But without
this, folks can't target a Bind zone as a secondary provider for their
domain, to get debug dumps of the zone output, because the checks for
"Can" block it.  So I think this commit achieves a happy compromise: we
write a comment into the Bind zonefile, indicating that DNSSEC was
requested.

Actually: we add support for arbitrary zone comments to be written into
a zonefile via a slightly ugly "can be `nil`" parameter.  We then write
in a generation timestamp comment, and if AutoDNSSEC was requested we
then write that in too.
2020-02-22 13:27:24 -05:00

531 lines
15 KiB
Go

package prettyzone
import (
"bytes"
"fmt"
"log"
"math/rand"
"testing"
"github.com/StackExchange/dnscontrol/v2/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", 99)
// 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", 99)
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", 99))
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", 99))
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", 99))
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", 99)
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", 99)
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", 99)
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", 99)
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", 99)
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", 99)
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", 99)
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", 99)
recs = append(recs, rsynm)
recs = append(recs, rsynm)
recs = append(recs, rsynz)
buf := &bytes.Buffer{}
WriteZoneFileRC(buf, recs, "bosun.org", []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 type= zone_id=
;myalias IN R53_ALIAS type= zone_id=
www IN CNAME bosun.org.
;zalias IN R53_ALIAS type= 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", 99)
// 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", 99)
// 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)
}
}
}