package models import ( "encoding/json" "fmt" "log" "sort" "strings" "github.com/jinzhu/copier" "github.com/miekg/dns" "github.com/miekg/dns/dnsutil" "github.com/qdm12/reprint" ) // RecordConfig stores a DNS record. // Valid types: // Official: // A // AAAA // ANAME // Technically not an official rtype yet. // CAA // CNAME // MX // NAPTR // NS // PTR // SRV // SOA // SSHFP // TLSA // TXT // Pseudo-Types: // ALIAS // CF_REDIRECT // CF_TEMP_REDIRECT // CF_WORKER_ROUTE // FRAME // IMPORT_TRANSFORM // NAMESERVER // NO_PURGE // NS1_URLFWD // PAGE_RULE // WORKER_ROUTE // PURGE // URL // URL301 // // Notes about the fields: // // Name: // This is the shortname i.e. the NameFQDN without the origin suffix. // It should never have a trailing "." // It should never be null. The apex (naked domain) is stored as "@". // If the origin is "foo.com." and Name is "foo.com", this literally means // the intended FQDN is "foo.com.foo.com." (which may look odd) // NameFQDN: // This is the FQDN version of Name. // It should never have a trailing ".". // NOTE: Eventually we will unexport Name/NameFQDN. Please start using // the setters (SetLabel/SetLabelFromFQDN) and getters (GetLabel/GetLabelFQDN). // as they will always work. // target: // This is the host or IP address of the record, with // the other related parameters (weight, priority, etc.) stored in individual // fields. // NOTE: Eventually we will unexport Target. Please start using the // setters (SetTarget*) and getters (GetTarget*) as they will always work. // SubDomain: // This is the subdomain path, if any, imported from the configuration. If // present at the time of canonicalization it is inserted between the // Name and origin when constructing a canonical (FQDN) target. // // Idioms: // rec.Label() == "@" // Is this record at the apex? // type RecordConfig struct { Type string `json:"type"` // All caps rtype name. Name string `json:"name"` // The short name. See above. SubDomain string `json:"subdomain,omitempty"` NameFQDN string `json:"-"` // Must end with ".$origin". See above. target string // If a name, must end with "." TTL uint32 `json:"ttl,omitempty"` Metadata map[string]string `json:"meta,omitempty"` Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing. // If you add a field to this struct, also add it to the list on MarshalJSON. MxPreference uint16 `json:"mxpreference,omitempty"` SrvPriority uint16 `json:"srvpriority,omitempty"` SrvWeight uint16 `json:"srvweight,omitempty"` SrvPort uint16 `json:"srvport,omitempty"` CaaTag string `json:"caatag,omitempty"` CaaFlag uint8 `json:"caaflag,omitempty"` DsKeyTag uint16 `json:"dskeytag,omitempty"` DsAlgorithm uint8 `json:"dsalgorithm,omitempty"` DsDigestType uint8 `json:"dsdigesttype,omitempty"` DsDigest string `json:"dsdigest,omitempty"` NaptrOrder uint16 `json:"naptrorder,omitempty"` NaptrPreference uint16 `json:"naptrpreference,omitempty"` NaptrFlags string `json:"naptrflags,omitempty"` NaptrService string `json:"naptrservice,omitempty"` NaptrRegexp string `json:"naptrregexp,omitempty"` SshfpAlgorithm uint8 `json:"sshfpalgorithm,omitempty"` SshfpFingerprint uint8 `json:"sshfpfingerprint,omitempty"` SoaMbox string `json:"soambox,omitempty"` SoaSerial uint32 `json:"soaserial,omitempty"` SoaRefresh uint32 `json:"soarefresh,omitempty"` SoaRetry uint32 `json:"soaretry,omitempty"` SoaExpire uint32 `json:"soaexpire,omitempty"` SoaMinttl uint32 `json:"soaminttl,omitempty"` TlsaUsage uint8 `json:"tlsausage,omitempty"` TlsaSelector uint8 `json:"tlsaselector,omitempty"` TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores all the strings joined. R53Alias map[string]string `json:"r53_alias,omitempty"` AzureAlias map[string]string `json:"azure_alias,omitempty"` } // MarshalJSON marshals RecordConfig. func (rc *RecordConfig) MarshalJSON() ([]byte, error) { recj := &struct { RecordConfig Target string `json:"target"` }{ RecordConfig: *rc, Target: rc.GetTargetField(), } j, err := json.Marshal(*recj) if err != nil { return nil, err } return j, nil } // UnmarshalJSON unmarshals RecordConfig. func (rc *RecordConfig) UnmarshalJSON(b []byte) error { recj := &struct { Target string `json:"target"` Type string `json:"type"` // All caps rtype name. Name string `json:"name"` // The short name. See above. SubDomain string `json:"subdomain,omitempty"` NameFQDN string `json:"-"` // Must end with ".$origin". See above. target string // If a name, must end with "." TTL uint32 `json:"ttl,omitempty"` Metadata map[string]string `json:"meta,omitempty"` Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing. MxPreference uint16 `json:"mxpreference,omitempty"` SrvPriority uint16 `json:"srvpriority,omitempty"` SrvWeight uint16 `json:"srvweight,omitempty"` SrvPort uint16 `json:"srvport,omitempty"` CaaTag string `json:"caatag,omitempty"` CaaFlag uint8 `json:"caaflag,omitempty"` DsKeyTag uint16 `json:"dskeytag,omitempty"` DsAlgorithm uint8 `json:"dsalgorithm,omitempty"` DsDigestType uint8 `json:"dsdigesttype,omitempty"` DsDigest string `json:"dsdigest,omitempty"` NaptrOrder uint16 `json:"naptrorder,omitempty"` NaptrPreference uint16 `json:"naptrpreference,omitempty"` NaptrFlags string `json:"naptrflags,omitempty"` NaptrService string `json:"naptrservice,omitempty"` NaptrRegexp string `json:"naptrregexp,omitempty"` SshfpAlgorithm uint8 `json:"sshfpalgorithm,omitempty"` SshfpFingerprint uint8 `json:"sshfpfingerprint,omitempty"` SoaMbox string `json:"soambox,omitempty"` SoaSerial uint32 `json:"soaserial,omitempty"` SoaRefresh uint32 `json:"soarefresh,omitempty"` SoaRetry uint32 `json:"soaretry,omitempty"` SoaExpire uint32 `json:"soaexpire,omitempty"` SoaMinttl uint32 `json:"soaminttl,omitempty"` TlsaUsage uint8 `json:"tlsausage,omitempty"` TlsaSelector uint8 `json:"tlsaselector,omitempty"` TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one. R53Alias map[string]string `json:"r53_alias,omitempty"` AzureAlias map[string]string `json:"azure_alias,omitempty"` // NB(tlim): If anyone can figure out how to do this without listing all // the fields, please let us know! }{} if err := json.Unmarshal(b, &recj); err != nil { return err } // Copy the exported fields. copier.CopyWithOption(&rc, &recj, copier.Option{IgnoreEmpty: true, DeepCopy: true}) // Set each unexported field. rc.SetTarget(recj.Target) // Some sanity checks: if recj.Type != rc.Type { panic("DEBUG: TYPE NOT COPIED\n") } if recj.Type == "" { panic("DEBUG: TYPE BLANK\n") } if recj.Name != rc.Name { panic("DEBUG: NAME NOT COPIED\n") } return nil } // Copy returns a deep copy of a RecordConfig. func (rc *RecordConfig) Copy() (*RecordConfig, error) { newR := &RecordConfig{} // Copy the exported fields. err := reprint.FromTo(rc, newR) // Deep copy // Set each unexported field. newR.target = rc.target return newR, err } // SetLabel sets the .Name/.NameFQDN fields given a short name and origin. // origin must not have a trailing dot: The entire code base // maintains dc.Name without the trailig dot. Finding a dot here means // something is very wrong. // short must not have a training dot: That would mean you have // a FQDN, and shouldn't be using SetLabel(). Maybe SetLabelFromFQDN()? func (rc *RecordConfig) SetLabel(short, origin string) { // Assertions that make sure the function is being used correctly: if strings.HasSuffix(origin, ".") { panic(fmt.Errorf("origin (%s) is not supposed to end with a dot", origin)) } if strings.HasSuffix(short, ".") { if short != "**current-domain**" { panic(fmt.Errorf("short (%s) is not supposed to end with a dot", origin)) } } // TODO(tlim): We should add more validation here or in a separate validation // module. We might want to check things like (\w+\.)+ short = strings.ToLower(short) origin = strings.ToLower(origin) if short == "" || short == "@" { rc.Name = "@" rc.NameFQDN = origin } else { rc.Name = short rc.NameFQDN = dnsutil.AddOrigin(short, origin) } } // UnsafeSetLabelNull sets the label to "". Normally the FQDN is denoted by .Name being // "@" however this can be used to violate that assertion. It should only be used // on copies of a RecordConfig that is being used for non-standard things like // Marshalling yaml. func (rc *RecordConfig) UnsafeSetLabelNull() { rc.Name = "" } // SetLabelFromFQDN sets the .Name/.NameFQDN fields given a FQDN and origin. // fqdn may have a trailing "." but it is not required. // origin may not have a trailing dot. func (rc *RecordConfig) SetLabelFromFQDN(fqdn, origin string) { // Assertions that make sure the function is being used correctly: if strings.HasSuffix(origin, ".") { panic(fmt.Errorf("origin (%s) is not supposed to end with a dot", origin)) } if strings.HasSuffix(fqdn, "..") { panic(fmt.Errorf("fqdn (%s) is not supposed to end with double dots", origin)) } // Trim off a trailing dot. fqdn = strings.TrimSuffix(fqdn, ".") fqdn = strings.ToLower(fqdn) origin = strings.ToLower(origin) rc.Name = dnsutil.TrimDomainName(fqdn, origin) rc.NameFQDN = fqdn } // GetLabel returns the shortname of the label associated with this RecordConfig. // It will never end with "." // It does not need further shortening (i.e. if it returns "foo.com" and the // domain is "foo.com" then the FQDN is actually "foo.com.foo.com"). // It will never be "" (the apex is returned as "@"). func (rc *RecordConfig) GetLabel() string { return rc.Name } // GetLabelFQDN returns the FQDN of the label associated with this RecordConfig. // It will not end with ".". func (rc *RecordConfig) GetLabelFQDN() string { return rc.NameFQDN } // ToDiffable returns a string that is comparable by a differ. // extraMaps: a list of maps that should be included in the comparison. func (rc *RecordConfig) ToDiffable(extraMaps ...map[string]string) string { content := fmt.Sprintf("%v ttl=%d", rc.GetTargetCombined(), rc.TTL) if rc.Type == "SOA" { 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) // SoaSerial is not used in comparison } for _, valueMap := range extraMaps { // sort the extra values map keys to perform a deterministic // comparison since Golang maps iteration order is not guaranteed // FIXME(tlim) The keys of each map is sorted per-map, not across // all maps. This may be intentional since we'd have no way to // deal with duplicates. keys := make([]string, 0) for k := range valueMap { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { v := valueMap[k] content += fmt.Sprintf(" %s=%s", k, v) } } return content } // ToRR converts a RecordConfig to a dns.RR. func (rc *RecordConfig) ToRR() dns.RR { // Don't call this on fake types. rdtype, ok := dns.StringToType[rc.Type] if !ok { log.Fatalf("No such DNS type as (%#v)\n", rc.Type) } // Magicallly create an RR of the correct type. rr := dns.TypeToRR[rdtype]() // Fill in the header. rr.Header().Name = rc.NameFQDN + "." rr.Header().Rrtype = rdtype rr.Header().Class = dns.ClassINET rr.Header().Ttl = rc.TTL if rc.TTL == 0 { rr.Header().Ttl = DefaultTTL } // Fill in the data. switch rdtype { // #rtype_variations case dns.TypeA: rr.(*dns.A).A = rc.GetTargetIP() case dns.TypeAAAA: rr.(*dns.AAAA).AAAA = rc.GetTargetIP() case dns.TypeCNAME: rr.(*dns.CNAME).Target = rc.GetTargetField() case dns.TypeDS: rr.(*dns.DS).Algorithm = rc.DsAlgorithm rr.(*dns.DS).DigestType = rc.DsDigestType rr.(*dns.DS).Digest = rc.DsDigest rr.(*dns.DS).KeyTag = rc.DsKeyTag case dns.TypePTR: rr.(*dns.PTR).Ptr = rc.GetTargetField() case dns.TypeNAPTR: rr.(*dns.NAPTR).Order = rc.NaptrOrder rr.(*dns.NAPTR).Preference = rc.NaptrPreference rr.(*dns.NAPTR).Flags = rc.NaptrFlags rr.(*dns.NAPTR).Service = rc.NaptrService rr.(*dns.NAPTR).Regexp = rc.NaptrRegexp rr.(*dns.NAPTR).Replacement = rc.GetTargetField() case dns.TypeMX: rr.(*dns.MX).Preference = rc.MxPreference rr.(*dns.MX).Mx = rc.GetTargetField() case dns.TypeNS: rr.(*dns.NS).Ns = rc.GetTargetField() case dns.TypeSOA: rr.(*dns.SOA).Ns = rc.GetTargetField() rr.(*dns.SOA).Mbox = rc.SoaMbox rr.(*dns.SOA).Serial = rc.SoaSerial rr.(*dns.SOA).Refresh = rc.SoaRefresh rr.(*dns.SOA).Retry = rc.SoaRetry rr.(*dns.SOA).Expire = rc.SoaExpire rr.(*dns.SOA).Minttl = rc.SoaMinttl case dns.TypeSRV: rr.(*dns.SRV).Priority = rc.SrvPriority rr.(*dns.SRV).Weight = rc.SrvWeight rr.(*dns.SRV).Port = rc.SrvPort rr.(*dns.SRV).Target = rc.GetTargetField() case dns.TypeSSHFP: rr.(*dns.SSHFP).Algorithm = rc.SshfpAlgorithm rr.(*dns.SSHFP).Type = rc.SshfpFingerprint rr.(*dns.SSHFP).FingerPrint = rc.GetTargetField() case dns.TypeCAA: rr.(*dns.CAA).Flag = rc.CaaFlag rr.(*dns.CAA).Tag = rc.CaaTag rr.(*dns.CAA).Value = rc.GetTargetField() case dns.TypeTLSA: rr.(*dns.TLSA).Usage = rc.TlsaUsage rr.(*dns.TLSA).MatchingType = rc.TlsaMatchingType rr.(*dns.TLSA).Selector = rc.TlsaSelector rr.(*dns.TLSA).Certificate = rc.GetTargetField() case dns.TypeSPF: rr.(*dns.SPF).Txt = rc.TxtStrings case dns.TypeTXT: rr.(*dns.TXT).Txt = rc.TxtStrings default: panic(fmt.Sprintf("ToRR: Unimplemented rtype %v", rc.Type)) // We panic so that we quickly find any switch statements // that have not been updated for a new RR type. } return rr } // RecordKey represents a resource record in a format used by some systems. type RecordKey struct { NameFQDN string Type string } // Key converts a RecordConfig into a RecordKey. func (rc *RecordConfig) Key() RecordKey { t := rc.Type if rc.R53Alias != nil { if v, ok := rc.R53Alias["type"]; ok { // Route53 aliases append their alias type, so that records for the same // label with different alias types are considered separate. t = fmt.Sprintf("%s_%s", t, v) } } else if rc.AzureAlias != nil { if v, ok := rc.AzureAlias["type"]; ok { // Azure aliases append their alias type, so that records for the same // label with different alias types are considered separate. t = fmt.Sprintf("%s_%s", t, v) } } return RecordKey{rc.NameFQDN, t} } // Records is a list of *RecordConfig. type Records []*RecordConfig // HasRecordTypeName returns True if there is a record with this rtype and name. func (recs Records) HasRecordTypeName(rtype, name string) bool { for _, r := range recs { if r.Type == rtype && r.Name == name { return true } } return false } // FQDNMap returns a map of all LabelFQDNs. Useful for making a // truthtable of labels that exist in Records. func (recs Records) FQDNMap() (m map[string]bool) { m = map[string]bool{} for _, rec := range recs { m[rec.GetLabelFQDN()] = true } return m } func (recs Records) GetByType(typeName string) Records { results := Records{} for _, rec := range recs { if rec.Type == typeName { results = append(results, rec) } } return results } // GroupedByKey returns a map of keys to records. func (recs Records) GroupedByKey() map[RecordKey]Records { groups := map[RecordKey]Records{} for _, rec := range recs { groups[rec.Key()] = append(groups[rec.Key()], rec) } return groups } // GroupedByLabel returns a map of keys to records, and their original key order. func (recs Records) GroupedByLabel() ([]string, map[string]Records) { order := []string{} groups := map[string]Records{} for _, rec := range recs { if _, found := groups[rec.Name]; !found { order = append(order, rec.Name) } groups[rec.Name] = append(groups[rec.Name], rec) } return order, groups } // GroupedByFQDN returns a map of keys to records, grouped by FQDN. func (recs Records) GroupedByFQDN() ([]string, map[string]Records) { order := []string{} groups := map[string]Records{} for _, rec := range recs { namefqdn := rec.GetLabelFQDN() if _, found := groups[namefqdn]; !found { order = append(order, namefqdn) } groups[namefqdn] = append(groups[namefqdn], rec) } return order, groups } // PostProcessRecords does any post-processing of the downloaded DNS records. func PostProcessRecords(recs []*RecordConfig) { downcase(recs) } // Downcase converts all labels and targets to lowercase in a list of RecordConfig. func downcase(recs []*RecordConfig) { for _, r := range recs { r.Name = strings.ToLower(r.Name) r.NameFQDN = strings.ToLower(r.NameFQDN) switch r.Type { // #rtype_variations case "ANAME", "CNAME", "DS", "MX", "NS", "PTR", "NAPTR", "SRV", "TLSA", "AKAMAICDN": // These record types have a target that is case insensitive, so we downcase it. r.target = strings.ToLower(r.target) case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TXT", "SSHFP", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE": // These record types have a target that is case sensitive, or is an IP address. We leave them alone. // Do nothing. case "SOA": if r.target != "DEFAULT_NOT_SET." { r.target = strings.ToLower(r.target) // .target stores the Ns } if r.SoaMbox != "DEFAULT_NOT_SET." { r.SoaMbox = strings.ToLower(r.SoaMbox) } default: // TODO: we'd like to panic here, but custom record types complicate things. } } }