From 283ae702c16c8da2033b0d23bab35260378abaf8 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 22 Nov 2021 21:45:34 -0800 Subject: [PATCH] ipn/ipnlocal: start adding DoH DNS server to peerapi when exit node Updates #1713 Change-Id: I8d9c488f779e7acc811a9bc18166a2726198a429 Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/tailscaled.go | 3 ++ ipn/ipnlocal/local.go | 27 ++++++++++++++ ipn/ipnlocal/peerapi.go | 68 ++++++++++++++++++++++++++++++++++++ net/dns/manager.go | 3 ++ wgengine/userspace.go | 14 ++++++++ wgengine/watchdog.go | 7 ++++ 6 files changed, 122 insertions(+) diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index bc4510b93..26917388a 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/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 { diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 8f766bdbb..a9a96286a 100644 --- a/ipn/ipnlocal/local.go +++ b/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 +} diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index c7641122b..d2b24e5c0 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/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, "" + } +} diff --git a/net/dns/manager.go b/net/dns/manager.go index 86b7e5301..6fd6d8f3a 100644 --- a/net/dns/manager.go +++ b/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) diff --git a/wgengine/userspace.go b/wgengine/userspace.go index ce52a9337..2071b3757 100644 --- a/wgengine/userspace.go +++ b/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 diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index 2385148c8..dec2858de 100644 --- a/wgengine/watchdog.go +++ b/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() }