From 3ea7ea84c975bff31fd9415de379a1255cb18c91 Mon Sep 17 00:00:00 2001 From: Max Chernoff Date: Wed, 11 Jun 2025 09:16:15 -0600 Subject: [PATCH] FEATURE: Support ignoring the `ech=` parameter in `HTTPS`/`SVCB` RR types (#3485) --- commands/types/dnscontrol.d.ts | 2 ++ .../domain-modifiers/HTTPS.md | 2 ++ integrationTest/integration_test.go | 12 +++++++++ models/record.go | 4 +++ pkg/diff2/analyze.go | 27 +++++++++++++++++++ 5 files changed, 47 insertions(+) diff --git a/commands/types/dnscontrol.d.ts b/commands/types/dnscontrol.d.ts index d77231fe9..2477a5c76 100644 --- a/commands/types/dnscontrol.d.ts +++ b/commands/types/dnscontrol.d.ts @@ -1181,6 +1181,8 @@ declare function HASH(algorithm: "SHA1" | "SHA256" | "SHA512", value: string): s * * Modifiers can be any number of [record modifiers](https://docs.dnscontrol.org/language-reference/record-modifiers) or JSON objects, which will be merged into the record's metadata. * + * If you set the parameter `ech` to the special value `IGNORE`, DNSControl will ignore the contents of that parameter when updating a zone. + * * ```javascript * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER), * HTTPS("@", 1, ".", "ipv4hint=123.123.123.123 alpn=h3,h2 port=443"), diff --git a/documentation/language-reference/domain-modifiers/HTTPS.md b/documentation/language-reference/domain-modifiers/HTTPS.md index 12d98ec95..345e19ee5 100644 --- a/documentation/language-reference/domain-modifiers/HTTPS.md +++ b/documentation/language-reference/domain-modifiers/HTTPS.md @@ -22,6 +22,8 @@ The params may be configured to specify the `alpn`, `ipv4hint`, `ipv6hint`, `ech Modifiers can be any number of [record modifiers](https://docs.dnscontrol.org/language-reference/record-modifiers) or JSON objects, which will be merged into the record's metadata. +If you set the parameter `ech` to the special value `IGNORE`, DNSControl will ignore the contents of that parameter when updating a zone. + {% code title="dnsconfig.js" %} ```javascript D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER), diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 384ddf6f6..ee61a1c95 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -288,6 +288,18 @@ func makeTests() []*TestGroup { tc("Change HTTPS all", https("@", 3, "example.com.", "port=100")), ), + testgroup("Ech", + requires(providers.CanUseHTTPS), + 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///")), + tc("Ignore the ECH key while changing other values", https("@", 1, "example.net.", "port=80 ech=IGNORE")), + // tc("Should be a no-op", https("@", 1, "example.net.", "port=80 ech=some+base64+encoded+value///")), + tc("Change the ECH key and other values", https("@", 1, "example.org.", "port=80 ipv4hint=127.0.0.1 ech=another+base64+encoded+value")), + // tc("Ignore the ECH key while not changing anything", https("@", 1, "example.org.", "port=80 ipv4hint=127.0.0.1 ech=IGNORE")), + // tc("Should be a no-op", https("@", 1, "example.org.", "port=80 ipv4hint=127.0.0.1 ech=another+base64+encoded+value")), + tc("Another domain with a different ECH value", https("ech", 1, "example.com.", "ech=some+base64+encoded+value///")), + ), + testgroup("SVCB", requires(providers.CanUseSVCB), tc("Create a SVCB record", svcb("@", 1, "test.com.", "port=80")), diff --git a/models/record.go b/models/record.go index b19cc184d..e9a9fdb30 100644 --- a/models/record.go +++ b/models/record.go @@ -529,6 +529,10 @@ func (rc *RecordConfig) Key() RecordKey { // GetSVCBValue returns the SVCB Key/Values as a list of Key/Values. func (rc *RecordConfig) GetSVCBValue() []dns.SVCBKeyValue { + if !strings.Contains(rc.SvcParams, "IGNORE+DNSCONTROL") { + rc.SvcParams = strings.ReplaceAll(rc.SvcParams, "ech=IGNORE", "ech=IGNORE+DNSCONTROL+++") + } + record, err := dns.NewRR(fmt.Sprintf("%s %s %d %s %s", rc.NameFQDN, rc.Type, rc.SvcPriority, rc.target, rc.SvcParams)) if err != nil { log.Fatalf("could not parse SVCB record: %s", err) diff --git a/pkg/diff2/analyze.go b/pkg/diff2/analyze.go index 2904e91dd..b2a1961a9 100644 --- a/pkg/diff2/analyze.go +++ b/pkg/diff2/analyze.go @@ -2,6 +2,7 @@ package diff2 import ( "fmt" + "regexp" "sort" "strings" @@ -247,6 +248,8 @@ func humanDiff(a, b targetConfig) string { return fmt.Sprintf("%s ttl=(%d->%d)", a.comparableNoTTL, a.rec.TTL, b.rec.TTL) } +var echRe = regexp.MustCompile(`ech="?([\w+/=]+)"?`) + func diffTargets(existing, desired []targetConfig) ChangeList { // fmt.Printf("DEBUG: diffTargets(\nexisting=%v\ndesired=%v\nDEBUG.\n", existing, desired) @@ -255,6 +258,30 @@ 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=%q", 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.