mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-12-09 13:46:07 +08:00
# 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>
664 lines
26 KiB
Go
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.
|
|
}
|
|
}
|
|
}
|