git » dnss » next » tree

[next] / internal / httpserver / server.go

// 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"
	"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() {
	srv := http.Server{
		Addr:    s.Addr,
		Handler: s.newMux(),
	}

	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)
}

func (s *Server) newMux() *http.ServeMux {
	mux := http.NewServeMux()
	for _, path := range []string{"/dns-query", "/resolve"} {
		mux.HandleFunc("GET "+path, s.resolveGET)
		mux.HandleFunc("POST "+path, s.resolvePOST)
	}
	return mux
}

func newTrace(req *http.Request) *trace.Trace {
	tr := trace.New("httpserver", "/resolve")
	tr.Printf("from:%v", req.RemoteAddr)
	tr.Printf("method:%v", req.Method)
	return tr
}

// Resolve incoming DoH GET requests, which have a "dns=" query parameter.
func (s *Server) resolveGET(w http.ResponseWriter, req *http.Request) {
	tr := newTrace(req)
	defer tr.Finish()

	dnsParam := req.URL.Query().Get("dns")
	if dnsParam == "" {
		tr.Errorf("unknown request type")
		http.Error(w, "unknown request type", http.StatusUnsupportedMediaType)
		return
	}

	tr.Printf("DoH:GET")
	dnsQuery, err := base64.RawURLEncoding.DecodeString(dnsParam)
	if err != nil {
		tr.Error(err)
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	s.resolveDoH(tr, w, dnsQuery)
}

// Resolve incoming DoH POST requests, which have a content-type of
// application/dns-message.
func (s *Server) resolvePOST(w http.ResponseWriter, req *http.Request) {
	tr := newTrace(req)
	defer tr.Finish()

	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.Errorf("unknown request type")
		http.Error(w, "unknown request type", http.StatusUnsupportedMediaType)
		return
	}

	tr.Printf("DoH:POST")
	// Limit the size of request to 4k.
	dnsQuery, err := io.ReadAll(http.MaxBytesReader(w, req.Body, 4096))
	if err != nil {
		tr.Error(err)
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	s.resolveDoH(tr, w, dnsQuery)
}

// 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)

	// Compress the reply. Messages are unpacked without compression, and
	// packing them back as-is can make them much larger, even beyond the
	// DoH limit of 64 KiB.
	fromUp.Compress = true

	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
}