git » dnss » commit 5cc895a

dnstohttps: Implement DoH (DNS over HTTPS) client mode

author Alberto Bertogli
2018-04-08 18:09:48 UTC
committer Alberto Bertogli
2018-04-08 18:18:24 UTC
parent 9c203de38b9e12f4dd94db10f7df8284a9091d83

dnstohttps: Implement DoH (DNS over HTTPS) client mode

This patch implements DoH in the DNS-to-HTTPS mode, where we use DoH to
resolve DNS queries.

It implements DoH as specified in the latest draft,
https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-05

This is still experimental, and its used can be enabled via a
command-line flag.

dnss.go +13 -2
dnss_test.go +1 -1
internal/dnstohttps/https_test.go +1 -1
internal/dnstohttps/resolver.go +80 -5

diff --git a/dnss.go b/dnss.go
index df88362..d80fe83 100644
--- a/dnss.go
+++ b/dnss.go
@@ -71,6 +71,9 @@ var (
 
 	insecureForTesting = flag.Bool("testing__insecure_http", false,
 		"INSECURE, for testing only")
+
+	dohMode = flag.Bool("experimental__doh_mode", false,
+		"DoH mode (experimental)")
 )
 
 func flushLogs() {
@@ -110,8 +113,16 @@ func main() {
 		if err != nil {
 			glog.Fatalf("-https_upstream is not a valid URL: %v", err)
 		}
-		var resolver dnsserver.Resolver = dnstohttps.NewHTTPSResolver(
-			upstream, *httpsClientCAFile)
+
+		var resolver dnsserver.Resolver
+		if *dohMode {
+			resolver = dnstohttps.NewDoHResolver(
+				upstream, *httpsClientCAFile)
+		} else {
+			resolver = dnstohttps.NewJSONResolver(
+				upstream, *httpsClientCAFile)
+		}
+
 		if *enableCache {
 			cr := dnsserver.NewCachingResolver(resolver)
 			cr.RegisterDebugHandlers()
diff --git a/dnss_test.go b/dnss_test.go
index 2007cf9..906b6a3 100644
--- a/dnss_test.go
+++ b/dnss_test.go
@@ -52,7 +52,7 @@ func realMain(m *testing.M) int {
 		fmt.Printf("invalid URL: %v", err)
 		return 1
 	}
-	r := dnstohttps.NewHTTPSResolver(HTTPSToDNSURL, "")
+	r := dnstohttps.NewJSONResolver(HTTPSToDNSURL, "")
 	dtoh := dnsserver.New(DNSToHTTPSAddr, r, "")
 	go dtoh.ListenAndServe()
 
diff --git a/internal/dnstohttps/https_test.go b/internal/dnstohttps/https_test.go
index 8ef9cb4..2a52108 100644
--- a/internal/dnstohttps/https_test.go
+++ b/internal/dnstohttps/https_test.go
@@ -136,7 +136,7 @@ func realMain(m *testing.M) int {
 		fmt.Printf("Failed to parse test http server URL: %v\n", err)
 		return 1
 	}
-	r := NewHTTPSResolver(srvURL, "")
+	r := NewJSONResolver(srvURL, "")
 	dth := dnsserver.New(DNSAddr, r, "")
 	go dth.ListenAndServe()
 
diff --git a/internal/dnstohttps/resolver.go b/internal/dnstohttps/resolver.go
index 49463e8..5465e0c 100644
--- a/internal/dnstohttps/resolver.go
+++ b/internal/dnstohttps/resolver.go
@@ -1,11 +1,14 @@
 package dnstohttps
 
 import (
+	"bytes"
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/json"
 	"fmt"
+	"io"
 	"io/ioutil"
+	"mime"
 	"net/http"
 	"net/url"
 	"time"
@@ -19,11 +22,15 @@ import (
 )
 
 // httpsResolver implements the dnsserver.Resolver interface by querying a
-// server via DNS-over-HTTPS (like https://dns.google.com).
+// server via DNS-over-HTTPS.
+//
+// It supports two modes: JSON (like https://dns.google.com) and DoH
+// (https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-05).
 type httpsResolver struct {
 	Upstream *url.URL
 	CAFile   string
 	client   *http.Client
+	mode     string
 }
 
 func loadCertPool(caFile string) (*x509.CertPool, error) {
@@ -40,12 +47,23 @@ func loadCertPool(caFile string) (*x509.CertPool, error) {
 	return pool, nil
 }
 
-// NewHTTPSResolver creates a new resolver which uses the given upstream URL
-// to resolve queries.
-func NewHTTPSResolver(upstream *url.URL, caFile string) *httpsResolver {
+// NewJSONResolver creates a new JSON resolver which uses the given upstream
+// URL to resolve queries.
+func NewJSONResolver(upstream *url.URL, caFile string) *httpsResolver {
 	return &httpsResolver{
 		Upstream: upstream,
 		CAFile:   caFile,
+		mode:     "JSON",
+	}
+}
+
+// NewDoHResolver creates a new DoH resolver, which uses the given upstream
+// URL to resolve queries.
+func NewDoHResolver(upstream *url.URL, caFile string) *httpsResolver {
+	return &httpsResolver{
+		Upstream: upstream,
+		CAFile:   caFile,
+		mode:     "DoH",
 	}
 }
 
@@ -85,6 +103,63 @@ func (r *httpsResolver) Maintain() {
 }
 
 func (r *httpsResolver) Query(req *dns.Msg, tr trace.Trace) (*dns.Msg, error) {
+	if r.mode == "DoH" {
+		return r.queryDoH(req, tr)
+	}
+	return r.queryJSON(req, tr)
+}
+
+func (r *httpsResolver) queryDoH(req *dns.Msg, tr trace.Trace) (*dns.Msg, error) {
+	packed, err := req.Pack()
+	if err != nil {
+		return nil, fmt.Errorf("cannot pack query: %v", err)
+	}
+
+	if glog.V(3) {
+		tr.LazyPrintf("DoH POST %v", r.Upstream)
+	}
+
+	// TODO: Accept header.
+
+	hr, err := r.client.Post(
+		r.Upstream.String(),
+		"application/dns-udpwireformat",
+		bytes.NewReader(packed))
+	if err != nil {
+		return nil, fmt.Errorf("POST failed: %v", err)
+	}
+	tr.LazyPrintf("%s  %s", hr.Proto, hr.Status)
+	defer hr.Body.Close()
+
+	if hr.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("Response status: %s", hr.Status)
+	}
+
+	// Read the HTTPS response, and parse the message.
+	ct, _, err := mime.ParseMediaType(hr.Header.Get("Content-Type"))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse content type: %v", err)
+	}
+
+	if ct != "application/dns-udpwireformat" {
+		return nil, fmt.Errorf("unknown response content type %q", ct)
+	}
+
+	respRaw, err := ioutil.ReadAll(io.LimitReader(hr.Body, 4092))
+	if err != nil {
+		return nil, fmt.Errorf("error reading from body: %v", err)
+	}
+
+	respDNS := &dns.Msg{}
+	err = respDNS.Unpack(respRaw)
+	if err != nil {
+		return nil, fmt.Errorf("error unpacking response: %v", err)
+	}
+
+	return respDNS, nil
+}
+
+func (r *httpsResolver) queryJSON(req *dns.Msg, tr trace.Trace) (*dns.Msg, error) {
 	// Only answer single-question queries.
 	// In practice, these are all we get, and almost no server supports
 	// multi-question requests anyway.
@@ -107,7 +182,7 @@ func (r *httpsResolver) Query(req *dns.Msg, tr trace.Trace) (*dns.Msg, error) {
 	// TODO: add random_padding.
 
 	if glog.V(3) {
-		tr.LazyPrintf("GET %v", url)
+		tr.LazyPrintf("JSON GET %v", url)
 	}
 
 	hr, err := r.client.Get(url.String())