resolver: lower-case DNS names in record values (closes #157) #160
@@ -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 keep
|
||||||
|
their letter case; they are not lower-cased.
|
||||||
- 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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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 keep
|
||||||
|
// their letter case.
|
||||||
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 ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user