|
|
|
@ -29,6 +29,7 @@ import (
|
|
|
|
|
"unicode"
|
|
|
|
|
"unicode/utf8"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/net/dns/dnsmessage"
|
|
|
|
|
"inet.af/netaddr"
|
|
|
|
|
"tailscale.com/client/tailscale/apitype"
|
|
|
|
|
"tailscale.com/hostinfo"
|
|
|
|
@ -767,6 +768,8 @@ func (h *peerAPIHandler) replyToDNSQueries() bool {
|
|
|
|
|
return h.isSelf || h.ps.b.OfferingExitNode()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleDNSQuery implements a DoH server (RFC 8484) over the peerapi.
|
|
|
|
|
// It's not over HTTPS as the spec dictates, but rather HTTP-over-WireGuard.
|
|
|
|
|
func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if h.ps.resolver == nil {
|
|
|
|
|
http.Error(w, "DNS not wired up", http.StatusNotImplemented)
|
|
|
|
@ -776,13 +779,45 @@ func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request)
|
|
|
|
|
http.Error(w, "DNS access denied", http.StatusForbidden)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
pretty := false // non-DoH debug mode for humans
|
|
|
|
|
q, publicError := dohQuery(r)
|
|
|
|
|
if publicError != "" && r.Method == "GET" {
|
|
|
|
|
if name := r.FormValue("q"); name != "" {
|
|
|
|
|
pretty = true
|
|
|
|
|
publicError = ""
|
|
|
|
|
q = dnsQueryForName(name, r.FormValue("t"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if publicError != "" {
|
|
|
|
|
http.Error(w, publicError, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// TODO(bradfitz): owl.
|
|
|
|
|
fmt.Fprintf(w, "## TODO: got %d bytes of DNS query", len(q))
|
|
|
|
|
|
|
|
|
|
// Some timeout that's short enough to be noticed by humans
|
|
|
|
|
// but long enough that it's longer than real DNS timeouts.
|
|
|
|
|
const arbitraryTimeout = 5 * time.Second
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), arbitraryTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
res, err := h.ps.resolver.HandleExitNodeDNSQuery(ctx, q, h.remoteAddr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.logf("handleDNS fwd error: %v", err)
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
|
} else {
|
|
|
|
|
http.Error(w, "DNS forwarding error", 500)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if pretty {
|
|
|
|
|
// Non-standard response for interactive debugging.
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
writePrettyDNSReply(w, res)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "application/dns-message")
|
|
|
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(q)))
|
|
|
|
|
w.Write(res)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func dohQuery(r *http.Request) (dnsQuery []byte, publicErr string) {
|
|
|
|
@ -817,3 +852,86 @@ func dohQuery(r *http.Request) (dnsQuery []byte, publicErr string) {
|
|
|
|
|
return q, ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func dnsQueryForName(name, typStr string) []byte {
|
|
|
|
|
typ := dnsmessage.TypeA
|
|
|
|
|
switch strings.ToLower(typStr) {
|
|
|
|
|
case "aaaa":
|
|
|
|
|
typ = dnsmessage.TypeAAAA
|
|
|
|
|
case "txt":
|
|
|
|
|
typ = dnsmessage.TypeTXT
|
|
|
|
|
}
|
|
|
|
|
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{
|
|
|
|
|
OpCode: 0, // query
|
|
|
|
|
RecursionDesired: true,
|
|
|
|
|
ID: 0,
|
|
|
|
|
})
|
|
|
|
|
if !strings.HasSuffix(name, ".") {
|
|
|
|
|
name += "."
|
|
|
|
|
}
|
|
|
|
|
b.StartQuestions()
|
|
|
|
|
b.Question(dnsmessage.Question{
|
|
|
|
|
Name: dnsmessage.MustNewName(name),
|
|
|
|
|
Type: typ,
|
|
|
|
|
Class: dnsmessage.ClassINET,
|
|
|
|
|
})
|
|
|
|
|
msg, _ := b.Finish()
|
|
|
|
|
return msg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
|
|
|
|
|
defer func() {
|
|
|
|
|
if err != nil {
|
|
|
|
|
j, _ := json.Marshal(struct {
|
|
|
|
|
Error string
|
|
|
|
|
}{err.Error()})
|
|
|
|
|
w.Write(j)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
var p dnsmessage.Parser
|
|
|
|
|
if _, err := p.Start(res); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := p.SkipAllQuestions(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var gotIPs []string
|
|
|
|
|
for {
|
|
|
|
|
h, err := p.AnswerHeader()
|
|
|
|
|
if err == dnsmessage.ErrSectionDone {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if h.Class != dnsmessage.ClassINET {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
switch h.Type {
|
|
|
|
|
case dnsmessage.TypeA:
|
|
|
|
|
r, err := p.AResource()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
gotIPs = append(gotIPs, net.IP(r.A[:]).String())
|
|
|
|
|
case dnsmessage.TypeAAAA:
|
|
|
|
|
r, err := p.AAAAResource()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
gotIPs = append(gotIPs, net.IP(r.AAAA[:]).String())
|
|
|
|
|
case dnsmessage.TypeTXT:
|
|
|
|
|
r, err := p.TXTResource()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
gotIPs = append(gotIPs, r.TXT...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
j, _ := json.Marshal(gotIPs)
|
|
|
|
|
j = append(j, '\n')
|
|
|
|
|
w.Write(j)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|