From f18209af7def919e731fe1a97c3d052d041f152a Mon Sep 17 00:00:00 2001 From: Austin Cherry Date: Sat, 4 Apr 2020 15:49:36 -0500 Subject: [PATCH] ARES-899: Fixes DoH client as system resolver. Fixes #91 --- cmd/cloudflared/tunnel/cmd.go | 13 +++++ cmd/cloudflared/tunnel/server.go | 2 +- tunneldns/https_upstream.go | 93 ++++++++++++++++++++++---------- tunneldns/tunnel.go | 6 +-- 4 files changed, 82 insertions(+), 32 deletions(-) diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 22dbcc2e..9e940126 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -138,6 +138,12 @@ func Commands() []*cli.Command { Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"), EnvVars: []string{"TUNNEL_DNS_UPSTREAM"}, }, + &cli.StringSliceFlag{ + Name: "bootstrap", + Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.", + Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"), + EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"}, + }, }, ArgsUsage: " ", // can't be the empty string or we get the default output Hidden: false, @@ -963,6 +969,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag { EnvVars: []string{"TUNNEL_DNS_UPSTREAM"}, Hidden: shouldHide, }), + altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ + Name: "proxy-dns-bootstrap", + Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.", + Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"), + EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"}, + Hidden: shouldHide, + }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: "grace-period", Usage: "Duration to accept new requests after cloudflared receives first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to shutdown immediately.", diff --git a/cmd/cloudflared/tunnel/server.go b/cmd/cloudflared/tunnel/server.go index 3d089f41..4595566d 100644 --- a/cmd/cloudflared/tunnel/server.go +++ b/cmd/cloudflared/tunnel/server.go @@ -14,7 +14,7 @@ func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}) logger.Errorf("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.") return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.") } - listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream")) + listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap")) if err != nil { close(dnsReadySignal) listener.Stop() diff --git a/tunneldns/https_upstream.go b/tunneldns/https_upstream.go index 57f51deb..8494fd5f 100644 --- a/tunneldns/https_upstream.go +++ b/tunneldns/https_upstream.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "fmt" "io/ioutil" + "net" "net/http" "net/url" "time" @@ -22,33 +23,18 @@ const ( // UpstreamHTTPS is the upstream implementation for DNS over HTTPS service type UpstreamHTTPS struct { - client *http.Client - endpoint *url.URL + client *http.Client + endpoint *url.URL + bootstraps []string } -// NewUpstreamHTTPS creates a new DNS over HTTPS upstream from hostname -func NewUpstreamHTTPS(endpoint string) (Upstream, error) { +// NewUpstreamHTTPS creates a new DNS over HTTPS upstream from endpoint +func NewUpstreamHTTPS(endpoint string, bootstraps []string) (Upstream, error) { u, err := url.Parse(endpoint) if err != nil { return nil, err } - - // Update TLS and HTTP client configuration - tls := &tls.Config{ServerName: u.Hostname()} - transport := &http.Transport{ - TLSClientConfig: tls, - DisableCompression: true, - MaxIdleConns: 1, - Proxy: http.ProxyFromEnvironment, - } - http2.ConfigureTransport(transport) - - client := &http.Client{ - Timeout: defaultTimeout, - Transport: transport, - } - - return &UpstreamHTTPS{client: client, endpoint: u}, nil + return &UpstreamHTTPS{client: configureClient(u.Hostname()), endpoint: u, bootstraps: bootstraps}, nil } // Exchange provides an implementation for the Upstream interface @@ -58,34 +44,55 @@ func (u *UpstreamHTTPS) Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, return nil, errors.Wrap(err, "failed to pack DNS query") } + if len(query.Question) > 0 && query.Question[0].Name == fmt.Sprintf("%s.", u.endpoint.Hostname()) { + for _, bootstrap := range u.bootstraps { + endpoint, client, err := configureBootstrap(bootstrap) + if err != nil { + log.WithError(err).Errorf("failed to configure boostrap upstream %s", bootstrap) + continue + } + msg, err := exchange(queryBuf, query.Id, endpoint, client) + if err != nil { + log.WithError(err).Errorf("failed to connect to a boostrap upstream %s", bootstrap) + continue + } + return msg, nil + } + return nil, fmt.Errorf("failed to reach any bootstrap upstream: %v", u.bootstraps) + } + + return exchange(queryBuf, query.Id, u.endpoint, u.client) +} + +func exchange(msg []byte, queryID uint16, endpoint *url.URL, client *http.Client) (*dns.Msg, error) { // No content negotiation for now, use DNS wire format - buf, backendErr := u.exchangeWireformat(queryBuf) + buf, backendErr := exchangeWireformat(msg, endpoint, client) if backendErr == nil { response := &dns.Msg{} if err := response.Unpack(buf); err != nil { return nil, errors.Wrap(err, "failed to unpack DNS response from body") } - response.Id = query.Id + response.Id = queryID return response, nil } - log.WithError(backendErr).Errorf("failed to connect to an HTTPS backend %q", u.endpoint) + log.WithError(backendErr).Errorf("failed to connect to an HTTPS backend %q", endpoint) return nil, backendErr } // Perform message exchange with the default UDP wireformat defined in current draft // https://datatracker.ietf.org/doc/draft-ietf-doh-dns-over-https -func (u *UpstreamHTTPS) exchangeWireformat(msg []byte) ([]byte, error) { - req, err := http.NewRequest("POST", u.endpoint.String(), bytes.NewBuffer(msg)) +func exchangeWireformat(msg []byte, endpoint *url.URL, client *http.Client) ([]byte, error) { + req, err := http.NewRequest("POST", endpoint.String(), bytes.NewBuffer(msg)) if err != nil { return nil, errors.Wrap(err, "failed to create an HTTPS request") } req.Header.Add("Content-Type", "application/dns-message") - req.Host = u.endpoint.Host + req.Host = endpoint.Host - resp, err := u.client.Do(req) + resp, err := client.Do(req) if err != nil { return nil, errors.Wrap(err, "failed to perform an HTTPS request") } @@ -104,3 +111,33 @@ func (u *UpstreamHTTPS) exchangeWireformat(msg []byte) ([]byte, error) { return buf, nil } + +func configureBootstrap(bootstrap string) (*url.URL, *http.Client, error) { + b, err := url.Parse(bootstrap) + if err != nil { + return nil, nil, err + } + if ip := net.ParseIP(b.Hostname()); ip == nil { + return nil, nil, fmt.Errorf("bootstrap address of %s must be an IP address", b.Hostname()) + } + + return b, configureClient(b.Hostname()), nil +} + +// configureClient will configure a HTTPS client for upstream DoH requests +func configureClient(hostname string) *http.Client { + // Update TLS and HTTP client configuration + tls := &tls.Config{ServerName: hostname} + transport := &http.Transport{ + TLSClientConfig: tls, + DisableCompression: true, + MaxIdleConns: 1, + Proxy: http.ProxyFromEnvironment, + } + http2.ConfigureTransport(transport) + + return &http.Client{ + Timeout: defaultTimeout, + Transport: transport, + } +} diff --git a/tunneldns/tunnel.go b/tunneldns/tunnel.go index 13db1fb6..1cbe1136 100644 --- a/tunneldns/tunnel.go +++ b/tunneldns/tunnel.go @@ -35,7 +35,7 @@ func Run(c *cli.Context) error { go metrics.ServeMetrics(metricsListener, nil, logger) - listener, err := CreateListener(c.String("address"), uint16(c.Uint("port")), c.StringSlice("upstream")) + listener, err := CreateListener(c.String("address"), uint16(c.Uint("port")), c.StringSlice("upstream"), c.StringSlice("bootstrap")) if err != nil { logger.WithError(err).Errorf("Failed to create the listeners") return err @@ -117,12 +117,12 @@ func (l *Listener) Stop() error { } // CreateListener configures the server and bound sockets -func CreateListener(address string, port uint16, upstreams []string) (*Listener, error) { +func CreateListener(address string, port uint16, upstreams []string, bootstraps []string) (*Listener, error) { // Build the list of upstreams upstreamList := make([]Upstream, 0) for _, url := range upstreams { logger.WithField("url", url).Infof("Adding DNS upstream") - upstream, err := NewUpstreamHTTPS(url) + upstream, err := NewUpstreamHTTPS(url, bootstraps) if err != nil { return nil, errors.Wrap(err, "failed to create HTTPS upstream") }