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 }