github.com/philhug/dnscontrol@v0.2.4-0.20180625181521-921fa9849001/pkg/normalize/validate.go (about)

     1  package normalize
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"strings"
     7  
     8  	"github.com/StackExchange/dnscontrol/models"
     9  	"github.com/StackExchange/dnscontrol/pkg/transform"
    10  	"github.com/StackExchange/dnscontrol/providers"
    11  	"github.com/miekg/dns"
    12  	"github.com/miekg/dns/dnsutil"
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  // Returns false if target does not validate.
    17  func checkIPv4(label string) error {
    18  	if net.ParseIP(label).To4() == nil {
    19  		return errors.Errorf("WARNING: target (%v) is not an IPv4 address", label)
    20  	}
    21  	return nil
    22  }
    23  
    24  // Returns false if target does not validate.
    25  func checkIPv6(label string) error {
    26  	if net.ParseIP(label).To16() == nil {
    27  		return errors.Errorf("WARNING: target (%v) is not an IPv6 address", label)
    28  	}
    29  	return nil
    30  }
    31  
    32  // make sure target is valid reference for cnames, mx, etc.
    33  func checkTarget(target string) error {
    34  	if target == "@" {
    35  		return nil
    36  	}
    37  	if len(target) < 1 {
    38  		return errors.Errorf("empty target")
    39  	}
    40  	if strings.ContainsAny(target, `'" +,|!£$%&/()=?^*ç°§;:<>[]()@`) {
    41  		return errors.Errorf("target (%v) includes invalid char", target)
    42  	}
    43  	// If it containts a ".", it must end in a ".".
    44  	if strings.ContainsRune(target, '.') && target[len(target)-1] != '.' {
    45  		return errors.Errorf("target (%v) must end with a (.) [https://stackexchange.github.io/dnscontrol/why-the-dot]", target)
    46  	}
    47  	return nil
    48  }
    49  
    50  // 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.
    51  func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []string) error {
    52  	var validTypes = map[string]bool{
    53  		"A":                true,
    54  		"AAAA":             true,
    55  		"CNAME":            true,
    56  		"CAA":              true,
    57  		"TLSA":             true,
    58  		"IMPORT_TRANSFORM": false,
    59  		"MX":               true,
    60  		"SRV":              true,
    61  		"TXT":              true,
    62  		"NS":               true,
    63  		"PTR":              true,
    64  		"ALIAS":            false,
    65  	}
    66  	_, ok := validTypes[rec.Type]
    67  	if !ok {
    68  		cType := providers.GetCustomRecordType(rec.Type)
    69  		if cType == nil {
    70  			return errors.Errorf("Unsupported record type (%v) domain=%v name=%v", rec.Type, domain, rec.GetLabel())
    71  		}
    72  		for _, providerType := range pTypes {
    73  			if providerType != cType.Provider {
    74  				return errors.Errorf("Custom record type %s is not compatible with provider type %s", rec.Type, providerType)
    75  			}
    76  		}
    77  		// it is ok. Lets replace the type with real type and add metadata to say we checked it
    78  		rec.Metadata["orig_custom_type"] = rec.Type
    79  		if cType.RealType != "" {
    80  			rec.Type = cType.RealType
    81  		}
    82  	}
    83  	return nil
    84  }
    85  
    86  // underscores in names are often used erroneously. They are valid for dns records, but invalid for urls.
    87  // here we list common records expected to have underscores. Anything else containing an underscore will print a warning.
    88  var labelUnderscores = []string{"_domainkey", "_dmarc", "_amazonses", "_acme-challenge"}
    89  
    90  // these record types may contain underscores
    91  var rTypeUnderscores = []string{"SRV", "TLSA", "TXT"}
    92  
    93  func checkLabel(label string, rType string, domain string, meta map[string]string) error {
    94  	if label == "@" {
    95  		return nil
    96  	}
    97  	if len(label) < 1 {
    98  		return errors.Errorf("empty %s label in %s", rType, domain)
    99  	}
   100  	if label[len(label)-1] == '.' {
   101  		return errors.Errorf("label %s.%s ends with a (.)", label, domain)
   102  	}
   103  	if strings.HasSuffix(label, domain) {
   104  		if m := meta["skip_fqdn_check"]; m != "true" {
   105  			return errors.Errorf(`label %s ends with domain name %s. Record names should not be fully qualified. Add {skip_fqdn_check:"true"} to this record if you really want to make %s.%s`, label, domain, label, domain)
   106  		}
   107  	}
   108  	// check for underscores last
   109  	for _, ex := range rTypeUnderscores {
   110  		if rType == ex {
   111  			return nil
   112  		}
   113  	}
   114  	for _, ex := range labelUnderscores {
   115  		if strings.Contains(label, ex) {
   116  			return nil
   117  		}
   118  	}
   119  	// underscores are warnings
   120  	if strings.ContainsRune(label, '_') {
   121  		return Warning{errors.Errorf("label %s.%s contains an underscore", label, domain)}
   122  	}
   123  
   124  	return nil
   125  }
   126  
   127  // checkTargets returns true if rec.Target is valid for the rec.Type.
   128  func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
   129  	label := rec.GetLabel()
   130  	target := rec.GetTargetField()
   131  	check := func(e error) {
   132  		if e != nil {
   133  			err := errors.Errorf("In %s %s.%s: %s", rec.Type, rec.GetLabel(), domain, e.Error())
   134  			if _, ok := e.(Warning); ok {
   135  				err = Warning{err}
   136  			}
   137  			errs = append(errs, err)
   138  		}
   139  	}
   140  	switch rec.Type { // #rtype_variations
   141  	case "A":
   142  		check(checkIPv4(target))
   143  	case "AAAA":
   144  		check(checkIPv6(target))
   145  	case "CNAME":
   146  		check(checkTarget(target))
   147  		if label == "@" {
   148  			check(errors.Errorf("cannot create CNAME record for bare domain"))
   149  		}
   150  	case "MX":
   151  		check(checkTarget(target))
   152  	case "NS":
   153  		check(checkTarget(target))
   154  		if label == "@" {
   155  			check(errors.Errorf("cannot create NS record for bare domain. Use NAMESERVER instead"))
   156  		}
   157  	case "PTR":
   158  		check(checkTarget(target))
   159  	case "ALIAS":
   160  		check(checkTarget(target))
   161  	case "SRV":
   162  		check(checkTarget(target))
   163  	case "TXT", "IMPORT_TRANSFORM", "CAA", "TLSA":
   164  	default:
   165  		if rec.Metadata["orig_custom_type"] != "" {
   166  			// it is a valid custom type. We perform no validation on target
   167  			return
   168  		}
   169  		errs = append(errs, errors.Errorf("checkTargets: Unimplemented record type (%v) domain=%v name=%v",
   170  			rec.Type, domain, rec.GetLabel()))
   171  	}
   172  	return
   173  }
   174  
   175  func transformCNAME(target, oldDomain, newDomain string) string {
   176  	// Canonicalize. If it isn't a FQDN, add the newDomain.
   177  	result := dnsutil.AddOrigin(target, oldDomain)
   178  	if dns.IsFqdn(result) {
   179  		result = result[:len(result)-1]
   180  	}
   181  	return dnsutil.AddOrigin(result, newDomain) + "."
   182  }
   183  
   184  // import_transform imports the records of one zone into another, modifying records along the way.
   185  func importTransform(srcDomain, dstDomain *models.DomainConfig, transforms []transform.IpConversion, ttl uint32) error {
   186  	// Read srcDomain.Records, transform, and append to dstDomain.Records:
   187  	// 1. Skip any that aren't A or CNAMEs.
   188  	// 2. Append destDomainname to the end of the label.
   189  	// 3. For CNAMEs, append destDomainname to the end of the target.
   190  	// 4. For As, change the target as described the transforms.
   191  
   192  	for _, rec := range srcDomain.Records {
   193  		if dstDomain.HasRecordTypeName(rec.Type, rec.GetLabelFQDN()) {
   194  			continue
   195  		}
   196  		newRec := func() *models.RecordConfig {
   197  			rec2, _ := rec.Copy()
   198  			newlabel := rec2.GetLabelFQDN()
   199  			rec2.SetLabel(newlabel, dstDomain.Name)
   200  			if ttl != 0 {
   201  				rec2.TTL = ttl
   202  			}
   203  			return rec2
   204  		}
   205  		switch rec.Type { // #rtype_variations
   206  		case "A":
   207  			trs, err := transform.TransformIPToList(net.ParseIP(rec.GetTargetField()), transforms)
   208  			if err != nil {
   209  				return errors.Errorf("import_transform: TransformIP(%v, %v) returned err=%s", rec.GetTargetField(), transforms, err)
   210  			}
   211  			for _, tr := range trs {
   212  				r := newRec()
   213  				r.SetTarget(tr.String())
   214  				dstDomain.Records = append(dstDomain.Records, r)
   215  			}
   216  		case "CNAME":
   217  			r := newRec()
   218  			r.SetTarget(transformCNAME(r.GetTargetField(), srcDomain.Name, dstDomain.Name))
   219  			dstDomain.Records = append(dstDomain.Records, r)
   220  		case "MX", "NS", "SRV", "TXT", "CAA", "TLSA":
   221  			// Not imported.
   222  			continue
   223  		default:
   224  			return errors.Errorf("import_transform: Unimplemented record type %v (%v)",
   225  				rec.Type, rec.GetLabel())
   226  		}
   227  	}
   228  	return nil
   229  }
   230  
   231  // deleteImportTransformRecords deletes any IMPORT_TRANSFORM records from a domain.
   232  func deleteImportTransformRecords(domain *models.DomainConfig) {
   233  	for i := len(domain.Records) - 1; i >= 0; i-- {
   234  		rec := domain.Records[i]
   235  		if rec.Type == "IMPORT_TRANSFORM" {
   236  			domain.Records = append(domain.Records[:i], domain.Records[i+1:]...)
   237  		}
   238  	}
   239  }
   240  
   241  // Warning is a wrapper around error that can be used to indicate it should not
   242  // stop execution, but is still likely a problem.
   243  type Warning struct {
   244  	error
   245  }
   246  
   247  // NormalizeAndValidateConfig performs and normalization and/or validation of the IR.
   248  func NormalizeAndValidateConfig(config *models.DNSConfig) (errs []error) {
   249  	for _, domain := range config.Domains {
   250  		pTypes := []string{}
   251  		txtMultiDissenters := []string{}
   252  		for _, provider := range domain.DNSProviderInstances {
   253  			pType := provider.ProviderType
   254  			// If NO_PURGE is in use, make sure this *isn't* a provider that *doesn't* support NO_PURGE.
   255  			if domain.KeepUnknown && providers.ProviderHasCabability(pType, providers.CantUseNOPURGE) {
   256  				errs = append(errs, errors.Errorf("%s uses NO_PURGE which is not supported by %s(%s)", domain.Name, provider.Name, pType))
   257  			}
   258  
   259  			// Record if any providers do not support TXTMulti:
   260  			if !providers.ProviderHasCabability(pType, providers.CanUseTXTMulti) {
   261  				txtMultiDissenters = append(txtMultiDissenters, provider.Name)
   262  			}
   263  		}
   264  
   265  		// Normalize Nameservers.
   266  		for _, ns := range domain.Nameservers {
   267  			ns.Name = dnsutil.AddOrigin(ns.Name, domain.Name)
   268  			ns.Name = strings.TrimRight(ns.Name, ".")
   269  		}
   270  		// Normalize Records.
   271  		models.PostProcessRecords(domain.Records)
   272  		for _, rec := range domain.Records {
   273  			if rec.TTL == 0 {
   274  				rec.TTL = models.DefaultTTL
   275  			}
   276  			// Validate the unmodified inputs:
   277  			if err := validateRecordTypes(rec, domain.Name, pTypes); err != nil {
   278  				errs = append(errs, err)
   279  			}
   280  			if err := checkLabel(rec.GetLabel(), rec.Type, domain.Name, rec.Metadata); err != nil {
   281  				errs = append(errs, err)
   282  			}
   283  			if errs2 := checkTargets(rec, domain.Name); errs2 != nil {
   284  				errs = append(errs, errs2...)
   285  			}
   286  
   287  			// Canonicalize Targets.
   288  			if rec.Type == "CNAME" || rec.Type == "MX" || rec.Type == "NS" {
   289  				rec.SetTarget(dnsutil.AddOrigin(rec.GetTargetField(), domain.Name+"."))
   290  			} else if rec.Type == "A" || rec.Type == "AAAA" {
   291  				rec.SetTarget(net.ParseIP(rec.GetTargetField()).String())
   292  			} else if rec.Type == "PTR" {
   293  				var err error
   294  				var name string
   295  				if name, err = transform.PtrNameMagic(rec.GetLabel(), domain.Name); err != nil {
   296  					errs = append(errs, err)
   297  				}
   298  				rec.SetLabel(name, domain.Name)
   299  			} else if rec.Type == "CAA" {
   300  				if rec.CaaTag != "issue" && rec.CaaTag != "issuewild" && rec.CaaTag != "iodef" {
   301  					errs = append(errs, errors.Errorf("CAA tag %s is invalid", rec.CaaTag))
   302  				}
   303  			} else if rec.Type == "TLSA" {
   304  				if rec.TlsaUsage < 0 || rec.TlsaUsage > 3 {
   305  					errs = append(errs, errors.Errorf("TLSA Usage %d is invalid in record %s (domain %s)",
   306  						rec.TlsaUsage, rec.GetLabel(), domain.Name))
   307  				}
   308  				if rec.TlsaSelector < 0 || rec.TlsaSelector > 1 {
   309  					errs = append(errs, errors.Errorf("TLSA Selector %d is invalid in record %s (domain %s)",
   310  						rec.TlsaSelector, rec.GetLabel(), domain.Name))
   311  				}
   312  				if rec.TlsaMatchingType < 0 || rec.TlsaMatchingType > 2 {
   313  					errs = append(errs, errors.Errorf("TLSA MatchingType %d is invalid in record %s (domain %s)",
   314  						rec.TlsaMatchingType, rec.GetLabel(), domain.Name))
   315  				}
   316  			} else if rec.Type == "TXT" && len(txtMultiDissenters) != 0 && len(rec.TxtStrings) > 1 {
   317  				// There are providers that  don't support TXTMulti yet there is
   318  				// a TXT record with multiple strings:
   319  				errs = append(errs,
   320  					errors.Errorf("TXT records with multiple strings (label %v domain: %v) not supported by %s",
   321  						rec.GetLabel(), domain.Name, strings.Join(txtMultiDissenters, ",")))
   322  			}
   323  
   324  			// Populate FQDN:
   325  			rec.SetLabel(rec.GetLabel(), domain.Name)
   326  		}
   327  	}
   328  
   329  	// SPF flattening
   330  	if ers := flattenSPFs(config); len(ers) > 0 {
   331  		errs = append(errs, ers...)
   332  	}
   333  
   334  	// Process IMPORT_TRANSFORM
   335  	for _, domain := range config.Domains {
   336  		for _, rec := range domain.Records {
   337  			if rec.Type == "IMPORT_TRANSFORM" {
   338  				table, err := transform.DecodeTransformTable(rec.Metadata["transform_table"])
   339  				if err != nil {
   340  					errs = append(errs, err)
   341  					continue
   342  				}
   343  				err = importTransform(config.FindDomain(rec.GetTargetField()), domain, table, rec.TTL)
   344  				if err != nil {
   345  					errs = append(errs, err)
   346  				}
   347  			}
   348  		}
   349  	}
   350  	// Clean up:
   351  	for _, domain := range config.Domains {
   352  		deleteImportTransformRecords(domain)
   353  	}
   354  	// Run record transforms
   355  	for _, domain := range config.Domains {
   356  		if err := applyRecordTransforms(domain); err != nil {
   357  			errs = append(errs, err)
   358  		}
   359  	}
   360  
   361  	for _, d := range config.Domains {
   362  		// Check that CNAMES don't have to co-exist with any other records
   363  		errs = append(errs, checkCNAMEs(d)...)
   364  		// Check that if any advanced record types are used in a domain, every provider for that domain supports them
   365  		err := checkProviderCapabilities(d)
   366  		if err != nil {
   367  			errs = append(errs, err)
   368  		}
   369  		// Validate FQDN consistency
   370  		for _, r := range d.Records {
   371  			if r.NameFQDN == "" || !strings.HasSuffix(r.NameFQDN, d.Name) {
   372  				errs = append(errs, fmt.Errorf("Record named '%s' does not have correct FQDN in domain '%s'. FQDN: %s", r.Name, d.Name, r.NameFQDN))
   373  			}
   374  		}
   375  	}
   376  
   377  	return errs
   378  }
   379  
   380  func checkCNAMEs(dc *models.DomainConfig) (errs []error) {
   381  	cnames := map[string]bool{}
   382  	for _, r := range dc.Records {
   383  		if r.Type == "CNAME" {
   384  			if cnames[r.GetLabel()] {
   385  				errs = append(errs, errors.Errorf("Cannot have multiple CNAMEs with same name: %s", r.GetLabelFQDN()))
   386  			}
   387  			cnames[r.GetLabel()] = true
   388  		}
   389  	}
   390  	for _, r := range dc.Records {
   391  		if cnames[r.GetLabel()] && r.Type != "CNAME" {
   392  			errs = append(errs, errors.Errorf("Cannot have CNAME and %s record with same name: %s", r.Type, r.GetLabelFQDN()))
   393  		}
   394  	}
   395  	return
   396  }
   397  
   398  func checkProviderCapabilities(dc *models.DomainConfig) error {
   399  	types := []struct {
   400  		rType string
   401  		cap   providers.Capability
   402  	}{
   403  		{"ALIAS", providers.CanUseAlias},
   404  		{"PTR", providers.CanUsePTR},
   405  		{"SRV", providers.CanUseSRV},
   406  		{"CAA", providers.CanUseCAA},
   407  		{"TLSA", providers.CanUseTLSA},
   408  	}
   409  	for _, ty := range types {
   410  		hasAny := false
   411  		for _, r := range dc.Records {
   412  			if r.Type == ty.rType {
   413  				hasAny = true
   414  				break
   415  			}
   416  		}
   417  		if !hasAny {
   418  			continue
   419  		}
   420  		for _, provider := range dc.DNSProviderInstances {
   421  			if !providers.ProviderHasCabability(provider.ProviderType, ty.cap) {
   422  				return errors.Errorf("Domain %s uses %s records, but DNS provider type %s does not support them", dc.Name, ty.rType, provider.ProviderType)
   423  			}
   424  		}
   425  	}
   426  	return nil
   427  }
   428  
   429  func applyRecordTransforms(domain *models.DomainConfig) error {
   430  	for _, rec := range domain.Records {
   431  		if rec.Type != "A" {
   432  			continue
   433  		}
   434  		tt, ok := rec.Metadata["transform"]
   435  		if !ok {
   436  			continue
   437  		}
   438  		table, err := transform.DecodeTransformTable(tt)
   439  		if err != nil {
   440  			return err
   441  		}
   442  		ip := net.ParseIP(rec.GetTargetField()) // ip already validated above
   443  		newIPs, err := transform.TransformIPToList(net.ParseIP(rec.GetTargetField()), table)
   444  		if err != nil {
   445  			return err
   446  		}
   447  		for i, newIP := range newIPs {
   448  			if i == 0 && !newIP.Equal(ip) {
   449  				rec.SetTarget(newIP.String()) // replace target of first record if different
   450  			} else if i > 0 {
   451  				// any additional ips need identical records with the alternate ip added to the domain
   452  				copy, err := rec.Copy()
   453  				if err != nil {
   454  					return err
   455  				}
   456  				copy.SetTarget(newIP.String())
   457  				domain.Records = append(domain.Records, copy)
   458  			}
   459  		}
   460  	}
   461  	return nil
   462  }