mirror of
https://github.com/AdguardTeam/dnsproxy.git
synced 2026-10-12 02:01:26 +00:00
1577 lines
39 KiB
Go
1577 lines
39 KiB
Go
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"math/big"
|
|
"net"
|
|
"net/netip"
|
|
"net/url"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
|
glcache "github.com/AdguardTeam/golibs/cache"
|
|
"github.com/AdguardTeam/golibs/contextutil"
|
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
|
"github.com/AdguardTeam/golibs/netutil"
|
|
"github.com/AdguardTeam/golibs/testutil"
|
|
"github.com/AdguardTeam/golibs/testutil/servicetest"
|
|
"github.com/miekg/dns"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
listenIP = "127.0.0.1"
|
|
testDefaultUpstreamAddr = "8.8.8.8:53"
|
|
tlsServerName = "testdns.adguard.com"
|
|
testMessagesCount = 10
|
|
|
|
// defaultTestTTL used to guarantee caching.
|
|
defaultTestTTL = 1000
|
|
|
|
// testTimeout is the default timeout for tests.
|
|
testTimeout = 500 * time.Millisecond
|
|
|
|
// testOptimisticTTL is a common optimistic cache ttl value for tests.
|
|
testOptimisticTTL = 10 * time.Second
|
|
|
|
// testOptimisticMaxAge is a common optimistic max age value for tests.
|
|
testOptimisticMaxAge = 12 * time.Hour
|
|
)
|
|
|
|
// testLogger is a common logger for tests.
|
|
var testLogger = slogutil.NewDiscardLogger()
|
|
|
|
// localhostAnyPort is a [netip.AddrPort] having a value of 127.0.0.1:0.
|
|
//
|
|
// TODO(e.burkov): Move into the proxytest package.
|
|
var localhostAnyPort = netip.MustParseAddrPort(netutil.JoinHostPort(listenIP, 0))
|
|
|
|
// defaultTrustedProxies is a set of trusted proxies that includes all possible
|
|
// IP addresses.
|
|
//
|
|
// TODO(e.burkov): Move into the proxytest package.
|
|
var defaultTrustedProxies netutil.SubnetSet = netutil.SliceSubnetSet{
|
|
netip.MustParsePrefix("0.0.0.0/0"),
|
|
netip.MustParsePrefix("::0/0"),
|
|
}
|
|
|
|
// mustNew wraps [New] function failing the test on error.
|
|
//
|
|
// TODO(e.burkov): Move into the proxytest package.
|
|
//
|
|
// TODO(e.burkov): Put default values into configuration.
|
|
func mustNew(tb testing.TB, conf *Config) (p *Proxy) {
|
|
tb.Helper()
|
|
|
|
p, err := New(conf)
|
|
require.NoError(tb, err)
|
|
|
|
return p
|
|
}
|
|
|
|
// sendTestMessages sends [testMessagesCount] DNS requests to the specified
|
|
// connection and checks the responses.
|
|
func sendTestMessages(tb testing.TB, conn *dns.Conn) {
|
|
tb.Helper()
|
|
|
|
for i := range testMessagesCount {
|
|
req := newTestMessage()
|
|
err := conn.WriteMsg(req)
|
|
require.NoErrorf(tb, err, "req number %d", i)
|
|
|
|
res, err := conn.ReadMsg()
|
|
require.NoErrorf(tb, err, "resp number %d", i)
|
|
|
|
requireResponse(tb, req, res)
|
|
}
|
|
}
|
|
|
|
func newTestMessage() *dns.Msg {
|
|
return newHostTestMessage("google-public-dns-a.google.com")
|
|
}
|
|
|
|
func newHostTestMessage(host string) (req *dns.Msg) {
|
|
return &dns.Msg{
|
|
MsgHdr: dns.MsgHdr{
|
|
Id: dns.Id(),
|
|
RecursionDesired: true,
|
|
},
|
|
Question: []dns.Question{{
|
|
Name: host + ".",
|
|
Qtype: dns.TypeA,
|
|
Qclass: dns.ClassINET,
|
|
}},
|
|
}
|
|
}
|
|
|
|
func requireResponse(tb testing.TB, req, reply *dns.Msg) {
|
|
tb.Helper()
|
|
|
|
require.NotNil(tb, reply)
|
|
require.Lenf(tb, reply.Answer, 1, "wrong number of answers: %d", len(reply.Answer))
|
|
require.Equal(tb, req.Id, reply.Id)
|
|
|
|
a := testutil.RequireTypeAssert[*dns.A](tb, reply.Answer[0])
|
|
|
|
require.Equalf(tb, net.IPv4(8, 8, 8, 8), a.A.To16(), "wrong answer: %v", a.A)
|
|
}
|
|
|
|
func newTLSConfig(tb testing.TB) (conf *tls.Config, certPem []byte) {
|
|
tb.Helper()
|
|
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
require.NoError(tb, err)
|
|
|
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
require.NoError(tb, err)
|
|
|
|
notBefore := time.Now()
|
|
notAfter := notBefore.Add(5 * 365 * time.Hour * 24)
|
|
|
|
keyUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign
|
|
template := x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
Subject: pkix.Name{Organization: []string{"AdGuard Tests"}},
|
|
NotBefore: notBefore,
|
|
NotAfter: notAfter,
|
|
KeyUsage: keyUsage,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
IsCA: true,
|
|
DNSNames: []string{tlsServerName},
|
|
}
|
|
|
|
derBytes, err := x509.CreateCertificate(
|
|
rand.Reader,
|
|
&template,
|
|
&template,
|
|
&privateKey.PublicKey,
|
|
privateKey,
|
|
)
|
|
require.NoError(tb, err)
|
|
|
|
certPem = pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: derBytes,
|
|
})
|
|
keyPem := pem.EncodeToMemory(&pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
|
})
|
|
|
|
cert, err := tls.X509KeyPair(certPem, keyPem)
|
|
require.NoError(tb, err)
|
|
|
|
return &tls.Config{Certificates: []tls.Certificate{cert}, ServerName: tlsServerName}, certPem
|
|
}
|
|
|
|
// firstIP returns the first IP address from the DNS response.
|
|
func firstIP(resp *dns.Msg) (ip net.IP) {
|
|
for _, ans := range resp.Answer {
|
|
a, ok := ans.(*dns.A)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
return a.A
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// newTestUpstreamConfigWithBoot creates a new UpstreamConfig with upstream
|
|
// addresses and a bootstrapped resolver.
|
|
func newTestUpstreamConfigWithBoot(
|
|
t require.TestingT,
|
|
timeout time.Duration,
|
|
addrs ...string,
|
|
) (u *UpstreamConfig) {
|
|
googleRslv, err := upstream.NewUpstreamResolver(
|
|
"8.8.8.8:53",
|
|
&upstream.Options{
|
|
Logger: testLogger,
|
|
Timeout: timeout,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
upsConf, err := ParseUpstreamsConfig(addrs, &upstream.Options{
|
|
Logger: testLogger,
|
|
Timeout: timeout,
|
|
Bootstrap: upstream.NewCachingResolver(googleRslv),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return upsConf
|
|
}
|
|
|
|
// newTestUpstreamConfig creates a new UpstreamConfig with a single upstream
|
|
// address and default timeout.
|
|
func newTestUpstreamConfig(
|
|
tb testing.TB,
|
|
timeout time.Duration,
|
|
addrs ...string,
|
|
) (u *UpstreamConfig) {
|
|
tb.Helper()
|
|
|
|
upsConf, err := ParseUpstreamsConfig(addrs, &upstream.Options{
|
|
Logger: testLogger,
|
|
Timeout: timeout,
|
|
})
|
|
require.NoError(tb, err)
|
|
|
|
return upsConf
|
|
}
|
|
|
|
// mustStartDefaultProxy starts a new proxy with default settings and returns
|
|
// it. It fails the test on error.
|
|
func mustStartDefaultProxy(tb testing.TB) (p *Proxy) {
|
|
tb.Helper()
|
|
|
|
p = mustNew(tb, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: newTestUpstreamConfig(tb, defaultTimeout, testDefaultUpstreamAddr),
|
|
TrustedProxies: defaultTrustedProxies,
|
|
})
|
|
|
|
servicetest.RequireRun(tb, p, testTimeout)
|
|
|
|
return p
|
|
}
|
|
|
|
// TestProxyRace sends multiple parallel DNS requests to the
|
|
// fully configured dnsproxy to check for race conditions
|
|
func TestProxyRace(t *testing.T) {
|
|
upsConf := newTestUpstreamConfig(
|
|
t,
|
|
defaultTimeout,
|
|
// Use the same upstream twice so that we could rotate them
|
|
testDefaultUpstreamAddr,
|
|
testDefaultUpstreamAddr,
|
|
)
|
|
dnsProxy := mustNew(t, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: upsConf,
|
|
TrustedProxies: defaultTrustedProxies,
|
|
})
|
|
|
|
servicetest.RequireRun(t, dnsProxy, testTimeout)
|
|
|
|
// Create a DNS-over-UDP client connection
|
|
addr := dnsProxy.Addr(ProtoUDP)
|
|
conn, err := dns.Dial("udp", addr.String())
|
|
require.NoError(t, err)
|
|
|
|
g := &sync.WaitGroup{}
|
|
g.Add(testMessagesCount)
|
|
|
|
pt := testutil.PanicT{}
|
|
for range testMessagesCount {
|
|
go func() {
|
|
defer g.Done()
|
|
|
|
req := newTestMessage()
|
|
writeErr := conn.WriteMsg(req)
|
|
require.NoError(pt, writeErr)
|
|
|
|
res, readErr := conn.ReadMsg()
|
|
require.NoError(pt, readErr)
|
|
|
|
// We do not check if msg IDs match because the order of responses may
|
|
// be different.
|
|
|
|
require.NotNil(pt, res)
|
|
require.Len(pt, res.Answer, 1)
|
|
require.IsType(pt, &dns.A{}, res.Answer[0])
|
|
|
|
a := res.Answer[0].(*dns.A)
|
|
require.Equal(pt, net.IPv4(8, 8, 8, 8), a.A.To16())
|
|
}()
|
|
}
|
|
|
|
g.Wait()
|
|
}
|
|
|
|
// newTxts returns new test TXT RR strings.
|
|
func newTxts(tb testing.TB, txtDataLen int) (txts []string) {
|
|
tb.Helper()
|
|
|
|
const txtDataChunkLen = 255
|
|
|
|
txtDataChunkNum := txtDataLen / txtDataChunkLen
|
|
if txtDataLen%txtDataChunkLen > 0 {
|
|
txtDataChunkNum++
|
|
}
|
|
|
|
txts = make([]string, txtDataChunkNum)
|
|
randData := make([]byte, txtDataLen)
|
|
n, err := rand.Read(randData)
|
|
require.NoError(tb, err)
|
|
require.Equal(tb, txtDataLen, n)
|
|
|
|
for i, c := range randData {
|
|
randData[i] = c%26 + 'a'
|
|
}
|
|
|
|
// *dns.TXT requires splitting the actual data into 256-byte chunks.
|
|
for i := range txtDataChunkNum {
|
|
r := min(txtDataChunkLen*(i+1), txtDataLen)
|
|
txts[i] = string(randData[txtDataChunkLen*i : r])
|
|
}
|
|
|
|
return txts
|
|
}
|
|
|
|
// newDNSContext returns new DNS request message context with Proto set to
|
|
// [ProtoUDP]. Constructs request message from the given parameters.
|
|
func newDNSContext(
|
|
domain string,
|
|
qtype uint16,
|
|
qclass uint16,
|
|
edns bool,
|
|
udpsize uint16,
|
|
) (dctx *DNSContext) {
|
|
req := newReq(domain, qtype, qclass)
|
|
if edns {
|
|
req.SetEdns0(udpsize, true)
|
|
}
|
|
|
|
return &DNSContext{
|
|
Req: req,
|
|
Proto: ProtoUDP,
|
|
}
|
|
}
|
|
|
|
// newReq returns new request message for provided parameters.
|
|
func newReq(domain string, qtype, qclass uint16) (req *dns.Msg) {
|
|
return &dns.Msg{
|
|
MsgHdr: dns.MsgHdr{
|
|
Id: dns.Id(),
|
|
},
|
|
Compress: true,
|
|
Question: []dns.Question{{
|
|
Name: dns.Fqdn(domain),
|
|
Qtype: qtype,
|
|
Qclass: qclass,
|
|
}},
|
|
}
|
|
}
|
|
|
|
func TestProxy_Resolve_dnssecCache(t *testing.T) {
|
|
const (
|
|
host = "example.com"
|
|
|
|
// Larger than UDP buffer size to invoke truncation.
|
|
txtDataLen = 1024
|
|
)
|
|
|
|
txt := &dns.TXT{
|
|
Hdr: dns.RR_Header{
|
|
Name: dns.Fqdn(host),
|
|
Rrtype: dns.TypeTXT,
|
|
Class: dns.ClassINET,
|
|
},
|
|
Txt: newTxts(t, txtDataLen),
|
|
}
|
|
|
|
a := &dns.A{
|
|
Hdr: dns.RR_Header{
|
|
Name: dns.Fqdn(host),
|
|
Rrtype: dns.TypeA,
|
|
Class: dns.ClassINET,
|
|
},
|
|
A: net.IP{1, 2, 3, 4},
|
|
}
|
|
|
|
ds := &dns.DS{
|
|
Hdr: dns.RR_Header{
|
|
Name: dns.Fqdn(host),
|
|
Rrtype: dns.TypeDS,
|
|
Class: dns.ClassINET,
|
|
},
|
|
Digest: "736f6d652064656c65676174696f6e207369676e6572",
|
|
}
|
|
|
|
rrsig := &dns.RRSIG{
|
|
Hdr: dns.RR_Header{
|
|
Name: dns.Fqdn(host),
|
|
Rrtype: dns.TypeRRSIG,
|
|
Class: dns.ClassINET,
|
|
Ttl: defaultTestTTL,
|
|
},
|
|
TypeCovered: dns.TypeA,
|
|
Algorithm: 8,
|
|
Labels: 2,
|
|
SignerName: dns.Fqdn(host),
|
|
Signature: "c29tZSBycnNpZyByZWxhdGVkIHN0dWZm",
|
|
}
|
|
|
|
u := &testUpstream{
|
|
OnExchange: func(m *dns.Msg) (resp *dns.Msg, err error) {
|
|
resp = (&dns.Msg{}).SetReply(m)
|
|
|
|
q := m.Question[0]
|
|
switch q.Qtype {
|
|
case dns.TypeA:
|
|
resp.Answer = append(resp.Answer, a)
|
|
case dns.TypeTXT:
|
|
resp.Answer = append(resp.Answer, txt)
|
|
case dns.TypeDS:
|
|
resp.Answer = append(resp.Answer, ds)
|
|
default:
|
|
// Go on. The RRSIG resource record is added afterward. This
|
|
// upstream.Upstream implementation doesn't handle explicit
|
|
// requests for it.
|
|
}
|
|
|
|
if len(resp.Answer) > 0 {
|
|
resp.Answer[0].Header().Ttl = defaultTestTTL
|
|
}
|
|
|
|
if o := m.IsEdns0(); o != nil {
|
|
resp.Answer = append(resp.Answer, rrsig)
|
|
resp.SetEdns0(defaultUDPBufSize, o.Do())
|
|
}
|
|
|
|
return resp, nil
|
|
},
|
|
OnAddress: func() (addr string) { return "" },
|
|
OnClose: func() (err error) { return nil },
|
|
}
|
|
|
|
p := mustNew(t, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: &UpstreamConfig{Upstreams: []upstream.Upstream{u}},
|
|
TrustedProxies: defaultTrustedProxies,
|
|
CacheEnabled: true,
|
|
DNSSECEnabled: true,
|
|
CacheSizeBytes: defaultCacheSize,
|
|
})
|
|
|
|
testCases := []struct {
|
|
wantAns dns.RR
|
|
name string
|
|
wantLen int
|
|
edns bool
|
|
}{{
|
|
wantAns: a,
|
|
name: "a_noedns",
|
|
wantLen: 1,
|
|
edns: false,
|
|
}, {
|
|
wantAns: a,
|
|
name: "a_ends",
|
|
wantLen: 2,
|
|
edns: true,
|
|
}, {
|
|
wantAns: txt,
|
|
name: "txt_noedns",
|
|
wantLen: 1,
|
|
edns: false,
|
|
}, {
|
|
wantAns: txt,
|
|
name: "txt_edns",
|
|
// Truncated.
|
|
wantLen: 0,
|
|
edns: true,
|
|
}, {
|
|
wantAns: ds,
|
|
name: "ds_noedns",
|
|
wantLen: 1,
|
|
edns: false,
|
|
}, {
|
|
wantAns: ds,
|
|
name: "ds_edns",
|
|
wantLen: 2,
|
|
edns: true,
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
ansHdr := tc.wantAns.Header()
|
|
dctx := newDNSContext(ansHdr.Name, ansHdr.Rrtype, ansHdr.Class, tc.edns, txtDataLen/2)
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Cleanup(p.cache.items.Clear)
|
|
|
|
err := p.Resolve(testutil.ContextWithTimeout(t, defaultTimeout), dctx)
|
|
require.NoError(t, err)
|
|
|
|
res := dctx.Res
|
|
require.NotNil(t, res)
|
|
|
|
require.Len(t, res.Answer, tc.wantLen, res.Answer)
|
|
switch tc.wantLen {
|
|
case 0:
|
|
assert.True(t, res.Truncated)
|
|
case 1:
|
|
res.Answer[0].Header().Ttl = defaultTestTTL
|
|
assert.Equal(t, tc.wantAns, res.Answer[0])
|
|
case 2:
|
|
res.Answer[0].Header().Ttl = defaultTestTTL
|
|
assert.Equal(t, tc.wantAns, res.Answer[0])
|
|
assert.Equal(t, rrsig, res.Answer[1])
|
|
default:
|
|
t.Fatalf("wanted length has unexpected value %d", tc.wantLen)
|
|
}
|
|
|
|
cached, expired, key := p.cache.get(dctx.Req)
|
|
require.NotNil(t, cached)
|
|
require.Len(t, cached.m.Answer, 2)
|
|
|
|
assert.False(t, expired)
|
|
assert.Equal(t, key, msgToKey(dctx.Req))
|
|
|
|
// Just make it match.
|
|
cached.m.Answer[0].Header().Ttl = defaultTestTTL
|
|
assert.Equal(t, tc.wantAns.String(), cached.m.Answer[0].String())
|
|
assert.Equal(t, rrsig.String(), cached.m.Answer[1].String())
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func TestExchangeWithReservedDomains(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dnsProxy := mustNew(t, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: newTestUpstreamConfigWithBoot(
|
|
t,
|
|
testTimeout,
|
|
"[/adguard.com/]192.0.2.1",
|
|
"[/google.ru/]192.0.2.2",
|
|
"[/maps.google.ru/]#",
|
|
"tls://1.1.1.1",
|
|
),
|
|
TrustedProxies: defaultTrustedProxies,
|
|
})
|
|
|
|
servicetest.RequireRun(t, dnsProxy, testTimeout)
|
|
|
|
// Create a DNS-over-TCP client connection.
|
|
addr := dnsProxy.Addr(ProtoTCP)
|
|
conn, err := dns.Dial("tcp", addr.String())
|
|
require.NoError(t, err)
|
|
|
|
// Create google-a test message.
|
|
req := newTestMessage()
|
|
err = conn.WriteMsg(req)
|
|
require.NoError(t, err)
|
|
|
|
// Make sure that dnsproxy is working.
|
|
res, err := conn.ReadMsg()
|
|
require.NoError(t, err)
|
|
requireResponse(t, req, res)
|
|
|
|
// Create adguard.com test message.
|
|
req = newHostTestMessage("adguard.com")
|
|
err = conn.WriteMsg(req)
|
|
require.NoError(t, err)
|
|
|
|
// Test message should not be resolved.
|
|
res, _ = conn.ReadMsg()
|
|
require.Nil(t, res.Answer)
|
|
|
|
// Create www.google.ru test message.
|
|
req = newHostTestMessage("www.google.ru")
|
|
err = conn.WriteMsg(req)
|
|
require.NoError(t, err)
|
|
|
|
// Test message should not be resolved.
|
|
res, _ = conn.ReadMsg()
|
|
require.Empty(t, res.Answer)
|
|
|
|
// Create maps.google.ru test message.
|
|
req = newHostTestMessage("maps.google.ru")
|
|
err = conn.WriteMsg(req)
|
|
require.NoError(t, err)
|
|
|
|
// Test message should be resolved.
|
|
res, _ = conn.ReadMsg()
|
|
require.NotNil(t, res.Answer)
|
|
}
|
|
|
|
// TestOneByOneUpstreamsExchange tries to resolve DNS request
|
|
// with one valid and two invalid upstreams
|
|
func TestOneByOneUpstreamsExchange(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dnsProxy := mustNew(t, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: newTestUpstreamConfigWithBoot(
|
|
t,
|
|
testTimeout,
|
|
"https://fake-dns.com/fake-dns-query",
|
|
"tls://fake-dns.com",
|
|
"1.1.1.1",
|
|
),
|
|
TrustedProxies: defaultTrustedProxies,
|
|
Fallbacks: newTestUpstreamConfig(t, testTimeout, "1.2.3.4:567"),
|
|
})
|
|
|
|
servicetest.RequireRun(t, dnsProxy, testTimeout)
|
|
|
|
// create a DNS-over-TCP client connection
|
|
addr := dnsProxy.Addr(ProtoTCP)
|
|
conn, err := dns.Dial("tcp", addr.String())
|
|
require.NoError(t, err)
|
|
|
|
// make sure that the response is okay and resolved by valid upstream
|
|
req := newTestMessage()
|
|
err = conn.WriteMsg(req)
|
|
require.NoError(t, err)
|
|
|
|
start := time.Now()
|
|
res, err := conn.ReadMsg()
|
|
require.NoError(t, err)
|
|
requireResponse(t, req, res)
|
|
|
|
elapsed := time.Since(start)
|
|
assert.Greater(t, 3*testTimeout, elapsed)
|
|
}
|
|
|
|
// newLocalUpstreamListener creates a new localhost listener on the specified
|
|
// port for tcp4 network and returns its listening address.
|
|
func newLocalUpstreamListener(tb testing.TB, port uint16, h dns.Handler) (real netip.AddrPort) {
|
|
tb.Helper()
|
|
|
|
startCh := make(chan struct{})
|
|
upsSrv := &dns.Server{
|
|
Addr: netip.AddrPortFrom(netutil.IPv4Localhost(), port).String(),
|
|
Net: "tcp",
|
|
Handler: h,
|
|
NotifyStartedFunc: func() { close(startCh) },
|
|
}
|
|
go func() {
|
|
err := upsSrv.ListenAndServe()
|
|
require.NoError(testutil.PanicT{}, err)
|
|
}()
|
|
|
|
<-startCh
|
|
testutil.CleanupAndRequireSuccess(tb, upsSrv.Shutdown)
|
|
|
|
return testutil.RequireTypeAssert[*net.TCPAddr](tb, upsSrv.Listener.Addr()).AddrPort()
|
|
}
|
|
|
|
func TestFallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
responseCh := make(chan uint16)
|
|
failCh := make(chan uint16)
|
|
|
|
successHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
|
testutil.RequireSend(testutil.PanicT{}, responseCh, r.Id, testTimeout)
|
|
|
|
require.NoError(testutil.PanicT{}, w.WriteMsg((&dns.Msg{}).SetReply(r)))
|
|
})
|
|
failHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
|
testutil.RequireSend(testutil.PanicT{}, failCh, r.Id, testTimeout)
|
|
|
|
require.NoError(testutil.PanicT{}, w.WriteMsg(&dns.Msg{}))
|
|
})
|
|
|
|
successAddr := (&url.URL{
|
|
Scheme: string(ProtoTCP),
|
|
Host: newLocalUpstreamListener(t, 0, successHandler).String(),
|
|
}).String()
|
|
alsoSuccessAddr := (&url.URL{
|
|
Scheme: string(ProtoTCP),
|
|
Host: newLocalUpstreamListener(t, 0, successHandler).String(),
|
|
}).String()
|
|
failAddr := (&url.URL{
|
|
Scheme: string(ProtoTCP),
|
|
Host: newLocalUpstreamListener(t, 0, failHandler).String(),
|
|
}).String()
|
|
|
|
dnsProxy := mustNew(t, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: newTestUpstreamConfig(
|
|
t,
|
|
testTimeout,
|
|
failAddr,
|
|
"[/specific.example/]"+alsoSuccessAddr,
|
|
// almost.failing.example will fall here first.
|
|
"[/failing.example/]"+failAddr,
|
|
),
|
|
TrustedProxies: defaultTrustedProxies,
|
|
Fallbacks: newTestUpstreamConfig(
|
|
t,
|
|
testTimeout,
|
|
failAddr,
|
|
successAddr,
|
|
"[/failing.example/]"+failAddr,
|
|
"[/almost.failing.example/]"+alsoSuccessAddr,
|
|
),
|
|
})
|
|
|
|
servicetest.RequireRun(t, dnsProxy, testTimeout)
|
|
|
|
conn, err := dns.Dial("tcp", dnsProxy.Addr(ProtoTCP).String())
|
|
require.NoError(t, err)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
wantSignals []chan uint16
|
|
}{{
|
|
name: "general.example",
|
|
wantSignals: []chan uint16{
|
|
failCh,
|
|
// Both non-specific fallbacks tried.
|
|
failCh,
|
|
responseCh,
|
|
},
|
|
}, {
|
|
name: "specific.example",
|
|
wantSignals: []chan uint16{
|
|
responseCh,
|
|
},
|
|
}, {
|
|
name: "failing.example",
|
|
wantSignals: []chan uint16{
|
|
failCh,
|
|
failCh,
|
|
},
|
|
}, {
|
|
name: "almost.failing.example",
|
|
wantSignals: []chan uint16{
|
|
failCh,
|
|
responseCh,
|
|
},
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := newHostTestMessage(tc.name)
|
|
err = conn.WriteMsg(req)
|
|
require.NoError(t, err)
|
|
|
|
for _, ch := range tc.wantSignals {
|
|
reqID, ok := testutil.RequireReceive(testutil.PanicT{}, ch, testTimeout)
|
|
require.True(t, ok)
|
|
|
|
assert.Equal(t, req.Id, reqID)
|
|
}
|
|
|
|
_, err = conn.ReadMsg()
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFallbackFromInvalidBootstrap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
invalidRslv, err := upstream.NewUpstreamResolver("8.8.8.8:555", &upstream.Options{
|
|
Logger: testLogger,
|
|
Timeout: testTimeout,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Prepare the proxy server
|
|
upsConf, err := ParseUpstreamsConfig([]string{"tls://dns.adguard.com"}, &upstream.Options{
|
|
Logger: testLogger,
|
|
Bootstrap: invalidRslv, Timeout: testTimeout,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
dnsProxy := mustNew(t, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: upsConf,
|
|
TrustedProxies: defaultTrustedProxies,
|
|
Fallbacks: newTestUpstreamConfig(
|
|
t,
|
|
testTimeout,
|
|
"1.0.0.1",
|
|
"8.8.8.8",
|
|
),
|
|
})
|
|
|
|
servicetest.RequireRun(t, dnsProxy, testTimeout)
|
|
|
|
// Create a DNS-over-UDP client connection
|
|
addr := dnsProxy.Addr(ProtoUDP)
|
|
conn, err := dns.Dial("udp", addr.String())
|
|
require.NoError(t, err)
|
|
|
|
// Make sure that the response is okay and resolved by the fallback
|
|
req := newTestMessage()
|
|
err = conn.WriteMsg(req)
|
|
require.NoError(t, err)
|
|
|
|
start := time.Now()
|
|
res, err := conn.ReadMsg()
|
|
require.NoError(t, err)
|
|
requireResponse(t, req, res)
|
|
|
|
elapsed := time.Since(start)
|
|
assert.Greater(t, 3*testTimeout, elapsed)
|
|
}
|
|
|
|
// Server must drop incoming Response messages
|
|
func TestResponseInRequest(t *testing.T) {
|
|
dnsProxy := mustStartDefaultProxy(t)
|
|
|
|
addr := dnsProxy.Addr(ProtoUDP)
|
|
client := &dns.Client{
|
|
Net: string(ProtoUDP),
|
|
Timeout: testTimeout,
|
|
}
|
|
|
|
req := newTestMessage()
|
|
req.Response = true
|
|
|
|
r, _, err := client.Exchange(req, addr.String())
|
|
|
|
netErr := &net.OpError{}
|
|
require.ErrorAs(t, err, &netErr)
|
|
assert.True(t, netErr.Timeout())
|
|
assert.Nil(t, r)
|
|
}
|
|
|
|
func TestProxy_ReplyFromUpstream_badResponse(t *testing.T) {
|
|
dnsProxy := mustStartDefaultProxy(t)
|
|
|
|
u := &testUpstream{
|
|
OnExchange: func(m *dns.Msg) (resp *dns.Msg, err error) {
|
|
resp = (&dns.Msg{}).SetReply(m)
|
|
resp.Answer = append(resp.Answer, &dns.A{
|
|
Hdr: dns.RR_Header{
|
|
Name: m.Question[0].Name,
|
|
Class: dns.ClassINET,
|
|
Rrtype: dns.TypeA,
|
|
},
|
|
A: net.IP{1, 2, 3, 4},
|
|
})
|
|
// Make the response invalid.
|
|
resp.Question = []dns.Question{}
|
|
|
|
return resp, nil
|
|
},
|
|
OnAddress: func() (addr string) { return "stub" },
|
|
OnClose: func() (_ error) { panic(testutil.UnexpectedCall()) },
|
|
}
|
|
|
|
d := &DNSContext{
|
|
CustomUpstreamConfig: NewCustomUpstreamConfig(
|
|
&UpstreamConfig{Upstreams: []upstream.Upstream{u}},
|
|
false,
|
|
0,
|
|
false,
|
|
),
|
|
Req: newHostTestMessage("host"),
|
|
Addr: netip.MustParseAddrPort("1.2.3.0:1234"),
|
|
}
|
|
|
|
var err error
|
|
require.NotPanics(t, func() {
|
|
err = dnsProxy.Resolve(testutil.ContextWithTimeout(t, defaultTimeout), d)
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, d.Req.Question[0], d.Res.Question[0])
|
|
}
|
|
|
|
func TestExchangeCustomUpstreamConfig(t *testing.T) {
|
|
prx := mustStartDefaultProxy(t)
|
|
|
|
ansIP := net.IP{4, 3, 2, 1}
|
|
ans := []dns.RR{&dns.A{
|
|
Hdr: dns.RR_Header{
|
|
Rrtype: dns.TypeA,
|
|
Name: "host.",
|
|
Ttl: 60,
|
|
},
|
|
A: ansIP,
|
|
}}
|
|
|
|
onExchange := newECSReplyHandler(&ans, nil, nil)
|
|
u := newTestECSUpstream(onExchange)
|
|
|
|
d := &DNSContext{
|
|
CustomUpstreamConfig: NewCustomUpstreamConfig(
|
|
&UpstreamConfig{Upstreams: []upstream.Upstream{u}},
|
|
false,
|
|
0,
|
|
false,
|
|
),
|
|
Req: newHostTestMessage("host"),
|
|
Addr: netip.MustParseAddrPort("1.2.3.0:1234"),
|
|
}
|
|
|
|
err := prx.Resolve(testutil.ContextWithTimeout(t, defaultTimeout), d)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, ansIP, firstIP(d.Res))
|
|
}
|
|
|
|
func TestExchangeCustomUpstreamConfigCache(t *testing.T) {
|
|
prx := mustNew(t, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: newTestUpstreamConfig(t, defaultTimeout, testDefaultUpstreamAddr),
|
|
TrustedProxies: defaultTrustedProxies,
|
|
CacheEnabled: true,
|
|
DNSSECEnabled: true,
|
|
})
|
|
|
|
servicetest.RequireRun(t, prx, testTimeout)
|
|
|
|
var count int
|
|
|
|
ansIP := net.IP{4, 3, 2, 1}
|
|
exchangeFunc := func(m *dns.Msg) (resp *dns.Msg, err error) {
|
|
resp = &dns.Msg{}
|
|
resp.SetReply(m)
|
|
resp.Answer = append(resp.Answer, &dns.A{
|
|
Hdr: dns.RR_Header{
|
|
Name: m.Question[0].Name,
|
|
Class: dns.ClassINET,
|
|
Rrtype: dns.TypeA,
|
|
Ttl: defaultTestTTL,
|
|
},
|
|
A: ansIP,
|
|
})
|
|
|
|
count++
|
|
|
|
return resp, nil
|
|
}
|
|
u := &testUpstream{
|
|
OnExchange: exchangeFunc,
|
|
OnAddress: func() (addr string) { return "stub" },
|
|
OnClose: func() (_ error) { panic(testutil.UnexpectedCall()) },
|
|
}
|
|
|
|
customUpstreamConfig := NewCustomUpstreamConfig(
|
|
&UpstreamConfig{Upstreams: []upstream.Upstream{u}},
|
|
true,
|
|
defaultCacheSize,
|
|
prx.enableEDNSClientSubnet,
|
|
)
|
|
|
|
d := &DNSContext{
|
|
CustomUpstreamConfig: customUpstreamConfig,
|
|
Req: newHostTestMessage("host"),
|
|
Addr: netip.MustParseAddrPort("1.2.3.0:1234"),
|
|
}
|
|
|
|
ctx := testutil.ContextWithTimeout(t, defaultTimeout)
|
|
err := prx.Resolve(ctx, d)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, count)
|
|
assert.Equal(t, ansIP, firstIP(d.Res))
|
|
|
|
err = prx.Resolve(ctx, d)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 1, count)
|
|
assert.Equal(t, ansIP, firstIP(d.Res))
|
|
|
|
customUpstreamConfig.ClearCache()
|
|
|
|
err = prx.Resolve(ctx, d)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 2, count)
|
|
assert.Equal(t, ansIP, firstIP(d.Res))
|
|
}
|
|
|
|
func TestECS(t *testing.T) {
|
|
t.Run("ipv4", func(t *testing.T) {
|
|
ip := net.IP{1, 2, 3, 4}
|
|
|
|
m := &dns.Msg{}
|
|
subnet := setECS(m, ip, 16)
|
|
|
|
ones, _ := subnet.Mask.Size()
|
|
assert.Equal(t, 24, ones)
|
|
|
|
var scope int
|
|
subnet, scope = ecsFromMsg(m)
|
|
assert.Equal(t, ip.Mask(subnet.Mask), subnet.IP)
|
|
|
|
ones, _ = subnet.Mask.Size()
|
|
assert.Equal(t, 24, ones)
|
|
assert.Equal(t, 16, scope)
|
|
})
|
|
|
|
t.Run("ipv6", func(t *testing.T) {
|
|
ip := net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
|
|
|
m := &dns.Msg{}
|
|
subnet := setECS(m, ip, 48)
|
|
|
|
ones, _ := subnet.Mask.Size()
|
|
assert.Equal(t, 56, ones)
|
|
|
|
var scope int
|
|
subnet, scope = ecsFromMsg(m)
|
|
assert.Equal(t, ip.Mask(subnet.Mask), subnet.IP)
|
|
|
|
ones, _ = subnet.Mask.Size()
|
|
assert.Equal(t, 56, ones)
|
|
assert.Equal(t, 48, scope)
|
|
})
|
|
}
|
|
|
|
// Resolve the same host with the different client subnet values
|
|
func TestECSProxy(t *testing.T) {
|
|
var (
|
|
ip1230 = net.IP{1, 2, 3, 0}
|
|
ip2230 = net.IP{2, 2, 3, 0}
|
|
ip4321 = net.IP{4, 3, 2, 1}
|
|
ip4322 = net.IP{4, 3, 2, 2}
|
|
ip4323 = net.IP{4, 3, 2, 3}
|
|
)
|
|
|
|
var (
|
|
ans []dns.RR
|
|
ecsIP net.IP
|
|
ecsReqIP net.IP
|
|
)
|
|
|
|
onExchange := newECSReplyHandler(&ans, &ecsIP, &ecsReqIP)
|
|
u := newTestECSUpstream(onExchange)
|
|
|
|
ans = []dns.RR{&dns.A{
|
|
Hdr: dns.RR_Header{Rrtype: dns.TypeA, Name: "host.", Ttl: 60},
|
|
A: ip4321,
|
|
}}
|
|
ecsIP = ip1230
|
|
|
|
prx := mustNew(t, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: &UpstreamConfig{
|
|
Upstreams: []upstream.Upstream{u},
|
|
},
|
|
TrustedProxies: defaultTrustedProxies,
|
|
DNSSECEnabled: true,
|
|
EnableEDNSClientSubnet: true,
|
|
CacheEnabled: true,
|
|
})
|
|
|
|
servicetest.RequireRun(t, prx, testTimeout)
|
|
|
|
t.Run("cache_subnet", func(t *testing.T) {
|
|
d := &DNSContext{
|
|
Req: newHostTestMessage("host"),
|
|
Addr: netip.MustParseAddrPort("1.2.3.0:1234"),
|
|
}
|
|
|
|
ctx := testutil.ContextWithTimeout(t, defaultTimeout)
|
|
err := prx.Resolve(ctx, d)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, net.IP{4, 3, 2, 1}, firstIP(d.Res))
|
|
assert.Equal(t, ip1230, ecsReqIP)
|
|
})
|
|
|
|
t.Run("serve_subnet_cache", func(t *testing.T) {
|
|
d := &DNSContext{
|
|
Req: newHostTestMessage("host"),
|
|
Addr: netip.MustParseAddrPort("1.2.3.1:1234"),
|
|
}
|
|
ans, ecsIP = nil, nil
|
|
ecsReqIP = nil
|
|
|
|
ctx := testutil.ContextWithTimeout(t, defaultTimeout)
|
|
err := prx.Resolve(ctx, d)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, ip4321, firstIP(d.Res))
|
|
assert.Nil(t, ecsReqIP)
|
|
})
|
|
|
|
t.Run("another_subnet", func(t *testing.T) {
|
|
d := &DNSContext{
|
|
Req: newHostTestMessage("host"),
|
|
Addr: netip.MustParseAddrPort("2.2.3.0:1234"),
|
|
}
|
|
ans = []dns.RR{&dns.A{
|
|
Hdr: dns.RR_Header{Rrtype: dns.TypeA, Name: "host.", Ttl: 60},
|
|
A: ip4322,
|
|
}}
|
|
ecsIP = ip2230
|
|
|
|
ctx := testutil.ContextWithTimeout(t, defaultTimeout)
|
|
err := prx.Resolve(ctx, d)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, ip4322, firstIP(d.Res))
|
|
assert.Equal(t, ip2230, ecsReqIP)
|
|
})
|
|
|
|
t.Run("cache_general", func(t *testing.T) {
|
|
d := &DNSContext{
|
|
Req: newHostTestMessage("host"),
|
|
Addr: netip.MustParseAddrPort("127.0.0.1:1234"),
|
|
}
|
|
ans = []dns.RR{&dns.A{
|
|
Hdr: dns.RR_Header{Rrtype: dns.TypeA, Name: "host.", Ttl: 60},
|
|
A: ip4323,
|
|
}}
|
|
ecsIP = nil
|
|
ecsReqIP = nil
|
|
|
|
ctx := testutil.ContextWithTimeout(t, defaultTimeout)
|
|
err := prx.Resolve(ctx, d)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, ip4323, firstIP(d.Res))
|
|
assert.Nil(t, ecsReqIP)
|
|
})
|
|
|
|
t.Run("serve_general_cache", func(t *testing.T) {
|
|
d := &DNSContext{
|
|
Req: newHostTestMessage("host"),
|
|
Addr: netip.MustParseAddrPort("127.0.0.2:1234"),
|
|
}
|
|
ans, ecsIP = nil, nil
|
|
ecsReqIP = nil
|
|
|
|
ctx := testutil.ContextWithTimeout(t, defaultTimeout)
|
|
err := prx.Resolve(ctx, d)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, ip4323, firstIP(d.Res))
|
|
assert.Nil(t, ecsReqIP)
|
|
})
|
|
}
|
|
|
|
func TestECSProxyCacheMinMaxTTL(t *testing.T) {
|
|
clientIP := net.IP{1, 2, 3, 0}
|
|
|
|
var (
|
|
ans []dns.RR
|
|
ecsIP net.IP
|
|
)
|
|
|
|
onExchange := newECSReplyHandler(&ans, &ecsIP, nil)
|
|
u := newTestECSUpstream(onExchange)
|
|
|
|
ans = []dns.RR{&dns.A{
|
|
Hdr: dns.RR_Header{
|
|
Rrtype: dns.TypeA,
|
|
Name: "host.",
|
|
Ttl: 10,
|
|
},
|
|
A: net.IP{4, 3, 2, 1},
|
|
}}
|
|
ecsIP = clientIP
|
|
|
|
prx := mustNew(t, &Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: &UpstreamConfig{Upstreams: []upstream.Upstream{u}},
|
|
TrustedProxies: defaultTrustedProxies,
|
|
DNSSECEnabled: true,
|
|
EnableEDNSClientSubnet: true,
|
|
CacheEnabled: true,
|
|
CacheMinTTL: 20,
|
|
CacheMaxTTL: 40,
|
|
})
|
|
|
|
servicetest.RequireRun(t, prx, testTimeout)
|
|
|
|
// first request
|
|
d := &DNSContext{
|
|
Req: newHostTestMessage("host"),
|
|
Addr: netip.MustParseAddrPort("1.2.3.0:1234"),
|
|
}
|
|
ctx := testutil.ContextWithTimeout(t, defaultTimeout)
|
|
err := prx.Resolve(ctx, d)
|
|
require.NoError(t, err)
|
|
|
|
// get from cache - check min TTL
|
|
ci, expired, key := prx.cache.getWithSubnet(d.Req, &net.IPNet{
|
|
IP: clientIP,
|
|
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
|
|
})
|
|
assert.False(t, expired)
|
|
|
|
assert.Equal(t, key, msgToKeyWithSubnet(d.Req, clientIP, 24))
|
|
assert.True(t, ci.m.Answer[0].Header().Ttl == prx.cacheMinTTL)
|
|
|
|
// 2nd request
|
|
clientIP = net.IP{1, 2, 4, 0}
|
|
d.Req = newHostTestMessage("host")
|
|
d.Addr = netip.MustParseAddrPort("1.2.4.0:1234")
|
|
ans = []dns.RR{&dns.A{
|
|
Hdr: dns.RR_Header{
|
|
Rrtype: dns.TypeA,
|
|
Name: "host.",
|
|
Ttl: 60,
|
|
},
|
|
A: net.IP{4, 3, 2, 1},
|
|
}}
|
|
ecsIP = clientIP
|
|
|
|
err = prx.Resolve(ctx, d)
|
|
require.NoError(t, err)
|
|
|
|
// get from cache - check max TTL
|
|
ci, expired, key = prx.cache.getWithSubnet(d.Req, &net.IPNet{
|
|
IP: clientIP,
|
|
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
|
|
})
|
|
assert.False(t, expired)
|
|
assert.Equal(t, key, msgToKeyWithSubnet(d.Req, clientIP, 24))
|
|
assert.True(t, ci.m.Answer[0].Header().Ttl == prx.cacheMaxTTL)
|
|
}
|
|
|
|
func TestProxy_Resolve_withOptimisticResolver(t *testing.T) {
|
|
const (
|
|
host = "some.domain.name."
|
|
nonOptimisticTTL = 3600
|
|
)
|
|
|
|
buildCtx := func() (dctx *DNSContext) {
|
|
req := &dns.Msg{
|
|
MsgHdr: dns.MsgHdr{Id: dns.Id()},
|
|
Question: []dns.Question{{
|
|
Name: host,
|
|
Qtype: dns.TypeA,
|
|
Qclass: dns.ClassINET,
|
|
}},
|
|
}
|
|
|
|
return &DNSContext{Req: req}
|
|
}
|
|
buildResp := func(req *dns.Msg, ttl uint32) (resp *dns.Msg) {
|
|
resp = (&dns.Msg{}).SetReply(req)
|
|
resp.Answer = []dns.RR{&dns.A{
|
|
Hdr: dns.RR_Header{
|
|
Name: host,
|
|
Rrtype: dns.TypeA,
|
|
Class: dns.ClassINET,
|
|
Ttl: ttl,
|
|
},
|
|
A: net.IP{1, 2, 3, 4},
|
|
}}
|
|
|
|
return resp
|
|
}
|
|
|
|
p := &Proxy{
|
|
reqCtx: contextutil.EmptyConstructor{},
|
|
requestHandler: DefaultHandler{},
|
|
cacheEnabled: true,
|
|
cacheOptimistic: true,
|
|
cacheOptimisticAnswerTTL: testOptimisticTTL,
|
|
cacheOptimisticMaxAge: testOptimisticMaxAge,
|
|
dnsSecEnabled: true,
|
|
// TODO(e.burkov): Set panicking upstream configuration.
|
|
logger: testLogger,
|
|
pendingRequests: newDefaultPendingRequests(),
|
|
mu: &sync.RWMutex{},
|
|
}
|
|
|
|
p.initCache()
|
|
out, in := make(chan unit), make(chan unit)
|
|
p.shortFlighter.cr = &testCachingResolver{
|
|
onReplyFromUpstream: func(dctx *DNSContext) (ok bool, err error) {
|
|
dctx.Res = buildResp(dctx.Req, nonOptimisticTTL)
|
|
|
|
return true, nil
|
|
},
|
|
onCacheResp: func(dctx *DNSContext) {
|
|
// Report adding to cache is in process.
|
|
out <- unit{}
|
|
// Wait for tests to finish.
|
|
<-in
|
|
|
|
p.cacheResp(dctx)
|
|
|
|
// Report adding to cache is finished.
|
|
out <- unit{}
|
|
},
|
|
}
|
|
|
|
// Two different contexts are made to emulate two different requests
|
|
// with the same question section.
|
|
firstCtx, secondCtx := buildCtx(), buildCtx()
|
|
|
|
// Add expired response into cache.
|
|
req := firstCtx.Req.Copy()
|
|
p.addDO(req)
|
|
key := msgToKey(req)
|
|
data := (&cacheItem{
|
|
m: buildResp(req, 0),
|
|
u: testUpsAddr,
|
|
}).pack()
|
|
items := glcache.New(glcache.Config{
|
|
EnableLRU: true,
|
|
})
|
|
items.Set(key, data)
|
|
p.cache.items = items
|
|
|
|
ctx := testutil.ContextWithTimeout(t, defaultTimeout)
|
|
|
|
err := p.Resolve(ctx, firstCtx)
|
|
require.NoError(t, err)
|
|
require.Len(t, firstCtx.Res.Answer, 1)
|
|
|
|
assert.Equal(t, uint32(testOptimisticTTL.Seconds()), firstCtx.Res.Answer[0].Header().Ttl)
|
|
|
|
// Wait for optimisticResolver to reach the tested function.
|
|
<-out
|
|
|
|
err = p.Resolve(ctx, secondCtx)
|
|
require.NoError(t, err)
|
|
require.Len(t, secondCtx.Res.Answer, 1)
|
|
|
|
assert.Equal(t, uint32(testOptimisticTTL.Seconds()), secondCtx.Res.Answer[0].Header().Ttl)
|
|
|
|
// Continue and wait for it to finish.
|
|
in <- unit{}
|
|
<-out
|
|
|
|
// Should be served from cache.
|
|
data = p.cache.items.Get(msgToKey(firstCtx.Req))
|
|
unpacked, expired := p.cache.unpackItem(data, firstCtx.Req)
|
|
require.False(t, expired)
|
|
require.NotNil(t, unpacked)
|
|
require.Len(t, unpacked.m.Answer, 1)
|
|
|
|
assert.EqualValues(t, nonOptimisticTTL, unpacked.m.Answer[0].Header().Ttl)
|
|
}
|
|
|
|
func TestProxy_validateRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
fqdn = "test.example."
|
|
privateARPAFQDN = "1.100.51.198.in-addr.arpa."
|
|
publicARPAFQDN = "8.8.8.8.in-addr.arpa."
|
|
)
|
|
|
|
testAddr := netip.MustParseAddrPort("192.0.2.1:53")
|
|
privateAddr := netip.MustParseAddrPort("198.51.100.1:53")
|
|
|
|
privateNets := netutil.SliceSubnetSet{
|
|
netip.MustParsePrefix("198.51.100.0/24"),
|
|
netip.MustParsePrefix("203.0.113.0/8"),
|
|
}
|
|
|
|
ups := &testUpstream{
|
|
OnExchange: func(m *dns.Msg) (resp *dns.Msg, err error) {
|
|
resp = &dns.Msg{}
|
|
resp.SetReply(m)
|
|
|
|
return resp, nil
|
|
},
|
|
OnAddress: func() (addr string) { return "stub" },
|
|
OnClose: func() (err error) { return nil },
|
|
}
|
|
|
|
p, err := New(&Config{
|
|
Logger: testLogger,
|
|
UDPListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
|
|
UpstreamConfig: &UpstreamConfig{Upstreams: []upstream.Upstream{ups}},
|
|
RefuseAny: true,
|
|
PrivateSubnets: privateNets,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
testCases := []struct {
|
|
req *dns.Msg
|
|
addr netip.AddrPort
|
|
name string
|
|
wantRcode int
|
|
isPrivateClient bool
|
|
wantNil bool
|
|
}{{
|
|
name: "valid_request",
|
|
req: (&dns.Msg{}).SetQuestion(fqdn, dns.TypeA),
|
|
addr: testAddr,
|
|
wantNil: true,
|
|
isPrivateClient: false,
|
|
}, {
|
|
name: "no_questions",
|
|
req: &dns.Msg{
|
|
MsgHdr: dns.MsgHdr{Id: dns.Id()},
|
|
Question: []dns.Question{},
|
|
},
|
|
addr: testAddr,
|
|
wantRcode: dns.RcodeFormatError,
|
|
wantNil: false,
|
|
isPrivateClient: false,
|
|
}, {
|
|
name: "refuse_any",
|
|
req: (&dns.Msg{}).SetQuestion(fqdn, dns.TypeANY),
|
|
addr: testAddr,
|
|
wantRcode: dns.RcodeNotImplemented,
|
|
wantNil: false,
|
|
isPrivateClient: false,
|
|
}, {
|
|
name: "private_arpa_from_public_client",
|
|
req: (&dns.Msg{}).SetQuestion(privateARPAFQDN, dns.TypePTR),
|
|
addr: testAddr,
|
|
wantRcode: dns.RcodeNameError,
|
|
wantNil: false,
|
|
isPrivateClient: false,
|
|
}, {
|
|
name: "private_arpa_from_private_client",
|
|
req: (&dns.Msg{}).SetQuestion(privateARPAFQDN, dns.TypePTR),
|
|
addr: privateAddr,
|
|
wantNil: true,
|
|
isPrivateClient: true,
|
|
}, {
|
|
name: "private_arpa_soa_from_public_client",
|
|
req: (&dns.Msg{}).SetQuestion(privateARPAFQDN, dns.TypeSOA),
|
|
addr: testAddr,
|
|
wantRcode: dns.RcodeNameError,
|
|
wantNil: false,
|
|
isPrivateClient: false,
|
|
}, {
|
|
name: "private_arpa_ns_from_public_client",
|
|
req: (&dns.Msg{}).SetQuestion(privateARPAFQDN, dns.TypeNS),
|
|
addr: testAddr,
|
|
wantRcode: dns.RcodeNameError,
|
|
wantNil: false,
|
|
isPrivateClient: false,
|
|
}, {
|
|
name: "public_arpa",
|
|
req: (&dns.Msg{}).SetQuestion(publicARPAFQDN, dns.TypePTR),
|
|
addr: testAddr,
|
|
wantNil: true,
|
|
isPrivateClient: false,
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dctx := &DNSContext{
|
|
Req: tc.req,
|
|
Addr: tc.addr,
|
|
IsPrivateClient: tc.isPrivateClient,
|
|
}
|
|
|
|
resp := p.validateRequest(dctx)
|
|
|
|
if tc.wantNil {
|
|
assert.Nil(t, resp)
|
|
|
|
return
|
|
}
|
|
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, tc.wantRcode, resp.Rcode)
|
|
})
|
|
}
|
|
}
|
|
|
|
// testHandler is a mock request handler implementation to simplify
|
|
// testing.
|
|
//
|
|
// TODO(m.kazantsev): Use [dnsproxytest.Handler].
|
|
type testHandler struct {
|
|
OnHandle func(ctx context.Context, p *Proxy, dctx *DNSContext) (err error)
|
|
}
|
|
|
|
// type check
|
|
var _ Handler = (*testHandler)(nil)
|
|
|
|
// ServeDNS implements the [Handler] interface for *testHandler.
|
|
func (h *testHandler) ServeDNS(ctx context.Context, p *Proxy, dctx *DNSContext) (err error) {
|
|
return h.OnHandle(ctx, p, dctx)
|
|
}
|
|
|
|
// testUpstream is a mock upstream implementation to simplify testing.
|
|
//
|
|
// TODO(m.kazantsev): Use [dnsproxytest.Upstream].
|
|
type testUpstream struct {
|
|
OnAddress func() (addr string)
|
|
OnExchange func(req *dns.Msg) (resp *dns.Msg, err error)
|
|
OnClose func() (err error)
|
|
}
|
|
|
|
// newTestECSUpstream creates a new test upstream with the given OnExchange
|
|
// handler.
|
|
func newTestECSUpstream(onExchange func(req *dns.Msg) (resp *dns.Msg, err error)) (u *testUpstream) {
|
|
return &testUpstream{
|
|
OnAddress: func() string { return "" },
|
|
OnClose: func() error { return nil },
|
|
OnExchange: onExchange,
|
|
}
|
|
}
|
|
|
|
// newECSReplyHandler creates an OnExchange handler that builds a DNS reply
|
|
// with optional answer records and ECS support.
|
|
//
|
|
// The handler dereferences ans, ecsIP, and ecsReqIP at call time, so tests
|
|
// can reassign those variables between sub-tests and the handler will see
|
|
// the current values.
|
|
func newECSReplyHandler(ans *[]dns.RR, ecsIP, ecsReqIP *net.IP) func(*dns.Msg) (*dns.Msg, error) {
|
|
return func(m *dns.Msg) (resp *dns.Msg, err error) {
|
|
resp = (&dns.Msg{}).SetReply(m)
|
|
|
|
if ans != nil && *ans != nil {
|
|
resp.Answer = append(resp.Answer, *ans...)
|
|
}
|
|
|
|
if ecsIP != nil && *ecsIP != nil {
|
|
setECS(resp, *ecsIP, 24)
|
|
}
|
|
|
|
if ecsReqIP == nil {
|
|
return resp, nil
|
|
}
|
|
|
|
ecs, _ := ecsFromMsg(m)
|
|
if ecs != nil {
|
|
*ecsReqIP = ecs.IP
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
}
|
|
|
|
// type check
|
|
var _ upstream.Upstream = (*testUpstream)(nil)
|
|
|
|
// Exchange implements the upstream.Upstream interface for *testUpstream.
|
|
func (u *testUpstream) Exchange(m *dns.Msg) (resp *dns.Msg, err error) {
|
|
return u.OnExchange(m)
|
|
}
|
|
|
|
// Address implements the upstream.Upstream interface for *testUpstream.
|
|
func (u *testUpstream) Address() (addr string) {
|
|
return u.OnAddress()
|
|
}
|
|
|
|
// Close implements the upstream.Upstream interface for *testUpstream.
|
|
func (u *testUpstream) Close() (err error) {
|
|
return u.OnClose()
|
|
}
|