Преглед изворни кода

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

Updates #1713

Change-Id: I8d9c488f779e7acc811a9bc18166a2726198a429
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick пре 4 година
родитељ
комит
283ae702c1
6 измењених фајлова са 122 додато и 0 уклоњено
  1. 3 0
      cmd/tailscaled/tailscaled.go
  2. 27 0
      ipn/ipnlocal/local.go
  3. 68 0
      ipn/ipnlocal/peerapi.go
  4. 3 0
      net/dns/manager.go
  5. 14 0
      wgengine/userspace.go
  6. 7 0
      wgengine/watchdog.go

+ 3 - 0
cmd/tailscaled/tailscaled.go

@@ -310,6 +310,9 @@ func run() error {
 		logf("wgengine.New: %v", err)
 		return err
 	}
+	if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
+		panic("internal error: exit node resolver not wired up")
+	}
 
 	ns, err := newNetstack(logf, e)
 	if err != nil {

+ 27 - 0
ipn/ipnlocal/local.go

@@ -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
+}

+ 68 - 0
ipn/ipnlocal/peerapi.go

@@ -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, ""
+	}
+}

+ 3 - 0
net/dns/manager.go

@@ -50,6 +50,9 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, li
 	return m
 }
 
+// Resolver returns the Manager's DNS Resolver.
+func (m *Manager) Resolver() *resolver.Resolver { return m.resolver }
+
 func (m *Manager) Set(cfg Config) error {
 	m.logf("Set: %v", logger.ArgWriter(func(w *bufio.Writer) {
 		cfg.WriteToBufioWriter(w)

+ 14 - 0
wgengine/userspace.go

@@ -147,6 +147,20 @@ func (e *userspaceEngine) GetInternals() (_ *tstun.Wrapper, _ *magicsock.Conn, o
 	return e.tundev, e.magicConn, true
 }
 
+// ResolvingEngine is implemented by Engines that have DNS resolvers.
+type ResolvingEngine interface {
+	GetResolver() (_ *resolver.Resolver, ok bool)
+}
+
+var (
+	_ ResolvingEngine = (*userspaceEngine)(nil)
+	_ ResolvingEngine = (*watchdogEngine)(nil)
+)
+
+func (e *userspaceEngine) GetResolver() (r *resolver.Resolver, ok bool) {
+	return e.dns.Resolver(), true
+}
+
 // BIRDClient handles communication with the BIRD Internet Routing Daemon.
 type BIRDClient interface {
 	EnableProtocol(proto string) error

+ 7 - 0
wgengine/watchdog.go

@@ -15,6 +15,7 @@ import (
 	"inet.af/netaddr"
 	"tailscale.com/ipn/ipnstate"
 	"tailscale.com/net/dns"
+	"tailscale.com/net/dns/resolver"
 	"tailscale.com/net/tstun"
 	"tailscale.com/tailcfg"
 	"tailscale.com/types/key"
@@ -139,6 +140,12 @@ func (e *watchdogEngine) GetInternals() (tw *tstun.Wrapper, c *magicsock.Conn, o
 	}
 	return
 }
+func (e *watchdogEngine) GetResolver() (r *resolver.Resolver, ok bool) {
+	if re, ok := e.wrap.(ResolvingEngine); ok {
+		return re.GetResolver()
+	}
+	return nil, false
+}
 func (e *watchdogEngine) Wait() {
 	e.wrap.Wait()
 }