git » dnss » commit c1f868d

Remove DNS over JSON support

author Alberto Bertogli
2020-07-28 00:03:57 UTC
committer Alberto Bertogli
2020-07-28 00:38:12 UTC
parent 90210b7a812e204a5782dddd648c3380e724e31b

Remove DNS over JSON support

Now that DoH has gained significant adoption, there is not much reason
to keep support for DNS over JSON, which was never standarized.

This patch removes the code necessary for supporting DNS over JSON in
both client and server.

This is unfortunately a backwards-incompatible change for those that
were forcing the use of this protocol to contact a JSON-only server.
Those users will need to adjust how they run dnss, and use DoH.
I don't expect there are many people in that situation, though.

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