author | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-07-28 00:03:57 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-07-28 00:38:12 UTC |
parent | 90210b7a812e204a5782dddd648c3380e724e31b |
README.md | +0 | -3 |
dnss.go | +2 | -13 |
dnss_test.go | +10 | -30 |
internal/dnsjson/dnsjson.go | +0 | -28 |
internal/httpresolver/json_test.go | +0 | -146 |
internal/httpresolver/resolver.go | +1 | -151 |
internal/httpserver/parser_test.go | +0 | -98 |
internal/httpserver/server.go | +3 | -223 |
tests/external.sh | +0 | -51 |
diff --git a/README.md b/README.md index 9cf2a81..8472465 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,6 @@ It can also act as a DoH server, in case you want end to end control. * Supports the [DNS Queries over HTTPS (DoH)](https://en.wikipedia.org/wiki/DNS_over_HTTPS) standard ([RFC 8484](https://tools.ietf.org/html/rfc8484)). -* Supports the older JSON-based protocol as implemented by - [dns.google](https://dns.google) - ([reference](https://developers.google.com/speed/public-dns/docs/dns-over-https)). * Local cache (optional). * HTTP(s) proxy support, autodetected from the environment. * Monitoring HTTP server, with exported variables and tracing to help diff --git a/dnss.go b/dnss.go index 45dff93..2c72257 100644 --- a/dnss.go +++ b/dnss.go @@ -69,13 +69,11 @@ var ( monitoringListenAddr = flag.String("monitoring_listen_addr", "", "address to listen on for monitoring HTTP requests") - forceMode = flag.String("force_mode", "", - "Force HTTPS resolver mode ('JSON', 'DoH', 'autodetect' (default))") - // Deprecated flags that no longer make sense; we keep them for backwards // compatibility but may be removed in the future. _ = flag.Duration("log_flush_every", 0, "deprecated, will be removed") _ = flag.Bool("logtostderr", false, "deprecated, will be removed") + _ = flag.String("force_mode", "", "deprecated, will be removed") ) func main() { @@ -103,16 +101,7 @@ func main() { } var resolver dnsserver.Resolver - switch *forceMode { - case "DoH": - resolver = httpresolver.NewDoH(upstream, *httpsClientCAFile) - case "JSON": - resolver = httpresolver.NewJSON(upstream, *httpsClientCAFile) - case "", "autodetect": - resolver = httpresolver.New(upstream, *httpsClientCAFile) - default: - log.Fatalf("-force_mode=%q is not a valid mode", *forceMode) - } + resolver = httpresolver.NewDoH(upstream, *httpsClientCAFile) if *enableCache { cr := dnsserver.NewCachingResolver(resolver) diff --git a/dnss_test.go b/dnss_test.go index c98d5a0..ec8d1b4 100644 --- a/dnss_test.go +++ b/dnss_test.go @@ -39,7 +39,7 @@ func TestMain(m *testing.M) { // responses as needed. // // Returns the address of the DNS-to-HTTPS server, for the tests to use. -func Setup(tb testing.TB, mode string) string { +func Setup(tb testing.TB) string { DNSToHTTPSAddr := testutil.GetFreePort() HTTPSToDNSAddr := testutil.GetFreePort() DNSServerAddr := testutil.GetFreePort() @@ -55,39 +55,25 @@ func Setup(tb testing.TB, mode string) string { // Test DNS server. go testutil.ServeTestDNSServer(DNSServerAddr, handleTestDNS) - // Wait for the above to start; the DNS to HTTPS server below needs them - // up for protocol autodetection. - if err := testutil.WaitForHTTPServer(HTTPSToDNSAddr); err != nil { - tb.Fatalf("Error waiting for HTTPS to DNS server to start: %v", err) - } - if err := testutil.WaitForDNSServer(DNSServerAddr); err != nil { - tb.Fatalf("Error waiting for testing DNS server to start: %v", err) - } - // DNS to HTTPS server. HTTPSToDNSURL, err := url.Parse("http://" + HTTPSToDNSAddr + "/resolve") if err != nil { tb.Fatalf("invalid URL: %v", err) } - var r dnsserver.Resolver - switch mode { - case "DoH": - r = httpresolver.NewDoH(HTTPSToDNSURL, "") - case "JSON": - r = httpresolver.NewJSON(HTTPSToDNSURL, "") - case "autodetect": - r = httpresolver.New(HTTPSToDNSURL, "") - default: - tb.Fatalf("%q is not a valid mode", mode) - } - + r := httpresolver.NewDoH(HTTPSToDNSURL, "") dtoh := dnsserver.New(DNSToHTTPSAddr, r, "") go dtoh.ListenAndServe() if err := testutil.WaitForDNSServer(DNSToHTTPSAddr); err != nil { tb.Fatalf("Error waiting for DNS to HTTPS server to start: %v", err) } + if err := testutil.WaitForHTTPServer(HTTPSToDNSAddr); err != nil { + tb.Fatalf("Error waiting for HTTPS to DNS server to start: %v", err) + } + if err := testutil.WaitForDNSServer(DNSServerAddr); err != nil { + tb.Fatalf("Error waiting for testing DNS server to start: %v", err) + } return DNSToHTTPSAddr } @@ -148,13 +134,7 @@ func handleTestDNS(w dns.ResponseWriter, r *dns.Msg) { // func TestEndToEnd(t *testing.T) { - t.Run("mode=JSON", func(t *testing.T) { testEndToEnd(t, "JSON") }) - t.Run("mode=DoH", func(t *testing.T) { testEndToEnd(t, "DoH") }) - t.Run("mode=autodetect", func(t *testing.T) { testEndToEnd(t, "autodetect") }) -} - -func testEndToEnd(t *testing.T, mode string) { - ServerAddr := Setup(t, mode) + ServerAddr := Setup(t) resetAnswers() addAnswers(t, "test.blah. 3600 A 1.2.3.4") _, ans, err := testutil.DNSQuery(ServerAddr, "test.blah.", dns.TypeA) @@ -188,7 +168,7 @@ func testEndToEnd(t *testing.T, mode string) { // func BenchmarkSimple(b *testing.B) { - ServerAddr := Setup(b, "DoH") + ServerAddr := Setup(b) resetAnswers() addAnswers(b, "test.blah. 3600 A 1.2.3.4") b.ResetTimer() diff --git a/internal/dnsjson/dnsjson.go b/internal/dnsjson/dnsjson.go deleted file mode 100644 index 4b88e9d..0000000 --- a/internal/dnsjson/dnsjson.go +++ /dev/null @@ -1,28 +0,0 @@ -// Package dnsjson contains structures for representing DNS responses as JSON. -// -// Matches the API implemented by https://dns.google/. -package dnsjson - -// Response is the highest level struct in the DNS JSON response. -// Note the fields must match the JSON API specified at -// https://developers.google.com/speed/public-dns/docs/dns-over-https#dns_response_in_json/. -type Response struct { - Status int // Standard DNS response code (32 bit integer). - TC bool // Whether the response is truncated - RD bool // Whether recursion is desired. - RA bool // Whether recursion is available. - AD bool // Whether all response data was validated with DNSSEC - CD bool // Whether the client asked to disable DNSSEC - Question []RR // Question we're responding to. - Answer []RR // Answer to the question. -} - -// RR represents a JSON-encoded DNS RR. -// Note the fields must match the JSON API specified at -// https://developers.google.com/speed/public-dns/docs/dns-over-https#dns_response_in_json/. -type RR struct { - Name string // FQDN for the RR. - Type uint16 // DNS RR type. - TTL uint32 // Record's time to live, in seconds. - Data string // Data for the record (e.g. for A it's the IP address). -} diff --git a/internal/httpresolver/json_test.go b/internal/httpresolver/json_test.go deleted file mode 100644 index e4ad0a0..0000000 --- a/internal/httpresolver/json_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package httpresolver - -import ( - "flag" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "os" - "testing" - - "blitiri.com.ar/go/dnss/internal/dnsserver" - "blitiri.com.ar/go/dnss/internal/testutil" - - "github.com/miekg/dns" -) - -// -// === Tests === -// - -func TestSimple(t *testing.T) { - _, ans, err := testutil.DNSQuery(DNSAddr, "test.blah.", dns.TypeA) - if err != nil { - t.Errorf("dns query returned error: %v", err) - } - if ans.(*dns.A).A.String() != "1.2.3.4" { - t.Errorf("unexpected result: %q", ans) - } - - _, ans, err = testutil.DNSQuery(DNSAddr, "test.blah.", dns.TypeMX) - if err != nil { - t.Errorf("dns query returned error: %v", err) - } - if ans.(*dns.MX).Mx != "mail.test.blah." { - t.Errorf("unexpected result: %q", ans.(*dns.MX).Mx) - } - - in, _, err := testutil.DNSQuery(DNSAddr, "unknown.", dns.TypeA) - if err != nil { - t.Errorf("dns query returned error: %v", err) - } - if in.Rcode != dns.RcodeNameError { - t.Errorf("unexpected result: %q", in) - } -} - -// -// === Benchmarks === -// - -func BenchmarkHTTPSimple(b *testing.B) { - var err error - for i := 0; i < b.N; i++ { - _, _, err = testutil.DNSQuery(DNSAddr, "test.blah.", dns.TypeA) - if err != nil { - b.Errorf("dns query returned error: %v", err) - } - } -} - -// -// === Test environment === -// - -// DNSHandler handles DNS-over-HTTP requests, and returns json data. -// This is used as the test server for our resolver. -func DNSHandler(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - panic(err) - } - - w.Header().Set("Content-Type", "text/json") - - resp := jsonNXDOMAIN - - if r.Form["name"][0] == "test.blah." { - switch r.Form["type"][0] { - case "1", "A": - resp = jsonA - case "15", "MX": - resp = jsonMX - default: - resp = jsonNXDOMAIN - } - } - - w.Write([]byte(resp)) -} - -// A record. -const jsonA = ` { - "Status": 0, "TC": false, "RD": true, "RA": true, "AD": false, "CD": false, - "Question": [ { "name": "test.blah.", "type": 1 } - ], - "Answer": [ { "name": "test.blah.", "type": 1, "TTL": 21599, - "data": "1.2.3.4" } ] } -` - -// MX record. -const jsonMX = ` { - "Status": 0, "TC": false, "RD": true, "RA": true, "AD": false, "CD": false, - "Question": [ { "name": "test.blah.", "type": 15 } ], - "Answer": [ { "name": "test.blah.", "type": 15, "TTL": 21599, - "data": "10 mail.test.blah." } ] } -` - -// NXDOMAIN error. -const jsonNXDOMAIN = ` { - "Status": 3, "TC": false, "RD": true, "RA": true, "AD": true, "CD": false, - "Question": [ { "name": "doesnotexist.", "type": 15 } ], - "Authority": [ { "name": ".", "type": 6, "TTL": 1798, - "data": "root. nstld. 2016052201 1800 900 604800 86400" } ] } -` - -// Address where we will set up the DNS server. -var DNSAddr string - -func TestMain(m *testing.M) { - flag.Parse() - - DNSAddr = testutil.GetFreePort() - - // Test http server. - httpsrv := httptest.NewServer(http.HandlerFunc(DNSHandler)) - - // DNS to HTTPS server. - srvURL, err := url.Parse(httpsrv.URL) - if err != nil { - fmt.Printf("Failed to parse test http server URL: %v\n", err) - os.Exit(1) - } - r := NewJSON(srvURL, "") - dth := dnsserver.New(DNSAddr, r, "") - go dth.ListenAndServe() - - // Wait for the servers to start up. - err = testutil.WaitForDNSServer(DNSAddr) - if err != nil { - fmt.Printf("Error waiting for the test servers to start: %v\n", err) - fmt.Printf("Check the INFO logs for more details\n") - os.Exit(1) - } - os.Exit(m.Run()) -} diff --git a/internal/httpresolver/resolver.go b/internal/httpresolver/resolver.go index b770a51..6bf2cac 100644 --- a/internal/httpresolver/resolver.go +++ b/internal/httpresolver/resolver.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/tls" "crypto/x509" - "encoding/json" "fmt" "io" "io/ioutil" @@ -13,7 +12,6 @@ import ( "net/url" "time" - "blitiri.com.ar/go/dnss/internal/dnsjson" "blitiri.com.ar/go/dnss/internal/dnsserver" "blitiri.com.ar/go/log" @@ -22,15 +20,11 @@ import ( ) // httpsResolver implements the dnsserver.Resolver interface by querying a -// server via DNS over HTTPS. -// -// It supports two modes: JSON (like https://dns.google) and DoH -// (https://en.wikipedia.org/wiki/DNS_over_HTTPS, RFC 8484). +// server via DNS over HTTPS (DoH, RFC 8484). type httpsResolver struct { Upstream *url.URL CAFile string client *http.Client - mode string } func loadCertPool(caFile string) (*x509.CertPool, error) { @@ -47,34 +41,12 @@ func loadCertPool(caFile string) (*x509.CertPool, error) { return pool, nil } -// New creates a new HTTPS resolver, which uses the given upstream URL to -// resolve queries. It will auto-detect the mode (JSON or DoH) by doing a -// resolution at initialization time. -func New(upstream *url.URL, caFile string) *httpsResolver { - return &httpsResolver{ - Upstream: upstream, - CAFile: caFile, - mode: "autodetect", - } -} - -// NewJSON creates a new JSON resolver which uses the given upstream URL to -// resolve queries. -func NewJSON(upstream *url.URL, caFile string) *httpsResolver { - return &httpsResolver{ - Upstream: upstream, - CAFile: caFile, - mode: "JSON", - } -} - // NewDoH creates a new DoH resolver, which uses the given upstream // URL to resolve queries. func NewDoH(upstream *url.URL, caFile string) *httpsResolver { return &httpsResolver{ Upstream: upstream, CAFile: caFile, - mode: "DoH", } } @@ -105,43 +77,13 @@ func (r *httpsResolver) Init() error { } } - if r.mode == "autodetect" { - if err := r.autodetect(); err != nil { - return err - } - } - return nil } -func (r *httpsResolver) autodetect() error { - tr := trace.New("httpsresolver", "Autodetect") - defer tr.Finish() - - m := &dns.Msg{} - m.SetQuestion("example.com.", dns.TypeA) - - for _, mode := range []string{"DoH", "JSON"} { - r.mode = mode - if _, err := r.Query(m, tr); err == nil { - return nil - } - } - - return fmt.Errorf("Failed to autodetect resolver mode") -} - 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) @@ -191,97 +133,5 @@ func (r *httpsResolver) queryDoH(req *dns.Msg, tr trace.Trace) (*dns.Msg, error) 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. - 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. - url := *r.Upstream - vs := url.Query() - vs.Set("name", question.Name) - vs.Set("type", dns.TypeToString[question.Qtype]) - url.RawQuery = vs.Encode() - // TODO: add random_padding. - - if log.V(3) { - tr.LazyPrintf("JSON GET %v", url) - } - - hr, err := r.client.Get(url.String()) - 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(io.LimitReader(hr.Body, 64*1024)) - if err != nil { - return nil, fmt.Errorf("Failed to read body: %v", err) - } - - jr := &dnsjson.Response{} - err = json.Unmarshal(body, jr) - if err != nil { - return nil, fmt.Errorf("Failed to unmarshall: %v", err) - } - - if len(jr.Question) != 1 { - return nil, fmt.Errorf("Wrong number of questions in the response") - } - - // 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, - }, - Question: []dns.Question{ - { - Name: jr.Question[0].Name, - Qtype: jr.Question[0].Type, - Qclass: dns.ClassINET, - }}, - } - - 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 -} - // Compile-time check that the implementation matches the interface. var _ dnsserver.Resolver = &httpsResolver{} diff --git a/internal/httpserver/parser_test.go b/internal/httpserver/parser_test.go deleted file mode 100644 index 37b84a1..0000000 --- a/internal/httpserver/parser_test.go +++ /dev/null @@ -1,98 +0,0 @@ -// Tests for the query parsing. -package httpserver - -import ( - "net" - "net/url" - "reflect" - "testing" - - "github.com/miekg/dns" -) - -func makeURL(t *testing.T, query string) *url.URL { - u, err := url.Parse("http://site/resolve?" + query) - if err != nil { - t.Fatalf("URL parsing failed: %v", err) - } - - return u -} - -func makeIPNet(s string) *net.IPNet { - _, n, err := net.ParseCIDR(s) - if err != nil { - panic(err) - } - return n -} - -func queryEq(q1, q2 query) bool { - return reflect.DeepEqual(q1, q2) -} - -// A DNS name which is too long (> 253 characters), but otherwise valid. -const longName = "pablitoclavounclavitoqueclavitoclavopablito-pablitoclavounclavitoqueclavitoclavopablito-pablitoclavounclavitoqueclavitoclavopablito-pablitoclavounclavitoqueclavitoclavopablito-pablitoclavounclavitoqueclavitoclavopablito-pablitoclavounclavitoqueclavitoclavopablito" - -func Test(t *testing.T) { - cases := []struct { - rawQ string - q query - }{ - {"name=hola", query{"hola", dns.TypeA, false, nil}}, - {"name=hola&type=a", query{"hola", dns.TypeA, false, nil}}, - {"name=hola&type=A", query{"hola", dns.TypeA, false, nil}}, - {"name=hola&type=1", query{"hola", dns.TypeA, false, nil}}, - {"name=hola&type=MX", query{"hola", dns.TypeMX, false, nil}}, - {"name=hola&type=txt", query{"hola", dns.TypeTXT, false, nil}}, - {"name=x&cd", query{"x", dns.TypeA, true, nil}}, - {"name=x&cd=1", query{"x", dns.TypeA, true, nil}}, - {"name=x&cd=true", query{"x", dns.TypeA, true, nil}}, - {"name=x&cd=0", query{"x", dns.TypeA, false, nil}}, - {"name=x&cd=false", query{"x", dns.TypeA, false, nil}}, - {"name=x&type=mx;cd", query{"x", dns.TypeMX, true, nil}}, - - { - "name=x&edns_client_subnet=1.2.3.0/21", - query{"x", dns.TypeA, false, makeIPNet("1.2.3.0/21")}, - }, - { - "name=x&edns_client_subnet=2001:700:300::/48", - query{"x", dns.TypeA, false, makeIPNet("2001:700:300::/48")}, - }, - { - "name=x&type=mx&cd&edns_client_subnet=2001:700:300::/48", - query{"x", dns.TypeMX, true, makeIPNet("2001:700:300::/48")}, - }, - } - for _, c := range cases { - q, err := parseQuery(makeURL(t, c.rawQ)) - if err != nil { - t.Errorf("query %q: error %v", c.rawQ, err) - } - if !queryEq(q, c.q) { - t.Errorf("query %q: expected %v, got %v", c.rawQ, c.q, q) - } - } - - errCases := []struct { - raw string - err error - }{ - {"", errEmptyName}, - {"name=" + longName, errNameTooLong}, - {"name=x;type=0", errIntOutOfRange}, - {"name=x;type=-1", errIntOutOfRange}, - {"name=x;type=65536", errUnknownType}, - {"name=x;type=merienda", errUnknownType}, - {"name=x;cd=lala", errInvalidCD}, - {"name=x;edns_client_subnet=lala", errInvalidSubnet}, - {"name=x;edns_client_subnet=1.2.3.4", errInvalidSubnet}, - } - for _, c := range errCases { - _, err := parseQuery(makeURL(t, c.raw)) - if err != c.err { - t.Errorf("query %q: expected error %v, got %v", c.raw, c.err, err) - } - } -} diff --git a/internal/httpserver/server.go b/internal/httpserver/server.go index 9114368..e439385 100644 --- a/internal/httpserver/server.go +++ b/internal/httpserver/server.go @@ -1,29 +1,17 @@ // Package httpserver implements an HTTPS server which handles DNS requests // over HTTPS. // -// It implements: -// - Google's DNS over HTTPS using JSON (dns-json), as specified in: -// https://developers.google.com/speed/public-dns/docs/dns-over-https#api_specification. -// This is also implemented by Cloudflare's 1.1.1.1, as documented in: -// https://developers.cloudflare.com/1.1.1.1/dns-over-https/json-format/. -// - DNS Queries over HTTPS (DoH), as specified in RFC 8484: -// https://tools.ietf.org/html/rfc8484. +// It implements DNS Queries over HTTPS (DoH), as specified in RFC 8484: +// https://tools.ietf.org/html/rfc8484. package httpserver import ( "encoding/base64" - "encoding/json" - "fmt" "io" "io/ioutil" "mime" - "net" "net/http" - "net/url" - "strconv" - "strings" - "blitiri.com.ar/go/dnss/internal/dnsjson" "blitiri.com.ar/go/dnss/internal/util" "blitiri.com.ar/go/log" "github.com/miekg/dns" @@ -60,9 +48,7 @@ func (s *Server) ListenAndServe() { log.Fatalf("HTTPS exiting: %s", err) } -// Resolve implements the HTTP handler for incoming DNS resolution requests. -// It handles "Google's DNS over HTTPS using JSON" requests, as well as "DoH" -// request. +// Resolve incoming DoH requests. func (s *Server) Resolve(w http.ResponseWriter, req *http.Request) { tr := trace.New("httpserver", "/resolve") defer tr.Finish() @@ -111,217 +97,11 @@ func (s *Server) Resolve(w http.ResponseWriter, req *http.Request) { } } - // Fall back to Google's JSON, the laxer format. - // It MUST have a "name" query parameter, so we use that for detection. - if req.Method == "GET" && req.FormValue("name") != "" { - tr.LazyPrintf("Google-JSON") - s.resolveJSON(tr, w, req) - return - } - // Could not found how to handle this request. util.TraceErrorf(tr, "unknown request type") http.Error(w, "unknown request type", http.StatusUnsupportedMediaType) } -// Resolve "Google's DNS over HTTPS using JSON" requests, and returns -// responses as specified in -// https://developers.google.com/speed/public-dns/docs/dns-over-https#api_specification. -func (s *Server) resolveJSON(tr trace.Trace, w http.ResponseWriter, req *http.Request) { - // Construct the DNS request from the http query. - q, err := parseQuery(req.URL) - if err != nil { - util.TraceError(tr, err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - r := &dns.Msg{} - r.CheckingDisabled = q.cd - r.SetQuestion(dns.Fqdn(q.name), q.rrType) - - if q.clientSubnet != nil { - o := new(dns.OPT) - o.Hdr.Name = "." - o.Hdr.Rrtype = dns.TypeOPT - e := new(dns.EDNS0_SUBNET) - e.Code = dns.EDNS0SUBNET - if ipv4 := q.clientSubnet.IP.To4(); ipv4 != nil { - e.Family = 1 // IPv4 source address - e.Address = ipv4 - } else { - e.Family = 2 // IPv6 source address - e.Address = q.clientSubnet.IP - } - e.SourceScope = 0 - - _, maskSize := q.clientSubnet.Mask.Size() - e.SourceNetmask = uint8(maskSize) - - o.Option = append(o.Option, e) - r.Extra = append(r.Extra, o) - } - - util.TraceQuestion(tr, r.Question) - - // Do the DNS request, get the reply. - fromUp, err := dns.Exchange(r, s.Upstream) - if err != nil { - err = util.TraceErrorf(tr, "dns exchange error: %v", err) - http.Error(w, err.Error(), http.StatusFailedDependency) - return - } - - if fromUp == nil { - err = util.TraceErrorf(tr, "no response from upstream") - http.Error(w, err.Error(), http.StatusRequestTimeout) - return - } - - util.TraceAnswer(tr, fromUp) - - // Convert the reply to json, and write it back. - jr := &dnsjson.Response{ - Status: fromUp.Rcode, - TC: fromUp.Truncated, - RD: fromUp.RecursionDesired, - RA: fromUp.RecursionAvailable, - AD: fromUp.AuthenticatedData, - CD: fromUp.CheckingDisabled, - } - - for _, q := range fromUp.Question { - rr := dnsjson.RR{ - Name: q.Name, - Type: q.Qtype, - } - jr.Question = append(jr.Question, rr) - } - - for _, a := range fromUp.Answer { - hdr := a.Header() - ja := dnsjson.RR{ - Name: hdr.Name, - Type: hdr.Rrtype, - TTL: hdr.Ttl, - } - - hs := hdr.String() - ja.Data = a.String()[len(hs):] - jr.Answer = append(jr.Answer, ja) - } - - buf, err := json.Marshal(jr) - if err != nil { - err = util.TraceErrorf(tr, "failed to marshal: %v", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Write(buf) -} - -type query struct { - name string - rrType uint16 - cd bool - - // EDNS client subnet (address+mask). - clientSubnet *net.IPNet -} - -var ( - errEmptyName = fmt.Errorf("empty name") - errNameTooLong = fmt.Errorf("name too long") - errInvalidSubnet = fmt.Errorf("invalid edns_client_subnet") - errIntOutOfRange = fmt.Errorf("invalid type (int out of range)") - errUnknownType = fmt.Errorf("invalid type (unknown string type)") - errInvalidCD = fmt.Errorf("invalid cd value") -) - -func parseQuery(u *url.URL) (query, error) { - q := query{ - name: "", - rrType: 1, - cd: false, - clientSubnet: nil, - } - - // Simplify the values map, as all our parameters are single-value only. - vs := map[string]string{} - for k, values := range u.Query() { - if len(values) > 0 { - vs[k] = values[0] - } else { - vs[k] = "" - } - } - var ok bool - var err error - - if q.name, ok = vs["name"]; !ok || q.name == "" { - return q, errEmptyName - } - if len(q.name) > 253 { - return q, errNameTooLong - } - - if _, ok = vs["type"]; ok { - q.rrType, err = stringToRRType(vs["type"]) - if err != nil { - return q, err - } - } - - if cd, ok := vs["cd"]; ok { - q.cd, err = stringToBool(cd) - if err != nil { - return q, err - } - } - - if clientSubnet, ok := vs["edns_client_subnet"]; ok { - _, q.clientSubnet, err = net.ParseCIDR(clientSubnet) - if err != nil { - return q, errInvalidSubnet - } - } - - return q, nil -} - -// stringToRRType converts a string into a DNS type constant. -// The string can be a number in the [1, 65535] range, or a canonical type -// string (case-insensitive, such as "A" or "aaaa"). -func stringToRRType(s string) (uint16, error) { - i, err := strconv.ParseInt(s, 10, 16) - if err == nil { - if 1 <= i && i <= 65535 { - return uint16(i), nil - } - return 0, errIntOutOfRange - } - - rrType, ok := dns.StringToType[strings.ToUpper(s)] - if !ok { - return 0, errUnknownType - } - return rrType, nil -} - -func stringToBool(s string) (bool, error) { - switch strings.ToLower(s) { - case "", "1", "true": - // Note the empty string is intentionally considered true, as long as - // the parameter is present in the query. - return true, nil - case "0", "false": - return false, nil - } - - return false, errInvalidCD -} - // Resolve DNS over HTTPS requests, as specified in RFC 8484. func (s *Server) resolveDoH(tr trace.Trace, w http.ResponseWriter, dnsQuery []byte) { r := &dns.Msg{} diff --git a/tests/external.sh b/tests/external.sh index 0170ef3..ccd5f5b 100755 --- a/tests/external.sh +++ b/tests/external.sh @@ -114,36 +114,8 @@ if ! grep -q "insecure_http_server=true" .wget.out; then exit 1 fi -echo "## Autodetect against dnss" -dnss -enable_dns_to_https -dns_listen_addr "localhost:1053" \ - -insecure_http_server \ - -https_upstream "http://localhost:1999/dns-query" - -resolve -kill $PID - -echo "## JSON against dnss" -dnss -enable_dns_to_https -dns_listen_addr "localhost:1053" \ - -insecure_http_server \ - -force_mode="JSON" \ - -https_upstream "http://localhost:1999/dns-query" - -resolve - -# Exercise some interesting JSON requests. -get "http://localhost:1999/dns-query?name=test&edns_client_subnet=1.2.3.4/24" -get "http://localhost:1999/dns-query?name=test&edns_client_subnet=2001:700:300::/48" -if get "http://localhost:1999/dns-query?name=test&type=lalala"; then - echo "GET with invalid query did not fail" - exit 1 -fi - -kill $PID - echo "## DoH against dnss" dnss -enable_dns_to_https -dns_listen_addr "localhost:1053" \ - -insecure_http_server \ - -force_mode="DoH" \ -https_upstream "http://localhost:1999/dns-query" # Exercise DoH via GET (dnss always uses POST). @@ -165,22 +137,6 @@ kill $PID kill $HTTP_PID -echo "## Autodetect against dns.google/resolve (JSON)" -dnss -enable_dns_to_https -dns_listen_addr "localhost:1053" \ - -https_upstream "https://dns.google/resolve" - -resolve -kill $PID - -echo "## JSON against dns.google/resolve" -dnss -enable_dns_to_https -dns_listen_addr "localhost:1053" \ - -force_mode="JSON" \ - -https_upstream "https://dns.google/resolve" - -resolve -kill $PID - - # DoH integration test against some publicly available servers. # https://github.com/curl/curl/wiki/DNS-over-HTTPS#publicly-available-servers # Note not all of the ones in the list are actually functional. @@ -194,13 +150,6 @@ for server in \ ; do echo "## DoH against $server" - dnss -enable_dns_to_https -dns_listen_addr "localhost:1053" \ - -force_mode="DoH" \ - -https_upstream "$server" - resolve - kill $PID - - echo "## Autodetect against $server" dnss -enable_dns_to_https -dns_listen_addr "localhost:1053" \ -https_upstream "$server" resolve