mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
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.
144 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|