github.com/teknogeek/dnscontrol@v0.2.8/models/record.go (about) 1 package models 2 3 import ( 4 "fmt" 5 "log" 6 "strings" 7 8 "github.com/miekg/dns" 9 "github.com/miekg/dns/dnsutil" 10 "github.com/pkg/errors" 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 // NS 23 // PTR 24 // SRV 25 // TLSA 26 // TXT 27 // Pseudo-Types: 28 // ALIAS 29 // CF_REDIRECT 30 // CF_TEMP_REDIRECT 31 // FRAME 32 // IMPORT_TRANSFORM 33 // NAMESERVER 34 // NO_PURGE 35 // PAGE_RULE 36 // PURGE 37 // URL 38 // URL301 39 // 40 // Notes about the fields: 41 // 42 // Name: 43 // This is the shortname i.e. the NameFQDN without the origin suffix. 44 // It should never have a trailing "." 45 // It should never be null. The apex (naked domain) is stored as "@". 46 // If the origin is "foo.com." and Name is "foo.com", this literally means 47 // the intended FQDN is "foo.com.foo.com." (which may look odd) 48 // NameFQDN: 49 // This is the FQDN version of Name. 50 // It should never have a trailiing ".". 51 // NOTE: Eventually we will unexport Name/NameFQDN. Please start using 52 // the setters (SetLabel/SetLabelFromFQDN) and getters (GetLabel/GetLabelFQDN). 53 // as they will always work. 54 // Target: 55 // This is the host or IP address of the record, with 56 // the other related paramters (weight, priority, etc.) stored in individual 57 // fields. 58 // NOTE: Eventually we will unexport Target. Please start using the 59 // setters (SetTarget*) and getters (GetTarget*) as they will always work. 60 // 61 // Idioms: 62 // rec.Label() == "@" // Is this record at the apex? 63 // 64 type RecordConfig struct { 65 Type string `json:"type"` // All caps rtype name. 66 Name string `json:"name"` // The short name. See above. 67 NameFQDN string `json:"-"` // Must end with ".$origin". See above. 68 Target string `json:"target"` // If a name, must end with "." 69 TTL uint32 `json:"ttl,omitempty"` 70 Metadata map[string]string `json:"meta,omitempty"` 71 MxPreference uint16 `json:"mxpreference,omitempty"` 72 SrvPriority uint16 `json:"srvpriority,omitempty"` 73 SrvWeight uint16 `json:"srvweight,omitempty"` 74 SrvPort uint16 `json:"srvport,omitempty"` 75 CaaTag string `json:"caatag,omitempty"` 76 CaaFlag uint8 `json:"caaflag,omitempty"` 77 TlsaUsage uint8 `json:"tlsausage,omitempty"` 78 TlsaSelector uint8 `json:"tlsaselector,omitempty"` 79 TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` 80 TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one. 81 R53Alias map[string]string `json:"r53_alias,omitempty"` 82 83 Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing. 84 } 85 86 // Copy returns a deep copy of a RecordConfig. 87 func (rc *RecordConfig) Copy() (*RecordConfig, error) { 88 newR := &RecordConfig{} 89 err := copyObj(rc, newR) 90 return newR, err 91 } 92 93 // SetLabel sets the .Name/.NameFQDN fields given a short name and origin. 94 // origin must not have a trailing dot: The entire code base 95 // maintains dc.Name without the trailig dot. Finding a dot here means 96 // something is very wrong. 97 // short must not have a training dot: That would mean you have 98 // a FQDN, and shouldn't be using SetLabel(). Maybe SetLabelFromFQDN()? 99 func (rc *RecordConfig) SetLabel(short, origin string) { 100 101 // Assertions that make sure the function is being used correctly: 102 if strings.HasSuffix(origin, ".") { 103 panic(errors.Errorf("origin (%s) is not supposed to end with a dot", origin)) 104 } 105 if strings.HasSuffix(short, ".") { 106 panic(errors.Errorf("short (%s) is not supposed to end with a dot", origin)) 107 } 108 109 // TODO(tlim): We should add more validation here or in a separate validation 110 // module. We might want to check things like (\w+\.)+ 111 112 short = strings.ToLower(short) 113 origin = strings.ToLower(origin) 114 if short == "" || short == "@" { 115 rc.Name = "@" 116 rc.NameFQDN = origin 117 } else { 118 rc.Name = short 119 rc.NameFQDN = dnsutil.AddOrigin(short, origin) 120 } 121 } 122 123 // UnsafeSetLabelNull sets the label to "". Normally the FQDN is denoted by .Name being 124 // "@" however this can be used to violate that assertion. It should only be used 125 // on copies of a RecordConfig that is being used for non-standard things like 126 // Marshalling yaml. 127 func (rc *RecordConfig) UnsafeSetLabelNull() { 128 rc.Name = "" 129 } 130 131 // SetLabelFromFQDN sets the .Name/.NameFQDN fields given a FQDN and origin. 132 // fqdn may have a trailing "." but it is not required. 133 // origin may not have a trailing dot. 134 func (rc *RecordConfig) SetLabelFromFQDN(fqdn, origin string) { 135 136 // Assertions that make sure the function is being used correctly: 137 if strings.HasSuffix(origin, ".") { 138 panic(errors.Errorf("origin (%s) is not supposed to end with a dot", origin)) 139 } 140 if strings.HasSuffix(fqdn, "..") { 141 panic(errors.Errorf("fqdn (%s) is not supposed to end with double dots", origin)) 142 } 143 144 if strings.HasSuffix(fqdn, ".") { 145 // Trim off a trailing dot. 146 fqdn = fqdn[:len(fqdn)-1] 147 } 148 149 fqdn = strings.ToLower(fqdn) 150 origin = strings.ToLower(origin) 151 rc.Name = dnsutil.TrimDomainName(fqdn, origin) 152 rc.NameFQDN = fqdn 153 } 154 155 // GetLabel returns the shortname of the label associated with this RecordConfig. 156 // It will never end with "." 157 // It does not need further shortening (i.e. if it returns "foo.com" and the 158 // domain is "foo.com" then the FQDN is actually "foo.com.foo.com"). 159 // It will never be "" (the apex is returned as "@"). 160 func (rc *RecordConfig) GetLabel() string { 161 return rc.Name 162 } 163 164 // GetLabelFQDN returns the FQDN of the label associated with this RecordConfig. 165 // It will not end with ".". 166 func (rc *RecordConfig) GetLabelFQDN() string { 167 return rc.NameFQDN 168 } 169 170 // ToRR converts a RecordConfig to a dns.RR. 171 func (rc *RecordConfig) ToRR() dns.RR { 172 173 // Don't call this on fake types. 174 rdtype, ok := dns.StringToType[rc.Type] 175 if !ok { 176 log.Fatalf("No such DNS type as (%#v)\n", rc.Type) 177 } 178 179 // Magicallly create an RR of the correct type. 180 rr := dns.TypeToRR[rdtype]() 181 182 // Fill in the header. 183 rr.Header().Name = rc.NameFQDN + "." 184 rr.Header().Rrtype = rdtype 185 rr.Header().Class = dns.ClassINET 186 rr.Header().Ttl = rc.TTL 187 if rc.TTL == 0 { 188 rr.Header().Ttl = DefaultTTL 189 } 190 191 // Fill in the data. 192 switch rdtype { // #rtype_variations 193 case dns.TypeA: 194 rr.(*dns.A).A = rc.GetTargetIP() 195 case dns.TypeAAAA: 196 rr.(*dns.AAAA).AAAA = rc.GetTargetIP() 197 case dns.TypeCNAME: 198 rr.(*dns.CNAME).Target = rc.GetTargetField() 199 case dns.TypePTR: 200 rr.(*dns.PTR).Ptr = rc.GetTargetField() 201 case dns.TypeMX: 202 rr.(*dns.MX).Preference = rc.MxPreference 203 rr.(*dns.MX).Mx = rc.GetTargetField() 204 case dns.TypeNS: 205 rr.(*dns.NS).Ns = rc.GetTargetField() 206 case dns.TypeSOA: 207 t := strings.Replace(rc.GetTargetField(), `\ `, ` `, -1) 208 parts := strings.Fields(t) 209 rr.(*dns.SOA).Ns = parts[0] 210 rr.(*dns.SOA).Mbox = parts[1] 211 rr.(*dns.SOA).Serial = atou32(parts[2]) 212 rr.(*dns.SOA).Refresh = atou32(parts[3]) 213 rr.(*dns.SOA).Retry = atou32(parts[4]) 214 rr.(*dns.SOA).Expire = atou32(parts[5]) 215 rr.(*dns.SOA).Minttl = atou32(parts[6]) 216 case dns.TypeSRV: 217 rr.(*dns.SRV).Priority = rc.SrvPriority 218 rr.(*dns.SRV).Weight = rc.SrvWeight 219 rr.(*dns.SRV).Port = rc.SrvPort 220 rr.(*dns.SRV).Target = rc.GetTargetField() 221 case dns.TypeCAA: 222 rr.(*dns.CAA).Flag = rc.CaaFlag 223 rr.(*dns.CAA).Tag = rc.CaaTag 224 rr.(*dns.CAA).Value = rc.GetTargetField() 225 case dns.TypeTLSA: 226 rr.(*dns.TLSA).Usage = rc.TlsaUsage 227 rr.(*dns.TLSA).MatchingType = rc.TlsaMatchingType 228 rr.(*dns.TLSA).Selector = rc.TlsaSelector 229 rr.(*dns.TLSA).Certificate = rc.GetTargetField() 230 case dns.TypeTXT: 231 rr.(*dns.TXT).Txt = rc.TxtStrings 232 default: 233 panic(fmt.Sprintf("ToRR: Unimplemented rtype %v", rc.Type)) 234 // We panic so that we quickly find any switch statements 235 // that have not been updated for a new RR type. 236 } 237 238 return rr 239 } 240 241 // RecordKey represents a resource record in a format used by some systems. 242 type RecordKey struct { 243 NameFQDN string 244 Type string 245 } 246 247 // Key converts a RecordConfig into a RecordKey. 248 func (rc *RecordConfig) Key() RecordKey { 249 t := rc.Type 250 if rc.R53Alias != nil { 251 if v, ok := rc.R53Alias["type"]; ok { 252 // Route53 aliases append their alias type, so that records for the same 253 // label with different alias types are considered separate. 254 t = fmt.Sprintf("%s_%s", t, v) 255 } 256 } 257 return RecordKey{rc.NameFQDN, t} 258 } 259 260 // Records is a list of *RecordConfig. 261 type Records []*RecordConfig 262 263 // Grouped returns a map of keys to records. 264 func (r Records) Grouped() map[RecordKey]Records { 265 groups := map[RecordKey]Records{} 266 for _, rec := range r { 267 groups[rec.Key()] = append(groups[rec.Key()], rec) 268 } 269 return groups 270 } 271 272 // GroupedByLabel returns a map of keys to records, and their original key order. 273 func (r Records) GroupedByLabel() ([]string, map[string]Records) { 274 order := []string{} 275 groups := map[string]Records{} 276 for _, rec := range r { 277 if _, found := groups[rec.Name]; !found { 278 order = append(order, rec.Name) 279 } 280 groups[rec.Name] = append(groups[rec.Name], rec) 281 } 282 return order, groups 283 } 284 285 // PostProcessRecords does any post-processing of the downloaded DNS records. 286 func PostProcessRecords(recs []*RecordConfig) { 287 downcase(recs) 288 } 289 290 // Downcase converts all labels and targets to lowercase in a list of RecordConfig. 291 func downcase(recs []*RecordConfig) { 292 for _, r := range recs { 293 r.Name = strings.ToLower(r.Name) 294 r.NameFQDN = strings.ToLower(r.NameFQDN) 295 switch r.Type { // #rtype_variations 296 case "ANAME", "CNAME", "MX", "NS", "PTR", "SRV": 297 // These record types have a target that is case insensitive, so we downcase it. 298 r.Target = strings.ToLower(r.Target) 299 case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TLSA", "TXT", "SOA", "CF_REDIRECT", "CF_TEMP_REDIRECT": 300 // These record types have a target that is case sensitive, or is an IP address. We leave them alone. 301 // Do nothing. 302 default: 303 // TODO: we'd like to panic here, but custom record types complicate things. 304 } 305 } 306 return 307 }