httpserver: Implement a DoH (DNS over HTTPS) handler
This patch adds a DoH handler to the HTTPS-to-DNS server.
The handler is defined according to the current DoH draft,
https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-05.
It's still experimental and will likely change in the future.
Alberto Bertogli
6 years ago
0 | 0 | // Package httpserver implements an HTTPS server which handles DNS requests |
1 | 1 | // over HTTPS. |
2 | // | |
3 | // It implements: | |
4 | // - Google's DNS over HTTPS using JSON (dns-json), as specified in: | |
5 | // https://developers.google.com/speed/public-dns/docs/dns-over-https#api_specification. | |
6 | // This is also implemented by Cloudflare's 1.1.1.1, as documented in: | |
7 | // https://developers.cloudflare.com/1.1.1.1/dns-over-https/json-format/. | |
8 | // - DNS Queries over HTTPS (DoH), as specified in: | |
9 | // https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-05. | |
2 | 10 | package httpserver |
3 | 11 | |
4 | 12 | import ( |
13 | "encoding/base64" | |
5 | 14 | "encoding/json" |
6 | 15 | "fmt" |
16 | "io" | |
17 | "io/ioutil" | |
18 | "mime" | |
7 | 19 | "net" |
8 | 20 | "net/http" |
9 | 21 | "net/url" |
17 | 29 | "golang.org/x/net/trace" |
18 | 30 | ) |
19 | 31 | |
20 | // Server is an HTTPS server that implements DNS over HTTPS, as specified in | |
21 | // https://developers.google.com/speed/public-dns/docs/dns-over-https#api_specification. | |
32 | // Server is an HTTPS server that implements DNS over HTTPS, see the | |
33 | // package-level documentation for more references. | |
22 | 34 | type Server struct { |
23 | 35 | Addr string |
24 | 36 | Upstream string |
33 | 45 | // ListenAndServe starts the HTTPS server. |
34 | 46 | func (s *Server) ListenAndServe() { |
35 | 47 | mux := http.NewServeMux() |
48 | mux.HandleFunc("/dns-query", s.Resolve) | |
36 | 49 | mux.HandleFunc("/resolve", s.Resolve) |
37 | 50 | srv := http.Server{ |
38 | 51 | Addr: s.Addr, |
49 | 62 | glog.Fatalf("HTTPS exiting: %s", err) |
50 | 63 | } |
51 | 64 | |
52 | // Resolve "DNS over HTTPS" requests, and returns responses as specified in | |
53 | // https://developers.google.com/speed/public-dns/docs/dns-over-https#api_specification. | |
54 | // It implements an http.HandlerFunc so it can be used with any standard Go | |
55 | // HTTP server. | |
65 | // Resolve implements the HTTP handler for incoming DNS resolution requests. | |
66 | // It handles "Google's DNS over HTTPS using JSON" requests, as well as "DoH" | |
67 | // request. | |
56 | 68 | func (s *Server) Resolve(w http.ResponseWriter, req *http.Request) { |
57 | 69 | tr := trace.New("httpserver", "/resolve") |
58 | 70 | defer tr.Finish() |
59 | ||
60 | 71 | tr.LazyPrintf("from:%v", req.RemoteAddr) |
61 | ||
72 | tr.LazyPrintf("method:%v", req.Method) | |
73 | ||
74 | req.ParseForm() | |
75 | ||
76 | // Identify DoH requests: | |
77 | // - GET requests have a "dns=" query parameter. | |
78 | // - POST requests have a content-type = application/dns-udpwireformat. | |
79 | if req.Method == "GET" && req.FormValue("dns") != "" { | |
80 | tr.LazyPrintf("DoH:GET") | |
81 | dnsQuery, err := base64.RawURLEncoding.DecodeString( | |
82 | req.FormValue("dns")) | |
83 | if err != nil { | |
84 | util.TraceError(tr, err) | |
85 | http.Error(w, err.Error(), http.StatusBadRequest) | |
86 | return | |
87 | } | |
88 | ||
89 | s.resolveDoH(tr, w, dnsQuery) | |
90 | return | |
91 | } | |
92 | ||
93 | if req.Method == "POST" { | |
94 | ct, _, err := mime.ParseMediaType(req.Header.Get("Content-Type")) | |
95 | if err != nil { | |
96 | util.TraceError(tr, err) | |
97 | http.Error(w, err.Error(), http.StatusBadRequest) | |
98 | return | |
99 | } | |
100 | ||
101 | if ct == "application/dns-udpwireformat" { | |
102 | tr.LazyPrintf("DoH:POST") | |
103 | // Limit the size of request to 4k. | |
104 | dnsQuery, err := ioutil.ReadAll(io.LimitReader(req.Body, 4092)) | |
105 | if err != nil { | |
106 | util.TraceError(tr, err) | |
107 | http.Error(w, err.Error(), http.StatusBadRequest) | |
108 | return | |
109 | } | |
110 | ||
111 | s.resolveDoH(tr, w, dnsQuery) | |
112 | return | |
113 | } | |
114 | } | |
115 | ||
116 | // Fall back to Google's JSON, the laxer format. | |
117 | // It MUST have a "name" query parameter, so we use that for detection. | |
118 | if req.Method == "GET" && req.FormValue("name") != "" { | |
119 | tr.LazyPrintf("Google-JSON") | |
120 | s.resolveJSON(tr, w, req) | |
121 | return | |
122 | } | |
123 | ||
124 | // Could not found how to handle this request. | |
125 | util.TraceErrorf(tr, "unknown request type") | |
126 | http.Error(w, "unknown request type", http.StatusUnsupportedMediaType) | |
127 | } | |
128 | ||
129 | // Resolve "Google's DNS over HTTPS using JSON" requests, and returns | |
130 | // responses as specified in | |
131 | // https://developers.google.com/speed/public-dns/docs/dns-over-https#api_specification. | |
132 | func (s *Server) resolveJSON(tr trace.Trace, w http.ResponseWriter, req *http.Request) { | |
62 | 133 | // Construct the DNS request from the http query. |
63 | 134 | q, err := parseQuery(req.URL) |
64 | 135 | if err != nil { |
256 | 327 | |
257 | 328 | return false, errInvalidCD |
258 | 329 | } |
330 | ||
331 | // Resolve DNS over HTTPS requests, as specified in | |
332 | // https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-05. | |
333 | func (s *Server) resolveDoH(tr trace.Trace, w http.ResponseWriter, dnsQuery []byte) { | |
334 | r := &dns.Msg{} | |
335 | err := r.Unpack(dnsQuery) | |
336 | if err != nil { | |
337 | util.TraceError(tr, err) | |
338 | http.Error(w, err.Error(), http.StatusBadRequest) | |
339 | return | |
340 | } | |
341 | ||
342 | util.TraceQuestion(tr, r.Question) | |
343 | ||
344 | // Do the DNS request, get the reply. | |
345 | fromUp, err := dns.Exchange(r, s.Upstream) | |
346 | if err != nil { | |
347 | err = util.TraceErrorf(tr, "dns exchange error: %v", err) | |
348 | http.Error(w, err.Error(), http.StatusFailedDependency) | |
349 | return | |
350 | } | |
351 | ||
352 | if fromUp == nil { | |
353 | err = util.TraceErrorf(tr, "no response from upstream") | |
354 | http.Error(w, err.Error(), http.StatusRequestTimeout) | |
355 | return | |
356 | } | |
357 | ||
358 | util.TraceAnswer(tr, fromUp) | |
359 | ||
360 | packed, err := fromUp.Pack() | |
361 | if err != nil { | |
362 | err = util.TraceErrorf(tr, "cannot pack reply: %v", err) | |
363 | http.Error(w, err.Error(), http.StatusFailedDependency) | |
364 | return | |
365 | } | |
366 | ||
367 | // Write the response back. | |
368 | w.Header().Set("Content-type", "application/dns-udpwireformat") | |
369 | // TODO: set cache-control based on the response. | |
370 | w.WriteHeader(http.StatusOK) | |
371 | w.Write(packed) | |
372 | } | |
373 | ||
374 | func parseContentType(s string) (string, error) { | |
375 | mt, _, err := mime.ParseMediaType(s) | |
376 | return mt, err | |
377 | } |