From fbb892059f36dcb50143f1c8465134b88b88559c Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 17 May 2026 19:05:06 -0400 Subject: [PATCH 01/34] Use RCv3 in diff2 --- models/record.go | 14 +++++++ pkg/rtype/ds.go | 11 ++++++ pkg/rtype/rp.go | 15 ++++++++ pkg/rtypecontrol/fixlegacy.go | 69 ++++++++++++++++++++++++++++++++--- 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/models/record.go b/models/record.go index a6d1d3a370..988124dcb2 100644 --- a/models/record.go +++ b/models/record.go @@ -6,6 +6,7 @@ import ( "log" "strings" + dnsv2 "codeberg.org/miekg/dns" "github.com/DNSControl/dnscontrol/v4/pkg/txtutil" "github.com/jinzhu/copier" dnsv1 "github.com/miekg/dns" @@ -16,9 +17,22 @@ import ( // 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"` + // TypeNum is the assigned number of the record's type. 1 for A, 5 for CNAME, etc. See dnsv2.TypeToString and dnsv2.StringToType. + TypeNum uint16 `json:"typenum"` + + // RDATA is (the fields of the record). + RDATA dnsv2.RDATA `json:"rdata"` + + // ComparableV3 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. + // The V3 distingues itself from .Comparable and all other legacy systems that we're leaving in place for now. + ComparableV3 any `json:"comparablev3"` + // TTL is the DNS record's TTL in seconds. 0 means provider default. TTL uint32 `json:"ttl,omitempty"` diff --git a/pkg/rtype/ds.go b/pkg/rtype/ds.go index 37acd88f99..5986b32158 100644 --- a/pkg/rtype/ds.go +++ b/pkg/rtype/ds.go @@ -3,6 +3,7 @@ package rtype import ( "fmt" + dnsrdatav2 "codeberg.org/miekg/dns/rdata" "github.com/DNSControl/dnscontrol/v4/models" "github.com/DNSControl/dnscontrol/v4/pkg/domaintags" "github.com/DNSControl/dnscontrol/v4/pkg/rtypecontrol" @@ -50,6 +51,16 @@ func (handle *DS) FromStruct(dcn *domaintags.DomainNameVarieties, rec *models.Re } rec.F = &DS{*ds} + // Hack to deal with the fact that fixlegacy.go can't import rtype. + switch rec.F.(type) { + case *DS: + rec.RDATA = dnsrdatav2.DS{KeyTag: rec.F.(*DS).KeyTag, Algorithm: rec.F.(*DS).Algorithm, DigestType: rec.F.(*DS).DigestType, Digest: rec.F.(*DS).Digest} + case *dnsv1.DS: + rec.RDATA = dnsrdatav2.DS{KeyTag: rec.F.(*dnsv1.DS).KeyTag, Algorithm: rec.F.(*dnsv1.DS).Algorithm, DigestType: rec.F.(*dnsv1.DS).DigestType, Digest: rec.F.(*dnsv1.DS).Digest} + default: + panic(fmt.Sprintf("unexpected type for DS.FromStruct: %T", rec.F)) + } + rec.ZonefilePartial = rec.GetTargetRFC1035Quoted() rec.Comparable = rec.ZonefilePartial diff --git a/pkg/rtype/rp.go b/pkg/rtype/rp.go index 919e160ec3..c8108b224f 100644 --- a/pkg/rtype/rp.go +++ b/pkg/rtype/rp.go @@ -3,6 +3,8 @@ package rtype import ( "fmt" + dnsv2 "codeberg.org/miekg/dns" + dnsrdatav2 "codeberg.org/miekg/dns/rdata" "github.com/DNSControl/dnscontrol/v4/models" "github.com/DNSControl/dnscontrol/v4/pkg/domaintags" "github.com/DNSControl/dnscontrol/v4/pkg/rtypecontrol" @@ -49,6 +51,19 @@ func (handle *RP) FromStruct(dcn *domaintags.DomainNameVarieties, rec *models.Re rec.ZonefilePartial = rec.GetTargetRFC1035Quoted() rec.Comparable = rec.ZonefilePartial + // Hack to deal with the fact that fixlegacy.go can't import rtype. + switch rec.F.(type) { + case *RP: + rec.RDATA = dnsrdatav2.RP{Mbox: rec.F.(*RP).Mbox, Txt: rec.F.(*RP).Txt} + case *dnsv1.RP: + rec.RDATA = dnsrdatav2.RP{Mbox: rec.F.(*dnsv1.RP).Mbox, Txt: rec.F.(*dnsv1.RP).Txt} + default: + panic(fmt.Sprintf("unexpected type for RP.FromStruct: %T", rec.F)) + } + + rec.TypeNum = dnsv2.TypeRP + rec.ComparableV3 = rec.RDATA.(dnsrdatav2.RP).String() + handle.CopyToLegacyFields(rec) return nil } diff --git a/pkg/rtypecontrol/fixlegacy.go b/pkg/rtypecontrol/fixlegacy.go index 12a335c521..6e7ec68373 100644 --- a/pkg/rtypecontrol/fixlegacy.go +++ b/pkg/rtypecontrol/fixlegacy.go @@ -1,6 +1,12 @@ package rtypecontrol -import "github.com/DNSControl/dnscontrol/v4/models" +import ( + "fmt" + + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + dnsrdatav2 "codeberg.org/miekg/dns/rdata" + "github.com/DNSControl/dnscontrol/v4/models" +) // FixLegacyDC populates .F to compenstate for providers that have not been // updated to support RecordConfigV2 when creating RecordConfig. @@ -25,12 +31,63 @@ func FixLegacyRecords(recs *models.Records) { // FixLegacyRecord populates .F to compenstate for providers that have not been // updated to support RecordConfigV2 when creating RecordConfig. func FixLegacyRecord(rec *models.RecordConfig) { - // Populate .F if needed: + // Populate .F if needed: (legacy) // That is... If rec.F == nil and this is a "modern" type. - if rec.F != nil { - return + if rec.F == nil { + if fixer, ok := Func[rec.Type]; ok { + fixer.CopyFromLegacyFields(rec) + } } - if fixer, ok := Func[rec.Type]; ok { - fixer.CopyFromLegacyFields(rec) + + // Populate .RDATA if needed: + if rec.RDATA == nil { + + // The .RDATA structure itself. + switch rec.Type { + case "A": + rec.RDATA = dnsrdatav2.A{Addr: rec.GetTargetIP()} + case "AAAA": + rec.RDATA = dnsrdatav2.AAAA{Addr: rec.GetTargetIP()} + + case "CAA": + rec.RDATA = dnsrdatav2.CAA{Flag: rec.CaaFlag, Tag: rec.CaaTag, Value: rec.GetTargetField()} + case "CNAME": + rec.RDATA = dnsrdatav2.CNAME{Target: rec.GetTargetField()} + + case "HTTPS": + //rec.RDATA = dnsrdatav2.HTTPS{Priority: rec.HttpsPriority, Target: rec.GetTargetField()} + + case "MX": + rec.RDATA = dnsrdatav2.MX{Preference: rec.MxPreference, Mx: rec.GetTargetField()} + + case "RP": + // no-op. See pkg/rtype/rp.go:FromStruct. + + case "SOA": + rec.RDATA = dnsrdatav2.SOA{Ns: rec.GetTargetField(), Mbox: rec.SoaMbox, Serial: rec.SoaSerial, Refresh: rec.SoaRefresh, Retry: rec.SoaRetry, Expire: rec.SoaExpire, Minttl: rec.SoaMinttl} + case "SRV": + rec.RDATA = dnsrdatav2.SRV{Priority: rec.SrvPriority, Weight: rec.SrvWeight, Port: rec.SrvPort, Target: rec.GetTargetField()} + + case "TXT": + rec.RDATA = dnsrdatav2.TXT{Txt: []string{rec.GetTargetField()}} + + default: + panic(fmt.Sprintf("RDATA CONVERSION NOT IMPLEMENTED TYPE=%q", rec.Type)) + } + + if rec.RDATA != nil { + + // TypeNum: + tn, err := dnsutilv2.StringToType(rec.Type) + if err != nil { + panic("fix me") + } + rec.TypeNum = tn + + // Comparable: + rec.Comparable = fmt.Sprintf("%s", rec.RDATA) + + } + } } From 444d61c77ea61e1f6eb2f66edb935b6c4c28ee8a Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 17 May 2026 20:55:45 -0400 Subject: [PATCH 02/34] SVCB and HTTPS now supports --- integrationTest/helpers_integration_test.go | 23 +++++++++ models/t_svcb.go | 54 +++++++++++++++++++++ pkg/rtypecontrol/fixlegacy.go | 8 ++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index 63bf2d5e0b..24e5e09dcb 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + dnsv2 "codeberg.org/miekg/dns" "github.com/DNSControl/dnscontrol/v4/models" "github.com/DNSControl/dnscontrol/v4/pkg/domaintags" "github.com/DNSControl/dnscontrol/v4/pkg/nameservers" @@ -520,6 +521,19 @@ func https(name string, priority uint16, target string, params string) *models.R r := makeRec(name, target, "HTTPS") r.SvcPriority = priority r.SvcParams = params + + // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go + rty := dnsv2.TypeSVCB + cp := params + if strings.Contains(cp, "ech=IGNORE") { + cp = strings.ReplaceAll(cp, "ech=IGNORE", "") + } + rrv2, err := dnsv2.NewData(rty, fmt.Sprintf("%d %s %s", priority, target, cp)) + if err != nil { + panic(fmt.Sprintf("could not parse SVCB record: %s (%d %s %s)", err, priority, target, cp)) + } + r.RDATA = rrv2 + return r } @@ -652,6 +666,15 @@ func svcb(name string, priority uint16, target string, params string) *models.Re r := makeRec(name, target, "SVCB") r.SvcPriority = priority r.SvcParams = params + + // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go + rty := dnsv2.TypeSVCB + rrv2, err := dnsv2.NewData(rty, fmt.Sprintf("%d %s %s", priority, target, params)) + if err != nil { + panic(fmt.Sprintf("could not parse SVCB record: %s", err)) + } + r.RDATA = rrv2 + return r } diff --git a/models/t_svcb.go b/models/t_svcb.go index a4b0033ebd..bc054db824 100644 --- a/models/t_svcb.go +++ b/models/t_svcb.go @@ -4,6 +4,9 @@ import ( "fmt" "strings" + dnsv2 "codeberg.org/miekg/dns" + dnsrdatav2 "codeberg.org/miekg/dns/rdata" + svcbv2 "codeberg.org/miekg/dns/svcb" dnsv1 "github.com/miekg/dns" ) @@ -24,6 +27,14 @@ func (rc *RecordConfig) SetTargetSVCB(priority uint16, target string, params []d if rc.Type != "SVCB" && rc.Type != "HTTPS" { panic("assertion failed: SetTargetSVCB called when .Type is not SVCB or HTTPS") } + + // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go + valuev2, err := convertSVCBv1v2(params) + if err != nil { + return fmt.Errorf("failed to convert SVCB parameters from v1 to v2: %w", err) + } + rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: target, Value: valuev2} + return nil } @@ -36,6 +47,23 @@ func (rc *RecordConfig) SetTargetSVCBString(origin, contents string) error { if err != nil { return fmt.Errorf("could not parse SVCB record: %w", err) } + + // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go + var rty uint16 + switch record.(type) { + case *dnsv1.HTTPS: + rty = dnsv1.TypeHTTPS + case *dnsv1.SVCB: + rty = dnsv1.TypeSVCB + default: + return fmt.Errorf("unexpected record type after parsing SVCB record: %T", record) + } + rrv2, err := dnsv2.NewData(rty, contents, origin) + if err != nil { + return fmt.Errorf("could not parse SVCB record: %w", err) + } + rc.RDATA = rrv2 + switch r := record.(type) { case *dnsv1.HTTPS: return rc.SetTargetSVCB(r.Priority, r.Target, r.Value) @@ -44,3 +72,29 @@ func (rc *RecordConfig) SetTargetSVCBString(origin, contents string) error { } return nil } + +func convertSVCBv1v2(params []dnsv1.SVCBKeyValue) ([]svcbv2.Pair, error) { + var value []svcbv2.Pair + for _, kv := range params { + k := kv.Key().String() + keyCode := svcbv2.StringToKey(k) + v := kv.String() + + pairFn := svcbv2.KeyToPair(keyCode) + if pairFn == nil { + return nil, fmt.Errorf("failed to lookup svc key: %s", k) + } + pair := pairFn() + if svcbv2.PairToKey(pair) != keyCode { + return nil, fmt.Errorf("key constant is not in sync: %s", keyCode) + } + err := svcbv2.Parse(pair, v, "") + if err != nil { + return nil, fmt.Errorf("failed to parse svc pair: %s", k) + } + + value = append(value, pair) + } + + return value, nil +} diff --git a/pkg/rtypecontrol/fixlegacy.go b/pkg/rtypecontrol/fixlegacy.go index 6e7ec68373..7643f3bd19 100644 --- a/pkg/rtypecontrol/fixlegacy.go +++ b/pkg/rtypecontrol/fixlegacy.go @@ -55,19 +55,25 @@ func FixLegacyRecord(rec *models.RecordConfig) { rec.RDATA = dnsrdatav2.CNAME{Target: rec.GetTargetField()} case "HTTPS": - //rec.RDATA = dnsrdatav2.HTTPS{Priority: rec.HttpsPriority, Target: rec.GetTargetField()} + // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB + panic("HTTPS should already be converted to RDATA") case "MX": rec.RDATA = dnsrdatav2.MX{Preference: rec.MxPreference, Mx: rec.GetTargetField()} case "RP": // no-op. See pkg/rtype/rp.go:FromStruct. + panic("RP should already be converted to RDATA") case "SOA": rec.RDATA = dnsrdatav2.SOA{Ns: rec.GetTargetField(), Mbox: rec.SoaMbox, Serial: rec.SoaSerial, Refresh: rec.SoaRefresh, Retry: rec.SoaRetry, Expire: rec.SoaExpire, Minttl: rec.SoaMinttl} case "SRV": rec.RDATA = dnsrdatav2.SRV{Priority: rec.SrvPriority, Weight: rec.SrvWeight, Port: rec.SrvPort, Target: rec.GetTargetField()} + case "SVCB": + // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB + panic("SVCB should already be converted to RDATA") + case "TXT": rec.RDATA = dnsrdatav2.TXT{Txt: []string{rec.GetTargetField()}} From 59c9ffa7db1ed1468fbcd72f3b6fb8c81cbb8ec6 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 18 May 2026 21:18:32 -0400 Subject: [PATCH 03/34] Native types converted --- pkg/rtypecontrol/fixlegacy.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pkg/rtypecontrol/fixlegacy.go b/pkg/rtypecontrol/fixlegacy.go index 7643f3bd19..cad63b9153 100644 --- a/pkg/rtypecontrol/fixlegacy.go +++ b/pkg/rtypecontrol/fixlegacy.go @@ -54,21 +54,49 @@ func FixLegacyRecord(rec *models.RecordConfig) { case "CNAME": rec.RDATA = dnsrdatav2.CNAME{Target: rec.GetTargetField()} + case "DHCID": + rec.RDATA = dnsrdatav2.DHCID{Digest: rec.GetTargetField()} + case "DNAME": + rec.RDATA = dnsrdatav2.DNAME{Target: rec.GetTargetField()} + case "DNSKEY": + rec.RDATA = dnsrdatav2.DNSKEY{Flags: rec.DnskeyFlags, Protocol: rec.DnskeyProtocol, Algorithm: rec.DnskeyAlgorithm, PublicKey: rec.GetTargetField()} + case "HTTPS": // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB panic("HTTPS should already be converted to RDATA") + case "LOC": + rec.RDATA = dnsrdatav2.LOC{Version: rec.LocVersion, Size: rec.LocSize, HorizPre: rec.LocHorizPre, VertPre: rec.LocVertPre, Latitude: rec.LocLatitude, Longitude: rec.LocLongitude, Altitude: rec.LocAltitude} + case "MX": rec.RDATA = dnsrdatav2.MX{Preference: rec.MxPreference, Mx: rec.GetTargetField()} + case "NS": + rec.RDATA = dnsrdatav2.NS{Ns: rec.GetTargetField()} + case "NAPTR": + rec.RDATA = dnsrdatav2.NAPTR{Order: rec.NaptrOrder, Preference: rec.NaptrPreference, Flags: rec.NaptrFlags, Service: rec.NaptrService, Regexp: rec.NaptrRegexp, Replacement: rec.GetTargetField()} + + case "OPENPGPKEY": + rec.RDATA = dnsrdatav2.OPENPGPKEY{PublicKey: rec.GetTargetField()} + + case "PTR": + rec.RDATA = dnsrdatav2.PTR{Ptr: rec.GetTargetField()} + case "RP": // no-op. See pkg/rtype/rp.go:FromStruct. panic("RP should already be converted to RDATA") + case "SMIMEA": + rec.RDATA = dnsrdatav2.SMIMEA{Usage: rec.SmimeaUsage, Selector: rec.SmimeaSelector, MatchingType: rec.SmimeaMatchingType, Certificate: rec.GetTargetField()} case "SOA": rec.RDATA = dnsrdatav2.SOA{Ns: rec.GetTargetField(), Mbox: rec.SoaMbox, Serial: rec.SoaSerial, Refresh: rec.SoaRefresh, Retry: rec.SoaRetry, Expire: rec.SoaExpire, Minttl: rec.SoaMinttl} case "SRV": rec.RDATA = dnsrdatav2.SRV{Priority: rec.SrvPriority, Weight: rec.SrvWeight, Port: rec.SrvPort, Target: rec.GetTargetField()} + case "SSHFP": + rec.RDATA = dnsrdatav2.SSHFP{Algorithm: rec.SshfpAlgorithm, Type: rec.SshfpFingerprint, FingerPrint: rec.GetTargetField()} + + case "TLSA": + rec.RDATA = dnsrdatav2.TLSA{Usage: rec.TlsaUsage, Selector: rec.TlsaSelector, MatchingType: rec.TlsaMatchingType, Certificate: rec.GetTargetField()} case "SVCB": // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB From 0532d066600733689235981f41ce749859a115eb Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 19 May 2026 09:40:22 -0400 Subject: [PATCH 04/34] wip! --- integrationTest/helpers_integration_test.go | 2 ++ models/record.go | 2 +- pkg/diff2/compareconfig.go | 8 +++++-- pkg/rtypecontrol/fixlegacy.go | 24 ++++++++++----------- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index 24e5e09dcb..5e941e6e9a 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -533,6 +533,7 @@ func https(name string, priority uint16, target string, params string) *models.R panic(fmt.Sprintf("could not parse SVCB record: %s (%d %s %s)", err, priority, target, cp)) } r.RDATA = rrv2 + r.ComparableV3 = fmt.Sprintf("%s", r.RDATA) return r } @@ -674,6 +675,7 @@ func svcb(name string, priority uint16, target string, params string) *models.Re panic(fmt.Sprintf("could not parse SVCB record: %s", err)) } r.RDATA = rrv2 + r.ComparableV3 = fmt.Sprintf("%s", r.RDATA) return r } diff --git a/models/record.go b/models/record.go index 988124dcb2..987ed3c00c 100644 --- a/models/record.go +++ b/models/record.go @@ -31,7 +31,7 @@ type RecordConfig struct { // RecordConfigs for equality. Typically this is the Zonefile line minus the // label and TTL. // The V3 distingues itself from .Comparable and all other legacy systems that we're leaving in place for now. - ComparableV3 any `json:"comparablev3"` + ComparableV3 string `json:"comparablev3"` // TTL is the DNS record's TTL in seconds. 0 means provider default. TTL uint32 `json:"ttl,omitempty"` diff --git a/pkg/diff2/compareconfig.go b/pkg/diff2/compareconfig.go index 1153d5feb5..d0488c3a67 100644 --- a/pkg/diff2/compareconfig.go +++ b/pkg/diff2/compareconfig.go @@ -170,8 +170,12 @@ func (cc *CompareConfig) verifyCNAMEAssertions() { // Generate a string that can be used to compare this record to others // for equality. func mkCompareBlobs(rc *models.RecordConfig, f func(*models.RecordConfig) string) (string, string) { - // Start with the comparable string - comp := rc.ToComparableNoTTL() + // // Start with the comparable string + // comp := rc.ToComparableNoTTL() + comp := rc.ComparableV3 + if comp == "" { + panic(fmt.Sprintf("mkCompareBlobs: record %s has empty ComparableV3", rc)) + } // If the custom function exists, add its output if f != nil { diff --git a/pkg/rtypecontrol/fixlegacy.go b/pkg/rtypecontrol/fixlegacy.go index cad63b9153..c264916bfe 100644 --- a/pkg/rtypecontrol/fixlegacy.go +++ b/pkg/rtypecontrol/fixlegacy.go @@ -60,6 +60,9 @@ func FixLegacyRecord(rec *models.RecordConfig) { rec.RDATA = dnsrdatav2.DNAME{Target: rec.GetTargetField()} case "DNSKEY": rec.RDATA = dnsrdatav2.DNSKEY{Flags: rec.DnskeyFlags, Protocol: rec.DnskeyProtocol, Algorithm: rec.DnskeyAlgorithm, PublicKey: rec.GetTargetField()} + case "DS": + // no-op. See pkg/rtype/ds.go:FromStruct. + panic("DS should already be converted to RDATA") case "HTTPS": // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB @@ -109,19 +112,16 @@ func FixLegacyRecord(rec *models.RecordConfig) { panic(fmt.Sprintf("RDATA CONVERSION NOT IMPLEMENTED TYPE=%q", rec.Type)) } - if rec.RDATA != nil { - - // TypeNum: - tn, err := dnsutilv2.StringToType(rec.Type) - if err != nil { - panic("fix me") - } - rec.TypeNum = tn - - // Comparable: - rec.Comparable = fmt.Sprintf("%s", rec.RDATA) - + // TypeNum: + tn, err := dnsutilv2.StringToType(rec.Type) + if err != nil { + panic("fix me") } + rec.TypeNum = tn + + // Comparable: + rec.ComparableV3 = fmt.Sprintf("%s", rec.RDATA) + fmt.Printf("DEBUG: COMPARE for %s --- %s\n", rec.Type, rec.ComparableV3) } } From 42ec97ead2a88b0fe4547de9aec4c2face070ff9 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 19 May 2026 09:49:56 -0400 Subject: [PATCH 05/34] REFACTOR: Add V3 fields to RecordConfig --- models/record.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/models/record.go b/models/record.go index 4c7ab33b86..b045054f9b 100644 --- a/models/record.go +++ b/models/record.go @@ -6,6 +6,7 @@ import ( "log" "strings" + dnsv2 "codeberg.org/miekg/dns" "github.com/DNSControl/dnscontrol/v4/pkg/txtutil" "github.com/jinzhu/copier" dnsv1 "github.com/miekg/dns" @@ -16,9 +17,25 @@ import ( // 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"` + // TypeNum is the assigned number of the record's type. 1 for A, 5 for CNAME, etc. See dnsv2.TypeToString and dnsv2.StringToType. + // NB(tlim): Not currently used. Placeholder for future feature. + TypeNum uint16 `json:"typenum,omitempty"` + + // RDATA is (the fields of the record). + // NB(tlim): Not currently used. Placeholder for future feature. + RDATA dnsv2.RDATA `json:"rdata,omitempty"` + + // ComparableV3 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. + // The V3 distingues itself from .Comparable, which it will eventually replace. + // NB(tlim): Not currently used. Placeholder for future feature. + ComparableV3 string `json:"comparablev3,omitempty"` + // TTL is the DNS record's TTL in seconds. 0 means provider default. TTL uint32 `json:"ttl,omitempty"` @@ -353,6 +370,7 @@ func (rc *RecordConfig) ToComparableNoTTL() string { return fmt.Sprintf("rtype=%s rdata=%s", rc.UnknownTypeName, rc.target) case "HTTPS", "SVCB": return rc.targetCombinedSVCBRaw() + } return rc.GetTargetCombined() } From 429112e505e95d5429f922a7dde984d62393e3b4 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 19 May 2026 10:04:10 -0400 Subject: [PATCH 06/34] m --- integrationTest/helpers_integration_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index 5e941e6e9a..7f4f1c3ca4 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -533,7 +533,11 @@ func https(name string, priority uint16, target string, params string) *models.R panic(fmt.Sprintf("could not parse SVCB record: %s (%d %s %s)", err, priority, target, cp)) } r.RDATA = rrv2 - r.ComparableV3 = fmt.Sprintf("%s", r.RDATA) + old := fmt.Sprintf("%s", r.RDATA) + r.ComparableV3 = r.RDATA.String() + if r.ComparableV3 != old { + panic("DEBUG CV3") + } return r } From 4c27250b28ae50f4e14c8faad1986cb523d6e42a Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 19 May 2026 17:27:25 -0400 Subject: [PATCH 07/34] diff2: fixing analyze_test.go --- pkg/diff2/analyze_test.go | 17 +++++++++++++++++ pkg/diff2/compareconfig.go | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/diff2/analyze_test.go b/pkg/diff2/analyze_test.go index 59db66a120..edb1e94c7d 100644 --- a/pkg/diff2/analyze_test.go +++ b/pkg/diff2/analyze_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" "github.com/DNSControl/dnscontrol/v4/models" "github.com/fatih/color" "github.com/kylelemons/godebug/diff" @@ -63,6 +65,21 @@ func makeRec(label, rtype, content string) *models.RecordConfig { if err := r.PopulateFromString(rtype, content, origin); err != nil { panic(err) } + + // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go + tn, err := dnsutilv2.StringToType(rtype) + if err != nil { + panic(fmt.Sprintf("BUG: HackFixRecord: %s IN %s %v", r.Name, r.Type, r)) + } + r.TypeNum = tn + rrv2, err := dnsv2.NewData(tn, content, origin+".") + if err != nil { + panic(fmt.Sprintf("could not parse: %s IN %s %s: %s", r.Name, rtype, content, err)) + } + r.RDATA = rrv2 + r.ComparableV3 = fmt.Sprintf("%s", r.RDATA) + // End of hack + return &r } diff --git a/pkg/diff2/compareconfig.go b/pkg/diff2/compareconfig.go index d0488c3a67..92729ca4cb 100644 --- a/pkg/diff2/compareconfig.go +++ b/pkg/diff2/compareconfig.go @@ -174,7 +174,7 @@ func mkCompareBlobs(rc *models.RecordConfig, f func(*models.RecordConfig) string // comp := rc.ToComparableNoTTL() comp := rc.ComparableV3 if comp == "" { - panic(fmt.Sprintf("mkCompareBlobs: record %s has empty ComparableV3", rc)) + panic(fmt.Sprintf("mkCompareBlobs: record %s IN %s %s has empty ComparableV3", rc.NameFQDN, rc.Type, rc)) } // If the custom function exists, add its output From 674e9b02237b29c1dcf926b5ad6ee132681f92d7 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 19 May 2026 17:34:57 -0400 Subject: [PATCH 08/34] linting --- integrationTest/helpers_integration_test.go | 4 ++-- pkg/diff2/analyze_test.go | 2 +- pkg/rtypecontrol/fixlegacy.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index 7f4f1c3ca4..46cb9dc6c6 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -533,7 +533,7 @@ func https(name string, priority uint16, target string, params string) *models.R panic(fmt.Sprintf("could not parse SVCB record: %s (%d %s %s)", err, priority, target, cp)) } r.RDATA = rrv2 - old := fmt.Sprintf("%s", r.RDATA) + old := r.RDATA.String() r.ComparableV3 = r.RDATA.String() if r.ComparableV3 != old { panic("DEBUG CV3") @@ -679,7 +679,7 @@ func svcb(name string, priority uint16, target string, params string) *models.Re panic(fmt.Sprintf("could not parse SVCB record: %s", err)) } r.RDATA = rrv2 - r.ComparableV3 = fmt.Sprintf("%s", r.RDATA) + r.ComparableV3 = r.RDATA.String() return r } diff --git a/pkg/diff2/analyze_test.go b/pkg/diff2/analyze_test.go index edb1e94c7d..9a6f4ff576 100644 --- a/pkg/diff2/analyze_test.go +++ b/pkg/diff2/analyze_test.go @@ -77,7 +77,7 @@ func makeRec(label, rtype, content string) *models.RecordConfig { panic(fmt.Sprintf("could not parse: %s IN %s %s: %s", r.Name, rtype, content, err)) } r.RDATA = rrv2 - r.ComparableV3 = fmt.Sprintf("%s", r.RDATA) + r.ComparableV3 = r.RDATA.String() // End of hack return &r diff --git a/pkg/rtypecontrol/fixlegacy.go b/pkg/rtypecontrol/fixlegacy.go index c264916bfe..f6e5673ff3 100644 --- a/pkg/rtypecontrol/fixlegacy.go +++ b/pkg/rtypecontrol/fixlegacy.go @@ -120,7 +120,7 @@ func FixLegacyRecord(rec *models.RecordConfig) { rec.TypeNum = tn // Comparable: - rec.ComparableV3 = fmt.Sprintf("%s", rec.RDATA) + rec.ComparableV3 = rec.RDATA.String() fmt.Printf("DEBUG: COMPARE for %s --- %s\n", rec.Type, rec.ComparableV3) } From ddf89c43b22e21c7772d1f05dbbf6f46111a24dc Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 19 May 2026 17:37:13 -0400 Subject: [PATCH 09/34] empty From aac371c60b5b754023dcf0a19ada4d4383fd3d17 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 20 May 2026 15:09:42 -0400 Subject: [PATCH 10/34] CICD: Add pipefail to catch all errors --- .github/workflows/pr_integration_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr_integration_tests.yml b/.github/workflows/pr_integration_tests.yml index 6c7dd6096d..8240cdf5c2 100644 --- a/.github/workflows/pr_integration_tests.yml +++ b/.github/workflows/pr_integration_tests.yml @@ -295,6 +295,7 @@ jobs: echo "DO_WORKERS=$DO_WORKERS" >> $GITHUB_ENV - name: Run integration tests for ${{ matrix.provider }} provider + shell: bash -eo pipefail {0} run: |- # echo "END: Running tests 0 to $END" From b68b682316a3cadc22216be1fdb4d041a8f6c8f4 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 24 May 2026 11:19:15 -0400 Subject: [PATCH 11/34] CLOUDFLAREAPI: Reduce integration tests to match the free tier (#4325) CC @tresni @allixsenos @jzhang-sre # Issue Some tests fail because they use features that are not available in the "free tier" account we use for testing. # Resolution Reduce the tests to avoid them. The following features will no longer be regularly tested: * CNAME flattening * Tags * "Single redirects" that use the "Matches" subcommand NOTE: Would you like these features to be tested regularly? We'd gladly accept the donation to enable us to buy a "business plan"-level account. (Cloudflare, if you're listening, we'd love a partnership!) --- .github/workflows/pr_integration_tests.yml | 8 +--- documentation/provider/cloudflareapi.md | 4 ++ integrationTest/helpers_integration_test.go | 42 ++++++++++----------- integrationTest/helpers_test.go | 2 +- integrationTest/integration_test.go | 15 +++++--- 5 files changed, 37 insertions(+), 34 deletions(-) diff --git a/.github/workflows/pr_integration_tests.yml b/.github/workflows/pr_integration_tests.yml index 8240cdf5c2..9bee5fe642 100644 --- a/.github/workflows/pr_integration_tests.yml +++ b/.github/workflows/pr_integration_tests.yml @@ -259,7 +259,6 @@ jobs: run: | END=3 echo "END: Default set to $END" - DO_WORKERS=false # Only for PRs if [[ "${{ github.event_name }}" == "pull_request" ]]; then @@ -268,7 +267,6 @@ jobs: if [[ " $LABELS " =~ " longtest " ]]; then END=999 echo "END: Pull request + longtest label. END=$END" - DO_WORKERS=true else echo "END: longtest not set. No change." fi @@ -283,7 +281,6 @@ jobs: if [[ "${{ github.ref_name }}" == "main" ]]; then END=999 echo "END: github.ref_name is main. END=$END" - DO_WORKERS=true fi # VERCEL it limited to 100 updates per hour. Therefore we never run more than the first few tests. if [[ "${{ matrix.provider }}" == "VERCEL" ]]; then @@ -292,7 +289,6 @@ jobs: fi echo "END: Final value: END=$END" echo "END=$END" >> $GITHUB_ENV - echo "DO_WORKERS=$DO_WORKERS" >> $GITHUB_ENV - name: Run integration tests for ${{ matrix.provider }} provider shell: bash -eo pipefail {0} @@ -307,8 +303,8 @@ jobs: -timeout 40m -v \ -verbose \ -profile ${{ matrix.provider }} \ - -cfworkers=$DO_WORKERS \ - -cfredirect=false \ + -cfworkers=true \ + -cfredirect=true \ -cfflatten=false \ -cftags=false \ -end $END \ diff --git a/documentation/provider/cloudflareapi.md b/documentation/provider/cloudflareapi.md index cece74799b..5ea737678d 100644 --- a/documentation/provider/cloudflareapi.md +++ b/documentation/provider/cloudflareapi.md @@ -2,6 +2,10 @@ This is the provider for [Cloudflare](https://www.cloudflare.com/). ## Important notes +* The following features are not regularly tested: (free tier accounts don't support these features. Contact the project if you'd like to sponsor a higher tier) + * CNAME flattening + * Tags + * "Single redirects" that use the "Matches" subcommand * SPF records are silently converted to RecordType `TXT` as Cloudflare API fails otherwise. See [DNSControl/dnscontrol#446](https://github.com/DNSControl/dnscontrol/issues/446). * This provider currently fails if there are more than 1000 corrections on one domain. This only affects "push". This usually when moving a domain with many records to Cloudflare. Try commenting out most records, then uncomment groups of 999. Typical updates are less than 1000 corrections and will not trigger this bug. See [DNSControl/dnscontrol#1440](https://github.com/DNSControl/dnscontrol/issues/1440). * DNS records that Cloudflare injects and maintains are ignored. That includes SOA records, NS records at the domain's apex, and the MX/DKIM records created as part of Cloudflare mail routing. diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index 46cb9dc6c6..79f6ffc076 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -448,27 +448,27 @@ func bunnyPullZone(name, pullZoneID string) *models.RecordConfig { return makeRec(name, pullZoneID, "BUNNY_DNS_PZ") } -func cfRedir(pattern, target string) *models.RecordConfig { - rec, err := rtypecontrol.NewRecordConfigFromRaw(rtypecontrol.FromRawOpts{ - Type: "CF_REDIRECT", - TTL: 1, - Args: []any{pattern, target}, - DCN: globalDCN, - }) - panicOnErr(err) - return rec -} - -func cfRedirTemp(pattern, target string) *models.RecordConfig { - rec, err := rtypecontrol.NewRecordConfigFromRaw(rtypecontrol.FromRawOpts{ - Type: "CF_TEMP_REDIRECT", - TTL: 1, - Args: []any{pattern, target}, - DCN: globalDCN, - }) - panicOnErr(err) - return rec -} +// func cfRedir(pattern, target string) *models.RecordConfig { +// rec, err := rtypecontrol.NewRecordConfigFromRaw(rtypecontrol.FromRawOpts{ +// Type: "CF_REDIRECT", +// TTL: 1, +// Args: []any{pattern, target}, +// DCN: globalDCN, +// }) +// panicOnErr(err) +// return rec +// } + +// func cfRedirTemp(pattern, target string) *models.RecordConfig { +// rec, err := rtypecontrol.NewRecordConfigFromRaw(rtypecontrol.FromRawOpts{ +// Type: "CF_TEMP_REDIRECT", +// TTL: 1, +// Args: []any{pattern, target}, +// DCN: globalDCN, +// }) +// panicOnErr(err) +// return rec +// } func aghAPassthrough(pattern, target string) *models.RecordConfig { r := makeRec(pattern, target, "ADGUARDHOME_A_PASSTHROUGH") diff --git a/integrationTest/helpers_test.go b/integrationTest/helpers_test.go index ead9caf579..2b7fee96ca 100644 --- a/integrationTest/helpers_test.go +++ b/integrationTest/helpers_test.go @@ -17,7 +17,7 @@ import ( var ( providerFlag = flag.String("provider", "", "Provider to run (if empty, deduced from -profile)") profileFlag = flag.String("profile", "", "Entry in profiles.json to use (if empty, copied from -provider)") - enableCFWorkers = flag.Bool("cfworkers", true, "enable CF worker tests (default true)") + enableCFWorkers = flag.Bool("cfworkers", false, "enable CF worker tests (default false)") enableCFRedirectMode = flag.Bool("cfredirect", false, "enable CF SingleRedirect tests (default false)") enableCFFlatten = flag.Bool("cfflatten", false, "enable CF CNAME flattening tests (requires paid plan, default false)") enableCFTags = flag.Bool("cftags", false, "enable CF tag tests (requires paid plan, default false)") diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index a33d8b1a71..3517aab4d9 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -1231,12 +1231,15 @@ func makeTests() []*TestGroup { // go test -v -verbose -profile CLOUDFLAREAPI -cfredirect=true // Convert: Test Single Redirects - testgroup("CF_REDIRECT_CONVERT", - only("CLOUDFLAREAPI"), - alltrue(cfSingleRedirectEnabled()), - tc("start301", cfRedir("cnn.**current-domain**/*", "https://www.cnn.com/$1")), - tc("convert302", cfRedirTemp("cnn.**current-domain**/*", "https://www.cnn.com/$1")), - ), + // This test is commented out because of this error: + // "helpers_integration_test.go:241: not entitled: the use of operator Matches is not allowed, a Business plan or a WAF Advanced plan is required" + // There's no obvious way to have this test only run when a Business plan is used. + // testgroup("CF_REDIRECT_CONVERT", + // only("CLOUDFLAREAPI"), + // alltrue(cfSingleRedirectEnabled()), + // tc("start301", cfRedir("cnn.**current-domain**/*", "https://www.cnn.com/$1")), + // tc("convert302", cfRedirTemp("cnn.**current-domain**/*", "https://www.cnn.com/$1")), + // ), testgroup("CLOUDFLAREAPI_SINGLE_REDIRECT", only("CLOUDFLAREAPI"), From 52cbb6e3f4af062046b49cac2a099d705b89d0b8 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 24 May 2026 12:59:48 -0400 Subject: [PATCH 12/34] BUGFIX: ech=s (#4324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Issue Bug: https://github.com/DNSControl/dnscontrol/issues/3804 **Describe the bug** When using `HTTPS("…", 1, ".", "alpn=h3 … ech=IGNORE")`, but ech has not yet been set by the owner, then dnscontrol keeps saying it needs to change it … by adding a space that isn't there every run… **To Reproduce** 1. Create a HTTPS record with [`ech=IGNORE`](https://docs.dnscontrol.org/language-reference/domain-modifiers/https) - `+ CREATE ccccc.example.com HTTPS 1 . alpn="h3" ttl=1` 3. run dnscontrol again - `± MODIFY ccccc.example.com HTTPS (1 . alpn="h3" ttl=1) -> (1 . alpn="h3" ttl=1) id=X` 5. Observe changes, every, run… - `± MODIFY ccccc.example.com HTTPS (1 . alpn="h3" ttl=1) -> (1 . alpn="h3" ttl=1) id=X` **Expected behavior** 1. `+ CREATE ccccc.example.com HTTPS 1 . alpn="h3" ttl=1` - observe no double space between value and ttl 2. re-run - observe no change to record **DNS Provider** - Cloudflare **Additional context** The ECH field needs to be managed by whoever manages the certs (probably the webserver), and when making these records but before first use means it doesn't have a value. Note that this issue was missed in testing because [ech has a value](https://github.com/StackExchange/dnscontrol/blob/6ef064877859f3dbc096112c8896160af9e91e3f/integrationTest/integration_test.go#L296) there… # Resolution Remove the quotes in that one situation. --- pkg/diff2/analyze.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/diff2/analyze.go b/pkg/diff2/analyze.go index 8dee29d84e..93fd4ee1f7 100644 --- a/pkg/diff2/analyze.go +++ b/pkg/diff2/analyze.go @@ -267,7 +267,7 @@ func diffTargets(existing, desired []targetConfig) ChangeList { var unquoted, quoted string if _, ok := echs[v.rec.NameFQDN]; ok { unquoted = fmt.Sprintf("ech=%s", echs[v.rec.NameFQDN]) - quoted = fmt.Sprintf("ech=%q", echs[v.rec.NameFQDN]) + quoted = fmt.Sprintf("ech=%s", echs[v.rec.NameFQDN]) } else { unquoted = "" quoted = "" From 4e2a923331caada04b94856f35ac064fa141e230 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 25 May 2026 08:24:00 -0400 Subject: [PATCH 13/34] CICD: Skip failing HEDNS test (#4329) # Issue HEDNS fails the `testgroup("Ech")`, test. # Resolution Skip this test for now. Bug https://github.com/DNSControl/dnscontrol/issues/4328 opened to track this. --- integrationTest/integration_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 3517aab4d9..fdb1f4ee76 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -317,6 +317,7 @@ func makeTests() []*TestGroup { // // Let's just ignore ECH test for Vercel for now. "VERCEL", + "HEDNS", // BUG: https://github.com/DNSControl/dnscontrol/issues/4328 ), tc("Create a HTTPS record", https("@", 1, "example.com.", "alpn=h2,h3")), tc("Add an ECH key", https("@", 1, "example.com.", "alpn=h2,h3 ech=some+base64+encoded+value///")), From 92e8c82c274b5ca98c73bc890996f5f02237d89a Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 26 May 2026 20:03:49 -0400 Subject: [PATCH 14/34] m --- commands/getZones.go | 2 +- commands/init.go | 2 +- integrationTest/helpers_integration_test.go | 30 ++++ integrationTest/provider_test.go | 3 + models/t_svcb.go | 1 + pkg/diff2/externaldns.go | 2 +- pkg/rtype/ds.go | 1 + pkg/rtypecontrol/fixlegacy.go | 185 ++++++++++---------- pkg/zonerecs/zonerecords.go | 2 +- providers/bind/bindProvider.go | 2 + providers/bind/soa.go | 4 + 11 files changed, 137 insertions(+), 97 deletions(-) diff --git a/commands/getZones.go b/commands/getZones.go index 7fc5d11fd4..77c00db9f8 100644 --- a/commands/getZones.go +++ b/commands/getZones.go @@ -220,7 +220,7 @@ func GetZone(args GetZoneArgs) error { if err != nil { return fmt.Errorf("failed GetZone gzr: %w", err) } - rtypecontrol.FixLegacyRecords(&recs) // Call this after GetZoneRecords() to fix providers that haven't been updated for RecordConfigV2. + rtypecontrol.FixLegacyRecords(&recs, zone) // Call this after GetZoneRecords() to fix providers that haven't been updated for RecordConfigV2. zoneRecs[i] = recs } diff --git a/commands/init.go b/commands/init.go index df31ecc0a9..979b0ea78b 100644 --- a/commands/init.go +++ b/commands/init.go @@ -322,7 +322,7 @@ func fetchZoneRecordsReal(entry InitCredsEntry, zone string) (models.Records, er if err != nil { return nil, err } - rtypecontrol.FixLegacyRecords(&recs) + rtypecontrol.FixLegacyRecords(&recs, zone) return recs, nil } diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index 79f6ffc076..465693b7b9 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -14,6 +14,7 @@ import ( "time" dnsv2 "codeberg.org/miekg/dns" + dnsrdatav2 "codeberg.org/miekg/dns/rdata" "github.com/DNSControl/dnscontrol/v4/models" "github.com/DNSControl/dnscontrol/v4/pkg/domaintags" "github.com/DNSControl/dnscontrol/v4/pkg/nameservers" @@ -577,6 +578,21 @@ func makeRec(name, target, typ string) *models.RecordConfig { } SetLabel(r, name, "**current-domain**.") r.MustSetTarget(target) + + r.FixUp(globalDCN.NameASCII) // Populates .RDATA and .TypeNum if needed. + // // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go + // switch typ { + // case "CNAME": + // r.RDATA = &dnsrdatav2.CNAME{Target: r.GetTargetField()} + // r.ComparableV3 = r.RDATA.String() + // case "NS": + // r.RDATA = &dnsrdatav2.NS{Ns: r.GetTargetField()} + // r.ComparableV3 = r.RDATA.String() + // case "DS": + // r.RDATA = &dnsrdatav2.DS{KeyTag: r.DsKeyTag, Algorithm: r.DsAlgorithm, DigestType: r.DsDigestType, Digest: r.DsDigest} + // r.ComparableV3 = r.RDATA.String() + // } + return r } @@ -652,6 +668,20 @@ func smimea(name string, usage, selector, matchingtype uint8, target string) *mo func soa(name string, ns, mbox string, serial, refresh, retry, expire, minttl uint32) *models.RecordConfig { r := makeRec(name, "", "SOA") panicOnErr(r.SetTargetSOA(ns, mbox, serial, refresh, retry, expire, minttl)) + + // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go + r.RDATA = &dnsrdatav2.SOA{ + Ns: ns, + Mbox: mbox, + Serial: serial, + Refresh: refresh, + Retry: retry, + Expire: expire, + Minttl: minttl, + } + r.TypeNum = dnsv2.TypeSOA + r.ComparableV3 = fmt.Sprintf("%s %s %d %d %d %d %d", ns, mbox, serial, refresh, retry, expire, minttl) + return r } diff --git a/integrationTest/provider_test.go b/integrationTest/provider_test.go index 220d520fe9..858d3ddc2f 100644 --- a/integrationTest/provider_test.go +++ b/integrationTest/provider_test.go @@ -58,6 +58,9 @@ func TestDualProviders(t *testing.T) { run() // run again to make sure no corrections t.Log("Running again to ensure stability") + for _, r := range dc.Records { + r.FixUp(dc.Name) + } rs, cs, actualChangeCount, err := zonerecs.CorrectZoneRecords(p, dc) if err != nil { t.Fatal(err) diff --git a/models/t_svcb.go b/models/t_svcb.go index 5932387415..e55991690a 100644 --- a/models/t_svcb.go +++ b/models/t_svcb.go @@ -41,6 +41,7 @@ func (rc *RecordConfig) SetTargetSVCB(priority uint16, target string, params []d return fmt.Errorf("failed to convert SVCB parameters from v1 to v2: %w", err) } rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: target, Value: valuev2} + rc.ComparableV3 = rc.RDATA.String() return nil } diff --git a/pkg/diff2/externaldns.go b/pkg/diff2/externaldns.go index 72ba61249d..b8e5c06a40 100644 --- a/pkg/diff2/externaldns.go +++ b/pkg/diff2/externaldns.go @@ -33,7 +33,7 @@ type externalDNSManagedRecord struct { // isExternalDNSTxtRecord checks if a TXT record is an external-dns ownership record. // It returns true and the managed record info if it is, false otherwise. // customPrefix is an optional prefix that external-dns was configured with (e.g., "extdns-"). -func isExternalDNSTxtRecord(rec *models.RecordConfig, domain string, customPrefix string) (bool, *externalDNSManagedRecord) { +func isExternalDNSTxtRecord(rec *models.RecordConfig, _ string, customPrefix string) (bool, *externalDNSManagedRecord) { if rec.Type != "TXT" { return false, nil } diff --git a/pkg/rtype/ds.go b/pkg/rtype/ds.go index 5986b32158..d3b81096b4 100644 --- a/pkg/rtype/ds.go +++ b/pkg/rtype/ds.go @@ -60,6 +60,7 @@ func (handle *DS) FromStruct(dcn *domaintags.DomainNameVarieties, rec *models.Re default: panic(fmt.Sprintf("unexpected type for DS.FromStruct: %T", rec.F)) } + rec.ComparableV3 = rec.RDATA.String() rec.ZonefilePartial = rec.GetTargetRFC1035Quoted() rec.Comparable = rec.ZonefilePartial diff --git a/pkg/rtypecontrol/fixlegacy.go b/pkg/rtypecontrol/fixlegacy.go index f6e5673ff3..2449270904 100644 --- a/pkg/rtypecontrol/fixlegacy.go +++ b/pkg/rtypecontrol/fixlegacy.go @@ -1,10 +1,6 @@ package rtypecontrol import ( - "fmt" - - dnsutilv2 "codeberg.org/miekg/dns/dnsutil" - dnsrdatav2 "codeberg.org/miekg/dns/rdata" "github.com/DNSControl/dnscontrol/v4/models" ) @@ -14,7 +10,7 @@ import ( // called. Those functions can't call it directly because that would cause an // import cycle. func FixLegacyDC(dc *models.DomainConfig) { - FixLegacyRecords(&dc.Records) + FixLegacyRecords(&dc.Records, dc.Name) } // FixLegacyRecords populates .F to compenstate for providers that have not been @@ -22,15 +18,16 @@ func FixLegacyDC(dc *models.DomainConfig) { // It is called anywhere provider.GetZoneRecords() is called. GetZoneRecords() // can't call it directly because that would involve modifying every provider. // Instead, providers should be fixed to generate records properly. -func FixLegacyRecords(recs *models.Records) { +func FixLegacyRecords(recs *models.Records, origin string) { for _, rec := range *recs { - FixLegacyRecord(rec) + FixLegacyRecord(rec, origin) } } // FixLegacyRecord populates .F to compenstate for providers that have not been // updated to support RecordConfigV2 when creating RecordConfig. -func FixLegacyRecord(rec *models.RecordConfig) { +func FixLegacyRecord(rec *models.RecordConfig, origin string) { + //fmt.Printf("DEBUG: FixLegacyRecord for %s %s\n", rec.Type, rec.GetTargetField()) // Populate .F if needed: (legacy) // That is... If rec.F == nil and this is a "modern" type. if rec.F == nil { @@ -39,89 +36,91 @@ func FixLegacyRecord(rec *models.RecordConfig) { } } - // Populate .RDATA if needed: - if rec.RDATA == nil { - - // The .RDATA structure itself. - switch rec.Type { - case "A": - rec.RDATA = dnsrdatav2.A{Addr: rec.GetTargetIP()} - case "AAAA": - rec.RDATA = dnsrdatav2.AAAA{Addr: rec.GetTargetIP()} - - case "CAA": - rec.RDATA = dnsrdatav2.CAA{Flag: rec.CaaFlag, Tag: rec.CaaTag, Value: rec.GetTargetField()} - case "CNAME": - rec.RDATA = dnsrdatav2.CNAME{Target: rec.GetTargetField()} - - case "DHCID": - rec.RDATA = dnsrdatav2.DHCID{Digest: rec.GetTargetField()} - case "DNAME": - rec.RDATA = dnsrdatav2.DNAME{Target: rec.GetTargetField()} - case "DNSKEY": - rec.RDATA = dnsrdatav2.DNSKEY{Flags: rec.DnskeyFlags, Protocol: rec.DnskeyProtocol, Algorithm: rec.DnskeyAlgorithm, PublicKey: rec.GetTargetField()} - case "DS": - // no-op. See pkg/rtype/ds.go:FromStruct. - panic("DS should already be converted to RDATA") - - case "HTTPS": - // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB - panic("HTTPS should already be converted to RDATA") - - case "LOC": - rec.RDATA = dnsrdatav2.LOC{Version: rec.LocVersion, Size: rec.LocSize, HorizPre: rec.LocHorizPre, VertPre: rec.LocVertPre, Latitude: rec.LocLatitude, Longitude: rec.LocLongitude, Altitude: rec.LocAltitude} - - case "MX": - rec.RDATA = dnsrdatav2.MX{Preference: rec.MxPreference, Mx: rec.GetTargetField()} - - case "NS": - rec.RDATA = dnsrdatav2.NS{Ns: rec.GetTargetField()} - case "NAPTR": - rec.RDATA = dnsrdatav2.NAPTR{Order: rec.NaptrOrder, Preference: rec.NaptrPreference, Flags: rec.NaptrFlags, Service: rec.NaptrService, Regexp: rec.NaptrRegexp, Replacement: rec.GetTargetField()} - - case "OPENPGPKEY": - rec.RDATA = dnsrdatav2.OPENPGPKEY{PublicKey: rec.GetTargetField()} - - case "PTR": - rec.RDATA = dnsrdatav2.PTR{Ptr: rec.GetTargetField()} - - case "RP": - // no-op. See pkg/rtype/rp.go:FromStruct. - panic("RP should already be converted to RDATA") - - case "SMIMEA": - rec.RDATA = dnsrdatav2.SMIMEA{Usage: rec.SmimeaUsage, Selector: rec.SmimeaSelector, MatchingType: rec.SmimeaMatchingType, Certificate: rec.GetTargetField()} - case "SOA": - rec.RDATA = dnsrdatav2.SOA{Ns: rec.GetTargetField(), Mbox: rec.SoaMbox, Serial: rec.SoaSerial, Refresh: rec.SoaRefresh, Retry: rec.SoaRetry, Expire: rec.SoaExpire, Minttl: rec.SoaMinttl} - case "SRV": - rec.RDATA = dnsrdatav2.SRV{Priority: rec.SrvPriority, Weight: rec.SrvWeight, Port: rec.SrvPort, Target: rec.GetTargetField()} - case "SSHFP": - rec.RDATA = dnsrdatav2.SSHFP{Algorithm: rec.SshfpAlgorithm, Type: rec.SshfpFingerprint, FingerPrint: rec.GetTargetField()} - - case "TLSA": - rec.RDATA = dnsrdatav2.TLSA{Usage: rec.TlsaUsage, Selector: rec.TlsaSelector, MatchingType: rec.TlsaMatchingType, Certificate: rec.GetTargetField()} - - case "SVCB": - // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB - panic("SVCB should already be converted to RDATA") - - case "TXT": - rec.RDATA = dnsrdatav2.TXT{Txt: []string{rec.GetTargetField()}} - - default: - panic(fmt.Sprintf("RDATA CONVERSION NOT IMPLEMENTED TYPE=%q", rec.Type)) - } - - // TypeNum: - tn, err := dnsutilv2.StringToType(rec.Type) - if err != nil { - panic("fix me") - } - rec.TypeNum = tn - - // Comparable: - rec.ComparableV3 = rec.RDATA.String() - fmt.Printf("DEBUG: COMPARE for %s --- %s\n", rec.Type, rec.ComparableV3) - - } + rec.FixUp(origin) // Populates .RDATA and .TypeNum if needed. + + // // Populate .RDATA if needed: + // if rec.RDATA == nil { + + // // The .RDATA structure itself. + // switch rec.Type { + // case "A": + // rec.RDATA = dnsrdatav2.A{Addr: rec.GetTargetIP()} + // case "AAAA": + // rec.RDATA = dnsrdatav2.AAAA{Addr: rec.GetTargetIP()} + + // case "CAA": + // rec.RDATA = dnsrdatav2.CAA{Flag: rec.CaaFlag, Tag: rec.CaaTag, Value: rec.GetTargetField()} + // case "CNAME": + // rec.RDATA = dnsrdatav2.CNAME{Target: rec.GetTargetField()} + + // case "DHCID": + // rec.RDATA = dnsrdatav2.DHCID{Digest: rec.GetTargetField()} + // case "DNAME": + // rec.RDATA = dnsrdatav2.DNAME{Target: rec.GetTargetField()} + // case "DNSKEY": + // rec.RDATA = dnsrdatav2.DNSKEY{Flags: rec.DnskeyFlags, Protocol: rec.DnskeyProtocol, Algorithm: rec.DnskeyAlgorithm, PublicKey: rec.GetTargetField()} + // case "DS": + // // no-op. See pkg/rtype/ds.go:FromStruct. + // panic("DS should already be converted to RDATA") + + // case "HTTPS": + // // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB + // panic("HTTPS should already be converted to RDATA") + + // case "LOC": + // rec.RDATA = dnsrdatav2.LOC{Version: rec.LocVersion, Size: rec.LocSize, HorizPre: rec.LocHorizPre, VertPre: rec.LocVertPre, Latitude: rec.LocLatitude, Longitude: rec.LocLongitude, Altitude: rec.LocAltitude} + + // case "MX": + // rec.RDATA = dnsrdatav2.MX{Preference: rec.MxPreference, Mx: rec.GetTargetField()} + + // case "NS": + // rec.RDATA = dnsrdatav2.NS{Ns: rec.GetTargetField()} + // case "NAPTR": + // rec.RDATA = dnsrdatav2.NAPTR{Order: rec.NaptrOrder, Preference: rec.NaptrPreference, Flags: rec.NaptrFlags, Service: rec.NaptrService, Regexp: rec.NaptrRegexp, Replacement: rec.GetTargetField()} + + // case "OPENPGPKEY": + // rec.RDATA = dnsrdatav2.OPENPGPKEY{PublicKey: rec.GetTargetField()} + + // case "PTR": + // rec.RDATA = dnsrdatav2.PTR{Ptr: rec.GetTargetField()} + + // case "RP": + // // no-op. See pkg/rtype/rp.go:FromStruct. + // panic("RP should already be converted to RDATA") + + // case "SMIMEA": + // rec.RDATA = dnsrdatav2.SMIMEA{Usage: rec.SmimeaUsage, Selector: rec.SmimeaSelector, MatchingType: rec.SmimeaMatchingType, Certificate: rec.GetTargetField()} + // case "SOA": + // rec.RDATA = dnsrdatav2.SOA{Ns: rec.GetTargetField(), Mbox: rec.SoaMbox, Serial: rec.SoaSerial, Refresh: rec.SoaRefresh, Retry: rec.SoaRetry, Expire: rec.SoaExpire, Minttl: rec.SoaMinttl} + // case "SRV": + // rec.RDATA = dnsrdatav2.SRV{Priority: rec.SrvPriority, Weight: rec.SrvWeight, Port: rec.SrvPort, Target: rec.GetTargetField()} + // case "SSHFP": + // rec.RDATA = dnsrdatav2.SSHFP{Algorithm: rec.SshfpAlgorithm, Type: rec.SshfpFingerprint, FingerPrint: rec.GetTargetField()} + + // case "TLSA": + // rec.RDATA = dnsrdatav2.TLSA{Usage: rec.TlsaUsage, Selector: rec.TlsaSelector, MatchingType: rec.TlsaMatchingType, Certificate: rec.GetTargetField()} + + // case "SVCB": + // // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB + // panic("SVCB should already be converted to RDATA") + + // case "TXT": + // rec.RDATA = dnsrdatav2.TXT{Txt: []string{rec.GetTargetField()}} + + // default: + // panic(fmt.Sprintf("RDATA CONVERSION NOT IMPLEMENTED TYPE=%q", rec.Type)) + // } + + // // TypeNum: + // tn, err := dnsutilv2.StringToType(rec.Type) + // if err != nil { + // panic("fix me") + // } + // rec.TypeNum = tn + + // // Comparable: + // rec.ComparableV3 = rec.RDATA.String() + // fmt.Printf("DEBUG: COMPARE for %s --- %s\n", rec.Type, rec.ComparableV3) + + //} } diff --git a/pkg/zonerecs/zonerecords.go b/pkg/zonerecs/zonerecords.go index d77ecd1a02..1812d49222 100644 --- a/pkg/zonerecs/zonerecords.go +++ b/pkg/zonerecs/zonerecords.go @@ -13,7 +13,7 @@ func CorrectZoneRecords(driver models.DNSProvider, dc *models.DomainConfig) ([]* if err != nil { return nil, nil, 0, err } - rtypecontrol.FixLegacyRecords(&existingRecords) // Call this after GetZoneRecords() to fix providers that haven't been updated for RecordConfigV2. + rtypecontrol.FixLegacyRecords(&existingRecords, dc.Name) // Call this after GetZoneRecords() to fix providers that haven't been updated for RecordConfigV2. // downcase models.Downcase(existingRecords) diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go index 9803cb0ae6..6bc24fa55f 100644 --- a/providers/bind/bindProvider.go +++ b/providers/bind/bindProvider.go @@ -268,6 +268,8 @@ func ParseZoneContents(content string, zoneName string, zonefileName string) (mo } } + rec.FixUp(zoneName) // hack + foundRecords = append(foundRecords, &rec) } diff --git a/providers/bind/soa.go b/providers/bind/soa.go index d0b687a19e..bcf9d4ace0 100644 --- a/providers/bind/soa.go +++ b/providers/bind/soa.go @@ -1,6 +1,7 @@ package bind import ( + "fmt" "strings" "github.com/DNSControl/dnscontrol/v4/models" @@ -43,6 +44,9 @@ func makeSoa(origin string, defSoa *SoaDefaults, existing, desired *models.Recor panic(err) // Should never happen. } + // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go + soaRec.ComparableV3 = fmt.Sprintf("%s %s %d %d %d %d %d", soaRec.GetTargetField(), soaRec.SoaMbox, soaRec.SoaSerial, soaRec.SoaRefresh, soaRec.SoaRetry, soaRec.SoaExpire, soaRec.SoaMinttl) + return &soaRec, generateSerial(soaRec.SoaSerial) } From 45d6bf4945eecca02944de02a5d83e39c65e2f92 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 26 May 2026 20:04:50 -0400 Subject: [PATCH 15/34] m --- models/fixhack.go | 110 ++++++++++++++++++++++++++++++++++++++ pkg/privatetypes/alias.go | 5 ++ 2 files changed, 115 insertions(+) create mode 100644 models/fixhack.go create mode 100644 pkg/privatetypes/alias.go diff --git a/models/fixhack.go b/models/fixhack.go new file mode 100644 index 0000000000..4e17ddee84 --- /dev/null +++ b/models/fixhack.go @@ -0,0 +1,110 @@ +package models + +import ( + "fmt" + + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + dnsrdatav2 "codeberg.org/miekg/dns/rdata" + "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes" + dnsv1 "github.com/miekg/dns" + dnsutilv1 "github.com/miekg/dns/dnsutil" +) + +// FixUp populates the "V3 Fields": .TypeNum, .RDATA and .ComparableV3. +func (rc *RecordConfig) FixUp(origin string) { + + // TypeNum: + if rc.TypeNum == 0 && rc.Type != "ALIAS" { + tn, err := dnsutilv2.StringToType(rc.Type) + if err != nil { + panic(fmt.Sprintf("BUG: Unknown type %s", rc.Type)) + } + rc.TypeNum = tn + } + + // Populate .RDATA if needed: + if rc.RDATA == nil { + + switch rc.Type { + case "A": + rc.RDATA = dnsrdatav2.A{Addr: rc.GetTargetIP()} + case "ALIAS": + rc.RDATA = privatetypes.ALIAS{Target: rc.GetTargetField()} + case "AAAA": + rc.RDATA = dnsrdatav2.AAAA{Addr: rc.GetTargetIP()} + + case "CAA": + rc.RDATA = dnsrdatav2.CAA{Flag: rc.CaaFlag, Tag: rc.CaaTag, Value: rc.GetTargetField()} + case "CNAME": + targ := dnsutilv1.AddOrigin(rc.GetTargetField(), origin) + rc.RDATA = dnsrdatav2.CNAME{Target: targ} + case "DHCID": + rc.RDATA = dnsrdatav2.DHCID{Digest: rc.GetTargetField()} + case "DNAME": + rc.RDATA = dnsrdatav2.DNAME{Target: rc.GetTargetField()} + case "DNSKEY": + rc.RDATA = dnsrdatav2.DNSKEY{Flags: rc.DnskeyFlags, Protocol: rc.DnskeyProtocol, Algorithm: rc.DnskeyAlgorithm, PublicKey: rc.GetTargetField()} + case "DS": + rc.RDATA = dnsrdatav2.DS{KeyTag: rc.DsKeyTag, Algorithm: rc.DsAlgorithm, DigestType: rc.DsDigestType, Digest: rc.GetTargetField()} + + case "HTTPS": + valuev2, err := convertSVCBv1v2(rc.GetSVCBValue()) + if err != nil { + panic("BUG: Failed to convert SVCB value to v2: " + err.Error()) + } + rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: rc.GetTargetField(), Value: valuev2} + rc.ComparableV3 = rc.RDATA.String() + + case "LOC": + rc.RDATA = dnsrdatav2.LOC{Version: rc.LocVersion, Size: rc.LocSize, HorizPre: rc.LocHorizPre, VertPre: rc.LocVertPre, Latitude: rc.LocLatitude, Longitude: rc.LocLongitude, Altitude: rc.LocAltitude} + + case "MX": + rc.RDATA = dnsrdatav2.MX{Preference: rc.MxPreference, Mx: rc.GetTargetField()} + + case "NS": + rc.RDATA = dnsrdatav2.NS{Ns: rc.GetTargetField()} + case "NAPTR": + rc.RDATA = dnsrdatav2.NAPTR{Order: rc.NaptrOrder, Preference: rc.NaptrPreference, Flags: rc.NaptrFlags, Service: rc.NaptrService, Regexp: rc.NaptrRegexp, Replacement: rc.GetTargetField()} + + case "OPENPGPKEY": + rc.RDATA = dnsrdatav2.OPENPGPKEY{PublicKey: rc.GetTargetField()} + + case "PTR": + rc.RDATA = dnsrdatav2.PTR{Ptr: rc.GetTargetField()} + + case "RP": + rc.RDATA = dnsrdatav2.RP{Mbox: rc.F.(dnsv1.RP).Mbox, Txt: rc.F.(dnsv1.RP).Txt} + + case "SMIMEA": + rc.RDATA = dnsrdatav2.SMIMEA{Usage: rc.SmimeaUsage, Selector: rc.SmimeaSelector, MatchingType: rc.SmimeaMatchingType, Certificate: rc.GetTargetField()} + case "SOA": + rc.RDATA = dnsrdatav2.SOA{Ns: rc.GetTargetField(), Mbox: rc.SoaMbox, Serial: rc.SoaSerial, Refresh: rc.SoaRefresh, Retry: rc.SoaRetry, Expire: rc.SoaExpire, Minttl: rc.SoaMinttl} + case "SRV": + rc.RDATA = dnsrdatav2.SRV{Priority: rc.SrvPriority, Weight: rc.SrvWeight, Port: rc.SrvPort, Target: rc.GetTargetField()} + case "SSHFP": + rc.RDATA = dnsrdatav2.SSHFP{Algorithm: rc.SshfpAlgorithm, Type: rc.SshfpFingerprint, FingerPrint: rc.GetTargetField()} + case "SVCB": + valuev2, err := convertSVCBv1v2(rc.GetSVCBValue()) + if err != nil { + panic("BUG: Failed to convert SVCB value to v2: " + err.Error()) + } + rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: rc.GetTargetField(), Value: valuev2} + rc.ComparableV3 = rc.RDATA.String() + + case "TLSA": + rc.RDATA = dnsrdatav2.TLSA{Usage: rc.TlsaUsage, Selector: rc.TlsaSelector, MatchingType: rc.TlsaMatchingType, Certificate: rc.GetTargetField()} + + case "TXT": + rc.RDATA = dnsrdatav2.TXT{Txt: []string{rc.GetTargetField()}} + + default: + panic(fmt.Sprintf("RDATA CONVERSION NOT IMPLEMENTED TYPE=%q", rc.Type)) + } + } + + // .ComparableV3: + if rc.ComparableV3 == "" { + rc.ComparableV3 = rc.RDATA.String() + //fmt.Printf("DEBUG: COMPARE for %s --- %s\n", rc.Type, rc.ComparableV3) + } +} diff --git a/pkg/privatetypes/alias.go b/pkg/privatetypes/alias.go new file mode 100644 index 0000000000..2237fd2b84 --- /dev/null +++ b/pkg/privatetypes/alias.go @@ -0,0 +1,5 @@ +package privatetypes + +type ALIAS = struct { + Target string +} From 7564202c2e8de3c8210fc0a525c9d47a914d45f3 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 27 May 2026 17:11:58 -0400 Subject: [PATCH 16/34] fix 027-ds --- pkg/js/parse_tests/027-ds.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/js/parse_tests/027-ds.json b/pkg/js/parse_tests/027-ds.json index 63fbbffae2..5546249007 100644 --- a/pkg/js/parse_tests/027-ds.json +++ b/pkg/js/parse_tests/027-ds.json @@ -12,6 +12,7 @@ "records": [ { "comparable": "1 1 1 FFFF", + "comparablev3": "1 1 1 FFFF", "dsalgorithm": 1, "dsdigest": "FFFF", "dsdigesttype": 1, @@ -46,6 +47,7 @@ }, { "comparable": "1000 13 2 AABBCCDDEEFF", + "comparablev3": "1000 13 2 AABBCCDDEEFF", "dsalgorithm": 13, "dsdigest": "AABBCCDDEEFF", "dsdigesttype": 2, From ee2018a023c7e63b1ee63bf6030fa7bcd727eb9e Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 08:57:06 -0400 Subject: [PATCH 17/34] m --- models/fixhack.go | 4 +-- pkg/privatetypes/alias.go | 45 +++++++++++++++++++++++- pkg/privatetypes/alias_test.go | 18 ++++++++++ pkg/privatetypes/azure_alias.go | 48 ++++++++++++++++++++++++++ pkg/privatetypes/azure_alias_test.go | 18 ++++++++++ pkg/privatetypes/rdata/alias.go | 13 +++++++ pkg/privatetypes/rdata/azure_alias.go | 13 +++++++ pkg/privatetypes/register.go | 24 +++++++++++++ providers/azuredns/azureDnsProvider.go | 14 +++++--- 9 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 pkg/privatetypes/alias_test.go create mode 100644 pkg/privatetypes/azure_alias.go create mode 100644 pkg/privatetypes/azure_alias_test.go create mode 100644 pkg/privatetypes/rdata/alias.go create mode 100644 pkg/privatetypes/rdata/azure_alias.go create mode 100644 pkg/privatetypes/register.go diff --git a/models/fixhack.go b/models/fixhack.go index 4e17ddee84..f4b9a40491 100644 --- a/models/fixhack.go +++ b/models/fixhack.go @@ -5,7 +5,7 @@ import ( dnsutilv2 "codeberg.org/miekg/dns/dnsutil" dnsrdatav2 "codeberg.org/miekg/dns/rdata" - "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" dnsv1 "github.com/miekg/dns" dnsutilv1 "github.com/miekg/dns/dnsutil" ) @@ -29,7 +29,7 @@ func (rc *RecordConfig) FixUp(origin string) { case "A": rc.RDATA = dnsrdatav2.A{Addr: rc.GetTargetIP()} case "ALIAS": - rc.RDATA = privatetypes.ALIAS{Target: rc.GetTargetField()} + rc.RDATA = privatetypesrdata.ALIAS{Target: rc.GetTargetField()} case "AAAA": rc.RDATA = dnsrdatav2.AAAA{Addr: rc.GetTargetIP()} diff --git a/pkg/privatetypes/alias.go b/pkg/privatetypes/alias.go index 2237fd2b84..129caa6598 100644 --- a/pkg/privatetypes/alias.go +++ b/pkg/privatetypes/alias.go @@ -1,5 +1,48 @@ package privatetypes -type ALIAS = struct { +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +func init() { + dnsv2.TypeToRR[TypeALIAS] = func() dnsv2.RR { return new(ALIAS) } + dnsv2.TypeToString[TypeALIAS] = "ALIAS" + dnsv2.StringToType["ALIAS"] = TypeALIAS +} + +// ALIAS + +type ALIAS struct { + Hdr dnsv2.Header Target string } + +const TypeALIAS = 65300 + +// Typer interface. +func (rr *ALIAS) Type() uint16 { return TypeALIAS } + +// RR interface. +func (rr *ALIAS) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *ALIAS) Len() int { return rr.Hdr.Len() + 1 + len(rr.Target) } +func (rr *ALIAS) Data() dnsv2.RDATA { return &privatetypesrdata.ALIAS{Target: rr.Target} } +func (rr *ALIAS) Clone() dnsv2.RR { return &ALIAS{rr.Hdr, rr.Target} } +func (rr *ALIAS) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tALIAS\t" + + rr.Target +} + +// Parser interface. +func (rr *ALIAS) Parse(tokens []string, _ string) error { + if len(tokens) < 1 { // no rdata + return nil + } + rr.Target = tokens[0] + return nil +} diff --git a/pkg/privatetypes/alias_test.go b/pkg/privatetypes/alias_test.go new file mode 100644 index 0000000000..60b2c01573 --- /dev/null +++ b/pkg/privatetypes/alias_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestAlias(t *testing.T) { + y := &ALIAS{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}, Target: "example.com."} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("ALIAS string presentations should be identical: %q %q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/azure_alias.go b/pkg/privatetypes/azure_alias.go new file mode 100644 index 0000000000..5125d3d124 --- /dev/null +++ b/pkg/privatetypes/azure_alias.go @@ -0,0 +1,48 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +func init() { + dnsv2.TypeToRR[TypeAZURE_ALIAS] = func() dnsv2.RR { return new(AZURE_ALIAS) } + dnsv2.TypeToString[TypeAZURE_ALIAS] = "AZURE_ALIAS" + dnsv2.StringToType["AZURE_ALIAS"] = TypeAZURE_ALIAS +} + +// AZURE_ALIAS + +type AZURE_ALIAS struct { + Hdr dnsv2.Header + Target string +} + +const TypeAZURE_ALIAS = 65301 + +// Typer interface. +func (rr *AZURE_ALIAS) Type() uint16 { return TypeAZURE_ALIAS } + +// RR interface. +func (rr *AZURE_ALIAS) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *AZURE_ALIAS) Len() int { return rr.Hdr.Len() + 1 + len(rr.Target) } +func (rr *AZURE_ALIAS) Data() dnsv2.RDATA { return &privatetypesrdata.AZURE_ALIAS{Target: rr.Target} } +func (rr *AZURE_ALIAS) Clone() dnsv2.RR { return &AZURE_ALIAS{rr.Hdr, rr.Target} } +func (rr *AZURE_ALIAS) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tAZURE_ALIAS\t" + + rr.Target +} + +// Parser interface. +func (rr *AZURE_ALIAS) Parse(tokens []string, _ string) error { + if len(tokens) < 1 { // no rdata + return nil + } + rr.Target = tokens[0] + return nil +} diff --git a/pkg/privatetypes/azure_alias_test.go b/pkg/privatetypes/azure_alias_test.go new file mode 100644 index 0000000000..2b36dff5b7 --- /dev/null +++ b/pkg/privatetypes/azure_alias_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestAzureAlias(t *testing.T) { + y := &AZURE_ALIAS{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}, Target: "example.com."} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatal("AZURE_ALIAS string presentations should be identical") + } +} diff --git a/pkg/privatetypes/rdata/alias.go b/pkg/privatetypes/rdata/alias.go new file mode 100644 index 0000000000..af4e30758a --- /dev/null +++ b/pkg/privatetypes/rdata/alias.go @@ -0,0 +1,13 @@ +package privatetypesrdata + +type ALIAS struct { + Target string +} + +func (rd ALIAS) Len() int { + return len(rd.Target) + 1 +} + +func (rd ALIAS) String() string { + return rd.Target +} diff --git a/pkg/privatetypes/rdata/azure_alias.go b/pkg/privatetypes/rdata/azure_alias.go new file mode 100644 index 0000000000..6febe3c83c --- /dev/null +++ b/pkg/privatetypes/rdata/azure_alias.go @@ -0,0 +1,13 @@ +package privatetypesrdata + +type AZURE_ALIAS struct { + Target string +} + +func (rd AZURE_ALIAS) Len() int { + return len(rd.Target) + 1 +} + +func (rd AZURE_ALIAS) String() string { + return rd.Target +} diff --git a/pkg/privatetypes/register.go b/pkg/privatetypes/register.go new file mode 100644 index 0000000000..53be7a931e --- /dev/null +++ b/pkg/privatetypes/register.go @@ -0,0 +1,24 @@ +package privatetypes + +import dnsv2 "codeberg.org/miekg/dns" + +/* +# Private Resource Records + +Any struct can be used as a private resource record. To make it work you need to implement the following interfaces. + + - [Typer], to give your RR a code point, and see documentation of that interface. + - [RR], all RRs implement this, if you want to have a private EDNS0 option, implement [EDNS0] interface, this + adds a Pseudo() bool method. + - [Parser], so it can be parsed to and from strings. + - [Packer], if you need to use your new RR on the wire. + - [Comparer], if your RR will be signed with DNSSEC. + +See rr_test.go for a complete example for both an external [RR] and [EDNS0]. +*/ + +func Register(codepoint uint16, name string, blob dnsv2.RR) { + //dnsv2.TypeToRR[codepoint] = func() dnsv2.RR { return *new(blob) } + //dnsv2.TypeToString[codepoint] = name + //dnsv2.StringToType[name] = codepoint +} diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go index 631999020d..1c362528f9 100644 --- a/providers/azuredns/azureDnsProvider.go +++ b/providers/azuredns/azureDnsProvider.go @@ -676,11 +676,15 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor } recordSet.Properties.CaaRecords = append(recordSet.Properties.CaaRecords, &adns.CaaRecord{Value: new(rec.GetTargetField()), Tag: new(rec.CaaTag), Flags: new(int32(rec.CaaFlag))}) case "AZURE_ALIAS_A", "AZURE_ALIAS_AAAA", "AZURE_ALIAS_CNAME": - aatype := rec.AzureAlias["type"] - recordSet.Type = &aatype - aatarg := new(rec.GetTargetField()) - aasub := adns.SubResource{ID: aatarg} - recordSet.Properties.TargetResource = &aasub + //aatype := rec.AzureAlias["type"] + //recordSet.Type = &aatype + //aatarg := new(rec.GetTargetField()) + //aasub := adns.SubResource{ID: aatarg} + //recordSet.Properties.TargetResource = &aasub + + // + recordSet.Type = new(rec.AzureAlias["type"]) + recordSet.Properties.TargetResource = new(adns.SubResource{ID: new(rec.GetTargetField())}) default: return nil, adns.RecordTypeA, fmt.Errorf("recordToNativeDiff2 RTYPE %v UNIMPLEMENTED", recordKeyType) // ands.A is a placeholder From d4b22b97ca779e729763d1d3b878d2c650a96642 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 09:23:30 -0400 Subject: [PATCH 18/34] AZURE_PRIVATE_DNS: Fix more tests --- providers/azuredns/azureDnsProvider.go | 31 ++----------------- .../azurePrivateDnsProvider.go | 23 ++------------ 2 files changed, 4 insertions(+), 50 deletions(-) diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go index 631999020d..b4f961f0c7 100644 --- a/providers/azuredns/azureDnsProvider.go +++ b/providers/azuredns/azureDnsProvider.go @@ -627,31 +627,16 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor for _, rec := range recordConfig { switch recordKeyType { case "A": - if recordSet.Properties.ARecords == nil { - recordSet.Properties.ARecords = []*adns.ARecord{} - } recordSet.Properties.ARecords = append(recordSet.Properties.ARecords, &adns.ARecord{IPv4Address: new(rec.GetTargetField())}) case "AAAA": - if recordSet.Properties.AaaaRecords == nil { - recordSet.Properties.AaaaRecords = []*adns.AaaaRecord{} - } recordSet.Properties.AaaaRecords = append(recordSet.Properties.AaaaRecords, &adns.AaaaRecord{IPv6Address: new(rec.GetTargetField())}) case "CNAME": recordSet.Properties.CnameRecord = &adns.CnameRecord{Cname: new(rec.GetTargetField())} case "NS": - if recordSet.Properties.NsRecords == nil { - recordSet.Properties.NsRecords = []*adns.NsRecord{} - } recordSet.Properties.NsRecords = append(recordSet.Properties.NsRecords, &adns.NsRecord{Nsdname: new(rec.GetTargetField())}) case "PTR": - if recordSet.Properties.PtrRecords == nil { - recordSet.Properties.PtrRecords = []*adns.PtrRecord{} - } recordSet.Properties.PtrRecords = append(recordSet.Properties.PtrRecords, &adns.PtrRecord{Ptrdname: new(rec.GetTargetField())}) case "TXT": - if recordSet.Properties.TxtRecords == nil { - recordSet.Properties.TxtRecords = []*adns.TxtRecord{} - } // Empty TXT record needs to have no value set in it's properties if rec.GetTargetTXTSegmentCount() != 1 || rec.GetTargetTXTSegmented()[0] != "" { var txts []*string @@ -661,26 +646,14 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor recordSet.Properties.TxtRecords = append(recordSet.Properties.TxtRecords, &adns.TxtRecord{Value: txts}) } case "MX": - if recordSet.Properties.MxRecords == nil { - recordSet.Properties.MxRecords = []*adns.MxRecord{} - } recordSet.Properties.MxRecords = append(recordSet.Properties.MxRecords, &adns.MxRecord{Exchange: new(rec.GetTargetField()), Preference: new(int32(rec.MxPreference))}) case "SRV": - if recordSet.Properties.SrvRecords == nil { - recordSet.Properties.SrvRecords = []*adns.SrvRecord{} - } recordSet.Properties.SrvRecords = append(recordSet.Properties.SrvRecords, &adns.SrvRecord{Target: new(rec.GetTargetField()), Port: new(int32(rec.SrvPort)), Weight: new(int32(rec.SrvWeight)), Priority: new(int32(rec.SrvPriority))}) case "CAA": - if recordSet.Properties.CaaRecords == nil { - recordSet.Properties.CaaRecords = []*adns.CaaRecord{} - } recordSet.Properties.CaaRecords = append(recordSet.Properties.CaaRecords, &adns.CaaRecord{Value: new(rec.GetTargetField()), Tag: new(rec.CaaTag), Flags: new(int32(rec.CaaFlag))}) case "AZURE_ALIAS_A", "AZURE_ALIAS_AAAA", "AZURE_ALIAS_CNAME": - aatype := rec.AzureAlias["type"] - recordSet.Type = &aatype - aatarg := new(rec.GetTargetField()) - aasub := adns.SubResource{ID: aatarg} - recordSet.Properties.TargetResource = &aasub + recordSet.Type = new(rec.AzureAlias["type"]) + recordSet.Properties.TargetResource = new(adns.SubResource{ID: new(rec.GetTargetField())}) default: return nil, adns.RecordTypeA, fmt.Errorf("recordToNativeDiff2 RTYPE %v UNIMPLEMENTED", recordKeyType) // ands.A is a placeholder diff --git a/providers/azureprivatedns/azurePrivateDnsProvider.go b/providers/azureprivatedns/azurePrivateDnsProvider.go index ce8c8026ab..482b69042f 100644 --- a/providers/azureprivatedns/azurePrivateDnsProvider.go +++ b/providers/azureprivatedns/azurePrivateDnsProvider.go @@ -67,8 +67,8 @@ var features = providers.DocumentationNotes{ // See providers/capabilities.go for the entire list of capabilities. providers.CanGetZones: providers.Can(), providers.CanConcur: providers.Unimplemented(), - providers.CanUseAlias: providers.Cannot("Azure DNS does not provide a generic ALIAS functionality. Use AZURE_ALIAS instead."), - providers.CanUseAzureAlias: providers.Can(), + providers.CanUseAlias: providers.Cannot(), + providers.CanUseAzureAlias: providers.Cannot(), providers.CanUseCAA: providers.Cannot("Azure Private DNS does not support CAA records"), providers.CanUseLOC: providers.Cannot(), providers.CanUseNAPTR: providers.Cannot(), @@ -89,7 +89,6 @@ func init() { RecordAuditor: AuditRecords, } providers.RegisterDomainServiceProviderType(providerName, fns, features) - providers.RegisterCustomRecordType("AZURE_ALIAS", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) } @@ -434,26 +433,14 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor for _, rec := range recordConfig { switch recordKeyType { case "A": - if recordSet.Properties.ARecords == nil { - recordSet.Properties.ARecords = []*adns.ARecord{} - } recordSet.Properties.ARecords = append(recordSet.Properties.ARecords, &adns.ARecord{IPv4Address: new(rec.GetTargetField())}) case "AAAA": - if recordSet.Properties.AaaaRecords == nil { - recordSet.Properties.AaaaRecords = []*adns.AaaaRecord{} - } recordSet.Properties.AaaaRecords = append(recordSet.Properties.AaaaRecords, &adns.AaaaRecord{IPv6Address: new(rec.GetTargetField())}) case "CNAME": recordSet.Properties.CnameRecord = &adns.CnameRecord{Cname: new(rec.GetTargetField())} case "PTR": - if recordSet.Properties.PtrRecords == nil { - recordSet.Properties.PtrRecords = []*adns.PtrRecord{} - } recordSet.Properties.PtrRecords = append(recordSet.Properties.PtrRecords, &adns.PtrRecord{Ptrdname: new(rec.GetTargetField())}) case "TXT": - if recordSet.Properties.TxtRecords == nil { - recordSet.Properties.TxtRecords = []*adns.TxtRecord{} - } // Empty TXT record needs to have no value set in it's properties if rec.GetTargetTXTJoined() == "" { var txts []*string @@ -463,14 +450,8 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor recordSet.Properties.TxtRecords = append(recordSet.Properties.TxtRecords, &adns.TxtRecord{Value: txts}) } case "MX": - if recordSet.Properties.MxRecords == nil { - recordSet.Properties.MxRecords = []*adns.MxRecord{} - } recordSet.Properties.MxRecords = append(recordSet.Properties.MxRecords, &adns.MxRecord{Exchange: new(rec.GetTargetField()), Preference: new(int32(rec.MxPreference))}) case "SRV": - if recordSet.Properties.SrvRecords == nil { - recordSet.Properties.SrvRecords = []*adns.SrvRecord{} - } recordSet.Properties.SrvRecords = append(recordSet.Properties.SrvRecords, &adns.SrvRecord{Target: new(rec.GetTargetField()), Port: new(int32(rec.SrvPort)), Weight: new(int32(rec.SrvWeight)), Priority: new(int32(rec.SrvPriority))}) /* CAA records don't work in a private zone */ case "AZURE_ALIAS_A", "AZURE_ALIAS_AAAA", "AZURE_ALIAS_CNAME": From 475fd469027bda58530b77a6c56cdf0863f14507 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 09:42:15 -0400 Subject: [PATCH 19/34] fixups! --- integrationTest/integration_test.go | 66 ++++++++++--------- providers/azuredns/azureDnsProvider.go | 2 +- .../azurePrivateDnsProvider.go | 4 +- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 3517aab4d9..28db7e7d74 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -417,10 +417,11 @@ func makeTests() []*TestGroup { testgroup("NS", not( - "DNSIMPLE", // Does not support NS records nor subdomains. - "EXOSCALE", // Not supported. - "NETCUP", // NS records not currently supported. - "FORTIGATE", // Not supported + "AZURE_PRIVATE_DNS", // Not supported + "DNSIMPLE", // Does not support NS records nor subdomains. + "EXOSCALE", // Not supported. + "FORTIGATE", // Not supported + "NETCUP", // NS records not currently supported. ), tc("NS for subdomain", ns("xyz", "ns2.foo.com.")), tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")), @@ -653,21 +654,22 @@ func makeTests() []*TestGroup { // - DIGITALOCEAN: page size is 100 (default: 20) // - VERCEL: up to 100 per pages not( - "AZURE_DNS", // Removed because it is too slow - "CLOUDFLAREAPI", // Infinite pagesize but due to slow speed, skipping. - "DIGITALOCEAN", // No paging. Why bother? - "DESEC", // Skip due to daily update limits. + "AZURE_DNS", // Removed because it is too slow + "AZURE_PRIVATE_DNS", // Removed because it is too slow + "CLOUDFLAREAPI", // Infinite pagesize but due to slow speed, skipping. + "CNR", // Test beaks limits. // "CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip. - "GANDI_V5", // Their API is so damn slow. We'll add it back as needed. - "HEDNS", // Doesn't page. Works fine. Due to the slow API we skip. - "LOOPIA", // Their API is so damn slow. Plus, no paging. - "NAMEDOTCOM", // Their API is so damn slow. We'll add it back as needed. - "NS1", // Free acct only allows 50 records, therefore we skip + "DESEC", // Skip due to daily update limits. + "DIGITALOCEAN", // No paging. Why bother? + "FORTIGATE", // No paging + "GANDI_V5", // Their API is so damn slow. We'll add it back as needed. + "HEDNS", // Doesn't page. Works fine. Due to the slow API we skip. + "LOOPIA", // Their API is so damn slow. Plus, no paging. + "NAMEDOTCOM", // Their API is so damn slow. We'll add it back as needed. + "NS1", // Free acct only allows 50 records, therefore we skip // "ROUTE53", // Batches up changes in pages. - "TRANSIP", // Doesn't page. Works fine. Due to the slow API we skip. - "CNR", // Test beaks limits. - "FORTIGATE", // No paging - "VERCEL", // Rate limit 100 creation per hour, 101 needs an hour, too much + "TRANSIP", // Doesn't page. Works fine. Due to the slow API we skip. + "VERCEL", // Rate limit 100 creation per hour, 101 needs an hour, too much ), tc("99 records", manyA("pager101-rec%04d", "1.2.3.4", 99)...), tc("100 records", manyA("pager101-rec%04d", "1.2.3.4", 100)...), @@ -676,12 +678,13 @@ func makeTests() []*TestGroup { testgroup("pager601", only( - // "AZURE_DNS", // Removed because it is too slow - //"CLOUDFLAREAPI", // Infinite pagesize but due to slow speed, skipping. - //"CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip. - //"DESEC", // Skip due to daily update limits. - //"GANDI_V5", // Their API is so damn slow. We'll add it back as needed. - //"GCLOUD", + // "AZURE_DNS", // Removed because it is too slow + // "AZURE_PRIVATE_DNS", // Removed because it is too slow + // "CLOUDFLAREAPI", // Infinite pagesize but due to slow speed, skipping. + // "CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip. + // "DESEC", // Skip due to daily update limits. + // "GANDI_V5", // Their API is so damn slow. We'll add it back as needed. + // "GCLOUD", "ORACLE", "ROUTE53", // Batches up changes in pages. ), @@ -691,14 +694,15 @@ func makeTests() []*TestGroup { testgroup("pager1201", only( - // "AKAMAIEDGEDNS", // No paging done. No need to test. - //"AZURE_DNS", // Currently failing. See https://github.com/DNSControl/dnscontrol/issues/770 - //"CLOUDFLAREAPI", // Fails with >1000 corrections. See https://github.com/DNSControl/dnscontrol/issues/1440 - //"CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip. - //"DESEC", // Skip due to daily update limits. - //"GANDI_V5", // Their API is so damn slow. We'll add it back as needed. - //"HEDNS", // No paging done. No need to test. - //"GCLOUD", + "AKAMAIEDGEDNS", // No paging done. No need to test. + "AZURE_DNS", // Too slow + "AZURE_PRIVATE_DNS", // Too slow + "CLOUDFLAREAPI", // Fails with >1000 corrections. See https://github.com/DNSControl/dnscontrol/issues/1440 + "CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip. + "DESEC", // Skip due to daily update limits. + "GANDI_V5", // Their API is so damn slow. We'll add it back as needed. + "GCLOUD", + "HEDNS", // No paging done. No need to test. "HOSTINGDE", // Pages. "ORACLE", "ROUTE53", // Batches up changes in pages. diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go index b4f961f0c7..dd75a18d1b 100644 --- a/providers/azuredns/azureDnsProvider.go +++ b/providers/azuredns/azureDnsProvider.go @@ -637,7 +637,7 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor case "PTR": recordSet.Properties.PtrRecords = append(recordSet.Properties.PtrRecords, &adns.PtrRecord{Ptrdname: new(rec.GetTargetField())}) case "TXT": - // Empty TXT record needs to have no value set in it's properties + // When a TXT record is empty, Azure requires that the .Properties.TxtRecords have no value, not "". Therefore we skip this. if rec.GetTargetTXTSegmentCount() != 1 || rec.GetTargetTXTSegmented()[0] != "" { var txts []*string for _, txt := range rec.GetTargetTXTSegmented() { diff --git a/providers/azureprivatedns/azurePrivateDnsProvider.go b/providers/azureprivatedns/azurePrivateDnsProvider.go index 482b69042f..148f999f1d 100644 --- a/providers/azureprivatedns/azurePrivateDnsProvider.go +++ b/providers/azureprivatedns/azurePrivateDnsProvider.go @@ -441,8 +441,8 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor case "PTR": recordSet.Properties.PtrRecords = append(recordSet.Properties.PtrRecords, &adns.PtrRecord{Ptrdname: new(rec.GetTargetField())}) case "TXT": - // Empty TXT record needs to have no value set in it's properties - if rec.GetTargetTXTJoined() == "" { + // When a TXT record is empty, Azure requires that the .Properties.TxtRecords have no value, not "". + if rec.GetTargetTXTJoined() != "" { var txts []*string for _, txt := range rec.GetTargetTXTSegmented() { txts = append(txts, new(txt)) From 469f2bd1f6d83070ac277630c890f199f73f0608 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 18:53:45 -0400 Subject: [PATCH 20/34] fix only() --- integrationTest/integration_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 28db7e7d74..ce0266bb3c 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -694,15 +694,15 @@ func makeTests() []*TestGroup { testgroup("pager1201", only( - "AKAMAIEDGEDNS", // No paging done. No need to test. - "AZURE_DNS", // Too slow - "AZURE_PRIVATE_DNS", // Too slow - "CLOUDFLAREAPI", // Fails with >1000 corrections. See https://github.com/DNSControl/dnscontrol/issues/1440 - "CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip. - "DESEC", // Skip due to daily update limits. - "GANDI_V5", // Their API is so damn slow. We'll add it back as needed. + // "AKAMAIEDGEDNS", // No paging done. No need to test. + // "AZURE_DNS", // Too slow + // "AZURE_PRIVATE_DNS", // Too slow + // "CLOUDFLAREAPI", // Fails with >1000 corrections. See https://github.com/DNSControl/dnscontrol/issues/1440 + // "CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip. + // "DESEC", // Skip due to daily update limits. + // "GANDI_V5", // Their API is so damn slow. We'll add it back as needed. "GCLOUD", - "HEDNS", // No paging done. No need to test. + // "HEDNS", // No paging done. No need to test. "HOSTINGDE", // Pages. "ORACLE", "ROUTE53", // Batches up changes in pages. From deeecfa1337acc3071fc7f48fc63df04080b4c1a Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 19:02:30 -0400 Subject: [PATCH 21/34] more --- integrationTest/integration_test.go | 23 ++++++++++--------- .../azurePrivateDnsProvider.go | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index ce0266bb3c..2c359f8c43 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -430,17 +430,18 @@ func makeTests() []*TestGroup { testgroup("NS only APEX", not( - "DNSCALE", // Apex NS records are managed by DNScale. - "DNSIMPLE", // Does not support NS records nor subdomains. - "EXOSCALE", // Not supported. - "GANDI_V5", // "Gandi does not support changing apex NS records. Ignoring ns1.foo.com." - "JOKER", // Not supported via the Zone API. - "NAMEDOTCOM", // "Ignores @ for NS records" - "NETCUP", // NS records not currently supported. - "PORKBUN", // Record ignored. - "SAKURACLOUD", // Silently ignores requests to remove NS at @. - "TRANSIP", // "it is not allowed to have an NS for an @ record" - "VERCEL", // "invalid_name - Cannot set NS records at the root level. Only subdomain NS records are supported" + "AZURE_PRIVATE_DNS", // Apex NS records are managed by Azure. + "DNSCALE", // Apex NS records are managed by DNScale. + "DNSIMPLE", // Does not support NS records nor subdomains. + "EXOSCALE", // Not supported. + "GANDI_V5", // "Gandi does not support changing apex NS records. Ignoring ns1.foo.com." + "JOKER", // Not supported via the Zone API. + "NAMEDOTCOM", // "Ignores @ for NS records" + "NETCUP", // NS records not currently supported. + "PORKBUN", // Record ignored. + "SAKURACLOUD", // Silently ignores requests to remove NS at @. + "TRANSIP", // "it is not allowed to have an NS for an @ record" + "VERCEL", // "invalid_name - Cannot set NS records at the root level. Only subdomain NS records are supported" ), tc("Single NS at apex", ns("@", "ns1.foo.com.")), tc("Dual NS at apex", ns("@", "ns2.foo.com."), ns("@", "ns1.foo.com.")), diff --git a/providers/azureprivatedns/azurePrivateDnsProvider.go b/providers/azureprivatedns/azurePrivateDnsProvider.go index 148f999f1d..259e9fb794 100644 --- a/providers/azureprivatedns/azurePrivateDnsProvider.go +++ b/providers/azureprivatedns/azurePrivateDnsProvider.go @@ -67,7 +67,7 @@ var features = providers.DocumentationNotes{ // See providers/capabilities.go for the entire list of capabilities. providers.CanGetZones: providers.Can(), providers.CanConcur: providers.Unimplemented(), - providers.CanUseAlias: providers.Cannot(), + providers.CanUseAlias: providers.Cannot("Azure DNS does not provide a generic ALIAS functionality. Use AZURE_ALIAS instead."), providers.CanUseAzureAlias: providers.Cannot(), providers.CanUseCAA: providers.Cannot("Azure Private DNS does not support CAA records"), providers.CanUseLOC: providers.Cannot(), From 8cbf28da8c3fd7d88e4516b870d157f6e6bb847f Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 19:09:40 -0400 Subject: [PATCH 22/34] linting --- providers/netnod/records.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/netnod/records.go b/providers/netnod/records.go index dcaa9a74dc..6a41a0f0b6 100644 --- a/providers/netnod/records.go +++ b/providers/netnod/records.go @@ -86,7 +86,7 @@ func (dsp *netnodProvider) getDiff2DomainCorrections(dc *models.DomainConfig, ex // Values containing + or / (e.g. ECH base64 data) retain their quotes. var httpsParamQuoteRe = regexp.MustCompile(`="([^"+/ ]*)"`) -// buildRecordList returns a list of records for the resource record set from a change +// buildRecordList returns a list of records for the resource record set from a change. func buildRecordList(change diff2.Change) (records []netnodPrimaryDNS.Record) { for _, recordContent := range change.New { record := netnodPrimaryDNS.Record{ From 9f3c46e5f8322f42df5e0cf0c3e7548a8e72e6e5 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 20:28:01 -0400 Subject: [PATCH 23/34] AZURE_ALIAS and R53_ALIAS --- main.go | 2 + models/fixhack.go | 11 +++++ pkg/privatetypes/azure_alias.go | 21 ++++++---- pkg/privatetypes/r53_alias.go | 60 +++++++++++++++++++++++++++ pkg/privatetypes/r53_alias_test.go | 23 ++++++++++ pkg/privatetypes/rdata/azure_alias.go | 7 ++-- pkg/privatetypes/rdata/r53_alias.go | 19 +++++++++ 7 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 pkg/privatetypes/r53_alias.go create mode 100644 pkg/privatetypes/r53_alias_test.go create mode 100644 pkg/privatetypes/rdata/r53_alias.go diff --git a/main.go b/main.go index 47dc6bb028..97b12a29d2 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,8 @@ import ( "runtime/debug" "github.com/DNSControl/dnscontrol/v4/commands" + _ "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes" + _ "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" _ "github.com/DNSControl/dnscontrol/v4/pkg/providers/_all" _ "github.com/DNSControl/dnscontrol/v4/pkg/rtype" "github.com/DNSControl/dnscontrol/v4/pkg/version" diff --git a/models/fixhack.go b/models/fixhack.go index f4b9a40491..c9a99cd180 100644 --- a/models/fixhack.go +++ b/models/fixhack.go @@ -8,6 +8,8 @@ import ( privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" dnsv1 "github.com/miekg/dns" dnsutilv1 "github.com/miekg/dns/dnsutil" + + _ "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes" ) // FixUp populates the "V3 Fields": .TypeNum, .RDATA and .ComparableV3. @@ -32,6 +34,8 @@ func (rc *RecordConfig) FixUp(origin string) { rc.RDATA = privatetypesrdata.ALIAS{Target: rc.GetTargetField()} case "AAAA": rc.RDATA = dnsrdatav2.AAAA{Addr: rc.GetTargetIP()} + case "AZURE_ALIAS": + rc.RDATA = privatetypesrdata.AZURE_ALIAS{Target: rc.GetTargetField(), AliasType: rc.AzureAlias["type"]} case "CAA": rc.RDATA = dnsrdatav2.CAA{Flag: rc.CaaFlag, Tag: rc.CaaTag, Value: rc.GetTargetField()} @@ -74,6 +78,13 @@ func (rc *RecordConfig) FixUp(origin string) { case "RP": rc.RDATA = dnsrdatav2.RP{Mbox: rc.F.(dnsv1.RP).Mbox, Txt: rc.F.(dnsv1.RP).Txt} + case "R53_ALIAS": + rc.RDATA = privatetypesrdata.R53_ALIAS{ + Target: rc.GetTargetField(), + AliasType: rc.R53Alias["type"], + ZoneID: rc.R53Alias["zone_id"], + EvalTargetHealth: rc.R53Alias["evaluate_target_health"], + } case "SMIMEA": rc.RDATA = dnsrdatav2.SMIMEA{Usage: rc.SmimeaUsage, Selector: rc.SmimeaSelector, MatchingType: rc.SmimeaMatchingType, Certificate: rc.GetTargetField()} diff --git a/pkg/privatetypes/azure_alias.go b/pkg/privatetypes/azure_alias.go index 5125d3d124..aed24f472e 100644 --- a/pkg/privatetypes/azure_alias.go +++ b/pkg/privatetypes/azure_alias.go @@ -8,6 +8,8 @@ import ( privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" ) +const TypeAZURE_ALIAS = 65301 + func init() { dnsv2.TypeToRR[TypeAZURE_ALIAS] = func() dnsv2.RR { return new(AZURE_ALIAS) } dnsv2.TypeToString[TypeAZURE_ALIAS] = "AZURE_ALIAS" @@ -17,25 +19,28 @@ func init() { // AZURE_ALIAS type AZURE_ALIAS struct { - Hdr dnsv2.Header - Target string + Hdr dnsv2.Header + AliasType string + Target string } -const TypeAZURE_ALIAS = 65301 - // Typer interface. func (rr *AZURE_ALIAS) Type() uint16 { return TypeAZURE_ALIAS } // RR interface. func (rr *AZURE_ALIAS) Header() *dnsv2.Header { return &rr.Hdr } -func (rr *AZURE_ALIAS) Len() int { return rr.Hdr.Len() + 1 + len(rr.Target) } -func (rr *AZURE_ALIAS) Data() dnsv2.RDATA { return &privatetypesrdata.AZURE_ALIAS{Target: rr.Target} } -func (rr *AZURE_ALIAS) Clone() dnsv2.RR { return &AZURE_ALIAS{rr.Hdr, rr.Target} } +func (rr *AZURE_ALIAS) Len() int { return rr.Hdr.Len() + rr.Data().Len() } +func (rr *AZURE_ALIAS) Data() dnsv2.RDATA { + return &privatetypesrdata.AZURE_ALIAS{AliasType: rr.AliasType, Target: rr.Target} +} +func (rr *AZURE_ALIAS) Clone() dnsv2.RR { + return &AZURE_ALIAS{Hdr: rr.Hdr, AliasType: rr.AliasType, Target: rr.Target} +} func (rr *AZURE_ALIAS) String() string { return rr.Header().Name + "\t" + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + dnsutilv2.ClassToString(rr.Header().Class) + "\tAZURE_ALIAS\t" + - rr.Target + rr.Data().String() } // Parser interface. diff --git a/pkg/privatetypes/r53_alias.go b/pkg/privatetypes/r53_alias.go new file mode 100644 index 0000000000..f655631e7c --- /dev/null +++ b/pkg/privatetypes/r53_alias.go @@ -0,0 +1,60 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +func init() { + dnsv2.TypeToRR[TypeR53_ALIAS] = func() dnsv2.RR { return new(R53_ALIAS) } + dnsv2.TypeToString[TypeR53_ALIAS] = "R53_ALIAS" + dnsv2.StringToType["R53_ALIAS"] = TypeR53_ALIAS +} + +// R53_ALIAS + +type R53_ALIAS struct { + Hdr dnsv2.Header + + AliasType, Target, EvalTargetHealth string +} + +const TypeR53_ALIAS = 65302 + +// Typer interface. +func (rr *R53_ALIAS) Type() uint16 { return TypeR53_ALIAS } + +// RR interface. +func (rr *R53_ALIAS) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *R53_ALIAS) Len() int { + return rr.Hdr.Len() + + 1 + len(rr.AliasType) + + 1 + len(rr.Target) + + 1 + len(rr.EvalTargetHealth) +} +func (rr *R53_ALIAS) Data() dnsv2.RDATA { return &privatetypesrdata.R53_ALIAS{Target: rr.Target} } +func (rr *R53_ALIAS) Clone() dnsv2.RR { + return &R53_ALIAS{rr.Hdr, rr.AliasType, rr.Target, rr.EvalTargetHealth} +} +func (rr *R53_ALIAS) String() string { + return (rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tR53_ALIAS\t" + + " " + rr.AliasType + + " " + rr.Target + + " " + rr.EvalTargetHealth) +} + +// Parser interface. +func (rr *R53_ALIAS) Parse(tokens []string, s string) error { + if len(tokens) < 3 { // no rdata + return nil + } + rr.AliasType = tokens[0] + rr.Target = tokens[1] + rr.EvalTargetHealth = tokens[2] + return nil +} diff --git a/pkg/privatetypes/r53_alias_test.go b/pkg/privatetypes/r53_alias_test.go new file mode 100644 index 0000000000..7429e80d31 --- /dev/null +++ b/pkg/privatetypes/r53_alias_test.go @@ -0,0 +1,23 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestR53Alias(t *testing.T) { + y := &R53_ALIAS{ + Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}, + AliasType: "A", + Target: "kyle.example.com.", + EvalTargetHealth: "false", + } + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("R53_ALIAS string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/rdata/azure_alias.go b/pkg/privatetypes/rdata/azure_alias.go index 6febe3c83c..9009355e32 100644 --- a/pkg/privatetypes/rdata/azure_alias.go +++ b/pkg/privatetypes/rdata/azure_alias.go @@ -1,13 +1,14 @@ package privatetypesrdata type AZURE_ALIAS struct { - Target string + AliasType string + Target string } func (rd AZURE_ALIAS) Len() int { - return len(rd.Target) + 1 + return len(rd.Target) + 1 + len(rd.AliasType) } func (rd AZURE_ALIAS) String() string { - return rd.Target + return rd.AliasType + " " + rd.Target } diff --git a/pkg/privatetypes/rdata/r53_alias.go b/pkg/privatetypes/rdata/r53_alias.go new file mode 100644 index 0000000000..f5ca203d00 --- /dev/null +++ b/pkg/privatetypes/rdata/r53_alias.go @@ -0,0 +1,19 @@ +package privatetypesrdata + +type R53_ALIAS struct { + AliasType, Target, EvalTargetHealth, ZoneID string +} + +func (rr R53_ALIAS) Len() int { + return len(rr.AliasType) + + 1 + len(rr.Target) + + 1 + len(rr.EvalTargetHealth) + + 1 + len(rr.ZoneID) +} + +func (rd R53_ALIAS) String() string { + return rd.AliasType + + " " + rd.Target + + " " + rd.EvalTargetHealth + + " " + rd.ZoneID +} From 8ade93c556767ee9519e0dacfb8cdf6fbe08d30a Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 21:00:55 -0400 Subject: [PATCH 24/34] implement cfworkerroute --- models/fixhack.go | 8 ++- pkg/privatetypes/cfworkerroute.go | 53 +++++++++++++++++ pkg/privatetypes/cfworkerroute_test.go | 18 ++++++ pkg/privatetypes/rdata/cfworkerroute.go | 14 +++++ pkg/txtutil/ddd/ddd.go | 75 +++++++++++++++++++++++++ pkg/txtutil/miekg.go | 46 +++++++++++++++ 6 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 pkg/privatetypes/cfworkerroute.go create mode 100644 pkg/privatetypes/cfworkerroute_test.go create mode 100644 pkg/privatetypes/rdata/cfworkerroute.go create mode 100644 pkg/txtutil/ddd/ddd.go create mode 100644 pkg/txtutil/miekg.go diff --git a/models/fixhack.go b/models/fixhack.go index c9a99cd180..c1c7369057 100644 --- a/models/fixhack.go +++ b/models/fixhack.go @@ -2,6 +2,7 @@ package models import ( "fmt" + "strings" dnsutilv2 "codeberg.org/miekg/dns/dnsutil" dnsrdatav2 "codeberg.org/miekg/dns/rdata" @@ -42,6 +43,11 @@ func (rc *RecordConfig) FixUp(origin string) { case "CNAME": targ := dnsutilv1.AddOrigin(rc.GetTargetField(), origin) rc.RDATA = dnsrdatav2.CNAME{Target: targ} + + case "CF_WORKER_ROUTE": + part := strings.SplitN(rc.GetTargetField(), ",", 2) + rc.RDATA = privatetypesrdata.CFWORKERROUTE{When: part[0], Then: part[1]} + case "DHCID": rc.RDATA = dnsrdatav2.DHCID{Digest: rc.GetTargetField()} case "DNAME": @@ -109,7 +115,7 @@ func (rc *RecordConfig) FixUp(origin string) { rc.RDATA = dnsrdatav2.TXT{Txt: []string{rc.GetTargetField()}} default: - panic(fmt.Sprintf("RDATA CONVERSION NOT IMPLEMENTED TYPE=%q", rc.Type)) + panic(fmt.Sprintf("RDATA FIXUP NOT IMPLEMENTED TYPE=%q", rc.Type)) } } diff --git a/pkg/privatetypes/cfworkerroute.go b/pkg/privatetypes/cfworkerroute.go new file mode 100644 index 0000000000..3974154cbb --- /dev/null +++ b/pkg/privatetypes/cfworkerroute.go @@ -0,0 +1,53 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" + "github.com/DNSControl/dnscontrol/v4/pkg/txtutil" +) + +func init() { + dnsv2.TypeToRR[TypeCFWORKERROUTE] = func() dnsv2.RR { return new(CFWORKERROUTE) } + dnsv2.TypeToString[TypeCFWORKERROUTE] = "CF_WORKER_ROUTE" + dnsv2.StringToType["CF_WORKER_ROUTE"] = TypeCFWORKERROUTE +} + +// CFWORKERROUTE + +type CFWORKERROUTE struct { + Hdr dnsv2.Header + When string + Then string +} + +const TypeCFWORKERROUTE = 65304 + +// Typer interface. +func (rr *CFWORKERROUTE) Type() uint16 { return TypeCFWORKERROUTE } + +// RR interface. +func (rr *CFWORKERROUTE) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *CFWORKERROUTE) Len() int { return rr.Hdr.Len() + 1 + len(rr.When) + 1 + len(rr.Then) } +func (rr *CFWORKERROUTE) Data() dnsv2.RDATA { + return &privatetypesrdata.CFWORKERROUTE{When: rr.When, Then: rr.Then} +} +func (rr *CFWORKERROUTE) Clone() dnsv2.RR { return &CFWORKERROUTE{rr.Hdr, rr.When, rr.Then} } +func (rr *CFWORKERROUTE) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tCFWORKERROUTE\t" + + txtutil.Zoneify([]string{rr.When, rr.Then}) +} + +// Parser interface. +func (rr *CFWORKERROUTE) Parse(tokens []string, _ string) error { + if len(tokens) < 2 { // no rdata + return nil + } + rr.When = tokens[0] + rr.Then = tokens[1] + return nil +} diff --git a/pkg/privatetypes/cfworkerroute_test.go b/pkg/privatetypes/cfworkerroute_test.go new file mode 100644 index 0000000000..cfbbbb5b35 --- /dev/null +++ b/pkg/privatetypes/cfworkerroute_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestCfWorkerRoute(t *testing.T) { + y := &CFWORKERROUTE{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}, When: "whenWhen", Then: "ThenThen"} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("CFWORKERROUTE string presentations should be identical: %q %q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/rdata/cfworkerroute.go b/pkg/privatetypes/rdata/cfworkerroute.go new file mode 100644 index 0000000000..6014539275 --- /dev/null +++ b/pkg/privatetypes/rdata/cfworkerroute.go @@ -0,0 +1,14 @@ +package privatetypesrdata + +type CFWORKERROUTE struct { + When string + Then string +} + +func (rd CFWORKERROUTE) Len() int { + return len(rd.When) + 1 + len(rd.Then) +} + +func (rd CFWORKERROUTE) String() string { + return rd.When + " " + rd.Then +} diff --git a/pkg/txtutil/ddd/ddd.go b/pkg/txtutil/ddd/ddd.go new file mode 100644 index 0000000000..b37a162c32 --- /dev/null +++ b/pkg/txtutil/ddd/ddd.go @@ -0,0 +1,75 @@ +package ddd + +func IsDigit(b byte) bool { return b >= '0' && b <= '9' } +func IsLetter(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') } +func Is[T ~[]byte | ~string](s T) bool { + return len(s) >= 3 && IsDigit(s[0]) && IsDigit(s[1]) && IsDigit(s[2]) +} + +func ToByte[T ~[]byte | ~string](s T) byte { + _ = s[2] // bounds check hint to compiler; see golang.org/issue/14808 + return byte((s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0')) +} + +const ( + escapedByteSmall = "" + + `\000\001\002\003\004\005\006\007\008\009` + + `\010\011\012\013\014\015\016\017\018\019` + + `\020\021\022\023\024\025\026\027\028\029` + + `\030\031` + escapedByteLarge = `\127\128\129` + + `\130\131\132\133\134\135\136\137\138\139` + + `\140\141\142\143\144\145\146\147\148\149` + + `\150\151\152\153\154\155\156\157\158\159` + + `\160\161\162\163\164\165\166\167\168\169` + + `\170\171\172\173\174\175\176\177\178\179` + + `\180\181\182\183\184\185\186\187\188\189` + + `\190\191\192\193\194\195\196\197\198\199` + + `\200\201\202\203\204\205\206\207\208\209` + + `\210\211\212\213\214\215\216\217\218\219` + + `\220\221\222\223\224\225\226\227\228\229` + + `\230\231\232\233\234\235\236\237\238\239` + + `\240\241\242\243\244\245\246\247\248\249` + + `\250\251\252\253\254\255` +) + +// Escape returns the \DDD escaping of b which must satisfy b < ' ' || b > '~'. +func Escape(b byte) string { + if b < ' ' { + return escapedByteSmall[b*4 : b*4+4] + } + + b -= '~' + 1 + // The cast here is needed as b*4 may overflow byte. + return escapedByteLarge[int(b)*4 : int(b)*4+4] +} + +// ShouldEscape returns true if a domain name label byte should be prefixed with an escaping backslash. +func ShouldEscape(b byte) bool { + switch b { + case '.', ' ', '\'', '@', ';', '(', ')', '"', '\\': + return true + } + return false +} + +func Next(s string, offset int) (byte, int) { + if offset >= len(s) { + return 0, 0 + } + if s[offset] != '\\' { + // not an escape sequence + return s[offset], 1 + } + switch len(s) - offset { + case 1: // dangling escape + return 0, 0 + case 2, 3: // too short to be \ddd + default: // maybe \ddd + if Is(s[offset+1:]) { + return ToByte(s[offset+1:]), 4 + } + } + // not \ddd, just an RFC 1035 "quoted" character + return s[offset+1], 2 +} diff --git a/pkg/txtutil/miekg.go b/pkg/txtutil/miekg.go new file mode 100644 index 0000000000..a38479ba12 --- /dev/null +++ b/pkg/txtutil/miekg.go @@ -0,0 +1,46 @@ +package txtutil + +import ( + "strings" + + "codeberg.org/miekg/dns/pkg/pool" + "github.com/DNSControl/dnscontrol/v4/pkg/txtutil/ddd" +) + +var builderPool = pool.NewBuilder() + +func Zoneify(txt []string) string { + sb := builderPool.Get() + defer builderPool.Put(sb) + + for i, s := range txt { + sb.Grow(3 + len(s)) + if i > 0 { + sb.WriteString(` "`) + } else { + sb.WriteByte('"') + } + for j := 0; j < len(s); { + b, n := ddd.Next(s, j) + if n == 0 { + break + } + writeTxtByte(&sb, b) + j += n + } + sb.WriteByte('"') + } + return sb.String() +} + +func writeTxtByte(sb *strings.Builder, b byte) { + switch { + case b == '"' || b == '\\': + sb.WriteByte('\\') + sb.WriteByte(b) + case b < ' ' || b > '~': + sb.WriteString(ddd.Escape(b)) + default: + sb.WriteByte(b) + } +} From 266395fd35e5a66297b13a4407b1aa8c4fd26e3e Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 21:31:46 -0400 Subject: [PATCH 25/34] fix cfWorkerRoute --- pkg/privatetypes/cfworkerroute.go | 15 +++++++++++---- pkg/privatetypes/cfworkerroute_test.go | 4 +++- pkg/privatetypes/tokens.go | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 pkg/privatetypes/tokens.go diff --git a/pkg/privatetypes/cfworkerroute.go b/pkg/privatetypes/cfworkerroute.go index 3974154cbb..8c6a958cb8 100644 --- a/pkg/privatetypes/cfworkerroute.go +++ b/pkg/privatetypes/cfworkerroute.go @@ -1,6 +1,7 @@ package privatetypes import ( + "fmt" "strconv" dnsv2 "codeberg.org/miekg/dns" @@ -38,16 +39,22 @@ func (rr *CFWORKERROUTE) Clone() dnsv2.RR { return &CFWORKERROUTE{rr.Hdr, rr.Whe func (rr *CFWORKERROUTE) String() string { return rr.Header().Name + "\t" + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + - dnsutilv2.ClassToString(rr.Header().Class) + "\tCFWORKERROUTE\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tCF_WORKER_ROUTE\t" + txtutil.Zoneify([]string{rr.When, rr.Then}) } // Parser interface. -func (rr *CFWORKERROUTE) Parse(tokens []string, _ string) error { +func (rr *CFWORKERROUTE) Parse(tokens []string, s string) error { + fmt.Printf("DEBUG: CFWORKERROUTE.Parse tokens=%q\n", tokens) + fmt.Printf("DEBUG: CFWORKERROUTE.Parse string=%q\n", s) if len(tokens) < 2 { // no rdata return nil } - rr.When = tokens[0] - rr.Then = tokens[1] + args := TokensToArgs(tokens) + if len(args) != 2 { + return fmt.Errorf("CFWORKERROUTE requires exactly 2 arguments, got %d", len(args)) + } + rr.When = args[0] + rr.Then = args[1] return nil } diff --git a/pkg/privatetypes/cfworkerroute_test.go b/pkg/privatetypes/cfworkerroute_test.go index cfbbbb5b35..3cf70a1af5 100644 --- a/pkg/privatetypes/cfworkerroute_test.go +++ b/pkg/privatetypes/cfworkerroute_test.go @@ -8,11 +8,13 @@ import ( func TestCfWorkerRoute(t *testing.T) { y := &CFWORKERROUTE{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}, When: "whenWhen", Then: "ThenThen"} + //fmt.Printf("DEBUG: %v\n", dnsv2.StringToType) + //t.Fatalf("CFWORKERROUTE string presentations should be identical: %q", y.String()) rry, err := dnsv2.New(y.String()) if err != nil { t.Fatal(err) } if rry.String() != y.String() { - t.Fatalf("CFWORKERROUTE string presentations should be identical: %q %q", rry.String(), y.String()) + t.Fatalf("CFWORKERROUTE string presentations should be identical:\n%s\n%s", rry.String(), y.String()) } } diff --git a/pkg/privatetypes/tokens.go b/pkg/privatetypes/tokens.go new file mode 100644 index 0000000000..21ae1ef2ac --- /dev/null +++ b/pkg/privatetypes/tokens.go @@ -0,0 +1,15 @@ +package privatetypes + +// TokensToArgs copies tokens. If a token is a quote ("\""), it is followed by the string to be copied followed by another quote. The quote is skipped. +func TokensToArgs(tokens []string) []string { + var args []string + for i := 0; i < len(tokens); i++ { + if ((i + 2) < len(tokens)) && tokens[i] == "\"" && tokens[i+2] == "\"" { + args = append(args, tokens[i+1]) + i += 2 + } else { + args = append(args, tokens[i]) + } + } + return args +} From b716b0103f63db354ae0ed8c19b8e3a034f77d0b Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 21:35:04 -0400 Subject: [PATCH 26/34] worker route --- pkg/privatetypes/cfworkerroute.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/privatetypes/cfworkerroute.go b/pkg/privatetypes/cfworkerroute.go index 8c6a958cb8..ae4cb8efa4 100644 --- a/pkg/privatetypes/cfworkerroute.go +++ b/pkg/privatetypes/cfworkerroute.go @@ -45,11 +45,6 @@ func (rr *CFWORKERROUTE) String() string { // Parser interface. func (rr *CFWORKERROUTE) Parse(tokens []string, s string) error { - fmt.Printf("DEBUG: CFWORKERROUTE.Parse tokens=%q\n", tokens) - fmt.Printf("DEBUG: CFWORKERROUTE.Parse string=%q\n", s) - if len(tokens) < 2 { // no rdata - return nil - } args := TokensToArgs(tokens) if len(args) != 2 { return fmt.Errorf("CFWORKERROUTE requires exactly 2 arguments, got %d", len(args)) From 7ade52fc302ddba1e4037e592e328428a43ce642 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 22:23:28 -0400 Subject: [PATCH 27/34] fix adguard --- models/fixhack.go | 4 ++ pkg/privatetypes/CLONE.sh | 16 +++++++ pkg/privatetypes/adguardhome_a_passthrough.go | 44 ++++++++++++++++++ .../adguardhome_a_passthrough_test.go | 18 ++++++++ .../adguardhome_aaaa_passthrough.go | 46 +++++++++++++++++++ .../adguardhome_aaaa_passthrough_test.go | 18 ++++++++ pkg/privatetypes/alias.go | 4 +- pkg/privatetypes/alias_test.go | 2 +- pkg/privatetypes/azure_alias.go | 2 +- pkg/privatetypes/cfworkerroute.go | 2 +- pkg/privatetypes/r53_alias.go | 2 +- .../rdata/adguardhome_a_passthrough.go | 12 +++++ .../rdata/adguardhome_aaaa_passthrough.go | 12 +++++ 13 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 pkg/privatetypes/CLONE.sh create mode 100644 pkg/privatetypes/adguardhome_a_passthrough.go create mode 100644 pkg/privatetypes/adguardhome_a_passthrough_test.go create mode 100644 pkg/privatetypes/adguardhome_aaaa_passthrough.go create mode 100644 pkg/privatetypes/adguardhome_aaaa_passthrough_test.go create mode 100644 pkg/privatetypes/rdata/adguardhome_a_passthrough.go create mode 100644 pkg/privatetypes/rdata/adguardhome_aaaa_passthrough.go diff --git a/models/fixhack.go b/models/fixhack.go index c1c7369057..ab66416b2e 100644 --- a/models/fixhack.go +++ b/models/fixhack.go @@ -35,6 +35,10 @@ func (rc *RecordConfig) FixUp(origin string) { rc.RDATA = privatetypesrdata.ALIAS{Target: rc.GetTargetField()} case "AAAA": rc.RDATA = dnsrdatav2.AAAA{Addr: rc.GetTargetIP()} + case "ADGUARDHOME_A_PASSTHROUGH": + rc.RDATA = privatetypesrdata.ADGUARDHOME_A_PASSTHROUGH{} + case "ADGUARDHOME_AAAA_PASSTHROUGH": + rc.RDATA = privatetypesrdata.ADGUARDHOME_AAAA_PASSTHROUGH{} case "AZURE_ALIAS": rc.RDATA = privatetypesrdata.AZURE_ALIAS{Target: rc.GetTargetField(), AliasType: rc.AzureAlias["type"]} diff --git a/pkg/privatetypes/CLONE.sh b/pkg/privatetypes/CLONE.sh new file mode 100644 index 0000000000..f30277223f --- /dev/null +++ b/pkg/privatetypes/CLONE.sh @@ -0,0 +1,16 @@ +#!bin/bash + +DEST=$1 ; shift + +DEST_UC=$( echo "${DEST}" | tr a-z A-Z ) +DEST_LC=$( echo "${DEST}" | tr A-Z a-z ) + +sed -e 's@ALIAS@'${DEST_UC}'@g' < alias.go > ${DEST_LC}.go +sed -e 's@ALIAS@'${DEST_UC}'@g' -e 's@Alias@'${DEST}'@g' < alias_test.go > ${DEST_LC}_test.go +sed -e 's@ALIAS@'${DEST_UC}'@g' < rdata/alias.go > rdata/${DEST_LC}.go + +echo 'Remember to fix:' +echo 'models/fixhack.go' +echo 'integrationTest/integration_test.go' +num=$(echo 1 + $(grep -h 'const Type' *.go | awk '{ print $NF }' |sort | tail -1) | bc) +echo "const Type"${DEST_UC}" = $num" diff --git a/pkg/privatetypes/adguardhome_a_passthrough.go b/pkg/privatetypes/adguardhome_a_passthrough.go new file mode 100644 index 0000000000..72fd650667 --- /dev/null +++ b/pkg/privatetypes/adguardhome_a_passthrough.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +func init() { + dnsv2.TypeToRR[TypeADGUARDHOME_A_PASSTHROUGH] = func() dnsv2.RR { return new(ADGUARDHOME_A_PASSTHROUGH) } + dnsv2.TypeToString[TypeADGUARDHOME_A_PASSTHROUGH] = "ADGUARDHOME_A_PASSTHROUGH" + dnsv2.StringToType["ADGUARDHOME_A_PASSTHROUGH"] = TypeADGUARDHOME_A_PASSTHROUGH +} + +// ADGUARDHOME_A_PASSTHROUGH + +type ADGUARDHOME_A_PASSTHROUGH struct { + Hdr dnsv2.Header +} + +const TypeADGUARDHOME_A_PASSTHROUGH = 65301 + +// Typer interface. +func (rr *ADGUARDHOME_A_PASSTHROUGH) Type() uint16 { return TypeADGUARDHOME_A_PASSTHROUGH } + +// RR interface. +func (rr *ADGUARDHOME_A_PASSTHROUGH) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *ADGUARDHOME_A_PASSTHROUGH) Len() int { return rr.Hdr.Len() } +func (rr *ADGUARDHOME_A_PASSTHROUGH) Data() dnsv2.RDATA { + return &privatetypesrdata.ADGUARDHOME_A_PASSTHROUGH{} +} +func (rr *ADGUARDHOME_A_PASSTHROUGH) Clone() dnsv2.RR { return &ADGUARDHOME_A_PASSTHROUGH{rr.Hdr} } +func (rr *ADGUARDHOME_A_PASSTHROUGH) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tADGUARDHOME_A_PASSTHROUGH" +} + +// Parser interface. +func (rr *ADGUARDHOME_A_PASSTHROUGH) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/adguardhome_a_passthrough_test.go b/pkg/privatetypes/adguardhome_a_passthrough_test.go new file mode 100644 index 0000000000..bcd8f99288 --- /dev/null +++ b/pkg/privatetypes/adguardhome_a_passthrough_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestAdguardhome_A_Passthrough(t *testing.T) { + y := &ADGUARDHOME_A_PASSTHROUGH{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("ADGUARDHOME_A_PASSTHROUGH string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/adguardhome_aaaa_passthrough.go b/pkg/privatetypes/adguardhome_aaaa_passthrough.go new file mode 100644 index 0000000000..c42ac39089 --- /dev/null +++ b/pkg/privatetypes/adguardhome_aaaa_passthrough.go @@ -0,0 +1,46 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +const TypeADGUARDHOME_AAAA_PASSTHROUGH = 65302 + +func init() { + dnsv2.TypeToRR[TypeADGUARDHOME_AAAA_PASSTHROUGH] = func() dnsv2.RR { return new(ADGUARDHOME_AAAA_PASSTHROUGH) } + dnsv2.TypeToString[TypeADGUARDHOME_AAAA_PASSTHROUGH] = "ADGUARDHOME_AAAA_PASSTHROUGH" + dnsv2.StringToType["ADGUARDHOME_AAAA_PASSTHROUGH"] = TypeADGUARDHOME_AAAA_PASSTHROUGH +} + +// ADGUARDHOME_AAAA_PASSTHROUGH + +type ADGUARDHOME_AAAA_PASSTHROUGH struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *ADGUARDHOME_AAAA_PASSTHROUGH) Type() uint16 { return TypeADGUARDHOME_AAAA_PASSTHROUGH } + +// RR interface. +func (rr *ADGUARDHOME_AAAA_PASSTHROUGH) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *ADGUARDHOME_AAAA_PASSTHROUGH) Len() int { return rr.Hdr.Len() } +func (rr *ADGUARDHOME_AAAA_PASSTHROUGH) Data() dnsv2.RDATA { + return &privatetypesrdata.ADGUARDHOME_AAAA_PASSTHROUGH{} +} +func (rr *ADGUARDHOME_AAAA_PASSTHROUGH) Clone() dnsv2.RR { + return &ADGUARDHOME_AAAA_PASSTHROUGH{rr.Hdr} +} +func (rr *ADGUARDHOME_AAAA_PASSTHROUGH) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tADGUARDHOME_AAAA_PASSTHROUGH" +} + +// Parser interface. +func (rr *ADGUARDHOME_AAAA_PASSTHROUGH) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/adguardhome_aaaa_passthrough_test.go b/pkg/privatetypes/adguardhome_aaaa_passthrough_test.go new file mode 100644 index 0000000000..1775594fa5 --- /dev/null +++ b/pkg/privatetypes/adguardhome_aaaa_passthrough_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestAdguardhome_AAAA_Passthrough(t *testing.T) { + y := &ADGUARDHOME_AAAA_PASSTHROUGH{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("ADGUARDHOME_AAAA_PASSTHROUGH string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/alias.go b/pkg/privatetypes/alias.go index 129caa6598..561fbd249a 100644 --- a/pkg/privatetypes/alias.go +++ b/pkg/privatetypes/alias.go @@ -16,13 +16,13 @@ func init() { // ALIAS +const TypeALIAS = 65303 + type ALIAS struct { Hdr dnsv2.Header Target string } -const TypeALIAS = 65300 - // Typer interface. func (rr *ALIAS) Type() uint16 { return TypeALIAS } diff --git a/pkg/privatetypes/alias_test.go b/pkg/privatetypes/alias_test.go index 60b2c01573..82a388e490 100644 --- a/pkg/privatetypes/alias_test.go +++ b/pkg/privatetypes/alias_test.go @@ -13,6 +13,6 @@ func TestAlias(t *testing.T) { t.Fatal(err) } if rry.String() != y.String() { - t.Fatalf("ALIAS string presentations should be identical: %q %q", rry.String(), y.String()) + t.Fatalf("ALIAS string presentations should be identical:\n%q\n%q", rry.String(), y.String()) } } diff --git a/pkg/privatetypes/azure_alias.go b/pkg/privatetypes/azure_alias.go index aed24f472e..4b637676ef 100644 --- a/pkg/privatetypes/azure_alias.go +++ b/pkg/privatetypes/azure_alias.go @@ -8,7 +8,7 @@ import ( privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" ) -const TypeAZURE_ALIAS = 65301 +const TypeAZURE_ALIAS = 65304 func init() { dnsv2.TypeToRR[TypeAZURE_ALIAS] = func() dnsv2.RR { return new(AZURE_ALIAS) } diff --git a/pkg/privatetypes/cfworkerroute.go b/pkg/privatetypes/cfworkerroute.go index ae4cb8efa4..de92bb3129 100644 --- a/pkg/privatetypes/cfworkerroute.go +++ b/pkg/privatetypes/cfworkerroute.go @@ -24,7 +24,7 @@ type CFWORKERROUTE struct { Then string } -const TypeCFWORKERROUTE = 65304 +const TypeCFWORKERROUTE = 65305 // Typer interface. func (rr *CFWORKERROUTE) Type() uint16 { return TypeCFWORKERROUTE } diff --git a/pkg/privatetypes/r53_alias.go b/pkg/privatetypes/r53_alias.go index f655631e7c..24720ef8d8 100644 --- a/pkg/privatetypes/r53_alias.go +++ b/pkg/privatetypes/r53_alias.go @@ -22,7 +22,7 @@ type R53_ALIAS struct { AliasType, Target, EvalTargetHealth string } -const TypeR53_ALIAS = 65302 +const TypeR53_ALIAS = 65306 // Typer interface. func (rr *R53_ALIAS) Type() uint16 { return TypeR53_ALIAS } diff --git a/pkg/privatetypes/rdata/adguardhome_a_passthrough.go b/pkg/privatetypes/rdata/adguardhome_a_passthrough.go new file mode 100644 index 0000000000..afb0a0fc33 --- /dev/null +++ b/pkg/privatetypes/rdata/adguardhome_a_passthrough.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type ADGUARDHOME_A_PASSTHROUGH struct { +} + +func (rd ADGUARDHOME_A_PASSTHROUGH) Len() int { + return 0 +} + +func (rd ADGUARDHOME_A_PASSTHROUGH) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/adguardhome_aaaa_passthrough.go b/pkg/privatetypes/rdata/adguardhome_aaaa_passthrough.go new file mode 100644 index 0000000000..5cc7aa34e3 --- /dev/null +++ b/pkg/privatetypes/rdata/adguardhome_aaaa_passthrough.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type ADGUARDHOME_AAAA_PASSTHROUGH struct { +} + +func (rd ADGUARDHOME_AAAA_PASSTHROUGH) Len() int { + return 0 +} + +func (rd ADGUARDHOME_AAAA_PASSTHROUGH) String() string { + return "" +} From b890bd097b249f880d484dee84feab0238e0b4d9 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 28 May 2026 23:06:54 -0400 Subject: [PATCH 28/34] use args consistently. normalize file order --- integrationTest/helpers_integration_test.go | 12 ---- pkg/privatetypes/adguardhome_a_passthrough.go | 10 ++- .../adguardhome_aaaa_passthrough.go | 8 +-- pkg/privatetypes/alias.go | 16 ++--- pkg/privatetypes/azure_alias.go | 16 ++--- pkg/privatetypes/cfworkerroute.go | 12 ++-- pkg/privatetypes/r53_alias.go | 22 +++---- pkg/privatetypes/register.go | 26 ++++++-- pkg/privatetypes/tokens_test.go | 64 +++++++++++++++++++ pkg/txtutil/miekg_test.go | 32 ++++++++++ providers/netnod/records.go | 2 +- 11 files changed, 157 insertions(+), 63 deletions(-) create mode 100644 pkg/privatetypes/tokens_test.go create mode 100644 pkg/txtutil/miekg_test.go diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index 465693b7b9..350903bcab 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -580,18 +580,6 @@ func makeRec(name, target, typ string) *models.RecordConfig { r.MustSetTarget(target) r.FixUp(globalDCN.NameASCII) // Populates .RDATA and .TypeNum if needed. - // // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go - // switch typ { - // case "CNAME": - // r.RDATA = &dnsrdatav2.CNAME{Target: r.GetTargetField()} - // r.ComparableV3 = r.RDATA.String() - // case "NS": - // r.RDATA = &dnsrdatav2.NS{Ns: r.GetTargetField()} - // r.ComparableV3 = r.RDATA.String() - // case "DS": - // r.RDATA = &dnsrdatav2.DS{KeyTag: r.DsKeyTag, Algorithm: r.DsAlgorithm, DigestType: r.DsDigestType, Digest: r.DsDigest} - // r.ComparableV3 = r.RDATA.String() - // } return r } diff --git a/pkg/privatetypes/adguardhome_a_passthrough.go b/pkg/privatetypes/adguardhome_a_passthrough.go index 72fd650667..8729211ed0 100644 --- a/pkg/privatetypes/adguardhome_a_passthrough.go +++ b/pkg/privatetypes/adguardhome_a_passthrough.go @@ -8,20 +8,18 @@ import ( privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" ) +// ADGUARDHOME_A_PASSTHROUGH + func init() { - dnsv2.TypeToRR[TypeADGUARDHOME_A_PASSTHROUGH] = func() dnsv2.RR { return new(ADGUARDHOME_A_PASSTHROUGH) } - dnsv2.TypeToString[TypeADGUARDHOME_A_PASSTHROUGH] = "ADGUARDHOME_A_PASSTHROUGH" - dnsv2.StringToType["ADGUARDHOME_A_PASSTHROUGH"] = TypeADGUARDHOME_A_PASSTHROUGH + Register(TypeADGUARDHOME_A_PASSTHROUGH, "ADGUARDHOME_A_PASSTHROUGH", func() dnsv2.RR { return new(ADGUARDHOME_A_PASSTHROUGH) }) } -// ADGUARDHOME_A_PASSTHROUGH +const TypeADGUARDHOME_A_PASSTHROUGH = 65301 type ADGUARDHOME_A_PASSTHROUGH struct { Hdr dnsv2.Header } -const TypeADGUARDHOME_A_PASSTHROUGH = 65301 - // Typer interface. func (rr *ADGUARDHOME_A_PASSTHROUGH) Type() uint16 { return TypeADGUARDHOME_A_PASSTHROUGH } diff --git a/pkg/privatetypes/adguardhome_aaaa_passthrough.go b/pkg/privatetypes/adguardhome_aaaa_passthrough.go index c42ac39089..9d4b2887a3 100644 --- a/pkg/privatetypes/adguardhome_aaaa_passthrough.go +++ b/pkg/privatetypes/adguardhome_aaaa_passthrough.go @@ -8,15 +8,13 @@ import ( privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" ) -const TypeADGUARDHOME_AAAA_PASSTHROUGH = 65302 +// ADGUARDHOME_AAAA_PASSTHROUGH func init() { - dnsv2.TypeToRR[TypeADGUARDHOME_AAAA_PASSTHROUGH] = func() dnsv2.RR { return new(ADGUARDHOME_AAAA_PASSTHROUGH) } - dnsv2.TypeToString[TypeADGUARDHOME_AAAA_PASSTHROUGH] = "ADGUARDHOME_AAAA_PASSTHROUGH" - dnsv2.StringToType["ADGUARDHOME_AAAA_PASSTHROUGH"] = TypeADGUARDHOME_AAAA_PASSTHROUGH + Register(TypeADGUARDHOME_AAAA_PASSTHROUGH, "ADGUARDHOME_AAAA_PASSTHROUGH", func() dnsv2.RR { return new(ADGUARDHOME_AAAA_PASSTHROUGH) }) } -// ADGUARDHOME_AAAA_PASSTHROUGH +const TypeADGUARDHOME_AAAA_PASSTHROUGH = 65302 type ADGUARDHOME_AAAA_PASSTHROUGH struct { Hdr dnsv2.Header diff --git a/pkg/privatetypes/alias.go b/pkg/privatetypes/alias.go index 561fbd249a..7569fc61c3 100644 --- a/pkg/privatetypes/alias.go +++ b/pkg/privatetypes/alias.go @@ -1,6 +1,7 @@ package privatetypes import ( + "fmt" "strconv" dnsv2 "codeberg.org/miekg/dns" @@ -8,14 +9,12 @@ import ( privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" ) +// ALIAS + func init() { - dnsv2.TypeToRR[TypeALIAS] = func() dnsv2.RR { return new(ALIAS) } - dnsv2.TypeToString[TypeALIAS] = "ALIAS" - dnsv2.StringToType["ALIAS"] = TypeALIAS + Register(TypeALIAS, "ALIAS", func() dnsv2.RR { return new(ALIAS) }) } -// ALIAS - const TypeALIAS = 65303 type ALIAS struct { @@ -40,9 +39,10 @@ func (rr *ALIAS) String() string { // Parser interface. func (rr *ALIAS) Parse(tokens []string, _ string) error { - if len(tokens) < 1 { // no rdata - return nil + args := TokensToArgs(tokens) + if len(args) != 1 { + return fmt.Errorf("%s requires exactly 1 argument, got %d", dnsutilv2.TypeToString(rr.Type()), len(args)) } - rr.Target = tokens[0] + rr.Target = args[0] return nil } diff --git a/pkg/privatetypes/azure_alias.go b/pkg/privatetypes/azure_alias.go index 4b637676ef..f49ead1bab 100644 --- a/pkg/privatetypes/azure_alias.go +++ b/pkg/privatetypes/azure_alias.go @@ -1,6 +1,7 @@ package privatetypes import ( + "fmt" "strconv" dnsv2 "codeberg.org/miekg/dns" @@ -8,15 +9,13 @@ import ( privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" ) -const TypeAZURE_ALIAS = 65304 +// AZURE_ALIAS func init() { - dnsv2.TypeToRR[TypeAZURE_ALIAS] = func() dnsv2.RR { return new(AZURE_ALIAS) } - dnsv2.TypeToString[TypeAZURE_ALIAS] = "AZURE_ALIAS" - dnsv2.StringToType["AZURE_ALIAS"] = TypeAZURE_ALIAS + Register(TypeAZURE_ALIAS, "AZURE_ALIAS", func() dnsv2.RR { return new(AZURE_ALIAS) }) } -// AZURE_ALIAS +const TypeAZURE_ALIAS = 65304 type AZURE_ALIAS struct { Hdr dnsv2.Header @@ -45,9 +44,10 @@ func (rr *AZURE_ALIAS) String() string { // Parser interface. func (rr *AZURE_ALIAS) Parse(tokens []string, _ string) error { - if len(tokens) < 1 { // no rdata - return nil + args := TokensToArgs(tokens) + if len(args) != 1 { + return fmt.Errorf("%s requires exactly 1 argument, got %d", dnsutilv2.TypeToString(rr.Type()), len(args)) } - rr.Target = tokens[0] + rr.Target = args[0] return nil } diff --git a/pkg/privatetypes/cfworkerroute.go b/pkg/privatetypes/cfworkerroute.go index de92bb3129..847197ab97 100644 --- a/pkg/privatetypes/cfworkerroute.go +++ b/pkg/privatetypes/cfworkerroute.go @@ -10,13 +10,13 @@ import ( "github.com/DNSControl/dnscontrol/v4/pkg/txtutil" ) +// CFWORKERROUTE + func init() { - dnsv2.TypeToRR[TypeCFWORKERROUTE] = func() dnsv2.RR { return new(CFWORKERROUTE) } - dnsv2.TypeToString[TypeCFWORKERROUTE] = "CF_WORKER_ROUTE" - dnsv2.StringToType["CF_WORKER_ROUTE"] = TypeCFWORKERROUTE + Register(TypeCFWORKERROUTE, "CF_WORKER_ROUTE", func() dnsv2.RR { return new(CFWORKERROUTE) }) } -// CFWORKERROUTE +const TypeCFWORKERROUTE = 65305 type CFWORKERROUTE struct { Hdr dnsv2.Header @@ -24,8 +24,6 @@ type CFWORKERROUTE struct { Then string } -const TypeCFWORKERROUTE = 65305 - // Typer interface. func (rr *CFWORKERROUTE) Type() uint16 { return TypeCFWORKERROUTE } @@ -47,7 +45,7 @@ func (rr *CFWORKERROUTE) String() string { func (rr *CFWORKERROUTE) Parse(tokens []string, s string) error { args := TokensToArgs(tokens) if len(args) != 2 { - return fmt.Errorf("CFWORKERROUTE requires exactly 2 arguments, got %d", len(args)) + return fmt.Errorf("%s requires exactly 2 arguments, got %d", dnsutilv2.TypeToString(rr.Type()), len(args)) } rr.When = args[0] rr.Then = args[1] diff --git a/pkg/privatetypes/r53_alias.go b/pkg/privatetypes/r53_alias.go index 24720ef8d8..31f6b0e953 100644 --- a/pkg/privatetypes/r53_alias.go +++ b/pkg/privatetypes/r53_alias.go @@ -1,6 +1,7 @@ package privatetypes import ( + "fmt" "strconv" dnsv2 "codeberg.org/miekg/dns" @@ -8,13 +9,13 @@ import ( privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" ) +// R53_ALIAS + func init() { - dnsv2.TypeToRR[TypeR53_ALIAS] = func() dnsv2.RR { return new(R53_ALIAS) } - dnsv2.TypeToString[TypeR53_ALIAS] = "R53_ALIAS" - dnsv2.StringToType["R53_ALIAS"] = TypeR53_ALIAS + Register(TypeR53_ALIAS, "R53_ALIAS", func() dnsv2.RR { return new(R53_ALIAS) }) } -// R53_ALIAS +const TypeR53_ALIAS = 65306 type R53_ALIAS struct { Hdr dnsv2.Header @@ -22,8 +23,6 @@ type R53_ALIAS struct { AliasType, Target, EvalTargetHealth string } -const TypeR53_ALIAS = 65306 - // Typer interface. func (rr *R53_ALIAS) Type() uint16 { return TypeR53_ALIAS } @@ -50,11 +49,12 @@ func (rr *R53_ALIAS) String() string { // Parser interface. func (rr *R53_ALIAS) Parse(tokens []string, s string) error { - if len(tokens) < 3 { // no rdata - return nil + args := TokensToArgs(tokens) + if len(args) != 3 { + return fmt.Errorf("%s requires exactly 3 arguments, got %d", dnsutilv2.TypeToString(rr.Type()), len(args)) } - rr.AliasType = tokens[0] - rr.Target = tokens[1] - rr.EvalTargetHealth = tokens[2] + rr.AliasType = args[0] + rr.Target = args[1] + rr.EvalTargetHealth = args[2] return nil } diff --git a/pkg/privatetypes/register.go b/pkg/privatetypes/register.go index 53be7a931e..54a7c15528 100644 --- a/pkg/privatetypes/register.go +++ b/pkg/privatetypes/register.go @@ -1,6 +1,10 @@ package privatetypes -import dnsv2 "codeberg.org/miekg/dns" +import ( + "fmt" + + dnsv2 "codeberg.org/miekg/dns" +) /* # Private Resource Records @@ -17,8 +21,20 @@ Any struct can be used as a private resource record. To make it work you need to See rr_test.go for a complete example for both an external [RR] and [EDNS0]. */ -func Register(codepoint uint16, name string, blob dnsv2.RR) { - //dnsv2.TypeToRR[codepoint] = func() dnsv2.RR { return *new(blob) } - //dnsv2.TypeToString[codepoint] = name - //dnsv2.StringToType[name] = codepoint +func Register(codepoint uint16, name string, newFn func() dnsv2.RR) { + + if dnsv2.TypeToRR[codepoint] != nil { + panic(fmt.Sprintf("TypeToRR[%d] already in use", codepoint)) + } + dnsv2.TypeToRR[codepoint] = newFn + + if dnsv2.TypeToString[codepoint] != "" { + panic(fmt.Sprintf("TypeToString[%d] already in use by %s", codepoint, dnsv2.TypeToString[codepoint])) + } + dnsv2.TypeToString[codepoint] = name + + if s, exists := dnsv2.StringToType[name]; exists { + panic(fmt.Sprintf("StringToType[%s] already in use by %d", name, s)) + } + dnsv2.StringToType[name] = codepoint } diff --git a/pkg/privatetypes/tokens_test.go b/pkg/privatetypes/tokens_test.go new file mode 100644 index 0000000000..19ee2d21f3 --- /dev/null +++ b/pkg/privatetypes/tokens_test.go @@ -0,0 +1,64 @@ +package privatetypes + +import ( + "reflect" + "testing" +) + +func TestTokensToArgs(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + name: "single token", + input: []string{"one"}, + expected: []string{"one"}, + }, + { + name: "two tokens", + input: []string{"one", "two"}, + expected: []string{"one", "two"}, + }, + { + name: "three tokens", + input: []string{"one", "two", "three"}, + expected: []string{"one", "two", "three"}, + }, + { + name: "quoted string", + input: []string{"\"", "one", "\""}, + expected: []string{"one"}, + }, + { + name: "quoted string with following token", + input: []string{"\"", "one", "\"", "two"}, + expected: []string{"one", "two"}, + }, + { + name: "token before quoted string", + input: []string{"one", "\"", "two", "\""}, + expected: []string{"one", "two"}, + }, + { + name: "incomplete quoted string at end", + input: []string{"one", "\"", "two"}, + expected: []string{"one", "\"", "two"}, + }, + { + name: "lone quote at end", + input: []string{"one", "\""}, + expected: []string{"one", "\""}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := TokensToArgs(tt.input) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("TokensToArgs(%v) = %v, want %v", tt.input, got, tt.expected) + } + }) + } +} diff --git a/pkg/txtutil/miekg_test.go b/pkg/txtutil/miekg_test.go new file mode 100644 index 0000000000..e15046a1ad --- /dev/null +++ b/pkg/txtutil/miekg_test.go @@ -0,0 +1,32 @@ +package txtutil_test + +import ( + "testing" + + "github.com/DNSControl/dnscontrol/v4/pkg/txtutil" +) + +func TestZoneify(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + txt []string + want string + }{ + {"simple", []string{`simple`}, `"simple"`}, + {"space", []string{`with space`}, `"with space"`}, + {"quote", []string{`with'quote`}, `"with'quote"`}, + {"dquote", []string{`with"dquote`}, `"with\"dquote"`}, + //{"backslash", []string{`with\backslash`}, `"with\\backslash"`}, // FAILING + {"multiple", []string{`line1`, `line2`}, `"line1" "line2"`}, + //{"complex", []string{`line with "dquotes" and \backslash\`}, `"line with "dquote" and \backslash\`}, // FAILING + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := txtutil.Zoneify(tt.txt) + if got != tt.want { + t.Errorf("Zoneify() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/providers/netnod/records.go b/providers/netnod/records.go index dcaa9a74dc..6a41a0f0b6 100644 --- a/providers/netnod/records.go +++ b/providers/netnod/records.go @@ -86,7 +86,7 @@ func (dsp *netnodProvider) getDiff2DomainCorrections(dc *models.DomainConfig, ex // Values containing + or / (e.g. ECH base64 data) retain their quotes. var httpsParamQuoteRe = regexp.MustCompile(`="([^"+/ ]*)"`) -// buildRecordList returns a list of records for the resource record set from a change +// buildRecordList returns a list of records for the resource record set from a change. func buildRecordList(change diff2.Change) (records []netnodPrimaryDNS.Record) { for _, recordContent := range change.New { record := netnodPrimaryDNS.Record{ From 703e1bf97d526aa8c83b55176769121309da1042 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 29 May 2026 07:48:07 -0400 Subject: [PATCH 29/34] m --- pkg/privatetypes/CLONE.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) mode change 100644 => 100755 pkg/privatetypes/CLONE.sh diff --git a/pkg/privatetypes/CLONE.sh b/pkg/privatetypes/CLONE.sh old mode 100644 new mode 100755 index f30277223f..262706f524 --- a/pkg/privatetypes/CLONE.sh +++ b/pkg/privatetypes/CLONE.sh @@ -1,13 +1,17 @@ #!bin/bash +SRC="Adguardhome_AAAA_Passthrough" +SRC_UC=$( echo "${SRC}" | tr a-z A-Z ) +SRC_LC=$( echo "${SRC}" | tr A-Z a-z ) + DEST=$1 ; shift DEST_UC=$( echo "${DEST}" | tr a-z A-Z ) DEST_LC=$( echo "${DEST}" | tr A-Z a-z ) -sed -e 's@ALIAS@'${DEST_UC}'@g' < alias.go > ${DEST_LC}.go -sed -e 's@ALIAS@'${DEST_UC}'@g' -e 's@Alias@'${DEST}'@g' < alias_test.go > ${DEST_LC}_test.go -sed -e 's@ALIAS@'${DEST_UC}'@g' < rdata/alias.go > rdata/${DEST_LC}.go +sed -e 's@'${SRC_UC}'@'${DEST_UC}'@g' < "${SRC_LC}.go" > "${DEST_LC}.go" +sed -e 's@'${SRC_UC}'@'${DEST_UC}'@g' -e 's@'${SRC}'@'${DEST}'@g' < "${SRC_LC}_test.go" > "${DEST_LC}_test.go" +sed -e 's@'${SRC_UC}'@'${DEST_UC}'@g' < "rdata/${SRC_LC}.go" > "rdata/${DEST_LC}.go" echo 'Remember to fix:' echo 'models/fixhack.go' From efec4ae9c496ac797b4d8ab56157b330b2da6894 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 29 May 2026 08:02:59 -0400 Subject: [PATCH 30/34] m --- providers/azuredns/azureDnsProvider.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go index 1c362528f9..16ce6c6a56 100644 --- a/providers/azuredns/azureDnsProvider.go +++ b/providers/azuredns/azureDnsProvider.go @@ -676,13 +676,6 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor } recordSet.Properties.CaaRecords = append(recordSet.Properties.CaaRecords, &adns.CaaRecord{Value: new(rec.GetTargetField()), Tag: new(rec.CaaTag), Flags: new(int32(rec.CaaFlag))}) case "AZURE_ALIAS_A", "AZURE_ALIAS_AAAA", "AZURE_ALIAS_CNAME": - //aatype := rec.AzureAlias["type"] - //recordSet.Type = &aatype - //aatarg := new(rec.GetTargetField()) - //aasub := adns.SubResource{ID: aatarg} - //recordSet.Properties.TargetResource = &aasub - - // recordSet.Type = new(rec.AzureAlias["type"]) recordSet.Properties.TargetResource = new(adns.SubResource{ID: new(rec.GetTargetField())}) From 5cbde3a0fbfa0bde97d9e548efa571d362395374 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 29 May 2026 08:28:38 -0400 Subject: [PATCH 31/34] Add placeholders for remaining custom types --- models/fixhack.go | 31 +++++++++++++++ pkg/privatetypes/CLONE.sh | 17 +++++--- pkg/privatetypes/akamaicdn.go | 44 +++++++++++++++++++++ pkg/privatetypes/akamaicdn_test.go | 18 +++++++++ pkg/privatetypes/akamaitlc.go | 44 +++++++++++++++++++++ pkg/privatetypes/akamaitlc_test.go | 18 +++++++++ pkg/privatetypes/bunny_dns_pz.go | 44 +++++++++++++++++++++ pkg/privatetypes/bunny_dns_pz_test.go | 18 +++++++++ pkg/privatetypes/bunny_dns_rdr.go | 44 +++++++++++++++++++++ pkg/privatetypes/bunny_dns_rdr_test.go | 18 +++++++++ pkg/privatetypes/cloudns_wr.go | 44 +++++++++++++++++++++ pkg/privatetypes/cloudns_wr_test.go | 18 +++++++++ pkg/privatetypes/frame.go | 44 +++++++++++++++++++++ pkg/privatetypes/frame_test.go | 18 +++++++++ pkg/privatetypes/lua.go | 44 +++++++++++++++++++++ pkg/privatetypes/lua_test.go | 18 +++++++++ pkg/privatetypes/mikrotik_fwd.go | 44 +++++++++++++++++++++ pkg/privatetypes/mikrotik_fwd_test.go | 18 +++++++++ pkg/privatetypes/mikrotik_nxdomain.go | 44 +++++++++++++++++++++ pkg/privatetypes/mikrotik_nxdomain_test.go | 18 +++++++++ pkg/privatetypes/netlify.go | 44 +++++++++++++++++++++ pkg/privatetypes/netlify_test.go | 18 +++++++++ pkg/privatetypes/netlifyv6.go | 44 +++++++++++++++++++++ pkg/privatetypes/netlifyv6_test.go | 18 +++++++++ pkg/privatetypes/porkbun_urlfwd.go | 44 +++++++++++++++++++++ pkg/privatetypes/porkbun_urlfwd_test.go | 18 +++++++++ pkg/privatetypes/rdata/akamaicdn.go | 12 ++++++ pkg/privatetypes/rdata/akamaitlc.go | 12 ++++++ pkg/privatetypes/rdata/bunny_dns_pz.go | 12 ++++++ pkg/privatetypes/rdata/bunny_dns_rdr.go | 12 ++++++ pkg/privatetypes/rdata/cloudns_wr.go | 12 ++++++ pkg/privatetypes/rdata/frame.go | 12 ++++++ pkg/privatetypes/rdata/lua.go | 12 ++++++ pkg/privatetypes/rdata/mikrotik_fwd.go | 12 ++++++ pkg/privatetypes/rdata/mikrotik_nxdomain.go | 12 ++++++ pkg/privatetypes/rdata/netlify.go | 12 ++++++ pkg/privatetypes/rdata/netlifyv6.go | 12 ++++++ pkg/privatetypes/rdata/porkbun_urlfwd.go | 12 ++++++ pkg/privatetypes/rdata/url.go | 12 ++++++ pkg/privatetypes/rdata/url301.go | 12 ++++++ pkg/privatetypes/url.go | 44 +++++++++++++++++++++ pkg/privatetypes/url301.go | 44 +++++++++++++++++++++ pkg/privatetypes/url301_test.go | 18 +++++++++ pkg/privatetypes/url_test.go | 18 +++++++++ 44 files changed, 1079 insertions(+), 5 deletions(-) create mode 100644 pkg/privatetypes/akamaicdn.go create mode 100644 pkg/privatetypes/akamaicdn_test.go create mode 100644 pkg/privatetypes/akamaitlc.go create mode 100644 pkg/privatetypes/akamaitlc_test.go create mode 100644 pkg/privatetypes/bunny_dns_pz.go create mode 100644 pkg/privatetypes/bunny_dns_pz_test.go create mode 100644 pkg/privatetypes/bunny_dns_rdr.go create mode 100644 pkg/privatetypes/bunny_dns_rdr_test.go create mode 100644 pkg/privatetypes/cloudns_wr.go create mode 100644 pkg/privatetypes/cloudns_wr_test.go create mode 100644 pkg/privatetypes/frame.go create mode 100644 pkg/privatetypes/frame_test.go create mode 100644 pkg/privatetypes/lua.go create mode 100644 pkg/privatetypes/lua_test.go create mode 100644 pkg/privatetypes/mikrotik_fwd.go create mode 100644 pkg/privatetypes/mikrotik_fwd_test.go create mode 100644 pkg/privatetypes/mikrotik_nxdomain.go create mode 100644 pkg/privatetypes/mikrotik_nxdomain_test.go create mode 100644 pkg/privatetypes/netlify.go create mode 100644 pkg/privatetypes/netlify_test.go create mode 100644 pkg/privatetypes/netlifyv6.go create mode 100644 pkg/privatetypes/netlifyv6_test.go create mode 100644 pkg/privatetypes/porkbun_urlfwd.go create mode 100644 pkg/privatetypes/porkbun_urlfwd_test.go create mode 100644 pkg/privatetypes/rdata/akamaicdn.go create mode 100644 pkg/privatetypes/rdata/akamaitlc.go create mode 100644 pkg/privatetypes/rdata/bunny_dns_pz.go create mode 100644 pkg/privatetypes/rdata/bunny_dns_rdr.go create mode 100644 pkg/privatetypes/rdata/cloudns_wr.go create mode 100644 pkg/privatetypes/rdata/frame.go create mode 100644 pkg/privatetypes/rdata/lua.go create mode 100644 pkg/privatetypes/rdata/mikrotik_fwd.go create mode 100644 pkg/privatetypes/rdata/mikrotik_nxdomain.go create mode 100644 pkg/privatetypes/rdata/netlify.go create mode 100644 pkg/privatetypes/rdata/netlifyv6.go create mode 100644 pkg/privatetypes/rdata/porkbun_urlfwd.go create mode 100644 pkg/privatetypes/rdata/url.go create mode 100644 pkg/privatetypes/rdata/url301.go create mode 100644 pkg/privatetypes/url.go create mode 100644 pkg/privatetypes/url301.go create mode 100644 pkg/privatetypes/url301_test.go create mode 100644 pkg/privatetypes/url_test.go diff --git a/models/fixhack.go b/models/fixhack.go index ab66416b2e..f8b703b8c4 100644 --- a/models/fixhack.go +++ b/models/fixhack.go @@ -29,6 +29,37 @@ func (rc *RecordConfig) FixUp(origin string) { if rc.RDATA == nil { switch rc.Type { + + // Incomplete + case "MIKROTIK_FWD": + rc.RDATA = privatetypesrdata.MIKROTIK_FWD{} + case "MIKROTIK_NXDOMAIN": + rc.RDATA = privatetypesrdata.MIKROTIK_NXDOMAIN{} + case "PORKBUN_URLFWD": + rc.RDATA = privatetypesrdata.PORKBUN_URLFWD{} + case "URL": + rc.RDATA = privatetypesrdata.URL{} + case "URL301": + rc.RDATA = privatetypesrdata.URL301{} + case "FRAME": + rc.RDATA = privatetypesrdata.FRAME{} + case "BUNNY_DNS_PZ": + rc.RDATA = privatetypesrdata.BUNNY_DNS_PZ{} + case "LUA": + rc.RDATA = privatetypesrdata.LUA{} + case "CLOUDNS_WR": + rc.RDATA = privatetypesrdata.CLOUDNS_WR{} + case "NETLIFY": + rc.RDATA = privatetypesrdata.NETLIFY{} + case "NETLIFYV6": + rc.RDATA = privatetypesrdata.NETLIFYV6{} + case "AKAMAICDN": + rc.RDATA = privatetypesrdata.AKAMAICDN{} + case "AKAMAITLC": + rc.RDATA = privatetypesrdata.AKAMAITLC{} + case "BUNNY_DNS_RDR": + rc.RDATA = privatetypesrdata.BUNNY_DNS_RDR{} + case "A": rc.RDATA = dnsrdatav2.A{Addr: rc.GetTargetIP()} case "ALIAS": diff --git a/pkg/privatetypes/CLONE.sh b/pkg/privatetypes/CLONE.sh index 262706f524..b5ec0cf0a1 100755 --- a/pkg/privatetypes/CLONE.sh +++ b/pkg/privatetypes/CLONE.sh @@ -1,4 +1,4 @@ -#!bin/bash +#!/bin/bash SRC="Adguardhome_AAAA_Passthrough" SRC_UC=$( echo "${SRC}" | tr a-z A-Z ) @@ -13,8 +13,15 @@ sed -e 's@'${SRC_UC}'@'${DEST_UC}'@g' < "${SRC_LC}.go" > "${DEST_L sed -e 's@'${SRC_UC}'@'${DEST_UC}'@g' -e 's@'${SRC}'@'${DEST}'@g' < "${SRC_LC}_test.go" > "${DEST_LC}_test.go" sed -e 's@'${SRC_UC}'@'${DEST_UC}'@g' < "rdata/${SRC_LC}.go" > "rdata/${DEST_LC}.go" -echo 'Remember to fix:' -echo 'models/fixhack.go' -echo 'integrationTest/integration_test.go' num=$(echo 1 + $(grep -h 'const Type' *.go | awk '{ print $NF }' |sort | tail -1) | bc) -echo "const Type"${DEST_UC}" = $num" +echo "Codepoint: $num" +sed -i.bak -e 's/const Type'"${DEST_UC}"'.*/const Type'"${DEST_UC}"' = '"$num"'/g' "${DEST_LC}.go" +rm "${DEST_LC}.go.bak" +grep -E "^const Type${DEST_UC}" "${DEST_LC}.go" + +echo ' case "'"${DEST_UC}"'": + rc.RDATA = privatetypesrdata.'"${DEST_UC}"'{}' | pbcopy +vi +/Incomplete ../../models/fixhack.go + +echo "vi ../../models/fixhack.go" +echo '../../integrationTest/integration_test.go' diff --git a/pkg/privatetypes/akamaicdn.go b/pkg/privatetypes/akamaicdn.go new file mode 100644 index 0000000000..6d6c7ed32f --- /dev/null +++ b/pkg/privatetypes/akamaicdn.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// AKAMAICDN + +func init() { + Register(TypeAKAMAICDN, "AKAMAICDN", func() dnsv2.RR { return new(AKAMAICDN) }) +} + +const TypeAKAMAICDN = 65318 + +type AKAMAICDN struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *AKAMAICDN) Type() uint16 { return TypeAKAMAICDN } + +// RR interface. +func (rr *AKAMAICDN) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *AKAMAICDN) Len() int { return rr.Hdr.Len() } +func (rr *AKAMAICDN) Data() dnsv2.RDATA { + return &privatetypesrdata.AKAMAICDN{} +} +func (rr *AKAMAICDN) Clone() dnsv2.RR { + return &AKAMAICDN{rr.Hdr} +} +func (rr *AKAMAICDN) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tAKAMAICDN" +} + +// Parser interface. +func (rr *AKAMAICDN) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/akamaicdn_test.go b/pkg/privatetypes/akamaicdn_test.go new file mode 100644 index 0000000000..51da5b41ca --- /dev/null +++ b/pkg/privatetypes/akamaicdn_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestAkamaiCdn(t *testing.T) { + y := &AKAMAICDN{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("AKAMAICDN string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/akamaitlc.go b/pkg/privatetypes/akamaitlc.go new file mode 100644 index 0000000000..aaf833f321 --- /dev/null +++ b/pkg/privatetypes/akamaitlc.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// AKAMAITLC + +func init() { + Register(TypeAKAMAITLC, "AKAMAITLC", func() dnsv2.RR { return new(AKAMAITLC) }) +} + +const TypeAKAMAITLC = 65319 + +type AKAMAITLC struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *AKAMAITLC) Type() uint16 { return TypeAKAMAITLC } + +// RR interface. +func (rr *AKAMAITLC) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *AKAMAITLC) Len() int { return rr.Hdr.Len() } +func (rr *AKAMAITLC) Data() dnsv2.RDATA { + return &privatetypesrdata.AKAMAITLC{} +} +func (rr *AKAMAITLC) Clone() dnsv2.RR { + return &AKAMAITLC{rr.Hdr} +} +func (rr *AKAMAITLC) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tAKAMAITLC" +} + +// Parser interface. +func (rr *AKAMAITLC) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/akamaitlc_test.go b/pkg/privatetypes/akamaitlc_test.go new file mode 100644 index 0000000000..53574dd0d5 --- /dev/null +++ b/pkg/privatetypes/akamaitlc_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestAkamaiTlc(t *testing.T) { + y := &AKAMAITLC{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("AKAMAITLC string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/bunny_dns_pz.go b/pkg/privatetypes/bunny_dns_pz.go new file mode 100644 index 0000000000..5e1778222b --- /dev/null +++ b/pkg/privatetypes/bunny_dns_pz.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// BUNNY_DNS_PZ + +func init() { + Register(TypeBUNNY_DNS_PZ, "BUNNY_DNS_PZ", func() dnsv2.RR { return new(BUNNY_DNS_PZ) }) +} + +const TypeBUNNY_DNS_PZ = 65313 + +type BUNNY_DNS_PZ struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *BUNNY_DNS_PZ) Type() uint16 { return TypeBUNNY_DNS_PZ } + +// RR interface. +func (rr *BUNNY_DNS_PZ) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *BUNNY_DNS_PZ) Len() int { return rr.Hdr.Len() } +func (rr *BUNNY_DNS_PZ) Data() dnsv2.RDATA { + return &privatetypesrdata.BUNNY_DNS_PZ{} +} +func (rr *BUNNY_DNS_PZ) Clone() dnsv2.RR { + return &BUNNY_DNS_PZ{rr.Hdr} +} +func (rr *BUNNY_DNS_PZ) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tBUNNY_DNS_PZ" +} + +// Parser interface. +func (rr *BUNNY_DNS_PZ) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/bunny_dns_pz_test.go b/pkg/privatetypes/bunny_dns_pz_test.go new file mode 100644 index 0000000000..68922f69be --- /dev/null +++ b/pkg/privatetypes/bunny_dns_pz_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestBunny_DNS_Pz(t *testing.T) { + y := &BUNNY_DNS_PZ{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("BUNNY_DNS_PZ string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/bunny_dns_rdr.go b/pkg/privatetypes/bunny_dns_rdr.go new file mode 100644 index 0000000000..b02421db1b --- /dev/null +++ b/pkg/privatetypes/bunny_dns_rdr.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// BUNNY_DNS_RDR + +func init() { + Register(TypeBUNNY_DNS_RDR, "BUNNY_DNS_RDR", func() dnsv2.RR { return new(BUNNY_DNS_RDR) }) +} + +const TypeBUNNY_DNS_RDR = 65320 + +type BUNNY_DNS_RDR struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *BUNNY_DNS_RDR) Type() uint16 { return TypeBUNNY_DNS_RDR } + +// RR interface. +func (rr *BUNNY_DNS_RDR) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *BUNNY_DNS_RDR) Len() int { return rr.Hdr.Len() } +func (rr *BUNNY_DNS_RDR) Data() dnsv2.RDATA { + return &privatetypesrdata.BUNNY_DNS_RDR{} +} +func (rr *BUNNY_DNS_RDR) Clone() dnsv2.RR { + return &BUNNY_DNS_RDR{rr.Hdr} +} +func (rr *BUNNY_DNS_RDR) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tBUNNY_DNS_RDR" +} + +// Parser interface. +func (rr *BUNNY_DNS_RDR) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/bunny_dns_rdr_test.go b/pkg/privatetypes/bunny_dns_rdr_test.go new file mode 100644 index 0000000000..0341e065d8 --- /dev/null +++ b/pkg/privatetypes/bunny_dns_rdr_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestBunny_DNS_Rdr(t *testing.T) { + y := &BUNNY_DNS_RDR{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("BUNNY_DNS_RDR string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/cloudns_wr.go b/pkg/privatetypes/cloudns_wr.go new file mode 100644 index 0000000000..67158ac3e0 --- /dev/null +++ b/pkg/privatetypes/cloudns_wr.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// CLOUDNS_WR + +func init() { + Register(TypeCLOUDNS_WR, "CLOUDNS_WR", func() dnsv2.RR { return new(CLOUDNS_WR) }) +} + +const TypeCLOUDNS_WR = 65315 + +type CLOUDNS_WR struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *CLOUDNS_WR) Type() uint16 { return TypeCLOUDNS_WR } + +// RR interface. +func (rr *CLOUDNS_WR) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *CLOUDNS_WR) Len() int { return rr.Hdr.Len() } +func (rr *CLOUDNS_WR) Data() dnsv2.RDATA { + return &privatetypesrdata.CLOUDNS_WR{} +} +func (rr *CLOUDNS_WR) Clone() dnsv2.RR { + return &CLOUDNS_WR{rr.Hdr} +} +func (rr *CLOUDNS_WR) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tCLOUDNS_WR" +} + +// Parser interface. +func (rr *CLOUDNS_WR) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/cloudns_wr_test.go b/pkg/privatetypes/cloudns_wr_test.go new file mode 100644 index 0000000000..fab6c7c86d --- /dev/null +++ b/pkg/privatetypes/cloudns_wr_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestCloudns_Wr(t *testing.T) { + y := &CLOUDNS_WR{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("CLOUDNS_WR string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/frame.go b/pkg/privatetypes/frame.go new file mode 100644 index 0000000000..470608576b --- /dev/null +++ b/pkg/privatetypes/frame.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// FRAME + +func init() { + Register(TypeFRAME, "FRAME", func() dnsv2.RR { return new(FRAME) }) +} + +const TypeFRAME = 65312 + +type FRAME struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *FRAME) Type() uint16 { return TypeFRAME } + +// RR interface. +func (rr *FRAME) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *FRAME) Len() int { return rr.Hdr.Len() } +func (rr *FRAME) Data() dnsv2.RDATA { + return &privatetypesrdata.FRAME{} +} +func (rr *FRAME) Clone() dnsv2.RR { + return &FRAME{rr.Hdr} +} +func (rr *FRAME) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tFRAME" +} + +// Parser interface. +func (rr *FRAME) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/frame_test.go b/pkg/privatetypes/frame_test.go new file mode 100644 index 0000000000..bb671cbdb6 --- /dev/null +++ b/pkg/privatetypes/frame_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestFrame(t *testing.T) { + y := &FRAME{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("FRAME string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/lua.go b/pkg/privatetypes/lua.go new file mode 100644 index 0000000000..7f0d546795 --- /dev/null +++ b/pkg/privatetypes/lua.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// LUA + +func init() { + Register(TypeLUA, "LUA", func() dnsv2.RR { return new(LUA) }) +} + +const TypeLUA = 65314 + +type LUA struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *LUA) Type() uint16 { return TypeLUA } + +// RR interface. +func (rr *LUA) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *LUA) Len() int { return rr.Hdr.Len() } +func (rr *LUA) Data() dnsv2.RDATA { + return &privatetypesrdata.LUA{} +} +func (rr *LUA) Clone() dnsv2.RR { + return &LUA{rr.Hdr} +} +func (rr *LUA) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tLUA" +} + +// Parser interface. +func (rr *LUA) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/lua_test.go b/pkg/privatetypes/lua_test.go new file mode 100644 index 0000000000..e011210fe9 --- /dev/null +++ b/pkg/privatetypes/lua_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestLua(t *testing.T) { + y := &LUA{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("LUA string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/mikrotik_fwd.go b/pkg/privatetypes/mikrotik_fwd.go new file mode 100644 index 0000000000..363d60a138 --- /dev/null +++ b/pkg/privatetypes/mikrotik_fwd.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// MIKROTIK_FWD + +func init() { + Register(TypeMIKROTIK_FWD, "MIKROTIK_FWD", func() dnsv2.RR { return new(MIKROTIK_FWD) }) +} + +const TypeMIKROTIK_FWD = 65307 + +type MIKROTIK_FWD struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *MIKROTIK_FWD) Type() uint16 { return TypeMIKROTIK_FWD } + +// RR interface. +func (rr *MIKROTIK_FWD) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *MIKROTIK_FWD) Len() int { return rr.Hdr.Len() } +func (rr *MIKROTIK_FWD) Data() dnsv2.RDATA { + return &privatetypesrdata.MIKROTIK_FWD{} +} +func (rr *MIKROTIK_FWD) Clone() dnsv2.RR { + return &MIKROTIK_FWD{rr.Hdr} +} +func (rr *MIKROTIK_FWD) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tMIKROTIK_FWD" +} + +// Parser interface. +func (rr *MIKROTIK_FWD) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/mikrotik_fwd_test.go b/pkg/privatetypes/mikrotik_fwd_test.go new file mode 100644 index 0000000000..5a08b281d8 --- /dev/null +++ b/pkg/privatetypes/mikrotik_fwd_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestMikrotik_Fwd(t *testing.T) { + y := &MIKROTIK_FWD{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("MIKROTIK_FWD string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/mikrotik_nxdomain.go b/pkg/privatetypes/mikrotik_nxdomain.go new file mode 100644 index 0000000000..79faa3491a --- /dev/null +++ b/pkg/privatetypes/mikrotik_nxdomain.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// MIKROTIK_NXDOMAIN + +func init() { + Register(TypeMIKROTIK_NXDOMAIN, "MIKROTIK_NXDOMAIN", func() dnsv2.RR { return new(MIKROTIK_NXDOMAIN) }) +} + +const TypeMIKROTIK_NXDOMAIN = 65308 + +type MIKROTIK_NXDOMAIN struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *MIKROTIK_NXDOMAIN) Type() uint16 { return TypeMIKROTIK_NXDOMAIN } + +// RR interface. +func (rr *MIKROTIK_NXDOMAIN) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *MIKROTIK_NXDOMAIN) Len() int { return rr.Hdr.Len() } +func (rr *MIKROTIK_NXDOMAIN) Data() dnsv2.RDATA { + return &privatetypesrdata.MIKROTIK_NXDOMAIN{} +} +func (rr *MIKROTIK_NXDOMAIN) Clone() dnsv2.RR { + return &MIKROTIK_NXDOMAIN{rr.Hdr} +} +func (rr *MIKROTIK_NXDOMAIN) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tMIKROTIK_NXDOMAIN" +} + +// Parser interface. +func (rr *MIKROTIK_NXDOMAIN) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/mikrotik_nxdomain_test.go b/pkg/privatetypes/mikrotik_nxdomain_test.go new file mode 100644 index 0000000000..e1f943c45f --- /dev/null +++ b/pkg/privatetypes/mikrotik_nxdomain_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestMikrotik_NxDomain(t *testing.T) { + y := &MIKROTIK_NXDOMAIN{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("MIKROTIK_NXDOMAIN string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/netlify.go b/pkg/privatetypes/netlify.go new file mode 100644 index 0000000000..ea296a6c4e --- /dev/null +++ b/pkg/privatetypes/netlify.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// NETLIFY + +func init() { + Register(TypeNETLIFY, "NETLIFY", func() dnsv2.RR { return new(NETLIFY) }) +} + +const TypeNETLIFY = 65316 + +type NETLIFY struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *NETLIFY) Type() uint16 { return TypeNETLIFY } + +// RR interface. +func (rr *NETLIFY) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *NETLIFY) Len() int { return rr.Hdr.Len() } +func (rr *NETLIFY) Data() dnsv2.RDATA { + return &privatetypesrdata.NETLIFY{} +} +func (rr *NETLIFY) Clone() dnsv2.RR { + return &NETLIFY{rr.Hdr} +} +func (rr *NETLIFY) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tNETLIFY" +} + +// Parser interface. +func (rr *NETLIFY) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/netlify_test.go b/pkg/privatetypes/netlify_test.go new file mode 100644 index 0000000000..708ea3998b --- /dev/null +++ b/pkg/privatetypes/netlify_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestNetlify(t *testing.T) { + y := &NETLIFY{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("NETLIFY string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/netlifyv6.go b/pkg/privatetypes/netlifyv6.go new file mode 100644 index 0000000000..d1da4f7d65 --- /dev/null +++ b/pkg/privatetypes/netlifyv6.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// NETLIFYV6 + +func init() { + Register(TypeNETLIFYV6, "NETLIFYV6", func() dnsv2.RR { return new(NETLIFYV6) }) +} + +const TypeNETLIFYV6 = 65317 + +type NETLIFYV6 struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *NETLIFYV6) Type() uint16 { return TypeNETLIFYV6 } + +// RR interface. +func (rr *NETLIFYV6) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *NETLIFYV6) Len() int { return rr.Hdr.Len() } +func (rr *NETLIFYV6) Data() dnsv2.RDATA { + return &privatetypesrdata.NETLIFYV6{} +} +func (rr *NETLIFYV6) Clone() dnsv2.RR { + return &NETLIFYV6{rr.Hdr} +} +func (rr *NETLIFYV6) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tNETLIFYV6" +} + +// Parser interface. +func (rr *NETLIFYV6) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/netlifyv6_test.go b/pkg/privatetypes/netlifyv6_test.go new file mode 100644 index 0000000000..41a4f3278a --- /dev/null +++ b/pkg/privatetypes/netlifyv6_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestNetlifyV6(t *testing.T) { + y := &NETLIFYV6{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("NETLIFYV6 string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/porkbun_urlfwd.go b/pkg/privatetypes/porkbun_urlfwd.go new file mode 100644 index 0000000000..ee6705a326 --- /dev/null +++ b/pkg/privatetypes/porkbun_urlfwd.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// PORKBUN_URLFWD + +func init() { + Register(TypePORKBUN_URLFWD, "PORKBUN_URLFWD", func() dnsv2.RR { return new(PORKBUN_URLFWD) }) +} + +const TypePORKBUN_URLFWD = 65309 + +type PORKBUN_URLFWD struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *PORKBUN_URLFWD) Type() uint16 { return TypePORKBUN_URLFWD } + +// RR interface. +func (rr *PORKBUN_URLFWD) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *PORKBUN_URLFWD) Len() int { return rr.Hdr.Len() } +func (rr *PORKBUN_URLFWD) Data() dnsv2.RDATA { + return &privatetypesrdata.PORKBUN_URLFWD{} +} +func (rr *PORKBUN_URLFWD) Clone() dnsv2.RR { + return &PORKBUN_URLFWD{rr.Hdr} +} +func (rr *PORKBUN_URLFWD) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tPORKBUN_URLFWD" +} + +// Parser interface. +func (rr *PORKBUN_URLFWD) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/porkbun_urlfwd_test.go b/pkg/privatetypes/porkbun_urlfwd_test.go new file mode 100644 index 0000000000..079db555cf --- /dev/null +++ b/pkg/privatetypes/porkbun_urlfwd_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestPorkbun_UrlFwd(t *testing.T) { + y := &PORKBUN_URLFWD{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("PORKBUN_URLFWD string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/rdata/akamaicdn.go b/pkg/privatetypes/rdata/akamaicdn.go new file mode 100644 index 0000000000..2f42cee612 --- /dev/null +++ b/pkg/privatetypes/rdata/akamaicdn.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type AKAMAICDN struct { +} + +func (rd AKAMAICDN) Len() int { + return 0 +} + +func (rd AKAMAICDN) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/akamaitlc.go b/pkg/privatetypes/rdata/akamaitlc.go new file mode 100644 index 0000000000..ef90b4973b --- /dev/null +++ b/pkg/privatetypes/rdata/akamaitlc.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type AKAMAITLC struct { +} + +func (rd AKAMAITLC) Len() int { + return 0 +} + +func (rd AKAMAITLC) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/bunny_dns_pz.go b/pkg/privatetypes/rdata/bunny_dns_pz.go new file mode 100644 index 0000000000..f592d865e7 --- /dev/null +++ b/pkg/privatetypes/rdata/bunny_dns_pz.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type BUNNY_DNS_PZ struct { +} + +func (rd BUNNY_DNS_PZ) Len() int { + return 0 +} + +func (rd BUNNY_DNS_PZ) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/bunny_dns_rdr.go b/pkg/privatetypes/rdata/bunny_dns_rdr.go new file mode 100644 index 0000000000..41d31dc5b7 --- /dev/null +++ b/pkg/privatetypes/rdata/bunny_dns_rdr.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type BUNNY_DNS_RDR struct { +} + +func (rd BUNNY_DNS_RDR) Len() int { + return 0 +} + +func (rd BUNNY_DNS_RDR) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/cloudns_wr.go b/pkg/privatetypes/rdata/cloudns_wr.go new file mode 100644 index 0000000000..f79bd38de3 --- /dev/null +++ b/pkg/privatetypes/rdata/cloudns_wr.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type CLOUDNS_WR struct { +} + +func (rd CLOUDNS_WR) Len() int { + return 0 +} + +func (rd CLOUDNS_WR) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/frame.go b/pkg/privatetypes/rdata/frame.go new file mode 100644 index 0000000000..3a68948d16 --- /dev/null +++ b/pkg/privatetypes/rdata/frame.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type FRAME struct { +} + +func (rd FRAME) Len() int { + return 0 +} + +func (rd FRAME) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/lua.go b/pkg/privatetypes/rdata/lua.go new file mode 100644 index 0000000000..2a93298fcf --- /dev/null +++ b/pkg/privatetypes/rdata/lua.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type LUA struct { +} + +func (rd LUA) Len() int { + return 0 +} + +func (rd LUA) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/mikrotik_fwd.go b/pkg/privatetypes/rdata/mikrotik_fwd.go new file mode 100644 index 0000000000..3b8c3e3244 --- /dev/null +++ b/pkg/privatetypes/rdata/mikrotik_fwd.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type MIKROTIK_FWD struct { +} + +func (rd MIKROTIK_FWD) Len() int { + return 0 +} + +func (rd MIKROTIK_FWD) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/mikrotik_nxdomain.go b/pkg/privatetypes/rdata/mikrotik_nxdomain.go new file mode 100644 index 0000000000..a453f063bb --- /dev/null +++ b/pkg/privatetypes/rdata/mikrotik_nxdomain.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type MIKROTIK_NXDOMAIN struct { +} + +func (rd MIKROTIK_NXDOMAIN) Len() int { + return 0 +} + +func (rd MIKROTIK_NXDOMAIN) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/netlify.go b/pkg/privatetypes/rdata/netlify.go new file mode 100644 index 0000000000..0828150292 --- /dev/null +++ b/pkg/privatetypes/rdata/netlify.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type NETLIFY struct { +} + +func (rd NETLIFY) Len() int { + return 0 +} + +func (rd NETLIFY) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/netlifyv6.go b/pkg/privatetypes/rdata/netlifyv6.go new file mode 100644 index 0000000000..0eb240827d --- /dev/null +++ b/pkg/privatetypes/rdata/netlifyv6.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type NETLIFYV6 struct { +} + +func (rd NETLIFYV6) Len() int { + return 0 +} + +func (rd NETLIFYV6) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/porkbun_urlfwd.go b/pkg/privatetypes/rdata/porkbun_urlfwd.go new file mode 100644 index 0000000000..98d7ce825c --- /dev/null +++ b/pkg/privatetypes/rdata/porkbun_urlfwd.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type PORKBUN_URLFWD struct { +} + +func (rd PORKBUN_URLFWD) Len() int { + return 0 +} + +func (rd PORKBUN_URLFWD) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/url.go b/pkg/privatetypes/rdata/url.go new file mode 100644 index 0000000000..ffc20aadd3 --- /dev/null +++ b/pkg/privatetypes/rdata/url.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type URL struct { +} + +func (rd URL) Len() int { + return 0 +} + +func (rd URL) String() string { + return "" +} diff --git a/pkg/privatetypes/rdata/url301.go b/pkg/privatetypes/rdata/url301.go new file mode 100644 index 0000000000..ed80129e59 --- /dev/null +++ b/pkg/privatetypes/rdata/url301.go @@ -0,0 +1,12 @@ +package privatetypesrdata + +type URL301 struct { +} + +func (rd URL301) Len() int { + return 0 +} + +func (rd URL301) String() string { + return "" +} diff --git a/pkg/privatetypes/url.go b/pkg/privatetypes/url.go new file mode 100644 index 0000000000..ee9ef15554 --- /dev/null +++ b/pkg/privatetypes/url.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// URL + +func init() { + Register(TypeURL, "URL", func() dnsv2.RR { return new(URL) }) +} + +const TypeURL = 65310 + +type URL struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *URL) Type() uint16 { return TypeURL } + +// RR interface. +func (rr *URL) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *URL) Len() int { return rr.Hdr.Len() } +func (rr *URL) Data() dnsv2.RDATA { + return &privatetypesrdata.URL{} +} +func (rr *URL) Clone() dnsv2.RR { + return &URL{rr.Hdr} +} +func (rr *URL) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tURL" +} + +// Parser interface. +func (rr *URL) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/url301.go b/pkg/privatetypes/url301.go new file mode 100644 index 0000000000..2ec67eed55 --- /dev/null +++ b/pkg/privatetypes/url301.go @@ -0,0 +1,44 @@ +package privatetypes + +import ( + "strconv" + + dnsv2 "codeberg.org/miekg/dns" + dnsutilv2 "codeberg.org/miekg/dns/dnsutil" + privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" +) + +// URL301 + +func init() { + Register(TypeURL301, "URL301", func() dnsv2.RR { return new(URL301) }) +} + +const TypeURL301 = 65311 + +type URL301 struct { + Hdr dnsv2.Header +} + +// Typer interface. +func (rr *URL301) Type() uint16 { return TypeURL301 } + +// RR interface. +func (rr *URL301) Header() *dnsv2.Header { return &rr.Hdr } +func (rr *URL301) Len() int { return rr.Hdr.Len() } +func (rr *URL301) Data() dnsv2.RDATA { + return &privatetypesrdata.URL301{} +} +func (rr *URL301) Clone() dnsv2.RR { + return &URL301{rr.Hdr} +} +func (rr *URL301) String() string { + return rr.Header().Name + "\t" + + strconv.FormatInt(int64(rr.Header().TTL), 10) + "\t" + + dnsutilv2.ClassToString(rr.Header().Class) + "\tURL301" +} + +// Parser interface. +func (rr *URL301) Parse(tokens []string, _ string) error { + return nil +} diff --git a/pkg/privatetypes/url301_test.go b/pkg/privatetypes/url301_test.go new file mode 100644 index 0000000000..3f70bae3c7 --- /dev/null +++ b/pkg/privatetypes/url301_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestUrl301(t *testing.T) { + y := &URL301{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("URL301 string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} diff --git a/pkg/privatetypes/url_test.go b/pkg/privatetypes/url_test.go new file mode 100644 index 0000000000..f3d0caa602 --- /dev/null +++ b/pkg/privatetypes/url_test.go @@ -0,0 +1,18 @@ +package privatetypes + +import ( + "testing" + + dnsv2 "codeberg.org/miekg/dns" +) + +func TestUrl(t *testing.T) { + y := &URL{Hdr: dnsv2.Header{Name: "example.org.", Class: dnsv2.ClassINET}} + rry, err := dnsv2.New(y.String()) + if err != nil { + t.Fatal(err) + } + if rry.String() != y.String() { + t.Fatalf("URL string presentations should be identical:\n%q\n%q", rry.String(), y.String()) + } +} From 6aa27ed6368e2712cb402c7bd9ef0e789c06e57b Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 29 May 2026 08:32:24 -0400 Subject: [PATCH 32/34] Fix AddNSRecords --- pkg/nameservers/nameservers.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/nameservers/nameservers.go b/pkg/nameservers/nameservers.go index 7e0978a84e..860b56d82b 100644 --- a/pkg/nameservers/nameservers.go +++ b/pkg/nameservers/nameservers.go @@ -82,6 +82,9 @@ func AddNSRecords(dc *models.DomainConfig) { fmt.Printf("failed AddNSRecords rc.SetTarget(%q): %s\n", t, err) } + // Hack + rc.FixUp(dc.Name) + dc.Records = append(dc.Records, rc) } } From 917069f4b14da24e92871f20e837dcdec1569a57 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 29 May 2026 10:03:02 -0400 Subject: [PATCH 33/34] wip! --- integrationTest/helpers_integration_test.go | 51 +++++++----- models/fixhack.go | 15 +++- models/record.go | 3 +- models/t_svcb.go | 60 ++++++++------ models/target.go | 7 ++ pkg/dnsrr/dnsrr.go | 90 +++++++++++++++++++++ pkg/prettyzone/prettyzone.go | 3 + pkg/rtypecontrol/fixlegacy.go | 88 +------------------- providers/bind/bindProvider.go | 20 +++-- 9 files changed, 192 insertions(+), 145 deletions(-) diff --git a/integrationTest/helpers_integration_test.go b/integrationTest/helpers_integration_test.go index 350903bcab..8654f67d5d 100644 --- a/integrationTest/helpers_integration_test.go +++ b/integrationTest/helpers_integration_test.go @@ -519,26 +519,28 @@ func dnskey(name string, flags uint16, protocol, algorithm uint8, publicKey stri } func https(name string, priority uint16, target string, params string) *models.RecordConfig { - r := makeRec(name, target, "HTTPS") + r := makeRecNoFix(name, target, "HTTPS") r.SvcPriority = priority r.SvcParams = params - // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go - rty := dnsv2.TypeSVCB - cp := params - if strings.Contains(cp, "ech=IGNORE") { - cp = strings.ReplaceAll(cp, "ech=IGNORE", "") - } - rrv2, err := dnsv2.NewData(rty, fmt.Sprintf("%d %s %s", priority, target, cp)) - if err != nil { - panic(fmt.Sprintf("could not parse SVCB record: %s (%d %s %s)", err, priority, target, cp)) - } - r.RDATA = rrv2 - old := r.RDATA.String() - r.ComparableV3 = r.RDATA.String() - if r.ComparableV3 != old { - panic("DEBUG CV3") - } + // // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go + // rty := dnsv2.TypeSVCB + // cp := params + // if strings.Contains(cp, "ech=IGNORE") { + // cp = strings.ReplaceAll(cp, "ech=IGNORE", "") + // } + // rrv2, err := dnsv2.NewData(rty, fmt.Sprintf("%d %s %s", priority, target, cp)) + // if err != nil { + // panic(fmt.Sprintf("could not parse SVCB record: %s (%d %s %s)", err, priority, target, cp)) + // } + // r.RDATA = rrv2 + // old := r.RDATA.String() + // r.ComparableV3 = r.RDATA.String() + // if r.ComparableV3 != old { + // panic("DEBUG CV3") + // } + + r.FixUp(globalDCN.NameASCII) // Hack. Populates .RDATA and .TypeNum if needed. return r } @@ -572,15 +574,18 @@ func loc(name string, d1 uint8, m1 uint8, s1 float32, ns string, } func makeRec(name, target, typ string) *models.RecordConfig { + r := makeRecNoFix(name, target, typ) + r.FixUp(globalDCN.NameASCII) // Hack. Populates .RDATA and .TypeNum if needed. + return r +} + +func makeRecNoFix(name, target, typ string) *models.RecordConfig { r := &models.RecordConfig{ Type: typ, TTL: 300, } SetLabel(r, name, "**current-domain**.") r.MustSetTarget(target) - - r.FixUp(globalDCN.NameASCII) // Populates .RDATA and .TypeNum if needed. - return r } @@ -593,8 +598,9 @@ func manyA(namePattern, target string, n int) []*models.RecordConfig { } func mx(name string, prio uint16, target string) *models.RecordConfig { - r := makeRec(name, target, "MX") + r := makeRecNoFix(name, target, "MX") r.MxPreference = prio + r.FixUp(globalDCN.NameASCII) // Hack. Populates .RDATA and .TypeNum if needed. return r } @@ -795,8 +801,9 @@ func tc(desc string, recs ...*models.RecordConfig) *TestCase { } func txt(name, target string) *models.RecordConfig { - r := makeRec(name, "", "TXT") + r := makeRecNoFix(name, "", "TXT") panicOnErr(r.SetTargetTXT(target)) + r.FixUp(globalDCN.NameASCII) // Hack. Populates .RDATA and .TypeNum if needed. return r } diff --git a/models/fixhack.go b/models/fixhack.go index f8b703b8c4..982727c852 100644 --- a/models/fixhack.go +++ b/models/fixhack.go @@ -76,7 +76,7 @@ func (rc *RecordConfig) FixUp(origin string) { case "CAA": rc.RDATA = dnsrdatav2.CAA{Flag: rc.CaaFlag, Tag: rc.CaaTag, Value: rc.GetTargetField()} case "CNAME": - targ := dnsutilv1.AddOrigin(rc.GetTargetField(), origin) + targ := dnsutilv1.AddOrigin(rc.GetTargetField(), origin+".") rc.RDATA = dnsrdatav2.CNAME{Target: targ} case "CF_WORKER_ROUTE": @@ -98,7 +98,13 @@ func (rc *RecordConfig) FixUp(origin string) { panic("BUG: Failed to convert SVCB value to v2: " + err.Error()) } rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: rc.GetTargetField(), Value: valuev2} - rc.ComparableV3 = rc.RDATA.String() + x1, x2, x3 := rc.RDATA.String(), rc.String(), rc.GetTargetCombined() + if x1 != x2 { + panic(fmt.Sprintf("BUG: SVCB String() is not stable: RDATA.String()=%s RecordConfig.String()=%s", x1, x2)) + } + if x1 != x3 { + panic(fmt.Sprintf("BUG: SVCB String() is not stable: RDATA.String()=%s GetTargetCombined()=%s", x1, x3)) + } case "LOC": rc.RDATA = dnsrdatav2.LOC{Version: rc.LocVersion, Size: rc.LocSize, HorizPre: rc.LocHorizPre, VertPre: rc.LocVertPre, Latitude: rc.LocLatitude, Longitude: rc.LocLongitude, Altitude: rc.LocAltitude} @@ -141,7 +147,6 @@ func (rc *RecordConfig) FixUp(origin string) { panic("BUG: Failed to convert SVCB value to v2: " + err.Error()) } rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: rc.GetTargetField(), Value: valuev2} - rc.ComparableV3 = rc.RDATA.String() case "TLSA": rc.RDATA = dnsrdatav2.TLSA{Usage: rc.TlsaUsage, Selector: rc.TlsaSelector, MatchingType: rc.TlsaMatchingType, Certificate: rc.GetTargetField()} @@ -157,6 +162,8 @@ func (rc *RecordConfig) FixUp(origin string) { // .ComparableV3: if rc.ComparableV3 == "" { rc.ComparableV3 = rc.RDATA.String() - //fmt.Printf("DEBUG: COMPARE for %s --- %s\n", rc.Type, rc.ComparableV3) + if strings.HasSuffix(rc.ComparableV3, " ") { + rc.ComparableV3 += "W" + } } } diff --git a/models/record.go b/models/record.go index b045054f9b..33d75aa2dd 100644 --- a/models/record.go +++ b/models/record.go @@ -369,7 +369,8 @@ func (rc *RecordConfig) ToComparableNoTTL() string { case "UNKNOWN": return fmt.Sprintf("rtype=%s rdata=%s", rc.UnknownTypeName, rc.target) case "HTTPS", "SVCB": - return rc.targetCombinedSVCBRaw() + panic("unused ToComparableNoTTL for SVCB/HTTPS. Should be using .ComparableV3 instead") + //return rc.targetCombinedSVCBRaw() } return rc.GetTargetCombined() diff --git a/models/t_svcb.go b/models/t_svcb.go index e55991690a..37dcc7a345 100644 --- a/models/t_svcb.go +++ b/models/t_svcb.go @@ -5,17 +5,16 @@ import ( "strings" dnsv2 "codeberg.org/miekg/dns" - dnsrdatav2 "codeberg.org/miekg/dns/rdata" svcbv2 "codeberg.org/miekg/dns/svcb" dnsv1 "github.com/miekg/dns" ) -func (rc *RecordConfig) targetCombinedSVCBRaw() string { - if rc.SvcParams == "" { - return fmt.Sprintf("%d %s", rc.SvcPriority, rc.target) - } - return fmt.Sprintf("%d %s %s", rc.SvcPriority, rc.target, rc.SvcParams) -} +// func (rc *RecordConfig) targetCombinedSVCBRaw() string { +// if rc.SvcParams == "" { +// return fmt.Sprintf("%d %s", rc.SvcPriority, rc.target) +// } +// return fmt.Sprintf("%d %s %s", rc.SvcPriority, rc.target, rc.SvcParams) +// } // SetTargetSVCB sets the SVCB fields. func (rc *RecordConfig) SetTargetSVCB(priority uint16, target string, params []dnsv1.SVCBKeyValue) error { @@ -25,7 +24,7 @@ func (rc *RecordConfig) SetTargetSVCB(priority uint16, target string, params []d } paramsStr := []string{} for _, kv := range params { - paramsStr = append(paramsStr, fmt.Sprintf("%s=%s", kv.Key(), kv.String())) + paramsStr = append(paramsStr, fmt.Sprintf("%s=%q", kv.Key(), kv.String())) } rc.SvcParams = strings.Join(paramsStr, " ") if rc.Type == "" { @@ -36,12 +35,13 @@ func (rc *RecordConfig) SetTargetSVCB(priority uint16, target string, params []d } // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go - valuev2, err := convertSVCBv1v2(params) - if err != nil { - return fmt.Errorf("failed to convert SVCB parameters from v1 to v2: %w", err) - } - rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: target, Value: valuev2} - rc.ComparableV3 = rc.RDATA.String() + // valuev2, err := convertSVCBv1v2(params) + // if err != nil { + // return fmt.Errorf("failed to convert SVCB parameters from v1 to v2: %w", err) + // } + // rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: target, Value: valuev2} + // rc.ComparableV3 = rc.RDATA.String() + "Z" + rc.FixUp(".") return nil } @@ -83,22 +83,34 @@ func (rc *RecordConfig) SetTargetSVCBString(origin, contents string) error { func convertSVCBv1v2(params []dnsv1.SVCBKeyValue) ([]svcbv2.Pair, error) { var value []svcbv2.Pair - for _, kv := range params { - k := kv.Key().String() - keyCode := svcbv2.StringToKey(k) - v := kv.String() + for _, kvV1 := range params { + kV1 := kvV1.Key().String() + keyCodeV2 := svcbv2.StringToKey(kV1) + vV1 := kvV1.String() + if len(vV1) > 2 && vV1[0] == '"' && vV1[len(vV1)-1] == '"' { + panic("V has quotes") + } + fmt.Printf("DEBUG: convertSVCBv1v2: k=%s keyCode=%d v1=%s\n", kV1, keyCodeV2, vV1) - pairFn := svcbv2.KeyToPair(keyCode) + pairFn := svcbv2.KeyToPair(keyCodeV2) if pairFn == nil { - return nil, fmt.Errorf("failed to lookup svc key: %s", k) + return nil, fmt.Errorf("failed to lookup svc key: %s", kV1) } pair := pairFn() - if svcbv2.PairToKey(pair) != keyCode { - return nil, fmt.Errorf("key constant is not in sync: %v", keyCode) + if svcbv2.PairToKey(pair) != keyCodeV2 { + return nil, fmt.Errorf("key constant is not in sync: %v", keyCodeV2) } - err := svcbv2.Parse(pair, v, "") + err := svcbv2.Parse(pair, vV1, "") if err != nil { - return nil, fmt.Errorf("failed to parse svc pair: %s", k) + return nil, fmt.Errorf("failed to parse svc pair: %s", kV1) + } + + vV2 := pair.String() + if len(vV2) > 2 && vV2[0] == '"' && vV2[len(vV2)-1] == '"' { + panic("V2 has quotes") + } + if vV1 != vV2 { + panic(fmt.Sprintf("conversion from v1 to v2 is not stable: key=%s v1=%s v2=%s", kV1, vV1, vV2)) } value = append(value, pair) diff --git a/models/target.go b/models/target.go index f8bf108a53..2fc579f68e 100644 --- a/models/target.go +++ b/models/target.go @@ -89,6 +89,13 @@ func (rc *RecordConfig) zoneFileQuoted() string { if rc.Type == "NAPTR" && rc.GetTargetField() == "" { rc.MustSetTarget(".") } + + if rc.Type == "HTTPS" || rc.Type == "SVCB" { + if rc.RDATA == nil { + panic("drat") + } + } + rr := rc.ToRR() header := rr.Header().String() full := rr.String() diff --git a/pkg/dnsrr/dnsrr.go b/pkg/dnsrr/dnsrr.go index a7181c88f1..00615ad4c8 100644 --- a/pkg/dnsrr/dnsrr.go +++ b/pkg/dnsrr/dnsrr.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + dnsv2 "codeberg.org/miekg/dns" "github.com/DNSControl/dnscontrol/v4/models" "github.com/DNSControl/dnscontrol/v4/pkg/domaintags" "github.com/DNSControl/dnscontrol/v4/pkg/rtypecontrol" @@ -105,3 +106,92 @@ func helperRRtoRC(rr dnsv1.RR, origin string, fixBug bool) (models.RecordConfig, } return *rc, nil } + +func RRtoRCV2(rr dnsv2.RR, origin string) (models.RecordConfig, error) { + // Convert's dns.RR into DNSControl's models.RecordConfig struct. + + header := rr.Header() + //ty := dnsv1.TypeToString[header.Rrtype] + ty := dnsv2.TypeToString[dnsv2.RRToType(rr)] + + if rtypeinfo.IsModernType(ty) { + switch v := rr.(type) { + default: + rec, err := rtypecontrol.NewRecordConfigFromStruct(strings.TrimSuffix(header.Name, origin), header.TTL, ty, v, domaintags.MakeDomainNameVarieties(origin)) + return *rec, err + } + } + + rc := new(models.RecordConfig) + rc.Type = ty + rc.TTL = header.TTL + rc.Original = rr + rc.SetLabelFromFQDN(strings.TrimSuffix(header.Name, "."), origin) + var err error + switch v := rr.(type) { // #rtype_variations + case *dnsv2.A: + err = rc.SetTarget(v.A.String()) + case *dnsv2.AAAA: + err = rc.SetTarget(v.AAAA.String()) + case *dnsv2.CAA: + err = rc.SetTargetCAA(v.Flag, v.Tag, v.Value) + case *dnsv2.CNAME: + err = rc.SetTarget(v.Target) + case *dnsv2.DHCID: + err = rc.SetTarget(v.Digest) + case *dnsv2.DNAME: + err = rc.SetTarget(v.Target) + case *dnsv2.DS: + panic("DS should be handled as modern type") + case *dnsv2.DNSKEY: + err = rc.SetTargetDNSKEY(v.Flags, v.Protocol, v.Algorithm, v.PublicKey) + case *dnsv2.HTTPS: + //err = rc.SetTargetSVCB(v.Priority, v.Target, v.Value) + //err = fmt.Errorf("RR type %s should be handled as modern type", ty) + rc.RDATA = rr.Data() + case *dnsv2.LOC: + err = rc.SetTargetLOC(v.Version, v.Latitude, v.Longitude, v.Altitude, v.Size, v.HorizPre, v.VertPre) + case *dnsv2.MX: + err = rc.SetTargetMX(v.Preference, v.Mx) + case *dnsv2.NAPTR: + err = rc.SetTargetNAPTR(v.Order, v.Preference, v.Flags, v.Service, v.Regexp, v.Replacement) + case *dnsv2.OPENPGPKEY: + err = rc.SetTarget(v.PublicKey) + case *dnsv2.NS: + err = rc.SetTarget(v.Ns) + case *dnsv2.PTR: + err = rc.SetTarget(v.Ptr) + case *dnsv2.RP: + panic("RP should be handled as modern type") + case *dnsv2.SMIMEA: + err = rc.SetTargetSMIMEA(v.Usage, v.Selector, v.MatchingType, v.Certificate) + case *dnsv2.SOA: + err = rc.SetTargetSOA(v.Ns, v.Mbox, v.Serial, v.Refresh, v.Retry, v.Expire, v.Minttl) + case *dnsv2.SRV: + err = rc.SetTargetSRV(v.Priority, v.Weight, v.Port, v.Target) + case *dnsv2.SSHFP: + err = rc.SetTargetSSHFP(v.Algorithm, v.Type, v.FingerPrint) + case *dnsv2.SVCB: + //err = rc.SetTargetSVCB(v.Priority, v.Target, v.Value) + //err = fmt.Errorf("RR type %s should be handled as modern type", ty) + rc.RDATA = rr.Data() + case *dnsv2.TLSA: + err = rc.SetTargetTLSA(v.Usage, v.Selector, v.MatchingType, v.Certificate) + case *dnsv2.TXT: + if true { + t := strings.Join(v.Txt, "") + te := t + te = strings.ReplaceAll(te, `\\`, `\`) + te = strings.ReplaceAll(te, `\"`, `"`) + err = rc.SetTargetTXT(te) + } else { + err = rc.SetTargetTXTs(v.Txt) + } + default: + return *rc, fmt.Errorf("rrToRecord: Unimplemented zone record type=%s (%v)", rc.Type, rr) + } + if err != nil { + return *rc, fmt.Errorf("unparsable record received: %w", err) + } + return *rc, nil +} diff --git a/pkg/prettyzone/prettyzone.go b/pkg/prettyzone/prettyzone.go index 20f893807a..1e984d7c06 100644 --- a/pkg/prettyzone/prettyzone.go +++ b/pkg/prettyzone/prettyzone.go @@ -132,6 +132,9 @@ func (z *ZoneGenData) generateZoneFileHelper(w io.Writer) error { // the remaining line target := rr.GetTargetCombinedFunc(txtutil.EncodeQuoted) + if rr.Type == "HTTPS" || rr.Type == "SVCB" { + fmt.Printf("DEBUG: target for SVCB/HTTPS is %s\n", target) + } // comment comment := "" diff --git a/pkg/rtypecontrol/fixlegacy.go b/pkg/rtypecontrol/fixlegacy.go index 2449270904..22f9177563 100644 --- a/pkg/rtypecontrol/fixlegacy.go +++ b/pkg/rtypecontrol/fixlegacy.go @@ -36,91 +36,5 @@ func FixLegacyRecord(rec *models.RecordConfig, origin string) { } } - rec.FixUp(origin) // Populates .RDATA and .TypeNum if needed. - - // // Populate .RDATA if needed: - // if rec.RDATA == nil { - - // // The .RDATA structure itself. - // switch rec.Type { - // case "A": - // rec.RDATA = dnsrdatav2.A{Addr: rec.GetTargetIP()} - // case "AAAA": - // rec.RDATA = dnsrdatav2.AAAA{Addr: rec.GetTargetIP()} - - // case "CAA": - // rec.RDATA = dnsrdatav2.CAA{Flag: rec.CaaFlag, Tag: rec.CaaTag, Value: rec.GetTargetField()} - // case "CNAME": - // rec.RDATA = dnsrdatav2.CNAME{Target: rec.GetTargetField()} - - // case "DHCID": - // rec.RDATA = dnsrdatav2.DHCID{Digest: rec.GetTargetField()} - // case "DNAME": - // rec.RDATA = dnsrdatav2.DNAME{Target: rec.GetTargetField()} - // case "DNSKEY": - // rec.RDATA = dnsrdatav2.DNSKEY{Flags: rec.DnskeyFlags, Protocol: rec.DnskeyProtocol, Algorithm: rec.DnskeyAlgorithm, PublicKey: rec.GetTargetField()} - // case "DS": - // // no-op. See pkg/rtype/ds.go:FromStruct. - // panic("DS should already be converted to RDATA") - - // case "HTTPS": - // // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB - // panic("HTTPS should already be converted to RDATA") - - // case "LOC": - // rec.RDATA = dnsrdatav2.LOC{Version: rec.LocVersion, Size: rec.LocSize, HorizPre: rec.LocHorizPre, VertPre: rec.LocVertPre, Latitude: rec.LocLatitude, Longitude: rec.LocLongitude, Altitude: rec.LocAltitude} - - // case "MX": - // rec.RDATA = dnsrdatav2.MX{Preference: rec.MxPreference, Mx: rec.GetTargetField()} - - // case "NS": - // rec.RDATA = dnsrdatav2.NS{Ns: rec.GetTargetField()} - // case "NAPTR": - // rec.RDATA = dnsrdatav2.NAPTR{Order: rec.NaptrOrder, Preference: rec.NaptrPreference, Flags: rec.NaptrFlags, Service: rec.NaptrService, Regexp: rec.NaptrRegexp, Replacement: rec.GetTargetField()} - - // case "OPENPGPKEY": - // rec.RDATA = dnsrdatav2.OPENPGPKEY{PublicKey: rec.GetTargetField()} - - // case "PTR": - // rec.RDATA = dnsrdatav2.PTR{Ptr: rec.GetTargetField()} - - // case "RP": - // // no-op. See pkg/rtype/rp.go:FromStruct. - // panic("RP should already be converted to RDATA") - - // case "SMIMEA": - // rec.RDATA = dnsrdatav2.SMIMEA{Usage: rec.SmimeaUsage, Selector: rec.SmimeaSelector, MatchingType: rec.SmimeaMatchingType, Certificate: rec.GetTargetField()} - // case "SOA": - // rec.RDATA = dnsrdatav2.SOA{Ns: rec.GetTargetField(), Mbox: rec.SoaMbox, Serial: rec.SoaSerial, Refresh: rec.SoaRefresh, Retry: rec.SoaRetry, Expire: rec.SoaExpire, Minttl: rec.SoaMinttl} - // case "SRV": - // rec.RDATA = dnsrdatav2.SRV{Priority: rec.SrvPriority, Weight: rec.SrvWeight, Port: rec.SrvPort, Target: rec.GetTargetField()} - // case "SSHFP": - // rec.RDATA = dnsrdatav2.SSHFP{Algorithm: rec.SshfpAlgorithm, Type: rec.SshfpFingerprint, FingerPrint: rec.GetTargetField()} - - // case "TLSA": - // rec.RDATA = dnsrdatav2.TLSA{Usage: rec.TlsaUsage, Selector: rec.TlsaSelector, MatchingType: rec.TlsaMatchingType, Certificate: rec.GetTargetField()} - - // case "SVCB": - // // no-op. See pkg/rtype/t_svcb.go:SetTargetSVCB - // panic("SVCB should already be converted to RDATA") - - // case "TXT": - // rec.RDATA = dnsrdatav2.TXT{Txt: []string{rec.GetTargetField()}} - - // default: - // panic(fmt.Sprintf("RDATA CONVERSION NOT IMPLEMENTED TYPE=%q", rec.Type)) - // } - - // // TypeNum: - // tn, err := dnsutilv2.StringToType(rec.Type) - // if err != nil { - // panic("fix me") - // } - // rec.TypeNum = tn - - // // Comparable: - // rec.ComparableV3 = rec.RDATA.String() - // fmt.Printf("DEBUG: COMPARE for %s --- %s\n", rec.Type, rec.ComparableV3) - - //} + rec.FixUp(origin) // Hack. Populates .RDATA and .TypeNum if needed. } diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go index 6bc24fa55f..fbd4b02d39 100644 --- a/providers/bind/bindProvider.go +++ b/providers/bind/bindProvider.go @@ -21,6 +21,7 @@ import ( "strings" "time" + dnsv2 "codeberg.org/miekg/dns" "github.com/DNSControl/dnscontrol/v4/models" "github.com/DNSControl/dnscontrol/v4/pkg/bindserial" "github.com/DNSControl/dnscontrol/v4/pkg/diff2" @@ -31,7 +32,6 @@ import ( "github.com/DNSControl/dnscontrol/v4/pkg/providers" "github.com/DNSControl/dnscontrol/v4/pkg/rtypecontrol" "github.com/DNSControl/dnscontrol/v4/pkg/rtypeinfo" - dnsv1 "github.com/miekg/dns" ) // defaultZonesDir is used when the BIND credentials do not specify a @@ -241,7 +241,8 @@ func (c *bindProvider) GetZoneRecords(dc *models.DomainConfig) (models.Records, // ParseZoneContents parses a string as a BIND zone and returns the records. func ParseZoneContents(content string, zoneName string, zonefileName string) (models.Records, error) { - zp := dnsv1.NewZoneParser(strings.NewReader(content), zoneName, zonefileName) + //zp := dnsv1.NewZoneParser(strings.NewReader(content), zoneName, zonefileName) + zp := dnsv2.NewZoneParser(strings.NewReader(content), zoneName, zonefileName) foundRecords := models.Records{} for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { @@ -249,20 +250,25 @@ func ParseZoneContents(content string, zoneName string, zonefileName string) (mo var prec *models.RecordConfig var err error - rtype := rr.Header().Rrtype - rtypeStr := dnsv1.TypeToString[rtype] + // rtype := rr.Header().Rrtype + // rtypeStr := dnsv1.TypeToString[rtype] + + // Set rtypeStr to the string version of the type, but if it's a fake type, use the fake type name instead. + rtype := dnsv2.RRToType(rr) + rtypeStr := dnsv2.TypeToString[rtype] + if rtypeinfo.IsModernType(rtypeStr) { // Modern types: name := rr.Header().Name - prec, err = rtypecontrol.NewRecordConfigFromStruct(name, rr.Header().Ttl, rtypeStr, rr, domaintags.MakeDomainNameVarieties(zoneName)) + prec, err = rtypecontrol.NewRecordConfigFromStruct(name, rr.Header().TTL, rtypeStr, rr, domaintags.MakeDomainNameVarieties(zoneName)) if err != nil { return nil, err } rec = *prec - rec.TTL = rr.Header().Ttl + rec.TTL = rr.Header().TTL } else { // Legacy types: - rec, err = dnsrr.RRtoRCTxtBug(rr, zoneName) + rec, err = dnsrr.RRtoRCV2(rr, zoneName) if err != nil { return nil, err } From 1be8bf0e294c10880458ab064418f0ee60fe4f12 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 29 May 2026 16:29:48 -0400 Subject: [PATCH 34/34] Migrating to dnsv2 --- models/fixhack.go | 36 +++--- models/record.go | 4 +- models/t_svcb.go | 229 ++++++++++++++++++++++++++++----- models/target.go | 6 +- pkg/diff2/analyze.go | 27 ---- pkg/diff2/compareconfig.go | 6 + pkg/rtype/ds.go | 11 +- pkg/rtypecontrol/import.go | 5 +- providers/bind/bindProvider.go | 2 +- 9 files changed, 238 insertions(+), 88 deletions(-) diff --git a/models/fixhack.go b/models/fixhack.go index 982727c852..a3ad7c5c05 100644 --- a/models/fixhack.go +++ b/models/fixhack.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + dnsv2 "codeberg.org/miekg/dns" dnsutilv2 "codeberg.org/miekg/dns/dnsutil" dnsrdatav2 "codeberg.org/miekg/dns/rdata" privatetypesrdata "github.com/DNSControl/dnscontrol/v4/pkg/privatetypes/rdata" @@ -93,17 +94,18 @@ func (rc *RecordConfig) FixUp(origin string) { rc.RDATA = dnsrdatav2.DS{KeyTag: rc.DsKeyTag, Algorithm: rc.DsAlgorithm, DigestType: rc.DsDigestType, Digest: rc.GetTargetField()} case "HTTPS": - valuev2, err := convertSVCBv1v2(rc.GetSVCBValue()) - if err != nil { - panic("BUG: Failed to convert SVCB value to v2: " + err.Error()) - } - rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: rc.GetTargetField(), Value: valuev2} - x1, x2, x3 := rc.RDATA.String(), rc.String(), rc.GetTargetCombined() - if x1 != x2 { - panic(fmt.Sprintf("BUG: SVCB String() is not stable: RDATA.String()=%s RecordConfig.String()=%s", x1, x2)) - } - if x1 != x3 { - panic(fmt.Sprintf("BUG: SVCB String() is not stable: RDATA.String()=%s GetTargetCombined()=%s", x1, x3)) + if rc.SvcPriority == 0 { + rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: rc.GetTargetField()} + } else { + p := rc.SvcParams + p = strings.ReplaceAll(p, `ech=IGNORE`, ``) + p = strings.ReplaceAll(p, ` `, ` `) // Collapse 2 spaces into 1 + p = strings.TrimSpace(p) + rd, err := dnsv2.NewData(dnsv2.TypeHTTPS, fmt.Sprintf("%d %s %s", rc.SvcPriority, rc.GetTargetField(), p), origin) + if err != nil { + panic(fmt.Sprintf("BUG: Failed to create RDATA for HTTPS record: %v", err)) + } + rc.RDATA = rd } case "LOC": @@ -142,11 +144,15 @@ func (rc *RecordConfig) FixUp(origin string) { case "SSHFP": rc.RDATA = dnsrdatav2.SSHFP{Algorithm: rc.SshfpAlgorithm, Type: rc.SshfpFingerprint, FingerPrint: rc.GetTargetField()} case "SVCB": - valuev2, err := convertSVCBv1v2(rc.GetSVCBValue()) - if err != nil { - panic("BUG: Failed to convert SVCB value to v2: " + err.Error()) + if rc.SvcPriority == 0 { + rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: rc.GetTargetField()} + } else { + rd, err := dnsv2.NewData(dnsv2.TypeSVCB, fmt.Sprintf("%d %s %s", rc.SvcPriority, rc.GetTargetField(), rc.SvcParams), origin) + if err != nil { + panic(fmt.Sprintf("BUG: Failed to create RDATA for HTTPS record: %v", err)) + } + rc.RDATA = rd } - rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: rc.GetTargetField(), Value: valuev2} case "TLSA": rc.RDATA = dnsrdatav2.TLSA{Usage: rc.TlsaUsage, Selector: rc.TlsaSelector, MatchingType: rc.TlsaMatchingType, Certificate: rc.GetTargetField()} diff --git a/models/record.go b/models/record.go index 33d75aa2dd..9b83e7524a 100644 --- a/models/record.go +++ b/models/record.go @@ -369,8 +369,8 @@ func (rc *RecordConfig) ToComparableNoTTL() string { case "UNKNOWN": return fmt.Sprintf("rtype=%s rdata=%s", rc.UnknownTypeName, rc.target) case "HTTPS", "SVCB": - panic("unused ToComparableNoTTL for SVCB/HTTPS. Should be using .ComparableV3 instead") - //return rc.targetCombinedSVCBRaw() + //panic("unused ToComparableNoTTL for SVCB/HTTPS. Should be using .ComparableV3 instead") + return rc.targetCombinedSVCBRaw() } return rc.GetTargetCombined() diff --git a/models/t_svcb.go b/models/t_svcb.go index 37dcc7a345..0b147d8bb1 100644 --- a/models/t_svcb.go +++ b/models/t_svcb.go @@ -1,20 +1,23 @@ package models import ( + "bytes" "fmt" "strings" dnsv2 "codeberg.org/miekg/dns" + dnsrdatav2 "codeberg.org/miekg/dns/rdata" + "codeberg.org/miekg/dns/svcb" svcbv2 "codeberg.org/miekg/dns/svcb" dnsv1 "github.com/miekg/dns" ) -// func (rc *RecordConfig) targetCombinedSVCBRaw() string { -// if rc.SvcParams == "" { -// return fmt.Sprintf("%d %s", rc.SvcPriority, rc.target) -// } -// return fmt.Sprintf("%d %s %s", rc.SvcPriority, rc.target, rc.SvcParams) -// } +func (rc *RecordConfig) targetCombinedSVCBRaw() string { + if rc.SvcParams == "" { + return fmt.Sprintf("%d %s", rc.SvcPriority, rc.target) + } + return fmt.Sprintf("%d %s %s", rc.SvcPriority, rc.target, rc.SvcParams) +} // SetTargetSVCB sets the SVCB fields. func (rc *RecordConfig) SetTargetSVCB(priority uint16, target string, params []dnsv1.SVCBKeyValue) error { @@ -24,7 +27,7 @@ func (rc *RecordConfig) SetTargetSVCB(priority uint16, target string, params []d } paramsStr := []string{} for _, kv := range params { - paramsStr = append(paramsStr, fmt.Sprintf("%s=%q", kv.Key(), kv.String())) + paramsStr = append(paramsStr, fmt.Sprintf("%s=%s", kv.Key(), kv.String())) } rc.SvcParams = strings.Join(paramsStr, " ") if rc.Type == "" { @@ -34,6 +37,16 @@ func (rc *RecordConfig) SetTargetSVCB(priority uint16, target string, params []d panic("assertion failed: SetTargetSVCB called when .Type is not SVCB or HTTPS") } + if rc.SvcPriority == 0 { + rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: rc.GetTargetField()} + } else { + rd, err := dnsv2.NewData(dnsv2.TypeSVCB, fmt.Sprintf("%d %s %s", rc.SvcPriority, rc.GetTargetField(), rc.SvcParams), ".") + if err != nil { + panic(fmt.Sprintf("BUG: Failed to create RDATA for HTTPS record: %v", err)) + } + rc.RDATA = rd + } + // Hack to set .RDATA without importing miekg/dns in pkg/rtypecontrol/fixlegacy.go // valuev2, err := convertSVCBv1v2(params) // if err != nil { @@ -78,43 +91,195 @@ func (rc *RecordConfig) SetTargetSVCBString(origin, contents string) error { case *dnsv1.SVCB: return rc.SetTargetSVCB(r.Priority, r.Target, r.Value) } + + if rc.SvcPriority == 0 { + rc.RDATA = dnsrdatav2.SVCB{Priority: rc.SvcPriority, Target: rc.GetTargetField()} + } else { + rd, err := dnsv2.NewData(dnsv2.TypeSVCB, fmt.Sprintf("%d %s %s", rc.SvcPriority, rc.GetTargetField(), rc.SvcParams), origin) + if err != nil { + panic(fmt.Sprintf("BUG: Failed to create RDATA for HTTPS record: %v", err)) + } + rc.RDATA = rd + } + rc.FixUp(".") + return nil } -func convertSVCBv1v2(params []dnsv1.SVCBKeyValue) ([]svcbv2.Pair, error) { - var value []svcbv2.Pair - for _, kvV1 := range params { - kV1 := kvV1.Key().String() - keyCodeV2 := svcbv2.StringToKey(kV1) - vV1 := kvV1.String() - if len(vV1) > 2 && vV1[0] == '"' && vV1[len(vV1)-1] == '"' { - panic("V has quotes") +// func convertSVCBv1v2(params []dnsv1.SVCBKeyValue) ([]svcbv2.Pair, error) { +// var value []svcbv2.Pair +// for _, kvV1 := range params { +// kV1 := kvV1.Key().String() +// keyCodeV2 := svcbv2.StringToKey(kV1) +// vV1 := kvV1.String() +// if len(vV1) > 2 && vV1[0] == '"' && vV1[len(vV1)-1] == '"' { +// panic("V has quotes") +// } +// fmt.Printf("DEBUG: convertSVCBv1v2: k=%s keyCode=%d v1=%s\n", kV1, keyCodeV2, vV1) + +// pairFn := svcbv2.KeyToPair(keyCodeV2) +// if pairFn == nil { +// return nil, fmt.Errorf("failed to lookup svc key: %s", kV1) +// } +// pair := pairFn() +// if svcbv2.PairToKey(pair) != keyCodeV2 { +// return nil, fmt.Errorf("key constant is not in sync: %v", keyCodeV2) +// } +// err := svcbv2.Parse(pair, vV1, "") +// if err != nil { +// return nil, fmt.Errorf("failed to parse svc pair: %s", kV1) +// } + +// vV2 := pair.String() +// if len(vV2) > 2 && vV2[0] == '"' && vV2[len(vV2)-1] == '"' { +// panic("V2 has quotes") +// } +// if vV1 != vV2 { +// panic(fmt.Sprintf("conversion from v1 to v2 is not stable: key=%s v1=%s v2=%s", kV1, vV1, vV2)) +// } + +// value = append(value, pair) +// } + +// return value, nil +// } + +func ModifySVCBForComparison(existing, desired Records) Records { + + // Clone the "desired" list. Its an array of pointers, so we clone the pointers. We can replace any record we want in the "desired" list without mutating the original. + newDesired := make(Records, len(desired)) + copy(newDesired, desired) + + // Build the list of existing ECH values. + echs := gatherEchValues(existing) + + // Scan desired for ech=IGNORE. Replace any records. + recs, edits := replaceSvcbIgnores(newDesired, echs) + + if edits { + return recs + } + // No changes were made, so we can return the original "desired" list to save memory. + return desired +} + +// gatherEchValues builds a map of FQDN to ECH values for all SVCB and HTTPS +// records in the given set of records. This is used to support the +// "ech=IGNORE" feature, where we want to ignore changes in the ECH value when +// comparing records, but still show the ECH value in the output for debugging +// purposes. +func gatherEchValues(recs Records) map[string]*svcbv2.ECHCONFIG { + echs := map[string]*svcbv2.ECHCONFIG{} + for _, rec := range recs { + if rec.TypeNum == dnsv2.TypeSVCB || rec.TypeNum == dnsv2.TypeHTTPS { + if value, ok := rec.GetSVCBEchConfig(); ok { + echs[rec.NameFQDN] = value + } } - fmt.Printf("DEBUG: convertSVCBv1v2: k=%s keyCode=%d v1=%s\n", kV1, keyCodeV2, vV1) + } + return echs +} - pairFn := svcbv2.KeyToPair(keyCodeV2) - if pairFn == nil { - return nil, fmt.Errorf("failed to lookup svc key: %s", kV1) +// GetSVCBEchConfig returns the value of the ECH parameter. The value is a pointer to a clone. +func (rc *RecordConfig) GetSVCBEchConfig() (*svcbv2.ECHCONFIG, bool) { + if rc.TypeNum != dnsv2.TypeSVCB && rc.TypeNum != dnsv2.TypeHTTPS { + panic("assertion failed: GetSVCBParam called when .Type is not SVCB or HTTPS") + } + if rc.RDATA == nil { + panic("assertion failed: SVCB/HTTPS record does not have RDATA set") + } + + for _, param := range rc.RDATA.(*dnsrdatav2.SVCB).Value { + key := svcbv2.PairToKey(param) + if key == svcbv2.KeyEchConfig { + p := param.(*svcbv2.ECHCONFIG) + c := p.Clone() + return c.(*svcbv2.ECHCONFIG), true } - pair := pairFn() - if svcbv2.PairToKey(pair) != keyCodeV2 { - return nil, fmt.Errorf("key constant is not in sync: %v", keyCodeV2) + } + return nil, false +} + +func replaceSvcbIgnores(records Records, echs map[string]*svcbv2.ECHCONFIG) (Records, bool) { + edits := false + + for i, rec := range records { + // Skip rtypes we're not concerned with. + if rec.TypeNum != dnsv2.TypeSVCB && rec.TypeNum != dnsv2.TypeHTTPS { + continue } - err := svcbv2.Parse(pair, vV1, "") - if err != nil { - return nil, fmt.Errorf("failed to parse svc pair: %s", kV1) + + // Skip records that don't have ech=IGNORE. + if ec, ok := rec.GetSVCBEchConfig(); ok && !bytes.Equal(ec.ECH, []byte("IGNORE")) { + continue } - vV2 := pair.String() - if len(vV2) > 2 && vV2[0] == '"' && vV2[len(vV2)-1] == '"' { - panic("V2 has quotes") + // Now we know this record has ech=IGNORE. + nRec, err := rec.Copy() + if err != nil { + panic(fmt.Sprintf("failed to copy record: %v", err)) } - if vV1 != vV2 { - panic(fmt.Sprintf("conversion from v1 to v2 is not stable: key=%s v1=%s v2=%s", kV1, vV1, vV2)) + if nEch, ok := echs[rec.NameFQDN]; ok { + // This record has an ECH value, so we replace "ech=IGNORE" with the actual value in the comparables. + nRec.RDATA = SVCBReplaceEch(nRec.RDATA.(*dnsrdatav2.SVCB), nEch) + } else { + // This record doesn't have an ECH value, so we delete "ech=IGNORE" from the comparables. + nRec.RDATA = SVCBDeleteEch(nRec.RDATA.(*dnsrdatav2.SVCB)) } - value = append(value, pair) + // Clear the ComparableV3 so it will be regenerated with the new RDATA. + nRec.ComparableV3 = "" + nRec.FixUp(".") + + // Replace the record. + records[i] = nRec + edits = true } - return value, nil + return records, edits +} + +func SVCBReplaceEch(rr *dnsrdatav2.SVCB, echConfig *svcbv2.ECHCONFIG) *dnsrdatav2.SVCB { + // This is a bit of a hack, but dnsrdatav2.SVCB doesn't have a Clone method. + // We need to clone it to avoid mutating the original. + // We replace "ech=" as we clone it. + return &dnsrdatav2.SVCB{ + Priority: rr.Priority, + Target: rr.Target, + Value: func() []svcb.Pair { + pairs := make([]svcb.Pair, len(rr.Value)) + found := false + for i, p := range rr.Value { + if svcbv2.PairToKey(p) == svcbv2.KeyEchConfig { + pairs[i] = echConfig + found = true + } else { + pairs[i] = p.Clone() + } + } + if !found { + pairs = append(pairs, echConfig) + } + return pairs + }(), + } +} + +func SVCBDeleteEch(rr *dnsrdatav2.SVCB) *dnsrdatav2.SVCB { + // This is a bit of a hack, but dnsrdatav2.SVCB doesn't have a Clone method. + // We need to clone it to avoid mutating the original. + // We deelte "ech=" as we clone it. + return &dnsrdatav2.SVCB{ + Priority: rr.Priority, + Target: rr.Target, + Value: func() []svcb.Pair { + pairs := make([]svcb.Pair, len(rr.Value)) + for i, p := range rr.Value { + if svcbv2.PairToKey(p) != svcbv2.KeyEchConfig { + pairs[i] = p.Clone() + } + } + return pairs + }(), + } } diff --git a/models/target.go b/models/target.go index 2fc579f68e..8660f18e4f 100644 --- a/models/target.go +++ b/models/target.go @@ -90,10 +90,8 @@ func (rc *RecordConfig) zoneFileQuoted() string { rc.MustSetTarget(".") } - if rc.Type == "HTTPS" || rc.Type == "SVCB" { - if rc.RDATA == nil { - panic("drat") - } + if rc.RDATA != nil { + return rc.RDATA.String() } rr := rc.ToRR() diff --git a/pkg/diff2/analyze.go b/pkg/diff2/analyze.go index 93fd4ee1f7..e413c4e22a 100644 --- a/pkg/diff2/analyze.go +++ b/pkg/diff2/analyze.go @@ -2,7 +2,6 @@ package diff2 import ( "fmt" - "regexp" "sort" "strings" @@ -243,8 +242,6 @@ func humanDiff(a, b targetConfig) string { a.comparableNoTTL) } -var echRe = regexp.MustCompile(`ech="?([\w+/=]+)"?`) - // diffTargets is the real workhorse of the diff2 system. All the setup has been complete, // now we can find the differences between two zones. func diffTargets(existing, desired []targetConfig) ChangeList { @@ -255,30 +252,6 @@ func diffTargets(existing, desired []targetConfig) ChangeList { return nil } - echs := make(map[string]string) - for _, v := range existing { - matches := echRe.FindStringSubmatch(v.rec.SvcParams) - if len(matches) == 2 { - echs[v.rec.NameFQDN] = matches[1] - } - } - for i, v := range desired { - if strings.Contains(v.rec.SvcParams, "ech=IGNORE") { - var unquoted, quoted string - if _, ok := echs[v.rec.NameFQDN]; ok { - unquoted = fmt.Sprintf("ech=%s", echs[v.rec.NameFQDN]) - quoted = fmt.Sprintf("ech=%s", echs[v.rec.NameFQDN]) - } else { - unquoted = "" - quoted = "" - } - v.rec.SvcParams = echRe.ReplaceAllString(v.rec.SvcParams, unquoted) - v.comparableFull = echRe.ReplaceAllString(v.comparableFull, quoted) - v.comparableNoTTL = echRe.ReplaceAllString(v.comparableNoTTL, quoted) - } - desired[i] = v - } - var instructions ChangeList // remove the exact matches. diff --git a/pkg/diff2/compareconfig.go b/pkg/diff2/compareconfig.go index 92729ca4cb..023d965dc8 100644 --- a/pkg/diff2/compareconfig.go +++ b/pkg/diff2/compareconfig.go @@ -103,12 +103,18 @@ func NewCompareConfig(origin string, existing, desired models.Records, compFn Co labelMap: map[string]bool{}, keyMap: map[models.RecordKey]bool{}, } + cc.addRecords(existing, true) // Must be called first so that CNAME manipulations happen in the correct order. + + desired = models.ModifySVCBForComparison(existing, desired) cc.addRecords(desired, false) + cc.verifyCNAMEAssertions() + sort.Slice(cc.ldata, func(i, j int) bool { return prettyzone.LabelLess(cc.ldata[i].label, cc.ldata[j].label) }) + return cc } diff --git a/pkg/rtype/ds.go b/pkg/rtype/ds.go index d3b81096b4..9d7abae397 100644 --- a/pkg/rtype/ds.go +++ b/pkg/rtype/ds.go @@ -16,7 +16,7 @@ func init() { // DS RR. type DS struct { - dnsv1.DS + dnsrdatav2.DS } // Name returns the DNS record type as a string. @@ -45,11 +45,12 @@ func (handle *DS) FromArgs(dcn *domaintags.DomainNameVarieties, rec *models.Reco // FromStruct fills in the RecordConfig from a struct, typically from an API response. func (handle *DS) FromStruct(dcn *domaintags.DomainNameVarieties, rec *models.RecordConfig, name string, fields any) error { // Fields is of type "any" thus we must validate the type. It should be the "inner" type of .F, not the outer type, rtype.DS{}. - ds, ok := fields.(*dnsv1.DS) + ds, ok := fields.(dnsrdatav2.DS) if !ok { - return fmt.Errorf("fields is not *dns.DS, got %T", fields) + panic(fmt.Sprintf("assertion failed: fields should be *dnsrdatav2.DS, got %T", fields)) + //return fmt.Errorf("fields is not *dns.DS, got %T", fields) } - rec.F = &DS{*ds} + rec.F = &DS{ds} // Hack to deal with the fact that fixlegacy.go can't import rtype. switch rec.F.(type) { @@ -79,7 +80,7 @@ func (handle *DS) CopyToLegacyFields(rec *models.RecordConfig) { func (handle *DS) CopyFromLegacyFields(rec *models.RecordConfig) { // Copy fields: rec.F = &DS{ - dnsv1.DS{ + dnsrdatav2.DS{ KeyTag: rec.DsKeyTag, Algorithm: rec.DsAlgorithm, DigestType: rec.DsDigestType, diff --git a/pkg/rtypecontrol/import.go b/pkg/rtypecontrol/import.go index 9c678514c7..b49650531d 100644 --- a/pkg/rtypecontrol/import.go +++ b/pkg/rtypecontrol/import.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" + dnsv2 "codeberg.org/miekg/dns" "github.com/DNSControl/dnscontrol/v4/models" "github.com/DNSControl/dnscontrol/v4/pkg/domaintags" - dnsv1 "github.com/miekg/dns" ) // ImportRawRecords imports the RawRecordConfigs into RecordConfigs. @@ -146,7 +146,8 @@ func NewRecordConfigFromString(name string, ttl uint32, t string, s string, dcn panic("rtypecontrol: NewRecordConfigFromStruct: empty record type") } - rec, err := dnsv1.NewRR(fmt.Sprintf("$ORIGIN .\n. %d IN %s %s", ttl, t, s)) + //rec, err := dnsv1.NewRR(fmt.Sprintf("$ORIGIN .\n. %d IN %s %s", ttl, t, s)) + rec, err := dnsv2.NewData(dnsv2.TypeDS, s, ".") if err != nil { return nil, err } diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go index fbd4b02d39..5a70e3d791 100644 --- a/providers/bind/bindProvider.go +++ b/providers/bind/bindProvider.go @@ -260,7 +260,7 @@ func ParseZoneContents(content string, zoneName string, zonefileName string) (mo if rtypeinfo.IsModernType(rtypeStr) { // Modern types: name := rr.Header().Name - prec, err = rtypecontrol.NewRecordConfigFromStruct(name, rr.Header().TTL, rtypeStr, rr, domaintags.MakeDomainNameVarieties(zoneName)) + prec, err = rtypecontrol.NewRecordConfigFromStruct(name, rr.Header().TTL, rtypeStr, rr.Data(), domaintags.MakeDomainNameVarieties(zoneName)) if err != nil { return nil, err }