mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-12-11 06:35:51 +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>
246 lines
9.4 KiB
Go
246 lines
9.4 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/domaintags"
|
|
"github.com/qdm12/reprint"
|
|
"golang.org/x/net/idna"
|
|
)
|
|
|
|
const (
|
|
DomainTag = "dnscontrol_tag" // A copy of DomainConfig.Tag
|
|
DomainUniqueName = "dnscontrol_uniquename" // A copy of DomainConfig.UniqueName
|
|
DomainNameRaw = "dnscontrol_nameraw" // A copy of DomainConfig.NameRaw
|
|
DomainNameASCII = "dnscontrol_nameascii" // A copy of DomainConfig.NameASCII
|
|
DomainNameUnicode = "dnscontrol_nameunicode" // A copy of DomainConfig.NameUnicode
|
|
)
|
|
|
|
// DomainConfig describes a DNS domain (technically a DNS zone).
|
|
type DomainConfig struct {
|
|
Name string `json:"name"` // NO trailing "." Converted to IDN (punycode) early in the pipeline.
|
|
NameRaw string `json:"-"` // name as entered by user in dnsconfig.js
|
|
NameUnicode string `json:"-"` // name in Unicode format
|
|
|
|
Tag string `json:"tag,omitempty"` // Split horizon tag.
|
|
UniqueName string `json:"-"` // .Name + "!" + .Tag
|
|
|
|
RegistrarName string `json:"registrar"`
|
|
DNSProviderNames map[string]int `json:"dnsProviders"`
|
|
|
|
Metadata map[string]string `json:"meta,omitempty"`
|
|
Records Records `json:"records"`
|
|
Nameservers []*Nameserver `json:"nameservers,omitempty"`
|
|
NameserversMutex sync.Mutex `json:"-"`
|
|
|
|
EnsureAbsent Records `json:"recordsabsent,omitempty"` // ENSURE_ABSENT
|
|
KeepUnknown bool `json:"keepunknown,omitempty"` // NO_PURGE
|
|
|
|
Unmanaged []*UnmanagedConfig `json:"unmanaged,omitempty"` // IGNORE()
|
|
UnmanagedUnsafe bool `json:"unmanaged_disable_safety_check,omitempty"` // DISABLE_IGNORE_SAFETY_CHECK
|
|
|
|
IgnoreExternalDNS bool `json:"ignore_external_dns,omitempty"` // IGNORE_EXTERNAL_DNS
|
|
ExternalDNSPrefix string `json:"external_dns_prefix,omitempty"` // IGNORE_EXTERNAL_DNS prefix
|
|
|
|
AutoDNSSEC string `json:"auto_dnssec,omitempty"` // "", "on", "off"
|
|
// DNSSEC bool `json:"dnssec,omitempty"`
|
|
|
|
// These fields contain instantiated provider instances once everything is linked up.
|
|
// This linking is in two phases:
|
|
// 1. Metadata (name/type) is available just from the dnsconfig. Validation can use that.
|
|
// 2. Final driver instances are loaded after we load credentials. Any actual provider interaction requires that.
|
|
RegistrarInstance *RegistrarInstance `json:"-"`
|
|
DNSProviderInstances []*DNSProviderInstance `json:"-"`
|
|
|
|
// Raw user-input from dnsconfig.js that will be processed into RecordConfigs later:
|
|
RawRecords []RawRecordConfig `json:"rawrecords,omitempty"`
|
|
|
|
// Pending work to do for each provider. Provider may be a registrar or DSP.
|
|
pendingCorrectionsMutex sync.Mutex // Protect pendingCorrections*
|
|
pendingCorrections map[string][]*Correction // Work to be done for each provider
|
|
pendingCorrectionsOrder []string // Call the providers in this order
|
|
pendingActualChangeCount map[string]int // Number of changes to report (cumulative)
|
|
pendingPopulateCorrections map[string][]*Correction // Corrections for zone creations at each provider
|
|
}
|
|
|
|
// PostProcess performs and post-processing required after running dnsconfig.js and loading the result.
|
|
// It is called by dns.go's PostProcess() function.
|
|
func (dc *DomainConfig) PostProcess() {
|
|
// Ensure the metadata map is initialized.
|
|
if dc.Metadata == nil {
|
|
dc.Metadata = map[string]string{}
|
|
}
|
|
|
|
// Turn the user-supplied name into the fixed forms.
|
|
ff := domaintags.MakeDomainNameVarieties(dc.Name)
|
|
dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameASCII, ff.NameUnicode, ff.UniqueName
|
|
|
|
// Store the FixForms is Metadata so we don't have to change the signature of every function that might need them.
|
|
// This is a bit ugly but avoids a huge refactor. Please avoid using these to make the future refactor easier.
|
|
if dc.Tag != "" {
|
|
dc.Metadata[DomainTag] = dc.Tag
|
|
}
|
|
//dc.Metadata[DomainNameRaw] = dc.NameRaw
|
|
//dc.Metadata[DomainNameASCII] = dc.Name
|
|
//dc.Metadata[DomainNameUnicode] = dc.NameUnicode
|
|
dc.Metadata[DomainUniqueName] = dc.UniqueName
|
|
}
|
|
|
|
// GetSplitHorizonNames returns the domain's name, uniquename, and tag.
|
|
// Deprecated: use .Name, .Uniquename, and .Tag directly instead.
|
|
func (dc *DomainConfig) GetSplitHorizonNames() (name, uniquename, tag string) {
|
|
return dc.Name, dc.UniqueName, dc.Tag
|
|
}
|
|
|
|
// GetUniqueName returns the domain's uniquename.
|
|
// Deprecated: dc.UniqueName directly instead.
|
|
func (dc *DomainConfig) GetUniqueName() (uniquename string) {
|
|
return dc.UniqueName
|
|
}
|
|
|
|
// Copy returns a deep copy of the DomainConfig.
|
|
func (dc *DomainConfig) Copy() (*DomainConfig, error) {
|
|
newDc := &DomainConfig{}
|
|
err := reprint.FromTo(dc, newDc) // Deep copy
|
|
return newDc, err
|
|
}
|
|
|
|
// Filter removes all records that don't match the filter f.
|
|
func (dc *DomainConfig) Filter(f func(r *RecordConfig) bool) {
|
|
recs := []*RecordConfig{}
|
|
for _, r := range dc.Records {
|
|
if f(r) {
|
|
recs = append(recs, r)
|
|
}
|
|
}
|
|
dc.Records = recs
|
|
}
|
|
|
|
// Punycode will convert all records to punycode format.
|
|
// It will encode:
|
|
// - Name
|
|
// - NameFQDN
|
|
// - Target (CNAME and MX only)
|
|
func (dc *DomainConfig) Punycode() error {
|
|
for _, rec := range dc.Records {
|
|
if rec.IsModernType() {
|
|
continue // Modern types handle punycode themselves.
|
|
}
|
|
|
|
// Update the label:
|
|
t, err := idna.ToASCII(rec.GetLabelFQDN())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rec.SetLabelFromFQDN(t, dc.Name)
|
|
|
|
// Set the target:
|
|
switch rec.Type { // #rtype_variations
|
|
case "ALIAS", "MX", "NS", "CNAME", "DNAME", "PTR", "SRV", "URL", "URL301", "FRAME", "R53_ALIAS", "AKAMAICDN", "AKAMAITLC", "CLOUDNS_WR", "PORKBUN_URLFWD", "BUNNY_DNS_RDR":
|
|
// These rtypes are hostnames, therefore need to be converted (unlike, for example, an AAAA record)
|
|
t, err := idna.ToASCII(rec.GetTargetField())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := rec.SetTarget(t); err != nil {
|
|
return err
|
|
}
|
|
case "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "ADGUARDHOME_A_PASSTHROUGH", "ADGUARDHOME_AAAA_PASSTHROUGH":
|
|
if err := rec.SetTarget(rec.GetTargetField()); err != nil {
|
|
return err
|
|
}
|
|
case "A", "AAAA", "CAA", "DHCID", "DNSKEY", "DS", "HTTPS", "LOC", "LUA", "NAPTR", "OPENPGPKEY", "SMIMEA", "SOA", "SSHFP", "SVCB", "TXT", "TLSA", "AZURE_ALIAS":
|
|
// Nothing to do.
|
|
default:
|
|
return fmt.Errorf("Punycode rtype %v unimplemented", rec.Type)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StoreCorrections accumulates corrections in a thread-safe way.
|
|
func (dc *DomainConfig) StoreCorrections(providerName string, corrections []*Correction) {
|
|
dc.pendingCorrectionsMutex.Lock()
|
|
defer dc.pendingCorrectionsMutex.Unlock()
|
|
|
|
if dc.pendingCorrections == nil {
|
|
// First time storing anything.
|
|
dc.pendingCorrections = make(map[string]([]*Correction))
|
|
dc.pendingCorrections[providerName] = corrections
|
|
dc.pendingCorrectionsOrder = []string{providerName}
|
|
} else if c, ok := dc.pendingCorrections[providerName]; !ok {
|
|
// First time key used
|
|
dc.pendingCorrections[providerName] = corrections
|
|
dc.pendingCorrectionsOrder = []string{providerName}
|
|
} else {
|
|
// Add to existing.
|
|
dc.pendingCorrections[providerName] = append(c, corrections...)
|
|
dc.pendingCorrectionsOrder = append(dc.pendingCorrectionsOrder, providerName)
|
|
}
|
|
}
|
|
|
|
// GetCorrections returns the accumulated corrections for providerName.
|
|
func (dc *DomainConfig) GetCorrections(providerName string) []*Correction {
|
|
dc.pendingCorrectionsMutex.Lock()
|
|
defer dc.pendingCorrectionsMutex.Unlock()
|
|
|
|
if dc.pendingCorrections == nil {
|
|
// First time storing anything.
|
|
return nil
|
|
}
|
|
if c, ok := dc.pendingCorrections[providerName]; ok {
|
|
return c
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IncrementChangeCount accumulates change count in a thread-safe way.
|
|
func (dc *DomainConfig) IncrementChangeCount(providerName string, delta int) {
|
|
dc.pendingCorrectionsMutex.Lock()
|
|
defer dc.pendingCorrectionsMutex.Unlock()
|
|
|
|
if dc.pendingActualChangeCount == nil {
|
|
// First time storing anything.
|
|
dc.pendingActualChangeCount = make(map[string](int))
|
|
}
|
|
dc.pendingActualChangeCount[providerName] += delta
|
|
}
|
|
|
|
// GetChangeCount accumulates change count in a thread-safe way.
|
|
func (dc *DomainConfig) GetChangeCount(providerName string) int {
|
|
dc.pendingCorrectionsMutex.Lock()
|
|
defer dc.pendingCorrectionsMutex.Unlock()
|
|
|
|
return dc.pendingActualChangeCount[providerName]
|
|
}
|
|
|
|
// StorePopulateCorrections accumulates corrections in a thread-safe way.
|
|
func (dc *DomainConfig) StorePopulateCorrections(providerName string, corrections []*Correction) {
|
|
dc.pendingCorrectionsMutex.Lock()
|
|
defer dc.pendingCorrectionsMutex.Unlock()
|
|
|
|
if dc.pendingPopulateCorrections == nil {
|
|
dc.pendingPopulateCorrections = make(map[string][]*Correction, 1)
|
|
}
|
|
dc.pendingPopulateCorrections[providerName] = append(dc.pendingPopulateCorrections[providerName], corrections...)
|
|
}
|
|
|
|
// GetPopulateCorrections returns zone corrections in a thread-safe way.
|
|
func (dc *DomainConfig) GetPopulateCorrections(providerName string) []*Correction {
|
|
dc.pendingCorrectionsMutex.Lock()
|
|
defer dc.pendingCorrectionsMutex.Unlock()
|
|
return dc.pendingPopulateCorrections[providerName]
|
|
}
|
|
|
|
// DomainNameVarieties returns the domain's names in various forms.
|
|
func (dc *DomainConfig) DomainNameVarieties() *domaintags.DomainNameVarieties {
|
|
return &domaintags.DomainNameVarieties{
|
|
NameRaw: dc.NameRaw,
|
|
NameASCII: dc.Name,
|
|
NameUnicode: dc.NameUnicode,
|
|
UniqueName: dc.UniqueName,
|
|
Tag: dc.Tag,
|
|
HasBang: dc.Tag != "",
|
|
}
|
|
}
|