github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/models/record.go (about) 1 package models 2 3 import ( 4 "fmt" 5 "log" 6 "sort" 7 "strings" 8 9 "github.com/miekg/dns" 10 "github.com/miekg/dns/dnsutil" 11 ) 12 13 // RecordConfig stores a DNS record. 14 // Valid types: 15 // Official: 16 // A 17 // AAAA 18 // ANAME // Technically not an official rtype yet. 19 // CAA 20 // CNAME 21 // MX 22 // NAPTR 23 // NS 24 // PTR 25 // SRV 26 // SSHFP 27 // TLSA 28 // TXT 29 // Pseudo-Types: 30 // ALIAS 31 // AUTODNSSEC 32 // CF_REDIRECT 33 // CF_TEMP_REDIRECT 34 // FRAME 35 // IMPORT_TRANSFORM 36 // NAMESERVER 37 // NO_PURGE 38 // PAGE_RULE 39 // PURGE 40 // URL 41 // URL301 42 // 43 // Notes about the fields: 44 // 45 // Name: 46 // This is the shortname i.e. the NameFQDN without the origin suffix. 47 // It should never have a trailing "." 48 // It should never be null. The apex (naked domain) is stored as "@". 49 // If the origin is "foo.com." and Name is "foo.com", this literally means 50 // the intended FQDN is "foo.com.foo.com." (which may look odd) 51 // NameFQDN: 52 // This is the FQDN version of Name. 53 // It should never have a trailiing ".". 54 // NOTE: Eventually we will unexport Name/NameFQDN. Please start using 55 // the setters (SetLabel/SetLabelFromFQDN) and getters (GetLabel/GetLabelFQDN). 56 // as they will always work. 57 // Target: 58 // This is the host or IP address of the record, with 59 // the other related paramters (weight, priority, etc.) stored in individual 60 // fields. 61 // NOTE: Eventually we will unexport Target. Please start using the 62 // setters (SetTarget*) and getters (GetTarget*) as they will always work. 63 // 64 // Idioms: 65 // rec.Label() == "@" // Is this record at the apex? 66 // 67 type RecordConfig struct { 68 Type string `json:"type"` // All caps rtype name. 69 Name string `json:"name"` // The short name. See above. 70 NameFQDN string `json:"-"` // Must end with ".$origin". See above. 71 Target string `json:"target"` // If a name, must end with "." 72 TTL uint32 `json:"ttl,omitempty"` 73 Metadata map[string]string `json:"meta,omitempty"` 74 MxPreference uint16 `json:"mxpreference,omitempty"` 75 SrvPriority uint16 `json:"srvpriority,omitempty"` 76 SrvWeight uint16 `json:"srvweight,omitempty"` 77 SrvPort uint16 `json:"srvport,omitempty"` 78 CaaTag string `json:"caatag,omitempty"` 79 CaaFlag uint8 `json:"caaflag,omitempty"` 80 NaptrOrder uint16 `json:"naptrorder,omitempty"` 81 NaptrPreference uint16 `json:"naptrpreference,omitempty"` 82 NaptrFlags string `json:"naptrflags,omitempty"` 83 NaptrService string `json:"naptrservice,omitempty"` 84 NaptrRegexp string `json:"naptrregexp,omitempty"` 85 SshfpAlgorithm uint8 `json:"sshfpalgorithm,omitempty"` 86 SshfpFingerprint uint8 `json:"sshfpfingerprint,omitempty"` 87 SoaMbox string `json:"soambox,omitempty"` 88 SoaSerial uint32 `json:"soaserial,omitempty"` 89 SoaRefresh uint32 `json:"soarefresh,omitempty"` 90 SoaRetry uint32 `json:"soaretry,omitempty"` 91 SoaExpire uint32 `json:"soaexpire,omitempty"` 92 SoaMinttl uint32 `json:"soaminttl,omitempty"` 93 TlsaUsage uint8 `json:"tlsausage,omitempty"` 94 TlsaSelector uint8 `json:"tlsaselector,omitempty"` 95 TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` 96 TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one. 97 R53Alias map[string]string `json:"r53_alias,omitempty"` 98 99 Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing. 100 } 101 102 // Copy returns a deep copy of a RecordConfig. 103 func (rc *RecordConfig) Copy() (*RecordConfig, error) { 104 newR := &RecordConfig{} 105 err := copyObj(rc, newR) 106 return newR, err 107 } 108 109 // SetLabel sets the .Name/.NameFQDN fields given a short name and origin. 110 // origin must not have a trailing dot: The entire code base 111 // maintains dc.Name without the trailig dot. Finding a dot here means 112 // something is very wrong. 113 // short must not have a training dot: That would mean you have 114 // a FQDN, and shouldn't be using SetLabel(). Maybe SetLabelFromFQDN()? 115 func (rc *RecordConfig) SetLabel(short, origin string) { 116 117 // Assertions that make sure the function is being used correctly: 118 if strings.HasSuffix(origin, ".") { 119 panic(fmt.Errorf("origin (%s) is not supposed to end with a dot", origin)) 120 } 121 if strings.HasSuffix(short, ".") { 122 panic(fmt.Errorf("short (%s) is not supposed to end with a dot", origin)) 123 } 124 125 // TODO(tlim): We should add more validation here or in a separate validation 126 // module. We might want to check things like (\w+\.)+ 127 128 short = strings.ToLower(short) 129 origin = strings.ToLower(origin) 130 if short == "" || short == "@" { 131 rc.Name = "@" 132 rc.NameFQDN = origin 133 } else { 134 rc.Name = short 135 rc.NameFQDN = dnsutil.AddOrigin(short, origin) 136 } 137 } 138 139 // UnsafeSetLabelNull sets the label to "". Normally the FQDN is denoted by .Name being 140 // "@" however this can be used to violate that assertion. It should only be used 141 // on copies of a RecordConfig that is being used for non-standard things like 142 // Marshalling yaml. 143 func (rc *RecordConfig) UnsafeSetLabelNull() { 144 rc.Name = "" 145 } 146 147 // SetLabelFromFQDN sets the .Name/.NameFQDN fields given a FQDN and origin. 148 // fqdn may have a trailing "." but it is not required. 149 // origin may not have a trailing dot. 150 func (rc *RecordConfig) SetLabelFromFQDN(fqdn, origin string) { 151 152 // Assertions that make sure the function is being used correctly: 153 if strings.HasSuffix(origin, ".") { 154 panic(fmt.Errorf("origin (%s) is not supposed to end with a dot", origin)) 155 } 156 if strings.HasSuffix(fqdn, "..") { 157 panic(fmt.Errorf("fqdn (%s) is not supposed to end with double dots", origin)) 158 } 159 160 if strings.HasSuffix(fqdn, ".") { 161 // Trim off a trailing dot. 162 fqdn = fqdn[:len(fqdn)-1] 163 } 164 165 fqdn = strings.ToLower(fqdn) 166 origin = strings.ToLower(origin) 167 rc.Name = dnsutil.TrimDomainName(fqdn, origin) 168 rc.NameFQDN = fqdn 169 } 170 171 // GetLabel returns the shortname of the label associated with this RecordConfig. 172 // It will never end with "." 173 // It does not need further shortening (i.e. if it returns "foo.com" and the 174 // domain is "foo.com" then the FQDN is actually "foo.com.foo.com"). 175 // It will never be "" (the apex is returned as "@"). 176 func (rc *RecordConfig) GetLabel() string { 177 return rc.Name 178 } 179 180 // GetLabelFQDN returns the FQDN of the label associated with this RecordConfig. 181 // It will not end with ".". 182 func (rc *RecordConfig) GetLabelFQDN() string { 183 return rc.NameFQDN 184 } 185 186 // ToDiffable returns a string that is comparable by a differ. 187 // extraMaps: a list of maps that should be included in the comparison. 188 func (rc *RecordConfig) ToDiffable(extraMaps ...map[string]string) string { 189 content := fmt.Sprintf("%v ttl=%d", rc.GetTargetCombined(), rc.TTL) 190 if rc.Type == "SOA" { 191 content = fmt.Sprintf("%s %v %d %d %d %d ttl=%d", rc.Target, rc.SoaMbox, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl, rc.TTL) 192 // SoaSerial is not used in comparison 193 } 194 for _, valueMap := range extraMaps { 195 // sort the extra values map keys to perform a deterministic 196 // comparison since Golang maps iteration order is not guaranteed 197 198 // FIXME(tlim) The keys of each map is sorted per-map, not across 199 // all maps. This may be intentional since we'd have no way to 200 // deal with duplicates. 201 202 keys := make([]string, 0) 203 for k := range valueMap { 204 keys = append(keys, k) 205 } 206 sort.Strings(keys) 207 for _, k := range keys { 208 v := valueMap[k] 209 content += fmt.Sprintf(" %s=%s", k, v) 210 } 211 } 212 return content 213 } 214 215 // ToRR converts a RecordConfig to a dns.RR. 216 func (rc *RecordConfig) ToRR() dns.RR { 217 218 // Don't call this on fake types. 219 rdtype, ok := dns.StringToType[rc.Type] 220 if !ok { 221 log.Fatalf("No such DNS type as (%#v)\n", rc.Type) 222 } 223 224 // Magicallly create an RR of the correct type. 225 rr := dns.TypeToRR[rdtype]() 226 227 // Fill in the header. 228 rr.Header().Name = rc.NameFQDN + "." 229 rr.Header().Rrtype = rdtype 230 rr.Header().Class = dns.ClassINET 231 rr.Header().Ttl = rc.TTL 232 if rc.TTL == 0 { 233 rr.Header().Ttl = DefaultTTL 234 } 235 236 // Fill in the data. 237 switch rdtype { // #rtype_variations 238 case dns.TypeA: 239 rr.(*dns.A).A = rc.GetTargetIP() 240 case dns.TypeAAAA: 241 rr.(*dns.AAAA).AAAA = rc.GetTargetIP() 242 case dns.TypeCNAME: 243 rr.(*dns.CNAME).Target = rc.GetTargetField() 244 case dns.TypePTR: 245 rr.(*dns.PTR).Ptr = rc.GetTargetField() 246 case dns.TypeNAPTR: 247 rr.(*dns.NAPTR).Order = rc.NaptrOrder 248 rr.(*dns.NAPTR).Preference = rc.NaptrPreference 249 rr.(*dns.NAPTR).Flags = rc.NaptrFlags 250 rr.(*dns.NAPTR).Service = rc.NaptrService 251 rr.(*dns.NAPTR).Regexp = rc.NaptrRegexp 252 rr.(*dns.NAPTR).Replacement = rc.GetTargetField() 253 case dns.TypeMX: 254 rr.(*dns.MX).Preference = rc.MxPreference 255 rr.(*dns.MX).Mx = rc.GetTargetField() 256 case dns.TypeNS: 257 rr.(*dns.NS).Ns = rc.GetTargetField() 258 case dns.TypeSOA: 259 rr.(*dns.SOA).Ns = rc.GetTargetField() 260 rr.(*dns.SOA).Mbox = rc.SoaMbox 261 rr.(*dns.SOA).Serial = rc.SoaSerial 262 rr.(*dns.SOA).Refresh = rc.SoaRefresh 263 rr.(*dns.SOA).Retry = rc.SoaRetry 264 rr.(*dns.SOA).Expire = rc.SoaExpire 265 rr.(*dns.SOA).Minttl = rc.SoaMinttl 266 case dns.TypeSRV: 267 rr.(*dns.SRV).Priority = rc.SrvPriority 268 rr.(*dns.SRV).Weight = rc.SrvWeight 269 rr.(*dns.SRV).Port = rc.SrvPort 270 rr.(*dns.SRV).Target = rc.GetTargetField() 271 case dns.TypeSSHFP: 272 rr.(*dns.SSHFP).Algorithm = rc.SshfpAlgorithm 273 rr.(*dns.SSHFP).Type = rc.SshfpFingerprint 274 rr.(*dns.SSHFP).FingerPrint = rc.GetTargetField() 275 case dns.TypeCAA: 276 rr.(*dns.CAA).Flag = rc.CaaFlag 277 rr.(*dns.CAA).Tag = rc.CaaTag 278 rr.(*dns.CAA).Value = rc.GetTargetField() 279 case dns.TypeTLSA: 280 rr.(*dns.TLSA).Usage = rc.TlsaUsage 281 rr.(*dns.TLSA).MatchingType = rc.TlsaMatchingType 282 rr.(*dns.TLSA).Selector = rc.TlsaSelector 283 rr.(*dns.TLSA).Certificate = rc.GetTargetField() 284 case dns.TypeTXT: 285 rr.(*dns.TXT).Txt = rc.TxtStrings 286 default: 287 panic(fmt.Sprintf("ToRR: Unimplemented rtype %v", rc.Type)) 288 // We panic so that we quickly find any switch statements 289 // that have not been updated for a new RR type. 290 } 291 292 return rr 293 } 294 295 // RecordKey represents a resource record in a format used by some systems. 296 type RecordKey struct { 297 NameFQDN string 298 Type string 299 } 300 301 // Key converts a RecordConfig into a RecordKey. 302 func (rc *RecordConfig) Key() RecordKey { 303 t := rc.Type 304 if rc.R53Alias != nil { 305 if v, ok := rc.R53Alias["type"]; ok { 306 // Route53 aliases append their alias type, so that records for the same 307 // label with different alias types are considered separate. 308 t = fmt.Sprintf("%s_%s", t, v) 309 } 310 } 311 return RecordKey{rc.NameFQDN, t} 312 } 313 314 // Records is a list of *RecordConfig. 315 type Records []*RecordConfig 316 317 // HasRecordTypeName returns True if there is a record with this rtype and name. 318 func (recs Records) HasRecordTypeName(rtype, name string) bool { 319 for _, r := range recs { 320 if r.Type == rtype && r.Name == name { 321 return true 322 } 323 } 324 return false 325 } 326 327 // FQDNMap returns a map of all LabelFQDNs. Useful for making a 328 // truthtable of labels that exist in Records. 329 func (recs Records) FQDNMap() (m map[string]bool) { 330 m = map[string]bool{} 331 for _, rec := range recs { 332 m[rec.GetLabelFQDN()] = true 333 } 334 return m 335 } 336 337 // GroupedByKey returns a map of keys to records. 338 func (recs Records) GroupedByKey() map[RecordKey]Records { 339 groups := map[RecordKey]Records{} 340 for _, rec := range recs { 341 groups[rec.Key()] = append(groups[rec.Key()], rec) 342 } 343 return groups 344 } 345 346 // GroupedByLabel returns a map of keys to records, and their original key order. 347 func (recs Records) GroupedByLabel() ([]string, map[string]Records) { 348 order := []string{} 349 groups := map[string]Records{} 350 for _, rec := range recs { 351 if _, found := groups[rec.Name]; !found { 352 order = append(order, rec.Name) 353 } 354 groups[rec.Name] = append(groups[rec.Name], rec) 355 } 356 return order, groups 357 } 358 359 // GroupedByFQDN returns a map of keys to records, grouped by FQDN. 360 func (recs Records) GroupedByFQDN() ([]string, map[string]Records) { 361 order := []string{} 362 groups := map[string]Records{} 363 for _, rec := range recs { 364 namefqdn := rec.GetLabelFQDN() 365 if _, found := groups[namefqdn]; !found { 366 order = append(order, namefqdn) 367 } 368 groups[namefqdn] = append(groups[namefqdn], rec) 369 } 370 return order, groups 371 } 372 373 // PostProcessRecords does any post-processing of the downloaded DNS records. 374 func PostProcessRecords(recs []*RecordConfig) { 375 downcase(recs) 376 } 377 378 // Downcase converts all labels and targets to lowercase in a list of RecordConfig. 379 func downcase(recs []*RecordConfig) { 380 for _, r := range recs { 381 r.Name = strings.ToLower(r.Name) 382 r.NameFQDN = strings.ToLower(r.NameFQDN) 383 switch r.Type { // #rtype_variations 384 case "ANAME", "CNAME", "MX", "NS", "PTR", "NAPTR", "SRV": 385 // These record types have a target that is case insensitive, so we downcase it. 386 r.Target = strings.ToLower(r.Target) 387 case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TLSA", "TXT", "SSHFP", "CF_REDIRECT", "CF_TEMP_REDIRECT": 388 // These record types have a target that is case sensitive, or is an IP address. We leave them alone. 389 // Do nothing. 390 case "SOA": 391 if r.Target != "DEFAULT_NOT_SET." { 392 r.Target = strings.ToLower(r.Target) // .Target stores the Ns 393 } 394 if r.SoaMbox != "DEFAULT_NOT_SET." { 395 r.SoaMbox = strings.ToLower(r.SoaMbox) 396 } 397 default: 398 // TODO: we'd like to panic here, but custom record types complicate things. 399 } 400 } 401 return 402 }