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 DomainNameIDN = "dnscontrol_nameidn" // A copy of DomainConfig.NameIDN 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 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.MakeDomainFixForms(dc.Name) dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameIDN, 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[DomainNameIDN] = 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 { // 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] }