79718ee19e
Fix bufpool leak when PackBuffer fails by extracting packResponse helper. Add write deadline before conn.Write to prevent goroutine hangs on slow clients. Log async exchange errors instead of silently discarding them. Raise defaultBufferSize from 1024 to 4096 to match EDNS0 standard. Deduplicate lookupHosts TypeA/TypeAAAA branches.
1124 lines
32 KiB
Go
1124 lines
32 KiB
Go
package dns
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/bypass"
|
|
"github.com/go-gost/core/chain"
|
|
"github.com/go-gost/core/handler"
|
|
"github.com/go-gost/core/hop"
|
|
"github.com/go-gost/core/hosts"
|
|
"github.com/go-gost/core/limiter/rate"
|
|
"github.com/go-gost/core/logger"
|
|
xlogger "github.com/go-gost/x/logger"
|
|
xmd "github.com/go-gost/x/metadata"
|
|
resolver_util "github.com/go-gost/x/internal/util/resolver"
|
|
xrecorder "github.com/go-gost/x/recorder"
|
|
"github.com/go-gost/x/resolver/exchanger"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mocks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type mockExchanger struct {
|
|
exchangeFn func(ctx context.Context, msg []byte) ([]byte, error)
|
|
addr string
|
|
}
|
|
|
|
func (m *mockExchanger) Exchange(ctx context.Context, msg []byte) ([]byte, error) {
|
|
if m.exchangeFn != nil {
|
|
return m.exchangeFn(ctx, msg)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockExchanger) String() string { return m.addr }
|
|
|
|
// mockHop implements hop.Hop and hop.NodeList.
|
|
type mockHop struct {
|
|
selectFn func(ctx context.Context, opts ...hop.SelectOption) *chain.Node
|
|
nodes []*chain.Node
|
|
}
|
|
|
|
func (m *mockHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
if m.selectFn != nil {
|
|
return m.selectFn(ctx, opts...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockHop) Nodes() []*chain.Node { return m.nodes }
|
|
|
|
type mockHostMapper struct {
|
|
lookupFn func(ctx context.Context, network, host string) ([]net.IP, bool)
|
|
}
|
|
|
|
func (m *mockHostMapper) Lookup(ctx context.Context, network, host string, opts ...hosts.Option) ([]net.IP, bool) {
|
|
if m.lookupFn != nil {
|
|
return m.lookupFn(ctx, network, host)
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
type mockBypass struct {
|
|
containsFn func(ctx context.Context, network, addr string) bool
|
|
whitelist bool
|
|
}
|
|
|
|
func (m *mockBypass) IsWhitelist() bool { return m.whitelist }
|
|
|
|
func (m *mockBypass) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
|
|
if m.containsFn != nil {
|
|
return m.containsFn(ctx, network, addr)
|
|
}
|
|
return false
|
|
}
|
|
|
|
type mockRateLimiter struct {
|
|
limiterFn func(key string) rate.Limiter
|
|
}
|
|
|
|
func (m *mockRateLimiter) Limiter(key string) rate.Limiter {
|
|
if m.limiterFn != nil {
|
|
return m.limiterFn(key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type mockLimiter struct {
|
|
allowFn func(n int) bool
|
|
limit float64
|
|
}
|
|
|
|
func (m *mockLimiter) Allow(n int) bool {
|
|
if m.allowFn != nil {
|
|
return m.allowFn(n)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (m *mockLimiter) Limit() float64 { return m.limit }
|
|
|
|
// mockRouter implements chain.Router.
|
|
type mockRouter struct {
|
|
opts *chain.RouterOptions
|
|
}
|
|
|
|
func (m *mockRouter) Options() *chain.RouterOptions { return m.opts }
|
|
func (m *mockRouter) Dial(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
|
|
return nil, nil
|
|
}
|
|
func (m *mockRouter) Bind(ctx context.Context, network, address string, opts ...chain.BindOption) (net.Listener, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func nopLog() logger.Logger { return xlogger.Nop() }
|
|
|
|
func newRecObj() *xrecorder.HandlerRecorderObject {
|
|
return &xrecorder.HandlerRecorderObject{Time: time.Now()}
|
|
}
|
|
|
|
func newTestHandler(opts ...handler.Option) *dnsHandler {
|
|
options := handler.Options{
|
|
Logger: nopLog(),
|
|
Router: &mockRouter{opts: &chain.RouterOptions{}},
|
|
}
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
return &dnsHandler{
|
|
options: options,
|
|
exchangers: make(map[string]exchanger.Exchanger),
|
|
}
|
|
}
|
|
|
|
func newInitdHandler(opts ...handler.Option) *dnsHandler {
|
|
h := newTestHandler(opts...)
|
|
_ = h.Init(xmd.NewMetadata(nil))
|
|
return h
|
|
}
|
|
|
|
// packDNSQuery creates a raw DNS query for the given name/type.
|
|
func packDNSQuery(name string, qtype uint16) []byte {
|
|
m := new(dns.Msg)
|
|
m.SetQuestion(name, qtype)
|
|
b, _ := m.Pack()
|
|
return b
|
|
}
|
|
|
|
func mustNewRR(s string) dns.RR {
|
|
rr, err := dns.NewRR(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return rr
|
|
}
|
|
|
|
// stringConn is a net.Conn backed by bytes.Buffers for testing.
|
|
type stringConn struct {
|
|
readBuf *bytes.Buffer
|
|
writeBuf *bytes.Buffer
|
|
local net.Addr
|
|
remote net.Addr
|
|
}
|
|
|
|
func newStringConn(data []byte) *stringConn {
|
|
return &stringConn{
|
|
readBuf: bytes.NewBuffer(data),
|
|
writeBuf: &bytes.Buffer{},
|
|
local: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53},
|
|
remote: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345},
|
|
}
|
|
}
|
|
|
|
func (c *stringConn) Read(b []byte) (int, error) { return c.readBuf.Read(b) }
|
|
func (c *stringConn) Write(b []byte) (int, error) { return c.writeBuf.Write(b) }
|
|
func (c *stringConn) Close() error { return nil }
|
|
func (c *stringConn) LocalAddr() net.Addr { return c.local }
|
|
func (c *stringConn) RemoteAddr() net.Addr { return c.remote }
|
|
func (c *stringConn) SetDeadline(t time.Time) error { return nil }
|
|
func (c *stringConn) SetReadDeadline(t time.Time) error { return nil }
|
|
func (c *stringConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
|
|
// slowConn blocks on Read for the given duration.
|
|
type slowConn struct{ readDelay time.Duration }
|
|
|
|
func (c *slowConn) Read(b []byte) (int, error) { time.Sleep(c.readDelay); return 0, errors.New("timeout") }
|
|
func (c *slowConn) Write(b []byte) (int, error) { return len(b), nil }
|
|
func (c *slowConn) Close() error { return nil }
|
|
func (c *slowConn) LocalAddr() net.Addr {
|
|
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}
|
|
}
|
|
func (c *slowConn) RemoteAddr() net.Addr {
|
|
return &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345}
|
|
}
|
|
func (c *slowConn) SetDeadline(t time.Time) error { return nil }
|
|
func (c *slowConn) SetReadDeadline(t time.Time) error { return nil }
|
|
func (c *slowConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: parseMetadata
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestParseMetadata_Defaults(t *testing.T) {
|
|
h := newTestHandler()
|
|
if err := h.parseMetadata(xmd.NewMetadata(nil)); err != nil {
|
|
t.Fatalf("parseMetadata: %v", err)
|
|
}
|
|
if h.md.timeout != defaultTimeout {
|
|
t.Errorf("timeout = %v, want %v", h.md.timeout, defaultTimeout)
|
|
}
|
|
if h.md.bufferSize != defaultBufferSize {
|
|
t.Errorf("bufferSize = %d, want %d", h.md.bufferSize, defaultBufferSize)
|
|
}
|
|
if h.md.async {
|
|
t.Error("async should be false by default")
|
|
}
|
|
if h.md.readTimeout != 0 {
|
|
t.Errorf("readTimeout = %v, want 0", h.md.readTimeout)
|
|
}
|
|
if h.md.ttl != 0 {
|
|
t.Errorf("ttl = %v, want 0", h.md.ttl)
|
|
}
|
|
}
|
|
|
|
func TestParseMetadata_AllFields(t *testing.T) {
|
|
h := newTestHandler()
|
|
err := h.parseMetadata(xmd.NewMetadata(map[string]any{
|
|
"readTimeout": "10s",
|
|
"ttl": "30s",
|
|
"timeout": "3s",
|
|
"clientIP": "1.2.3.4",
|
|
"dns": "udp://8.8.8.8:53, tcp://1.1.1.1:53",
|
|
"bufferSize": 2048,
|
|
"async": true,
|
|
}))
|
|
if err != nil {
|
|
t.Fatalf("parseMetadata: %v", err)
|
|
}
|
|
if h.md.readTimeout != 10*time.Second {
|
|
t.Errorf("readTimeout = %v, want 10s", h.md.readTimeout)
|
|
}
|
|
if h.md.ttl != 30*time.Second {
|
|
t.Errorf("ttl = %v, want 30s", h.md.ttl)
|
|
}
|
|
if h.md.timeout != 3*time.Second {
|
|
t.Errorf("timeout = %v, want 3s", h.md.timeout)
|
|
}
|
|
if !h.md.clientIP.Equal(net.ParseIP("1.2.3.4")) {
|
|
t.Errorf("clientIP = %v, want 1.2.3.4", h.md.clientIP)
|
|
}
|
|
if h.md.bufferSize != 2048 {
|
|
t.Errorf("bufferSize = %d, want 2048", h.md.bufferSize)
|
|
}
|
|
if !h.md.async {
|
|
t.Error("async should be true")
|
|
}
|
|
if len(h.md.dns) != 2 {
|
|
t.Fatalf("len(dns) = %d, want 2", len(h.md.dns))
|
|
}
|
|
if h.md.dns[0] != "udp://8.8.8.8:53" {
|
|
t.Errorf("dns[0] = %q, want %q", h.md.dns[0], "udp://8.8.8.8:53")
|
|
}
|
|
if h.md.dns[1] != "tcp://1.1.1.1:53" {
|
|
t.Errorf("dns[1] = %q, want %q", h.md.dns[1], "tcp://1.1.1.1:53")
|
|
}
|
|
}
|
|
|
|
func TestParseMetadata_EmptyDNS(t *testing.T) {
|
|
h := newTestHandler()
|
|
if err := h.parseMetadata(xmd.NewMetadata(map[string]any{"dns": ""})); err != nil {
|
|
t.Fatalf("parseMetadata: %v", err)
|
|
}
|
|
if len(h.md.dns) != 0 {
|
|
t.Errorf("len(dns) = %d, want 0", len(h.md.dns))
|
|
}
|
|
}
|
|
|
|
func TestParseMetadata_TimeoutAsInt(t *testing.T) {
|
|
h := newTestHandler()
|
|
if err := h.parseMetadata(xmd.NewMetadata(map[string]any{"timeout": 10})); err != nil {
|
|
t.Fatalf("parseMetadata: %v", err)
|
|
}
|
|
if h.md.timeout != 10*time.Second {
|
|
t.Errorf("timeout = %v, want 10s", h.md.timeout)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: NewHandler
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestNewHandler(t *testing.T) {
|
|
h := NewHandler(handler.LoggerOption(nopLog()))
|
|
if _, ok := h.(*dnsHandler); !ok {
|
|
t.Error("NewHandler should return *dnsHandler")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: Init
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestInit_DefaultExchanger(t *testing.T) {
|
|
h := newTestHandler()
|
|
if err := h.Init(xmd.NewMetadata(nil)); err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
if len(h.exchangers) != 1 {
|
|
t.Fatalf("len(exchangers) = %d, want 1", len(h.exchangers))
|
|
}
|
|
if _, ok := h.exchangers["default"]; !ok {
|
|
t.Error("expected 'default' exchanger")
|
|
}
|
|
}
|
|
|
|
func TestInit_WithHopNodes(t *testing.T) {
|
|
h := newTestHandler()
|
|
h.hop = &mockHop{nodes: []*chain.Node{
|
|
chain.NewNode("ns1", "udp://8.8.8.8:53"),
|
|
chain.NewNode("ns2", "tcp://1.1.1.1:53"),
|
|
}}
|
|
if err := h.Init(xmd.NewMetadata(nil)); err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
if len(h.exchangers) != 2 {
|
|
t.Errorf("len(exchangers) = %d, want 2", len(h.exchangers))
|
|
}
|
|
if _, ok := h.exchangers["ns1"]; !ok {
|
|
t.Error("expected 'ns1' exchanger")
|
|
}
|
|
if _, ok := h.exchangers["ns2"]; !ok {
|
|
t.Error("expected 'ns2' exchanger")
|
|
}
|
|
}
|
|
|
|
func TestInit_SkipsEmptyAddr(t *testing.T) {
|
|
h := newTestHandler()
|
|
h.hop = &mockHop{nodes: []*chain.Node{
|
|
chain.NewNode("empty", ""),
|
|
chain.NewNode("spaces", " "),
|
|
chain.NewNode("valid", "udp://8.8.8.8:53"),
|
|
}}
|
|
if err := h.Init(xmd.NewMetadata(nil)); err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
if len(h.exchangers) != 1 {
|
|
t.Errorf("len(exchangers) = %d, want 1", len(h.exchangers))
|
|
}
|
|
if _, ok := h.exchangers["valid"]; !ok {
|
|
t.Error("expected 'valid' exchanger")
|
|
}
|
|
}
|
|
|
|
func TestInit_MetadataDNS(t *testing.T) {
|
|
h := newTestHandler()
|
|
if err := h.Init(xmd.NewMetadata(map[string]any{"dns": "udp://8.8.8.8:53"})); err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
if len(h.exchangers) != 1 {
|
|
t.Errorf("len(exchangers) = %d, want 1", len(h.exchangers))
|
|
}
|
|
if _, ok := h.exchangers["target-0"]; !ok {
|
|
t.Error("expected 'target-0' exchanger")
|
|
}
|
|
}
|
|
|
|
func TestInit_HostMapper(t *testing.T) {
|
|
mapper := &mockHostMapper{}
|
|
h := newTestHandler()
|
|
h.options.Router = &mockRouter{
|
|
opts: &chain.RouterOptions{HostMapper: mapper},
|
|
}
|
|
if err := h.Init(xmd.NewMetadata(nil)); err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
if h.hostMapper != hosts.HostMapper(mapper) {
|
|
t.Error("hostMapper not set from router options")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: Forward
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestForward(t *testing.T) {
|
|
h := newInitdHandler()
|
|
newHop := &mockHop{}
|
|
h.Forward(newHop)
|
|
if h.hop != hop.Hop(newHop) {
|
|
t.Error("Forward did not set hop")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: checkRateLimit
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCheckRateLimit_NilLimiter(t *testing.T) {
|
|
h := newInitdHandler()
|
|
addr := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345}
|
|
if !h.checkRateLimit(addr) {
|
|
t.Error("should allow when no rate limiter configured")
|
|
}
|
|
}
|
|
|
|
func TestCheckRateLimit_Allowed(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.options.RateLimiter = &mockRateLimiter{
|
|
limiterFn: func(key string) rate.Limiter {
|
|
return &mockLimiter{allowFn: func(n int) bool { return true }}
|
|
},
|
|
}
|
|
addr := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345}
|
|
if !h.checkRateLimit(addr) {
|
|
t.Error("should allow when limiter permits")
|
|
}
|
|
}
|
|
|
|
func TestCheckRateLimit_Blocked(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.options.RateLimiter = &mockRateLimiter{
|
|
limiterFn: func(key string) rate.Limiter {
|
|
return &mockLimiter{allowFn: func(n int) bool { return false }}
|
|
},
|
|
}
|
|
addr := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345}
|
|
if h.checkRateLimit(addr) {
|
|
t.Error("should block when limiter denies")
|
|
}
|
|
}
|
|
|
|
func TestCheckRateLimit_NoLimiterForKey(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.options.RateLimiter = &mockRateLimiter{
|
|
limiterFn: func(key string) rate.Limiter { return nil },
|
|
}
|
|
addr := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345}
|
|
if !h.checkRateLimit(addr) {
|
|
t.Error("should allow when no limiter for key")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: selectExchanger
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSelectExchanger_NilHop(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hop = nil
|
|
if ex := h.selectExchanger(context.Background(), "example.com"); ex != nil {
|
|
t.Error("expected nil for nil hop")
|
|
}
|
|
}
|
|
|
|
func TestSelectExchanger_NoNode(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hop = &mockHop{}
|
|
if ex := h.selectExchanger(context.Background(), "example.com"); ex != nil {
|
|
t.Error("expected nil when hop returns no node")
|
|
}
|
|
}
|
|
|
|
func TestSelectExchanger_FoundNode(t *testing.T) {
|
|
mockEx := &mockExchanger{addr: "udp://8.8.8.8:53"}
|
|
h := newInitdHandler()
|
|
h.hop = &mockHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
return chain.NewNode("ns1", "udp://8.8.8.8:53")
|
|
},
|
|
}
|
|
h.exchangers["ns1"] = mockEx
|
|
if ex := h.selectExchanger(context.Background(), "example.com"); ex != mockEx {
|
|
t.Error("expected mock exchanger")
|
|
}
|
|
}
|
|
|
|
func TestSelectExchanger_NodeNotInExchangers(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hop = &mockHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
return chain.NewNode("unknown", "udp://8.8.8.8:53")
|
|
},
|
|
}
|
|
if ex := h.selectExchanger(context.Background(), "example.com"); ex != nil {
|
|
t.Error("expected nil for unknown node")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: lookupHosts
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestLookupHosts_NilMapper(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hostMapper = nil
|
|
q := new(dns.Msg)
|
|
q.SetQuestion("example.com.", dns.TypeA)
|
|
if mr := h.lookupHosts(context.Background(), q, nopLog()); mr != nil {
|
|
t.Error("expected nil for nil mapper")
|
|
}
|
|
}
|
|
|
|
func TestLookupHosts_NonINETClass(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hostMapper = &mockHostMapper{}
|
|
q := new(dns.Msg)
|
|
q.SetQuestion("example.com.", dns.TypeA)
|
|
q.Question[0].Qclass = dns.ClassCHAOS
|
|
if mr := h.lookupHosts(context.Background(), q, nopLog()); mr != nil {
|
|
t.Error("expected nil for non-INET class")
|
|
}
|
|
}
|
|
|
|
func TestLookupHosts_UnsupportedType(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hostMapper = &mockHostMapper{}
|
|
q := new(dns.Msg)
|
|
q.SetQuestion("example.com.", dns.TypeMX)
|
|
if mr := h.lookupHosts(context.Background(), q, nopLog()); mr != nil {
|
|
t.Error("expected nil for unsupported type")
|
|
}
|
|
}
|
|
|
|
func TestLookupHosts_ARecord(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hostMapper = &mockHostMapper{
|
|
lookupFn: func(ctx context.Context, network, host string) ([]net.IP, bool) {
|
|
if network != "ip4" {
|
|
t.Errorf("network = %q, want %q", network, "ip4")
|
|
}
|
|
if host != "example.com" {
|
|
t.Errorf("host = %q, want %q", host, "example.com")
|
|
}
|
|
return []net.IP{net.ParseIP("1.2.3.4"), net.ParseIP("5.6.7.8")}, true
|
|
},
|
|
}
|
|
q := new(dns.Msg)
|
|
q.SetQuestion("example.com.", dns.TypeA)
|
|
mr := h.lookupHosts(context.Background(), q, nopLog())
|
|
if mr == nil {
|
|
t.Fatal("expected non-nil response")
|
|
}
|
|
if len(mr.Answer) != 2 {
|
|
t.Fatalf("len(Answer) = %d, want 2", len(mr.Answer))
|
|
}
|
|
for _, rr := range mr.Answer {
|
|
a, ok := rr.(*dns.A)
|
|
if !ok {
|
|
t.Fatal("expected A record")
|
|
}
|
|
if a.Hdr.Name != "example.com." {
|
|
t.Errorf("Name = %q, want %q", a.Hdr.Name, "example.com.")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLookupHosts_AAAARecord(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hostMapper = &mockHostMapper{
|
|
lookupFn: func(ctx context.Context, network, host string) ([]net.IP, bool) {
|
|
if network != "ip6" {
|
|
t.Errorf("network = %q, want %q", network, "ip6")
|
|
}
|
|
return []net.IP{net.ParseIP("::1")}, true
|
|
},
|
|
}
|
|
q := new(dns.Msg)
|
|
q.SetQuestion("example.com.", dns.TypeAAAA)
|
|
mr := h.lookupHosts(context.Background(), q, nopLog())
|
|
if mr == nil {
|
|
t.Fatal("expected non-nil response")
|
|
}
|
|
if len(mr.Answer) != 1 {
|
|
t.Fatalf("len(Answer) = %d, want 1", len(mr.Answer))
|
|
}
|
|
aaaa, ok := mr.Answer[0].(*dns.AAAA)
|
|
if !ok {
|
|
t.Fatal("expected AAAA record")
|
|
}
|
|
if aaaa.Hdr.Name != "example.com." {
|
|
t.Errorf("Name = %q, want %q", aaaa.Hdr.Name, "example.com.")
|
|
}
|
|
}
|
|
|
|
func TestLookupHosts_EmptyResult(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hostMapper = &mockHostMapper{
|
|
lookupFn: func(ctx context.Context, network, host string) ([]net.IP, bool) {
|
|
return nil, false
|
|
},
|
|
}
|
|
q := new(dns.Msg)
|
|
q.SetQuestion("example.com.", dns.TypeA)
|
|
if mr := h.lookupHosts(context.Background(), q, nopLog()); mr != nil {
|
|
t.Error("expected nil for empty result")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: exchange
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestExchange_StoresInCache(t *testing.T) {
|
|
query := new(dns.Msg)
|
|
query.SetQuestion("cached.example.com.", dns.TypeA)
|
|
query.Id = 42
|
|
|
|
respMsg := query.Copy()
|
|
respMsg.Answer = []dns.RR{mustNewRR("cached.example.com. 300 IN A 9.8.7.6")}
|
|
respBytes, _ := respMsg.Pack()
|
|
|
|
mockEx := &mockExchanger{
|
|
exchangeFn: func(ctx context.Context, msg []byte) ([]byte, error) {
|
|
return respBytes, nil
|
|
},
|
|
addr: "udp://8.8.8.8:53",
|
|
}
|
|
|
|
h := newInitdHandler()
|
|
h.md.bufferSize = defaultBufferSize
|
|
h.md.ttl = 30 * time.Second
|
|
|
|
mr, err := h.exchange(context.Background(), mockEx, query)
|
|
if err != nil {
|
|
t.Fatalf("exchange: %v", err)
|
|
}
|
|
if mr == nil {
|
|
t.Fatal("expected non-nil response")
|
|
}
|
|
if len(mr.Answer) != 1 {
|
|
t.Errorf("len(Answer) = %d, want 1", len(mr.Answer))
|
|
}
|
|
}
|
|
|
|
func TestExchange_UpstreamError(t *testing.T) {
|
|
query := new(dns.Msg)
|
|
query.SetQuestion("fail.example.com.", dns.TypeA)
|
|
|
|
mockEx := &mockExchanger{
|
|
exchangeFn: func(ctx context.Context, msg []byte) ([]byte, error) {
|
|
return nil, errors.New("upstream unreachable")
|
|
},
|
|
}
|
|
|
|
h := newInitdHandler()
|
|
mr, err := h.exchange(context.Background(), mockEx, query)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if mr != nil {
|
|
t.Error("expected nil response on error")
|
|
}
|
|
}
|
|
|
|
func TestExchange_InvalidResponse(t *testing.T) {
|
|
query := new(dns.Msg)
|
|
query.SetQuestion("bad.example.com.", dns.TypeA)
|
|
|
|
mockEx := &mockExchanger{
|
|
exchangeFn: func(ctx context.Context, msg []byte) ([]byte, error) {
|
|
return []byte("not a dns message"), nil
|
|
},
|
|
}
|
|
|
|
h := newInitdHandler()
|
|
mr, err := h.exchange(context.Background(), mockEx, query)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid DNS response")
|
|
}
|
|
if mr != nil {
|
|
t.Error("expected nil response on error")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: request
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestRequest_EmptyQuestion(t *testing.T) {
|
|
h := newInitdHandler()
|
|
msg := new(dns.Msg)
|
|
raw, _ := msg.Pack()
|
|
_, err := h.request(context.Background(), raw, newRecObj(), nopLog())
|
|
if err == nil {
|
|
t.Fatal("expected error for empty question")
|
|
}
|
|
if got := err.Error(); got != "msg: empty question" {
|
|
t.Errorf("error = %q, want %q", got, "msg: empty question")
|
|
}
|
|
}
|
|
|
|
func TestRequest_InvalidMessage(t *testing.T) {
|
|
h := newInitdHandler()
|
|
_, err := h.request(context.Background(), []byte{0xFF, 0xFF, 0xFF}, newRecObj(), nopLog())
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid DNS message")
|
|
}
|
|
}
|
|
|
|
func TestRequest_Bypass(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.options.Bypass = &mockBypass{
|
|
containsFn: func(ctx context.Context, network, addr string) bool {
|
|
if network != "udp" {
|
|
t.Errorf("network = %q, want %q", network, "udp")
|
|
}
|
|
if addr != "blocked.example.com" {
|
|
t.Errorf("addr = %q, want %q", addr, "blocked.example.com")
|
|
}
|
|
return true
|
|
},
|
|
}
|
|
|
|
reply, err := h.request(context.Background(), packDNSQuery("blocked.example.com.", dns.TypeA), newRecObj(), nopLog())
|
|
if err != nil {
|
|
t.Fatalf("request: %v", err)
|
|
}
|
|
if len(reply) == 0 {
|
|
t.Fatal("expected non-empty reply")
|
|
}
|
|
mr := new(dns.Msg)
|
|
if err := mr.Unpack(reply); err != nil {
|
|
t.Fatalf("unpack: %v", err)
|
|
}
|
|
if len(mr.Answer) != 0 {
|
|
t.Errorf("len(Answer) = %d, want 0 (bypassed)", len(mr.Answer))
|
|
}
|
|
}
|
|
|
|
func TestRequest_HostMapperHit(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hostMapper = &mockHostMapper{
|
|
lookupFn: func(ctx context.Context, network, host string) ([]net.IP, bool) {
|
|
return []net.IP{net.ParseIP("1.2.3.4")}, true
|
|
},
|
|
}
|
|
|
|
reply, err := h.request(context.Background(), packDNSQuery("mapped.example.com.", dns.TypeA), newRecObj(), nopLog())
|
|
if err != nil {
|
|
t.Fatalf("request: %v", err)
|
|
}
|
|
mr := new(dns.Msg)
|
|
if err := mr.Unpack(reply); err != nil {
|
|
t.Fatalf("unpack: %v", err)
|
|
}
|
|
if len(mr.Answer) != 1 {
|
|
t.Errorf("len(Answer) = %d, want 1", len(mr.Answer))
|
|
}
|
|
}
|
|
|
|
func TestRequest_NoExchanger(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.hop = nil
|
|
h.exchangers = make(map[string]exchanger.Exchanger)
|
|
|
|
_, err := h.request(context.Background(), packDNSQuery("none.example.com.", dns.TypeA), newRecObj(), nopLog())
|
|
if err == nil {
|
|
t.Fatal("expected error when no exchanger")
|
|
}
|
|
if got := err.Error(); got != "exchange not found for none.example.com." {
|
|
t.Errorf("error = %q, want to contain 'exchange not found'", got)
|
|
}
|
|
}
|
|
|
|
func TestRequest_CacheHit(t *testing.T) {
|
|
h := newInitdHandler()
|
|
|
|
q := new(dns.Msg)
|
|
q.SetQuestion("cached.example.com.", dns.TypeA)
|
|
respMsg := q.Copy()
|
|
respMsg.Answer = []dns.RR{mustNewRR("cached.example.com. 300 IN A 9.8.7.6")}
|
|
key := resolver_util.NewCacheKey(&q.Question[0])
|
|
h.cache.Store(context.Background(), key, respMsg, 30*time.Second)
|
|
|
|
reply, err := h.request(context.Background(), packDNSQuery("cached.example.com.", dns.TypeA), newRecObj(), nopLog())
|
|
if err != nil {
|
|
t.Fatalf("request: %v", err)
|
|
}
|
|
mr := new(dns.Msg)
|
|
if err := mr.Unpack(reply); err != nil {
|
|
t.Fatalf("unpack: %v", err)
|
|
}
|
|
if len(mr.Answer) != 1 {
|
|
t.Errorf("len(Answer) = %d, want 1 (cached)", len(mr.Answer))
|
|
}
|
|
}
|
|
|
|
func TestRequest_ExchangeSuccess(t *testing.T) {
|
|
query := new(dns.Msg)
|
|
query.SetQuestion("test.example.com.", dns.TypeA)
|
|
respMsg := query.Copy()
|
|
respMsg.Answer = []dns.RR{mustNewRR("test.example.com. 60 IN A 10.0.0.1")}
|
|
respBytes, _ := respMsg.Pack()
|
|
|
|
mockEx := &mockExchanger{
|
|
exchangeFn: func(ctx context.Context, msg []byte) ([]byte, error) {
|
|
return respBytes, nil
|
|
},
|
|
addr: "udp://8.8.8.8:53",
|
|
}
|
|
|
|
h := newInitdHandler()
|
|
h.hop = &mockHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
return chain.NewNode("ns1", "udp://8.8.8.8:53")
|
|
},
|
|
}
|
|
h.exchangers["ns1"] = mockEx
|
|
|
|
reply, err := h.request(context.Background(), packDNSQuery("test.example.com.", dns.TypeA), newRecObj(), nopLog())
|
|
if err != nil {
|
|
t.Fatalf("request: %v", err)
|
|
}
|
|
mr := new(dns.Msg)
|
|
if err := mr.Unpack(reply); err != nil {
|
|
t.Fatalf("unpack: %v", err)
|
|
}
|
|
if len(mr.Answer) != 1 {
|
|
t.Errorf("len(Answer) = %d, want 1", len(mr.Answer))
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: Handle (integration)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHandle_BasicQuery(t *testing.T) {
|
|
answerRR := mustNewRR("test.example.com. 60 IN A 10.0.0.1")
|
|
mockEx := &mockExchanger{
|
|
exchangeFn: func(ctx context.Context, msg []byte) ([]byte, error) {
|
|
mq := new(dns.Msg)
|
|
_ = mq.Unpack(msg)
|
|
resp := mq.Copy()
|
|
resp.Answer = []dns.RR{answerRR}
|
|
return resp.Pack()
|
|
},
|
|
addr: "udp://8.8.8.8:53",
|
|
}
|
|
|
|
h := newInitdHandler()
|
|
h.hop = &mockHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
return chain.NewNode("ns1", "udp://8.8.8.8:53")
|
|
},
|
|
}
|
|
h.exchangers["ns1"] = mockEx
|
|
|
|
conn := newStringConn(packDNSQuery("test.example.com.", dns.TypeA))
|
|
if err := h.Handle(context.Background(), conn); err != nil {
|
|
t.Fatalf("Handle: %v", err)
|
|
}
|
|
|
|
written := conn.writeBuf.Bytes()
|
|
if len(written) == 0 {
|
|
t.Fatal("expected response written to conn")
|
|
}
|
|
mr := new(dns.Msg)
|
|
if err := mr.Unpack(written); err != nil {
|
|
t.Fatalf("unpack response: %v", err)
|
|
}
|
|
if len(mr.Answer) != 1 {
|
|
t.Errorf("len(Answer) = %d, want 1", len(mr.Answer))
|
|
}
|
|
}
|
|
|
|
func TestHandle_RateLimited(t *testing.T) {
|
|
h := newInitdHandler()
|
|
h.options.RateLimiter = &mockRateLimiter{
|
|
limiterFn: func(key string) rate.Limiter {
|
|
return &mockLimiter{allowFn: func(n int) bool { return false }}
|
|
},
|
|
}
|
|
conn := newStringConn(packDNSQuery("test.example.com.", dns.TypeA))
|
|
if err := h.Handle(context.Background(), conn); err == nil {
|
|
t.Fatal("expected error when rate limited")
|
|
}
|
|
}
|
|
|
|
func TestHandle_InvalidData(t *testing.T) {
|
|
h := newInitdHandler()
|
|
conn := newStringConn([]byte{0xFF, 0xFF, 0xFF})
|
|
if err := h.Handle(context.Background(), conn); err == nil {
|
|
t.Fatal("expected error for invalid DNS data")
|
|
}
|
|
}
|
|
|
|
func TestHandle_ReadTimeout(t *testing.T) {
|
|
h := newTestHandler()
|
|
h.md.readTimeout = 50 * time.Millisecond
|
|
h.md.timeout = defaultTimeout
|
|
h.md.bufferSize = defaultBufferSize
|
|
h.cache = resolver_util.NewCache().WithLogger(nopLog())
|
|
|
|
conn := &slowConn{readDelay: 200 * time.Millisecond}
|
|
if err := h.Handle(context.Background(), conn); err == nil {
|
|
t.Fatal("expected error from slow read")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: async mode
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestRequest_AsyncRefresh(t *testing.T) {
|
|
var (
|
|
answerRR = mustNewRR("async.example.com. 300 IN A 10.0.0.1")
|
|
done = make(chan struct{})
|
|
)
|
|
|
|
h := newInitdHandler()
|
|
h.md.async = true
|
|
h.md.ttl = 30 * time.Second
|
|
|
|
// Pre-populate cache with a tiny TTL so it expires before the request.
|
|
// The async path triggers when: cache hit but TTL <= 0.
|
|
q := new(dns.Msg)
|
|
q.SetQuestion("async.example.com.", dns.TypeA)
|
|
respMsg := q.Copy()
|
|
respMsg.Answer = []dns.RR{answerRR}
|
|
key := resolver_util.NewCacheKey(&q.Question[0])
|
|
h.cache.Store(context.Background(), key, respMsg, time.Nanosecond)
|
|
time.Sleep(5 * time.Millisecond) // let entry expire
|
|
|
|
// Set up exchanger for async refresh
|
|
mockEx := &mockExchanger{
|
|
exchangeFn: func(ctx context.Context, msg []byte) ([]byte, error) {
|
|
defer close(done)
|
|
|
|
mq := new(dns.Msg)
|
|
_ = mq.Unpack(msg)
|
|
resp := mq.Copy()
|
|
resp.Answer = []dns.RR{answerRR}
|
|
return resp.Pack()
|
|
},
|
|
addr: "udp://8.8.8.8:53",
|
|
}
|
|
|
|
h.hop = &mockHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
return chain.NewNode("ns1", "udp://8.8.8.8:53")
|
|
},
|
|
}
|
|
h.exchangers["ns1"] = mockEx
|
|
|
|
reply, err := h.request(context.Background(), packDNSQuery("async.example.com.", dns.TypeA), newRecObj(), nopLog())
|
|
if err != nil {
|
|
t.Fatalf("request: %v", err)
|
|
}
|
|
mr := new(dns.Msg)
|
|
if err := mr.Unpack(reply); err != nil {
|
|
t.Fatalf("unpack: %v", err)
|
|
}
|
|
if len(mr.Answer) != 1 {
|
|
t.Errorf("len(Answer) = %d, want 1 (cached)", len(mr.Answer))
|
|
}
|
|
|
|
// Wait for async goroutine via channel synchronization.
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("async exchange did not occur within timeout")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: Concurrent access
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHandle_ConcurrentRequests(t *testing.T) {
|
|
mockEx := &mockExchanger{
|
|
exchangeFn: func(ctx context.Context, msg []byte) ([]byte, error) {
|
|
mq := new(dns.Msg)
|
|
_ = mq.Unpack(msg)
|
|
resp := mq.Copy()
|
|
rr := mustNewRR(fmt.Sprintf("%s 60 IN A 10.0.0.1", mq.Question[0].Name))
|
|
resp.Answer = []dns.RR{rr}
|
|
return resp.Pack()
|
|
},
|
|
addr: "udp://8.8.8.8:53",
|
|
}
|
|
|
|
h := newInitdHandler()
|
|
h.hop = &mockHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
return chain.NewNode("ns1", "udp://8.8.8.8:53")
|
|
},
|
|
}
|
|
h.exchangers["ns1"] = mockEx
|
|
|
|
var wg sync.WaitGroup
|
|
for i := range 10 {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
name := fmt.Sprintf("test%d.example.com.", i)
|
|
conn := newStringConn(packDNSQuery(name, dns.TypeA))
|
|
if err := h.Handle(context.Background(), conn); err != nil {
|
|
t.Errorf("Handle(%d): %v", i, err)
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: async error handling
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestRequest_AsyncExchangeError(t *testing.T) {
|
|
answerRR := mustNewRR("async-err.example.com. 300 IN A 10.0.0.1")
|
|
|
|
h := newInitdHandler()
|
|
h.md.async = true
|
|
h.md.ttl = 30 * time.Second
|
|
|
|
// Pre-populate cache with expired entry.
|
|
q := new(dns.Msg)
|
|
q.SetQuestion("async-err.example.com.", dns.TypeA)
|
|
respMsg := q.Copy()
|
|
respMsg.Answer = []dns.RR{answerRR}
|
|
key := resolver_util.NewCacheKey(&q.Question[0])
|
|
h.cache.Store(context.Background(), key, respMsg, time.Nanosecond)
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
done := make(chan struct{})
|
|
mockEx := &mockExchanger{
|
|
exchangeFn: func(ctx context.Context, msg []byte) ([]byte, error) {
|
|
defer close(done)
|
|
return nil, errors.New("upstream unreachable")
|
|
},
|
|
addr: "udp://8.8.8.8:53",
|
|
}
|
|
|
|
h.hop = &mockHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
return chain.NewNode("ns1", "udp://8.8.8.8:53")
|
|
},
|
|
}
|
|
h.exchangers["ns1"] = mockEx
|
|
|
|
// The stale response should still be returned despite async error.
|
|
reply, err := h.request(context.Background(), packDNSQuery("async-err.example.com.", dns.TypeA), newRecObj(), nopLog())
|
|
if err != nil {
|
|
t.Fatalf("request: %v", err)
|
|
}
|
|
mr := new(dns.Msg)
|
|
if err := mr.Unpack(reply); err != nil {
|
|
t.Fatalf("unpack: %v", err)
|
|
}
|
|
if len(mr.Answer) != 1 {
|
|
t.Errorf("len(Answer) = %d, want 1 (stale cached)", len(mr.Answer))
|
|
}
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("async exchange did not complete within timeout")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests: write deadline
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHandle_WriteDeadline(t *testing.T) {
|
|
answerRR := mustNewRR("timeout.example.com. 60 IN A 10.0.0.1")
|
|
mockEx := &mockExchanger{
|
|
exchangeFn: func(ctx context.Context, msg []byte) ([]byte, error) {
|
|
mq := new(dns.Msg)
|
|
_ = mq.Unpack(msg)
|
|
resp := mq.Copy()
|
|
resp.Answer = []dns.RR{answerRR}
|
|
return resp.Pack()
|
|
},
|
|
addr: "udp://8.8.8.8:53",
|
|
}
|
|
|
|
h := newTestHandler()
|
|
h.md.readTimeout = 50 * time.Millisecond
|
|
h.md.timeout = defaultTimeout
|
|
h.md.bufferSize = defaultBufferSize
|
|
h.cache = resolver_util.NewCache().WithLogger(nopLog())
|
|
h.hop = &mockHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
return chain.NewNode("ns1", "udp://8.8.8.8:53")
|
|
},
|
|
}
|
|
h.exchangers["ns1"] = mockEx
|
|
|
|
conn := &blockingWriteConn{
|
|
readBuf: bytes.NewBuffer(packDNSQuery("timeout.example.com.", dns.TypeA)),
|
|
}
|
|
if err := h.Handle(context.Background(), conn); err == nil {
|
|
t.Fatal("expected error from blocked write")
|
|
}
|
|
}
|
|
|
|
// blockingWriteConn is a net.Conn where Write blocks forever.
|
|
type blockingWriteConn struct {
|
|
readBuf *bytes.Buffer
|
|
}
|
|
|
|
func (c *blockingWriteConn) Read(b []byte) (int, error) { return c.readBuf.Read(b) }
|
|
func (c *blockingWriteConn) Write(b []byte) (int, error) { time.Sleep(200 * time.Millisecond); return 0, errors.New("write deadline exceeded") }
|
|
func (c *blockingWriteConn) Close() error { return nil }
|
|
func (c *blockingWriteConn) LocalAddr() net.Addr { return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53} }
|
|
func (c *blockingWriteConn) RemoteAddr() net.Addr { return &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345} }
|
|
func (c *blockingWriteConn) SetDeadline(t time.Time) error { return nil }
|
|
func (c *blockingWriteConn) SetReadDeadline(t time.Time) error { return nil }
|
|
func (c *blockingWriteConn) SetWriteDeadline(t time.Time) error { return nil }
|