Skip to content
Commits on Source (3)
  • Eugene Burkov's avatar
    Pull request 261: 333-opt-timeout · c70af9f4
    Eugene Burkov authored
    Merge in GO/dnsproxy from 333-opt-timeout to master
    
    Closes #333.
    
    Squashed commit of the following:
    
    commit f049d5e64d92635bb63fdb2e9cf73c97416df198
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Tue Jun 13 16:54:26 2023 +0300
    
        main: imp docs, code
    
    commit 627ed3b9399f94d67caf33cc86cc354375b38536
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Tue Jun 13 16:22:04 2023 +0300
    
        main: rm short form, consider for local
    
    commit d46aa0016929ada0968588f9a4fb0719ed046d40
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Tue Jun 13 13:58:21 2023 +0300
    
        main: upd docs
    
    commit 641cc9265f6e59b8dd6f9c6fac2f3aea4adbab28
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Tue Jun 13 13:38:28 2023 +0300
    
        main: configurable timeout
    c70af9f4
  • Dimitry Kolyshev's avatar
    Pull request: 5285-doq-errors · 31e37406
    Dimitry Kolyshev authored
    Updates AdguardTeam/AdGuardHome#5285.
    
    Squashed commit of the following:
    
    commit cc6acb46afcb983a0192023a231c6784131f57b0
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Tue Jun 27 16:25:20 2023 +0400
    
        proxy: quic errors
    
    commit f29f71d067eb9623125625e6f87049003f1cbe0d
    Author: Dimitry Kolyshev <dkolyshev@adguard.com>
    Date:   Tue Jun 27 10:51:30 2023 +0400
    
        proxy: quic errors
    31e37406
  • Eugene Burkov's avatar
    Pull request 264: AGDNS-1528 fallback ups conf · 8497a8dc
    Eugene Burkov authored
    Merge in GO/dnsproxy from AGDNS-1528-fallback-ups-conf to master
    
    Squashed commit of the following:
    
    commit d00ca113b07d4e489855cefa8ed110a382c7daf5
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Fri Jun 30 14:33:53 2023 +0300
    
        all: imp code, docs
    
    commit d9703445fd9e73cd466d603ff6543a5450330ffc
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Fri Jun 30 13:15:36 2023 +0300
    
        proxy: close ups configs
    
    commit ad2e2da0
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Thu Jun 29 16:03:16 2023 +0300
    
        proxy: imp fmt
    
    commit 92918afb
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Thu Jun 29 15:58:51 2023 +0300
    
        proxy: imp code
    
    commit 0dc3f3f3
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Jun 28 20:35:54 2023 +0300
    
        proxy: imp code
    
    commit a83136b7
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Jun 28 18:47:30 2023 +0300
    
        proxy: fix name, typo
    
    commit 08514218
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Jun 28 18:46:11 2023 +0300
    
        proxy: fix lint
    
    commit 2b282843
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Jun 28 18:45:06 2023 +0300
    
        proxy: imp test, doc
    
    commit 7487b537
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Jun 28 18:26:20 2023 +0300
    
        proxy: imp docs
    
    commit 146cbb95
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Jun 28 18:21:12 2023 +0300
    
        proxy: imp tests
    
    commit 3720aa42
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Jun 28 14:16:35 2023 +0300
    
        all: imp code
    
    commit 5c9ab059
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Thu Jun 22 17:53:00 2023 +0300
    
        all: make fallback an ups conf
    8497a8dc
......@@ -63,6 +63,8 @@ Application Options:
be specified multiple times
--all-servers If specified, parallel queries to all configured upstream servers are enabled
--fastest-addr Respond to A or AAAA requests only with the fastest IP address
--timeout= Timeout for outbound DNS queries to remote upstream servers in a
human-readable form (default: 10s)
--cache If specified, DNS cache is enabled
--cache-size= Cache size (in bytes). Default: 64k
--cache-min-ttl= Minimum TTL value for DNS entries, in seconds. Capped at 3600.
......
......@@ -15,3 +15,4 @@ ratelimit: 0
udp-buf-size: 0
upstream:
- "1.1.1.1:53"
timeout: '10s'
......@@ -67,6 +67,9 @@ func ResolveDialContext(
// NewDialContext returns a DialHandler that dials addrs and returns the first
// successful connection. At least a single addr should be specified.
//
// TODO(e.burkov): Consider using [Resolver] instead of
// [upstream.Options.Bootstrap] and [upstream.Options.ServerIPAddrs].
func NewDialContext(timeout time.Duration, addrs ...string) (h DialHandler) {
dialer := &net.Dialer{
Timeout: timeout,
......@@ -81,6 +84,8 @@ func NewDialContext(timeout time.Duration, addrs ...string) (h DialHandler) {
}
}
// TODO(e.burkov): Check IPv6 preference here.
return func(ctx context.Context, network, _ string) (conn net.Conn, err error) {
var errs []error
......
......@@ -17,7 +17,9 @@ import (
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mathutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/ameshkov/dnscrypt/v2"
goFlags "github.com/jessevdk/go-flags"
"gopkg.in/yaml.v3"
......@@ -110,6 +112,10 @@ type Options struct {
// detected by ICMP response time or TCP connection time
FastestAddress bool `yaml:"fastest-addr" long:"fastest-addr" description:"Respond to A or AAAA requests only with the fastest IP address" optional:"yes" optional-value:"true"`
// Timeout for outbound DNS queries to remote upstream servers in a
// human-readable form. Default is 10s.
Timeout timeutil.Duration `yaml:"timeout" long:"timeout" description:"Timeout for outbound DNS queries to remote upstream servers in a human-readable form" default:"10s"`
// Cache settings
// --
......@@ -184,7 +190,6 @@ type Options struct {
var VersionString = "dev" // nolint:gochecknoglobals
const (
defaultTimeout = 10 * time.Second
defaultLocalTimeout = 1 * time.Second
)
......@@ -336,6 +341,17 @@ func createProxyConfig(options *Options) proxy.Config {
return config
}
// containsUpstreams returns true if uc contains at least a single upstream.
// Otherwise it's considered nil.
//
// TODO(e.burkov): Think of a better way to validate the config. Perhaps,
// return an error from [ParseUpstreamsConfig] if no upstreams were initialized.
func containsUpstreams(uc *proxy.UpstreamConfig) (ok bool) {
return len(uc.Upstreams) > 0 ||
len(uc.DomainReservedUpstreams) > 0 ||
len(uc.SpecifiedDomainUpstreams) > 0
}
// initUpstreams inits upstream-related config
func initUpstreams(config *proxy.Config, options *Options) {
// Init upstreams
......@@ -351,48 +367,39 @@ func initUpstreams(config *proxy.Config, options *Options) {
var err error
upstreams := loadServersList(options.Upstreams)
timeout := options.Timeout.Duration
upsOpts := &upstream.Options{
HTTPVersions: httpVersions,
InsecureSkipVerify: options.Insecure,
Bootstrap: options.BootstrapDNS,
Timeout: defaultTimeout,
Timeout: timeout,
}
upstreams := loadServersList(options.Upstreams)
config.UpstreamConfig, err = proxy.ParseUpstreamsConfig(upstreams, upsOpts)
if err != nil {
log.Fatalf("error while parsing upstreams configuration: %s", err)
}
privUpstreams := loadServersList(options.PrivateRDNSUpstreams)
privUpsOpts := &upstream.Options{
HTTPVersions: httpVersions,
Bootstrap: options.BootstrapDNS,
Timeout: defaultLocalTimeout,
Timeout: mathutil.Min(defaultLocalTimeout, timeout),
}
config.PrivateRDNSUpstreamConfig, err = proxy.ParseUpstreamsConfig(privUpstreams, privUpsOpts)
privUpstreams := loadServersList(options.PrivateRDNSUpstreams)
private, err := proxy.ParseUpstreamsConfig(privUpstreams, privUpsOpts)
if err != nil {
log.Fatalf("error while parsing private rdns upstreams configuration: %s", err)
}
if containsUpstreams(private) {
config.PrivateRDNSUpstreamConfig = private
}
if options.Fallbacks != nil {
fallbacks := []upstream.Upstream{}
for i, f := range loadServersList(options.Fallbacks) {
// Use the same options for fallback servers as for
// upstream servers until it is possible to configure it
// separately.
//
// See https://github.com/AdguardTeam/dnsproxy/issues/161.
var fallback upstream.Upstream
fallback, err = upstream.AddressToUpstream(f, upsOpts)
if err != nil {
log.Fatalf("cannot parse the fallback %s (%s): %s", f, options.BootstrapDNS, err)
}
log.Printf("fallback at index %d is %s", i, fallback.Address())
fallbacks = append(fallbacks, fallback)
}
fallbackUpstreams := loadServersList(options.Fallbacks)
fallbacks, err := proxy.ParseUpstreamsConfig(fallbackUpstreams, upsOpts)
if err != nil {
log.Fatalf("error while parsing fallback upstreams configuration: %s", err)
}
if containsUpstreams(fallbacks) {
config.Fallbacks = fallbacks
}
......
......@@ -2,11 +2,11 @@ package proxy
import (
"crypto/tls"
"fmt"
"net"
"net/netip"
"time"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/ameshkov/dnscrypt/v2"
......@@ -86,7 +86,7 @@ type Config struct {
// Fallbacks is a list of fallback resolvers. Those will be used if the
// general set fails responding.
Fallbacks []upstream.Upstream
Fallbacks *UpstreamConfig
// UpstreamMode determines the logic through which upstreams will be used.
UpstreamMode UpstreamModeType
......@@ -177,25 +177,28 @@ type Config struct {
// validateConfig verifies that the supplied configuration is valid and returns an error if it's not
func (p *Proxy) validateConfig() error {
if p.started {
return errors.Error("server has been already started")
}
err := p.validateListenAddrs()
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
if p.UpstreamConfig == nil {
return errors.Error("no default upstreams specified")
err = p.UpstreamConfig.validate()
if err != nil {
return fmt.Errorf("validating general usptreams: %w", err)
}
if len(p.UpstreamConfig.Upstreams) == 0 {
if len(p.UpstreamConfig.DomainReservedUpstreams) == 0 {
return errors.Error("no upstreams specified")
}
// Allow both [Proxy.PrivateRDNSUpstreamConfig] and [Proxy.Fallbacks] to be
// nil, but not empty.
err = p.PrivateRDNSUpstreamConfig.validate()
if err != nil && !errors.Is(err, errNoDefaultUpstreams) {
return fmt.Errorf("validating private RDNS upstreams: %w", err)
}
return errors.Error("no default upstreams specified")
err = p.Fallbacks.validate()
if err != nil && !errors.Is(err, errNoDefaultUpstreams) {
return fmt.Errorf("validating fallbacks: %w", err)
}
if p.CacheMinTTL > 0 || p.CacheMaxTTL > 0 {
......
......@@ -343,7 +343,9 @@ func TestProxy_Resolve_dns64(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
p := createTestProxy(t, nil)
p.Config.UpstreamConfig.Upstreams = []upstream.Upstream{newUps(tc.upsAns)}
p.Config.PrivateRDNSUpstreamConfig.Upstreams = []upstream.Upstream{localUps}
p.Config.PrivateRDNSUpstreamConfig = &UpstreamConfig{
Upstreams: []upstream.Upstream{localUps},
}
p.Config.UseDNS64 = true
require.NoError(t, p.Start())
......
......@@ -232,6 +232,10 @@ func (p *Proxy) Start() (err error) {
p.Lock()
defer p.Unlock()
if p.started {
return errors.Error("server has been already started")
}
err = p.validateConfig()
if err != nil {
return err
......@@ -272,6 +276,7 @@ func (p *Proxy) Stop() error {
p.Lock()
defer p.Unlock()
if !p.started {
log.Info("dnsproxy: dns proxy server is not started")
......@@ -312,8 +317,14 @@ func (p *Proxy) Stop() error {
errs = closeAll(errs, p.dnsCryptTCPListen...)
p.dnsCryptTCPListen = nil
if p.UpstreamConfig != nil {
errs = closeAll(errs, p.UpstreamConfig)
for _, u := range []*UpstreamConfig{
p.UpstreamConfig,
p.PrivateRDNSUpstreamConfig,
p.Fallbacks,
} {
if u != nil {
errs = closeAll(errs, u)
}
}
p.started = false
......@@ -443,88 +454,99 @@ func (p *Proxy) needsLocalUpstream(req *dns.Msg) (ok bool) {
// selectUpstreams returns the upstreams to use for the specified host. It
// firstly considers custom upstreams if those aren't empty and then the
// configured ones. It returns false, if no upstreams are available for current
// request.
func (p *Proxy) selectUpstreams(d *DNSContext) (upstreams []upstream.Upstream, ok bool) {
// configured ones. The returned slice may be empty or nil.
func (p *Proxy) selectUpstreams(d *DNSContext) (upstreams []upstream.Upstream) {
host := d.Req.Question[0].Name
if p.needsLocalUpstream(d.Req) {
if p.PrivateRDNSUpstreamConfig == nil {
return nil, false
}
ip, _ := netutil.IPAndPortFromAddr(d.Addr)
// TODO(e.burkov): Detect against the actual configured subnet set.
// Perhaps, even much earlier.
if !netutil.IsLocallyServed(ip) {
return nil, false
if !p.needsLocalUpstream(d.Req) {
if custom := d.CustomUpstreamConfig; custom != nil {
// Try to use custom.
upstreams = custom.getUpstreamsForDomain(host)
if len(upstreams) > 0 {
return upstreams
}
}
return p.PrivateRDNSUpstreamConfig.getUpstreamsForDomain(host), true
// Use configured.
return p.UpstreamConfig.getUpstreamsForDomain(host)
}
if d.CustomUpstreamConfig != nil {
upstreams = d.CustomUpstreamConfig.getUpstreamsForDomain(host)
// Use private upstreams.
private := p.PrivateRDNSUpstreamConfig
if private == nil {
return nil
}
if upstreams != nil {
return upstreams, true
ip, _ := netutil.IPAndPortFromAddr(d.Addr)
// TODO(e.burkov): Detect against the actual configured subnet set.
// Perhaps, even much earlier.
if !netutil.IsLocallyServed(ip) {
return nil
}
return p.UpstreamConfig.getUpstreamsForDomain(host), true
return private.getUpstreamsForDomain(host)
}
// replyFromUpstream tries to resolve the request.
func (p *Proxy) replyFromUpstream(d *DNSContext) (ok bool, err error) {
req := d.Req
upstreams, ok := p.selectUpstreams(d)
if !ok {
return false, upstream.ErrNoUpstreams
upstreams := p.selectUpstreams(d)
if len(upstreams) == 0 {
return false, fmt.Errorf("selecting general upstream: %w", upstream.ErrNoUpstreams)
}
start := time.Now()
// Perform the DNS request.
var reply *dns.Msg
var u upstream.Upstream
reply, u, err = p.exchange(req, upstreams)
if dns64Ups := p.performDNS64(req, reply, upstreams); dns64Ups != nil {
resp, u, err := p.exchange(req, upstreams)
if dns64Ups := p.performDNS64(req, resp, upstreams); dns64Ups != nil {
u = dns64Ups
} else if p.isBogusNXDomain(reply) {
log.Tracef("response ip is contained in bogus-nxdomain list")
reply = p.genWithRCode(req, dns.RcodeNameError)
} else if p.isBogusNXDomain(resp) {
log.Debug("proxy: replying from upstream: response contains bogus-nxdomain ip")
resp = p.genWithRCode(req, dns.RcodeNameError)
}
log.Tracef("RTT: %s", time.Since(start))
log.Debug("proxy: replying from upstream: rtt is %s", time.Since(start))
if err != nil && p.Fallbacks != nil {
log.Tracef("using the fallback upstream due to %s", err)
log.Debug("proxy: replying from upstream: using fallback due to %s", err)
reply, u, err = upstream.ExchangeParallel(p.Fallbacks, req)
upstreams = p.Fallbacks.getUpstreamsForDomain(req.Question[0].Name)
if len(upstreams) == 0 {
return false, fmt.Errorf("selecting fallback upstream: %w", upstream.ErrNoUpstreams)
}
resp, u, err = upstream.ExchangeParallel(upstreams, req)
}
if ok = reply != nil; ok {
// This branch handles the successfully exchanged response.
p.handleExchangeResult(d, req, resp, u)
// Set upstream that have resolved the request to DNSContext.
d.Upstream = u
p.setMinMaxTTL(reply)
return resp != nil, err
}
// handleExchangeResult handles the result after the upstream exchange. It sets
// the response to d and sets the upstream that have resolved the request. If
// the response is nil, it generates a server failure response.
func (p *Proxy) handleExchangeResult(d *DNSContext, req, resp *dns.Msg, u upstream.Upstream) {
if resp == nil {
d.Res = p.genServerFailure(req)
d.hasEDNS0 = false
return
}
d.Upstream = u
d.Res = resp
p.setMinMaxTTL(resp)
if len(req.Question) > 0 && len(resp.Question) == 0 {
// Explicitly construct the question section since some upstreams may
// respond with invalidly constructed messages which cause out-of-range
// panics afterwards.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3551.
if len(req.Question) > 0 && len(reply.Question) == 0 {
reply.Question = []dns.Question{req.Question[0]}
}
} else {
reply = p.genServerFailure(req)
d.hasEDNS0 = false
resp.Question = []dns.Question{req.Question[0]}
}
d.Res = reply
return ok, err
}
// addDO adds EDNS0 RR if needed and sets DO bit of msg to true.
......
......@@ -11,6 +11,8 @@ import (
"io"
"math/big"
"net"
"net/netip"
"net/url"
"os"
"sync"
"testing"
......@@ -430,17 +432,14 @@ func TestOneByOneUpstreamsExchange(t *testing.T) {
timeOut := 1 * time.Second
dnsProxy := createTestProxy(t, nil)
// invalid fallback to make sure that reply is not coming from fallback server
dnsProxy.Fallbacks = []upstream.Upstream{}
fallback := "1.2.3.4:567"
f, err := upstream.AddressToUpstream(
fallback,
// invalid fallback to make sure that reply is not coming from fallback
// server
var err error
dnsProxy.Fallbacks, err = ParseUpstreamsConfig(
[]string{"1.2.3.4:567"},
&upstream.Options{Timeout: timeOut},
)
if err != nil {
t.Fatalf("cannot create fallback upstream %s cause %s", fallback, err)
}
dnsProxy.Fallbacks = append(dnsProxy.Fallbacks, f)
require.NoError(t, err)
// add one valid and two invalid upstreams
upstreams := []string{"https://fake-dns.com/fake-dns-query", "tls://fake-dns.com", "1.1.1.1"}
......@@ -454,113 +453,167 @@ func TestOneByOneUpstreamsExchange(t *testing.T) {
Timeout: timeOut,
},
)
if err != nil {
t.Fatalf("cannot create upstream %s cause %s", line, err)
}
require.NoError(t, err)
dnsProxy.UpstreamConfig.Upstreams = append(dnsProxy.UpstreamConfig.Upstreams, u)
}
err = dnsProxy.Start()
if err != nil {
t.Fatalf("cannot start the DNS proxy: %s", err)
}
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, dnsProxy.Stop)
// create a DNS-over-TCP client connection
addr := dnsProxy.Addr(ProtoTCP)
conn, err := dns.Dial("tcp", addr.String())
if err != nil {
t.Fatalf("cannot connect to the proxy: %s", err)
}
require.NoError(t, err)
// make sure that the response is okay and resolved by valid upstream
req := createTestMessage()
err = conn.WriteMsg(req)
if err != nil {
t.Fatalf("cannot write message: %s", err)
}
require.NoError(t, err)
start := time.Now()
res, err := conn.ReadMsg()
if err != nil {
t.Fatalf("cannot read response to message: %s", err)
}
require.NoError(t, err)
requireResponse(t, req, res)
elapsed := time.Since(start)
if elapsed > 3*timeOut {
t.Fatalf("the operation took much more time than the configured timeout")
}
}
// Stop the proxy
err = dnsProxy.Stop()
if err != nil {
t.Fatalf("cannot stop the DNS proxy: %s", err)
// newLocalUpstreamListener creates a new localhost listener on the specified
// port for tcp4 network and returns its listening address.
func newLocalUpstreamListener(t *testing.T, port uint16, h dns.Handler) (real netip.AddrPort) {
t.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(t, upsSrv.Shutdown)
return testutil.RequireTypeAssert[*net.TCPAddr](t, upsSrv.Listener.Addr()).AddrPort()
}
func TestFallback(t *testing.T) {
timeout := 1 * time.Second
// Prepare the proxy server
dnsProxy := createTestProxy(t, nil)
responseCh := make(chan uint16)
failCh := make(chan uint16)
// List of fallback server addresses. Only one is valid
fallbackAddresses := []string{"1.2.3.4", "1.2.3.5", "8.8.8.8"}
dnsProxy.Fallbacks = []upstream.Upstream{}
const timeout = 1 * time.Second
for _, s := range fallbackAddresses {
f, _ := upstream.AddressToUpstream(
s,
&upstream.Options{Timeout: timeout},
)
dnsProxy.Fallbacks = append(dnsProxy.Fallbacks, f)
}
successHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
testutil.RequireSend(testutil.PanicT{}, responseCh, r.Id, timeout)
// using some random port to make sure that this upstream won't work
u, _ := upstream.AddressToUpstream(
"8.8.8.8:555",
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, timeout)
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 := createTestProxy(t, nil)
var err error
dnsProxy.UpstreamConfig, err = ParseUpstreamsConfig(
[]string{
failAddr,
"[/specific.example/]" + alsoSuccessAddr,
// almost.failing.example will fall here first.
"[/failing.example/]" + failAddr,
},
&upstream.Options{Timeout: timeout},
)
dnsProxy.UpstreamConfig = &UpstreamConfig{}
dnsProxy.UpstreamConfig.Upstreams = make([]upstream.Upstream, 0)
dnsProxy.UpstreamConfig.Upstreams = append(dnsProxy.UpstreamConfig.Upstreams, u)
require.NoError(t, err)
// Start listening
err := dnsProxy.Start()
if err != nil {
t.Fatalf("cannot start the DNS proxy: %s", err)
}
dnsProxy.Fallbacks, err = ParseUpstreamsConfig(
[]string{
failAddr,
successAddr,
"[/failing.example/]" + failAddr,
"[/almost.failing.example/]" + alsoSuccessAddr,
},
&upstream.Options{Timeout: timeout},
)
require.NoError(t, err)
// Create a DNS-over-UDP client connection
addr := dnsProxy.Addr(ProtoUDP)
conn, err := dns.Dial("udp", addr.String())
if err != nil {
t.Fatalf("cannot connect to the proxy: %s", err)
}
err = dnsProxy.Start()
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, dnsProxy.Stop)
// Make sure that the response is okay and resolved by the fallback
req := createTestMessage()
err = conn.WriteMsg(req)
if err != nil {
t.Fatalf("cannot write message: %s", err)
}
conn, err := dns.Dial("tcp", dnsProxy.Addr(ProtoTCP).String())
require.NoError(t, err)
start := time.Now()
res, err := conn.ReadMsg()
if err != nil {
t.Fatalf("cannot read response to message: %s", err)
}
requireResponse(t, req, res)
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,
},
}}
elapsed := time.Since(start)
if elapsed > 3*timeout {
t.Fatalf("the operation took much more time than the configured timeout")
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := createHostTestMessage(tc.name)
err = conn.WriteMsg(req)
require.NoError(t, err)
// Stop the proxy
err = dnsProxy.Stop()
if err != nil {
t.Fatalf("cannot stop the DNS proxy: %s", err)
for _, ch := range tc.wantSignals {
reqID, ok := testutil.RequireReceive(testutil.PanicT{}, ch, timeout)
require.True(t, ok)
assert.Equal(t, req.Id, reqID)
}
_, err := conn.ReadMsg()
require.NoError(t, err)
})
}
}
......@@ -570,16 +623,12 @@ func TestFallbackFromInvalidBootstrap(t *testing.T) {
dnsProxy := createTestProxy(t, nil)
// List of fallback server addresses. Both are valid
fallbackAddresses := []string{"1.0.0.1", "8.8.8.8"}
dnsProxy.Fallbacks = []upstream.Upstream{}
for _, s := range fallbackAddresses {
f, _ := upstream.AddressToUpstream(
s,
&upstream.Options{Timeout: timeout},
)
dnsProxy.Fallbacks = append(dnsProxy.Fallbacks, f)
}
var err error
dnsProxy.Fallbacks, err = ParseUpstreamsConfig(
[]string{"1.0.0.1", "8.8.8.8"},
&upstream.Options{Timeout: timeout},
)
require.NoError(t, err)
// Using a DoT server with invalid bootstrap.
u, _ := upstream.AddressToUpstream(
......@@ -593,42 +642,29 @@ func TestFallbackFromInvalidBootstrap(t *testing.T) {
dnsProxy.UpstreamConfig.Upstreams = append(dnsProxy.UpstreamConfig.Upstreams, u)
// Start listening
err := dnsProxy.Start()
if err != nil {
t.Fatalf("cannot start the DNS proxy: %s", err)
}
err = dnsProxy.Start()
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, dnsProxy.Stop)
// Create a DNS-over-UDP client connection
addr := dnsProxy.Addr(ProtoUDP)
conn, err := dns.Dial("udp", addr.String())
if err != nil {
t.Fatalf("cannot connect to the proxy: %s", err)
}
require.NoError(t, err)
// Make sure that the response is okay and resolved by the fallback
req := createTestMessage()
err = conn.WriteMsg(req)
if err != nil {
t.Fatalf("cannot write message: %s", err)
}
require.NoError(t, err)
start := time.Now()
res, err := conn.ReadMsg()
if err != nil {
t.Fatalf("cannot read response to message: %s", err)
}
require.NoError(t, err)
requireResponse(t, req, res)
elapsed := time.Since(start)
if elapsed > 3*timeout {
t.Fatalf("the operation took much more time than the configured timeout")
}
// Stop the proxy
err = dnsProxy.Stop()
if err != nil {
t.Fatalf("cannot stop the DNS proxy: %s", err)
}
}
func TestRefuseAny(t *testing.T) {
......@@ -1095,40 +1131,29 @@ func getFreePort() uint {
}
func createTestProxy(t *testing.T, tlsConfig *tls.Config) *Proxy {
t.Helper()
p := Proxy{}
if tlsConfig != nil {
p.TLSListenAddr = []*net.TCPAddr{
{Port: 0, IP: net.ParseIP(listenIP)},
}
p.HTTPSListenAddr = []*net.TCPAddr{
{Port: 0, IP: net.ParseIP(listenIP)},
}
p.QUICListenAddr = []*net.UDPAddr{
{Port: 0, IP: net.ParseIP(listenIP)},
}
if ip := net.ParseIP(listenIP); tlsConfig != nil {
p.TLSListenAddr = []*net.TCPAddr{{IP: ip, Port: 0}}
p.HTTPSListenAddr = []*net.TCPAddr{{IP: ip, Port: 0}}
p.QUICListenAddr = []*net.UDPAddr{{IP: ip, Port: 0}}
p.TLSConfig = tlsConfig
} else {
p.UDPListenAddr = []*net.UDPAddr{
{Port: 0, IP: net.ParseIP(listenIP)},
}
p.TCPListenAddr = []*net.TCPAddr{
{Port: 0, IP: net.ParseIP(listenIP)},
}
p.UDPListenAddr = []*net.UDPAddr{{IP: ip, Port: 0}}
p.TCPListenAddr = []*net.TCPAddr{{IP: ip, Port: 0}}
}
upstreams := make([]upstream.Upstream, 0)
dnsUpstream, err := upstream.AddressToUpstream(
upstreamAddr,
&upstream.Options{Timeout: defaultTimeout},
)
if err != nil {
t.Fatalf("cannot prepare the upstream: %s", err)
}
require.NoError(t, err)
p.UpstreamConfig = &UpstreamConfig{}
p.UpstreamConfig.Upstreams = append(upstreams, dnsUpstream)
p.PrivateRDNSUpstreamConfig = &UpstreamConfig{}
p.TrustedProxies = []string{"0.0.0.0/0", "::0/0"}
return &p
......
......@@ -87,7 +87,7 @@ func (p *Proxy) quicPacketLoop(l *quic.EarlyListener, requestGoroutinesSema sema
for {
conn, err := l.Accept(context.Background())
if err != nil {
if isQUICNonCrit(err) {
if isQUICErrorForDebugLog(err) {
log.Debug("accepting quic conn: closed or timed out: %s", err)
} else {
log.Error("accepting quic conn: %s", err)
......@@ -117,7 +117,7 @@ func (p *Proxy) handleQUICConnection(conn quic.Connection, requestGoroutinesSema
// bidirectional stream.
stream, err := conn.AcceptStream(context.Background())
if err != nil {
if isQUICNonCrit(err) {
if isQUICErrorForDebugLog(err) {
log.Debug("accepting quic stream: closed or timed out: %s", err)
} else {
log.Error("accepting quic stream: %s", err)
......@@ -309,22 +309,28 @@ func logShortQUICRead(err error) {
return
}
if isQUICNonCrit(err) {
if isQUICErrorForDebugLog(err) {
log.Debug("reading from quic stream: closed or timeout: %s", err)
} else {
log.Error("reading from quic stream: %s", err)
}
}
// isQUICNonCrit returns true if err is a non-critical error, most probably
// related to the current QUIC implementation.
const (
// qCodeNoError is returned when the QUIC connection was gracefully closed
// and there is no error to signal.
qCodeNoError = quic.ApplicationErrorCode(quic.NoError)
// qCodeApplicationErrorError is used for Initial and Handshake packets.
// This error is considered as non-critical and will not be logged as error.
qCodeApplicationErrorError = quic.ApplicationErrorCode(quic.ApplicationErrorErrorCode)
)
// isQUICErrorForDebugLog returns true if err is a non-critical error, most probably
// related to the current QUIC implementation. err must not be nil.
//
// TODO(ameshkov): re-test when updating quic-go.
func isQUICNonCrit(err error) (ok bool) {
if err == nil {
return false
}
func isQUICErrorForDebugLog(err error) (ok bool) {
if errors.Is(err, quic.ErrServerClosed) {
// This error is returned when the QUIC listener was closed by us. This
// is an expected error, we don't need the detailed logs here.
......@@ -332,9 +338,11 @@ func isQUICNonCrit(err error) (ok bool) {
}
var qAppErr *quic.ApplicationError
if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 {
// This error is returned when a QUIC connection was gracefully closed.
// No need to have detailed logs for it either.
if errors.As(err, &qAppErr) &&
(qAppErr.ErrorCode == qCodeNoError || qAppErr.ErrorCode == qCodeApplicationErrorError) {
// No need to have detailed logs for these error codes either.
//
// TODO(a.garipov): Consider adding other error codes.
return true
}
......
......@@ -36,6 +36,8 @@ var _ io.Closer = (*UpstreamConfig)(nil)
// To exclude top level domain from reserved upstreams querying you could use the following: [/*.domain.com/]<upstreamString>
// So the following config: ["[/*.domain.com/]1.2.3.4", "3.4.5.6"] will send queries for all subdomains *.domain.com to 1.2.3.4,
// but domain.com query will be sent to default server 3.4.5.6 as every other query.
//
// TODO(e.burkov): Refactor this mess.
func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (*UpstreamConfig, error) {
if options == nil {
options = &upstream.Options{}
......@@ -129,6 +131,27 @@ func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (*
}, nil
}
// errNoDefaultUpstreams is returned when no default upstreams specified within
// a [Config.UpstreamConfig].
const errNoDefaultUpstreams errors.Error = "no default upstreams specified"
// validate returns an error if the upstreams aren't configured properly. c
// considered valid if it contains at least a single default upstream. Nil c,
// as well as c with no default upstreams causes [ErrNoDefaultUpstreams]. Empty
// c causes [upstream.ErrNoUpstreams].
func (uc *UpstreamConfig) validate() (err error) {
switch {
case uc == nil:
return fmt.Errorf("%w; uc is nil", errNoDefaultUpstreams)
case len(uc.Upstreams) > 0:
return nil
case len(uc.DomainReservedUpstreams) == 0 && len(uc.SpecifiedDomainUpstreams) == 0:
return upstream.ErrNoUpstreams
default:
return errNoDefaultUpstreams
}
}
// parseUpstreamLine - parses upstream line and returns the following:
// upstream address
// list of domains for which this upstream is reserved (may be nil)
......
......@@ -36,6 +36,48 @@ func TestGetUpstreamsForDomain(t *testing.T) {
assertUpstreamsForDomain(t, config, "maps.google.com.", []string{})
}
func TestUpstreamConfig_Validate(t *testing.T) {
testCases := []struct {
name string
wantValidateErr error
in []string
}{{
name: "empty",
wantValidateErr: upstream.ErrNoUpstreams,
in: []string{},
}, {
name: "nil",
wantValidateErr: upstream.ErrNoUpstreams,
in: nil,
}, {
name: "valid",
wantValidateErr: nil,
in: []string{
"udp://upstream.example:53",
},
}, {
name: "no_default",
wantValidateErr: errNoDefaultUpstreams,
in: []string{
"[/domain.example/]udp://upstream.example:53",
"[/another.domain.example/]#",
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c, err := ParseUpstreamsConfig(tc.in, nil)
require.NoError(t, err)
assert.ErrorIs(t, c.validate(), tc.wantValidateErr)
})
}
t.Run("actual_nil", func(t *testing.T) {
assert.ErrorIs(t, (*UpstreamConfig)(nil).validate(), errNoDefaultUpstreams)
})
}
func TestGetUpstreamsForDomainWithoutDuplicates(t *testing.T) {
upstreams := []string{"[/example.com/]1.1.1.1", "[/example.org/]1.1.1.1"}
config, err := ParseUpstreamsConfig(
......
......@@ -367,7 +367,9 @@ func newDialerInitializer(u *url.URL, opts *Options) (di DialerInitializer, err
return nil, fmt.Errorf("creating dial handler: %w", resolveErr)
}
dialHandler.Store(h)
if !dialHandler.CompareAndSwap(nil, h) {
return dialHandler.Load().(bootstrap.DialHandler), nil
}
return h, nil
}
......
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.