ipn/ipnlocal: start adding DoH DNS server to peerapi when exit node

Updates #1713

Change-Id: I8d9c488f779e7acc811a9bc18166a2726198a429
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2021-11-22 21:45:34 -08:00
committed by Brad Fitzpatrick
parent 6fd6fe11f2
commit 283ae702c1
6 changed files with 122 additions and 0 deletions

View File

@@ -2142,6 +2142,11 @@ func (b *LocalBackend) initPeerAPIListener() {
selfNode: selfNode,
directFileMode: b.directFileRoot != "",
}
if re, ok := b.e.(wgengine.ResolvingEngine); ok {
if r, ok := re.GetResolver(); ok {
ps.resolver = r
}
}
b.peerAPIServer = ps
isNetstack := wgengine.IsNetstack(b.e)
@@ -2947,3 +2952,25 @@ func (b *LocalBackend) DERPMap() *tailcfg.DERPMap {
}
return b.netMap.DERPMap
}
// OfferingExitNode reports whether b is currently offering exit node
// access.
func (b *LocalBackend) OfferingExitNode() bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil {
return false
}
var def4, def6 bool
for _, r := range b.prefs.AdvertiseRoutes {
if r.Bits() != 0 {
continue
}
if r.IP().Is4() {
def4 = true
} else if r.IP().Is6() {
def6 = true
}
}
return def4 && def6
}

View File

@@ -6,6 +6,7 @@ package ipnlocal
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -33,6 +34,7 @@ import (
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/interfaces"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
@@ -48,6 +50,7 @@ type peerAPIServer struct {
tunName string
selfNode *tailcfg.Node
knownEmpty syncs.AtomicBool
resolver *resolver.Resolver
// directFileMode is whether we're writing files directly to a
// download directory (as *.partial files), rather than making
@@ -503,6 +506,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handlePeerPut(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/dns-query") {
h.handleDNSQuery(w, r)
return
}
switch r.URL.Path {
case "/v0/goroutines":
h.handleServeGoroutines(w, r)
@@ -749,3 +756,64 @@ func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Reque
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
}
func (h *peerAPIHandler) replyToDNSQueries() bool {
// TODO(bradfitz): maybe lock this down more? what if we're an
// exit node but ACLs don't permit autogroup:internet access
// from h.peerNode via this node? peerapi bypasses ACL checks,
// so we should do additional checks here; but on what? this
// node's UDP port 53? our upstream DNS forwarder IP(s)?
// For now just offer DNS to any peer if we're an exit node.
return h.isSelf || h.ps.b.OfferingExitNode()
}
func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request) {
if h.ps.resolver == nil {
http.Error(w, "DNS not wired up", http.StatusNotImplemented)
return
}
if !h.replyToDNSQueries() {
http.Error(w, "DNS access denied", http.StatusForbidden)
return
}
q, publicError := dohQuery(r)
if publicError != "" {
http.Error(w, publicError, http.StatusBadRequest)
return
}
// TODO(bradfitz): owl.
fmt.Fprintf(w, "## TODO: got %d bytes of DNS query", len(q))
}
func dohQuery(r *http.Request) (dnsQuery []byte, publicErr string) {
const maxQueryLen = 256 << 10
switch r.Method {
default:
return nil, "bad HTTP method"
case "GET":
q64 := r.FormValue("dns")
if q64 == "" {
return nil, "missing 'dns' parameter"
}
if base64.RawURLEncoding.DecodedLen(len(q64)) > maxQueryLen {
return nil, "query too large"
}
q, err := base64.RawURLEncoding.DecodeString(q64)
if err != nil {
return nil, "invalid 'dns' base64 encoding"
}
return q, ""
case "POST":
if r.Header.Get("Content-Type") != "application/dns-message" {
return nil, "unexpected Content-Type"
}
q, err := io.ReadAll(io.LimitReader(r.Body, maxQueryLen+1))
if err != nil {
return nil, "error reading post body with DNS query"
}
if len(q) > maxQueryLen {
return nil, "query too large"
}
return q, ""
}
}