dnscontrol/pkg/normalize/validate.go

679 lines
21 KiB
Go
Raw Normal View History

2016-08-23 08:31:50 +08:00
package normalize
import (
"fmt"
2016-08-23 08:31:50 +08:00
"net"
"strings"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/transform"
"github.com/StackExchange/dnscontrol/v3/providers"
"github.com/miekg/dns"
"github.com/miekg/dns/dnsutil"
2016-08-23 08:31:50 +08:00
)
// Returns false if target does not validate.
func checkIPv4(label string) error {
2016-08-23 08:31:50 +08:00
if net.ParseIP(label).To4() == nil {
return fmt.Errorf("WARNING: target (%v) is not an IPv4 address", label)
2016-08-23 08:31:50 +08:00
}
return nil
}
// Returns false if target does not validate.
func checkIPv6(label string) error {
2016-08-23 08:31:50 +08:00
if net.ParseIP(label).To16() == nil {
return fmt.Errorf("WARNING: target (%v) is not an IPv6 address", label)
2016-08-23 08:31:50 +08:00
}
return nil
}
// make sure target is valid reference for cnames, mx, etc.
func checkTarget(target string) error {
if target == "@" {
2016-08-23 08:31:50 +08:00
return nil
}
if target == "" {
return fmt.Errorf("empty target")
2016-08-23 08:31:50 +08:00
}
2017-06-11 21:31:26 +08:00
if strings.ContainsAny(target, `'" +,|!£$%&/()=?^*ç°§;:<>[]()@`) {
return fmt.Errorf("target (%v) includes invalid char", target)
}
2020-07-07 08:18:24 +08:00
// If it contains a ".", it must end in a ".".
if strings.ContainsRune(target, '.') && target[len(target)-1] != '.' {
return fmt.Errorf("target (%v) must end with a (.) [https://stackexchange.github.io/dnscontrol/why-the-dot]", target)
2016-08-23 08:31:50 +08:00
}
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 string, pTypes []string) error {
var validTypes = map[string]bool{
2016-08-23 08:31:50 +08:00
"A": true,
"AAAA": true,
"CNAME": true,
"CAA": true,
"DS": true,
2017-09-15 21:03:29 +08:00
"TLSA": true,
2016-08-23 08:31:50 +08:00
"IMPORT_TRANSFORM": false,
"MX": true,
"SRV": true,
"SSHFP": true,
2016-08-23 08:31:50 +08:00
"TXT": true,
"NS": true,
"PTR": true,
2019-03-28 22:40:13 +08:00
"NAPTR": true,
"ALIAS": false,
2016-08-23 08:31:50 +08:00
}
_, ok := validTypes[rec.Type]
if !ok {
cType := providers.GetCustomRecordType(rec.Type)
if cType == nil {
return fmt.Errorf("unsupported record type (%v) domain=%v name=%v", rec.Type, domain, rec.GetLabel())
}
for _, providerType := range pTypes {
if providerType != cType.Provider {
return fmt.Errorf("custom record type %s is not compatible with provider type %s", rec.Type, providerType)
}
}
// it is ok. Lets replace the type with real type and add metadata to say we checked it
rec.Metadata["orig_custom_type"] = rec.Type
if cType.RealType != "" {
rec.Type = cType.RealType
}
2016-08-23 08:31:50 +08:00
}
return nil
}
func checkLabel(label string, rType string, target, domain string, meta map[string]string) error {
if label == "@" {
return nil
}
if label == "" {
return fmt.Errorf("empty %s label in %s", rType, domain)
}
if label[len(label)-1] == '.' {
return fmt.Errorf("label %s.%s ends with a (.)", label, domain)
}
if label == domain || strings.HasSuffix(label, "."+domain) {
if m := meta["skip_fqdn_check"]; m != "true" {
return fmt.Errorf(`label %q ends with domain name %q. 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)
}
}
// Underscores are permitted in labels, but we print a warning unless they
// are used in a way we consider typical. Yes, we're opinionated here.
// Don't warn for certain rtypes:
for _, ex := range []string{"SRV", "TLSA", "TXT"} {
2017-09-15 21:03:29 +08:00
if rType == ex {
return nil
}
2017-09-15 21:03:29 +08:00
}
// Don't warn for records that start with _
// See https://github.com/StackExchange/dnscontrol/issues/829
if strings.HasPrefix(label, "_") || strings.Contains(label, "._") {
return nil
}
// Otherwise, warn.
2017-09-15 21:03:29 +08:00
if strings.ContainsRune(label, '_') {
return Warning{fmt.Errorf("label %s.%s contains an underscore", label, domain)}
2017-09-15 21:03:29 +08:00
}
return nil
}
// checkTargets returns true if rec.Target is valid for the rec.Type.
func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
label := rec.GetLabel()
target := rec.GetTargetField()
2016-08-23 08:31:50 +08:00
check := func(e error) {
if e != nil {
err := fmt.Errorf("in %s %s.%s: %s", rec.Type, rec.GetLabel(), domain, e.Error())
if _, ok := e.(Warning); ok {
err = Warning{err}
}
errs = append(errs, err)
2016-08-23 08:31:50 +08:00
}
}
switch rec.Type { // #rtype_variations
2016-08-23 08:31:50 +08:00
case "A":
check(checkIPv4(target))
2016-08-23 08:31:50 +08:00
case "AAAA":
check(checkIPv6(target))
2016-08-23 08:31:50 +08:00
case "CNAME":
check(checkTarget(target))
if label == "@" {
check(fmt.Errorf("cannot create CNAME record for bare domain"))
}
2016-08-23 08:31:50 +08:00
case "MX":
check(checkTarget(target))
2016-08-23 08:31:50 +08:00
case "NS":
check(checkTarget(target))
if label == "@" {
check(fmt.Errorf("cannot create NS record for bare domain. Use NAMESERVER instead"))
}
case "PTR":
check(checkTarget(target))
2019-04-01 15:15:43 +08:00
case "NAPTR":
check(checkTarget(target))
case "ALIAS":
check(checkTarget(target))
case "SOA":
check(checkTarget(target))
case "SRV":
check(checkTarget(target))
2020-05-26 19:46:10 +08:00
case "TXT", "IMPORT_TRANSFORM", "CAA", "SSHFP", "TLSA", "DS":
2016-08-23 08:31:50 +08:00
default:
if rec.Metadata["orig_custom_type"] != "" {
// it is a valid custom type. We perform no validation on target
return
}
errs = append(errs, fmt.Errorf("checkTargets: Unimplemented record type (%v) domain=%v name=%v",
rec.Type, domain, rec.GetLabel()))
2016-08-23 08:31:50 +08:00
}
return
}
// TODO: Write a test.
func checkTxtStrings(rc *models.RecordConfig) error {
for i := range rc.TxtStrings {
l := len([]byte(rc.TxtStrings[i]))
if l > 255 {
return fmt.Errorf("length of TxtStrings[%d] is %d, which is >255", i, l)
}
}
return nil
}
func transformCNAME(target, oldDomain, newDomain string) string {
// Canonicalize. If it isn't a FQDN, add the newDomain.
result := dnsutil.AddOrigin(target, oldDomain)
2016-08-23 08:31:50 +08:00
if dns.IsFqdn(result) {
result = result[:len(result)-1]
}
return dnsutil.AddOrigin(result, newDomain) + "."
2016-08-23 08:31:50 +08:00
}
// import_transform imports the records of one zone into another, modifying records along the way.
func importTransform(srcDomain, dstDomain *models.DomainConfig, transforms []transform.IPConversion, ttl uint32) error {
// Read srcDomain.Records, transform, and append to dstDomain.Records:
2016-08-23 08:31:50 +08:00
// 1. Skip any that aren't A or CNAMEs.
// 2. Append destDomainname to the end of the label.
// 3. For CNAMEs, append destDomainname to the end of the target.
2016-08-23 08:31:50 +08:00
// 4. For As, change the target as described the transforms.
for _, rec := range srcDomain.Records {
if dstDomain.Records.HasRecordTypeName(rec.Type, rec.GetLabelFQDN()) {
continue
}
newRec := func() *models.RecordConfig {
rec2, _ := rec.Copy()
newlabel := rec2.GetLabelFQDN()
rec2.SetLabel(newlabel, dstDomain.Name)
if ttl != 0 {
rec2.TTL = ttl
}
return rec2
}
switch rec.Type { // #rtype_variations
2016-08-23 08:31:50 +08:00
case "A":
trs, err := transform.IPToList(net.ParseIP(rec.GetTargetField()), transforms)
2016-08-23 08:31:50 +08:00
if err != nil {
return fmt.Errorf("import_transform: TransformIP(%v, %v) returned err=%s", rec.GetTargetField(), transforms, err)
}
for _, tr := range trs {
r := newRec()
r.SetTarget(tr.String())
dstDomain.Records = append(dstDomain.Records, r)
2016-08-23 08:31:50 +08:00
}
case "CNAME":
r := newRec()
r.SetTarget(transformCNAME(r.GetTargetField(), srcDomain.Name, dstDomain.Name))
dstDomain.Records = append(dstDomain.Records, r)
case "MX", "NAPTR", "NS", "SOA", "SRV", "TXT", "CAA", "TLSA":
2016-08-23 08:31:50 +08:00
// Not imported.
continue
default:
return fmt.Errorf("import_transform: Unimplemented record type %v (%v)",
rec.Type, rec.GetLabel())
2016-08-23 08:31:50 +08:00
}
}
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:]...)
}
}
}
// Warning is a wrapper around error that can be used to indicate it should not
// stop execution, but is still likely a problem.
type Warning struct {
error
}
// ValidateAndNormalizeConfig performs and normalization and/or validation of the IR.
func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
err := processSplitHorizonDomains(config)
if err != nil {
return []error{err}
}
for _, domain := range config.Domains {
pTypes := []string{}
for _, provider := range domain.DNSProviderInstances {
pType := provider.ProviderType
// If NO_PURGE is in use, make sure this *isn't* a provider that *doesn't* support NO_PURGE.
2020-01-13 00:24:10 +08:00
if domain.KeepUnknown && providers.ProviderHasCapability(pType, providers.CantUseNOPURGE) {
errs = append(errs, fmt.Errorf("%s uses NO_PURGE which is not supported by %s(%s)", domain.Name, provider.Name, pType))
}
}
2016-08-23 08:31:50 +08:00
// Normalize Nameservers.
for _, ns := range domain.Nameservers {
// NB(tlim): Like any target, NAMESERVER() is input by the user
// as a shortname or a FQDN+dot.
if err := checkTarget(ns.Name); err != nil {
errs = append(errs, err)
}
// Unlike any other FQDN in this system, it is stored as a FQDN without the trailing dot.
n := dnsutil.AddOrigin(ns.Name, domain.Name+".")
ns.Name = strings.TrimSuffix(n, ".")
2016-08-23 08:31:50 +08:00
}
2016-08-23 08:31:50 +08:00
// Normalize Records.
models.PostProcessRecords(domain.Records)
2016-08-23 08:31:50 +08:00
for _, rec := range domain.Records {
if rec.TTL == 0 {
rec.TTL = models.DefaultTTL
}
// Canonicalize Label:
if rec.GetLabel() == (domain.Name + ".") {
// If label == ${domain}DOT, change to "@"
rec.SetLabel("@", domain.Name)
} else if lab, suf := rec.GetLabel(), "."+domain.Name+"."; strings.HasSuffix(lab, suf) {
// If label ends with DOT${domain}DOT, strip it to a short name.
rec.SetLabel(lab[:len(lab)-len(suf)], domain.Name)
}
// If label ends with dot, add to the list of errors.
if strings.HasSuffix(rec.GetLabel(), ".") {
errs = append(errs, fmt.Errorf("label %q does not match D(%q)", rec.GetLabel(), domain.Name))
return errs // Exit early.
}
// in-addr.arpa magic
if strings.HasSuffix(domain.Name, ".in-addr.arpa") || strings.HasSuffix(domain.Name, ".ip6.arpa") {
label := rec.GetLabel()
if strings.HasSuffix(label, "."+domain.Name) {
rec.SetLabel(label[0:(len(label)-len("."+domain.Name))], domain.Name)
}
}
2016-08-23 08:31:50 +08:00
// Validate the unmodified inputs:
if err := validateRecordTypes(rec, domain.Name, pTypes); err != nil {
2016-08-23 08:31:50 +08:00
errs = append(errs, err)
}
if err := checkLabel(rec.GetLabel(), rec.Type, rec.GetTargetField(), domain.Name, rec.Metadata); err != nil {
errs = append(errs, err)
}
if errs2 := checkTargets(rec, domain.Name); errs2 != nil {
2016-08-23 08:31:50 +08:00
errs = append(errs, errs2...)
}
if rec.HasFormatIdenticalToTXT() { // i.e. if it is a TXT or SPF record.
if err := checkTxtStrings(rec); err != nil {
errs = append(errs, err)
}
}
2016-08-23 08:31:50 +08:00
// Canonicalize Targets.
2020-05-30 22:56:20 +08:00
if rec.Type == "CNAME" || rec.Type == "MX" || rec.Type == "NAPTR" || rec.Type == "NS" || rec.Type == "SRV" {
// #rtype_variations
// These record types have a target that is a hostname.
// We normalize them to a FQDN so there is less variation to handle. If a
// provider API requires a shortname, the provider must do the shortening.
Add D_EXTEND (#885) (thanks to @ad8-bdl!) * fix get-zones code block indentation * extend D_EXTEND to handle subdomains * fix targets: make absolute incl. subdomain where necessary * clarify subdomain target test (not IP && not fqdn) * Add parse_tests for D and D_EXTEND * _getDomainObject: examine all domains * human readable form * consistent test IP addresses * Improve docs and formatting * propagate subdomain to canonicalisation * en-US spelling * rm extraneous console.log * ignore subdomain for CF_REDIRECT * clarify D_EXTEND doc re. CF_REDIRECT * rm extraneous linebreak * _getDomainObject: examine all domains * human readable form * consistent test IP addresses * propagate subdomain to canonicalisation * en-US spelling * rm extraneous console.log * ignore subdomain for CF_REDIRECT * clarify D_EXTEND doc re. CF_REDIRECT * rm extraneous linebreak * GANDI_V5: Use github.com/go-gandi/go-gandi, not github.com/tiramiseb/go-gandi (#883) * DOCUMENTATION: Fix error in CNAME.md (#877) The current example `CNAME("def", "test.subdomain"), // def.example.com -> test.subdomain.example.com` is invalid (correctly raises a validation error, "ERROR: in CNAME def.example.com: target (test.subdomain) must end with a (.)") * typos, fmt; example syntax fixes and real output * formatting; re-add lost comment * RecordConfig subdomain should be nullable * providers/cscglobal/api.go: Fix fmt string * More tests and docs * go generate Co-authored-by: Ben L <47653825+ad8-bdl@users.noreply.github.com>
2020-10-08 02:27:33 +08:00
origin := domain.Name + "."
if rec.SubDomain != "" {
Add D_EXTEND (#885) (thanks to @ad8-bdl!) * fix get-zones code block indentation * extend D_EXTEND to handle subdomains * fix targets: make absolute incl. subdomain where necessary * clarify subdomain target test (not IP && not fqdn) * Add parse_tests for D and D_EXTEND * _getDomainObject: examine all domains * human readable form * consistent test IP addresses * Improve docs and formatting * propagate subdomain to canonicalisation * en-US spelling * rm extraneous console.log * ignore subdomain for CF_REDIRECT * clarify D_EXTEND doc re. CF_REDIRECT * rm extraneous linebreak * _getDomainObject: examine all domains * human readable form * consistent test IP addresses * propagate subdomain to canonicalisation * en-US spelling * rm extraneous console.log * ignore subdomain for CF_REDIRECT * clarify D_EXTEND doc re. CF_REDIRECT * rm extraneous linebreak * GANDI_V5: Use github.com/go-gandi/go-gandi, not github.com/tiramiseb/go-gandi (#883) * DOCUMENTATION: Fix error in CNAME.md (#877) The current example `CNAME("def", "test.subdomain"), // def.example.com -> test.subdomain.example.com` is invalid (correctly raises a validation error, "ERROR: in CNAME def.example.com: target (test.subdomain) must end with a (.)") * typos, fmt; example syntax fixes and real output * formatting; re-add lost comment * RecordConfig subdomain should be nullable * providers/cscglobal/api.go: Fix fmt string * More tests and docs * go generate Co-authored-by: Ben L <47653825+ad8-bdl@users.noreply.github.com>
2020-10-08 02:27:33 +08:00
origin = rec.SubDomain + "." + origin
}
rec.SetTarget(dnsutil.AddOrigin(rec.GetTargetField(), origin))
} else if rec.Type == "A" || rec.Type == "AAAA" {
rec.SetTarget(net.ParseIP(rec.GetTargetField()).String())
} else if rec.Type == "PTR" {
var err error
var name string
if name, err = transform.PtrNameMagic(rec.GetLabel(), domain.Name); err != nil {
errs = append(errs, err)
}
rec.SetLabel(name, domain.Name)
} else if rec.Type == "CAA" {
if rec.CaaTag != "issue" && rec.CaaTag != "issuewild" && rec.CaaTag != "iodef" {
errs = append(errs, fmt.Errorf("CAA tag %s is invalid", rec.CaaTag))
}
2017-09-15 21:03:29 +08:00
} else if rec.Type == "TLSA" {
if rec.TlsaUsage > 3 {
errs = append(errs, fmt.Errorf("TLSA Usage %d is invalid in record %s (domain %s)",
rec.TlsaUsage, rec.GetLabel(), domain.Name))
2017-09-15 21:03:29 +08:00
}
if rec.TlsaSelector > 1 {
errs = append(errs, fmt.Errorf("TLSA Selector %d is invalid in record %s (domain %s)",
rec.TlsaSelector, rec.GetLabel(), domain.Name))
2017-09-15 21:03:29 +08:00
}
if rec.TlsaMatchingType > 2 {
errs = append(errs, fmt.Errorf("TLSA MatchingType %d is invalid in record %s (domain %s)",
rec.TlsaMatchingType, rec.GetLabel(), domain.Name))
2017-09-15 21:03:29 +08:00
}
2016-08-23 08:31:50 +08:00
}
2017-09-15 21:03:29 +08:00
2016-08-23 08:31:50 +08:00
// Populate FQDN:
rec.SetLabel(rec.GetLabel(), domain.Name)
2016-08-23 08:31:50 +08:00
}
}
2017-09-30 03:30:36 +08:00
// SPF flattening
if ers := flattenSPFs(config); len(ers) > 0 {
errs = append(errs, ers...)
}
// Process IMPORT_TRANSFORM
2016-08-23 08:31:50 +08:00
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
}
c := config.FindDomain(rec.GetTargetField())
if c == nil {
err = fmt.Errorf("IMPORT_TRANSFORM mentions non-existant domain %q", rec.GetTargetField())
errs = append(errs, err)
}
err = importTransform(c, domain, table, rec.TTL)
2016-08-23 08:31:50 +08:00
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)
}
}
for _, d := range config.Domains {
// Check that CNAMES don't have to co-exist with any other records
errs = append(errs, checkCNAMEs(d)...)
// Check that if any advanced record types are used in a domain, every provider for that domain supports them
err := checkProviderCapabilities(d)
if err != nil {
2017-05-03 23:56:08 +08:00
errs = append(errs, err)
}
// Check for duplicates
errs = append(errs, checkDuplicates(d.Records)...)
// Validate FQDN consistency
for _, r := range d.Records {
if r.NameFQDN == "" || !strings.HasSuffix(r.NameFQDN, d.Name) {
errs = append(errs, fmt.Errorf("record named '%s' does not have correct FQDN for domain '%s'. FQDN: %s", r.Name, d.Name, r.NameFQDN))
}
}
// Verify AutoDNSSEC is valid.
errs = append(errs, checkAutoDNSSEC(d)...)
}
// At this point we've munged anything that needs to be munged, and
// validated anything that can be globally validated.
// Let's ask // the provider if there are any records they can't handle.
for _, domain := range config.Domains { // For each domain..
for _, provider := range domain.DNSProviderInstances { // For each provider...
if err := providers.AuditRecords(provider.ProviderBase.ProviderType, domain.Records); err != nil {
errs = append(errs, err)
}
}
}
2016-08-23 08:31:50 +08:00
return errs
}
// UpdateNameSplitHorizon fills in the split horizon fields.
func UpdateNameSplitHorizon(dc *models.DomainConfig) {
if dc.UniqueName == "" {
dc.UniqueName = dc.Name
}
if dc.Tag == "" {
l := strings.SplitN(dc.Name, "!", 2)
if len(l) == 2 {
dc.Name = l[0]
dc.Tag = l[1]
}
}
}
// processSplitHorizonDomains finds "domain.tld!tag" domains and pre-processes them.
func processSplitHorizonDomains(config *models.DNSConfig) error {
// Parse out names and tags.
for _, d := range config.Domains {
UpdateNameSplitHorizon(d)
}
// Verify uniquenames are unique
seen := map[string]bool{}
for _, d := range config.Domains {
if seen[d.UniqueName] {
return fmt.Errorf("duplicate domain name: %q", d.UniqueName)
}
seen[d.UniqueName] = true
}
return nil
}
// parseDomainSpec parses "domain.tld!tag" into its component parts.
func parseDomainSpec(s string) (domain, tag string) {
l := strings.SplitN(s, "!", 2)
if len(l) == 2 {
return l[0], l[1]
}
return l[0], ""
}
func checkAutoDNSSEC(dc *models.DomainConfig) (errs []error) {
if dc.AutoDNSSEC != "" && dc.AutoDNSSEC != "on" && dc.AutoDNSSEC != "off" {
errs = append(errs, fmt.Errorf("Domain %q AutoDNSSEC=%q is invalid (expecting \"\", \"off\", or \"on\")", dc.Name, dc.AutoDNSSEC))
}
return
}
func checkCNAMEs(dc *models.DomainConfig) (errs []error) {
cnames := map[string]bool{}
for _, r := range dc.Records {
if r.Type == "CNAME" {
if cnames[r.GetLabel()] {
errs = append(errs, fmt.Errorf("cannot have multiple CNAMEs with same name: %s", r.GetLabelFQDN()))
}
cnames[r.GetLabel()] = true
}
}
for _, r := range dc.Records {
if cnames[r.GetLabel()] && r.Type != "CNAME" {
errs = append(errs, fmt.Errorf("cannot have CNAME and %s record with same name: %s", r.Type, r.GetLabelFQDN()))
}
}
return
}
func checkDuplicates(records []*models.RecordConfig) (errs []error) {
seen := map[string]*models.RecordConfig{}
for _, r := range records {
diffable := fmt.Sprintf("%s %s %s", r.GetLabelFQDN(), r.Type, r.ToDiffable())
if seen[diffable] != nil {
errs = append(errs, fmt.Errorf("exact duplicate record found: %s", diffable))
}
seen[diffable] = r
}
return errs
}
Tests: ensure provider capabilities are checked (#650) * Tests: ensure provider capabilities are checked Adds test: `TestCapabilitiesAreFiltered` We have a number of records and pseudo-records which in theory can only be used with a given provider if that provider indicates support. In practice, we've been missing the checks for that support and have been passing the records down anyway. The advice comment in the providers/capabilities.go file to edit `checkProviderCapabilities()` has not been reliably followed. We need an internal self-consistency test. The constants are not directly exported or enumerable based solely on the package interfaces at run-time, but with source access for a test suite, we can use the `go/ast` and related interfaces to examine the code, extract all the constants from a given package, figure out which ones we want to be handled, and then insist that they're handled. Before my recent work, we only checked: ALIAS PTR SRV CAA TLSA After this commit, we check: ALIAS AUTODNSSEC CAA NAPTR PTR R53_ALIAS SSHFP SRV TLSA I've added `AUTODNSSEC` as a new feature; `SSHFP` and `PTR` were caught in other recent commits from me; implementing this test caused me to have to add `NAPTR` and `R53_ALIAS`. I whitelist `CanUseTXTMulti` as a special-case. This should prevent regressions. We will probably want to post publicly to warn people that if they're using SSHFP/PTR/NAPTR/R53_ALIAS then they should check the feature matrix and if they don't see their provider listed, to report is as "hey that actually works" so we can update the provider flags. Bonus: our feature matrix will suddenly be more accurate. * Add comments/docs for capabilities authors * fixup! * fixup!
2020-02-25 20:22:32 +08:00
// We pull this out of checkProviderCapabilities() so that it's visible within
// the package elsewhere, so that our test suite can look at the list of
// capabilities we're checking and make sure that it's up-to-date.
var providerCapabilityChecks = []pairTypeCapability{
// If a zone uses rType X, the provider must support capability Y.
//{"X", providers.Y},
capabilityCheck("ALIAS", providers.CanUseAlias),
capabilityCheck("AUTODNSSEC", providers.CanAutoDNSSEC),
capabilityCheck("CAA", providers.CanUseCAA),
capabilityCheck("NAPTR", providers.CanUseNAPTR),
capabilityCheck("PTR", providers.CanUsePTR),
capabilityCheck("R53_ALIAS", providers.CanUseRoute53Alias),
capabilityCheck("SSHFP", providers.CanUseSSHFP),
capabilityCheck("SRV", providers.CanUseSRV),
capabilityCheck("TLSA", providers.CanUseTLSA),
capabilityCheck("AZURE_ALIAS", providers.CanUseAzureAlias),
// DS needs special record-level checks
{
rType: "DS",
caps: []providers.Capability{providers.CanUseDS, providers.CanUseDSForChildren},
checkFunc: checkProviderDS,
},
}
Tests: ensure provider capabilities are checked (#650) * Tests: ensure provider capabilities are checked Adds test: `TestCapabilitiesAreFiltered` We have a number of records and pseudo-records which in theory can only be used with a given provider if that provider indicates support. In practice, we've been missing the checks for that support and have been passing the records down anyway. The advice comment in the providers/capabilities.go file to edit `checkProviderCapabilities()` has not been reliably followed. We need an internal self-consistency test. The constants are not directly exported or enumerable based solely on the package interfaces at run-time, but with source access for a test suite, we can use the `go/ast` and related interfaces to examine the code, extract all the constants from a given package, figure out which ones we want to be handled, and then insist that they're handled. Before my recent work, we only checked: ALIAS PTR SRV CAA TLSA After this commit, we check: ALIAS AUTODNSSEC CAA NAPTR PTR R53_ALIAS SSHFP SRV TLSA I've added `AUTODNSSEC` as a new feature; `SSHFP` and `PTR` were caught in other recent commits from me; implementing this test caused me to have to add `NAPTR` and `R53_ALIAS`. I whitelist `CanUseTXTMulti` as a special-case. This should prevent regressions. We will probably want to post publicly to warn people that if they're using SSHFP/PTR/NAPTR/R53_ALIAS then they should check the feature matrix and if they don't see their provider listed, to report is as "hey that actually works" so we can update the provider flags. Bonus: our feature matrix will suddenly be more accurate. * Add comments/docs for capabilities authors * fixup! * fixup!
2020-02-25 20:22:32 +08:00
type pairTypeCapability struct {
rType string
// Capabilities the provider must implement if any records of type rType are found
// in the zonefile. This is a disjunction - implementing at least one of the listed
// capabilities is sufficient.
caps []providers.Capability
// checkFunc provides additional checks of each provider. This function should be
// called if records of type rType are found in the zonefile.
checkFunc func(pType string, _ models.Records) error
}
func capabilityCheck(rType string, caps ...providers.Capability) pairTypeCapability {
return pairTypeCapability{
rType: rType,
caps: caps,
}
}
func providerHasAtLeastOneCapability(pType string, caps ...providers.Capability) bool {
for _, cap := range caps {
if providers.ProviderHasCapability(pType, cap) {
return true
}
}
return false
Tests: ensure provider capabilities are checked (#650) * Tests: ensure provider capabilities are checked Adds test: `TestCapabilitiesAreFiltered` We have a number of records and pseudo-records which in theory can only be used with a given provider if that provider indicates support. In practice, we've been missing the checks for that support and have been passing the records down anyway. The advice comment in the providers/capabilities.go file to edit `checkProviderCapabilities()` has not been reliably followed. We need an internal self-consistency test. The constants are not directly exported or enumerable based solely on the package interfaces at run-time, but with source access for a test suite, we can use the `go/ast` and related interfaces to examine the code, extract all the constants from a given package, figure out which ones we want to be handled, and then insist that they're handled. Before my recent work, we only checked: ALIAS PTR SRV CAA TLSA After this commit, we check: ALIAS AUTODNSSEC CAA NAPTR PTR R53_ALIAS SSHFP SRV TLSA I've added `AUTODNSSEC` as a new feature; `SSHFP` and `PTR` were caught in other recent commits from me; implementing this test caused me to have to add `NAPTR` and `R53_ALIAS`. I whitelist `CanUseTXTMulti` as a special-case. This should prevent regressions. We will probably want to post publicly to warn people that if they're using SSHFP/PTR/NAPTR/R53_ALIAS then they should check the feature matrix and if they don't see their provider listed, to report is as "hey that actually works" so we can update the provider flags. Bonus: our feature matrix will suddenly be more accurate. * Add comments/docs for capabilities authors * fixup! * fixup!
2020-02-25 20:22:32 +08:00
}
func checkProviderDS(pType string, records models.Records) error {
switch {
case providers.ProviderHasCapability(pType, providers.CanUseDS):
// The provider can use DS records anywhere, including at the root
return nil
case !providers.ProviderHasCapability(pType, providers.CanUseDSForChildren):
// Provider has no support for DS records
return fmt.Errorf("provider %s uses DS records but does not support them", pType)
default:
// Provider supports DS records but not at the root
for _, record := range records {
if record.Type == "DS" && record.Name == "@" {
return fmt.Errorf(
"provider %s only supports child DS records, but zone had a record at the root (@)",
pType,
)
}
}
}
return nil
Tests: ensure provider capabilities are checked (#650) * Tests: ensure provider capabilities are checked Adds test: `TestCapabilitiesAreFiltered` We have a number of records and pseudo-records which in theory can only be used with a given provider if that provider indicates support. In practice, we've been missing the checks for that support and have been passing the records down anyway. The advice comment in the providers/capabilities.go file to edit `checkProviderCapabilities()` has not been reliably followed. We need an internal self-consistency test. The constants are not directly exported or enumerable based solely on the package interfaces at run-time, but with source access for a test suite, we can use the `go/ast` and related interfaces to examine the code, extract all the constants from a given package, figure out which ones we want to be handled, and then insist that they're handled. Before my recent work, we only checked: ALIAS PTR SRV CAA TLSA After this commit, we check: ALIAS AUTODNSSEC CAA NAPTR PTR R53_ALIAS SSHFP SRV TLSA I've added `AUTODNSSEC` as a new feature; `SSHFP` and `PTR` were caught in other recent commits from me; implementing this test caused me to have to add `NAPTR` and `R53_ALIAS`. I whitelist `CanUseTXTMulti` as a special-case. This should prevent regressions. We will probably want to post publicly to warn people that if they're using SSHFP/PTR/NAPTR/R53_ALIAS then they should check the feature matrix and if they don't see their provider listed, to report is as "hey that actually works" so we can update the provider flags. Bonus: our feature matrix will suddenly be more accurate. * Add comments/docs for capabilities authors * fixup! * fixup!
2020-02-25 20:22:32 +08:00
}
func checkProviderCapabilities(dc *models.DomainConfig) error {
// Check if the zone uses a capability that the provider doesn't
// support.
for _, ty := range providerCapabilityChecks {
2017-07-06 22:24:21 +08:00
hasAny := false
switch ty.rType {
case "AUTODNSSEC":
if dc.AutoDNSSEC != "" {
2017-07-06 22:24:21 +08:00
hasAny = true
}
default:
for _, r := range dc.Records {
if r.Type == ty.rType {
hasAny = true
break
}
}
}
2017-07-06 22:24:21 +08:00
if !hasAny {
continue
}
for _, provider := range dc.DNSProviderInstances {
// fmt.Printf(" (checking if %q can %q for domain %q)\n", provider.ProviderType, ty.rType, dc.Name)
if !providerHasAtLeastOneCapability(provider.ProviderType, ty.caps...) {
return fmt.Errorf("domain %s uses %s records, but DNS provider type %s does not support them", dc.Name, ty.rType, provider.ProviderType)
2017-07-06 22:24:21 +08:00
}
if ty.checkFunc != nil {
checkErr := ty.checkFunc(provider.ProviderType, dc.Records)
if checkErr != nil {
return fmt.Errorf("while checking %s records in domain %s: %w", ty.rType, dc.Name, checkErr)
}
}
2017-07-06 22:24:21 +08:00
}
}
return nil
}
2016-08-23 08:31:50 +08:00
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.GetTargetField()) // ip already validated above
newIPs, err := transform.IPToList(net.ParseIP(rec.GetTargetField()), table)
2016-08-23 08:31:50 +08:00
if err != nil {
return err
}
for i, newIP := range newIPs {
if i == 0 && !newIP.Equal(ip) {
rec.SetTarget(newIP.String()) // replace target of first record if different
2016-08-23 08:31:50 +08:00
} 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.SetTarget(newIP.String())
2016-08-23 08:31:50 +08:00
domain.Records = append(domain.Records, copy)
}
}
}
return nil
}