mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-07-29 01:18:48 +02:00
With cache_enable = true, one cache entry was shared by every client
asking the same name against the same upstream: the cache key
({Qtype, Qclass, Name, Upstream}) and the osResolver hot-cache/singleflight
key ("name:qtype:") both ignored the EDNS Client Subnet (ECS). A response
tailored for subnet A was therefore served to subnet B.
A cached answer's records are scoped to the network that generated them
(RFC 7871 §7.3), so sharing them across subnets returns the wrong
CDN/policy answer. Rewriting only the ECS option on the shared answer is
worse: forwarders that validate the echoed ECS (e.g. dnsmasq with
add-subnet) then accept the wrong-subnet answer instead of rejecting it as
a mismatch.
Partition both cache paths by a canonical ECS tuple (family, source-prefix,
masked address) via the new dnscache.CanonicalECS: the LRU key gains an ECS
field and the singleflight/hot-cache key appends the canonical ECS. Same
subnet still shares an entry; different subnets (or address families) never
do. Only a request with no ECS option collapses to the shared empty
partition; a carried /0 keeps its own family-scoped token, since it is
forwarded with an ECS option and must stay distinguishable from a no-ECS
query (RFC 7871 §7.3.1). SetCacheReply no longer touches ECS and only
reconciles the EDNS Cookie.
Adds real cache-path regression tests (LRU and osResolver hot cache) that
serve a different A record per subnet and verify the second subnet never
receives the first's record.
Fixes https://github.com/Control-D-Inc/ctrld/issues/324
58 lines
1.8 KiB
Go
58 lines
1.8 KiB
Go
package ctrld
|
|
|
|
import (
|
|
"net"
|
|
"testing"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// Test_SetCacheReply_DoesNotRewriteECS documents the post-#564 contract: cross-client
|
|
// correctness is guaranteed by partitioning the cache/singleflight keys by ECS
|
|
// (dnscache.CanonicalECS), NOT by rewriting the cached answer's ECS option. Rewriting the
|
|
// ECS metadata while leaving the Answer records scoped to another subnet would violate
|
|
// RFC 7871 §7.3 and make forwarders accept a wrong-subnet answer. SetCacheReply must
|
|
// therefore leave the answer's ECS untouched.
|
|
func Test_SetCacheReply_DoesNotRewriteECS(t *testing.T) {
|
|
answer := new(dns.Msg)
|
|
answer.SetQuestion(dns.Fqdn("controld.com"), dns.TypeA)
|
|
answer.SetEdns0(4096, false)
|
|
cachedSubnet := &dns.EDNS0_SUBNET{
|
|
Code: dns.EDNS0SUBNET,
|
|
Family: 2,
|
|
SourceNetmask: 64,
|
|
SourceScope: 64,
|
|
Address: net.ParseIP("2001:db8:1::"),
|
|
}
|
|
answer.IsEdns0().Option = append(answer.IsEdns0().Option, cachedSubnet)
|
|
|
|
req := new(dns.Msg)
|
|
req.SetQuestion(dns.Fqdn("controld.com"), dns.TypeA)
|
|
req.SetEdns0(4096, true)
|
|
req.IsEdns0().Option = append(req.IsEdns0().Option, &dns.EDNS0_SUBNET{
|
|
Code: dns.EDNS0SUBNET,
|
|
Family: 2,
|
|
SourceNetmask: 64,
|
|
Address: net.ParseIP("2001:db8:2::"),
|
|
})
|
|
|
|
SetCacheReply(answer, req, dns.RcodeSuccess)
|
|
|
|
var got *dns.EDNS0_SUBNET
|
|
for _, o := range answer.IsEdns0().Option {
|
|
if e, ok := o.(*dns.EDNS0_SUBNET); ok {
|
|
got = e
|
|
break
|
|
}
|
|
}
|
|
if got == nil {
|
|
t.Fatal("SetCacheReply dropped the answer's ECS option")
|
|
}
|
|
if want := net.ParseIP("2001:db8:1::"); !got.Address.Equal(want) {
|
|
t.Fatalf("SetCacheReply rewrote the answer ECS to the requester's subnet: got %v, want %v (unchanged)", got.Address, want)
|
|
}
|
|
if got.SourceScope != 64 {
|
|
t.Fatalf("SetCacheReply altered the answer ECS scope: got %d, want 64 (unchanged)", got.SourceScope)
|
|
}
|
|
}
|