mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
migrate code for github
This commit is contained in:
294
normalize/validate.go
Normal file
294
normalize/validate.go
Normal file
@ -0,0 +1,294 @@
|
||||
package normalize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/StackExchange/dnscontrol/transform"
|
||||
)
|
||||
|
||||
// Returns false if label does not validate.
|
||||
func assert_no_enddot(label string) error {
|
||||
if label == "@" {
|
||||
return nil
|
||||
}
|
||||
if len(label) < 1 {
|
||||
return fmt.Errorf("WARNING: null label.")
|
||||
}
|
||||
if label[len(label)-1] == '.' {
|
||||
return fmt.Errorf("WARNING: label (%v) ends with a (.)", label)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns false if label does not validate.
|
||||
func assert_no_underscores(label string) error {
|
||||
if strings.ContainsRune(label, '_') {
|
||||
return fmt.Errorf("WARNING: label (%v) contains an underscore", label)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns false if target does not validate.
|
||||
func assert_valid_ipv4(label string) error {
|
||||
if net.ParseIP(label).To4() == nil {
|
||||
return fmt.Errorf("WARNING: target (%v) is not an IPv4 address", label)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns false if target does not validate.
|
||||
func assert_valid_ipv6(label string) error {
|
||||
if net.ParseIP(label).To16() == nil {
|
||||
return fmt.Errorf("WARNING: target (%v) is not an IPv6 address", label)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// assert_valid_cname_target returns 1 if target is not valid for cnames.
|
||||
func assert_valid_target(label string) error {
|
||||
if label == "@" {
|
||||
return nil
|
||||
}
|
||||
if len(label) < 1 {
|
||||
return fmt.Errorf("WARNING: null label.")
|
||||
}
|
||||
// If it containts a ".", it must end in a ".".
|
||||
if strings.ContainsRune(label, '.') && label[len(label)-1] != '.' {
|
||||
return fmt.Errorf("WARNING: label (%v) includes a (.), must end with a (.)", label)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRecordTypes list of valid rec.Type values. Returns true if this is a real DNS record type, false means it is a pseudo-type used internally.
|
||||
func validateRecordTypes(rec *models.RecordConfig, domain_name string) error {
|
||||
var valid_types = map[string]bool{
|
||||
"A": true,
|
||||
"AAAA": true,
|
||||
"CNAME": true,
|
||||
"IMPORT_TRANSFORM": false,
|
||||
"MX": true,
|
||||
"TXT": true,
|
||||
"NS": true,
|
||||
}
|
||||
|
||||
if _, ok := valid_types[rec.Type]; !ok {
|
||||
return fmt.Errorf("Unsupported record type (%v) domain=%v name=%v", rec.Type, domain_name, rec.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTargets returns true if rec.Target is valid for the rec.Type.
|
||||
func validateTargets(rec *models.RecordConfig, domain_name string) (errs []error) {
|
||||
label := rec.Name
|
||||
target := rec.Target
|
||||
check := func(e error) {
|
||||
if e != nil {
|
||||
errs = append(errs, e)
|
||||
}
|
||||
}
|
||||
switch rec.Type {
|
||||
case "A":
|
||||
check(assert_no_enddot(label))
|
||||
check(assert_no_underscores(label))
|
||||
check(assert_valid_ipv4(target))
|
||||
case "AAAA":
|
||||
check(assert_no_enddot(label))
|
||||
check(assert_no_underscores(label))
|
||||
check(assert_valid_ipv6(target))
|
||||
case "CNAME":
|
||||
check(assert_no_enddot(label))
|
||||
check(assert_no_underscores(label))
|
||||
check(assert_valid_target(target))
|
||||
case "MX":
|
||||
check(assert_no_enddot(label))
|
||||
check(assert_no_underscores(label))
|
||||
check(assert_valid_target(target))
|
||||
case "NS":
|
||||
check(assert_no_enddot(label))
|
||||
check(assert_no_underscores(label))
|
||||
check(assert_valid_target(target))
|
||||
case "TXT", "IMPORT_TRANSFORM":
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("Unimplemented record type (%v) domain=%v name=%v",
|
||||
rec.Type, domain_name, rec.Name))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func transform_cname(target, old_domain, new_domain string) string {
|
||||
// Canonicalize. If it isn't a FQDN, add the new_domain.
|
||||
result := dnsutil.AddOrigin(target, old_domain)
|
||||
if dns.IsFqdn(result) {
|
||||
result = result[:len(result)-1]
|
||||
}
|
||||
return dnsutil.AddOrigin(result, new_domain) + "."
|
||||
}
|
||||
|
||||
// import_transform imports the records of one zone into another, modifying records along the way.
|
||||
func import_transform(src_domain, dst_domain *models.DomainConfig, transforms []transform.IpConversion) error {
|
||||
// Read src_domain.Records, transform, and append to dst_domain.Records:
|
||||
// 1. Skip any that aren't A or CNAMEs.
|
||||
// 2. Append dest_domainname to the end of the label.
|
||||
// 3. For CNAMEs, append dest_domainname to the end of the target.
|
||||
// 4. For As, change the target as described the transforms.
|
||||
|
||||
for _, rec := range src_domain.Records {
|
||||
var newrec models.RecordConfig
|
||||
newrec = *rec
|
||||
newrec.Name = newrec.NameFQDN
|
||||
newrec.NameFQDN = dnsutil.AddOrigin(newrec.Name, dst_domain.Name)
|
||||
switch rec.Type {
|
||||
case "A":
|
||||
tr, err := transform.TransformIP(net.ParseIP(newrec.Target), transforms)
|
||||
if err != nil {
|
||||
return fmt.Errorf("import_transform: TransformIP(%v, %v) returned err=%s", newrec.Target, transforms, err)
|
||||
}
|
||||
newrec.Target = tr.String()
|
||||
case "CNAME":
|
||||
newrec.Target = transform_cname(newrec.Target, src_domain.Name, dst_domain.Name)
|
||||
case "MX", "NS", "TXT":
|
||||
// Not imported.
|
||||
continue
|
||||
default:
|
||||
return fmt.Errorf("import_transform: Unimplemented record type %v (%v)",
|
||||
rec.Type, rec.Name)
|
||||
}
|
||||
dst_domain.Records = append(dst_domain.Records, &newrec)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteImportTransformRecords deletes any IMPORT_TRANSFORM records from a domain.
|
||||
func deleteImportTransformRecords(domain *models.DomainConfig) {
|
||||
for i := len(domain.Records) - 1; i >= 0; i-- {
|
||||
rec := domain.Records[i]
|
||||
if rec.Type == "IMPORT_TRANSFORM" {
|
||||
domain.Records = append(domain.Records[:i], domain.Records[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeAndValidateConfig(config *models.DNSConfig) (errs []error) {
|
||||
// TODO(tlim): Before any changes are made, we should check the rules
|
||||
// such as MX/CNAME/NS .Target must be a single word, "@", or FQDN.
|
||||
// Validate and normalize
|
||||
for _, domain := range config.Domains {
|
||||
|
||||
// Normalize Nameservers.
|
||||
for _, ns := range domain.Nameservers {
|
||||
ns.Name = dnsutil.AddOrigin(ns.Name, domain.Name)
|
||||
ns.Name = strings.TrimRight(ns.Name, ".")
|
||||
}
|
||||
|
||||
// Normalize Records.
|
||||
for _, rec := range domain.Records {
|
||||
|
||||
// Validate the unmodified inputs:
|
||||
if err := validateRecordTypes(rec, domain.Name); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if errs2 := validateTargets(rec, domain.Name); errs2 != nil {
|
||||
errs = append(errs, errs2...)
|
||||
}
|
||||
|
||||
// Canonicalize Targets.
|
||||
if rec.Type == "CNAME" || rec.Type == "MX" || rec.Type == "NS" {
|
||||
rec.Target = dnsutil.AddOrigin(rec.Target, domain.Name+".")
|
||||
}
|
||||
// Populate FQDN:
|
||||
rec.NameFQDN = dnsutil.AddOrigin(rec.Name, domain.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Process any pseudo-records:
|
||||
for _, domain := range config.Domains {
|
||||
for _, rec := range domain.Records {
|
||||
if rec.Type == "IMPORT_TRANSFORM" {
|
||||
table, err := transform.DecodeTransformTable(rec.Metadata["transform_table"])
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
err = import_transform(config.FindDomain(rec.Target), domain, table)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clean up:
|
||||
for _, domain := range config.Domains {
|
||||
deleteImportTransformRecords(domain)
|
||||
}
|
||||
|
||||
// Run record transforms
|
||||
for _, domain := range config.Domains {
|
||||
if err := applyRecordTransforms(domain); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all labels are FQDN ending with ".":
|
||||
for _, domain := range config.Domains {
|
||||
for _, rec := range domain.Records {
|
||||
// .Name must NOT end in "."
|
||||
if rec.Name[len(rec.Name)-1] == '.' {
|
||||
errs = append(errs, fmt.Errorf("Should not happen: Label ends with '.': %v %v %v %v %v",
|
||||
domain.Name, rec.Name, rec.NameFQDN, rec.Type, rec.Target))
|
||||
}
|
||||
// .NameFQDN must NOT end in "."
|
||||
if rec.NameFQDN[len(rec.NameFQDN)-1] == '.' {
|
||||
errs = append(errs, fmt.Errorf("Should not happen: FQDN ends with '.': %v %v %v %v %v",
|
||||
domain.Name, rec.Name, rec.NameFQDN, rec.Type, rec.Target))
|
||||
}
|
||||
// .Target MUST end in "."
|
||||
if rec.Type == "CNAME" || rec.Type == "NS" || rec.Type == "MX" {
|
||||
if rec.Target[len(rec.Target)-1] != '.' {
|
||||
errs = append(errs, fmt.Errorf("Should not happen: Target does NOT ends with '.': %v %v %v %v %v",
|
||||
domain.Name, rec.Name, rec.NameFQDN, rec.Type, rec.Target))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func applyRecordTransforms(domain *models.DomainConfig) error {
|
||||
for _, rec := range domain.Records {
|
||||
if rec.Type != "A" {
|
||||
continue
|
||||
}
|
||||
tt, ok := rec.Metadata["transform"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
table, err := transform.DecodeTransformTable(tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ip := net.ParseIP(rec.Target) //ip already validated above
|
||||
newIPs, err := transform.TransformIPToList(net.ParseIP(rec.Target), table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, newIP := range newIPs {
|
||||
if i == 0 && !newIP.Equal(ip) {
|
||||
rec.Target = newIP.String() //replace target of first record if different
|
||||
} else if i > 0 {
|
||||
// any additional ips need identical records with the alternate ip added to the domain
|
||||
copy, err := rec.Copy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copy.Target = newIP.String()
|
||||
domain.Records = append(domain.Records, copy)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
140
normalize/validate_test.go
Normal file
140
normalize/validate_test.go
Normal file
@ -0,0 +1,140 @@
|
||||
package normalize
|
||||
|
||||
import (
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_assert_no_enddot(t *testing.T) {
|
||||
var tests = []struct {
|
||||
experiment string
|
||||
isError bool
|
||||
}{
|
||||
{"@", false},
|
||||
{"foo", false},
|
||||
{"foo.bar", false},
|
||||
{"foo.", true},
|
||||
{"foo.bar.", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := assert_no_enddot(test.experiment)
|
||||
checkError(t, err, test.isError, test.experiment)
|
||||
}
|
||||
}
|
||||
|
||||
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_no_underscores(t *testing.T) {
|
||||
var tests = []struct {
|
||||
experiment string
|
||||
isError bool
|
||||
}{
|
||||
{"@", false},
|
||||
{"foo", false},
|
||||
{"_foo", true},
|
||||
{"foo_", true},
|
||||
{"fo_o", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := assert_no_underscores(test.experiment)
|
||||
checkError(t, err, test.isError, test.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 := assert_valid_ipv4(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},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := assert_valid_target(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 := transform_cname(test.experiment, "old.com", "new.com")
|
||||
if test.expected != actual {
|
||||
t.Errorf("%v: expected (%v) got (%v)\n", test.experiment, test.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
{Type: "A", Target: test.givenIP, 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.Target != 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.Target)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user