fix: harden dns proxy routing

This commit is contained in:
wweir
2026-04-25 04:12:42 +08:00
parent 09e4fc49fc
commit ad338598ae
6 changed files with 411 additions and 43 deletions
+5 -1
View File
@@ -51,13 +51,17 @@
1. Load config from flags, env, and files.
2. Validate remote type, listener addresses, and DNS IP fields.
3. Log startup metadata with secrets redacted.
4. Resolve the effective upstream DNS server, falling back to the configured fallback DNS when needed.
4. Build the upstream proxy dialer with a stable DNS target. If `dns.upstream` is empty, the dialer uses `dns.fallback` to avoid recursive lookup through the local DNS listener.
5. Build the upstream dialer for the configured remote transport, using standard TLS by default and optional uTLS fingerprints for `sower` and `trojan`.
6. Build the router with suffix-tree rules and optional country CIDRs.
Remote rule files are fetched through the configured upstream proxy dialer, never by direct outbound HTTP, so rule bootstrap uses the same stable egress path as proxied traffic.
Remote domain rule files are filtered through per-router `file_skip_rules` before their prefixed entries are appended.
7. Start enabled local listeners for `udp/53`, `tcp/80`, `tcp/443`, and `tcp/1080` only after rule loading completes.
8. For DNS requests, return local proxy IPs for proxy-routed domains and query upstream DNS for direct domains.
Empty `dns.upstream` keeps router-side DHCP DNS discovery enabled; `dns.fallback` is appended as a backup upstream and is also used while initial discovery is in flight.
Proxy-routed domains return local A/AAAA records, suppress HTTPS/SVCB and other non-address metadata locally, and never leak proxy-matched names to direct upstream DNS.
Direct upstream DNS failures fall back only for retryable upstream service errors; when no fallback succeeds, the last upstream DNS response code is returned as-is.
Service discovery names are matched against both the full query name and the base domain only for service record types.
9. For HTTP traffic, parse the target host from the request line. For HTTPS transparent traffic, peek the TLS ClientHello to extract SNI. For SOCKS5 traffic, read the SOCKS5 target address. Apply routing rules and either dial directly or wrap traffic in the configured upstream transport.
HTTPS transparent proxying reads only the TLS ClientHello, then replays the untouched bytes to the selected upstream; it must not complete or terminate TLS locally.
10. On shutdown signal, stop listeners and DNS servers through `context` propagation.
+3 -3
View File
@@ -102,7 +102,7 @@ func run(ctx context.Context, stop context.CancelFunc, cfg config.SowerConfig) e
if err != nil {
return fmt.Errorf("build proxy dialer: %w", err)
}
r := newRouter(cfg, upstreamDNS, proxyDial)
r := newRouter(cfg, proxyDial)
start := time.Now()
if err := loadRouterRules(ctx, r, proxyDial, cfg); err != nil {
@@ -138,8 +138,8 @@ func effectiveUpstreamDNS(cfg config.SowerConfig) string {
return cfg.DNS.Fallback
}
func newRouter(cfg config.SowerConfig, upstreamDNS string, proxyDial router.ProxyDialFn) *router.Router {
r := router.NewRouter([]string{cfg.DNS.Serve, cfg.DNS.Serve6}, upstreamDNS, cfg.DNS.Fallback, cfg.Router.Country.MMDB, proxyDial)
func newRouter(cfg config.SowerConfig, proxyDial router.ProxyDialFn) *router.Router {
r := router.NewRouter([]string{cfg.DNS.Serve, cfg.DNS.Serve6}, cfg.DNS.Upstream, cfg.DNS.Fallback, cfg.Router.Country.MMDB, proxyDial)
r.BlockRule = suffixtree.NewNodeFromRules(cfg.Router.Block.Rules...)
r.DirectRule = suffixtree.NewNodeFromRules(cfg.Router.Direct.Rules...)
r.ProxyRule = suffixtree.NewNodeFromRules(cfg.Router.Proxy.Rules...)
+23
View File
@@ -9,12 +9,14 @@ import (
"net/http"
"net/http/httptest"
"os"
"reflect"
"slices"
"strconv"
"strings"
"testing"
"time"
"github.com/sower-proxy/sower/config"
"github.com/sower-proxy/sower/pkg/suffixtree"
)
@@ -30,6 +32,27 @@ func TestFetchRuleFileEmptyPathReturnsClosedChannel(t *testing.T) {
}
}
func TestNewRouterPreservesEmptyUpstreamForDiscovery(t *testing.T) {
t.Parallel()
cfg := config.SowerConfig{}
cfg.DNS.Serve = "127.0.0.1"
cfg.DNS.Upstream = ""
cfg.DNS.Fallback = "223.5.5.5"
r := newRouter(cfg, nil)
dnsState := reflect.ValueOf(r).Elem().FieldByName("dns")
upstreamDNS := dnsState.FieldByName("upstreamDNS").String()
fallbackDNS := dnsState.FieldByName("fallbackDNS").String()
if upstreamDNS != "" {
t.Fatalf("expected empty router upstream DNS to enable discovery, got %q", upstreamDNS)
}
if fallbackDNS != "223.5.5.5" {
t.Fatalf("unexpected fallback DNS: %q", fallbackDNS)
}
}
func TestFetchRuleFileRemoteGzip(t *testing.T) {
t.Parallel()
+99 -19
View File
@@ -13,6 +13,14 @@ import (
var errNoLocalIPForQuestionType = errors.New("no local IP for question type")
type retryableDNSError struct {
rcode int
}
func (e retryableDNSError) Error() string {
return fmt.Sprintf("retryable dns response code %s", dns.RcodeToString[e.rcode])
}
func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
// https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071
if len(req.Question) != 1 {
@@ -22,13 +30,19 @@ func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
domain := req.Question[0].Name
qtype := req.Question[0].Qtype
if req.Question[0].Qclass != dns.ClassINET {
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeNotImplemented))
return
}
// 1. rule_based( block > direct > proxy )
if r.BlockRule.Match(domain) {
routeDomains := dnsRouteDomains(domain, qtype)
if dnsRuleMatch(r.BlockRule, routeDomains) {
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeNameError))
return
}
if !r.DirectRule.Match(domain) && r.ProxyRule.Match(domain) {
if !dnsRuleMatch(r.DirectRule, routeDomains) && dnsRuleMatch(r.ProxyRule, routeDomains) {
switch {
case isAddressQuestion(qtype):
resp, err := r.dnsProxyReply(domain, w.LocalAddr(), req)
@@ -39,7 +53,7 @@ func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
}
_ = w.WriteMsg(resp)
return
case isProxySensitiveQuestion(qtype):
default:
_ = w.WriteMsg(r.dnsNoData(req))
return
}
@@ -85,28 +99,35 @@ func (r *Router) dnsProxyReply(domain string, localAddr net.Addr, req *dns.Msg)
}
func (r *Router) proxyReplyIP(localAddr net.Addr, qtype uint16) (net.IP, error) {
host, _, err := net.SplitHostPort(localAddr.String())
if err != nil {
return nil, fmt.Errorf("split local dns address %q: %w", localAddr.String(), err)
}
ip := net.ParseIP(strings.Trim(host, "[]"))
if ip != nil && !ip.IsUnspecified() && matchesQuestionTypeIP(ip, qtype) {
return ip, nil
}
if ip == nil {
return nil, fmt.Errorf("parse local dns IP %q", host)
}
if !isAddressQuestion(qtype) {
return nil, fmt.Errorf("unsupported question type %d", qtype)
}
var localAddrErr error
if localAddr != nil {
host, _, err := net.SplitHostPort(localAddr.String())
if err != nil {
localAddrErr = fmt.Errorf("split local dns address %q: %w", localAddr.String(), err)
} else {
ip := net.ParseIP(strings.Trim(host, "[]"))
switch {
case ip == nil:
localAddrErr = fmt.Errorf("parse local dns IP %q", host)
case !ip.IsUnspecified() && matchesQuestionTypeIP(ip, qtype):
return ip, nil
}
}
}
for _, serveIP := range r.dns.serveIPs {
if !serveIP.IsUnspecified() && matchesQuestionTypeIP(serveIP, qtype) {
return serveIP, nil
}
}
if localAddrErr != nil {
return nil, localAddrErr
}
return nil, fmt.Errorf("%w %d", errNoLocalIPForQuestionType, qtype)
}
@@ -114,8 +135,43 @@ func isAddressQuestion(qtype uint16) bool {
return qtype == dns.TypeA || qtype == dns.TypeAAAA
}
func isProxySensitiveQuestion(qtype uint16) bool {
return qtype == dns.TypeHTTPS || qtype == dns.TypeSVCB
func dnsRuleMatch(rule interface{ Match(string) bool }, domains []string) bool {
for _, domain := range domains {
if rule.Match(domain) {
return true
}
}
return false
}
func dnsRouteDomains(domain string, qtype uint16) []string {
if !isServiceDiscoveryQuestion(qtype) {
return []string{domain}
}
base, ok := stripDNSServicePrefix(domain)
if !ok {
return []string{domain}
}
return []string{domain, base}
}
func isServiceDiscoveryQuestion(qtype uint16) bool {
switch qtype {
case dns.TypeSRV, dns.TypeSVCB, dns.TypeHTTPS:
return true
default:
return false
}
}
func stripDNSServicePrefix(domain string) (string, bool) {
trimmed := strings.TrimSuffix(domain, ".")
labels := strings.Split(trimmed, ".")
if len(labels) <= 2 || !strings.HasPrefix(labels[0], "_") || !strings.HasPrefix(labels[1], "_") {
return "", false
}
return strings.Join(labels[2:], ".") + ".", true
}
func dnsReply(req *dns.Msg) *dns.Msg {
@@ -183,9 +239,17 @@ func (r *Router) Exchange(req *dns.Msg) (_ *dns.Msg, err error) {
}
}
if resp != nil && isRetryableDNSResponseErr(err) {
return resp, nil
}
return resp, err
}
func isRetryableDNSResponseErr(err error) bool {
var retryableErr retryableDNSError
return errors.As(err, &retryableErr)
}
func (r *Router) currentUpstreamState(now time.Time) ([]string, int, bool, error) {
for {
addrs, index, shouldProbe, needRefresh, err := r.prepareUpstreamState(now)
@@ -383,10 +447,26 @@ func (r *Router) exchangeWithRetry(req *dns.Msg, addr string) (*dns.Msg, error)
return nil, err
}
if !resp.Truncated {
return resp, nil
return resp, dnsResponseErr(resp)
}
return r.exchangeTCP(req, addr)
resp, err = r.exchangeTCP(req, addr)
if err != nil {
return nil, err
}
return resp, dnsResponseErr(resp)
}
func dnsResponseErr(resp *dns.Msg) error {
if resp == nil {
return fmt.Errorf("empty dns response")
}
switch resp.Rcode {
case dns.RcodeServerFailure, dns.RcodeRefused, dns.RcodeNotImplemented:
return retryableDNSError{rcode: resp.Rcode}
default:
return nil
}
}
func (r *Router) exchangeUDP(req *dns.Msg, addr string) (*dns.Msg, error) {
+4 -1
View File
@@ -52,7 +52,10 @@ var ErrBlocked = errors.New("route blocked")
func NewRouter(serveIPs []string, upstreamDNS, fallbackDNS, mmdbFile string, proxyDial ProxyDialFn) *Router {
r := Router{
ProxyDial: proxyDial,
BlockRule: suffixtree.NewNodeFromRules(),
DirectRule: suffixtree.NewNodeFromRules(),
ProxyRule: suffixtree.NewNodeFromRules(),
ProxyDial: proxyDial,
}
r.dns.upstreamDNS = upstreamDNS
+277 -19
View File
@@ -24,6 +24,15 @@ func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return fn(req)
}
func TestNewRouterInitializesRules(t *testing.T) {
t.Parallel()
r := NewRouter(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()
@@ -98,6 +107,27 @@ func TestDNSProxyReplyIgnoresUnspecifiedLocalAddr(t *testing.T) {
}
}
func TestDNSProxyReplyFallsBackWhenLocalAddrMissing(t *testing.T) {
t.Parallel()
r := NewRouter([]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 TestExchangeSkipsServeIPInUpstreamList(t *testing.T) {
t.Parallel()
@@ -130,6 +160,153 @@ func TestBuildUpstreamAddrsPrefersConfiguredDNSBeforeFallback(t *testing.T) {
}
}
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 := NewRouter(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 := NewRouter(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 := NewRouter(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 := NewRouter(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 TestAddCountryCIDRsSkipsInvalidEntries(t *testing.T) {
t.Parallel()
@@ -195,7 +372,7 @@ func TestIsAccessChecksRequestedPortOnly(t *testing.T) {
}
}
func TestServeDNSForProxyRuleForwardsNonAddressQuestion(t *testing.T) {
func TestServeDNSForProxyRuleSuppressesNonAddressQuestion(t *testing.T) {
t.Parallel()
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
@@ -204,7 +381,9 @@ func TestServeDNSForProxyRuleForwardsNonAddressQuestion(t *testing.T) {
}
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{
@@ -239,15 +418,11 @@ func TestServeDNSForProxyRuleForwardsNonAddressQuestion(t *testing.T) {
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 len(writer.msg.Answer) != 0 {
t.Fatalf("expected NODATA response, got %d answers", len(writer.msg.Answer))
}
answer, ok := writer.msg.Answer[0].(*dns.TXT)
if !ok {
t.Fatalf("expected TXT record, got %T", writer.msg.Answer[0])
}
if len(answer.Txt) != 1 || answer.Txt[0] != "forwarded" {
t.Fatalf("unexpected TXT payload: %v", answer.Txt)
if upstreamQueries.Load() != 0 {
t.Fatalf("expected TXT query to be suppressed locally, got %d upstream queries", upstreamQueries.Load())
}
}
@@ -301,7 +476,7 @@ func TestServeDNSForProxyRuleSuppressesHTTPSQuestion(t *testing.T) {
}
}
func TestServeDNSForProxyRuleForwardsSRVQuestion(t *testing.T) {
func TestServeDNSForProxyRuleSuppressesSRVQuestion(t *testing.T) {
t.Parallel()
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
@@ -344,6 +519,60 @@ func TestServeDNSForProxyRuleForwardsSRVQuestion(t *testing.T) {
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 := NewRouter([]string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
r.BlockRule = suffixtree.NewNodeFromRules()
r.DirectRule = suffixtree.NewNodeFromRules()
r.ProxyRule = suffixtree.NewNodeFromRules("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")
}
@@ -351,17 +580,10 @@ func TestServeDNSForProxyRuleForwardsSRVQuestion(t *testing.T) {
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
}
if len(writer.msg.Answer) != 1 {
t.Fatalf("expected forwarded SRV answer, got %d answers", len(writer.msg.Answer))
}
answer, ok := writer.msg.Answer[0].(*dns.SRV)
if !ok {
t.Fatalf("expected SRV record, got %T", writer.msg.Answer[0])
}
if answer.Target != "srv.example.com." || answer.Port != 5222 {
t.Fatalf("unexpected SRV answer: target=%s port=%d", answer.Target, answer.Port)
t.Fatalf("expected forwarded TXT answer, got %d answers", len(writer.msg.Answer))
}
if upstreamQueries.Load() != 1 {
t.Fatalf("expected SRV query to be forwarded once, got %d upstream queries", upstreamQueries.Load())
t.Fatalf("expected TXT query to be forwarded once, got %d upstream queries", upstreamQueries.Load())
}
}
@@ -715,6 +937,25 @@ func TestServeDNSReturnsNXDomainForBlockedDomain(t *testing.T) {
}
}
func TestServeDNSRejectsUnsupportedQuestionClass(t *testing.T) {
t.Parallel()
r := NewRouter([]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()
@@ -803,3 +1044,20 @@ 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()
}