From fbb892059f36dcb50143f1c8465134b88b88559c Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 17 May 2026 19:05:06 -0400 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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"