From 652588c0cf48246ef8484bb69b95b81b3737fdc1 Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Mon, 7 Feb 2022 18:03:42 +0300 Subject: [PATCH] upstream: support udp scheme --- README.md | 23 ++ upstream/bootstrap.go | 5 +- upstream/upstream.go | 170 +++++-------- upstream/upstream_doh.go | 14 ++ upstream/upstream_dot.go | 14 ++ upstream/upstream_plain.go | 16 ++ upstream/upstream_quic.go | 19 ++ upstream/upstream_test.go | 482 ++++++++++++++++++------------------- 8 files changed, 390 insertions(+), 353 deletions(-) diff --git a/README.md b/README.md index b531677a6..a069b9a56 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,29 @@ Listen on multiple interfaces and ports: ./dnsproxy -l 127.0.0.1 -l 192.168.1.10 -p 5353 -p 5354 -u 1.1.1.1 ``` +The plain DNS upstream server may be specified in several ways: + + - With a plain IP address: + ```shell + ./dnsproxy -l 127.0.0.1 -u 8.8.8.8:53 + ``` + + - With a hostname or plain IP address and the `udp://` scheme: + ```shell + ./dnsproxy -l 127.0.0.1 -u udp://dns.google -u udp://1.1.1.1 + ``` + + - With a hostname or plain IP address and the `dns://` scheme (Deprecated): + ```shell + ./dnsproxy -l 127.0.0.1 -u dns://dns.google -u dns://1.1.1.1 + ``` + + - With a hostname or plain IP address and the `tcp://` scheme to force using + TCP: + ```shell + ./dnsproxy -l 127.0.0.1 -u tcp://dns.google -u tcp://1.1.1.1 + ``` + ### Encrypted upstreams DNS-over-TLS upstream: diff --git a/upstream/bootstrap.go b/upstream/bootstrap.go index 662d59ca7..26916a357 100755 --- a/upstream/bootstrap.go +++ b/upstream/bootstrap.go @@ -80,7 +80,7 @@ func newBootstrapperResolved(upsURL *url.URL, options *Options) (*bootstrapper, // newBootstrapper initializes a new bootstrapper instance // address -- original resolver address string (i.e. tls://one.one.one.one:853) // options -- Upstream customization options -func newBootstrapper(address *url.URL, options *Options) (*bootstrapper, error) { +func newBootstrapper(u *url.URL, options *Options) (*bootstrapper, error) { resolvers := []*Resolver{} if len(options.Bootstrap) != 0 { // Create a list of resolvers for parallel lookup @@ -89,6 +89,7 @@ func newBootstrapper(address *url.URL, options *Options) (*bootstrapper, error) if err != nil { return nil, err } + resolvers = append(resolvers, r) } } else { @@ -98,7 +99,7 @@ func newBootstrapper(address *url.URL, options *Options) (*bootstrapper, error) } return &bootstrapper{ - URL: address, + URL: u, resolvers: resolvers, options: options, }, nil diff --git a/upstream/upstream.go b/upstream/upstream.go index abb5df43e..6b0a230ea 100644 --- a/upstream/upstream.go +++ b/upstream/upstream.go @@ -11,6 +11,7 @@ import ( "time" "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/golibs/netutil" "github.com/ameshkov/dnscrypt/v2" "github.com/ameshkov/dnsstamps" "github.com/miekg/dns" @@ -24,81 +25,76 @@ type Upstream interface { // Options for AddressToUpstream func type Options struct { - // Bootstrap is a list of DNS servers to be used to resolve DOH/DOT hostnames (if any) - // You can use plain DNS, DNSCrypt, or DOT/DOH with IP addresses (not hostnames) - Bootstrap []string - - // Timeout is the default upstream timeout. Also, it is used as a timeout for bootstrap DNS requests. - // timeout=0 means infinite timeout. - Timeout time.Duration + // VerifyServerCertificate used to be set to crypto/tls + // Config.VerifyPeerCertificate for DNS-over-HTTPS, DNS-over-QUIC, + // DNS-over-TLS. + VerifyServerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error - // List of IP addresses of upstream DNS server - // Bootstrap DNS servers won't be used at all - ServerIPAddrs []net.IP + // VerifyDNSCryptCertificate is the callback the DNSCrypt server certificate + // will be passed to. It's called in dnsCrypt.exchangeDNSCrypt. + // Upstream.Exchange method returns any error caused by it. + VerifyDNSCryptCertificate func(cert *dnscrypt.Cert) error - // InsecureSkipVerify - if true, do not verify the server certificate - InsecureSkipVerify bool + // Bootstrap is a list of DNS servers to be used to resolve + // DNS-over-HTTPS/DNS-over-TLS hostnames. Plain DNS, DNSCrypt, or + // DNS-over-HTTPS/DNS-over-TLS with IP addresses (not hostnames) could be + // used. + Bootstrap []string - // VerifyServerCertificate will be set to crypto/tls Config.VerifyPeerCertificate for DoH, DoQ, DoT - VerifyServerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error + // List of IP addresses of the upstream DNS server. If not empty, bootstrap + // DNS servers won't be used at all. + ServerIPAddrs []net.IP - // VerifyDNSCryptCertificate is callback to which the DNSCrypt server certificate will be passed. - // is called in dnsCrypt.exchangeDNSCrypt; if error != nil then Upstream.Exchange() will return it - VerifyDNSCryptCertificate func(cert *dnscrypt.Cert) error -} + // Timeout is the default upstream timeout. It's also used as a timeout for + // bootstrap DNS requests. Zero value disables the timeout. + Timeout time.Duration -// Parse "host:port" string and validate port number -func parseHostAndPort(addr string) (string, string, error) { - host, port, err := net.SplitHostPort(addr) - if err != nil { - host = addr - } else { - // validate port - portN, err := strconv.Atoi(port) - if err != nil || portN <= 0 || portN > 0xffff { - return "", "", fmt.Errorf("invalid address: %s", addr) - } - port = strconv.Itoa(portN) - } - return host, port, nil + // InsecureSkipVerify disables verifying the server's certificate. + InsecureSkipVerify bool } -// AddressToUpstream converts the specified address to an Upstream instance -// * 8.8.8.8:53 -- plain DNS -// * tcp://8.8.8.8:53 -- plain DNS over TCP -// * tls://1.1.1.1 -- DNS-over-TLS -// * https://dns.adguard.com/dns-query -- DNS-over-HTTPS -// * sdns://... -- DNS stamp (see https://dnscrypt.info/stamps-specifications) -// options -- Upstream customization options, nil means default options. -func AddressToUpstream(address string, options *Options) (Upstream, error) { - if options == nil { - options = &Options{} +// AddressToUpstream converts addr to an Upstream instance: +// +// 8.8.8.8:53 or udp://dns.adguard.com for plain DNS; +// tcp://8.8.8.8:53 for plain DNS-over-TCP; +// tls://1.1.1.1 for DNS-over-TLS; +// https://dns.adguard.com/dns-query for DNS-over-HTTPS; +// sdns://... for DNS stamp, see https://dnscrypt.info/stamps-specifications. +// +// opts are applied to the u. nil is a valid value for opts. +func AddressToUpstream(addr string, opts *Options) (u Upstream, err error) { + if opts == nil { + opts = &Options{} } - if strings.Contains(address, "://") { - upstreamURL, err := url.Parse(address) + if strings.Contains(addr, "://") { + var uu *url.URL + uu, err = url.Parse(addr) if err != nil { - return nil, fmt.Errorf("failed to parse %s: %w", address, err) + return nil, fmt.Errorf("failed to parse %s: %w", addr, err) } - return urlToUpstream(upstreamURL, options) + return urlToUpstream(uu, opts) } - // we don't have scheme in the url, so it's just a plain DNS host:port - host, port, err := parseHostAndPort(address) + var host, port string + host, port, err = net.SplitHostPort(addr) if err != nil { - return nil, err + return &plainDNS{address: net.JoinHostPort(addr, "53"), timeout: opts.Timeout}, nil } - if port == "" { - port = "53" + + // Validate port. + portN, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return nil, fmt.Errorf("invalid address: %s", addr) } - return &plainDNS{address: net.JoinHostPort(host, port), timeout: options.Timeout}, nil + return &plainDNS{address: netutil.JoinHostPort(host, int(portN)), timeout: opts.Timeout}, nil } // urlToBoot creates an instance of the bootstrapper with the specified options // options -- Upstream customization options -func urlToBoot(resolverURL *url.URL, opts *Options) (*bootstrapper, error) { +func urlToBoot(resolverURL *url.URL, opts *Options) (b *bootstrapper, err error) { if len(opts.ServerIPAddrs) == 0 { return newBootstrapper(resolverURL, opts) } @@ -106,60 +102,25 @@ func urlToBoot(resolverURL *url.URL, opts *Options) (*bootstrapper, error) { return newBootstrapperResolved(resolverURL, opts) } -// urlToUpstream converts a URL to an Upstream -// options -- Upstream customization options -func urlToUpstream(upstreamURL *url.URL, opts *Options) (Upstream, error) { - switch upstreamURL.Scheme { +// urlToUpstream converts uu to an Upstream using opts. +func urlToUpstream(uu *url.URL, opts *Options) (u Upstream, err error) { + switch sch := uu.Scheme; sch { case "sdns": - return stampToUpstream(upstreamURL, opts) - + return stampToUpstream(uu, opts) case "dns": - return &plainDNS{address: getHostWithPort(upstreamURL, "53"), timeout: opts.Timeout}, nil - - case "tcp": - return &plainDNS{address: getHostWithPort(upstreamURL, "53"), timeout: opts.Timeout, preferTCP: true}, nil + log.Info("warning: using %q scheme is deprecated", sch) + return newPlain(uu, opts.Timeout, false), nil + case "udp", "tcp": + return newPlain(uu, opts.Timeout, sch == "tcp"), nil case "quic": - if upstreamURL.Port() == "" { - // https://datatracker.ietf.org/doc/html/draft-ietf-dprive-dnsoquic-02#section-10.2.1 - // Early experiments MAY use port 8853. This port is marked in the IANA registry as unassigned. - // (Note that prior to version -02 of this draft, experiments were directed to use port 784.) - upstreamURL.Host += ":8853" - } - - b, err := urlToBoot(upstreamURL, opts) - if err != nil { - return nil, fmt.Errorf("creating quic bootstrapper: %w", err) - } - - return &dnsOverQUIC{boot: b}, nil - + return newDoQ(uu, opts) case "tls": - if upstreamURL.Port() == "" { - upstreamURL.Host += ":853" - } - - b, err := urlToBoot(upstreamURL, opts) - if err != nil { - return nil, fmt.Errorf("creating tls bootstrapper: %w", err) - } - - return &dnsOverTLS{boot: b}, nil - + return newDoT(uu, opts) case "https": - if upstreamURL.Port() == "" { - upstreamURL.Host += ":443" - } - - b, err := urlToBoot(upstreamURL, opts) - if err != nil { - return nil, fmt.Errorf("creating https bootstrapper: %w", err) - } - - return &dnsOverHTTPS{boot: b}, nil - + return newDoH(uu, opts) default: - return nil, fmt.Errorf("unsupported URL scheme: %s", upstreamURL.Scheme) + return nil, fmt.Errorf("unsupported url scheme: %s", sch) } } @@ -205,12 +166,11 @@ func stampToUpstream(upsURL *url.URL, opts *Options) (Upstream, error) { return nil, fmt.Errorf("unsupported protocol %v in %s", stamp.Proto, upsURL) } -// getHostWithPort is a helper function that appends port if needed -func getHostWithPort(upstreamURL *url.URL, defaultPort string) string { - if upstreamURL.Port() == "" { - return upstreamURL.Host + ":" + defaultPort +// addPort is a helper function that appends port if needed +func addPort(u *url.URL, port string) { + if u.Port() == "" { + u.Host = net.JoinHostPort(u.Host, port) } - return upstreamURL.Host } // Write to log DNS request information that we are going to send diff --git a/upstream/upstream_doh.go b/upstream/upstream_doh.go index 165ffd2c4..7f02fd812 100644 --- a/upstream/upstream_doh.go +++ b/upstream/upstream_doh.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "sync" "time" @@ -47,6 +48,19 @@ type dnsOverHTTPS struct { // type check var _ Upstream = &dnsOverHTTPS{} +// newDoH returns the DNS-over-HTTPS Upstream. +func newDoH(uu *url.URL, opts *Options) (u Upstream, err error) { + addPort(uu, "443") + + var b *bootstrapper + b, err = urlToBoot(uu, opts) + if err != nil { + return nil, fmt.Errorf("creating https bootstrapper: %w", err) + } + + return &dnsOverHTTPS{boot: b}, nil +} + func (p *dnsOverHTTPS) Address() string { return p.boot.URL.String() } func (p *dnsOverHTTPS) Exchange(m *dns.Msg) (*dns.Msg, error) { diff --git a/upstream/upstream_dot.go b/upstream/upstream_dot.go index 2db4ab203..cd022365e 100644 --- a/upstream/upstream_dot.go +++ b/upstream/upstream_dot.go @@ -3,6 +3,7 @@ package upstream import ( "fmt" "net" + "net/url" "sync" "github.com/AdguardTeam/golibs/log" @@ -22,6 +23,19 @@ type dnsOverTLS struct { // type check var _ Upstream = &dnsOverTLS{} +// newDoT returns the DNS-over-TLS Upstream. +func newDoT(uu *url.URL, opts *Options) (u Upstream, err error) { + addPort(uu, "853") + + var b *bootstrapper + b, err = urlToBoot(uu, opts) + if err != nil { + return nil, fmt.Errorf("creating tls bootstrapper: %w", err) + } + + return &dnsOverTLS{boot: b}, nil +} + func (p *dnsOverTLS) Address() string { return p.boot.URL.String() } func (p *dnsOverTLS) Exchange(m *dns.Msg) (*dns.Msg, error) { diff --git a/upstream/upstream_plain.go b/upstream/upstream_plain.go index 5af052891..033bec56a 100644 --- a/upstream/upstream_plain.go +++ b/upstream/upstream_plain.go @@ -1,6 +1,7 @@ package upstream import ( + "net/url" "time" "github.com/AdguardTeam/golibs/log" @@ -19,20 +20,34 @@ type plainDNS struct { // type check var _ Upstream = &plainDNS{} +// newPlain returns the plain DNS Upstream. +func newPlain(uu *url.URL, timeout time.Duration, preferTCP bool) (u *plainDNS) { + addPort(uu, "53") + + return &plainDNS{ + address: uu.Host, + timeout: timeout, + preferTCP: preferTCP, + } +} + // Address returns the original address that we've put in initially, not resolved one func (p *plainDNS) Address() string { if p.preferTCP { return "tcp://" + p.address } + return p.address } func (p *plainDNS) Exchange(m *dns.Msg) (*dns.Msg, error) { if p.preferTCP { tcpClient := dns.Client{Net: "tcp", Timeout: p.timeout} + logBegin(p.Address(), m) reply, _, tcpErr := tcpClient.Exchange(m, p.address) logFinish(p.Address(), tcpErr) + return reply, tcpErr } @@ -45,6 +60,7 @@ func (p *plainDNS) Exchange(m *dns.Msg) (*dns.Msg, error) { if reply != nil && reply.Truncated { log.Tracef("Truncated message was received, retrying over TCP, question: %s", m.Question[0].String()) tcpClient := dns.Client{Net: "tcp", Timeout: p.timeout} + logBegin(p.Address(), m) reply, _, err = tcpClient.Exchange(m, p.address) logFinish(p.Address(), err) diff --git a/upstream/upstream_quic.go b/upstream/upstream_quic.go index 3c563fe58..85dcbb671 100644 --- a/upstream/upstream_quic.go +++ b/upstream/upstream_quic.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "net/url" "sync" "time" @@ -28,6 +29,24 @@ type dnsOverQUIC struct { // type check var _ Upstream = &dnsOverQUIC{} +// newDoQ returns the DNS-over-QUIC Upstream. +func newDoQ(uu *url.URL, opts *Options) (u Upstream, err error) { + // Early experiments MAY use port 8853. This port is marked in the IANA + // registry as unassigned. (Note that prior to version -02 of this + // draft, experiments were directed to use port 784.) + // + // See https://datatracker.ietf.org/doc/html/draft-ietf-dprive-dnsoquic-02#section-10.2.1. + addPort(uu, "8853") + + var b *bootstrapper + b, err = urlToBoot(uu, opts) + if err != nil { + return nil, fmt.Errorf("creating quic bootstrapper: %w", err) + } + + return &dnsOverQUIC{boot: b}, nil +} + func (p *dnsOverQUIC) Address() string { return p.boot.URL.String() } func (p *dnsOverQUIC) Exchange(m *dns.Msg) (*dns.Msg, error) { diff --git a/upstream/upstream_test.go b/upstream/upstream_test.go index c55c0c549..39c84a062 100644 --- a/upstream/upstream_test.go +++ b/upstream/upstream_test.go @@ -9,8 +9,10 @@ import ( "time" "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/golibs/testutil" "github.com/miekg/dns" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { @@ -27,16 +29,11 @@ func TestBootstrapTimeout(t *testing.T) { ) // Specifying some wrong port instead so that bootstrap DNS timed out for sure - u, err := AddressToUpstream( - "tls://one.one.one.one", - &Options{ - Bootstrap: []string{"8.8.8.8:555"}, - Timeout: timeout, - }, - ) - if err != nil { - t.Fatalf("cannot create upstream: %s", err) - } + u, err := AddressToUpstream("tls://one.one.one.one", &Options{ + Bootstrap: []string{"8.8.8.8:555"}, + Timeout: timeout, + }) + require.NoError(t, err) ch := make(chan int, count) abort := make(chan string, 1) @@ -120,148 +117,157 @@ func TestUpstreams(t *testing.T) { upstreams := []struct { address string bootstrap []string - }{ - { - address: "8.8.8.8:53", - bootstrap: []string{"8.8.8.8:53"}, - }, - { - address: "1.1.1.1", - bootstrap: []string{}, - }, - { - address: "1.1.1.1", - bootstrap: []string{"1.0.0.1"}, - }, - { - address: "tcp://1.1.1.1:53", - bootstrap: []string{}, - }, - { - address: "94.140.14.14:5353", - bootstrap: []string{}, - }, - { - address: "tls://1.1.1.1", - bootstrap: []string{}, - }, - { - address: "tls://9.9.9.9:853", - bootstrap: []string{}, - }, - { - address: "tls://dns.adguard.com", - bootstrap: []string{"8.8.8.8:53"}, - }, - { - address: "tls://dns.adguard.com:853", - bootstrap: []string{"8.8.8.8:53"}, - }, - { - address: "tls://dns.adguard.com:853", - bootstrap: []string{"8.8.8.8"}, - }, - { - address: "tls://one.one.one.one", - bootstrap: []string{}, - }, - { - address: "https://1dot1dot1dot1.cloudflare-dns.com/dns-query", - bootstrap: []string{"8.8.8.8:53"}, - }, - { - address: "https://dns.google/dns-query", - bootstrap: []string{}, - }, - { - address: "https://doh.opendns.com/dns-query", - bootstrap: []string{}, - }, - { - // AdGuard DNS (DNSCrypt) - address: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", - bootstrap: []string{}, - }, - { - // AdGuard Family (DNSCrypt) - address: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMjo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ", - bootstrap: []string{"8.8.8.8"}, - }, - { - // Cloudflare DNS (DoH) - address: "sdns://AgcAAAAAAAAABzEuMC4wLjGgENk8mGSlIfMGXMOlIlCcKvq7AVgcrZxtjon911-ep0cg63Ul-I8NlFj4GplQGb_TTLiczclX57DvMV8Q-JdjgRgSZG5zLmNsb3VkZmxhcmUuY29tCi9kbnMtcXVlcnk", - bootstrap: []string{"8.8.8.8:53"}, - }, - { - // Google (Plain) - address: "sdns://AAcAAAAAAAAABzguOC44Ljg", - bootstrap: []string{}, - }, - { - // AdGuard DNS (DNS-over-TLS) - address: "sdns://AwAAAAAAAAAAAAAPZG5zLmFkZ3VhcmQuY29t", - bootstrap: []string{"8.8.8.8:53"}, - }, - { - // AdGuard DNS (DNS-over-QUIC) - address: "sdns://BAcAAAAAAAAAAAATZG5zLmFkZ3VhcmQuY29tOjc4NA", - bootstrap: []string{"8.8.8.8:53"}, - }, - { - // Cloudflare DNS - address: "https://1.1.1.1/dns-query", - bootstrap: []string{}, - }, - { - // Cloudflare DNS - address: "quic://dns-unfiltered.adguard.com:784", - bootstrap: []string{}, - }, - } + }{{ + address: "8.8.8.8:53", + bootstrap: []string{"8.8.8.8:53"}, + }, { + address: "1.1.1.1", + bootstrap: []string{}, + }, { + address: "1.1.1.1", + bootstrap: []string{"1.0.0.1"}, + }, { + address: "tcp://1.1.1.1:53", + bootstrap: []string{}, + }, { + address: "94.140.14.14:5353", + bootstrap: []string{}, + }, { + address: "tls://1.1.1.1", + bootstrap: []string{}, + }, { + address: "tls://9.9.9.9:853", + bootstrap: []string{}, + }, { + address: "tls://dns.adguard.com", + bootstrap: []string{"8.8.8.8:53"}, + }, { + address: "tls://dns.adguard.com:853", + bootstrap: []string{"8.8.8.8:53"}, + }, { + address: "tls://dns.adguard.com:853", + bootstrap: []string{"8.8.8.8"}, + }, { + address: "tls://one.one.one.one", + bootstrap: []string{}, + }, { + address: "https://1dot1dot1dot1.cloudflare-dns.com/dns-query", + bootstrap: []string{"8.8.8.8:53"}, + }, { + address: "https://dns.google/dns-query", + bootstrap: []string{}, + }, { + address: "https://doh.opendns.com/dns-query", + bootstrap: []string{}, + }, { + // AdGuard DNS (DNSCrypt) + address: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", + bootstrap: []string{}, + }, { + // AdGuard Family (DNSCrypt) + address: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMjo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ", + bootstrap: []string{"8.8.8.8"}, + }, { + // Cloudflare DNS (DoH) + address: "sdns://AgcAAAAAAAAABzEuMC4wLjGgENk8mGSlIfMGXMOlIlCcKvq7AVgcrZxtjon911-ep0cg63Ul-I8NlFj4GplQGb_TTLiczclX57DvMV8Q-JdjgRgSZG5zLmNsb3VkZmxhcmUuY29tCi9kbnMtcXVlcnk", + bootstrap: []string{"8.8.8.8:53"}, + }, { + // Google (Plain) + address: "sdns://AAcAAAAAAAAABzguOC44Ljg", + bootstrap: []string{}, + }, { + // AdGuard DNS (DNS-over-TLS) + address: "sdns://AwAAAAAAAAAAAAAPZG5zLmFkZ3VhcmQuY29t", + bootstrap: []string{"8.8.8.8:53"}, + }, { + // AdGuard DNS (DNS-over-QUIC) + address: "sdns://BAcAAAAAAAAAAAATZG5zLmFkZ3VhcmQuY29tOjc4NA", + bootstrap: []string{"8.8.8.8:53"}, + }, { + // Cloudflare DNS + address: "https://1.1.1.1/dns-query", + bootstrap: []string{}, + }, { + // Cloudflare DNS + address: "quic://dns-unfiltered.adguard.com:784", + bootstrap: []string{}, + }} for _, test := range upstreams { t.Run(test.address, func(t *testing.T) { u, err := AddressToUpstream( test.address, &Options{Bootstrap: test.bootstrap, Timeout: timeout}, ) - if err != nil { - t.Fatalf("Failed to generate upstream from address %s: %s", test.address, err) - } + require.NoErrorf(t, err, "failed to generate upstream from address %s", test.address) checkUpstream(t, u, test.address) }) } } -func TestUpstreamAddress(t *testing.T) { +func TestAddressToUpstream(t *testing.T) { opt := &Options{Bootstrap: []string{"1.1.1.1"}} - u, _ := AddressToUpstream("1.1.1.1", nil) - assert.Equal(t, "1.1.1.1:53", u.Address()) - - u, _ = AddressToUpstream("one.one.one.one", nil) - assert.Equal(t, "one.one.one.one:53", u.Address()) - - u, _ = AddressToUpstream("tcp://one.one.one.one", opt) - assert.Equal(t, "tcp://one.one.one.one:53", u.Address()) - - u, _ = AddressToUpstream("tls://one.one.one.one", opt) - assert.Equal(t, "tls://one.one.one.one:853", u.Address()) - - u, _ = AddressToUpstream("https://one.one.one.one", opt) - assert.Equal(t, "https://one.one.one.one:443", u.Address()) + testCases := []struct { + addr string + opt *Options + want string + }{{ + addr: "1.1.1.1", + opt: nil, + want: "1.1.1.1:53", + }, { + addr: "one.one.one.one", + opt: nil, + want: "one.one.one.one:53", + }, { + addr: "tcp://one.one.one.one", + opt: opt, + want: "tcp://one.one.one.one:53", + }, { + addr: "tls://one.one.one.one", + opt: opt, + want: "tls://one.one.one.one:853", + }, { + addr: "https://one.one.one.one", + opt: opt, + want: "https://one.one.one.one:443", + }} - _, err := AddressToUpstream("asdf://1.1.1.1", nil) - assert.NotNil(t, err) // bad scheme + for _, tc := range testCases { + t.Run(tc.addr, func(t *testing.T) { + u, err := AddressToUpstream(tc.addr, tc.opt) + require.NoError(t, err) - _, err = AddressToUpstream("12345.1.1.1:1234567", nil) - assert.NotNil(t, err) // bad port + assert.Equal(t, tc.want, u.Address()) + }) + } +} - _, err = AddressToUpstream(":1234567", nil) - assert.NotNil(t, err) // empty host +func TestAddressToUpstream_bads(t *testing.T) { + testCases := []struct { + addr string + wantErrMsg string + }{{ + addr: "asdf://1.1.1.1", + wantErrMsg: "unsupported url scheme: asdf", + }, { + addr: "12345.1.1.1:1234567", + wantErrMsg: "invalid address: 12345.1.1.1:1234567", + }, { + addr: ":1234567", + wantErrMsg: "invalid address: :1234567", + }, { + addr: "host:", + wantErrMsg: "invalid address: host:", + }} - _, err = AddressToUpstream("host:", nil) - assert.NotNil(t, err) // empty port + for _, tc := range testCases { + t.Run(tc.addr, func(t *testing.T) { + _, err := AddressToUpstream(tc.addr, nil) + testutil.AssertErrorMsg(t, tc.wantErrMsg, err) + }) + } } func TestUpstreamDOTBootstrap(t *testing.T) { @@ -280,17 +286,15 @@ func TestUpstreamDOTBootstrap(t *testing.T) { bootstrap: []string{"sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ"}, }} - for _, test := range upstreams { - t.Run(test.address, func(t *testing.T) { - u, err := AddressToUpstream( - test.address, - &Options{Bootstrap: test.bootstrap, Timeout: timeout}, - ) - if err != nil { - t.Fatalf("Failed to generate upstream from address %s: %s", test.address, err) - } + for _, tc := range upstreams { + t.Run(tc.address, func(t *testing.T) { + u, err := AddressToUpstream(tc.address, &Options{ + Bootstrap: tc.bootstrap, + Timeout: timeout, + }) + require.NoErrorf(t, err, "failed to generate upstream from address %s", tc.address) - checkUpstream(t, u, test.address) + checkUpstream(t, u, tc.address) }) } } @@ -300,9 +304,8 @@ func TestUpstreamDefaultOptions(t *testing.T) { for _, address := range addresses { u, err := AddressToUpstream(address, nil) - if err != nil { - t.Fatalf("Failed to generate upstream from address %s", address) - } + require.NoErrorf(t, err, "failed to generate upstream from address %s", address) + checkUpstream(t, u, address) } } @@ -312,53 +315,44 @@ func TestUpstreamsInvalidBootstrap(t *testing.T) { upstreams := []struct { address string bootstrap []string - }{ - { - address: "tls://dns.adguard.com", - bootstrap: []string{"1.1.1.1:555", "8.8.8.8:53"}, - }, - { - address: "tls://dns.adguard.com:853", - bootstrap: []string{"1.0.0.1", "8.8.8.8:535"}, - }, - { - address: "https://1dot1dot1dot1.cloudflare-dns.com/dns-query", - bootstrap: []string{"8.8.8.1", "1.0.0.1"}, - }, - { - address: "https://doh.opendns.com:443/dns-query", - bootstrap: []string{"1.2.3.4:79", "8.8.8.8:53"}, - }, - { - // Cloudflare DNS (DoH) - address: "sdns://AgcAAAAAAAAABzEuMC4wLjGgENk8mGSlIfMGXMOlIlCcKvq7AVgcrZxtjon911-ep0cg63Ul-I8NlFj4GplQGb_TTLiczclX57DvMV8Q-JdjgRgSZG5zLmNsb3VkZmxhcmUuY29tCi9kbnMtcXVlcnk", - bootstrap: []string{"8.8.8.8:53", "8.8.8.1:53"}, - }, - { - // AdGuard DNS (DNS-over-TLS) - address: "sdns://AwAAAAAAAAAAAAAPZG5zLmFkZ3VhcmQuY29t", - bootstrap: []string{"1.2.3.4:55", "8.8.8.8"}, - }, - } - for _, test := range upstreams { - t.Run(test.address, func(t *testing.T) { - u, err := AddressToUpstream( - test.address, - &Options{Bootstrap: test.bootstrap, Timeout: timeout}, - ) - if err != nil { - t.Fatalf("Failed to generate upstream from address %s: %s", test.address, err) - } + }{{ + address: "tls://dns.adguard.com", + bootstrap: []string{"1.1.1.1:555", "8.8.8.8:53"}, + }, { + address: "tls://dns.adguard.com:853", + bootstrap: []string{"1.0.0.1", "8.8.8.8:535"}, + }, { + address: "https://1dot1dot1dot1.cloudflare-dns.com/dns-query", + bootstrap: []string{"8.8.8.1", "1.0.0.1"}, + }, { + address: "https://doh.opendns.com:443/dns-query", + bootstrap: []string{"1.2.3.4:79", "8.8.8.8:53"}, + }, { + // Cloudflare DNS (DoH) + address: "sdns://AgcAAAAAAAAABzEuMC4wLjGgENk8mGSlIfMGXMOlIlCcKvq7AVgcrZxtjon911-ep0cg63Ul-I8NlFj4GplQGb_TTLiczclX57DvMV8Q-JdjgRgSZG5zLmNsb3VkZmxhcmUuY29tCi9kbnMtcXVlcnk", + bootstrap: []string{"8.8.8.8:53", "8.8.8.1:53"}, + }, { + // AdGuard DNS (DNS-over-TLS) + address: "sdns://AwAAAAAAAAAAAAAPZG5zLmFkZ3VhcmQuY29t", + bootstrap: []string{"1.2.3.4:55", "8.8.8.8"}, + }} - checkUpstream(t, u, test.address) + for _, tc := range upstreams { + t.Run(tc.address, func(t *testing.T) { + u, err := AddressToUpstream(tc.address, &Options{ + Bootstrap: tc.bootstrap, + Timeout: timeout, + }) + require.NoErrorf(t, err, "failed to generate upstream from address %s", tc.address) + + checkUpstream(t, u, tc.address) }) } - _, err := AddressToUpstream( - "tls://example.org", - &Options{Bootstrap: []string{"8.8.8.8", "asdfasdf"}}, - ) - assert.NotNil(t, err) // bad bootstrap "asdfasdf" + _, err := AddressToUpstream("tls://example.org", &Options{ + Bootstrap: []string{"8.8.8.8", "asdfasdf"}, + }) + assert.Error(t, err) // bad bootstrap "asdfasdf" } func TestUpstreamsWithServerIP(t *testing.T) { @@ -367,44 +361,41 @@ func TestUpstreamsWithServerIP(t *testing.T) { upstreams := []struct { address string + serverIP net.IP bootstrap []string - serverIP string - }{ - { - address: "tls://dns.adguard.com", - bootstrap: invalidBootstrap, - serverIP: "94.140.14.14", - }, - { - address: "https://dns.adguard.com/dns-query", - bootstrap: invalidBootstrap, - serverIP: "94.140.14.14", - }, - { - // AdGuard DNS DOH with the IP address specified - address: "sdns://AgcAAAAAAAAADzE3Ni4xMDMuMTMwLjEzMAAPZG5zLmFkZ3VhcmQuY29tCi9kbnMtcXVlcnk", - bootstrap: invalidBootstrap, - }, - { - // AdGuard DNS DOT with the IP address specified - address: "sdns://AwAAAAAAAAAAEzE3Ni4xMDMuMTMwLjEzMDo4NTMAD2Rucy5hZGd1YXJkLmNvbQ", - bootstrap: invalidBootstrap, - }, - } + }{{ + address: "tls://dns.adguard.com", + serverIP: net.IP{94, 140, 14, 14}, + bootstrap: invalidBootstrap, + }, { + address: "https://dns.adguard.com/dns-query", + serverIP: net.IP{94, 140, 14, 14}, + bootstrap: invalidBootstrap, + }, { + // AdGuard DNS DOH with the IP address specified + address: "sdns://AgcAAAAAAAAADzE3Ni4xMDMuMTMwLjEzMAAPZG5zLmFkZ3VhcmQuY29tCi9kbnMtcXVlcnk", + serverIP: nil, + bootstrap: invalidBootstrap, + }, { + // AdGuard DNS DOT with the IP address specified + address: "sdns://AwAAAAAAAAAAEzE3Ni4xMDMuMTMwLjEzMDo4NTMAD2Rucy5hZGd1YXJkLmNvbQ", + serverIP: nil, + bootstrap: invalidBootstrap, + }} - for _, test := range upstreams { - t.Run(test.address, func(t *testing.T) { - opts := &Options{ - Bootstrap: test.bootstrap, - Timeout: timeout, - ServerIPAddrs: []net.IP{net.ParseIP(test.serverIP)}, - } - u, err := AddressToUpstream(test.address, opts) - if err != nil { - t.Fatalf("Failed to generate upstream from address %s: %s", test.address, err) - } + for _, tc := range upstreams { + opts := &Options{ + Bootstrap: tc.bootstrap, + Timeout: timeout, + ServerIPAddrs: []net.IP{tc.serverIP}, + } + u, err := AddressToUpstream(tc.address, opts) + if err != nil { + t.Fatalf("Failed to generate upstream from address %s: %s", tc.address, err) + } - checkUpstream(t, u, test.address) + t.Run(tc.address, func(t *testing.T) { + checkUpstream(t, u, tc.address) }) } } @@ -414,9 +405,8 @@ func checkUpstream(t *testing.T, u Upstream, addr string) { req := createTestMessage() reply, err := u.Exchange(req) - if err != nil { - t.Fatalf("Couldn't talk to upstream %s: %s", addr, err) - } + require.NoErrorf(t, err, "couldn't talk to upstream %s", addr) + assertResponse(t, reply) } @@ -424,26 +414,26 @@ func createTestMessage() *dns.Msg { return createHostTestMessage("google-public-dns-a.google.com") } -func createHostTestMessage(host string) *dns.Msg { - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - name := host + "." - req.Question = []dns.Question{ - {Name: name, Qtype: dns.TypeA, Qclass: dns.ClassINET}, +func createHostTestMessage(host string) (req *dns.Msg) { + return &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, + Question: []dns.Question{{ + Name: dns.Fqdn(host), + Qtype: dns.TypeA, + Qclass: dns.ClassINET, + }}, } - return &req } func assertResponse(t *testing.T, reply *dns.Msg) { - if len(reply.Answer) != 1 { - t.Fatalf("DNS upstream returned reply with wrong number of answers - %d", len(reply.Answer)) - } - if a, ok := reply.Answer[0].(*dns.A); ok { - if !net.IPv4(8, 8, 8, 8).Equal(a.A) { - t.Fatalf("DNS upstream returned wrong answer instead of 8.8.8.8: %v", a.A) - } - } else { - t.Fatalf("DNS upstream returned wrong answer type instead of A: %v", reply.Answer[0]) - } + require.NotNil(t, reply) + require.Lenf(t, reply.Answer, 1, "wrong number of answers: %d", len(reply.Answer)) + + a, ok := reply.Answer[0].(*dns.A) + require.Truef(t, ok, "wrong answer type: %v", reply.Answer[0]) + + assert.Equalf(t, net.IPv4(8, 8, 8, 8), a.A.To16(), "wrong answer: %v", a.A) }