package models import ( "encoding/json" "fmt" "log" "strings" "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/jinzhu/copier" "github.com/miekg/dns" "github.com/miekg/dns/dnsutil" "github.com/qdm12/reprint" ) // RecordConfig stores a DNS record whether it was created from data downloaded from // a provider's API ("actual") or from user input in dndsconfig.js ("desired"). type RecordConfig struct { // Type is the DNS record type (rtype), all caps, "A", "MX", etc. Type string `json:"type"` // TTL is the DNS record's TTL in seconds. 0 means provider default. TTL uint32 `json:"ttl,omitempty"` // Name is the shortname i.e. the FQDN without the parent directory's suffix. // It should never be "". Record at the apex (naked domain) are represented by "@". Name string `json:"name"` // The short name, PunyCode. See above. NameRaw string `json:"name_raw,omitempty"` // .Name as the user entered it in dnsconfig.js (downcased). NameUnicode string `json:"name_unicode,omitempty"` // .Name as Unicode (downcased, then convertedot Unicode). // This is the FQDN version of .Name. It should never have a trailing ".". NameFQDN string `json:"-"` // Must end with ".$origin". NameFQDNRaw string `json:"-"` // .NameFQDN as the user entered it in dnsconfig.js (downcased). NameFQDNUnicode string `json:"-"` // .NameFQDN as Unicode (downcased, then convertedot Unicode). // F is the binary representation of the record's data usually a dns.XYZ struct. // Always stored in Punycode, not Unicode. Downcased where applicable. F any `json:"fields,omitempty"` //FieldsAsRaw []string // Fields as received from the dnsconfig.js file, converted to strings. //FieldsAsUnicode []string // fields with IDN fields converted to Unicode for display purposes. // Comparable is an opaque string that can be used to compare two // RecordConfigs for equality. Typically this is the Zonefile line minus the // label and TTL. Comparable string `json:"comparable,omitempty"` // Cache of ToComparableNoTTL() // ZonefilePartial is the partial zonefile line for this record, excluding // the label and TTL. If this is not an official RR type, we invent the format. ZonefilePartial string `json:"zonfefilepartial,omitempty"` //// Fields only relevant when RecordConfig was created from data in dnsconfig.js: // Metadata (desired) added to the record via dnsconfig.js. For example: A("foo", "1.2.3.4", {metakey: "value"}) Metadata map[string]string `json:"meta,omitempty"` // FilePos (desired) is "filename:line:char" of the record in dnsconfig.js (desired). FilePos string `json:"filepos"` // Subdomain (if non-empty) contains the subdomain path for this record. // When .Name* fields are updated to include the subdomain, this field is // cleared. SubDomain string `json:"subdomain,omitempty"` //// Fields only relevant when RecordConfig was created from data downloaded from a provider: // Original is a pointer to the provider-specific record object. When // getting the records via the API, we store the original object here. // Later if we need to pull out an ID or other provider-specific field, we // can. Typically deleting or updating a record requires knowing its ID. Original interface{} `json:"-"` //// Legacy fields we hope to remove someday // If you add a field to this struct, also add it to the list in the UnmarshalJSON function. target string // If a name, must end with "." 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"` DnskeyFlags uint16 `json:"dnskeyflags,omitempty"` DnskeyProtocol uint8 `json:"dnskeyprotocol,omitempty"` DnskeyAlgorithm uint8 `json:"dnskeyalgorithm,omitempty"` DnskeyPublicKey string `json:"dnskeypublickey,omitempty"` LocVersion uint8 `json:"locversion,omitempty"` LocSize uint8 `json:"locsize,omitempty"` LocHorizPre uint8 `json:"lochorizpre,omitempty"` LocVertPre uint8 `json:"locvertpre,omitempty"` LocLatitude uint32 `json:"loclatitude,omitempty"` LocLongitude uint32 `json:"loclongitude,omitempty"` LocAltitude uint32 `json:"localtitude,omitempty"` LuaRType string `json:"luartype,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"` SmimeaUsage uint8 `json:"smimeausage,omitempty"` SmimeaSelector uint8 `json:"smimeaselector,omitempty"` SmimeaMatchingType uint8 `json:"smimeamatchingtype,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"` SvcPriority uint16 `json:"svcpriority,omitempty"` SvcParams string `json:"svcparams,omitempty"` TlsaUsage uint8 `json:"tlsausage,omitempty"` TlsaSelector uint8 `json:"tlsaselector,omitempty"` TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` R53Alias map[string]string `json:"r53_alias,omitempty"` AzureAlias map[string]string `json:"azure_alias,omitempty"` AnswerType string `json:"answer_type,omitempty"` UnknownTypeName string `json:"unknown_type_name,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"` FilePos string `json:"filepos"` // Where in the file this record was defined. Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing. Args []any `json:"args,omitempty"` 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"` DnskeyFlags uint16 `json:"dnskeyflags,omitempty"` DnskeyProtocol uint8 `json:"dnskeyprotocol,omitempty"` DnskeyAlgorithm uint8 `json:"dnskeyalgorithm,omitempty"` DnskeyPublicKey string `json:"dnskeypublickey,omitempty"` LocVersion uint8 `json:"locversion,omitempty"` LocSize uint8 `json:"locsize,omitempty"` LocHorizPre uint8 `json:"lochorizpre,omitempty"` LocVertPre uint8 `json:"locvertpre,omitempty"` LocLatitude int `json:"loclatitude,omitempty"` LocLongitude int `json:"loclongitude,omitempty"` LocAltitude uint32 `json:"localtitude,omitempty"` LuaRType string `json:"luartype,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"` SmimeaUsage uint8 `json:"smimeausage,omitempty"` SmimeaSelector uint8 `json:"smimeaselector,omitempty"` SmimeaMatchingType uint8 `json:"smimeamatchingtype,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"` SvcPriority uint16 `json:"svcpriority,omitempty"` SvcParams string `json:"svcparams,omitempty"` TlsaUsage uint8 `json:"tlsausage,omitempty"` TlsaSelector uint8 `json:"tlsaselector,omitempty"` TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` R53Alias map[string]string `json:"r53_alias,omitempty"` AzureAlias map[string]string `json:"azure_alias,omitempty"` AnswerType string `json:"answer_type,omitempty"` UnknownTypeName string `json:"unknown_type_name,omitempty"` EnsureAbsent bool `json:"ensure_absent,omitempty"` // Override NO_PURGE and delete this record // 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 } recj.FilePos = FixPosition(recj.FilePos) // Copy the exported fields. if err := copier.CopyWithOption(&rc, &recj, copier.Option{IgnoreEmpty: true, DeepCopy: true}); err != nil { return err } // Set each unexported field. if err := rc.SetTarget(recj.Target); err != nil { return err } // 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 } // TODO: Move this to rtypecontrol or a similar package. func FixPosition(str string) string { if str == "" { return "" } str = strings.TrimSpace(str) str = strings.ReplaceAll(str, "\n", " ") str = strings.TrimPrefix(str, "at :") return fmt.Sprintf("[line:%s]", str) } // 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", short)) } } // 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) } } // 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 } // ToComparableNoTTL returns a comparison string. If you need to compare two // RecordConfigs, you can simply compare the string returned by this function. // The comparison includes all fields except TTL and any provider-specific // metafields. Provider-specific metafields like CF_PROXY are not the same as // pseudo-records like ANAME or R53_ALIAS func (rc *RecordConfig) ToComparableNoTTL() string { if rc.IsModernType() { return rc.Comparable } switch rc.Type { case "SOA": return fmt.Sprintf("%s %v %d %d %d %d", rc.target, rc.SoaMbox, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl) // SoaSerial is not included because it isn't used in comparisons. case "TXT": // fmt.Fprintf(os.Stdout, "DEBUG: ToComNoTTL raw txts=%s q=%q\n", rc.target, rc.target) r := txtutil.EncodeQuoted(rc.target) // fmt.Fprintf(os.Stdout, "DEBUG: ToComNoTTL cmp txts=%s q=%q\n", r, r) return r case "LUA": return rc.luaCombined() case "UNKNOWN": return fmt.Sprintf("rtype=%s rdata=%s", rc.UnknownTypeName, rc.target) } return rc.GetTargetCombined() } // ToRR converts a RecordConfig to a dns.RR. func (rc *RecordConfig) ToRR() dns.RR { // IsModernType types store standard types as dns.RR directly in rc.F. if rr, ok := rc.F.(dns.RR); ok { return 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) } // Magically 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.TypeCAA: rr.(*dns.CAA).Flag = rc.CaaFlag rr.(*dns.CAA).Tag = rc.CaaTag rr.(*dns.CAA).Value = rc.GetTargetField() case dns.TypeCNAME: rr.(*dns.CNAME).Target = rc.GetTargetField() case dns.TypeDHCID: rr.(*dns.DHCID).Digest = rc.GetTargetField() case dns.TypeDNAME: rr.(*dns.DNAME).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.TypeDNSKEY: rr.(*dns.DNSKEY).Flags = rc.DnskeyFlags rr.(*dns.DNSKEY).Protocol = rc.DnskeyProtocol rr.(*dns.DNSKEY).Algorithm = rc.DnskeyAlgorithm rr.(*dns.DNSKEY).PublicKey = rc.DnskeyPublicKey case dns.TypeHTTPS: rr.(*dns.HTTPS).Priority = rc.SvcPriority rr.(*dns.HTTPS).Target = rc.GetTargetField() rr.(*dns.HTTPS).Value = rc.GetSVCBValue() case dns.TypeLOC: // fmt.Printf("ToRR long: %d, lat:%d, sz: %d, hz:%d, vt:%d\n", rc.LocLongitude, rc.LocLatitude, rc.LocSize, rc.LocHorizPre, rc.LocVertPre) // fmt.Printf("ToRR rc: %+v\n", *rc) rr.(*dns.LOC).Version = rc.LocVersion rr.(*dns.LOC).Longitude = rc.LocLongitude rr.(*dns.LOC).Latitude = rc.LocLatitude rr.(*dns.LOC).Altitude = rc.LocAltitude rr.(*dns.LOC).Size = rc.LocSize rr.(*dns.LOC).HorizPre = rc.LocHorizPre rr.(*dns.LOC).VertPre = rc.LocVertPre case dns.TypeMX: rr.(*dns.MX).Preference = rc.MxPreference rr.(*dns.MX).Mx = 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.TypeNS: rr.(*dns.NS).Ns = rc.GetTargetField() case dns.TypeOPENPGPKEY: rr.(*dns.OPENPGPKEY).PublicKey = rc.GetTargetField() case dns.TypePTR: rr.(*dns.PTR).Ptr = rc.GetTargetField() case dns.TypeSMIMEA: rr.(*dns.SMIMEA).Usage = rc.SmimeaUsage rr.(*dns.SMIMEA).MatchingType = rc.SmimeaMatchingType rr.(*dns.SMIMEA).Selector = rc.SmimeaSelector rr.(*dns.SMIMEA).Certificate = 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.TypeSPF: rr.(*dns.SPF).Txt = rc.GetTargetTXTSegmented() 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.TypeSVCB: rr.(*dns.SVCB).Priority = rc.SvcPriority rr.(*dns.SVCB).Target = rc.GetTargetField() rr.(*dns.SVCB).Value = rc.GetSVCBValue() 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.TypeTXT: rr.(*dns.TXT).Txt = rc.GetTargetTXTSegmented() 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 } // GetDependencies returns the FQDNs on which this record dependents func (rc *RecordConfig) GetDependencies() []string { switch rc.Type { // #rtype_variations case "NS", "SRV", "CNAME", "DNAME", "MX", "ALIAS", "AZURE_ALIAS", "R53_ALIAS": return []string{ rc.target, } } return []string{} } // RecordKey represents a resource record in a format used by some systems. type RecordKey struct { NameFQDN string Type string } func (rk *RecordKey) String() string { return rk.NameFQDN + ":" + rk.Type } // 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} } // GetSVCBValue returns the SVCB Key/Values as a list of Key/Values. func (rc *RecordConfig) GetSVCBValue() []dns.SVCBKeyValue { if !strings.Contains(rc.SvcParams, "IGNORE+DNSCONTROL") { rc.SvcParams = strings.ReplaceAll(rc.SvcParams, "ech=IGNORE", "ech=IGNORE+DNSCONTROL+++") } record, err := dns.NewRR(fmt.Sprintf("%s %s %d %s %s", rc.NameFQDN, rc.Type, rc.SvcPriority, rc.target, rc.SvcParams)) if err != nil { log.Fatalf("could not parse SVCB record: %s", err) } switch r := record.(type) { case *dns.HTTPS: return r.Value case *dns.SVCB: return r.Value } return nil } func (rc *RecordConfig) IsModernType() bool { //fmt.Printf("DEBUG: IsModernType rtype=%s\n", rc.Type) return rc.F != nil // switch rc.Type { // case "CLOUDFLAREAPI_SINGLE_REDIRECT", "RP": // return true // } // return false } // 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 } // GetByType returns the records that match rtype typeName. 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 } // 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 } // GetAllDependencies concatinates all dependencies of all records func (recs Records) GetAllDependencies() []string { var dependencies []string for _, rec := range recs { dependencies = append(dependencies, rec.GetDependencies()...) } return dependencies } // PostProcessRecords does any post-processing of the downloaded DNS records. // Deprecated. zonerecords.CorrectZoneRecords() calls Downcase directly. 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 { if r.IsModernType() { continue } r.Name = strings.ToLower(r.Name) r.NameFQDN = strings.ToLower(r.NameFQDN) switch r.Type { // #rtype_variations case "AKAMAICDN", "AKAMAITLC", "ALIAS", "AAAA", "ANAME", "CNAME", "DNAME", "DS", "DNSKEY", "MX", "NS", "NAPTR", "OPENPGPKEY", "SMIMEA", "PTR", "SRV", "TLSA", "AZURE_ALIAS": // Target is case insensitive. Downcase it. r.target = strings.ToLower(r.target) // BUGFIX(tlim): isn't ALIAS in the wrong case statement? case "A", "CAA", "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "DHCID", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TXT", "ADGUARDHOME_A_PASSTHROUGH", "ADGUARDHOME_AAAA_PASSTHROUGH": // Do nothing. (IP address or case sensitive target) 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. } } } // CanonicalizeTargets turns Targets into FQDNs func CanonicalizeTargets(recs []*RecordConfig, origin string) { originFQDN := origin + "." for _, r := range recs { if r.IsModernType() { continue } switch r.Type { // #rtype_variations case "ALIAS", "ANAME", "CNAME", "DNAME", "DS", "DNSKEY", "MX", "NS", "NAPTR", "PTR", "SRV": // Target is a hostname that might be a shortname. Turn it into a FQDN. r.target = dnsutil.AddOrigin(r.target, originFQDN) case "A", "AKAMAICDN", "AKAMAITLC", "CAA", "DHCID", "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "HTTPS", "IMPORT_TRANSFORM", "LOC", "OPENPGPKEY", "SMIMEA", "SSHFP", "SVCB", "TLSA", "TXT", "ADGUARDHOME_A_PASSTHROUGH", "ADGUARDHOME_AAAA_PASSTHROUGH": // Do nothing. case "SOA": if r.target != "DEFAULT_NOT_SET." { r.target = dnsutil.AddOrigin(r.target, originFQDN) // .target stores the Ns } if r.SoaMbox != "DEFAULT_NOT_SET." { r.SoaMbox = dnsutil.AddOrigin(r.SoaMbox, originFQDN) } default: // TODO: we'd like to panic here, but custom record types complicate things. } } }