Skip to content

Commit

Permalink
upstream: support udp scheme
Browse files Browse the repository at this point in the history
  • Loading branch information
EugeneOne1 committed Mar 1, 2022
1 parent 85d124d commit 652588c
Show file tree
Hide file tree
Showing 8 changed files with 390 additions and 353 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions upstream/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -89,6 +89,7 @@ func newBootstrapper(address *url.URL, options *Options) (*bootstrapper, error)
if err != nil {
return nil, err
}

resolvers = append(resolvers, r)
}
} else {
Expand All @@ -98,7 +99,7 @@ func newBootstrapper(address *url.URL, options *Options) (*bootstrapper, error)
}

return &bootstrapper{
URL: address,
URL: u,
resolvers: resolvers,
options: options,
}, nil
Expand Down
170 changes: 65 additions & 105 deletions upstream/upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,142 +25,102 @@ 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)
}

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)
}
}

Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions upstream/upstream_doh.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"sync"
"time"
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 14 additions & 0 deletions upstream/upstream_dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package upstream
import (
"fmt"
"net"
"net/url"
"sync"

"github.com/AdguardTeam/golibs/log"
Expand All @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions upstream/upstream_plain.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package upstream

import (
"net/url"
"time"

"github.com/AdguardTeam/golibs/log"
Expand All @@ -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
}

Expand All @@ -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)
Expand Down
Loading

0 comments on commit 652588c

Please sign in to comment.