mirror of
https://github.com/AdguardTeam/dnsproxy.git
synced 2026-10-12 02:01:26 +00:00
240 lines
6.6 KiB
Go
240 lines
6.6 KiB
Go
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"math/rand/v2"
|
|
"net"
|
|
"net/netip"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/dnsproxy/internal/dnsproxytest"
|
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
|
"github.com/AdguardTeam/golibs/netutil"
|
|
"github.com/AdguardTeam/golibs/testutil"
|
|
"github.com/AdguardTeam/golibs/testutil/faketime"
|
|
"github.com/AdguardTeam/golibs/timeutil"
|
|
"github.com/miekg/dns"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// newUpstreamWithErrorRate returns an [upstream.Upstream] that responds with an
|
|
// error every [rate] requests. The returned upstream isn't safe for concurrent
|
|
// use.
|
|
func newUpstreamWithErrorRate(rate uint, name string) (u upstream.Upstream) {
|
|
var n uint
|
|
|
|
return &testUpstream{
|
|
OnExchange: func(_ context.Context, req *dns.Msg) (resp *dns.Msg, err error) {
|
|
n++
|
|
if n%rate == 0 {
|
|
return nil, assert.AnError
|
|
}
|
|
|
|
return (&dns.Msg{}).SetReply(req), nil
|
|
},
|
|
OnAddress: func() (addr string) { return name },
|
|
OnClose: func() (_ error) { panic(testutil.UnexpectedCall()) },
|
|
}
|
|
}
|
|
|
|
// measuredUpstream is an [upstream.Upstream] that increments the counter every
|
|
// time it's used.
|
|
type measuredUpstream struct {
|
|
// Upstream is embedded here to avoid implementing all the methods.
|
|
upstream.Upstream
|
|
|
|
// stats is the statistics collector for current upstream.
|
|
stats map[string]int64
|
|
}
|
|
|
|
// type check
|
|
var _ upstream.Upstream = measuredUpstream{}
|
|
|
|
// Exchange implements the [upstream.Upstream] interface for measuredUpstream.
|
|
func (u measuredUpstream) Exchange(ctx context.Context, req *dns.Msg) (resp *dns.Msg, err error) {
|
|
u.stats[u.Address()]++
|
|
|
|
return u.Upstream.Exchange(ctx, req)
|
|
}
|
|
|
|
func TestProxy_Exchange_loadBalance(t *testing.T) {
|
|
// Make the test deterministic.
|
|
randSrc := rand.New(rand.NewPCG(42, 42))
|
|
|
|
const (
|
|
testRTT = 1 * time.Second
|
|
requestsNum = 10_000
|
|
)
|
|
|
|
// zeroingClock returns the value of currentNow and sets it back to
|
|
// zeroTime, so that all the calls since the second one return the same zero
|
|
// value until currentNow is modified elsewhere.
|
|
zeroTime := time.Unix(0, 0)
|
|
currentNow := zeroTime
|
|
zeroingClock := &faketime.Clock{
|
|
OnNow: func() (now time.Time) {
|
|
now, currentNow = currentNow, zeroTime
|
|
|
|
return now
|
|
},
|
|
}
|
|
constClock := &faketime.Clock{
|
|
OnNow: func() (now time.Time) {
|
|
now, currentNow = currentNow, currentNow.Add(testRTT/50)
|
|
|
|
return now
|
|
},
|
|
}
|
|
|
|
fastUps := &testUpstream{
|
|
OnExchange: func(_ context.Context, req *dns.Msg) (resp *dns.Msg, err error) {
|
|
currentNow = zeroTime.Add(testRTT / 100)
|
|
|
|
return (&dns.Msg{}).SetReply(req), nil
|
|
},
|
|
OnAddress: func() (addr string) { return "fast" },
|
|
OnClose: func() (_ error) { panic(testutil.UnexpectedCall()) },
|
|
}
|
|
slowerUps := &testUpstream{
|
|
OnExchange: func(_ context.Context, req *dns.Msg) (resp *dns.Msg, err error) {
|
|
currentNow = zeroTime.Add(testRTT / 10)
|
|
|
|
return (&dns.Msg{}).SetReply(req), nil
|
|
},
|
|
OnAddress: func() (addr string) { return "slower" },
|
|
OnClose: func() (_ error) { panic(testutil.UnexpectedCall()) },
|
|
}
|
|
slowestUps := &testUpstream{
|
|
OnExchange: func(_ context.Context, req *dns.Msg) (resp *dns.Msg, err error) {
|
|
currentNow = zeroTime.Add(testRTT / 2)
|
|
|
|
return (&dns.Msg{}).SetReply(req), nil
|
|
},
|
|
OnAddress: func() (addr string) { return "slowest" },
|
|
OnClose: func() (_ error) { panic(testutil.UnexpectedCall()) },
|
|
}
|
|
|
|
err1Ups := &testUpstream{
|
|
OnExchange: func(_ context.Context, _ *dns.Msg) (r *dns.Msg, err error) {
|
|
return nil, assert.AnError
|
|
},
|
|
OnAddress: func() (addr string) { return "error1" },
|
|
OnClose: func() (_ error) { panic(testutil.UnexpectedCall()) },
|
|
}
|
|
err2Ups := &testUpstream{
|
|
OnExchange: func(_ context.Context, _ *dns.Msg) (r *dns.Msg, err error) {
|
|
return nil, assert.AnError
|
|
},
|
|
OnAddress: func() (addr string) { return "error2" },
|
|
OnClose: func() (_ error) { panic(testutil.UnexpectedCall()) },
|
|
}
|
|
|
|
singleError := &sync.Once{}
|
|
// fastestUps responds with an error on the first request.
|
|
fastestUps := &testUpstream{
|
|
OnExchange: func(_ context.Context, req *dns.Msg) (resp *dns.Msg, err error) {
|
|
singleError.Do(func() { err = assert.AnError })
|
|
currentNow = zeroTime.Add(testRTT / 200)
|
|
|
|
return (&dns.Msg{}).SetReply(req), err
|
|
},
|
|
OnAddress: func() (addr string) { return "fastest" },
|
|
OnClose: func() (_ error) { panic(testutil.UnexpectedCall()) },
|
|
}
|
|
|
|
each200 := newUpstreamWithErrorRate(200, "each_200")
|
|
each100 := newUpstreamWithErrorRate(100, "each_100")
|
|
each50 := newUpstreamWithErrorRate(50, "each_50")
|
|
|
|
testCases := []struct {
|
|
wantStat map[string]int64
|
|
clock timeutil.Clock
|
|
name string
|
|
servers []upstream.Upstream
|
|
}{{
|
|
wantStat: map[string]int64{
|
|
fastUps.Address(): 8910,
|
|
slowerUps.Address(): 902,
|
|
slowestUps.Address(): 188,
|
|
},
|
|
clock: zeroingClock,
|
|
name: "all_good",
|
|
servers: []upstream.Upstream{slowestUps, slowerUps, fastUps},
|
|
}, {
|
|
wantStat: map[string]int64{
|
|
fastUps.Address(): 9110,
|
|
slowerUps.Address(): 890,
|
|
err1Ups.Address(): 6,
|
|
},
|
|
clock: zeroingClock,
|
|
name: "one_bad",
|
|
servers: []upstream.Upstream{fastUps, err1Ups, slowerUps},
|
|
}, {
|
|
wantStat: map[string]int64{
|
|
err1Ups.Address(): requestsNum,
|
|
err2Ups.Address(): requestsNum,
|
|
},
|
|
clock: zeroingClock,
|
|
name: "all_bad",
|
|
servers: []upstream.Upstream{err2Ups, err1Ups},
|
|
}, {
|
|
wantStat: map[string]int64{
|
|
fastUps.Address(): 7222,
|
|
slowerUps.Address(): 748,
|
|
fastestUps.Address(): 2031,
|
|
},
|
|
clock: zeroingClock,
|
|
name: "error_once",
|
|
servers: []upstream.Upstream{fastUps, slowerUps, fastestUps},
|
|
}, {
|
|
wantStat: map[string]int64{
|
|
each200.Address(): 5258,
|
|
each100.Address(): 3142,
|
|
each50.Address(): 1690,
|
|
},
|
|
clock: constClock,
|
|
name: "error_each_nth",
|
|
servers: []upstream.Upstream{each200, each100, each50},
|
|
}}
|
|
|
|
req := dnsproxytest.NewTestRequest()
|
|
cli := netip.AddrPortFrom(netutil.IPv4Localhost(), 1234)
|
|
|
|
for _, tc := range testCases {
|
|
ups := []upstream.Upstream{}
|
|
stats := map[string]int64{}
|
|
for _, s := range tc.servers {
|
|
ups = append(ups, measuredUpstream{
|
|
Upstream: s,
|
|
stats: stats,
|
|
})
|
|
}
|
|
|
|
p := mustNew(t, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{dnsproxytest.LocalhostAnyPortUDP},
|
|
TCPListenAddr: []*net.TCPAddr{dnsproxytest.LocalhostAnyPortTCP},
|
|
UpstreamConfig: &UpstreamConfig{
|
|
Upstreams: ups,
|
|
},
|
|
TrustedProxies: dnsproxytest.DefaultTrustedProxies,
|
|
})
|
|
p.time = tc.clock
|
|
p.randSrc = randSrc
|
|
|
|
wantStat := tc.wantStat
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
for range requestsNum {
|
|
ctx := testutil.ContextWithTimeout(t, defaultTimeout)
|
|
|
|
_ = p.Resolve(ctx, &DNSContext{Req: req, Addr: cli})
|
|
}
|
|
|
|
assert.Equal(t, wantStat, stats)
|
|
})
|
|
}
|
|
}
|