Files
sower/router/router_test.go
wweir 30e80ab510 fix(router): bound degrade retry to snapshot, gate internal PTR on selected upstream
- degradeUpstream now takes the caller's snapshot length: a concurrent
  refresh that grows the live upstream list no longer lets the retry
  index past the stale snapshot and panic the DNS handler.
- Internal reverse lookups take a dedicated forward path that never runs
  the degrade state machine: the query goes only to the selected internal
  upstream and surfaces SERVFAIL on failure instead of retrying onto a
  public fallback, closing the TOCTOU leak in the PTR gate.
- Local DNS replies set the RecursionAvailable bit per RFC 1035.
- A failed TCP retry for a truncated UDP response keeps the truncated
  response (with TC set) instead of dropping it for SERVFAIL, so
  networks that block TCP/53 still get usable answers.
2026-08-12 17:08:32 +08:00

1288 lines
38 KiB
Go

package router
import (
"context"
"errors"
"net"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/miekg/dns"
)
func newTestRouter(t *testing.T, serveIPs []string, upstreamDNS, fallbackDNS, mmdbFile string, proxyDial ProxyDialFn) *Router {
t.Helper()
r, err := NewRouter(serveIPs, upstreamDNS, fallbackDNS, mmdbFile, proxyDial)
if err != nil {
t.Fatalf("new router: %v", err)
}
return r
}
func TestNewRouterInitializesRules(t *testing.T) {
t.Parallel()
r := newTestRouter(t, nil, "", "223.5.5.5", "", nil)
if r.BlockRule == nil || r.DirectRule == nil || r.ProxyRule == nil {
t.Fatal("expected router rules to be initialized")
}
}
func TestDNSProxyReplyMatchesQuestionType(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1", "::1"}, "", "223.5.5.5", "", nil)
reqA := new(dns.Msg)
reqA.SetQuestion("example.com.", dns.TypeA)
respA, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}, reqA)
if err != nil {
t.Fatalf("dnsProxyReply A failed: %v", err)
}
if len(respA.Answer) != 1 {
t.Fatalf("unexpected A answer count: %d", len(respA.Answer))
}
if _, ok := respA.Answer[0].(*dns.A); !ok {
t.Fatalf("expected A record, got %T", respA.Answer[0])
}
reqAAAA := new(dns.Msg)
reqAAAA.SetQuestion("example.com.", dns.TypeAAAA)
respAAAA, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("::1"), Port: 53}, reqAAAA)
if err != nil {
t.Fatalf("dnsProxyReply AAAA failed: %v", err)
}
if len(respAAAA.Answer) != 1 {
t.Fatalf("unexpected AAAA answer count: %d", len(respAAAA.Answer))
}
if _, ok := respAAAA.Answer[0].(*dns.AAAA); !ok {
t.Fatalf("expected AAAA record, got %T", respAAAA.Answer[0])
}
}
func TestDNSProxyReplyPrefersRequestLocalAddr(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1", "127.0.0.2"}, "", "223.5.5.5", "", nil)
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
resp, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}, req)
if err != nil {
t.Fatalf("dnsProxyReply failed: %v", err)
}
answer, ok := resp.Answer[0].(*dns.A)
if !ok {
t.Fatalf("expected A record, got %T", resp.Answer[0])
}
if !answer.A.Equal(net.ParseIP("127.0.0.2")) {
t.Fatalf("expected request local IP 127.0.0.2, got %s", answer.A)
}
}
func TestDNSProxyReplyIgnoresUnspecifiedLocalAddr(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.2", "0.0.0.0"}, "", "223.5.5.5", "", nil)
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
resp, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("0.0.0.0"), Port: 53}, req)
if err != nil {
t.Fatalf("dnsProxyReply failed: %v", err)
}
answer, ok := resp.Answer[0].(*dns.A)
if !ok {
t.Fatalf("expected A record, got %T", resp.Answer[0])
}
if !answer.A.Equal(net.ParseIP("127.0.0.2")) {
t.Fatalf("expected configured serve IP 127.0.0.2, got %s", answer.A)
}
}
func TestDNSProxyReplyFallsBackWhenLocalAddrMissing(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
resp, err := r.dnsProxyReply("example.com.", nil, req)
if err != nil {
t.Fatalf("dnsProxyReply failed: %v", err)
}
answer, ok := resp.Answer[0].(*dns.A)
if !ok {
t.Fatalf("expected A record, got %T", resp.Answer[0])
}
if !answer.A.Equal(net.ParseIP("127.0.0.2")) {
t.Fatalf("expected configured serve IP 127.0.0.2, got %s", answer.A)
}
}
func TestServeDNSForUnknownDomainForwardsUpstream(t *testing.T) {
t.Parallel()
var upstreamQueries atomic.Int32
upstreamAddr := startUDPTestDNSServer(t, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
upstreamQueries.Add(1)
resp := new(dns.Msg)
resp.SetReply(req)
resp.Answer = []dns.RR{&dns.A{
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
A: net.ParseIP("198.51.100.10"),
}}
_ = w.WriteMsg(resp)
}))
r := newTestRouter(t, []string{"127.0.0.2"}, "", "", "", nil)
r.dns.upstreamAddrs = []string{upstreamAddr}
r.dns.upstreamIndex = 0
req := new(dns.Msg)
req.SetQuestion("unknown.example.", dns.TypeA)
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
r.ServeDNS(writer, req)
if writer.msg == nil {
t.Fatal("expected response message")
}
if upstreamQueries.Load() != 1 {
t.Fatalf("expected unknown DNS name to be forwarded once, got %d", upstreamQueries.Load())
}
if len(writer.msg.Answer) != 1 {
t.Fatalf("expected one upstream answer, got rcode=%d answers=%d", writer.msg.Rcode, len(writer.msg.Answer))
}
answer, ok := writer.msg.Answer[0].(*dns.A)
if !ok {
t.Fatalf("expected upstream A answer, got %T", writer.msg.Answer[0])
}
if !answer.A.Equal(net.ParseIP("198.51.100.10")) {
t.Fatalf("unexpected upstream answer: %s", answer.A)
}
}
func TestDialSmartFallsBackToProxyForUnknownTCP(t *testing.T) {
t.Parallel()
var gotNetwork, gotHost string
var gotPort uint16
proxyErr := errors.New("proxy called")
r := newTestRouter(t, nil, "", "223.5.5.5", "", func(network, host string, port uint16) (net.Conn, error) {
gotNetwork = network
gotHost = host
gotPort = port
return nil, proxyErr
})
_, err := r.DialSmart("tcp", "203.0.113.10", 12345)
if !errors.Is(err, proxyErr) {
t.Fatalf("expected proxy fallback error, got %v", err)
}
if gotNetwork != "tcp" || gotHost != "203.0.113.10" || gotPort != 12345 {
t.Fatalf("unexpected proxy target: %s %s:%d", gotNetwork, gotHost, gotPort)
}
}
func TestExchangeSkipsServeIPInUpstreamList(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "127.0.0.1", "127.0.0.2", "", nil)
addrs, err := r.buildUpstreamAddrs("127.0.0.1", "127.0.0.2")
if err != nil {
t.Fatalf("buildUpstreamAddrs failed: %v", err)
}
if len(addrs) != 1 {
t.Fatalf("unexpected upstream list: %v", addrs)
}
if addrs[0] != "127.0.0.2:53" {
t.Fatalf("expected fallback only, got %v", addrs)
}
}
func TestBuildUpstreamAddrsPrefersConfiguredDNSBeforeFallback(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
addrs, err := r.buildUpstreamAddrs("1.1.1.1", "9.9.9.9")
if err != nil {
t.Fatalf("buildUpstreamAddrs failed: %v", err)
}
want := []string{"1.1.1.1:53", "9.9.9.9:53"}
if strings.Join(addrs, ",") != strings.Join(want, ",") {
t.Fatalf("unexpected upstream order: got %v want %v", addrs, want)
}
}
func TestExchangeFallsBackOnRetryableDNSResponse(t *testing.T) {
t.Parallel()
primaryAddr := startUDPTestDNSServer(t, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
resp := new(dns.Msg)
resp.SetRcode(req, dns.RcodeServerFailure)
_ = w.WriteMsg(resp)
}))
fallbackAddr := startUDPTestDNSServer(t, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
resp := new(dns.Msg)
resp.SetReply(req)
resp.Answer = []dns.RR{&dns.A{
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
A: net.ParseIP("203.0.113.10"),
}}
_ = w.WriteMsg(resp)
}))
r := newTestRouter(t, nil, "", "", "", nil)
r.dns.upstreamAddrs = []string{primaryAddr, fallbackAddr}
r.dns.upstreamIndex = 0
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
resp, err := r.Exchange(req)
if err != nil {
t.Fatalf("exchange failed: %v", err)
}
if r.dns.upstreamIndex != 1 {
t.Fatalf("expected degraded upstream index 1, got %d", r.dns.upstreamIndex)
}
answer, ok := resp.Answer[0].(*dns.A)
if !ok {
t.Fatalf("expected fallback A answer, got %T", resp.Answer[0])
}
if !answer.A.Equal(net.ParseIP("203.0.113.10")) {
t.Fatalf("unexpected fallback answer: %s", answer.A)
}
}
func TestExchangeDoesNotFallbackOnTerminalDNSResponse(t *testing.T) {
t.Parallel()
var fallbackQueries atomic.Int32
primaryAddr := startUDPTestDNSServer(t, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
resp := new(dns.Msg)
resp.SetRcode(req, dns.RcodeNameError)
_ = w.WriteMsg(resp)
}))
fallbackAddr := startUDPTestDNSServer(t, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
fallbackQueries.Add(1)
resp := new(dns.Msg)
resp.SetReply(req)
_ = w.WriteMsg(resp)
}))
r := newTestRouter(t, nil, "", "", "", nil)
r.dns.upstreamAddrs = []string{primaryAddr, fallbackAddr}
r.dns.upstreamIndex = 0
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
resp, err := r.Exchange(req)
if err != nil {
t.Fatalf("exchange failed: %v", err)
}
if resp.Rcode != dns.RcodeNameError {
t.Fatalf("expected NXDOMAIN response, got rcode %d", resp.Rcode)
}
if r.dns.upstreamIndex != 0 {
t.Fatalf("expected preferred upstream to remain selected, got %d", r.dns.upstreamIndex)
}
if fallbackQueries.Load() != 0 {
t.Fatalf("expected fallback to remain unused, got %d queries", fallbackQueries.Load())
}
}
func TestServeDNSReturnsRetryableResponseWhenUpstreamsExhausted(t *testing.T) {
t.Parallel()
upstreamAddr := startUDPTestDNSServer(t, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
resp := new(dns.Msg)
resp.SetRcode(req, dns.RcodeRefused)
_ = w.WriteMsg(resp)
}))
r := newTestRouter(t, nil, "", "", "", nil)
r.dns.upstreamAddrs = []string{upstreamAddr}
r.dns.upstreamIndex = 0
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
r.ServeDNS(writer, req)
if writer.msg == nil {
t.Fatal("expected response message")
}
if writer.msg.Rcode != dns.RcodeRefused {
t.Fatalf("expected upstream REFUSED response, got rcode %d", writer.msg.Rcode)
}
}
func TestExchangeDoesNotPromoteRetryableProbeResponse(t *testing.T) {
t.Parallel()
var primaryQueries atomic.Int32
var fallbackQueries atomic.Int32
primaryAddr := startUDPTestDNSServer(t, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
primaryQueries.Add(1)
resp := new(dns.Msg)
resp.SetRcode(req, dns.RcodeRefused)
_ = w.WriteMsg(resp)
}))
fallbackAddr := startUDPTestDNSServer(t, dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
fallbackQueries.Add(1)
resp := new(dns.Msg)
resp.SetReply(req)
resp.Answer = []dns.RR{&dns.A{
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
A: net.ParseIP("203.0.113.20"),
}}
_ = w.WriteMsg(resp)
}))
r := newTestRouter(t, nil, "", "", "", nil)
r.dns.upstreamAddrs = []string{primaryAddr, fallbackAddr}
r.dns.upstreamIndex = 1
r.dns.retryAt = time.Now().Add(-time.Second)
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
if _, err := r.Exchange(req); err != nil {
t.Fatalf("exchange failed: %v", err)
}
if r.dns.upstreamIndex != 1 {
t.Fatalf("expected retryable probe response to keep fallback index 1, got %d", r.dns.upstreamIndex)
}
if primaryQueries.Load() != 1 {
t.Fatalf("expected one preferred probe, got %d", primaryQueries.Load())
}
if fallbackQueries.Load() != 1 {
t.Fatalf("expected one fallback query, got %d", fallbackQueries.Load())
}
}
func TestAddCountryCIDRsRejectsInvalidEntries(t *testing.T) {
t.Parallel()
r := newTestRouter(t, nil, "", "223.5.5.5", "", nil)
if err := r.AddCountryCIDRs("invalid-cidr"); err == nil {
t.Fatal("expected invalid CIDR to fail")
}
if len(r.country.cidrs) != 0 {
t.Fatalf("expected invalid CIDR to be rejected, got %d entries", len(r.country.cidrs))
}
if r.localSite(context.Background(), "127.0.0.1") {
t.Fatal("unexpected localSite match without valid CIDRs or MMDB")
}
}
func TestNewRouterSkipsEmptyCountryMMDB(t *testing.T) {
t.Parallel()
for _, mmdbFile := range []string{"", " "} {
r := newTestRouter(t, nil, "", "223.5.5.5", mmdbFile, nil)
if r.country.Reader != nil {
t.Fatalf("expected empty MMDB %q to disable GeoIP lookup", mmdbFile)
}
}
}
func TestNewRouterRejectsInvalidCountryMMDB(t *testing.T) {
t.Parallel()
if _, err := NewRouter(nil, "", "223.5.5.5", "/path/to/missing.mmdb", nil); err == nil {
t.Fatal("expected missing MMDB to fail")
}
}
func TestIsAccessChecksRequestedPortOnly(t *testing.T) {
t.Parallel()
r := newTestRouter(t, nil, "", "223.5.5.5", "", nil)
r.accessCache = newAccessCache(accessCacheTTL, func(key string) (bool, error) {
scheme, _, err := accessProbeTarget(key)
if err != nil {
return false, err
}
return scheme == "https", nil
})
if !r.isAccess("example.com", 443) {
t.Fatal("expected HTTPS probe to succeed")
}
if r.isAccess("example.com", 80) {
t.Fatal("expected HTTP probe to fail")
}
}
func TestServeDNSForProxyRuleSuppressesNonAddressQuestion(t *testing.T) {
t.Parallel()
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen udp: %v", err)
}
defer udpConn.Close()
var upstreamQueries atomic.Int32
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
upstreamQueries.Add(1)
resp := new(dns.Msg)
resp.SetReply(req)
resp.Answer = []dns.RR{&dns.TXT{
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 20},
Txt: []string{"forwarded"},
}}
_ = w.WriteMsg(resp)
})
server := &dns.Server{PacketConn: udpConn, Handler: handler}
go func() { _ = server.ActivateAndServe() }()
t.Cleanup(func() {
_ = server.Shutdown()
})
r := newTestRouter(t, []string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
r.BlockRule = NewRuleSet()
r.DirectRule = NewRuleSet()
r.ProxyRule = NewRuleSet("example.com.")
r.dns.upstreamAddrs = []string{udpConn.LocalAddr().String()}
r.dns.upstreamIndex = 0
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeTXT)
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
r.ServeDNS(writer, req)
if writer.msg == nil {
t.Fatal("expected response message")
}
if writer.msg.Rcode != dns.RcodeSuccess {
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
}
if len(writer.msg.Answer) != 0 {
t.Fatalf("expected NODATA response, got %d answers", len(writer.msg.Answer))
}
if upstreamQueries.Load() != 0 {
t.Fatalf("expected TXT query to be suppressed locally, got %d upstream queries", upstreamQueries.Load())
}
}
func TestServeDNSForProxyRuleSuppressesHTTPSQuestion(t *testing.T) {
t.Parallel()
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen udp: %v", err)
}
defer udpConn.Close()
var upstreamQueries atomic.Int32
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
upstreamQueries.Add(1)
resp := new(dns.Msg)
resp.SetReply(req)
_ = w.WriteMsg(resp)
})
server := &dns.Server{PacketConn: udpConn, Handler: handler}
go func() { _ = server.ActivateAndServe() }()
t.Cleanup(func() {
_ = server.Shutdown()
})
r := newTestRouter(t, []string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
r.BlockRule = NewRuleSet()
r.DirectRule = NewRuleSet()
r.ProxyRule = NewRuleSet("example.com.")
r.dns.upstreamAddrs = []string{udpConn.LocalAddr().String()}
r.dns.upstreamIndex = 0
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeHTTPS)
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
r.ServeDNS(writer, req)
if writer.msg == nil {
t.Fatal("expected response message")
}
if writer.msg.Rcode != dns.RcodeSuccess {
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
}
if len(writer.msg.Answer) != 0 {
t.Fatalf("expected NODATA response, got %d answers", len(writer.msg.Answer))
}
if upstreamQueries.Load() != 0 {
t.Fatalf("expected HTTPS query to be suppressed locally, got %d upstream queries", upstreamQueries.Load())
}
}
func TestServeDNSForProxyRuleSuppressesSRVQuestion(t *testing.T) {
t.Parallel()
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen udp: %v", err)
}
defer udpConn.Close()
var upstreamQueries atomic.Int32
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
upstreamQueries.Add(1)
resp := new(dns.Msg)
resp.SetReply(req)
resp.Answer = []dns.RR{&dns.SRV{
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: 20},
Priority: 10,
Weight: 5,
Port: 5222,
Target: "srv.example.com.",
}}
_ = w.WriteMsg(resp)
})
server := &dns.Server{PacketConn: udpConn, Handler: handler}
go func() { _ = server.ActivateAndServe() }()
t.Cleanup(func() {
_ = server.Shutdown()
})
r := newTestRouter(t, []string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
r.BlockRule = NewRuleSet()
r.DirectRule = NewRuleSet()
r.ProxyRule = NewRuleSet("example.com.")
r.dns.upstreamAddrs = []string{udpConn.LocalAddr().String()}
r.dns.upstreamIndex = 0
req := new(dns.Msg)
req.SetQuestion("_xmpp-client._tcp.example.com.", dns.TypeSRV)
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
r.ServeDNS(writer, req)
if writer.msg == nil {
t.Fatal("expected response message")
}
if writer.msg.Rcode != dns.RcodeSuccess {
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
}
if len(writer.msg.Answer) != 0 {
t.Fatalf("expected NODATA response, got %d answers", len(writer.msg.Answer))
}
if upstreamQueries.Load() != 0 {
t.Fatalf("expected SRV query to be suppressed locally, got %d upstream queries", upstreamQueries.Load())
}
}
func TestServeDNSDoesNotStripServicePrefixForNonServiceQuestion(t *testing.T) {
t.Parallel()
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen udp: %v", err)
}
defer udpConn.Close()
var upstreamQueries atomic.Int32
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
upstreamQueries.Add(1)
resp := new(dns.Msg)
resp.SetReply(req)
resp.Answer = []dns.RR{&dns.TXT{
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 20},
Txt: []string{"forwarded"},
}}
_ = w.WriteMsg(resp)
})
server := &dns.Server{PacketConn: udpConn, Handler: handler}
go func() { _ = server.ActivateAndServe() }()
t.Cleanup(func() {
_ = server.Shutdown()
})
r := newTestRouter(t, []string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
r.BlockRule = NewRuleSet()
r.DirectRule = NewRuleSet()
r.ProxyRule = NewRuleSet("example.com.")
r.dns.upstreamAddrs = []string{udpConn.LocalAddr().String()}
r.dns.upstreamIndex = 0
req := new(dns.Msg)
req.SetQuestion("_xmpp-client._tcp.example.com.", dns.TypeTXT)
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
r.ServeDNS(writer, req)
if writer.msg == nil {
t.Fatal("expected response message")
}
if writer.msg.Rcode != dns.RcodeSuccess {
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
}
if len(writer.msg.Answer) != 1 {
t.Fatalf("expected forwarded TXT answer, got %d answers", len(writer.msg.Answer))
}
if upstreamQueries.Load() != 1 {
t.Fatalf("expected TXT query to be forwarded once, got %d upstream queries", upstreamQueries.Load())
}
}
func TestServeDNSForProxyRuleReturnsNoDataWhenAddressFamilyUnavailable(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "", "223.5.5.5", "", nil)
r.BlockRule = NewRuleSet()
r.DirectRule = NewRuleSet()
r.ProxyRule = NewRuleSet("example.com.")
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeAAAA)
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
r.ServeDNS(writer, req)
if writer.msg == nil {
t.Fatal("expected response message")
}
if writer.msg.Rcode != dns.RcodeSuccess {
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
}
if len(writer.msg.Answer) != 0 {
t.Fatalf("expected NODATA response, got %d answers", len(writer.msg.Answer))
}
}
func TestBuildUpstreamAddrsIsPerRouter(t *testing.T) {
t.Parallel()
r1 := newTestRouter(t, []string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
r2 := newTestRouter(t, []string{"127.0.0.2"}, "8.8.8.8", "4.4.4.4", "", nil)
addrs1, err := r1.buildUpstreamAddrs("1.1.1.1", "9.9.9.9")
if err != nil {
t.Fatalf("buildUpstreamAddrs r1 failed: %v", err)
}
addrs2, err := r2.buildUpstreamAddrs("8.8.8.8", "4.4.4.4")
if err != nil {
t.Fatalf("buildUpstreamAddrs r2 failed: %v", err)
}
if strings.Join(addrs1, ",") == strings.Join(addrs2, ",") {
t.Fatalf("expected router-specific upstreams, got %v and %v", addrs1, addrs2)
}
}
func TestCurrentUpstreamStateSchedulesPreferredProbeAfterRetryInterval(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
r.dns.upstreamIndex = 1
r.dns.retryAt = time.Now().Add(-time.Second)
_, index, shouldProbe, err := r.currentUpstreamState(time.Now())
if err != nil {
t.Fatalf("currentUpstreamState failed: %v", err)
}
if index != 1 {
t.Fatalf("expected current degraded upstream index 1, got %d", index)
}
if !shouldProbe {
t.Fatal("expected preferred upstream probe to be scheduled")
}
}
func TestCurrentUpstreamStateAllowsOnlyOnePreferredProbe(t *testing.T) {
t.Parallel()
now := time.Now()
r := newTestRouter(t, []string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
r.dns.upstreamIndex = 1
r.dns.retryAt = now.Add(-time.Second)
_, _, shouldProbe, err := r.currentUpstreamState(now)
if err != nil {
t.Fatalf("currentUpstreamState failed: %v", err)
}
if !shouldProbe {
t.Fatal("expected first caller to probe preferred upstream")
}
_, _, shouldProbe, err = r.currentUpstreamState(now)
if err != nil {
t.Fatalf("currentUpstreamState second call failed: %v", err)
}
if shouldProbe {
t.Fatal("expected second caller to reuse in-flight probe state")
}
}
func TestDegradeUpstreamMovesTowardFallback(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
r.dns.upstreamIndex = 0
index, switched := r.degradeUpstream(0, r.dns.generation, len(r.dns.upstreamAddrs))
if !switched {
t.Fatal("expected upstream switch")
}
if index != 1 {
t.Fatalf("expected fallback index 1, got %d", index)
}
if r.dns.retryAt.IsZero() {
t.Fatal("expected retry deadline to be set")
}
}
func TestDegradeUpstreamSkipsRetryPastSnapshot(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
r.dns.upstreamIndex = 0
// The caller's snapshot is shorter than the live list (a concurrent
// refresh grew it); degrading onto index 1 would index past the
// snapshot. The retry must be skipped, not panic.
if index, switched := r.degradeUpstream(0, r.dns.generation, 1); switched || index != 0 {
t.Fatalf("expected stale snapshot to skip retry, got index=%d switched=%v", index, switched)
}
if r.dns.upstreamIndex != 0 {
t.Fatalf("upstream index must stay 0, got %d", r.dns.upstreamIndex)
}
}
func TestPromoteUpstreamRestoresPreferredAndClearsRetry(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
r.dns.upstreamIndex = 1
r.dns.retryAt = time.Now().Add(time.Minute)
r.promoteUpstream(r.dns.generation)
if r.dns.upstreamIndex != 0 {
t.Fatalf("expected preferred index 0, got %d", r.dns.upstreamIndex)
}
if !r.dns.retryAt.IsZero() {
t.Fatal("expected retry deadline to be cleared")
}
}
func TestSetDNSDiscardsStaleProbeCompletion(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
r.dns.upstreamIndex = 1
r.dns.retryAt = time.Now().Add(-time.Second)
_, _, shouldProbe, generation, err := r.currentUpstreamStateWithGeneration(time.Now())
if err != nil || !shouldProbe {
t.Fatalf("expected probe under old generation: probe=%v err=%v", shouldProbe, err)
}
// Simulate a new configuration that has already selected its fallback
// while the old preferred-upstream probe is still exchanging.
r.SetDNS("8.8.8.8", "4.4.4.4")
r.dns.Lock()
r.dns.upstreamAddrs = []string{"8.8.8.8:53", "4.4.4.4:53"}
r.dns.upstreamIndex = 1
retryAt := time.Now().Add(time.Minute)
r.dns.retryAt = retryAt
r.dns.probeInFlight = true
r.dns.Unlock()
r.promoteUpstream(generation)
r.scheduleRetry(time.Now(), generation)
r.dns.Lock()
defer r.dns.Unlock()
if r.dns.upstreamIndex != 1 {
t.Fatalf("stale probe changed new upstream index to %d", r.dns.upstreamIndex)
}
if !r.dns.retryAt.Equal(retryAt) {
t.Fatalf("stale probe changed new retry deadline: got %v want %v", r.dns.retryAt, retryAt)
}
if !r.dns.probeInFlight {
t.Fatal("stale probe cleared new probe ownership")
}
}
func TestCurrentUpstreamStateRefreshesDHCPUpstreams(t *testing.T) {
t.Parallel()
var calls atomic.Int32
r := newTestRouter(t, []string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
r.dns.getDNSServer = func() ([]string, error) {
if calls.Add(1) == 1 {
return []string{"1.1.1.1"}, nil
}
return []string{"8.8.8.8"}, nil
}
now := time.Now()
addrs, index, shouldProbe, err := r.currentUpstreamState(now)
if err != nil {
t.Fatalf("initial currentUpstreamState failed: %v", err)
}
if index != 0 || shouldProbe {
t.Fatalf("unexpected initial state: index=%d shouldProbe=%v", index, shouldProbe)
}
if strings.Join(addrs, ",") != "1.1.1.1:53,9.9.9.9:53" {
t.Fatalf("unexpected initial upstreams: %v", addrs)
}
addrs, index, shouldProbe, err = r.currentUpstreamState(now.Add(dnsRefreshTTL + time.Second))
if err != nil {
t.Fatalf("refreshed currentUpstreamState failed: %v", err)
}
if index != 0 || shouldProbe {
t.Fatalf("unexpected refreshed state: index=%d shouldProbe=%v", index, shouldProbe)
}
if strings.Join(addrs, ",") != "8.8.8.8:53,9.9.9.9:53" {
t.Fatalf("unexpected refreshed upstreams: %v", addrs)
}
}
func TestCurrentUpstreamStateBacksOffInitialDHCPFailure(t *testing.T) {
t.Parallel()
var calls atomic.Int32
r := newTestRouter(t, []string{"127.0.0.1"}, "", "", "", nil)
r.dns.getDNSServer = func() ([]string, error) {
calls.Add(1)
return nil, errors.New("dhcp unavailable")
}
now := time.Now()
if _, _, _, err := r.currentUpstreamState(now); err == nil {
t.Fatal("expected initial DHCP failure")
}
if calls.Load() != 1 {
t.Fatalf("expected one DHCP call, got %d", calls.Load())
}
if _, _, _, err := r.currentUpstreamState(now.Add(time.Second)); err == nil {
t.Fatal("expected cached DHCP failure during backoff")
}
if calls.Load() != 1 {
t.Fatalf("expected no extra DHCP call during backoff, got %d", calls.Load())
}
}
func TestCurrentUpstreamStateRefreshDoesNotBlockConcurrentReaders(t *testing.T) {
t.Parallel()
startRefresh := make(chan struct{})
releaseRefresh := make(chan struct{})
var calls atomic.Int32
r := newTestRouter(t, []string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
r.dns.upstreamIndex = 0
r.dns.refreshAt = time.Now().Add(-time.Second)
r.dns.getDNSServer = func() ([]string, error) {
if calls.Add(1) == 1 {
close(startRefresh)
<-releaseRefresh
}
return []string{"8.8.8.8"}, nil
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_, _, _, _ = r.currentUpstreamState(time.Now())
}()
<-startRefresh
done := make(chan struct{})
go func() {
defer close(done)
addrs, index, shouldProbe, err := r.currentUpstreamState(time.Now())
if err != nil {
t.Errorf("concurrent currentUpstreamState failed: %v", err)
return
}
if index != 0 || shouldProbe {
t.Errorf("unexpected concurrent state: index=%d shouldProbe=%v", index, shouldProbe)
return
}
if strings.Join(addrs, ",") != "1.1.1.1:53,9.9.9.9:53" {
t.Errorf("unexpected concurrent upstreams: %v", addrs)
}
}()
select {
case <-done:
case <-time.After(200 * time.Millisecond):
t.Fatal("concurrent reader blocked on refresh lock")
}
close(releaseRefresh)
wg.Wait()
}
func TestCurrentUpstreamStateUsesFallbackWhileInitialRefreshInFlight(t *testing.T) {
t.Parallel()
startRefresh := make(chan struct{})
releaseRefresh := make(chan struct{})
r := newTestRouter(t, []string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
r.dns.getDNSServer = func() ([]string, error) {
close(startRefresh)
<-releaseRefresh
return []string{"1.1.1.1"}, nil
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_, _, _, _ = r.currentUpstreamState(time.Now())
}()
<-startRefresh
done := make(chan struct{})
go func() {
defer close(done)
addrs, index, shouldProbe, err := r.currentUpstreamState(time.Now())
if err != nil {
t.Errorf("currentUpstreamState during initial refresh failed: %v", err)
return
}
if index != 0 || shouldProbe {
t.Errorf("unexpected fallback state: index=%d shouldProbe=%v", index, shouldProbe)
return
}
if strings.Join(addrs, ",") != "9.9.9.9:53" {
t.Errorf("expected fallback-only upstreams, got %v", addrs)
}
}()
select {
case <-done:
case <-time.After(200 * time.Millisecond):
t.Fatal("concurrent caller blocked instead of using fallback")
}
close(releaseRefresh)
wg.Wait()
}
func TestCurrentUpstreamStatePreservesProbeWhenRefreshIsDue(t *testing.T) {
t.Parallel()
now := time.Now()
r := newTestRouter(t, []string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
r.dns.upstreamIndex = 1
r.dns.retryAt = now.Add(-time.Second)
r.dns.refreshAt = now.Add(-time.Second)
r.dns.getDNSServer = func() ([]string, error) {
return []string{"8.8.8.8"}, nil
}
addrs, index, shouldProbe, err := r.currentUpstreamState(now)
if err != nil {
t.Fatalf("currentUpstreamState with refresh failed: %v", err)
}
if !shouldProbe {
t.Fatal("expected refresh to preserve due preferred probe")
}
if index != 1 {
t.Fatalf("expected degraded upstream to be preserved across refresh, got %d", index)
}
if strings.Join(addrs, ",") != "8.8.8.8:53,9.9.9.9:53" {
t.Fatalf("unexpected refreshed upstreams: %v", addrs)
}
if !r.dns.probeInFlight {
t.Fatal("expected probe to be marked in-flight after refresh")
}
if !r.dns.retryAt.After(now) {
t.Fatalf("expected retryAt to be pushed forward after scheduling probe, got %v", r.dns.retryAt)
}
}
func TestServeDNSReturnsNXDomainForBlockedDomain(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "", "223.5.5.5", "", nil)
r.BlockRule = NewRuleSet("example.com.")
r.DirectRule = NewRuleSet()
r.ProxyRule = NewRuleSet()
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
r.ServeDNS(writer, req)
if writer.msg == nil {
t.Fatal("expected response message")
}
if writer.msg.Rcode != dns.RcodeNameError {
t.Fatalf("expected NXDOMAIN, got %d", writer.msg.Rcode)
}
}
func TestServeDNSRejectsUnsupportedQuestionClass(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "", "223.5.5.5", "", nil)
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
req.Question[0].Qclass = dns.ClassCHAOS
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
r.ServeDNS(writer, req)
if writer.msg == nil {
t.Fatal("expected response message")
}
if writer.msg.Rcode != dns.RcodeNotImplemented {
t.Fatalf("expected NOTIMP, got %d", writer.msg.Rcode)
}
}
func TestServeDNSRejectsMultipleQuestions(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "", "223.5.5.5", "", nil)
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
req.Question = append(req.Question, dns.Question{Name: "example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET})
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
r.ServeDNS(writer, req)
if writer.msg == nil {
t.Fatal("expected response message")
}
if writer.msg.Rcode != dns.RcodeFormatError {
t.Fatalf("expected format error, got %d", writer.msg.Rcode)
}
}
func TestExchangeWithRetryFallsBackToTCPOnTruncatedUDP(t *testing.T) {
t.Parallel()
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen udp: %v", err)
}
defer udpConn.Close()
tcpLn, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strings.Split(udpConn.LocalAddr().String(), ":")[1]))
if err != nil {
t.Fatalf("listen tcp: %v", err)
}
defer tcpLn.Close()
var tcpQueries atomic.Int32
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
resp := new(dns.Msg)
resp.SetReply(req)
if _, ok := w.RemoteAddr().(*net.TCPAddr); ok {
tcpQueries.Add(1)
resp.Answer = []dns.RR{&dns.A{
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
A: net.ParseIP("127.0.0.1"),
}}
} else {
resp.Truncated = true
}
_ = w.WriteMsg(resp)
})
udpServer := &dns.Server{PacketConn: udpConn, Handler: handler}
tcpServer := &dns.Server{Listener: tcpLn, Handler: handler}
go func() { _ = udpServer.ActivateAndServe() }()
go func() { _ = tcpServer.ActivateAndServe() }()
t.Cleanup(func() {
_ = udpServer.Shutdown()
_ = tcpServer.Shutdown()
})
r := newTestRouter(t, nil, "", "223.5.5.5", "", nil)
req := new(dns.Msg)
req.SetQuestion("example.com.", dns.TypeA)
resp, err := r.exchangeWithRetry(req, udpConn.LocalAddr().String())
if err != nil {
t.Fatalf("exchangeWithRetry failed: %v", err)
}
if len(resp.Answer) != 1 {
t.Fatalf("expected tcp answer, got %d answers", len(resp.Answer))
}
if tcpQueries.Load() == 0 {
t.Fatal("expected TCP fallback query")
}
}
type mockDNSWriter struct {
msg *dns.Msg
localAddr net.Addr
}
func (w *mockDNSWriter) LocalAddr() net.Addr { return w.localAddr }
func (w *mockDNSWriter) RemoteAddr() net.Addr { return &net.UDPAddr{} }
func (w *mockDNSWriter) WriteMsg(msg *dns.Msg) error { w.msg = msg; return nil }
func (w *mockDNSWriter) Write([]byte) (int, error) { return 0, nil }
func (w *mockDNSWriter) Close() error { return nil }
func (w *mockDNSWriter) TsigStatus() error { return nil }
func (w *mockDNSWriter) TsigTimersOnly(bool) {}
func (w *mockDNSWriter) Hijack() {}
func startUDPTestDNSServer(t *testing.T, handler dns.Handler) string {
t.Helper()
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen udp: %v", err)
}
server := &dns.Server{PacketConn: udpConn, Handler: handler}
go func() { _ = server.ActivateAndServe() }()
t.Cleanup(func() {
_ = server.Shutdown()
_ = udpConn.Close()
})
return udpConn.LocalAddr().String()
}
func TestRouteObserverFiresOncePerConnection(t *testing.T) {
t.Parallel()
r := newTestRouter(t, nil, "", "223.5.5.5", "", func(network, host string, port uint16) (net.Conn, error) {
return nil, errors.New("dial failed")
})
r.BlockRule.Add("blocked.example")
r.DirectRule.Add("direct.example")
var mu sync.Mutex
var got []RouteCategory
r.SetRouteObserver(func(c RouteCategory, _ string) {
mu.Lock()
got = append(got, c)
mu.Unlock()
})
if _, err := r.DialSmart("tcp", "blocked.example", 443); !errors.Is(err, ErrBlocked) {
t.Fatalf("expected ErrBlocked, got %v", err)
}
if _, err := r.DialSmart("tcp", "direct.example", 443); err == nil {
t.Fatal("expected direct dial to fail")
}
if _, err := r.DialSmart("tcp", "proxy.example", 443); err == nil {
t.Fatal("expected proxy dial to fail")
}
if _, err := r.DialProxyOnly("tcp", "http.example", 80); err == nil {
t.Fatal("expected proxy dial to fail")
}
mu.Lock()
defer mu.Unlock()
want := []RouteCategory{RouteBlock, RouteDirect, RouteProxy, RouteProxy}
if len(got) != len(want) {
t.Fatalf("expected %d observations, got %d: %v", len(want), len(got), got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("observation %d: expected %q, got %q", i, want[i], got[i])
}
}
}
func TestSetDNSSwapsUpstreamsAtRuntime(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
r.dns.refreshAt = time.Now().Add(time.Hour)
r.SetDNS("8.8.8.8", "4.4.4.4")
r.dns.Lock()
if r.dns.upstreamDNS != "8.8.8.8" || r.dns.fallbackDNS != "4.4.4.4" {
t.Fatalf("dns config not swapped: %v %v", r.dns.upstreamDNS, r.dns.fallbackDNS)
}
if len(r.dns.upstreamAddrs) != 0 {
t.Fatalf("cached upstreams not invalidated: %v", r.dns.upstreamAddrs)
}
if !r.dns.refreshAt.IsZero() {
t.Fatalf("refresh schedule not cleared: %v", r.dns.refreshAt)
}
r.dns.Unlock()
// The next query rebuilds upstreams from the new configuration.
addrs, _, _, err := r.currentUpstreamState(time.Now())
if err != nil {
t.Fatalf("currentUpstreamState after SetDNS: %v", err)
}
if strings.Join(addrs, ",") != "8.8.8.8:53,4.4.4.4:53" {
t.Fatalf("expected rebuilt upstreams from new config, got %v", addrs)
}
}
func TestSetDNSDiscardsStaleRefresh(t *testing.T) {
t.Parallel()
r := newTestRouter(t, []string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
// A refresh starts under the current configuration.
now := time.Now()
_, _, _, needRefresh, oldGeneration, err := r.prepareUpstreamState(now)
if err != nil || !needRefresh {
t.Fatalf("expected old refresh to start, needRefresh=%v err=%v", needRefresh, err)
}
// SetDNS invalidates the old owner and permits an immediate new refresh.
r.SetDNS("8.8.8.8", "4.4.4.4")
_, _, _, needRefresh, newGeneration, err := r.prepareUpstreamState(now)
if err != nil || !needRefresh || newGeneration == oldGeneration {
t.Fatalf("expected new-generation refresh, refresh=%v old=%d new=%d err=%v", needRefresh, oldGeneration, newGeneration, err)
}
// The old completion must not clear ownership of the newer refresh.
r.finishUpstreamRefresh(now, []string{"1.1.1.1:53", "9.9.9.9:53"}, nil, oldGeneration)
r.dns.Lock()
if !r.dns.refreshInFlight || r.dns.refreshGeneration != newGeneration {
t.Fatalf("stale completion changed new refresh owner: inFlight=%v owner=%d", r.dns.refreshInFlight, r.dns.refreshGeneration)
}
if len(r.dns.upstreamAddrs) != 0 {
t.Fatalf("stale refresh result applied: %v", r.dns.upstreamAddrs)
}
r.dns.Unlock()
r.finishUpstreamRefresh(now, []string{"8.8.8.8:53", "4.4.4.4:53"}, nil, newGeneration)
addrs, _, _, err := r.currentUpstreamState(now)
if err != nil {
t.Fatalf("currentUpstreamState after new refresh: %v", err)
}
if strings.Join(addrs, ",") != "8.8.8.8:53,4.4.4.4:53" {
t.Fatalf("expected upstreams from new config, got %v", addrs)
}
}