Files
ctrld/cmd/cli/dns64_dnssec_test.go
T
Dev Scribe c7b3881e13 Port DNS64, VPN routing and DNS recovery ownership to master
Retain master's context-aware VPN and resolver APIs, per-program logger,
shutdown fencing, native CLAT service identity and firewall behavior.
Keep saved-DNS paths owned by the root ctrld package; isolate the added
ownership/sweep tests with a shared path seam and temporary backups.

Preserve the source DNS64 DNSSEC/cache gates, longest-suffix VPN routing,
DNS-journal convergence, scoped IPv6 handling, target ownership checks,
custom-listener regressions and documented native QA limits.
2026-09-28 14:44:47 +07:00

144 lines
5.2 KiB
Go

package cli
import (
"context"
"fmt"
"net"
"testing"
"time"
"github.com/miekg/dns"
ctrld "github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/internal/dnscache"
)
func TestDNS64DNSSECCacheAndSynthesis(t *testing.T) {
captureDebugMainLog(t)
for _, do := range []bool{false, true} {
for _, denialAD := range []bool{false, true} {
for _, aAD := range []bool{false, true} {
t.Run(fmt.Sprintf("DO=%v/denialAD=%v/A_AD=%v", do, denialAD, aAD), func(t *testing.T) {
req := mkAAAAReq("dnssec.example.")
req.SetEdns0(1232, do)
denial := new(dns.Msg)
denial.SetReply(req)
denial.AuthenticatedData = denialAD
cache, err := dnscache.NewLRUCache(10)
if err != nil {
t.Fatal(err)
}
p := &prog{cache: cache}
p.logger.Store(mainLog.Load())
p.dns64.active = true
p.dns64.prefix = dns64WellKnownPrefix
p.dns64.checkedAt = time.Now()
calls := 0
resolveA := func(q *dns.Msg) *dns.Msg {
calls++
if q.Question[0].Qtype != dns.TypeA {
t.Fatal("not an A lookup")
}
a := new(dns.Msg)
a.SetReply(q)
a.AuthenticatedData = aAD
a.Answer = []dns.RR{&dns.A{Hdr: dns.RR_Header{Name: q.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, A: net.ParseIP("198.51.100.7")}}
return a
}
out, prefix := p.maybeDNS64(context.Background(), req, denial, resolveA)
wantAD := do && denialAD && aAD
if calls != 1 || !answerHasAAAA(out) || !prefix.IsValid() || out.AuthenticatedData != wantAD {
t.Fatalf("cold: calls=%d AAAA=%v prefix=%v AD=%v wantAD=%v", calls, answerHasAAAA(out), prefix, out.AuthenticatedData, wantAD)
}
now := time.Now()
for _, stale := range []bool{false, true} {
expiry := now.Add(time.Minute)
if stale {
expiry = now.Add(-time.Minute)
}
cache.Add(dns64CacheKey(req, "upstream.0", prefix), dnscache.NewValue(out, expiry))
answer, old, hit, variant, _ := p.cachedResponse(req, "upstream.0", prefix, true, now)
got := answer
if stale {
got = old
}
if got == nil || !answerHasAAAA(got) || got.AuthenticatedData != wantAD || hit == stale || variant == stale {
t.Fatalf("normal cache stale=%v: answer=%v old=%v hit=%v variant=%v", stale, answer, old, hit, variant)
}
cd := req.Copy()
cd.CheckingDisabled = true
answer, old, hit, variant, _ = p.cachedResponse(cd, "upstream.0", prefix, true, now)
if answerHasAAAA(answer) || answerHasAAAA(old) || hit || variant {
t.Errorf("CD cache stale=%v leaked synthesis: answer=%v old=%v hit=%v variant=%v", stale, answer, old, hit, variant)
}
cold, coldPrefix := p.maybeDNS64(context.Background(), cd, denial, resolveA)
if cold != denial || coldPrefix.IsValid() || calls != 1 {
t.Fatal("CD cold path performed synthesis")
}
}
if denial.AuthenticatedData != denialAD {
t.Fatal("mutated denial")
}
})
}
}
}
}
func TestDNS64DNSSECOrdinaryCacheFallback(t *testing.T) {
req := mkAAAAReq("dnssec.example.")
req.CheckingDisabled = true
req.SetEdns0(1232, true)
ordinary := new(dns.Msg)
ordinary.SetReply(req)
cache, err := dnscache.NewLRUCache(10)
if err != nil {
t.Fatal(err)
}
p := &prog{cache: cache}
p.logger.Store(mainLog.Load())
now := time.Now()
cache.Add(dnscache.NewKey(req, "upstream.0"), dnscache.NewValue(ordinary, now.Add(time.Minute)))
answer, _, hit, variant, bypass := p.cachedResponse(req, "upstream.0", dns64WellKnownPrefix, true, now)
if !hit || variant || bypass || answerHasAAAA(answer) {
t.Fatalf("ordinary cache: hit=%v variant=%v bypass=%v answer=%v", hit, variant, bypass, answer)
}
}
func TestRecoveryOSOnlyCandidatePool(t *testing.T) {
for _, reason := range []RecoveryReason{RecoveryReasonNetworkChange, RecoveryReasonRegularFailure} {
p := &prog{cfg: &ctrld.Config{Upstream: map[string]*ctrld.UpstreamConfig{"0": {Type: ctrld.ResolverTypeOS}, "nil": nil}}}
p.logger.Store(mainLog.Load())
pool := p.buildRecoveryUpstreams(reason)
if len(pool) != 1 || pool[upstreamOS] != osUpstreamConfig {
t.Errorf("reason=%v: OS-only recovery candidates=%v", reason, pool)
}
}
}
func TestDNS64DNSSECCacheDOIsolation(t *testing.T) {
for _, do := range []bool{false, true} {
req := mkAAAAReq("dnssec.example.")
req.SetEdns0(1232, do)
response := new(dns.Msg)
response.SetReply(req)
response.AuthenticatedData = do
response.Answer = []dns.RR{&dns.AAAA{Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 60}, AAAA: net.ParseIP("64:ff9b::c633:6407")}}
cache, err := dnscache.NewLRUCache(10)
if err != nil {
t.Fatal(err)
}
p := &prog{cache: cache}
p.logger.Store(mainLog.Load())
now := time.Now()
other := req.Copy()
other.IsEdns0().SetDo(!do)
for _, expiry := range []time.Time{now.Add(time.Minute), now.Add(-time.Minute)} {
cache.Add(dns64CacheKey(req, "upstream.0", dns64WellKnownPrefix), dnscache.NewValue(response, expiry))
answer, stale, hit, variant, _ := p.cachedResponse(other, "upstream.0", dns64WellKnownPrefix, true, now)
if answer != nil || stale != nil || hit || variant {
t.Fatalf("DO=%v result reused for DO=%v: answer=%v stale=%v", do, !do, answer, stale)
}
}
}
}