1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00
Files
stackexchange-dnscontrol/pkg/normalize/validate_test.go
Costas Drogos c8a5060dfb Emit warning in case of label having multiple TTLs (#1489)
* Emit warning in case of label having multiple TTLs

An RRSet (=label) consisting of multiple records with different TTLs is
something not supported by most providers, and should be avoided.
Furthermore it is deprecated in rfc2181#section-5.2

Emit a warning for now during validation, eventually turning it into a full-blown error.

Fixes #1372

* normalize: less verbose checkLabelHasMultipleTTLs

Code would previously emit a warning for each record it found matching a
previously found label but with a different ttl. This could potentially become
too verbose of an output for larger zones.

Split the loop into two loops, one storing labels and their records' TTLs, the
second checking for multiple TTLs, in order to minimize the messages logged to
one message per problematic label, regardless for the number of records involved.

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
2022-05-04 08:41:16 -04:00

496 lines
15 KiB
Go

package normalize
import (
"fmt"
"reflect"
"testing"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/providers"
)
func TestSoaLabelAndTarget(t *testing.T) {
var tests = []struct {
isError bool
label string
target string
}{
{false, "@", "ns1.foo.com."},
// Invalid target
{true, "@", "ns1.foo.com"},
// Invalid label, only '@' is allowed for SOA records
{true, "foo.com", "ns1.foo.com."},
}
for _, test := range tests {
experiment := fmt.Sprintf("%s %s", test.label, test.target)
rc := makeRC(test.label, "foo.com", test.target, models.RecordConfig{Type: "SOA",
SoaExpire: 1, SoaMinttl: 1, SoaRefresh: 1, SoaRetry: 1, SoaSerial: 1, SoaMbox: "bar.foo.com"})
err := checkTargets(rc, "foo.com")
if err != nil && !test.isError {
t.Errorf("%v: Error (%v)\n", experiment, err)
}
if err == nil && test.isError {
t.Errorf("%v: Expected error but got none \n", experiment)
}
}
}
func TestCheckSoa(t *testing.T) {
var tests = []struct {
isError bool
expire uint32
minttl uint32
refresh uint32
retry uint32
serial uint32
mbox string
}{
// Expire
{false, 123, 123, 123, 123, 123, "foo.bar.com."},
{true, 0, 123, 123, 123, 123, "foo.bar.com."},
// MinTTL
{false, 123, 123, 123, 123, 123, "foo.bar.com."},
{true, 123, 0, 123, 123, 123, "foo.bar.com."},
// Refresh
{false, 123, 123, 123, 123, 123, "foo.bar.com."},
{true, 123, 123, 0, 123, 123, "foo.bar.com."},
// Retry
{false, 123, 123, 123, 123, 123, "foo.bar.com."},
{true, 123, 123, 123, 0, 123, "foo.bar.com."},
// Serial
{false, 123, 123, 123, 123, 123, "foo.bar.com."},
{false, 123, 123, 123, 123, 0, "foo.bar.com."},
// MBox
{true, 123, 123, 123, 123, 123, ""},
{true, 123, 123, 123, 123, 123, "foo@bar.com."},
{false, 123, 123, 123, 123, 123, "foo.bar.com."},
}
for _, test := range tests {
experiment := fmt.Sprintf("%d %d %d %d %d %s", test.expire, test.minttl, test.refresh,
test.retry, test.serial, test.mbox)
t.Run(experiment, func(t *testing.T) {
err := checkSoa(test.expire, test.minttl, test.refresh, test.retry, test.serial, test.mbox)
checkError(t, err, test.isError, experiment)
})
}
}
func TestCheckLabel(t *testing.T) {
var tests = []struct {
label string
rType string
target string
isError bool
hasSkipMeta bool
}{
{"@", "A", "zap", false, false},
{"foo.bar", "A", "zap", false, false},
{"_foo", "A", "zap", false, false},
{"_foo", "SRV", "zap", false, false},
{"_foo", "TLSA", "zap", false, false},
{"_foo", "TXT", "zap", false, false},
{"_y2", "CNAME", "foo", false, false},
{"s1._domainkey", "CNAME", "foo", false, false},
{"_y3", "CNAME", "asfljds.acm-validations.aws.", false, false},
{"test.foo.tld", "A", "zap", true, false},
{"test.foo.tld", "A", "zap", false, true},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%s %s", test.label, test.rType), func(t *testing.T) {
meta := map[string]string{}
if test.hasSkipMeta {
meta["skip_fqdn_check"] = "true"
}
err := checkLabel(test.label, test.rType, test.target, "foo.tld", meta)
if err != nil && !test.isError {
t.Errorf("%02d: Expected no error but got %s", i, err)
}
if err == nil && test.isError {
t.Errorf("%02d: Expected error but got none", i)
}
})
}
}
func checkError(t *testing.T, err error, shouldError bool, experiment string) {
if err != nil && !shouldError {
t.Errorf("%v: Error (%v)\n", experiment, err)
}
if err == nil && shouldError {
t.Errorf("%v: Expected error but got none \n", experiment)
}
}
func Test_assert_valid_ipv4(t *testing.T) {
var tests = []struct {
experiment string
isError bool
}{
{"1.2.3.4", false},
{"1.2.3.4/10", true},
{"1.2.3", true},
{"foo", true},
}
for _, test := range tests {
err := checkIPv4(test.experiment)
checkError(t, err, test.isError, test.experiment)
}
}
func Test_assert_valid_target(t *testing.T) {
var tests = []struct {
experiment string
isError bool
}{
{"@", false},
{"foo", false},
{"foo.bar.", false},
{"foo.", false},
{"foo.bar", true},
{"foo&bar", true},
{"foo bar", true},
{"elb21.freshdesk.com/", true},
{"elb21.freshdesk.com/.", true},
}
for _, test := range tests {
err := checkTarget(test.experiment)
checkError(t, err, test.isError, test.experiment)
}
}
func Test_transform_cname(t *testing.T) {
var tests = []struct {
experiment string
expected string
}{
{"@", "old.com.new.com."},
{"foo", "foo.old.com.new.com."},
{"foo.bar", "foo.bar.old.com.new.com."},
{"foo.bar.", "foo.bar.new.com."},
{"chat.stackexchange.com.", "chat.stackexchange.com.new.com."},
}
for _, test := range tests {
actual := transformCNAME(test.experiment, "old.com", "new.com")
if test.expected != actual {
t.Errorf("%v: expected (%v) got (%v)\n", test.experiment, test.expected, actual)
}
}
}
func TestNSAtRoot(t *testing.T) {
// do not allow ns records for @
rec := &models.RecordConfig{Type: "NS"}
rec.SetLabel("test", "foo.com")
rec.SetTarget("ns1.name.com.")
errs := checkTargets(rec, "foo.com")
if len(errs) > 0 {
t.Error("Expect no error with ns record on subdomain")
}
rec.SetLabel("@", "foo.com")
errs = checkTargets(rec, "foo.com")
if len(errs) != 1 {
t.Error("Expect error with ns record on @")
}
}
func TestTransforms(t *testing.T) {
var tests = []struct {
givenIP string
expectedRecords []string
}{
{"0.0.5.5", []string{"2.0.5.5"}},
{"3.0.5.5", []string{"5.5.5.5"}},
{"7.0.5.5", []string{"9.9.9.9", "10.10.10.10"}},
}
const transform = "0.0.0.0~1.0.0.0~2.0.0.0~; 3.0.0.0~4.0.0.0~~5.5.5.5; 7.0.0.0~8.0.0.0~~9.9.9.9,10.10.10.10"
for i, test := range tests {
dc := &models.DomainConfig{
Records: []*models.RecordConfig{
makeRC("f", "example.tld", test.givenIP, models.RecordConfig{Type: "A", Metadata: map[string]string{"transform": transform}}),
},
}
err := applyRecordTransforms(dc)
if err != nil {
t.Errorf("error on test %d: %s", i, err)
continue
}
if len(dc.Records) != len(test.expectedRecords) {
t.Errorf("test %d: expect %d records but found %d", i, len(test.expectedRecords), len(dc.Records))
continue
}
for r, rec := range dc.Records {
if rec.GetTargetField() != test.expectedRecords[r] {
t.Errorf("test %d at index %d: records don't match. Expect %s but found %s.", i, r, test.expectedRecords[r], rec.GetTargetField())
continue
}
}
}
}
func TestCNAMEMutex(t *testing.T) {
var recA = &models.RecordConfig{Type: "CNAME"}
recA.SetLabel("foo", "foo.example.com")
recA.SetTarget("example.com.")
tests := []struct {
rType string
name string
fail bool
}{
{"A", "foo", true},
{"A", "foo2", false},
{"CNAME", "foo", true},
{"CNAME", "foo2", false},
}
for _, tst := range tests {
t.Run(fmt.Sprintf("%s %s", tst.rType, tst.name), func(t *testing.T) {
var recB = &models.RecordConfig{Type: tst.rType}
recB.SetLabel(tst.name, "example.com")
recB.SetTarget("example2.com.")
dc := &models.DomainConfig{
Name: "example.com",
Records: []*models.RecordConfig{recA, recB},
}
errs := checkCNAMEs(dc)
if errs != nil && !tst.fail {
t.Error("Got error but expected none")
}
if errs == nil && tst.fail {
t.Error("Expected error but got none")
}
})
}
}
func TestCAAValidation(t *testing.T) {
config := &models.DNSConfig{
Domains: []*models.DomainConfig{
{
Name: "example.com",
RegistrarName: "BIND",
Records: []*models.RecordConfig{
makeRC("@", "example.com", "example.com", models.RecordConfig{Type: "CAA", CaaTag: "invalid"}),
},
},
},
}
errs := ValidateAndNormalizeConfig(config)
if len(errs) != 1 {
t.Error("Expect error on invalid CAA but got none")
}
}
func TestCheckDuplicates(t *testing.T) {
records := []*models.RecordConfig{
// The only difference is the target:
makeRC("www", "example.com", "4.4.4.4", models.RecordConfig{Type: "A"}),
makeRC("www", "example.com", "5.5.5.5", models.RecordConfig{Type: "A"}),
// The only difference is the rType:
makeRC("aaa", "example.com", "uniquestring.com.", models.RecordConfig{Type: "NS"}),
makeRC("aaa", "example.com", "uniquestring.com.", models.RecordConfig{Type: "PTR"}),
// The only difference is the TTL.
makeRC("zzz", "example.com", "4.4.4.4", models.RecordConfig{Type: "A", TTL: 111}),
makeRC("zzz", "example.com", "4.4.4.4", models.RecordConfig{Type: "A", TTL: 222}),
// Three records each with a different target.
makeRC("@", "example.com", "ns1.foo.com.", models.RecordConfig{Type: "NS"}),
makeRC("@", "example.com", "ns2.foo.com.", models.RecordConfig{Type: "NS"}),
makeRC("@", "example.com", "ns3.foo.com.", models.RecordConfig{Type: "NS"}),
}
errs := checkDuplicates(records)
if len(errs) != 0 {
t.Errorf("Expect duplicate NOT found but found %q", errs)
}
}
func TestCheckDuplicates_dup_a(t *testing.T) {
records := []*models.RecordConfig{
// A records that are exact dupliates.
makeRC("@", "example.com", "1.1.1.1", models.RecordConfig{Type: "A"}),
makeRC("@", "example.com", "1.1.1.1", models.RecordConfig{Type: "A"}),
}
errs := checkDuplicates(records)
if len(errs) == 0 {
t.Error("Expect duplicate found but found none")
}
}
func TestCheckDuplicates_dup_ns(t *testing.T) {
records := []*models.RecordConfig{
// Three records, the last 2 are duplicates.
// NB: This is a common issue.
makeRC("@", "example.com", "ns1.foo.com.", models.RecordConfig{Type: "NS"}),
makeRC("@", "example.com", "ns2.foo.com.", models.RecordConfig{Type: "NS"}),
makeRC("@", "example.com", "ns2.foo.com.", models.RecordConfig{Type: "NS"}),
}
errs := checkDuplicates(records)
if len(errs) == 0 {
t.Error("Expect duplicate found but found none")
}
}
func TestUniq(t *testing.T) {
a := []uint32 {1, 2, 2, 3, 4, 5, 5, 6}
expected := []uint32 {1, 2, 3, 4, 5, 6}
r := uniq(a)
if !reflect.DeepEqual(r, expected) {
t.Error("Deduplicated slice is different than expected")
}
}
func TestCheckLabelHasMultipleTTLs(t *testing.T) {
records := []*models.RecordConfig{
// different ttl per record
makeRC("zzz", "example.com", "4.4.4.4", models.RecordConfig{Type: "A", TTL: 111}),
makeRC("zzz", "example.com", "4.4.4.5", models.RecordConfig{Type: "A", TTL: 222}),
}
errs := checkLabelHasMultipleTTLs(records)
if len(errs) == 0 {
t.Error("Expected error on multiple TTLs under the same label, but got none")
}
}
func TestCheckLabelHasNoMultipleTTLs(t *testing.T) {
records := []*models.RecordConfig{
// different ttl per record
makeRC("zzz", "example.com", "4.4.4.4", models.RecordConfig{Type: "A", TTL: 111}),
makeRC("zzz", "example.com", "4.4.4.5", models.RecordConfig{Type: "A", TTL: 111}),
}
errs := checkLabelHasMultipleTTLs(records)
if len(errs) != 0 {
t.Errorf("Expected 0 errors on records having the same TTL under the same label, but got %d", len(errs))
}
}
func TestTLSAValidation(t *testing.T) {
config := &models.DNSConfig{
Domains: []*models.DomainConfig{
{
Name: "_443._tcp.example.com",
RegistrarName: "BIND",
Records: []*models.RecordConfig{
makeRC("_443._tcp", "_443._tcp.example.com", "abcdef0", models.RecordConfig{
Type: "TLSA", TlsaUsage: 4, TlsaSelector: 1, TlsaMatchingType: 1}),
},
},
},
}
errs := ValidateAndNormalizeConfig(config)
if len(errs) != 1 {
t.Error("Expect error on invalid TLSA but got none")
}
}
const (
ProviderNoDS = "NO_DS_SUPPORT"
ProviderFullDS = "FULL_DS_SUPPORT"
ProviderChildDSOnly = "CHILD_DS_SUPPORT"
ProviderBothDSCaps = "BOTH_DS_CAPABILITIES"
)
func init() {
providers.RegisterDomainServiceProviderType(ProviderNoDS, providers.DspFuncs{}, providers.DocumentationNotes{})
providers.RegisterDomainServiceProviderType(ProviderFullDS, providers.DspFuncs{}, providers.DocumentationNotes{
providers.CanUseDS: providers.Can(),
})
providers.RegisterDomainServiceProviderType(ProviderChildDSOnly, providers.DspFuncs{}, providers.DocumentationNotes{
providers.CanUseDSForChildren: providers.Can(),
})
providers.RegisterDomainServiceProviderType(ProviderBothDSCaps, providers.DspFuncs{}, providers.DocumentationNotes{
providers.CanUseDS: providers.Can(),
providers.CanUseDSForChildren: providers.Can(),
})
}
func Test_DSChecks(t *testing.T) {
t.Run("no DS support", func(t *testing.T) {
err := checkProviderDS(ProviderNoDS, nil)
if err == nil {
t.Errorf("Provider %s implements no DS capabilities, so should have failed the check", ProviderNoDS)
}
})
t.Run("full DS support", func(t *testing.T) {
apexDS := models.RecordConfig{Type: "DS"}
apexDS.SetLabel("@", "example.com")
childDS := models.RecordConfig{Type: "DS"}
childDS.SetLabel("child", "example.com")
records := models.Records{&apexDS, &childDS}
// check permutations of ProviderCanDS and having both DS caps
for _, pType := range []string{ProviderFullDS, ProviderBothDSCaps} {
err := checkProviderDS(pType, records)
if err != nil {
t.Errorf("Provider %s implements full DS capabilities and should process the provided records", ProviderFullDS)
}
}
})
t.Run("child DS support only", func(t *testing.T) {
apexDS := models.RecordConfig{Type: "DS"}
apexDS.SetLabel("@", "example.com")
childDS := models.RecordConfig{Type: "DS"}
childDS.SetLabel("child", "example.com")
// this record is included at the apex to check the Type of the
// recordset is verified to only inspect records with type == DS
apexA := models.RecordConfig{Type: "A"}
apexA.SetLabel("@", "example.com")
t.Run("accepts when child DS records only", func(t *testing.T) {
records := models.Records{&childDS, &apexA}
err := checkProviderDS(ProviderChildDSOnly, records)
if err != nil {
t.Errorf("Provider %s implements child DS support so the provided records should be accepted",
ProviderChildDSOnly,
)
}
})
t.Run("fails with apex and child DS records", func(t *testing.T) {
records := models.Records{&apexDS, &childDS, &apexA}
err := checkProviderDS(ProviderChildDSOnly, records)
if err == nil {
t.Errorf("Provider %s does not implement DS support at the zone apex, so should reject provided records",
ProviderChildDSOnly,
)
}
})
})
}
func Test_errorRepeat(t *testing.T) {
type args struct {
label string
domain string
}
tests := []struct {
name string
args args
want string
}{
{
name: "1",
args: args{label: "foo.bar.com", domain: "bar.com"},
want: `The name "foo.bar.com.bar.com." is an error (repeats the domain).` +
` Maybe instead of "foo.bar.com" you intended "foo"?` +
` If not add DISABLE_REPEATED_DOMAIN_CHECK to this record to permit this as-is.`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := errorRepeat(tt.args.label, tt.args.domain); got != tt.want {
t.Errorf("errorRepeat() = \n'%s', want\n'%s'", got, tt.want)
}
})
}
}