1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
|
// Package httpserver implements an HTTPS server which handles DNS requests
// over HTTPS.
//
// It implements DNS Queries over HTTPS (DoH), as specified in RFC 8484:
// https://tools.ietf.org/html/rfc8484.
package httpserver
import (
"encoding/base64"
"io"
"io/ioutil"
"mime"
"net/http"
"blitiri.com.ar/go/dnss/internal/trace"
"blitiri.com.ar/go/log"
"github.com/miekg/dns"
)
// Server is an HTTPS server that implements DNS over HTTPS, see the
// package-level documentation for more references.
type Server struct {
Addr string
Upstream string
CertFile string
KeyFile string
Insecure bool
}
// ListenAndServe starts the HTTPS server.
func (s *Server) ListenAndServe() {
mux := http.NewServeMux()
mux.HandleFunc("/dns-query", s.Resolve)
mux.HandleFunc("/resolve", s.Resolve)
srv := http.Server{
Addr: s.Addr,
Handler: mux,
}
log.Infof("HTTPS listening on %s", s.Addr)
var err error
if s.Insecure {
err = srv.ListenAndServe()
} else {
err = srv.ListenAndServeTLS(s.CertFile, s.KeyFile)
}
log.Fatalf("HTTPS exiting: %s", err)
}
// Resolve incoming DoH requests.
func (s *Server) Resolve(w http.ResponseWriter, req *http.Request) {
tr := trace.New("httpserver", "/resolve")
defer tr.Finish()
tr.Printf("from:%v", req.RemoteAddr)
tr.Printf("method:%v", req.Method)
req.ParseForm()
// Identify DoH requests:
// - GET requests have a "dns=" query parameter.
// - POST requests have a content-type = application/dns-message.
if req.Method == "GET" && req.FormValue("dns") != "" {
tr.Printf("DoH:GET")
dnsQuery, err := base64.RawURLEncoding.DecodeString(
req.FormValue("dns"))
if err != nil {
tr.Error(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.resolveDoH(tr, w, dnsQuery)
return
}
if req.Method == "POST" {
ct, _, err := mime.ParseMediaType(req.Header.Get("Content-Type"))
if err != nil {
tr.Error(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if ct == "application/dns-message" {
tr.Printf("DoH:POST")
// Limit the size of request to 4k.
dnsQuery, err := ioutil.ReadAll(io.LimitReader(req.Body, 4092))
if err != nil {
tr.Error(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.resolveDoH(tr, w, dnsQuery)
return
}
}
// Could not found how to handle this request.
tr.Errorf("unknown request type")
http.Error(w, "unknown request type", http.StatusUnsupportedMediaType)
}
// 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{}
err := r.Unpack(dnsQuery)
if err != nil {
tr.Error(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
tr.Question(r.Question)
// Do the DNS request, get the reply.
fromUp, err := exchange(tr, r, s.Upstream)
if err != nil {
err = tr.Errorf("dns exchange error: %v", err)
http.Error(w, err.Error(), http.StatusFailedDependency)
return
}
if fromUp == nil {
err = tr.Errorf("no response from upstream")
http.Error(w, err.Error(), http.StatusRequestTimeout)
return
}
tr.Answer(fromUp)
packed, err := fromUp.Pack()
if err != nil {
err = tr.Errorf("cannot pack reply: %v", err)
http.Error(w, err.Error(), http.StatusFailedDependency)
return
}
// Write the response back.
w.Header().Set("Content-type", "application/dns-message")
// TODO: set cache-control based on the response.
w.WriteHeader(http.StatusOK)
w.Write(packed)
}
func exchange(tr *trace.Trace, r *dns.Msg, addr string) (*dns.Msg, error) {
reply, err := dns.Exchange(r, addr)
if err == nil && !reply.Truncated {
tr.Printf("UDP exchange successful")
return reply, err
}
// If we had issues over UDP, or the message was truncated, retry over
// TCP. We don't try beyond that.
if err != nil {
tr.Printf("error on UDP exchange: %v", err)
} else if reply.Truncated {
tr.Printf("UDP exchange returned truncated reply: %v", reply.MsgHdr)
}
tr.Printf("retrying on TCP")
c := &dns.Client{
Net: "tcp",
}
reply, _, err = c.Exchange(r, addr)
return reply, err
}
|