Pull request 285: AG-23599 use hostsfile vol.2

Merge in GO/dnsproxy from AG-23599-use-hostsfile-vol.2 to master

Squashed commit of the following:

commit b8fd8d5bbd2803ea6908a5ca938d60b9f256530f
Author: Eugene Burkov <[email protected]>
Date:   Fri Nov 10 19:21:33 2023 +0300

    upstream: fix nil deref

commit 11ec4feb9f1f5e575f12310c0438ed89c81c722e
Author: Eugene Burkov <[email protected]>
Date:   Fri Nov 10 18:54:45 2023 +0300

    bootstrap: imp docs

commit 93aa3b1c54f762f59bcadd145294028f38fe4d93
Author: Eugene Burkov <[email protected]>
Date:   Fri Nov 10 17:30:15 2023 +0300

    bootstrap: imp doc

commit 9779260e70d5ef7a3d8ff1d550bf5558007879d6
Merge: 1240ee1 ea0f2b1
Author: Eugene Burkov <[email protected]>
Date:   Fri Nov 10 16:43:54 2023 +0300

    Merge branch 'master' into AG-23599-use-hostsfile-vol.2

commit 1240ee16e968b63aca8ac27a024716b6f4159cb4
Author: Eugene Burkov <[email protected]>
Date:   Thu Nov 9 17:37:58 2023 +0300

    upstream: remove sort from upstream resolver

commit f22fe9bdfdcaf4ef9b81cbb0f94bd926eb45b6bb
Author: Eugene Burkov <[email protected]>
Date:   Thu Nov 9 15:37:01 2023 +0300

    all: upd go

commit 45c52151c8ed50a94fc040fbfd585499f3beabae
Author: Eugene Burkov <[email protected]>
Date:   Thu Nov 9 15:34:22 2023 +0300

    upstream: remove empty impl

commit 8582ed91e27955660bd787896d7945b07e204140
Author: Eugene Burkov <[email protected]>
Date:   Wed Nov 8 13:00:14 2023 +0300

    upstream: imp cognit

commit 35ce9dcac57b3eb9e4e80fe6dd0152e568077d73
Author: Eugene Burkov <[email protected]>
Date:   Wed Nov 8 12:47:25 2023 +0300

    all: imp code, fmt

commit 1823da58c765fdeb17a1ab70aebb0bd5eee8cdcf
Author: Eugene Burkov <[email protected]>
Date:   Wed Nov 8 12:33:35 2023 +0300

    all: imp docs, restore behavior

commit aeda1791f483e9ddb6910a66a1415ed424578fbb
Author: Eugene Burkov <[email protected]>
Date:   Tue Nov 7 15:23:17 2023 +0300

    main: fix import

commit fa3746453f77e32fe90f9cb53f9dbbc10e421dc8
Merge: eb12aec 254e5a8
Author: Eugene Burkov <[email protected]>
Date:   Tue Nov 7 14:33:18 2023 +0300

    Merge branch 'master' into AG-23599-use-hostsfile-vol.2

commit eb12aecb33afcb6c05f86c4787bffeb8a6018a33
Author: Eugene Burkov <[email protected]>
Date:   Fri Nov 3 19:44:15 2023 +0300

    upstream: fix nil deref

commit e8975360323190e91043ad4ec13e302696210971
Author: Eugene Burkov <[email protected]>
Date:   Fri Nov 3 18:38:00 2023 +0300

    all: imp code, docs

commit 60e709976413d5c64b54b000eb243ff4d83f6ca2
Merge: 0e6e9ad 8d20902
Author: Eugene Burkov <[email protected]>
Date:   Tue Oct 31 17:16:14 2023 +0300

    Merge branch 'master' into AG-23599-use-hostsfile-vol.2

commit 0e6e9ad9ca61f11ebf17de6e047f005d94d630d1
Merge: b10f900 b4abccf
Author: Eugene Burkov <[email protected]>
Date:   Wed Oct 11 17:09:08 2023 +0300

    Merge branch 'master' into AG-23599-use-hostsfile-vol.2

commit b10f900a3bb8682e6de2105569c0e6569ef84afb
Author: Eugene Burkov <[email protected]>
Date:   Wed Oct 11 16:30:18 2023 +0300

    upstream: imp returning types & docs

commit a5f50776da1b0396bd0e66d784996ea4832ab26b
Author: Eugene Burkov <[email protected]>
Date:   Thu Oct 5 18:52:20 2023 +0300

    proxy: fix merged test

commit a1535566677d6a1b7ccbfd7c89efeff87501d7b5
Author: Eugene Burkov <[email protected]>
Date:   Thu Oct 5 18:50:27 2023 +0300

    netutil: doc more, fix type check

commit 718aee887448e5fc38c9069579c69757ba067c38
Merge: 34750a6 60d2174
Author: Eugene Burkov <[email protected]>
Date:   Thu Oct 5 16:07:10 2023 +0300

    Merge branch 'master' into AG-23599-use-hostsfile-vol.2

... and 8 more commits
This commit is contained in:
Eugene Burkov
2023-11-10 19:35:11 +03:00
parent ea0f2b1d4e
commit 65b5293105
31 changed files with 735 additions and 643 deletions
+8 -4
View File
@@ -21,12 +21,12 @@ import (
// specified at initialization and ignores the addr.
type DialHandler func(ctx context.Context, network, addr string) (conn net.Conn, err error)
// ResolveDialContext returns a DialHandler that uses addresses resolved from
// u using resolvers. u must not be nil.
// ResolveDialContext returns a DialHandler that uses addresses resolved from u
// using resolver. u must not be nil.
func ResolveDialContext(
u *url.URL,
timeout time.Duration,
resolvers []Resolver,
resolver Resolver,
preferIPv6 bool,
) (h DialHandler, err error) {
defer func() { err = errors.Annotate(err, "dialing %q: %w", u.Host) }()
@@ -38,6 +38,10 @@ func ResolveDialContext(
return nil, err
}
if resolver == nil {
return nil, fmt.Errorf("resolver is nil: %w", ErrNoResolvers)
}
ctx := context.Background()
if timeout > 0 {
var cancel func()
@@ -45,7 +49,7 @@ func ResolveDialContext(
defer cancel()
}
ips, err := LookupParallel(ctx, resolvers, host)
ips, err := resolver.LookupNetIP(ctx, "ip", host)
if err != nil {
return nil, fmt.Errorf("resolving hostname: %w", err)
}
+2 -2
View File
@@ -98,7 +98,7 @@ func TestResolveDialContext(t *testing.T) {
dialContext, err := bootstrap.ResolveDialContext(
&url.URL{Host: netutil.JoinHostPort(hostname, port)},
testTimeout,
[]bootstrap.Resolver{r},
bootstrap.ParallelResolver{r},
tc.preferIPv6,
)
require.NoError(t, err)
@@ -130,7 +130,7 @@ func TestResolveDialContext(t *testing.T) {
dialContext, err := bootstrap.ResolveDialContext(
&url.URL{Host: netutil.JoinHostPort(hostname, port)},
testTimeout,
[]bootstrap.Resolver{r},
bootstrap.ParallelResolver{r},
false,
)
require.NoError(t, err)
+38 -33
View File
@@ -13,7 +13,8 @@ import (
// Resolver resolves the hostnames to IP addresses.
type Resolver interface {
// LookupNetIP looks up the IP addresses for the given host. network must
// be one of "ip", "ip4" or "ip6".
// be one of "ip", "ip4" or "ip6". The response may be empty even if err is
// nil.
LookupNetIP(ctx context.Context, network, host string) (addrs []netip.Addr, err error)
}
@@ -23,67 +24,71 @@ var _ Resolver = &net.Resolver{}
// ErrNoResolvers is returned when zero resolvers specified.
const ErrNoResolvers errors.Error = "no resolvers specified"
// LookupParallel performs lookup for IP address of host with all resolvers
// concurrently.
func LookupParallel(
// ParallelResolver is a slice of resolvers that are queried concurrently. The
// first successful response is returned.
type ParallelResolver []Resolver
// type check
var _ Resolver = ParallelResolver(nil)
// LookupNetIP implements the [Resolver] interface for ParallelResolver.
func (r ParallelResolver) LookupNetIP(
ctx context.Context,
resolvers []Resolver,
network string,
host string,
) (addrs []netip.Addr, err error) {
resolversNum := len(resolvers)
resolversNum := len(r)
switch resolversNum {
case 0:
return nil, ErrNoResolvers
case 1:
return lookup(ctx, resolvers[0], host)
return lookup(ctx, r[0], network, host)
default:
// Go on.
}
// Size of channel must accommodate results of lookups from all resolvers,
// sending into channel will be block otherwise.
ch := make(chan *lookupResult, resolversNum)
for _, res := range resolvers {
go lookupAsync(ctx, res, host, ch)
ch := make(chan any, resolversNum)
for _, rslv := range r {
go lookupAsync(ctx, rslv, network, host, ch)
}
var errs []error
for range resolvers {
result := <-ch
if result.err == nil {
return result.addrs, nil
for range r {
switch result := <-ch; result := result.(type) {
case error:
errs = append(errs, result)
case []netip.Addr:
return result, nil
}
errs = append(errs, result.err)
}
// TODO(e.burkov): Use [errors.Join] in Go 1.20.
return nil, errors.List("all resolvers failed", errs...)
return nil, errors.Join(errs...)
}
// lookupResult is a structure that represents the result of a lookup.
type lookupResult struct {
err error
addrs []netip.Addr
}
// lookupAsync tries to lookup for ip of host with r and sends the result into
// resCh. It's inteneded to be used as a goroutine.
func lookupAsync(ctx context.Context, r Resolver, host string, resCh chan<- *lookupResult) {
// lookupAsync performs a lookup for ip of host with r and sends the result into
// resCh. It is intended to be used as a goroutine.
func lookupAsync(ctx context.Context, r Resolver, network, host string, resCh chan<- any) {
defer log.OnPanic("parallel lookup")
addrs, err := lookup(ctx, r, host)
resCh <- &lookupResult{
err: err,
addrs: addrs,
addrs, err := lookup(ctx, r, network, host)
if err != nil {
resCh <- err
} else {
resCh <- addrs
}
}
// lookup tries to lookup ip of host with r.
func lookup(ctx context.Context, r Resolver, host string) (addrs []netip.Addr, err error) {
//
// TODO(e.burkov): Get rid of this function? It only wraps the actual lookup
// with dubious logging.
func lookup(ctx context.Context, r Resolver, network, host string) (addrs []netip.Addr, err error) {
start := time.Now()
addrs, err = r.LookupNetIP(ctx, "ip", host)
addrs, err = r.LookupNetIP(ctx, network, host)
elapsed := time.Since(start)
if err != nil {
log.Debug("parallel lookup: lookup for %s failed in %s: %s", host, elapsed, err)
} else {
+10 -9
View File
@@ -2,8 +2,8 @@ package bootstrap_test
import (
"context"
"fmt"
"net/netip"
"strings"
"testing"
"github.com/AdguardTeam/dnsproxy/internal/bootstrap"
@@ -31,7 +31,7 @@ func TestLookupParallel(t *testing.T) {
const hostname = "host.name"
t.Run("no_resolvers", func(t *testing.T) {
addrs, err := bootstrap.LookupParallel(context.Background(), nil, "")
addrs, err := bootstrap.ParallelResolver(nil).LookupNetIP(context.Background(), "ip", "")
assert.ErrorIs(t, err, bootstrap.ErrNoResolvers)
assert.Nil(t, addrs)
})
@@ -49,9 +49,9 @@ func TestLookupParallel(t *testing.T) {
}
t.Run("one_resolver", func(t *testing.T) {
addrs, err := bootstrap.LookupParallel(
addrs, err := bootstrap.ParallelResolver{immediate}.LookupNetIP(
context.Background(),
[]bootstrap.Resolver{immediate},
"ip",
hostname,
)
require.NoError(t, err)
@@ -72,9 +72,9 @@ func TestLookupParallel(t *testing.T) {
},
}
addrs, err := bootstrap.LookupParallel(
addrs, err := bootstrap.ParallelResolver{immediate, delayed}.LookupNetIP(
context.Background(),
[]bootstrap.Resolver{immediate, delayed},
"ip",
hostname,
)
require.NoError(t, err)
@@ -85,7 +85,8 @@ func TestLookupParallel(t *testing.T) {
t.Run("all_errors", func(t *testing.T) {
err := assert.AnError
wantErrMsg := fmt.Sprintf("all resolvers failed: 3 errors: %[1]q, %[1]q, %[1]q", err)
errStr := err.Error()
wantErrMsg := strings.Join([]string{errStr, errStr, errStr}, "\n")
r := &testResolver{
onLookupNetIP: func(_ context.Context, network, host string) ([]netip.Addr, error) {
@@ -93,9 +94,9 @@ func TestLookupParallel(t *testing.T) {
},
}
addrs, err := bootstrap.LookupParallel(
addrs, err := bootstrap.ParallelResolver{r, r, r}.LookupNetIP(
context.Background(),
[]bootstrap.Resolver{r, r, r},
"ip",
hostname,
)
testutil.AssertErrorMsg(t, wantErrMsg, err)
+3 -3
View File
@@ -44,6 +44,9 @@ type (
// It must be initialized with [NewHosts].
//
// TODO(e.burkov): Think of storing only slices.
//
// TODO(e.burkov): Move to netutil/hostsfile in module golibs as a default
// implementation of some storage interface.
type Hosts struct {
// names maps each address to its names in original case and in original
// adding order without duplicates.
@@ -54,9 +57,6 @@ type Hosts struct {
addrs map[string]*addrsSet
}
// type check
var _ hostsfile.HandleSet = (*Hosts)(nil)
// NewHosts parses hosts files from r and returns a new Hosts set. readers are
// optional, the error is only returned in case of parsing error.
func NewHosts(readers ...io.Reader) (h *Hosts, err error) {
+24
View File
@@ -8,6 +8,7 @@ package netutil
import (
"net"
"net/netip"
"strings"
glnetutil "github.com/AdguardTeam/golibs/netutil"
"golang.org/x/exp/slices"
@@ -93,3 +94,26 @@ func SortNetIPAddrs(addrs []netip.Addr, preferIPv6 bool) {
return 1
})
}
// ParseSubnet parses s either as a CIDR prefix itself, or as an IP address,
// returning the corresponding single-IP CIDR prefix.
//
// TODO(e.burkov): Move to golibs.
func ParseSubnet(s string) (p netip.Prefix, err error) {
if strings.Contains(s, "/") {
p, err = netip.ParsePrefix(s)
if err != nil {
return netip.Prefix{}, err
}
} else {
var ip netip.Addr
ip, err = netip.ParseAddr(s)
if err != nil {
return netip.Prefix{}, err
}
p = netip.PrefixFrom(ip, ip.BitLen())
}
return p, nil
}
+14
View File
@@ -0,0 +1,14 @@
// Package osutil contains utilities for functions requiring system calls and
// other OS-specific APIs, except for network-related ones.
package osutil
import "io/fs"
// RootDirFS returns the fs.FS rooted at the operating system's root. On
// Windows it returns the fs.FS rooted at the volume of the system directory
// (usually, C:).
//
// TODO(e.burkov): Move to golibs.
func RootDirFS() (fsys fs.FS) {
return rootDirFS()
}
+12
View File
@@ -0,0 +1,12 @@
//go:build !windows
package osutil
import (
"io/fs"
"os"
)
func rootDirFS() (fsys fs.FS) {
return os.DirFS("/")
}
+25
View File
@@ -0,0 +1,25 @@
//go:build windows
package osutil
import (
"io/fs"
"os"
"path/filepath"
"github.com/AdguardTeam/golibs/log"
"golang.org/x/sys/windows"
)
func rootDirFS() (fsys fs.FS) {
// TODO(a.garipov): Use a better way if golang/go#44279 is ever resolved.
sysDir, err := windows.GetSystemDirectory()
if err != nil {
log.Error("aghos: getting root filesystem: %s; using C:", err)
// Assume that C: is the safe default.
return os.DirFS("C:")
}
return os.DirFS(filepath.VolumeName(sysDir))
}
+9 -9
View File
@@ -8,27 +8,27 @@ require (
github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601
github.com/kisielk/errcheck v1.6.3
github.com/kyoh86/looppointer v0.2.1
github.com/securego/gosec/v2 v2.16.0
github.com/securego/gosec/v2 v2.17.0
github.com/uudashr/gocognit v1.0.7
golang.org/x/tools v0.12.0
golang.org/x/vuln v1.0.0
honnef.co/go/tools v0.4.3
golang.org/x/tools v0.13.0
golang.org/x/vuln v1.0.1
honnef.co/go/tools v0.4.6
mvdan.cc/gofumpt v0.5.0
mvdan.cc/unparam v0.0.0-20230815095028-f7c6fb1088f0
mvdan.cc/unparam v0.0.0-20230917202934-3ee2d22f45fb
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/ccojocar/zxcvbn-go v1.0.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/kyoh86/nolint v0.0.1 // indirect
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20230307190834-24139beb5833 // indirect
golang.org/x/exp/typeparams v0.0.0-20230811145659-89c5cff77bcb // indirect
golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/sys v0.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+23 -26
View File
@@ -1,6 +1,7 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ccojocar/zxcvbn-go v1.0.1 h1:+sxrANSCj6CdadkcMnvde/GWU1vZiiXRbqYSCalV4/4=
github.com/ccojocar/zxcvbn-go v1.0.1/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
@@ -14,8 +15,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 h1:mrEEilTAUmaAORhssPPkxj84TsHrPMLBGW2Z4SoTxm8=
@@ -28,16 +29,12 @@ github.com/kyoh86/looppointer v0.2.1 h1:Jx9fnkBj/JrIryBLMTYNTj9rvc2SrPS98Dg0w7fx
github.com/kyoh86/looppointer v0.2.1/go.mod h1:q358WcM8cMWU+5vzqukvaZtnJi1kw/MpRHQm3xvTrjw=
github.com/kyoh86/nolint v0.0.1 h1:GjNxDEkVn2wAxKHtP7iNTrRxytRZ1wXxLV5j4XzGfRU=
github.com/kyoh86/nolint v0.0.1/go.mod h1:1ZiZZ7qqrZ9dZegU96phwVcdQOMKIqRzFJL3ewq9gtI=
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/securego/gosec/v2 v2.16.0 h1:Pi0JKoasQQ3NnoRao/ww/N/XdynIB9NRYYZT5CyOs5U=
github.com/securego/gosec/v2 v2.16.0/go.mod h1:xvLcVZqUfo4aAQu56TNv7/Ltz6emAOQAEsrZrt7uGlI=
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/securego/gosec/v2 v2.17.0 h1:ZpAStTDKY39insEG9OH6kV3IkhQZPTq9a9eGOLOjcdI=
github.com/securego/gosec/v2 v2.17.0/go.mod h1:lt+mgC91VSmriVoJLentrMkRCYs+HLTBnUFUBuhV2hc=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/uudashr/gocognit v1.0.7 h1:e9aFXgKgUJrQ5+bs61zBigmj7bFJ/5cC6HmMahVzuDo=
github.com/uudashr/gocognit v1.0.7/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY=
@@ -52,8 +49,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp/typeparams v0.0.0-20230811145659-89c5cff77bcb h1:v3JOchFBzuOEFQgVl0t5JnLg3yx29q2e1IjrEovxAt4=
golang.org/x/exp/typeparams v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9 h1:j3D9DvWRpUfIyFfDPws7LoIZ2MAI1OJHdQXtTnYtN+k=
golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
@@ -66,7 +63,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -82,24 +79,24 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/vuln v1.0.0 h1:tYLAU3jD9LQr98Y+3el06lWyGMCnvzw06PIWP3LIy7g=
golang.org/x/vuln v1.0.0/go.mod h1:V0eyhHwaAaHrt42J9bgrN6rd12f6GU4T0Lu0ex2wDg4=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/vuln v1.0.1 h1:KUas02EjQK5LTuIx1OylBQdKKZ9jeugs+HiqO5HormU=
golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -107,9 +104,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.4.3 h1:o/n5/K5gXqk8Gozvs2cnL0F2S1/g1vcGCAx2vETjITw=
honnef.co/go/tools v0.4.3/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA=
honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8=
honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
mvdan.cc/unparam v0.0.0-20230815095028-f7c6fb1088f0 h1:NAENkqZ+Xofhqs4R4Af+i3HpZj1M23SFn/lHfRh1D4E=
mvdan.cc/unparam v0.0.0-20230815095028-f7c6fb1088f0/go.mod h1:flQN1deud3vIpPdF88533Lpp/MvzGLgPIPjB1kgBf4I=
mvdan.cc/unparam v0.0.0-20230917202934-3ee2d22f45fb h1:xiF91GJnDSbyPdiZB5d52N2VpZfGhjM4Ji75cjzuooQ=
mvdan.cc/unparam v0.0.0-20230917202934-3ee2d22f45fb/go.mod h1:ZzZjEpJDOmx8TdVU6umamY3Xy0UAQUI2DHbf05USVbI=
+53 -11
View File
@@ -15,6 +15,9 @@ import (
"syscall"
"time"
"github.com/AdguardTeam/dnsproxy/internal/bootstrap"
proxynetutil "github.com/AdguardTeam/dnsproxy/internal/netutil"
"github.com/AdguardTeam/dnsproxy/internal/osutil"
"github.com/AdguardTeam/dnsproxy/internal/version"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
@@ -399,13 +402,21 @@ func initUpstreams(config *proxy.Config, options *Options) {
}
}
var err error
timeout := options.Timeout.Duration
bootOpts := &upstream.Options{
HTTPVersions: httpVersions,
InsecureSkipVerify: options.Insecure,
Timeout: timeout,
}
boot, err := initBootstrap(options.BootstrapDNS, bootOpts)
if err != nil {
log.Fatalf("error while initializing bootstrap: %s", err)
}
upsOpts := &upstream.Options{
HTTPVersions: httpVersions,
InsecureSkipVerify: options.Insecure,
Bootstrap: options.BootstrapDNS,
Bootstrap: boot,
Timeout: timeout,
}
upstreams := loadServersList(options.Upstreams)
@@ -417,7 +428,7 @@ func initUpstreams(config *proxy.Config, options *Options) {
privUpsOpts := &upstream.Options{
HTTPVersions: httpVersions,
Bootstrap: options.BootstrapDNS,
Bootstrap: boot,
Timeout: mathutil.Min(defaultLocalTimeout, timeout),
}
privUpstreams := loadServersList(options.PrivateRDNSUpstreams)
@@ -449,6 +460,39 @@ func initUpstreams(config *proxy.Config, options *Options) {
}
}
// initBootstrap initializes the [upstream.Resolver] for bootstrapping upstream
// servers. It returns the default resolver if no bootstraps were specified.
// The returned resolver will also use system hosts files first.
func initBootstrap(bootstraps []string, opts *upstream.Options) (r upstream.Resolver, err error) {
var resolvers []upstream.Resolver
for i, b := range bootstraps {
var resolver upstream.Resolver
resolver, err = upstream.NewUpstreamResolver(b, opts)
if err != nil {
return nil, fmt.Errorf("creating bootstrap resolver at index %d: %w", i, err)
}
resolvers = append(resolvers, resolver)
}
switch len(resolvers) {
case 0:
etcHosts, hostsErr := bootstrap.NewDefaultHostsResolver(osutil.RootDirFS())
if hostsErr != nil {
log.Error("creating default hosts resolver: %s", hostsErr)
return net.DefaultResolver, nil
}
return upstream.ConsequentResolver{etcHosts, net.DefaultResolver}, nil
case 1:
return resolvers[0], nil
default:
return upstream.ParallelResolver(resolvers), nil
}
}
// initEDNS inits EDNS-related config
func initEDNS(config *proxy.Config, options *Options) {
if options.EDNSAddr != "" {
@@ -470,15 +514,13 @@ func initBogusNXDomain(config *proxy.Config, options *Options) {
return
}
for _, s := range options.BogusNXDomain {
subnet, err := netutil.ParseSubnet(s)
for i, s := range options.BogusNXDomain {
p, err := proxynetutil.ParseSubnet(s)
if err != nil {
log.Error("%s", err)
continue
log.Error("parsing bogus nxdomain subnet at index %d: %s", i, err)
} else {
config.BogusNXDomain = append(config.BogusNXDomain, p)
}
config.BogusNXDomain = append(config.BogusNXDomain, subnet)
}
}
+3 -4
View File
@@ -1,10 +1,9 @@
package proxy
import (
"net"
"net/netip"
"github.com/AdguardTeam/dnsproxy/proxyutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
)
@@ -27,8 +26,8 @@ func (p *Proxy) isBogusNXDomain(m *dns.Msg) (ok bool) {
return false
}
func containsIP(nets []*net.IPNet, ip net.IP) (ok bool) {
if netutil.ValidateIP(ip) != nil {
func containsIP(nets []netip.Prefix, ip netip.Addr) (ok bool) {
if !ip.IsValid() {
return false
}
+24 -43
View File
@@ -2,10 +2,10 @@ package proxy
import (
"net"
"net/netip"
"testing"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
@@ -16,19 +16,12 @@ func TestProxy_IsBogusNXDomain(t *testing.T) {
prx := createTestProxy(t, nil)
prx.CacheEnabled = true
prx.BogusNXDomain = []*net.IPNet{{
IP: net.IP{4, 3, 2, 1},
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
}, {
IP: net.IPv4(1, 2, 3, 4),
Mask: net.IPv4Mask(255, 0, 0, 0),
}, {
IP: net.IP{10, 11, 12, 13},
Mask: net.CIDRMask(netutil.IPv4BitLen, netutil.IPv4BitLen),
}, {
IP: net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
Mask: net.CIDRMask(120, netutil.IPv6BitLen),
}}
prx.BogusNXDomain = []netip.Prefix{
netip.MustParsePrefix("4.3.2.1/24"),
netip.MustParsePrefix("1.2.3.4/8"),
netip.MustParsePrefix("10.11.12.13/32"),
netip.MustParsePrefix("102:304:506:708:90a:b0c:d0e:f10/120"),
}
testCases := []struct {
name string
@@ -103,64 +96,52 @@ func TestProxy_IsBogusNXDomain(t *testing.T) {
}
func TestContainsIP(t *testing.T) {
nets := []*net.IPNet{{
// IPv4.
IP: net.IP{1, 2, 3, 0},
Mask: net.IPv4Mask(255, 255, 255, 0),
}, {
// IPv6 from IPv4.
IP: net.IPv4(1, 2, 4, 0),
Mask: net.CIDRMask(16, 32),
}, {
// IPv6.
IP: net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0},
Mask: net.CIDRMask(120, net.IPv6len*8),
}}
nets := []netip.Prefix{
netip.MustParsePrefix("1.2.3.0/24"),
netip.MustParsePrefix("ffff::1.2.4.0/112"),
netip.MustParsePrefix("102:304:506:708:90a:b0c:d0e:f00/120"),
}
testCases := []struct {
name string
want assert.BoolAssertionFunc
ip net.IP
ip netip.Addr
name string
}{{
name: "ipv4_yes",
want: assert.True,
ip: net.IP{1, 2, 3, 255},
ip: netip.MustParseAddr("1.2.3.255"),
}, {
name: "ipv4_6_yes",
want: assert.True,
ip: net.IPv4(1, 2, 4, 254),
ip: netip.MustParseAddr("ffff::1.2.4.254"),
}, {
name: "ipv6_yes",
want: assert.True,
ip: net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
ip: netip.MustParseAddr("102:304:506:708:90a:b0c:d0e:f0f"),
}, {
name: "ipv6_4_yes",
want: assert.True,
ip: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 1, 2, 3, 0},
ip: netip.MustParseAddr("ffff::1.2.3.0"),
}, {
name: "ipv4_no",
want: assert.False,
ip: net.IP{2, 1, 3, 255},
ip: netip.MustParseAddr("2.1.3.255"),
}, {
name: "ipv4_6_no",
want: assert.False,
ip: net.IPv4(2, 1, 4, 254),
ip: netip.MustParseAddr("2.1.4.254"),
}, {
name: "ipv6_no",
want: assert.False,
ip: net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 15},
ip: netip.MustParseAddr("102:304:506:708:90a:b0c:d0e:10f"),
}, {
name: "ipv6_4_no",
want: assert.False,
ip: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 2, 1, 4, 0},
ip: netip.MustParseAddr("ffff::2.1.4.0"),
}, {
name: "nil_no",
name: "invalid",
want: assert.False,
ip: nil,
}, {
name: "bad_ip",
want: assert.False,
ip: net.IP{42},
ip: netip.Addr{},
}}
for _, tc := range testCases {
+1 -1
View File
@@ -111,7 +111,7 @@ type Config struct {
// BogusNXDomain is the set of networks used to transform responses into
// NXDOMAIN ones if they contain at least a single IP address within these
// networks. It's similar to dnsmasq's "bogus-nxdomain".
BogusNXDomain []*net.IPNet
BogusNXDomain []netip.Prefix
// Enable EDNS Client Subnet option DNS requests to the upstream server will
// contain an OPT record with Client Subnet option. If the original request
+18 -3
View File
@@ -353,6 +353,11 @@ func TestUpstreamsSort(t *testing.T) {
func TestExchangeWithReservedDomains(t *testing.T) {
dnsProxy := createTestProxy(t, nil)
googleRslv, err := upstream.NewUpstreamResolver("8.8.8.8", &upstream.Options{
Timeout: 1 * time.Second,
})
require.NoError(t, err)
// Upstreams specification. Domains adguard.com and google.ru reserved
// with fake upstreams, maps.google.ru excluded from dnsmasq.
upstreams := []string{
@@ -365,7 +370,7 @@ func TestExchangeWithReservedDomains(t *testing.T) {
upstreams,
&upstream.Options{
InsecureSkipVerify: false,
Bootstrap: []string{"8.8.8.8"},
Bootstrap: googleRslv,
Timeout: 1 * time.Second,
},
)
@@ -435,6 +440,11 @@ func TestOneByOneUpstreamsExchange(t *testing.T) {
)
require.NoError(t, err)
googleRslv, err := upstream.NewUpstreamResolver("8.8.8.8:53", &upstream.Options{
Timeout: timeOut,
})
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"}
dnsProxy.UpstreamConfig.Upstreams = []upstream.Upstream{}
@@ -443,7 +453,7 @@ func TestOneByOneUpstreamsExchange(t *testing.T) {
u, err = upstream.AddressToUpstream(
line,
&upstream.Options{
Bootstrap: []string{"8.8.8.8:53"},
Bootstrap: googleRslv,
Timeout: timeOut,
},
)
@@ -624,11 +634,16 @@ func TestFallbackFromInvalidBootstrap(t *testing.T) {
)
require.NoError(t, err)
invalidRslv, err := upstream.NewUpstreamResolver("8.8.8.8:555", &upstream.Options{
Timeout: 1 * time.Second,
})
require.NoError(t, err)
// Using a DoT server with invalid bootstrap.
u, _ := upstream.AddressToUpstream(
"tls://dns.adguard.com",
&upstream.Options{
Bootstrap: []string{"8.8.8.8:555"},
Bootstrap: invalidRslv,
Timeout: timeout,
},
)
-2
View File
@@ -89,8 +89,6 @@ func ParseUpstreamsConfig(upstreamConfig []string, options *upstream.Options) (*
options = &upstream.Options{}
}
log.Debug("Bootstraps: %s", options.Bootstrap)
p := &configParser{
options: options,
upstreamsIndex: map[string]upstream.Upstream{},
+3 -4
View File
@@ -23,7 +23,7 @@ func TestUpstreamConfig_GetUpstreamsForDomain(t *testing.T) {
upstreams,
&upstream.Options{
InsecureSkipVerify: false,
Bootstrap: []string{},
Bootstrap: nil,
Timeout: 1 * time.Second,
},
)
@@ -84,7 +84,6 @@ func TestUpstreamConfig_GetUpstreamsForDS(t *testing.T) {
upstreams,
&upstream.Options{
InsecureSkipVerify: false,
Bootstrap: []string{},
Timeout: 1 * time.Second,
},
)
@@ -172,7 +171,7 @@ func TestGetUpstreamsForDomainWithoutDuplicates(t *testing.T) {
upstreams,
&upstream.Options{
InsecureSkipVerify: false,
Bootstrap: []string{},
Bootstrap: nil,
Timeout: 1 * time.Second,
},
)
@@ -349,7 +348,7 @@ func BenchmarkGetUpstreamsForDomain(b *testing.B) {
upstreams,
&upstream.Options{
InsecureSkipVerify: false,
Bootstrap: []string{},
Bootstrap: nil,
Timeout: 1 * time.Second,
},
)
+8 -5
View File
@@ -4,7 +4,7 @@ package proxyutil
import (
"encoding/binary"
"net"
"net/netip"
"github.com/miekg/dns"
)
@@ -19,15 +19,18 @@ func AddPrefix(b []byte) (m []byte) {
}
// IPFromRR returns the IP address from rr if any.
func IPFromRR(rr dns.RR) (ip net.IP) {
func IPFromRR(rr dns.RR) (ip netip.Addr) {
var data []byte
switch rr := rr.(type) {
case *dns.A:
ip = rr.A.To4()
data = rr.A.To4()
case *dns.AAAA:
ip = rr.AAAA
data = rr.AAAA
default:
// Go on.
return netip.Addr{}
}
ip, _ = netip.AddrFromSlice(data)
return ip
}
+7 -5
View File
@@ -173,26 +173,28 @@ run_linter govulncheck ./...
run_linter gocyclo --over 10\
./internal/bootstrap/\
./internal/netutil/\
./internal/osutil/\
./internal/version/\
./proxyutil/\
./upstream/\
;
run_linter gocyclo --over 20 ./main.go
run_linter gocyclo --over 18 ./fastip/
run_linter gocyclo --over 15 ./proxy/
run_linter gocyclo --over 14 ./upstream/
# TODO(a.garipov): Enable for all.
run_linter gocognit --over 10\
./internal/bootstrap/\
./internal/osutil/\
./internal/version/\
./proxyutil/\
./upstream/\
;
run_linter gocognit --over 39 ./main.go
run_linter gocognit --over 33 ./proxy/
run_linter gocognit --over 32 ./fastip/
run_linter gocognit --over 24 ./upstream/
run_linter gocognit --over 35 ./main.go
run_linter gocognit --over 31 ./proxy/
run_linter gocognit --over 29 ./fastip/
run_linter gocognit --over 14 ./internal/netutil/
run_linter ineffassign ./...
+2 -12
View File
@@ -49,9 +49,6 @@ type dnsOverHTTPS struct {
// one.
getDialer DialerInitializer
// closeBoot is the function to close the bootstrap upstreams.
closeBoot closeFunc
// addr is the DNS-over-HTTPS server URL.
addr *url.URL
@@ -93,15 +90,8 @@ func newDoH(addr *url.URL, opts *Options) (u Upstream, err error) {
httpVersions = DefaultHTTPVersions
}
getDialer, closeBoot, err := newDialerInitializer(addr, opts)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
ups := &dnsOverHTTPS{
getDialer: getDialer,
closeBoot: closeBoot,
getDialer: newDialerInitializer(addr, opts),
addr: addr,
quicConf: &quic.Config{
KeepAlivePeriod: QUICKeepAlivePeriod,
@@ -208,7 +198,7 @@ func (p *dnsOverHTTPS) Close() (err error) {
err = p.closeClient(p.client)
}
return errors.Join(err, errors.Annotate(p.closeBoot(), "closing bootstrap: %w"))
return err
}
// closeClient cleans up resources used by client if necessary. Note, that at
+4 -16
View File
@@ -31,9 +31,6 @@ type dnsOverTLS struct {
// new one.
getDialer DialerInitializer
// closeBoot is the function to close the bootstrap upstreams.
closeBoot closeFunc
// tlsConf is the configuration of TLS.
tlsConf *tls.Config
@@ -50,23 +47,13 @@ type dnsOverTLS struct {
conns []net.Conn
}
// type check
var _ Upstream = (*dnsOverTLS)(nil)
// newDoT returns the DNS-over-TLS Upstream.
func newDoT(addr *url.URL, opts *Options) (ups Upstream, err error) {
addPort(addr, defaultPortDoT)
getDialer, closeBoot, err := newDialerInitializer(addr, opts)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
tlsUps := &dnsOverTLS{
addr: addr,
getDialer: getDialer,
closeBoot: closeBoot,
getDialer: newDialerInitializer(addr, opts),
// #nosec G402 -- TLS certificate verification could be disabled by
// configuration.
tlsConf: &tls.Config{
@@ -90,6 +77,9 @@ func newDoT(addr *url.URL, opts *Options) (ups Upstream, err error) {
return tlsUps, nil
}
// type check
var _ Upstream = (*dnsOverTLS)(nil)
// Address implements the [Upstream] interface for *dnsOverTLS.
func (p *dnsOverTLS) Address() string { return p.addr.String() }
@@ -151,8 +141,6 @@ func (p *dnsOverTLS) Close() (err error) {
}
}
closeErrs = append(closeErrs, errors.Annotate(p.closeBoot(), "closing bootstrap: %w"))
return errors.Join(closeErrs...)
}
+61 -55
View File
@@ -5,15 +5,19 @@ import (
"net/netip"
"time"
"github.com/AdguardTeam/dnsproxy/internal/bootstrap"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
)
// ErrNoUpstreams is returned from the methods that expect at least a single
// upstream to work with when no upstreams specified.
const ErrNoUpstreams errors.Error = "no upstream specified"
const (
// ErrNoUpstreams is returned from the methods that expect at least a single
// upstream to work with when no upstreams specified.
ErrNoUpstreams errors.Error = "no upstream specified"
// ErrNoReply is returned from [ExchangeAll] when no upstreams replied.
ErrNoReply errors.Error = "no reply"
)
// ExchangeParallel returns the dirst successful response from one of u. It
// returns an error if all upstreams failed to exchange the request.
@@ -30,23 +34,21 @@ func ExchangeParallel(u []Upstream, req *dns.Msg) (reply *dns.Msg, resolved Upst
// Go on.
}
ch := make(chan *exchangeResult, upsNum)
resCh := make(chan *ExchangeAllResult)
errCh := make(chan error)
for _, f := range u {
go exchangeAsync(f, req, ch)
go exchangeAsync(f, req, resCh, errCh)
}
errs := []error{}
for range u {
rep := <-ch
if rep.err != nil {
errs = append(errs, rep.err)
continue
}
if rep.reply != nil {
return rep.reply, rep.upstream, nil
select {
case excErr := <-errCh:
errs = append(errs, excErr)
case rep := <-resCh:
if rep.Resp != nil {
return rep.Resp, rep.Upstream, nil
}
}
}
@@ -54,8 +56,7 @@ func ExchangeParallel(u []Upstream, req *dns.Msg) (reply *dns.Msg, resolved Upst
return nil, nil, errors.Error("none of upstream servers responded")
}
// TODO(e.burkov): Use [errors.Join] in Go 1.20.
return nil, nil, errors.List("all upstreams failed to respond", errs...)
return nil, nil, errors.Join(errs...)
}
// ExchangeAllResult is the successful result of [ExchangeAll] for a single
@@ -81,7 +82,7 @@ func ExchangeAll(ups []Upstream, req *dns.Msg) (res []ExchangeAllResult, err err
if err != nil {
return nil, err
} else if reply == nil {
return nil, errors.Error("no reply")
return nil, ErrNoReply
}
return []ExchangeAllResult{{Upstream: ups[0], Resp: reply}}, nil
@@ -90,33 +91,25 @@ func ExchangeAll(ups []Upstream, req *dns.Msg) (res []ExchangeAllResult, err err
}
res = make([]ExchangeAllResult, 0, upsl)
errs := make([]error, 0, upsl)
resCh := make(chan *exchangeResult, upsl)
var errs []error
resCh := make(chan *ExchangeAllResult)
errCh := make(chan error)
// Start exchanging concurrently.
for _, u := range ups {
go exchangeAsync(u, req, resCh)
go exchangeAsync(u, req, resCh, errCh)
}
// Wait for all exchanges to finish.
for range ups {
rep := <-resCh
if rep.err != nil {
errs = append(errs, rep.err)
continue
var r *ExchangeAllResult
r, err = receiveAsyncResult(resCh, errCh)
if err != nil {
errs = append(errs, err)
} else {
res = append(res, *r)
}
if rep.reply == nil {
errs = append(errs, errors.Error("no reply"))
continue
}
res = append(res, ExchangeAllResult{
Resp: rep.reply,
Upstream: rep.upstream,
})
}
if len(errs) == upsl {
@@ -127,26 +120,33 @@ func ExchangeAll(ups []Upstream, req *dns.Msg) (res []ExchangeAllResult, err err
return res, nil
}
// exchangeResult represents the result of DNS exchange.
type exchangeResult = struct {
// upstream is the Upstream that successfully resolved the request.
upstream Upstream
// receiveAsyncResult receives a single result from resCh or an error from
// errCh. It returns either a non-nil result or an error.
func receiveAsyncResult(
resCh chan *ExchangeAllResult,
errCh chan error,
) (res *ExchangeAllResult, err error) {
select {
case err = <-errCh:
return nil, err
case rep := <-resCh:
if rep.Resp == nil {
return nil, ErrNoReply
}
// reply is the response DNS request resolved into.
reply *dns.Msg
// err is the error that occurred while resolving the request.
err error
return rep, nil
}
}
// exchangeAsync tries to resolve DNS request with one upstream and sends the
// result to respCh.
func exchangeAsync(u Upstream, req *dns.Msg, respCh chan *exchangeResult) {
res := &exchangeResult{upstream: u}
res.reply, res.err = exchangeAndLog(u, req)
respCh <- res
func exchangeAsync(u Upstream, req *dns.Msg, respCh chan *ExchangeAllResult, errCh chan error) {
reply, err := exchangeAndLog(u, req)
if err != nil {
errCh <- err
} else {
respCh <- &ExchangeAllResult{Resp: reply, Upstream: u}
}
}
// exchangeAndLog wraps the [Upstream.Exchange] method with logging.
@@ -169,6 +169,12 @@ func exchangeAndLog(u Upstream, req *dns.Msg) (resp *dns.Msg, err error) {
// LookupParallel tries to lookup for ip of host with all resolvers
// concurrently.
func LookupParallel(ctx context.Context, resolvers []Resolver, host string) ([]netip.Addr, error) {
return bootstrap.LookupParallel(ctx, resolvers, host)
//
// Deprecated: Use [ParallelResolver] instead.
func LookupParallel(
ctx context.Context,
resolvers []Resolver,
host string,
) (addrs []netip.Addr, err error) {
return ParallelResolver(resolvers).LookupNetIP(ctx, "ip", host)
}
+2 -2
View File
@@ -75,8 +75,8 @@ func TestLookupParallelEmpty(t *testing.T) {
u2 := testUpstream{}
resolvers := []Resolver{}
resolvers = append(resolvers, &upstreamResolver{Upstream: &u1})
resolvers = append(resolvers, &upstreamResolver{Upstream: &u2})
resolvers = append(resolvers, &UpstreamResolver{Upstream: &u1})
resolvers = append(resolvers, &UpstreamResolver{Upstream: &u2})
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
+5 -14
View File
@@ -37,9 +37,6 @@ type plainDNS struct {
// one.
getDialer DialerInitializer
// closeBoot is the function to close the bootstrap upstreams.
closeBoot closeFunc
// net is the network of the connections.
net network
@@ -47,9 +44,6 @@ type plainDNS struct {
timeout time.Duration
}
// type check
var _ Upstream = &plainDNS{}
// newPlain returns the plain DNS Upstream. addr.Scheme should be either "udp"
// or "tcp".
func newPlain(addr *url.URL, opts *Options) (u *plainDNS, err error) {
@@ -62,20 +56,17 @@ func newPlain(addr *url.URL, opts *Options) (u *plainDNS, err error) {
addPort(addr, defaultPortPlain)
getDialer, closeBoot, err := newDialerInitializer(addr, opts)
if err != nil {
return nil, err
}
return &plainDNS{
addr: addr,
getDialer: getDialer,
closeBoot: closeBoot,
getDialer: newDialerInitializer(addr, opts),
net: addr.Scheme,
timeout: opts.Timeout,
}, nil
}
// type check
var _ Upstream = &plainDNS{}
// Address implements the [Upstream] interface for *plainDNS.
func (p *plainDNS) Address() string {
switch p.net {
@@ -179,7 +170,7 @@ func (p *plainDNS) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
// Close implements the [Upstream] interface for *plainDNS.
func (p *plainDNS) Close() (err error) {
return errors.Annotate(p.closeBoot(), "closing bootstrap: %w")
return nil
}
// errQuestion is returned when a message has malformed question section.
+4 -13
View File
@@ -57,9 +57,6 @@ type dnsOverQUIC struct {
// one.
getDialer DialerInitializer
// closeBoot is the function to close the bootstrap upstreams.
closeBoot closeFunc
// addr is the DNS-over-QUIC server URL.
addr *url.URL
@@ -93,21 +90,12 @@ type dnsOverQUIC struct {
timeout time.Duration
}
// type check
var _ Upstream = (*dnsOverQUIC)(nil)
// newDoQ returns the DNS-over-QUIC Upstream.
func newDoQ(addr *url.URL, opts *Options) (u Upstream, err error) {
addPort(addr, defaultPortDoQ)
getDialer, closeBoot, err := newDialerInitializer(addr, opts)
if err != nil {
return nil, err
}
u = &dnsOverQUIC{
getDialer: getDialer,
closeBoot: closeBoot,
getDialer: newDialerInitializer(addr, opts),
addr: addr,
quicConfig: &quic.Config{
KeepAlivePeriod: QUICKeepAlivePeriod,
@@ -138,6 +126,9 @@ func newDoQ(addr *url.URL, opts *Options) (u Upstream, err error) {
return u, nil
}
// type check
var _ Upstream = (*dnsOverQUIC)(nil)
// Address implements the [Upstream] interface for *dnsOverQUIC.
func (p *dnsOverQUIC) Address() string { return p.addr.String() }
+247
View File
@@ -0,0 +1,247 @@
package upstream
import (
"context"
"fmt"
"net/netip"
"net/url"
"github.com/AdguardTeam/dnsproxy/internal/bootstrap"
"github.com/AdguardTeam/dnsproxy/proxyutil"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
"golang.org/x/exp/slices"
)
// Resolver is an alias for the internal [bootstrap.Resolver] to allow custom
// implementations. Note, that the [net.Resolver] from standard library also
// implements this interface.
type Resolver = bootstrap.Resolver
// UpstreamResolver is a wrapper around Upstream that implements the
// [bootstrap.Resolver] interface.
type UpstreamResolver struct {
// Upstream is used for lookups. It must not be nil.
Upstream
}
// NewUpstreamResolver creates an upstream that can be used as bootstrap
// [Resolver]. resolverAddress format is the same as in the
// [AddressToUpstream]. If the upstream can't be used as a bootstrap, the
// returned error will have the underlying type of [NotBootstrapError], and r
// itself will be fully usable. Closing r.Upstream is caller's responsibility.
func NewUpstreamResolver(resolverAddress string, opts *Options) (r *UpstreamResolver, err error) {
upsOpts := &Options{}
// TODO(ameshkov): Aren't other options needed here?
if opts != nil {
upsOpts.Timeout = opts.Timeout
upsOpts.VerifyServerCertificate = opts.VerifyServerCertificate
upsOpts.PreferIPv6 = opts.PreferIPv6
}
ups, err := AddressToUpstream(resolverAddress, upsOpts)
if err != nil {
err = fmt.Errorf("creating upstream: %w", err)
log.Error("upstream bootstrap: %s", err)
return nil, err
}
return &UpstreamResolver{Upstream: ups}, validateBootstrap(ups)
}
// NotBootstrapError is returned by [AddressToUpstream] when the parsed upstream
// can't be used as a bootstrap and wraps the actual reason.
type NotBootstrapError struct {
// err is the actual reason why the upstream can't be used as a bootstrap.
err error
}
// type check
var _ error = NotBootstrapError{}
// Error implements the [error] interface for NotBootstrapError.
func (e NotBootstrapError) Error() (msg string) {
return fmt.Sprintf("not a bootstrap: %s", e.err)
}
// type check
var _ errors.Wrapper = NotBootstrapError{}
// Unwrap implements the [errors.Wrapper] interface.
func (e NotBootstrapError) Unwrap() (reason error) {
return e.err
}
// validateBootstrap returns an error if u can't be used as a bootstrap.
func validateBootstrap(u Upstream) (err error) {
var upsURL *url.URL
switch u := u.(type) {
case *dnsCrypt:
return nil
case *plainDNS:
upsURL = u.addr
case *dnsOverTLS:
upsURL = u.addr
case *dnsOverHTTPS:
upsURL = u.addr
case *dnsOverQUIC:
upsURL = u.addr
default:
return fmt.Errorf("unknown upstream type: %T", u)
}
// Make sure the upstream doesn't need a bootstrap.
_, err = netip.ParseAddr(upsURL.Hostname())
if err != nil {
return NotBootstrapError{err: err}
}
return nil
}
// type check
var _ Resolver = &UpstreamResolver{}
// LookupNetIP implements the [Resolver] interface for upstreamResolver.
//
// TODO(e.burkov): Use context.
func (r *UpstreamResolver) LookupNetIP(
_ context.Context,
network string,
host string,
) (ips []netip.Addr, err error) {
if host == "" {
return nil, nil
}
switch network {
case "ip4", "ip6":
host = dns.Fqdn(host)
ips, err = r.resolve(host, network)
case "ip":
host = dns.Fqdn(host)
resCh := make(chan any, 2)
go r.resolveAsync(resCh, host, "ip4")
go r.resolveAsync(resCh, host, "ip6")
var errs []error
for i := 0; i < 2; i++ {
switch res := <-resCh; res := res.(type) {
case error:
errs = append(errs, res)
case []netip.Addr:
ips = append(ips, res...)
}
}
err = errors.Join(errs...)
default:
return []netip.Addr{}, fmt.Errorf("unsupported network %s", network)
}
if len(ips) == 0 {
ips = []netip.Addr{}
}
return ips, err
}
// resolve performs a single DNS lookup of host and returns all the valid
// addresses from the answer section of the response. network must be either
// "ip4" or "ip6".
func (r *UpstreamResolver) resolve(host, network string) (addrs []netip.Addr, err error) {
qtype := dns.TypeA
if network == "ip6" {
qtype = dns.TypeAAAA
}
req := &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
RecursionDesired: true,
},
Question: []dns.Question{{
Name: host,
Qtype: qtype,
Qclass: dns.ClassINET,
}},
}
resp, err := r.Upstream.Exchange(req)
if err != nil || resp == nil {
return nil, err
}
for _, rr := range resp.Answer {
if addr := proxyutil.IPFromRR(rr); addr.IsValid() {
addrs = append(addrs, addr)
}
}
return addrs, nil
}
// resolveAsync performs a single DNS lookup and sends the result to ch. It's
// intended to be used as a goroutine.
func (r *UpstreamResolver) resolveAsync(resCh chan<- any, host, network string) {
resp, err := r.resolve(host, network)
if err != nil {
resCh <- err
} else {
resCh <- resp
}
}
// StaticResolver is a resolver which always responds with an underlying slice
// of IP addresses.
type StaticResolver []netip.Addr
// type check
var _ Resolver = StaticResolver(nil)
// LookupNetIP implements the [Resolver] interface for StaticResolver.
func (r StaticResolver) LookupNetIP(
ctx context.Context,
network string,
host string,
) (addrs []netip.Addr, err error) {
return slices.Clone(r), nil
}
// ConsequentResolver is a slice of resolvers that are queried in order until
// the first successful non-empty response, as opposed to just successful
// response requirement in [ParallelResolver].
type ConsequentResolver []Resolver
// type check
var _ Resolver = ConsequentResolver(nil)
// LookupNetIP implements the [Resolver] interface for ConsequentResolver.
func (resolvers ConsequentResolver) LookupNetIP(
ctx context.Context,
network string,
host string,
) (addrs []netip.Addr, err error) {
if len(resolvers) == 0 {
return nil, bootstrap.ErrNoResolvers
}
var errs []error
for _, r := range resolvers {
addrs, err = r.LookupNetIP(ctx, network, host)
if err == nil && len(addrs) > 0 {
return addrs, nil
}
errs = append(errs, err)
}
return nil, errors.Join(errs...)
}
// ParallelResolver is an alias for the internal [bootstrap.ParallelResolver] to
// allow it's usage outside of the module.
type ParallelResolver = bootstrap.ParallelResolver
@@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -49,22 +50,22 @@ func TestNewUpstreamResolver_validity(t *testing.T) {
}, {
name: "invalid_tls",
addr: "tls://dns.adguard.com",
wantErrMsg: `bootstrap tls://dns.adguard.com:853: ` +
`ParseAddr("dns.adguard.com"): unexpected character (at "dns.adguard.com")`,
wantErrMsg: `not a bootstrap: ParseAddr("dns.adguard.com"): ` +
`unexpected character (at "dns.adguard.com")`,
}, {
name: "invalid_https",
addr: "https://dns.adguard.com/dns-query",
wantErrMsg: `bootstrap https://dns.adguard.com:443/dns-query: ` +
`ParseAddr("dns.adguard.com"): unexpected character (at "dns.adguard.com")`,
wantErrMsg: `not a bootstrap: ParseAddr("dns.adguard.com"): ` +
`unexpected character (at "dns.adguard.com")`,
}, {
name: "invalid_tcp",
addr: "tcp://dns.adguard.com",
wantErrMsg: `bootstrap tcp://dns.adguard.com:53: ` +
`ParseAddr("dns.adguard.com"): unexpected character (at "dns.adguard.com")`,
wantErrMsg: `not a bootstrap: ParseAddr("dns.adguard.com"): ` +
`unexpected character (at "dns.adguard.com")`,
}, {
name: "invalid_no_scheme",
addr: "dns.adguard.com",
wantErrMsg: `bootstrap dns.adguard.com:53: ParseAddr("dns.adguard.com"): ` +
wantErrMsg: `not a bootstrap: ParseAddr("dns.adguard.com"): ` +
`unexpected character (at "dns.adguard.com")`,
}}
@@ -73,6 +74,9 @@ func TestNewUpstreamResolver_validity(t *testing.T) {
r, err := NewUpstreamResolver(tc.addr, withTimeoutOpt)
if tc.wantErrMsg != "" {
assert.Equal(t, tc.wantErrMsg, err.Error())
if nberr := (&NotBootstrapError{}); errors.As(err, &nberr) {
assert.NotNil(t, r)
}
return
}
+20 -104
View File
@@ -17,7 +17,6 @@ import (
"time"
"github.com/AdguardTeam/dnsproxy/internal/bootstrap"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/ameshkov/dnscrypt/v2"
@@ -25,7 +24,6 @@ import (
"github.com/miekg/dns"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/logging"
"golang.org/x/exp/slices"
)
// Upstream is an interface for a DNS resolver.
@@ -78,16 +76,9 @@ type Options struct {
// CipherSuites is a custom list of TLSv1.2 ciphers.
CipherSuites []uint16
// Bootstrap is a list of DNS servers to be used to resolve DoH/DoT/DoQ
// hostnames. Plain DNS, DNSCrypt, or DoH/DoT/DoQ with IP addresses (not
// hostnames) could be used. Those servers will be turned to upstream
// servers and will be closed as soon as the resolved upstream itself is
// closed.
Bootstrap []string
// List of IP addresses of the upstream DNS server. If not empty, bootstrap
// DNS servers won't be used at all.
ServerIPAddrs []net.IP
// Bootstrap is used to resolve upstreams' hostnames. If nil, the
// [net.DefaultResolver] will be used.
Bootstrap Resolver
// HTTPVersions is a list of HTTP versions that should be supported by the
// DNS-over-HTTPS client. If not set, HTTP/1.1 and HTTP/2 will be used.
@@ -110,7 +101,6 @@ func (o *Options) Clone() (clone *Options) {
return &Options{
Bootstrap: o.Bootstrap,
Timeout: o.Timeout,
ServerIPAddrs: o.ServerIPAddrs,
HTTPVersions: o.HTTPVersions,
VerifyServerCertificate: o.VerifyServerCertificate,
VerifyConnection: o.VerifyConnection,
@@ -244,14 +234,13 @@ func parseStamp(upsURL *url.URL, opts *Options) (u Upstream, err error) {
host = stamp.ServerAddrStr
}
// Parse and add to options.
ip := net.ParseIP(host)
if ip == nil {
var ip netip.Addr
ip, err = netip.ParseAddr(host)
if err != nil {
return nil, fmt.Errorf("invalid server stamp address %s", stamp.ServerAddrStr)
}
// TODO(e.burkov): Append?
opts.ServerIPAddrs = []net.IP{ip}
opts.Bootstrap = StaticResolver{ip}
}
switch stamp.Proto {
@@ -311,53 +300,25 @@ func logFinish(upsAddr string, n network, err error) {
// resolving will be performed only once.
type DialerInitializer func() (handler bootstrap.DialHandler, err error)
// closeFunc is the signature of a function that closes an upstream.
type closeFunc func() (err error)
// nopClose is the [closeFunc] that does nothing.
func nopClose() (err error) { return nil }
// newDialerInitializer creates an initializer of the dialer that will dial the
// addresses resolved from u using opts.
//
// TODO(e.burkov): Returning closeFunc is a temporary solution. It's needed
// to close the bootstrap upstreams, which may require closing. It should be
// gone when the [Options.Bootstrap] will be turned into [Resolver] and it's
// closing will be handled by the caller.
func newDialerInitializer(
u *url.URL,
opts *Options,
) (di DialerInitializer, closeBoot closeFunc, err error) {
host, port, err := netutil.SplitHostPort(u.Host)
if err != nil {
return nil, nopClose, fmt.Errorf("invalid address: %s: %w", u.Host, err)
}
if addrsLen := len(opts.ServerIPAddrs); addrsLen > 0 {
// Don't resolve the addresses of the server since those from the
// options should be used.
addrs := make([]string, 0, addrsLen)
for _, addr := range opts.ServerIPAddrs {
addrs = append(addrs, netutil.JoinHostPort(addr.String(), port))
}
handler := bootstrap.NewDialContext(opts.Timeout, addrs...)
return func() (h bootstrap.DialHandler, err error) { return handler, nil }, nopClose, nil
} else if _, err = netip.ParseAddr(host); err == nil {
func newDialerInitializer(u *url.URL, opts *Options) (di DialerInitializer) {
if _, err := netip.ParseAddrPort(u.Host); err == nil {
// Don't resolve the address of the server since it's already an IP.
handler := bootstrap.NewDialContext(opts.Timeout, u.Host)
return func() (h bootstrap.DialHandler, err error) { return handler, nil }, nopClose, nil
return func() (bootstrap.DialHandler, error) { return handler, nil }
}
resolvers, closeBoot, err := newResolvers(opts)
if err != nil {
return nil, nopClose, errors.Join(err, closeBoot())
boot := opts.Bootstrap
if boot == nil {
// Use the default resolver for bootstrapping.
boot = net.DefaultResolver
}
var dialHandler atomic.Pointer[bootstrap.DialHandler]
di = func() (h bootstrap.DialHandler, resErr error) {
return func() (h bootstrap.DialHandler, err error) {
// Check if the dial handler has already been created.
if hPtr := dialHandler.Load(); hPtr != nil {
return *hPtr, nil
@@ -366,61 +327,16 @@ func newDialerInitializer(
// TODO(e.burkov): It may appear that several exchanges will try to
// resolve the upstream hostname at the same time. Currently, the last
// successful value will be stored in dialHandler, but ideally we should
// resolve only once at a time.
h, resolveErr := bootstrap.ResolveDialContext(u, opts.Timeout, resolvers, opts.PreferIPv6)
if resolveErr != nil {
return nil, fmt.Errorf("creating dial handler: %w", resolveErr)
// resolve only once.
h, err = bootstrap.ResolveDialContext(u, opts.Timeout, boot, opts.PreferIPv6)
if err != nil {
return nil, fmt.Errorf("creating dial handler: %w", err)
}
if !dialHandler.CompareAndSwap(nil, &h) {
// The dial handler has just been created by another exchange.
return *dialHandler.Load(), nil
}
return h, nil
}
return di, closeBoot, nil
}
// newResolvers prepares resolvers for bootstrapping. If opts.Bootstrap is
// empty, the only new [net.Resolver] will be returned. Otherwise, the it will
// be added for each occurrence of an empty string in [Options.Bootstrap].
func newResolvers(opts *Options) (resolvers []Resolver, closeBoot closeFunc, err error) {
bootstraps := opts.Bootstrap
l := len(bootstraps)
if l == 0 {
return []Resolver{&net.Resolver{}}, nopClose, nil
}
resolvers, closeBoots := make([]Resolver, 0, l), make([]closeFunc, 0, l)
for i, boot := range bootstraps {
if boot == "" {
resolvers = append(resolvers, &net.Resolver{})
continue
}
r, rErr := NewUpstreamResolver(boot, opts)
if rErr != nil {
resolvers = nil
err = fmt.Errorf("preparing bootstrap resolver at index %d: %w", i, rErr)
break
}
resolvers = append(resolvers, r)
closeBoots = append(closeBoots, r.(upstreamResolver).Close)
}
closeBoots = slices.Clip(closeBoots)
return resolvers, func() (closeErr error) {
var errs []error
for _, cb := range closeBoots {
errs = append(errs, cb())
}
return errors.Join(errs...)
}, err
}
+94 -61
View File
@@ -48,9 +48,14 @@ func TestUpstream_bootstrapTimeout(t *testing.T) {
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, udpListener.Close)
rslv, err := NewUpstreamResolver(udpListener.LocalAddr().String(), &Options{
Timeout: timeout,
})
require.NoError(t, err)
// Create an upstream that uses this faulty bootstrap.
u, err := AddressToUpstream("tls://random-domain-name", &Options{
Bootstrap: []string{udpListener.LocalAddr().String()},
Bootstrap: rslv,
Timeout: timeout,
})
require.NoError(t, err)
@@ -100,95 +105,105 @@ func TestUpstream_bootstrapTimeout(t *testing.T) {
}
func TestUpstreams(t *testing.T) {
googleRslv, err := NewUpstreamResolver("8.8.8.8:53", &Options{
Timeout: timeout,
})
require.NoError(t, err)
cloudflareRslv, err := NewUpstreamResolver("1.0.0.1:53", &Options{
Timeout: timeout,
})
require.NoError(t, err)
upstreams := []struct {
bootstrap Resolver
address string
bootstrap []string
}{{
bootstrap: googleRslv,
address: "8.8.8.8:53",
bootstrap: []string{"8.8.8.8:53"},
}, {
bootstrap: nil,
address: "1.1.1.1",
bootstrap: []string{},
}, {
bootstrap: cloudflareRslv,
address: "1.1.1.1",
bootstrap: []string{"1.0.0.1"},
}, {
bootstrap: nil,
address: "tcp://1.1.1.1:53",
bootstrap: []string{},
}, {
bootstrap: nil,
address: "94.140.14.14:5353",
bootstrap: []string{},
}, {
bootstrap: nil,
address: "tls://1.1.1.1",
bootstrap: []string{},
}, {
bootstrap: nil,
address: "tls://9.9.9.9:853",
bootstrap: []string{},
}, {
bootstrap: googleRslv,
address: "tls://dns.adguard.com",
bootstrap: []string{"8.8.8.8:53"},
}, {
bootstrap: googleRslv,
address: "tls://dns.adguard.com:853",
bootstrap: []string{"8.8.8.8:53"},
}, {
bootstrap: googleRslv,
address: "tls://dns.adguard.com:853",
bootstrap: []string{"8.8.8.8"},
}, {
bootstrap: nil,
address: "tls://one.one.one.one",
bootstrap: []string{},
}, {
bootstrap: googleRslv,
address: "https://1dot1dot1dot1.cloudflare-dns.com/dns-query",
bootstrap: []string{"8.8.8.8:53"},
}, {
bootstrap: nil,
address: "https://dns.google/dns-query",
bootstrap: []string{},
}, {
bootstrap: nil,
address: "https://doh.opendns.com/dns-query",
bootstrap: []string{},
}, {
// AdGuard DNS (DNSCrypt)
bootstrap: nil,
address: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
bootstrap: []string{},
}, {
// AdGuard Family (DNSCrypt)
bootstrap: googleRslv,
address: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMjo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
bootstrap: []string{"8.8.8.8"},
}, {
// Cloudflare DNS (DNS-over-HTTPS)
bootstrap: googleRslv,
address: "sdns://AgcAAAAAAAAABzEuMC4wLjGgENk8mGSlIfMGXMOlIlCcKvq7AVgcrZxtjon911-ep0cg63Ul-I8NlFj4GplQGb_TTLiczclX57DvMV8Q-JdjgRgSZG5zLmNsb3VkZmxhcmUuY29tCi9kbnMtcXVlcnk",
bootstrap: []string{"8.8.8.8:53"},
}, {
// Google (Plain)
bootstrap: nil,
address: "sdns://AAcAAAAAAAAABzguOC44Ljg",
bootstrap: []string{},
}, {
// AdGuard DNS (DNS-over-TLS)
bootstrap: googleRslv,
address: "sdns://AwAAAAAAAAAAAAAPZG5zLmFkZ3VhcmQuY29t",
bootstrap: []string{"8.8.8.8:53"},
}, {
// AdGuard DNS (DNS-over-QUIC)
bootstrap: googleRslv,
address: "sdns://BAcAAAAAAAAAAAAXZG5zLmFkZ3VhcmQtZG5zLmNvbTo3ODQ",
bootstrap: []string{"8.8.8.8:53"},
}, {
// Cloudflare DNS (DNS-over-HTTPS)
bootstrap: nil,
address: "https://1.1.1.1/dns-query",
bootstrap: []string{},
}, {
// AdGuard DNS (DNS-over-QUIC)
bootstrap: googleRslv,
address: "quic://dns.adguard-dns.com",
bootstrap: []string{"1.1.1.1:53"},
}, {
// Google DNS (HTTP3)
bootstrap: nil,
address: "h3://dns.google/dns-query",
bootstrap: []string{},
}}
for _, test := range upstreams {
t.Run(test.address, func(t *testing.T) {
u, err := AddressToUpstream(
u, upsErr := AddressToUpstream(
test.address,
&Options{Bootstrap: test.bootstrap, Timeout: timeout},
)
require.NoErrorf(t, err, "failed to generate upstream from address %s", test.address)
require.NoErrorf(t, upsErr, "failed to generate upstream from address %s", test.address)
testutil.CleanupAndRequireSuccess(t, u.Close)
checkUpstream(t, u, test.address)
@@ -197,7 +212,10 @@ func TestUpstreams(t *testing.T) {
}
func TestAddressToUpstream(t *testing.T) {
opt := &Options{Bootstrap: []string{"1.1.1.1"}}
cloudflareRslv, err := NewUpstreamResolver("1.1.1.1", nil)
require.NoError(t, err)
opt := &Options{Bootstrap: cloudflareRslv}
testCases := []struct {
addr string
@@ -235,8 +253,8 @@ func TestAddressToUpstream(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.addr, func(t *testing.T) {
u, err := AddressToUpstream(tc.addr, tc.opt)
require.NoError(t, err)
u, upsErr := AddressToUpstream(tc.addr, tc.opt)
require.NoError(t, upsErr)
testutil.CleanupAndRequireSuccess(t, u.Close)
assert.Equal(t, tc.want, u.Address())
@@ -275,23 +293,28 @@ func TestAddressToUpstream_bads(t *testing.T) {
func TestUpstreamDoTBootstrap(t *testing.T) {
upstreams := []struct {
address string
bootstrap []string
bootstrap string
}{{
address: "tls://one.one.one.one/",
bootstrap: []string{"tls://1.1.1.1"},
bootstrap: "tls://1.1.1.1",
}, {
address: "tls://one.one.one.one/",
bootstrap: []string{"https://1.1.1.1/dns-query"},
bootstrap: "https://1.1.1.1/dns-query",
}, {
address: "tls://one.one.one.one/",
// Cisco OpenDNS
bootstrap: []string{"sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ"},
bootstrap: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
}}
for _, tc := range upstreams {
t.Run(tc.address, func(t *testing.T) {
rslv, err := NewUpstreamResolver(tc.bootstrap, &Options{
Timeout: timeout,
})
require.NoError(t, err)
u, err := AddressToUpstream(tc.address, &Options{
Bootstrap: tc.bootstrap,
Bootstrap: rslv,
Timeout: timeout,
})
require.NoErrorf(t, err, "failed to generate upstream from address %s", tc.address)
@@ -331,8 +354,18 @@ func TestUpstreamsInvalidBootstrap(t *testing.T) {
for _, tc := range upstreams {
t.Run(tc.address, func(t *testing.T) {
var rslv ConsequentResolver
for _, b := range tc.bootstrap {
r, err := NewUpstreamResolver(b, &Options{
Timeout: timeout,
})
require.NoError(t, err)
rslv = append(rslv, r)
}
u, err := AddressToUpstream(tc.address, &Options{
Bootstrap: tc.bootstrap,
Bootstrap: rslv,
Timeout: timeout,
})
require.NoErrorf(t, err, "failed to generate upstream from address %s", tc.address)
@@ -342,16 +375,13 @@ func TestUpstreamsInvalidBootstrap(t *testing.T) {
})
}
_, err := AddressToUpstream("tls://example.org", &Options{
Bootstrap: []string{"8.8.8.8", "asdfasdf"},
t.Run("bad_bootstrap", func(t *testing.T) {
_, err := NewUpstreamResolver("asdfasdf", nil)
assert.Error(t, err) // bad bootstrap "asdfasdf"
})
assert.Error(t, err) // bad bootstrap "asdfasdf"
}
func TestUpstreamsWithServerIP(t *testing.T) {
// use invalid bootstrap to make sure it fails if tries to use it
invalidBootstrap := []string{"1.2.3.4:55"}
func TestAddressToUpstream_StaticResolver(t *testing.T) {
h := func(w dns.ResponseWriter, m *dns.Msg) {
require.NoError(testutil.PanicT{}, w.WriteMsg(respondToTestMessage(m)))
}
@@ -360,6 +390,8 @@ func TestUpstreamsWithServerIP(t *testing.T) {
_, dohPort, err := net.SplitHostPort(dohSrv.addr)
require.NoError(t, err)
badResolver := &UpstreamResolver{Upstream: nil}
dotStamp := (&dnsstamps.ServerStamp{
ServerAddrStr: netip.AddrPortFrom(netutil.IPv4Localhost(), uint16(dotSrv.port)).String(),
Proto: dnsstamps.StampProtoTypeTLS,
@@ -373,40 +405,41 @@ func TestUpstreamsWithServerIP(t *testing.T) {
}).String()
upstreams := []struct {
name string
address string
serverIPs []net.IP
rslv Resolver
name string
address string
}{{
name: "dot",
address: fmt.Sprintf("tls://some.dns.server:%d", dotSrv.port),
serverIPs: []net.IP{netutil.IPv4Localhost().AsSlice()},
rslv: StaticResolver{netutil.IPv4Localhost()},
name: "dot",
address: fmt.Sprintf("tls://some.dns.server:%d", dotSrv.port),
}, {
name: "doh",
address: fmt.Sprintf("https://some.dns.server:%s/dns-query", dohPort),
serverIPs: []net.IP{netutil.IPv4Localhost().AsSlice()},
rslv: StaticResolver{netutil.IPv4Localhost()},
name: "doh",
address: fmt.Sprintf("https://some.dns.server:%s/dns-query", dohPort),
}, {
name: "dot_stamp",
address: dotStamp,
serverIPs: nil,
rslv: badResolver,
name: "dot_stamp",
address: dotStamp,
}, {
name: "doh_stamp",
address: dohStamp,
serverIPs: nil,
rslv: badResolver,
name: "doh_stamp",
address: dohStamp,
}}
for _, tc := range upstreams {
t.Run(tc.name, func(t *testing.T) {
opts := &Options{
Bootstrap: invalidBootstrap,
Bootstrap: tc.rslv,
Timeout: timeout,
ServerIPAddrs: tc.serverIPs,
InsecureSkipVerify: true,
}
u, uErr := AddressToUpstream(tc.address, opts)
require.NoError(t, uErr)
testutil.CleanupAndRequireSuccess(t, u.Close)
checkUpstream(t, u, tc.address)
assert.NotPanics(t, func() {
checkUpstream(t, u, tc.address)
})
})
}
}
-195
View File
@@ -1,195 +0,0 @@
package upstream
import (
"context"
"fmt"
"net/netip"
"github.com/AdguardTeam/dnsproxy/internal/bootstrap"
proxynetutil "github.com/AdguardTeam/dnsproxy/internal/netutil"
"github.com/AdguardTeam/dnsproxy/proxyutil"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
)
// Resolver is an alias for [bootstrap.Resolver] to avoid the import cycle.
type Resolver = bootstrap.Resolver
// NewUpstreamResolver creates an upstream that can be used as [Resolver].
// resolverAddress format is the same as in the [AddressToUpstream], except that
// it also shouldn't need a bootstrap, i.e. have an IP address in hostname, or
// be a DNSCrypt. resolverAddress must not be empty, use another [Resolver]
// instead, e.g. [net.Resolver].
func NewUpstreamResolver(resolverAddress string, opts *Options) (r Resolver, err error) {
upsOpts := &Options{}
// TODO(ameshkov): Aren't other options needed here?
if opts != nil {
upsOpts.Timeout = opts.Timeout
upsOpts.VerifyServerCertificate = opts.VerifyServerCertificate
upsOpts.PreferIPv6 = opts.PreferIPv6
}
ur := upstreamResolver{}
ur.Upstream, err = AddressToUpstream(resolverAddress, upsOpts)
if err != nil {
err = fmt.Errorf("creating upstream: %w", err)
log.Error("upstream bootstrap: %s", err)
return ur, err
}
if err = validateBootstrap(ur.Upstream); err != nil {
log.Error("upstream bootstrap %s: %s", resolverAddress, err)
ur.Upstream = nil
return ur, err
}
return ur, err
}
// validateBootstrap returns error if the upstream is not eligible to be a
// bootstrap DNS server. DNSCrypt is always okay. Plain DNS, DNS-over-TLS,
// DNS-over-HTTPS, and DNS-over-QUIC are okay only if those are defined by IP.
func validateBootstrap(upstream Upstream) (err error) {
switch upstream := upstream.(type) {
case *dnsCrypt:
return nil
case *dnsOverTLS:
_, err = netip.ParseAddr(upstream.addr.Hostname())
case *dnsOverHTTPS:
_, err = netip.ParseAddr(upstream.addr.Hostname())
case *dnsOverQUIC:
_, err = netip.ParseAddr(upstream.addr.Hostname())
case *plainDNS:
_, err = netip.ParseAddr(upstream.addr.Hostname())
default:
err = fmt.Errorf("unknown upstream type: %T", upstream)
}
return errors.Annotate(err, "bootstrap %s: %w", upstream.Address())
}
// upstreamResolver is a wrapper around Upstream that implements the
// [bootstrap.Resolver] interface. It sorts the resolved addresses preferring
// IPv4.
type upstreamResolver struct {
// Upstream is embedded here to avoid implementing another Upstream's
// methods.
Upstream
}
// type check
var _ Resolver = upstreamResolver{}
// LookupNetIP implements the [Resolver] interface for upstreamResolver.
//
// TODO(e.burkov): Use context.
func (r upstreamResolver) LookupNetIP(
_ context.Context,
network string,
host string,
) (ipAddrs []netip.Addr, err error) {
// TODO(e.burkov): Investigate when [r.Upstream] is nil and why.
if r.Upstream == nil || host == "" {
return []netip.Addr{}, nil
}
host = dns.Fqdn(host)
answers := make([][]dns.RR, 1, 2)
var errs []error
switch network {
case "ip4", "ip6":
qtype := dns.TypeA
if network == "ip6" {
qtype = dns.TypeAAAA
}
var resp *dns.Msg
resp, err = r.resolve(host, qtype)
if err != nil {
return []netip.Addr{}, err
}
answers[0] = resp.Answer
case "ip":
resCh := make(chan *resolveResult, 2)
go r.resolveAsync(resCh, host, dns.TypeA)
go r.resolveAsync(resCh, host, dns.TypeAAAA)
answers = answers[:0:cap(answers)]
for i := 0; i < 2; i++ {
res := <-resCh
if res.err != nil {
errs = append(errs, res.err)
continue
}
answers = append(answers, res.resp.Answer)
}
default:
return []netip.Addr{}, fmt.Errorf("unsupported network %s", network)
}
for _, ans := range answers {
for _, rr := range ans {
if addr, ok := netip.AddrFromSlice(proxyutil.IPFromRR(rr)); ok {
ipAddrs = append(ipAddrs, addr)
}
}
}
// TODO(e.burkov): Use [errors.Join] in Go 1.20.
if len(ipAddrs) == 0 && len(errs) > 0 {
return []netip.Addr{}, errs[0]
}
// Use the previous dnsproxy behavior: prefer IPv4 by default.
//
// TODO(a.garipov): Consider unexporting this entire method or
// documenting that the order of addrs is undefined.
proxynetutil.SortNetIPAddrs(ipAddrs, false)
return ipAddrs, nil
}
// resolve performs a single DNS lookup of host.
func (r upstreamResolver) resolve(host string, qtype uint16) (resp *dns.Msg, err error) {
req := &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
RecursionDesired: true,
},
Question: []dns.Question{{
Name: host,
Qtype: qtype,
Qclass: dns.ClassINET,
}},
}
return r.Exchange(req)
}
// resolveResult is the result of a single concurrent lookup.
type resolveResult = struct {
resp *dns.Msg
err error
}
// resolveAsync performs a single DNS lookup and sends the result to ch. It's
// intended to be used as a goroutine.
func (r upstreamResolver) resolveAsync(
resCh chan<- *resolveResult,
host string,
qtype uint16,
) {
resp, err := r.resolve(host, qtype)
resCh <- &resolveResult{resp: resp, err: err}
}