Compare commits

...
1 Commits
Author SHA1 Message Date
clawbot 354db6b524 resolver: lower-case DNS names in record values (closes #157)
check / check (push) Successful in 1m1s
Nameservers may answer with DNS names in any letter case. For eeqj.de,
y.ns.joker.com answers in upper case while its peers answer in lower
case, so the inconsistency check fired on every cycle. extractRecordValue
now lower-cases CNAME, MX, SRV and NS targets, so the inconsistency check
and the record-change check both compare names regardless of case. A,
AAAA, TXT and CAA values are unchanged. State saved before this change
can hold upper-case names, which report a one-time record change on the
first check after upgrading.

Model: opus-5-5
2026-09-28 21:16:07 +00:00
5 changed files with 85 additions and 5 deletions
+4
View File
@@ -61,6 +61,10 @@ rejected.
record types: A, AAAA, CNAME, MX, TXT, SRV, CAA, NS. record types: A, AAAA, CNAME, MX, TXT, SRV, CAA, NS.
- Stores results **per nameserver**. The state for a hostname is not a - Stores results **per nameserver**. The state for a hostname is not a
merged view — it is a map from nameserver to record set. merged view — it is a map from nameserver to record set.
- DNS names inside record values (CNAME, MX, SRV and NS targets) are
stored in lower case, because names are case-insensitive and
nameservers may answer in any letter case. TXT and CAA values are
stored exactly as received.
- Any observable change in any nameserver's response triggers a - Any observable change in any nameserver's response triggers a
notification. This includes: notification. This includes:
- **Record change**: A nameserver returns different records than it - **Record change**: A nameserver returns different records than it
+3
View File
@@ -23,6 +23,9 @@ Rationale, Design, TODO, License, Author) if any are still missing.
# Completed Steps # Completed Steps
- 2026-09-28: DNS names in record values (CNAME, MX, SRV and NS targets) are
lower-cased, so nameservers that answer in different letter case no longer
count as inconsistent or as a record change (closes #157).
- 2026-09-28: `script/cibuild` and `script/docker` now pass - 2026-09-28: `script/cibuild` and `script/docker` now pass
`--no-cache-filter=lint,builder` so lint and tests run every build (closes `--no-cache-filter=lint,builder` so lint and tests run every build (closes
#115). #115).
+8
View File
@@ -0,0 +1,8 @@
package resolver
import "github.com/miekg/dns"
// ExtractRecordValue exports extractRecordValue for testing.
func ExtractRecordValue(rr dns.RR) string {
return extractRecordValue(rr)
}
+8 -5
View File
@@ -608,7 +608,10 @@ func classifyResponse(resp *NameserverResponse, state queryState) {
} }
} }
// extractRecordValue formats a DNS RR value as a string. // extractRecordValue formats a DNS RR value as a string. DNS names
// are case-insensitive and nameservers may answer in any letter case,
// so names are lower-cased to compare equal. TXT and CAA values are
// kept exactly as received.
func extractRecordValue(rr dns.RR) string { func extractRecordValue(rr dns.RR) string {
switch r := rr.(type) { switch r := rr.(type) {
case *dns.A: case *dns.A:
@@ -616,22 +619,22 @@ func extractRecordValue(rr dns.RR) string {
case *dns.AAAA: case *dns.AAAA:
return r.AAAA.String() return r.AAAA.String()
case *dns.CNAME: case *dns.CNAME:
return r.Target return strings.ToLower(r.Target)
case *dns.MX: case *dns.MX:
return fmt.Sprintf("%d %s", r.Preference, r.Mx) return fmt.Sprintf("%d %s", r.Preference, strings.ToLower(r.Mx))
case *dns.TXT: case *dns.TXT:
return strings.Join(r.Txt, "") return strings.Join(r.Txt, "")
case *dns.SRV: case *dns.SRV:
return fmt.Sprintf( return fmt.Sprintf(
"%d %d %d %s", "%d %d %d %s",
r.Priority, r.Weight, r.Port, r.Target, r.Priority, r.Weight, r.Port, strings.ToLower(r.Target),
) )
case *dns.CAA: case *dns.CAA:
return fmt.Sprintf( return fmt.Sprintf(
"%d %s \"%s\"", r.Flag, r.Tag, r.Value, "%d %s \"%s\"", r.Flag, r.Tag, r.Value,
) )
case *dns.NS: case *dns.NS:
return r.Ns return strings.ToLower(r.Ns)
default: default:
return "" return ""
} }
+62
View File
@@ -0,0 +1,62 @@
package resolver_test
import (
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"sneak.berlin/go/dnswatcher/internal/resolver"
)
func TestExtractRecordValue_LetterCase(t *testing.T) {
t.Parallel()
tests := []struct {
name string
rr dns.RR
want string
}{
{
name: "MX target lower-cased",
rr: &dns.MX{Preference: 1, Mx: "ASPMX.L.GOOGLE.COM."},
want: "1 aspmx.l.google.com.",
},
{
name: "NS target lower-cased",
rr: &dns.NS{Ns: "x.ns.joker.COM."},
want: "x.ns.joker.com.",
},
{
name: "CNAME target lower-cased",
rr: &dns.CNAME{Target: "WWW.Example.Com."},
want: "www.example.com.",
},
{
name: "SRV target lower-cased",
rr: &dns.SRV{
Priority: 10, Weight: 5, Port: 443,
Target: "SIP.Example.Com.",
},
want: "10 5 443 sip.example.com.",
},
{
name: "TXT value keeps its case",
rr: &dns.TXT{Txt: []string{"Verify=AbC123"}},
want: "Verify=AbC123",
},
{
name: "CAA value keeps its case",
rr: &dns.CAA{Flag: 0, Tag: "issue", Value: "LetsEncrypt.org"},
want: `0 issue "LetsEncrypt.org"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.want, resolver.ExtractRecordValue(tt.rr))
})
}
}