2016-08-22 18:31:50 -06:00
package normalize
import (
2018-04-26 14:45:10 -04:00
"fmt"
2016-08-22 18:31:50 -06:00
"net"
2023-02-15 10:00:02 -05:00
"sort"
2016-08-22 18:31:50 -06:00
"strings"
2020-04-14 16:47:30 -04:00
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/transform"
"github.com/StackExchange/dnscontrol/v3/providers"
2016-09-27 12:28:09 -06:00
"github.com/miekg/dns"
"github.com/miekg/dns/dnsutil"
2023-02-15 10:00:02 -05:00
"golang.org/x/exp/slices"
2016-08-22 18:31:50 -06:00
)
// Returns false if target does not validate.
2017-03-20 14:20:02 -06:00
func checkIPv4 ( label string ) error {
2016-08-22 18:31:50 -06:00
if net . ParseIP ( label ) . To4 ( ) == nil {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
return fmt . Errorf ( "WARNING: target (%v) is not an IPv4 address" , label )
2016-08-22 18:31:50 -06:00
}
return nil
}
// Returns false if target does not validate.
2017-03-20 14:20:02 -06:00
func checkIPv6 ( label string ) error {
2016-08-22 18:31:50 -06:00
if net . ParseIP ( label ) . To16 ( ) == nil {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
return fmt . Errorf ( "WARNING: target (%v) is not an IPv6 address" , label )
2016-08-22 18:31:50 -06:00
}
return nil
}
2017-03-20 14:20:02 -06:00
// make sure target is valid reference for cnames, mx, etc.
func checkTarget ( target string ) error {
2016-12-16 13:10:27 -07:00
if target == "@" {
2016-08-22 18:31:50 -06:00
return nil
}
2020-09-20 10:41:42 -04:00
if target == "" {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
return fmt . Errorf ( "empty target" )
2016-08-22 18:31:50 -06:00
}
2022-01-03 10:24:33 -05:00
if strings . ContainsAny ( target , ` '" +,|!£$%&()=?^*ç°§;:<>[]()@ ` ) {
return fmt . Errorf ( "target (%v) includes invalid char" , target )
}
if ! strings . HasSuffix ( target , ".in-addr.arpa." ) && strings . Contains ( target , "/" ) {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
return fmt . Errorf ( "target (%v) includes invalid char" , target )
2017-06-11 09:30:12 -04:00
}
2020-07-06 20:18:24 -04:00
// If it contains a ".", it must end in a ".".
2016-12-16 13:10:27 -07:00
if strings . ContainsRune ( target , '.' ) && target [ len ( target ) - 1 ] != '.' {
2023-01-29 19:14:22 +01:00
return fmt . Errorf ( "target (%v) must end with a (.) [https://docs.dnscontrol.org/language-reference/why-the-dot]" , target )
2016-08-22 18:31:50 -06: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.
2017-05-19 14:15:57 -04:00
func validateRecordTypes ( rec * models . RecordConfig , domain string , pTypes [ ] string ) error {
2017-03-20 14:20:02 -06:00
var validTypes = map [ string ] bool {
2016-08-22 18:31:50 -06:00
"A" : true ,
"AAAA" : true ,
2022-03-02 11:19:15 -05:00
"ALIAS" : false ,
2017-07-25 14:59:40 -04:00
"CAA" : true ,
2022-03-02 11:19:15 -05:00
"CNAME" : true ,
2020-05-30 10:40:21 -04:00
"DS" : true ,
2016-08-22 18:31:50 -06:00
"IMPORT_TRANSFORM" : false ,
2023-03-16 19:04:20 +01:00
"LOC" : true ,
2016-08-22 18:31:50 -06:00
"MX" : true ,
2022-03-02 11:19:15 -05:00
"NAPTR" : true ,
"NS" : true ,
"PTR" : true ,
2021-05-04 21:47:26 +02:00
"SOA" : true ,
2017-07-19 15:53:40 -04:00
"SRV" : true ,
2019-01-28 23:26:20 +01:00
"SSHFP" : true ,
2022-03-02 11:19:15 -05:00
"TLSA" : true ,
2016-08-22 18:31:50 -06:00
"TXT" : true ,
}
2017-05-19 14:15:57 -04:00
_ , ok := validTypes [ rec . Type ]
if ! ok {
cType := providers . GetCustomRecordType ( rec . Type )
if cType == nil {
2020-08-30 19:52:37 -04:00
return fmt . Errorf ( "unsupported record type (%v) domain=%v name=%v" , rec . Type , domain , rec . GetLabel ( ) )
2017-05-19 14:15:57 -04:00
}
for _ , providerType := range pTypes {
if providerType != cType . Provider {
2020-08-30 19:52:37 -04:00
return fmt . Errorf ( "custom record type %s is not compatible with provider type %s" , rec . Type , providerType )
2017-05-19 14:15:57 -04:00
}
}
2018-01-09 12:53:16 -05:00
// it is ok. Lets replace the type with real type and add metadata to say we checked it
2017-05-19 14:15:57 -04:00
rec . Metadata [ "orig_custom_type" ] = rec . Type
if cType . RealType != "" {
rec . Type = cType . RealType
}
2016-08-22 18:31:50 -06:00
}
return nil
}
2022-04-04 14:05:49 -04:00
func errorRepeat ( label , domain string ) string {
shortname := strings . TrimSuffix ( label , "." + domain )
return fmt . Sprintf (
` The name "%s.%s." is an error (repeats the domain). Maybe instead of "%s" you intended "%s"? If not add DISABLE_REPEATED_DOMAIN_CHECK to this record to permit this as-is. ` ,
label , domain ,
label ,
shortname ,
)
}
2020-02-27 23:10:35 -05:00
func checkLabel ( label string , rType string , target , domain string , meta map [ string ] string ) error {
2017-03-20 14:20:02 -06:00
if label == "@" {
return nil
}
2020-09-20 10:41:42 -04:00
if label == "" {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
return fmt . Errorf ( "empty %s label in %s" , rType , domain )
2017-03-20 14:20:02 -06:00
}
if label [ len ( label ) - 1 ] == '.' {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
return fmt . Errorf ( "label %s.%s ends with a (.)" , label , domain )
2017-03-20 14:20:02 -06:00
}
2020-12-03 08:33:37 -05:00
if label == domain || strings . HasSuffix ( label , "." + domain ) {
2017-11-14 23:13:50 -05:00
if m := meta [ "skip_fqdn_check" ] ; m != "true" {
2022-04-04 14:05:49 -04:00
return fmt . Errorf ( errorRepeat ( label , domain ) )
2017-11-14 23:13:50 -05:00
}
}
2019-03-04 12:11:25 -05:00
// 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:
2021-03-07 13:19:22 -05:00
for _ , ex := range [ ] string { "SRV" , "TLSA" , "TXT" } {
2017-09-15 09:03:29 -04:00
if rType == ex {
return nil
2017-03-20 14:20:02 -06:00
}
2017-09-15 09:03:29 -04:00
}
2020-08-30 19:35:07 -05:00
// Don't warn for records that start with _
// See https://github.com/StackExchange/dnscontrol/issues/829
2023-01-28 11:10:02 -05:00
if strings . HasPrefix ( label , "_" ) || strings . Contains ( label , "._" ) || strings . HasPrefix ( label , "sql-" ) {
2020-02-27 23:10:35 -05:00
return nil
}
2020-08-30 19:35:07 -05:00
2019-03-04 12:11:25 -05:00
// Otherwise, warn.
2017-09-15 09:03:29 -04:00
if strings . ContainsRune ( label , '_' ) {
2023-01-28 11:10:02 -05:00
return Warning { fmt . Errorf ( "label %s.%s contains \"_\" (can't be used in a URL)" , label , domain ) }
2017-09-15 09:03:29 -04:00
}
2017-11-14 23:13:50 -05:00
2017-03-20 14:20:02 -06:00
return nil
}
2021-05-04 21:47:26 +02:00
func checkSoa ( expire uint32 , minttl uint32 , refresh uint32 , retry uint32 , serial uint32 , mbox string ) error {
if expire <= 0 {
return fmt . Errorf ( "SOA Expire must be > 0" )
}
if minttl <= 0 {
return fmt . Errorf ( "SOA Minimum TTL must be > 0" )
}
if refresh <= 0 {
return fmt . Errorf ( "SOA Refresh must be > 0" )
}
if retry <= 0 {
return fmt . Errorf ( "SOA Retry must be > 0" )
}
if mbox == "" {
return fmt . Errorf ( "SOA MBox must be specified" )
}
if strings . ContainsRune ( mbox , '@' ) {
return fmt . Errorf ( "SOA MBox must have '.' instead of '@'" )
}
return nil
}
2017-03-20 14:20:02 -06:00
// checkTargets returns true if rec.Target is valid for the rec.Type.
func checkTargets ( rec * models . RecordConfig , domain string ) ( errs [ ] error ) {
2018-03-19 17:18:58 -04:00
label := rec . GetLabel ( )
target := rec . GetTargetField ( )
2016-08-22 18:31:50 -06:00
check := func ( e error ) {
if e != nil {
2020-08-30 19:52:37 -04:00
err := fmt . Errorf ( "in %s %s.%s: %s" , rec . Type , rec . GetLabel ( ) , domain , e . Error ( ) )
2017-03-20 14:20:02 -06:00
if _ , ok := e . ( Warning ) ; ok {
err = Warning { err }
}
errs = append ( errs , err )
2016-08-22 18:31:50 -06:00
}
}
2017-08-04 12:26:29 -07:00
switch rec . Type { // #rtype_variations
2016-08-22 18:31:50 -06:00
case "A" :
2017-03-20 14:20:02 -06:00
check ( checkIPv4 ( target ) )
2016-08-22 18:31:50 -06:00
case "AAAA" :
2017-03-20 14:20:02 -06:00
check ( checkIPv6 ( target ) )
2022-03-02 11:19:15 -05:00
case "ALIAS" :
check ( checkTarget ( target ) )
2016-08-22 18:31:50 -06:00
case "CNAME" :
2017-03-20 14:20:02 -06:00
check ( checkTarget ( target ) )
2017-04-19 13:13:28 -06:00
if label == "@" {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
check ( fmt . Errorf ( "cannot create CNAME record for bare domain" ) )
2017-04-19 13:13:28 -06:00
}
2023-03-01 10:15:41 -05:00
labelFQDN := dnsutil . AddOrigin ( label , domain )
targetFQDN := dnsutil . AddOrigin ( target , domain )
if labelFQDN == targetFQDN {
check ( fmt . Errorf ( "CNAME loop (target points at itself)" ) )
}
2023-03-16 19:04:20 +01:00
case "LOC" :
2016-08-22 18:31:50 -06:00
case "MX" :
2017-03-20 14:20:02 -06:00
check ( checkTarget ( target ) )
2022-03-02 11:19:15 -05:00
case "NAPTR" :
if target != "" {
check ( checkTarget ( target ) )
}
2016-08-22 18:31:50 -06:00
case "NS" :
2017-03-20 14:20:02 -06:00
check ( checkTarget ( target ) )
2016-12-16 13:10:27 -07:00
if label == "@" {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
check ( fmt . Errorf ( "cannot create NS record for bare domain. Use NAMESERVER instead" ) )
2016-12-16 13:10:27 -07:00
}
2023-01-30 04:01:41 +01:00
case "URLFWD" :
if len ( strings . Fields ( target ) ) != 5 {
check ( fmt . Errorf ( "record should follow format: \"from to redirectType pathForwardingMode queryForwarding\"" ) )
}
2017-07-06 10:18:15 -04:00
case "PTR" :
check ( checkTarget ( target ) )
2020-02-23 13:58:49 -05:00
case "SOA" :
2021-05-04 21:47:26 +02:00
check ( checkSoa ( rec . SoaExpire , rec . SoaMinttl , rec . SoaRefresh , rec . SoaRetry , rec . SoaSerial , rec . SoaMbox ) )
2020-02-23 13:58:49 -05:00
check ( checkTarget ( target ) )
2021-05-04 21:47:26 +02:00
if label != "@" {
2021-12-14 09:47:32 -05:00
check ( fmt . Errorf ( "SOA record is only valid for bare domain" ) )
2021-05-04 21:47:26 +02:00
}
2017-07-19 15:53:40 -04:00
case "SRV" :
check ( checkTarget ( target ) )
2020-05-26 13:46:10 +02:00
case "TXT" , "IMPORT_TRANSFORM" , "CAA" , "SSHFP" , "TLSA" , "DS" :
2016-08-22 18:31:50 -06:00
default :
2017-05-19 14:15:57 -04:00
if rec . Metadata [ "orig_custom_type" ] != "" {
2018-01-09 12:53:16 -05:00
// it is a valid custom type. We perform no validation on target
2017-05-19 14:15:57 -04:00
return
}
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
errs = append ( errs , fmt . Errorf ( "checkTargets: Unimplemented record type (%v) domain=%v name=%v" ,
2018-03-19 17:18:58 -04:00
rec . Type , domain , rec . GetLabel ( ) ) )
2016-08-22 18:31:50 -06:00
}
return
}
2017-03-20 14:20:02 -06:00
func transformCNAME ( target , oldDomain , newDomain string ) string {
// Canonicalize. If it isn't a FQDN, add the newDomain.
result := dnsutil . AddOrigin ( target , oldDomain )
2016-08-22 18:31:50 -06:00
if dns . IsFqdn ( result ) {
result = result [ : len ( result ) - 1 ]
}
2017-03-20 14:20:02 -06:00
return dnsutil . AddOrigin ( result , newDomain ) + "."
2016-08-22 18:31:50 -06:00
}
// import_transform imports the records of one zone into another, modifying records along the way.
2020-06-18 09:37:57 -04:00
func importTransform ( srcDomain , dstDomain * models . DomainConfig , transforms [ ] transform . IPConversion , ttl uint32 ) error {
2017-03-20 14:20:02 -06:00
// Read srcDomain.Records, transform, and append to dstDomain.Records:
2016-08-22 18:31:50 -06:00
// 1. Skip any that aren't A or CNAMEs.
2017-03-20 14:20:02 -06:00
// 2. Append destDomainname to the end of the label.
// 3. For CNAMEs, append destDomainname to the end of the target.
2016-08-22 18:31:50 -06:00
// 4. For As, change the target as described the transforms.
2017-03-20 14:20:02 -06:00
for _ , rec := range srcDomain . Records {
2020-02-18 08:59:18 -05:00
if dstDomain . Records . HasRecordTypeName ( rec . Type , rec . GetLabelFQDN ( ) ) {
2017-02-07 12:42:11 -07:00
continue
}
2016-09-27 12:28:09 -06:00
newRec := func ( ) * models . RecordConfig {
rec2 , _ := rec . Copy ( )
2018-03-19 17:18:58 -04:00
newlabel := rec2 . GetLabelFQDN ( )
2018-04-26 14:45:10 -04:00
rec2 . SetLabel ( newlabel , dstDomain . Name )
2016-09-28 12:45:59 -06:00
if ttl != 0 {
rec2 . TTL = ttl
}
2016-09-27 12:28:09 -06:00
return rec2
}
2017-08-04 12:26:29 -07:00
switch rec . Type { // #rtype_variations
2016-08-22 18:31:50 -06:00
case "A" :
2020-06-18 09:37:57 -04:00
trs , err := transform . IPToList ( net . ParseIP ( rec . GetTargetField ( ) ) , transforms )
2016-08-22 18:31:50 -06:00
if err != nil {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
return fmt . Errorf ( "import_transform: TransformIP(%v, %v) returned err=%s" , rec . GetTargetField ( ) , transforms , err )
2016-09-27 12:28:09 -06:00
}
for _ , tr := range trs {
r := newRec ( )
2018-03-19 17:18:58 -04:00
r . SetTarget ( tr . String ( ) )
2017-03-20 14:20:02 -06:00
dstDomain . Records = append ( dstDomain . Records , r )
2016-08-22 18:31:50 -06:00
}
case "CNAME" :
2016-09-27 12:28:09 -06:00
r := newRec ( )
2018-03-19 17:18:58 -04:00
r . SetTarget ( transformCNAME ( r . GetTargetField ( ) , srcDomain . Name , dstDomain . Name ) )
2017-03-20 14:20:02 -06:00
dstDomain . Records = append ( dstDomain . Records , r )
2021-06-22 10:24:49 -04:00
case "AKAMAICDN" , "MX" , "NAPTR" , "NS" , "SOA" , "SRV" , "TXT" , "CAA" , "TLSA" :
2016-08-22 18:31:50 -06:00
// Not imported.
continue
2023-03-16 19:04:20 +01:00
case "LOC" :
continue
2016-08-22 18:31:50 -06:00
default :
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
return fmt . Errorf ( "import_transform: Unimplemented record type %v (%v)" ,
2018-03-19 17:18:58 -04:00
rec . Type , rec . GetLabel ( ) )
2016-08-22 18:31:50 -06: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 : ] ... )
}
}
}
2017-03-20 14:20:02 -06:00
// 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
}
2020-03-10 16:53:17 -04:00
// ValidateAndNormalizeConfig performs and normalization and/or validation of the IR.
func ValidateAndNormalizeConfig ( config * models . DNSConfig ) ( errs [ ] error ) {
2021-02-05 12:12:45 -05:00
err := processSplitHorizonDomains ( config )
if err != nil {
return [ ] error { err }
}
2017-03-20 14:20:02 -06:00
for _ , domain := range config . Domains {
2017-05-19 14:15:57 -04:00
pTypes := [ ] string { }
2018-02-01 11:45:53 -05:00
for _ , provider := range domain . DNSProviderInstances {
pType := provider . ProviderType
2022-05-29 12:14:17 -04:00
if pType == "-" {
// "-" indicates that we don't yet know who the provider type
// is. This is probably due to the fact that `dnscontrol
// check` doesn't read creds.json, which is where the TYPE is
// set. We will skip this test in this instance. Later if
// `dnscontrol preview` or `push` is used, the full check will
// be performed.
continue
}
2018-01-09 12:53:16 -05:00
// If NO_PURGE is in use, make sure this *isn't* a provider that *doesn't* support NO_PURGE.
2020-01-12 11:24:10 -05:00
if domain . KeepUnknown && providers . ProviderHasCapability ( pType , providers . CantUseNOPURGE ) {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
errs = append ( errs , fmt . Errorf ( "%s uses NO_PURGE which is not supported by %s(%s)" , domain . Name , provider . Name , pType ) )
2017-08-11 12:43:06 -07:00
}
2017-05-19 14:15:57 -04:00
}
2016-08-22 18:31:50 -06:00
// Normalize Nameservers.
for _ , ns := range domain . Nameservers {
2020-03-01 10:33:24 -05:00
// NB(tlim): Like any target, NAMESERVER() is input by the user
2020-09-20 10:41:42 -04:00
// 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-22 18:31:50 -06:00
}
2020-03-01 10:33:24 -05:00
2016-08-22 18:31:50 -06:00
// Normalize Records.
2018-01-04 19:19:35 -05:00
models . PostProcessRecords ( domain . Records )
2016-08-22 18:31:50 -06:00
for _ , rec := range domain . Records {
2023-01-30 04:01:41 +01:00
2017-01-11 12:38:07 -07:00
if rec . TTL == 0 {
rec . TTL = models . DefaultTTL
}
2021-02-05 11:58:17 -05:00
// 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.
}
2020-12-03 08:33:37 -05:00
// in-addr.arpa magic
if strings . HasSuffix ( domain . Name , ".in-addr.arpa" ) || strings . HasSuffix ( domain . Name , ".ip6.arpa" ) {
label := rec . GetLabel ( )
2021-02-05 11:58:17 -05:00
if strings . HasSuffix ( label , "." + domain . Name ) {
2020-12-03 08:33:37 -05:00
rec . SetLabel ( label [ 0 : ( len ( label ) - len ( "." + domain . Name ) ) ] , domain . Name )
}
}
2021-02-05 11:58:17 -05:00
2016-08-22 18:31:50 -06:00
// Validate the unmodified inputs:
2017-05-19 14:15:57 -04:00
if err := validateRecordTypes ( rec , domain . Name , pTypes ) ; err != nil {
2016-08-22 18:31:50 -06:00
errs = append ( errs , err )
}
2020-02-27 23:10:35 -05:00
if err := checkLabel ( rec . GetLabel ( ) , rec . Type , rec . GetTargetField ( ) , domain . Name , rec . Metadata ) ; err != nil {
2017-03-20 14:20:02 -06:00
errs = append ( errs , err )
}
2023-01-30 04:01:41 +01:00
2017-03-20 14:20:02 -06:00
if errs2 := checkTargets ( rec , domain . Name ) ; errs2 != nil {
2016-08-22 18:31:50 -06:00
errs = append ( errs , errs2 ... )
}
// Canonicalize Targets.
2021-06-24 18:26:21 -04:00
if rec . Type == "CNAME" || rec . Type == "MX" || rec . Type == "NS" || rec . Type == "SRV" {
2018-12-07 16:30:04 -05:00
// #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.
2020-10-07 14:27:33 -04:00
origin := domain . Name + "."
2020-12-03 08:33:37 -05:00
if rec . SubDomain != "" {
2020-10-07 14:27:33 -04:00
origin = rec . SubDomain + "." + origin
}
rec . SetTarget ( dnsutil . AddOrigin ( rec . GetTargetField ( ) , origin ) )
2017-06-05 14:57:32 -04:00
} else if rec . Type == "A" || rec . Type == "AAAA" {
2018-03-19 17:18:58 -04:00
rec . SetTarget ( net . ParseIP ( rec . GetTargetField ( ) ) . String ( ) )
2017-07-07 13:59:29 -04:00
} else if rec . Type == "PTR" {
var err error
2018-03-19 17:18:58 -04:00
var name string
if name , err = transform . PtrNameMagic ( rec . GetLabel ( ) , domain . Name ) ; err != nil {
2017-07-07 13:59:29 -04:00
errs = append ( errs , err )
}
2018-03-19 17:18:58 -04:00
rec . SetLabel ( name , domain . Name )
2017-07-25 14:59:40 -04:00
} else if rec . Type == "CAA" {
if rec . CaaTag != "issue" && rec . CaaTag != "issuewild" && rec . CaaTag != "iodef" {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
errs = append ( errs , fmt . Errorf ( "CAA tag %s is invalid" , rec . CaaTag ) )
2017-07-25 14:59:40 -04:00
}
2017-09-15 09:03:29 -04:00
} else if rec . Type == "TLSA" {
2020-08-30 20:38:08 -04:00
if rec . TlsaUsage > 3 {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
errs = append ( errs , fmt . Errorf ( "TLSA Usage %d is invalid in record %s (domain %s)" ,
2018-03-19 17:18:58 -04:00
rec . TlsaUsage , rec . GetLabel ( ) , domain . Name ) )
2017-09-15 09:03:29 -04:00
}
2020-08-30 20:38:08 -04:00
if rec . TlsaSelector > 1 {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
errs = append ( errs , fmt . Errorf ( "TLSA Selector %d is invalid in record %s (domain %s)" ,
2018-03-19 17:18:58 -04:00
rec . TlsaSelector , rec . GetLabel ( ) , domain . Name ) )
2017-09-15 09:03:29 -04:00
}
2020-08-30 20:38:08 -04:00
if rec . TlsaMatchingType > 2 {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
errs = append ( errs , fmt . Errorf ( "TLSA MatchingType %d is invalid in record %s (domain %s)" ,
2018-03-19 17:18:58 -04:00
rec . TlsaMatchingType , rec . GetLabel ( ) , domain . Name ) )
2017-09-15 09:03:29 -04:00
}
2016-08-22 18:31:50 -06:00
}
2017-09-15 09:03:29 -04:00
2016-08-22 18:31:50 -06:00
// Populate FQDN:
2018-03-22 14:29:55 +01:00
rec . SetLabel ( rec . GetLabel ( ) , domain . Name )
2016-08-22 18:31:50 -06:00
}
}
2017-09-29 15:30:36 -04:00
// SPF flattening
if ers := flattenSPFs ( config ) ; len ( ers ) > 0 {
errs = append ( errs , ers ... )
}
// Process IMPORT_TRANSFORM
2016-08-22 18:31:50 -06: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
}
2020-12-03 09:33:39 -05:00
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-22 18:31:50 -06: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 )
}
}
2017-04-19 13:13:28 -06:00
2017-04-13 11:10:15 -06:00
for _ , d := range config . Domains {
2018-04-26 14:45:10 -04:00
// Check that CNAMES don't have to co-exist with any other records
2017-04-13 11:10:15 -06:00
errs = append ( errs , checkCNAMEs ( d ) ... )
2018-04-26 14:45:10 -04:00
// Check that if any advanced record types are used in a domain, every provider for that domain supports them
2018-02-01 11:45:53 -05:00
err := checkProviderCapabilities ( d )
2017-04-19 13:13:28 -06:00
if err != nil {
2017-05-03 09:56:08 -06:00
errs = append ( errs , err )
2017-04-19 13:13:28 -06:00
}
2019-04-22 15:41:39 -04:00
// Check for duplicates
errs = append ( errs , checkDuplicates ( d . Records ) ... )
2022-05-04 14:41:16 +02:00
// Check for different TTLs under the same label
errs = append ( errs , checkLabelHasMultipleTTLs ( d . Records ) ... )
2018-04-26 14:45:10 -04:00
// Validate FQDN consistency
for _ , r := range d . Records {
if r . NameFQDN == "" || ! strings . HasSuffix ( r . NameFQDN , d . Name ) {
2020-08-30 19:52:37 -04:00
errs = append ( errs , fmt . Errorf ( "record named '%s' does not have correct FQDN for domain '%s'. FQDN: %s" , r . Name , d . Name , r . NameFQDN ) )
2018-04-26 14:45:10 -04:00
}
}
2020-09-27 16:37:42 -04:00
// Verify AutoDNSSEC is valid.
errs = append ( errs , checkAutoDNSSEC ( d ) ... )
2017-04-19 13:13:28 -06:00
}
2021-03-07 13:19:22 -05:00
// At this point we've munged anything that needs to be munged, and
// validated anything that can be globally validated.
2022-05-08 14:23:45 -04:00
// Let's ask the provider if there are any records they can't handle.
2021-03-07 13:19:22 -05:00
for _ , domain := range config . Domains { // For each domain..
for _ , provider := range domain . DNSProviderInstances { // For each provider...
2022-05-23 13:27:53 -04:00
if provider . ProviderBase . ProviderType == "-" {
2022-05-29 12:14:17 -04:00
// "-" indicates that we don't yet know who the provider type
// is. This is probably due to the fact that `dnscontrol
// check` doesn't read creds.json, which is where the TYPE is
// set. We will skip this test in this instance. Later if
// `dnscontrol preview` or `push` is used, the full check will
// be performed.
2022-05-23 13:27:53 -04:00
continue
}
2022-08-11 17:24:47 -04:00
if es := providers . AuditRecords ( provider . ProviderBase . ProviderType , domain . Records ) ; len ( es ) != 0 {
for _ , e := range es {
errs = append ( errs , fmt . Errorf ( "%s rejects domain %s: %w" , provider . ProviderBase . ProviderType , domain . Name , e ) )
}
2021-03-07 13:19:22 -05:00
}
}
}
2016-08-22 18:31:50 -06:00
return errs
}
2021-02-05 12:12:45 -05:00
// 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
}
2022-01-27 15:58:56 -05:00
//// 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], ""
//}
2021-02-05 12:12:45 -05:00
2020-09-27 16:37:42 -04:00
func checkAutoDNSSEC ( dc * models . DomainConfig ) ( errs [ ] error ) {
2023-02-23 01:13:27 +05:30
if dc . AutoDNSSEC == "on" {
2023-02-27 20:28:17 -05:00
for providerName := range dc . DNSProviderNames {
2023-02-23 01:13:27 +05:30
if dc . RegistrarName != providerName {
2023-03-17 11:37:06 -04:00
errs = append ( errs , fmt . Errorf ( "AutoDNSSEC is enabled, but DNS provider %s does not match registrar %s" , providerName , dc . RegistrarName ) )
2023-02-23 01:13:27 +05:30
}
}
}
2020-09-27 16:37:42 -04:00
return
}
2017-04-13 11:10:15 -06:00
func checkCNAMEs ( dc * models . DomainConfig ) ( errs [ ] error ) {
cnames := map [ string ] bool { }
for _ , r := range dc . Records {
if r . Type == "CNAME" {
2018-03-19 17:18:58 -04:00
if cnames [ r . GetLabel ( ) ] {
2020-08-30 19:52:37 -04:00
errs = append ( errs , fmt . Errorf ( "cannot have multiple CNAMEs with same name: %s" , r . GetLabelFQDN ( ) ) )
2017-04-13 11:10:15 -06:00
}
2018-03-19 17:18:58 -04:00
cnames [ r . GetLabel ( ) ] = true
2017-04-13 11:10:15 -06:00
}
}
for _ , r := range dc . Records {
2018-03-19 17:18:58 -04:00
if cnames [ r . GetLabel ( ) ] && r . Type != "CNAME" {
2020-08-30 19:52:37 -04:00
errs = append ( errs , fmt . Errorf ( "cannot have CNAME and %s record with same name: %s" , r . Type , r . GetLabelFQDN ( ) ) )
2017-04-13 11:10:15 -06:00
}
}
return
}
2019-04-22 15:41:39 -04:00
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 {
2020-08-30 19:52:37 -04:00
errs = append ( errs , fmt . Errorf ( "exact duplicate record found: %s" , diffable ) )
2019-04-22 15:41:39 -04:00
}
seen [ diffable ] = r
}
return errs
}
2023-02-15 10:00:02 -05:00
// uniq returns the unique values in a map. The result is sorted lexigraphically.
func uniq ( s [ ] string ) [ ] string {
seen := make ( map [ string ] struct { } )
var result [ ] string
2022-05-04 14:41:16 +02:00
for _ , k := range s {
if _ , ok := seen [ k ] ; ! ok {
seen [ k ] = struct { } { }
result = append ( result , k )
}
}
2023-02-15 10:00:02 -05:00
sort . Strings ( result )
2022-05-04 14:41:16 +02:00
return result
}
func checkLabelHasMultipleTTLs ( records [ ] * models . RecordConfig ) ( errs [ ] error ) {
2023-02-15 10:00:02 -05:00
// The RFCs say that all records at a particular label should have
// the same TTL. Most providers don't care, and if they do the
// dnscontrol provider code usually picks the lowest TTL for all of them.
// General algorithm:
// gather all records at a particular label.
// has[label] -> ttl -> type(s)
// for each label, if there is more than one ttl, output ttl:A/TXT ttl:TXT/NS
// Find the inconsistencies:
m := make ( map [ string ] map [ uint32 ] map [ string ] bool )
2022-05-04 14:41:16 +02:00
for _ , r := range records {
2023-02-15 10:00:02 -05:00
label := r . GetLabelFQDN ( )
ttl := r . TTL
rtype := r . Type
2022-05-04 14:41:16 +02:00
2023-02-15 10:00:02 -05:00
if _ , ok := m [ label ] ; ! ok {
m [ label ] = make ( map [ uint32 ] map [ string ] bool )
}
if _ , ok := m [ label ] [ ttl ] ; ! ok {
m [ label ] [ ttl ] = make ( map [ string ] bool )
}
m [ label ] [ ttl ] [ rtype ] = true
}
labels := make ( [ ] string , len ( m ) )
i := 0
for k := range m {
labels [ i ] = k
i ++
}
sort . Strings ( labels )
slices . Compact ( labels )
// Less clear error message:
// for _, label := range labels {
// if len(m[label]) > 1 {
// result := ""
// for ttl, v := range m[label] {
// result += fmt.Sprintf(" %d:", ttl)
// rtypes := make([]string, len(v))
// i := 0
// for k := range v {
// rtypes[i] = k
// i++
// }
// result += strings.Join(rtypes, "/")
// }
// errs = append(errs, Warning{fmt.Errorf("inconsistent TTLs at %q:%v", label, result)})
// }
// }
// Invert for a more clear error message:
for _ , label := range labels {
if len ( m [ label ] ) > 1 {
r := make ( map [ string ] map [ uint32 ] bool )
for ttl , rtypes := range m [ label ] {
for rtype := range rtypes {
if _ , ok := r [ rtype ] ; ! ok {
r [ rtype ] = make ( map [ uint32 ] bool )
}
r [ rtype ] [ ttl ] = true
}
}
result := formatInconsistency ( r )
errs = append ( errs , Warning { fmt . Errorf ( "inconsistent TTLs at %q: %s" , label , result ) } )
}
2022-05-04 14:41:16 +02:00
}
2023-02-15 10:00:02 -05:00
return errs
}
func formatInconsistency ( r map [ string ] map [ uint32 ] bool ) string {
var rtypeResult [ ] string
for rtype , ttlsMap := range r {
ttlList := make ( [ ] int , len ( ttlsMap ) )
i := 0
for k := range ttlsMap {
ttlList [ i ] = int ( k )
i ++
2022-05-04 14:41:16 +02:00
}
2023-02-15 10:00:02 -05:00
sort . Ints ( ttlList )
rtypeResult = append ( rtypeResult , fmt . Sprintf ( "%s:%v" , rtype , commaSepInts ( ttlList ) ) )
2022-05-04 14:41:16 +02:00
}
2023-02-15 10:00:02 -05:00
sort . Strings ( rtypeResult )
return strings . Join ( rtypeResult , " " )
}
func commaSepInts ( list [ ] int ) string {
slist := make ( [ ] string , len ( list ) )
for i , v := range list {
slist [ i ] = fmt . Sprintf ( "%d" , v )
}
return strings . Join ( slist , "," )
2022-05-04 14:41:16 +02:00
}
2020-02-25 07:22:32 -05: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.
2020-06-18 22:24:13 +01:00
var providerCapabilityChecks = [ ] pairTypeCapability {
// If a zone uses rType X, the provider must support capability Y.
//{"X", providers.Y},
2022-03-02 11:19:15 -05:00
capabilityCheck ( "AKAMAICDN" , providers . CanUseAKAMAICDN ) ,
2020-06-18 22:24:13 +01:00
capabilityCheck ( "ALIAS" , providers . CanUseAlias ) ,
capabilityCheck ( "AUTODNSSEC" , providers . CanAutoDNSSEC ) ,
2022-03-02 11:19:15 -05:00
capabilityCheck ( "AZURE_ALIAS" , providers . CanUseAzureAlias ) ,
2020-06-18 22:24:13 +01:00
capabilityCheck ( "CAA" , providers . CanUseCAA ) ,
2023-03-16 19:04:20 +01:00
capabilityCheck ( "LOC" , providers . CanUseLOC ) ,
2020-06-18 22:24:13 +01:00
capabilityCheck ( "NAPTR" , providers . CanUseNAPTR ) ,
capabilityCheck ( "PTR" , providers . CanUsePTR ) ,
capabilityCheck ( "R53_ALIAS" , providers . CanUseRoute53Alias ) ,
2021-05-04 21:47:26 +02:00
capabilityCheck ( "SOA" , providers . CanUseSOA ) ,
2020-06-18 22:24:13 +01:00
capabilityCheck ( "SRV" , providers . CanUseSRV ) ,
2022-03-02 11:19:15 -05:00
capabilityCheck ( "SSHFP" , providers . CanUseSSHFP ) ,
2020-06-18 22:24:13 +01:00
capabilityCheck ( "TLSA" , providers . CanUseTLSA ) ,
// DS needs special record-level checks
{
rType : "DS" ,
caps : [ ] providers . Capability { providers . CanUseDS , providers . CanUseDSForChildren } ,
checkFunc : checkProviderDS ,
} ,
}
2020-02-25 07:22:32 -05:00
type pairTypeCapability struct {
rType string
2020-06-18 22:24:13 +01:00
// 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
2020-02-25 07:22:32 -05:00
}
2020-06-18 22:24:13 +01: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 ,
)
}
}
2017-04-19 13:13:28 -06:00
}
2020-06-18 22:24:13 +01:00
return nil
2020-02-25 07:22:32 -05: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 10:24:21 -04:00
hasAny := false
2020-02-22 07:09:31 -05:00
switch ty . rType {
case "AUTODNSSEC" :
2020-09-27 16:37:42 -04:00
if dc . AutoDNSSEC != "" {
2017-07-06 10:24:21 -04:00
hasAny = true
2017-04-19 13:13:28 -06:00
}
2020-02-22 07:09:31 -05:00
default :
for _ , r := range dc . Records {
if r . Type == ty . rType {
hasAny = true
break
}
}
2017-04-19 13:13:28 -06:00
}
2017-07-06 10:24:21 -04:00
if ! hasAny {
continue
}
2018-02-01 11:45:53 -05:00
for _ , provider := range dc . DNSProviderInstances {
2022-05-29 12:14:17 -04:00
if provider . ProviderType == "-" {
// "-" indicates that we don't yet know who the provider type
// is. This is probably due to the fact that `dnscontrol
// check` doesn't read creds.json, which is where the TYPE is
// set. We will skip this test in this instance. Later if
// `dnscontrol preview` or `push` is used, the full check will
// be performed.
continue
}
2020-02-22 07:09:31 -05:00
// fmt.Printf(" (checking if %q can %q for domain %q)\n", provider.ProviderType, ty.rType, dc.Name)
2020-06-18 22:24:13 +01:00
if ! providerHasAtLeastOneCapability ( provider . ProviderType , ty . caps ... ) {
2020-08-30 19:52:37 -04:00
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 10:24:21 -04:00
}
2020-06-18 22:24:13 +01: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 10:24:21 -04:00
}
2017-04-19 13:13:28 -06:00
}
return nil
}
2016-08-22 18:31:50 -06: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
}
2018-03-19 17:18:58 -04:00
ip := net . ParseIP ( rec . GetTargetField ( ) ) // ip already validated above
2020-06-18 09:37:57 -04:00
newIPs , err := transform . IPToList ( net . ParseIP ( rec . GetTargetField ( ) ) , table )
2016-08-22 18:31:50 -06:00
if err != nil {
return err
}
for i , newIP := range newIPs {
if i == 0 && ! newIP . Equal ( ip ) {
2018-03-19 17:18:58 -04:00
rec . SetTarget ( newIP . String ( ) ) // replace target of first record if different
2016-08-22 18:31:50 -06: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
}
2018-03-19 17:18:58 -04:00
copy . SetTarget ( newIP . String ( ) )
2016-08-22 18:31:50 -06:00
domain . Records = append ( domain . Records , copy )
}
}
}
return nil
}