dd76e9f685
- Apply parsed readTimeout as connection read deadline (was dead code) - Use context.WithoutCancel for async cache refresh goroutine - Add doc comments to all exported symbols - Add 40 tests covering metadata parsing, init, rate limiting, exchanger selection, host lookup, cache, bypass, async refresh, and concurrent request handling
1022 lines
29 KiB
Go
1022 lines
29 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 (
|
|
mu sync.Mutex
|
|
exchanged bool
|
|
answerRR = mustNewRR("async.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 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) {
|
|
mu.Lock()
|
|
exchanged = true
|
|
mu.Unlock()
|
|
|
|
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
|
|
time.Sleep(100 * time.Millisecond)
|
|
mu.Lock()
|
|
wasExchanged := exchanged
|
|
mu.Unlock()
|
|
if !wasExchanged {
|
|
t.Error("expected async exchange to occur")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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()
|
|
}
|