dnscontrol/models/record.go
Tom Limoncelli 703084160f
REFACTOR: BIND/GANDI_V5 add "RP" record type, rewrite CLOUDFLAREAPI CF_* and more (#3886)
# Issue

* New record type: "RP" (supported by BIND and GANDI_V5) 
* Cloudflare: CF_REDIRECT/CF_TEMP_REDIRECT now generate
CF_SINGLE_REDIRECT records. All PAGE_RULE-based code is removed.
PAGE_RULEs are deprecated at Cloudflare. (be careful when upgrading!)
* New "v2" RecordConfig: RP and CF_SINGLE_REDIRECT are the only record
types that use this method. It shifts most of the work out of JavaScript
and into the Go code, making new record types easier to make, easier to
test, and easier to use by providers. This opens the door to new things
like a potential code-generator for rtypes. Converting existing rtypes
will happen over the next year.
* When only the TTL changes (MODIFY-TTL), the output lists the TTL
change first, not at the end of the line where it is visually lost.
* CF_REDIRECT/CF_TEMP_REDIRECT generate different rule "names". They
will be updated the first time you "push" with this release. The order
of the rules may also change. If you rules depend on a particular order,
be very careful with this upgrade!

Refactoring:

* New "v2" RecordConfig: Record types using this new method simply
package the parameters from dnsconfig.js statements like
CF_REDIRECT(foo,bar) and send them (raw) to the Go code. The Go code
does all processing, validation, etc. and turns them into RecordConfig
that store all the rdata in `RecordConfig.F`. No more adding fields to
RecordConfig for each new record type!
* RecordConfig.IsModernType() returns true if the record uses the new v2
record mechanism.
* PostProcess is now a method on DnsConfig and DomainConfig.
* DOC: How to create new rtypes using the v2 method (incomplete)

Other things:

* Integration tests for CF "full proxy" are removed. This feature
doesn't exist any more.
* DEV: Debugger tips now includes VSCode advice
* TESTING: The names of testgroup's can now have extra spaces to make
data align better
* CF_TEMP_REDIRECT/CF_REDIRECT is now a "builder" that generates
CLOUDFLAREAPI_SINGLE_REDIRECT records.
* And more!

# Resolution

---------

Co-authored-by: Jakob Ackermann <das7pad@outlook.com>
2025-12-04 16:42:20 -05:00

664 lines
26 KiB
Go

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 <anonymous>:")
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
}
// IsModernType returns true if this RecordConfig uses the new "F" field to store its rdata.
// Once all record types have been migrated to use "F", this function can be removed.
func (rc *RecordConfig) IsModernType() bool {
return rc.F != nil
}
// 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.
}
}
}