git » dnss » commit e5c4787

Implement a DNS-over-HTTPS proxy

author Alberto Bertogli
2016-05-17 22:12:47 UTC
committer Alberto Bertogli
2016-05-19 09:35:21 UTC
parent 313bcf21cf8861017011009ed68978a6ed8749e2

Implement a DNS-over-HTTPS proxy

This commit extends dnstox to support DNS-over-HTTPS, as implemented by
https://dns.google.com.

README.md +28 -7
dnss.go +37 -7
dnstox/resolver.go +176 -0

diff --git a/README.md b/README.md
index 4f1a4f9..1fe676a 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,39 @@
 
-# dnss - Encapsulate DNS over GRPC
+# dnss
 
-dnss encapsulates DNS over GRPC.
+dnss is a tool for encapsulating DNS over more secure protocols.
+
+
+## DNS over HTTPS
+
+dnss can act as a DNS-over-HTTPS proxy, using https://dns.google.com as a
+server.
+
+```
++--------+       +----------------+        +----------------+
+|        |       |     dnss       |        |                |
+| client +-------> (dns-to-https) +--------> dns.google.com |
+|        |  DNS  |                |        |                |
++--------+  UDP  +----------------+  HTTP  +----------------+
+                                     SSL
+                                     TCP
+```
+
+
+## DNS over GRPC
+
+dnss can encapsulate DNS over GRPC.
 
 It can be useful when you want to use a particular DNS server, but don't want
 some parts of the network in between to be able to see your traffic.
 
 
 ```
-+--------+       +---------------+        +---------------+       +------------+
-|        |       |     dnss      |        |     dnss      |       |            |
-| client +-------> (dns-to-grpc) +--------> (grpc-to-dns) +-------> DNS server |
-|        |  DNS  |               |  DNS   |               |  DNS  |            |
-+--------+  UDP  +---------------+  GRPC  +---------------+  UDP  +------------+
++--------+       +---------------+        +---------------+      +------------+
+|        |       |     dnss      |        |     dnss      |      |            |
+| client +-------> (dns-to-grpc) +--------> (grpc-to-dns) +------> DNS server |
+|        |  DNS  |               |  DNS   |               | DNS  |            |
++--------+  UDP  +---------------+  GRPC  +---------------+ UDP  +------------+
                                     SSL
                                     TCP
 ```
diff --git a/dnss.go b/dnss.go
index 54c4c0d..4dd6cc1 100644
--- a/dnss.go
+++ b/dnss.go
@@ -20,16 +20,17 @@ import (
 )
 
 var (
-	enableDNStoGRPC = flag.Bool("enable_dns_to_grpc", false,
-		"enable DNS-to-GRPC server")
 	dnsListenAddr = flag.String("dns_listen_addr", ":53",
 		"address to listen on for DNS")
+	dnsUnqualifiedUpstream = flag.String("dns_unqualified_upstream", "",
+		"DNS server to forward unqualified requests to")
+
+	enableDNStoGRPC = flag.Bool("enable_dns_to_grpc", false,
+		"enable DNS-to-GRPC server")
 	grpcUpstream = flag.String("grpc_upstream", "localhost:9953",
 		"address of the upstream GRPC server")
 	grpcClientCAFile = flag.String("grpc_client_cafile", "",
 		"CA file to use for the GRPC client")
-	dnsUnqualifiedUpstream = flag.String("dns_unqualified_upstream", "",
-		"DNS server to forward unqualified requests to")
 
 	enableGRPCtoDNS = flag.Bool("enable_grpc_to_dns", false,
 		"enable GRPC-to-DNS server")
@@ -38,6 +39,14 @@ var (
 	dnsUpstream = flag.String("dns_upstream", "8.8.8.8:53",
 		"address of the upstream DNS server")
 
+	enableDNStoHTTPS = flag.Bool("enable_dns_to_https", false,
+		"enable DNS-to-HTTPS proxy")
+	httpsUpstream = flag.String("https_upstream",
+		"https://dns.google.com/resolve",
+		"URL of upstream DNS-to-HTTP server")
+	httpsClientCAFile = flag.String("https_client_cafile", "",
+		"CA file to use for the HTTPS client")
+
 	grpcCert = flag.String("grpc_cert", "",
 		"certificate file for the GRPC server")
 	grpcKey = flag.String("grpc_key", "",
@@ -71,9 +80,18 @@ func main() {
 		go http.ListenAndServe(*monitoringListenAddr, nil)
 	}
 
-	if !*enableDNStoGRPC && !*enableGRPCtoDNS {
-		glog.Fatal(
-			"Error: pass --enable_dns_to_grpc or --enable_grpc_to_dns")
+	if !*enableDNStoGRPC && !*enableGRPCtoDNS && !*enableDNStoHTTPS {
+		glog.Error("Need to set one of the following:")
+		glog.Error("  --enable_dns_to_https")
+		glog.Error("  --enable_dns_to_grpc")
+		glog.Error("  --enable_grpc_to_dns")
+		glog.Fatal("")
+	}
+
+	if *enableDNStoGRPC && *enableDNStoHTTPS {
+		glog.Error("The following options cannot be set at the same time:")
+		glog.Error("  --enable_dns_to_grpc and --enable_dns_to_https")
+		glog.Fatal("")
 	}
 
 	var wg sync.WaitGroup
@@ -105,5 +123,17 @@ func main() {
 		}()
 	}
 
+	// DNS to HTTPS.
+	if *enableDNStoHTTPS {
+		r := dnstox.NewHTTPSResolver(*httpsUpstream, *httpsClientCAFile)
+		cr := dnstox.NewCachingResolver(r)
+		dth := dnstox.New(*dnsListenAddr, cr, *dnsUnqualifiedUpstream)
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			dth.ListenAndServe()
+		}()
+	}
+
 	wg.Wait()
 }
diff --git a/dnstox/resolver.go b/dnstox/resolver.go
index 2c8c6dd..3db5e0f 100644
--- a/dnstox/resolver.go
+++ b/dnstox/resolver.go
@@ -1,9 +1,14 @@
 package dnstox
 
 import (
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
 	"expvar"
 	"fmt"
+	"io/ioutil"
 	"net/http"
+	"net/url"
 	"sync"
 	"time"
 
@@ -32,6 +37,9 @@ type Resolver interface {
 	Query(r *dns.Msg, tr trace.Trace) (*dns.Msg, error)
 }
 
+///////////////////////////////////////////////////////////////////////////
+// GRPC resolver.
+
 // grpcResolver implements the Resolver interface by querying a server via
 // GRPC.
 type grpcResolver struct {
@@ -92,6 +100,174 @@ func (g *grpcResolver) Query(r *dns.Msg, tr trace.Trace) (*dns.Msg, error) {
 	return m, err
 }
 
+///////////////////////////////////////////////////////////////////////////
+// HTTPS resolver.
+
+// httpsResolver implements the Resolver interface by querying a server via
+// DNS-over-HTTPS (like https://dns.google.com).
+type httpsResolver struct {
+	Upstream string
+	CAFile   string
+	client   *http.Client
+}
+
+func loadCertPool(caFile string) (*x509.CertPool, error) {
+	pemData, err := ioutil.ReadFile(caFile)
+	if err != nil {
+		return nil, err
+	}
+
+	pool := x509.NewCertPool()
+	if !pool.AppendCertsFromPEM(pemData) {
+		return nil, fmt.Errorf("Error appending certificates")
+	}
+
+	return pool, nil
+}
+
+func NewHTTPSResolver(upstream, caFile string) *httpsResolver {
+	return &httpsResolver{
+		Upstream: upstream,
+		CAFile:   caFile,
+	}
+}
+
+func (r *httpsResolver) Init() error {
+	r.client = &http.Client{
+		// Give our HTTP requests 4 second timeouts: DNS usually doesn't wait
+		// that long anyway, but this helps with slow connections.
+		Timeout: 4 * time.Second,
+	}
+
+	if r.CAFile == "" {
+		return nil
+	}
+
+	pool, err := loadCertPool(r.CAFile)
+	if err != nil {
+		return err
+	}
+
+	r.client.Transport = &http.Transport{
+		TLSClientConfig: &tls.Config{
+			ClientCAs: pool,
+		},
+	}
+
+	return nil
+}
+
+func (r *httpsResolver) Maintain() {
+}
+
+// Structure for parsing JSON responses.
+type jsonResponse struct {
+	Status   int
+	TC       bool
+	RD       bool
+	RA       bool
+	AD       bool
+	CD       bool
+	Question []jsonRR
+	Answer   []jsonRR
+}
+
+type jsonRR struct {
+	Name string `json:name`
+	Type uint16 `json:type`
+	TTL  uint32 `json:TTL`
+	Data string `json:data`
+}
+
+func (r *httpsResolver) Query(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.
+	if len(req.Question) != 1 {
+		return nil, fmt.Errorf("multi-question query")
+	}
+
+	question := req.Question[0]
+	// Only answer IN-class queries, which are the ones used in practice.
+	if question.Qclass != dns.ClassINET {
+		return nil, fmt.Errorf("query class != IN")
+	}
+
+	// Build the query and send the request.
+	v := url.Values{}
+	v.Set("name", question.Name)
+	v.Set("type", dns.TypeToString[question.Qtype])
+	// TODO: add random_padding.
+
+	url := r.Upstream + "?" + v.Encode()
+	if glog.V(3) {
+		tr.LazyPrintf("GET %q", url)
+	}
+
+	hr, err := r.client.Get(url)
+	if err != nil {
+		return nil, fmt.Errorf("GET 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 JSON.
+	body, err := ioutil.ReadAll(hr.Body)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to read body: %v", err)
+	}
+
+	jr := &jsonResponse{}
+	err = json.Unmarshal(body, jr)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to unmarshall: %v", err)
+	}
+
+	// Build the DNS response.
+	resp := &dns.Msg{
+		MsgHdr: dns.MsgHdr{
+			Id:       req.Id,
+			Response: true,
+			Opcode:   req.Opcode,
+			Rcode:    jr.Status,
+
+			Truncated:          jr.TC,
+			RecursionDesired:   jr.RD,
+			RecursionAvailable: jr.RA,
+			AuthenticatedData:  jr.AD,
+			CheckingDisabled:   jr.CD,
+		},
+	}
+
+	if len(jr.Question) != 1 {
+		return nil, fmt.Errorf("Wrong number of questions in the response")
+	}
+	resp.SetQuestion(jr.Question[0].Name, jr.Question[0].Type)
+
+	for _, answer := range jr.Answer {
+		// TODO: This "works" but is quite hacky. Is there a better way,
+		// without doing lots of data parsing?
+		s := fmt.Sprintf("%s %d IN %s %s",
+			answer.Name, answer.TTL,
+			dns.TypeToString[answer.Type], answer.Data)
+		rr, err := dns.NewRR(s)
+		if err != nil {
+			return nil, fmt.Errorf("Error parsing answer: %v", err)
+		}
+
+		resp.Answer = append(resp.Answer, rr)
+	}
+
+	return resp, nil
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Caching resolver.
+
 // cachingResolver implements a caching Resolver.
 // It is backed by another Resolver, but will cache results.
 type cachingResolver struct {