From f3077c6ab5f4fb7bb82415834f4f260c2cc05970 Mon Sep 17 00:00:00 2001 From: Sonia Appasamy Date: Thu, 24 Aug 2023 16:24:57 -0400 Subject: [PATCH] client/web: add self node cache Adds a cached self node to the web client Server struct, which will be used from the web client api to verify that request came from the node's own machine (i.e. came from the web client frontend). We'll be using when we switch the web client api over to acting as a proxy to the localapi, to protect against DNS rebinding attacks. Updates tailscale/corp#13775 Signed-off-by: Sonia Appasamy --- client/web/web.go | 77 +++++++++++++++++++++++++- cmd/tailscale/cli/web.go | 2 +- tsnet/example/web-client/web-client.go | 4 +- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/client/web/web.go b/client/web/web.go index 2a2313a67..816890e2f 100644 --- a/client/web/web.go +++ b/client/web/web.go @@ -20,6 +20,7 @@ import ( "os" "path/filepath" "strings" + "sync" "github.com/gorilla/csrf" "tailscale.com/client/tailscale" @@ -56,6 +57,18 @@ type Server struct { cgiMode bool cgiPath string apiHandler http.Handler // csrf-protected api handler + + selfMu sync.Mutex // protects self field + // self is a cached NodeView of the active self node, + // refreshed by watching the IPN notification bus + // (see Server.watchSelf). + // + // self's hostname and Tailscale IP are used to verify + // that incoming requests to the web client api are coming + // from the web client frontend and not some other source. + // Particularly to protect against DNS rebinding attacks. + // self should not be used to fill data for frontend views. + self tailcfg.NodeView } // ServerOpts contains options for constructing a new Server. @@ -74,7 +87,8 @@ type ServerOpts struct { } // NewServer constructs a new Tailscale web client server. -func NewServer(opts ServerOpts) (s *Server, cleanup func()) { +// The provided context should live for the duration of the Server's lifetime. +func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func()) { if opts.LocalClient == nil { opts.LocalClient = &tailscale.LocalClient{} } @@ -97,6 +111,15 @@ func NewServer(opts ServerOpts) (s *Server, cleanup func()) { csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false)) s.apiHandler = csrfProtect(&api{s: s}) } + + var wg sync.WaitGroup + defer wg.Wait() + wg.Add(1) + go func() { + defer wg.Done() + go s.watchSelf(ctx) + }() + s.lc.IncrementCounter(context.Background(), "web_client_initialization", 1) return s, cleanup } @@ -105,6 +128,58 @@ func init() { tmpls = template.Must(template.New("").ParseFS(embeddedFS, "*")) } +// watchSelf watches the IPN notification bus to refresh +// the Server's self node cache. +func (s *Server) watchSelf(ctx context.Context) { + watchCtx, cancelWatch := context.WithCancel(ctx) + defer cancelWatch() + + watcher, err := s.lc.WatchIPNBus(watchCtx, ipn.NotifyInitialNetMap|ipn.NotifyNoPrivateKeys) + if err != nil { + log.Fatalf("lost connection to tailscaled: %v", err) + } + defer watcher.Close() + + for { + n, err := watcher.Next() + if err != nil { + log.Fatalf("lost connection to tailscaled: %v", err) + } + if state := n.State; state != nil && *state == ipn.NeedsLogin { + s.updateSelf(tailcfg.NodeView{}) + continue + } + if n.NetMap == nil { + continue + } + s.updateSelf(n.NetMap.SelfNode) + } +} + +// updateSelf grabs the lock and updates s.self. +// Then logs if anything changed. +func (s *Server) updateSelf(self tailcfg.NodeView) { + s.selfMu.Lock() + prev := s.self + s.self = self + s.selfMu.Unlock() + + var old, new tailcfg.StableNodeID + if prev.Valid() { + old = prev.StableID() + } + if s.self.Valid() { + new = s.self.StableID() + } + if old != new { + if new.IsZero() { + log.Printf("self node logout") + } else { + log.Printf("self node login") + } + } +} + // ServeHTTP processes all requests for the Tailscale web client. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // some platforms where the client runs have their own authentication diff --git a/cmd/tailscale/cli/web.go b/cmd/tailscale/cli/web.go index d3daaa6c5..2758ba695 100644 --- a/cmd/tailscale/cli/web.go +++ b/cmd/tailscale/cli/web.go @@ -78,7 +78,7 @@ func runWeb(ctx context.Context, args []string) error { return fmt.Errorf("too many non-flag arguments: %q", args) } - webServer, cleanup := web.NewServer(web.ServerOpts{ + webServer, cleanup := web.NewServer(ctx, web.ServerOpts{ DevMode: webArgs.dev, CGIMode: webArgs.cgi, LocalClient: &localClient, diff --git a/tsnet/example/web-client/web-client.go b/tsnet/example/web-client/web-client.go index 903088f23..52a0adff0 100644 --- a/tsnet/example/web-client/web-client.go +++ b/tsnet/example/web-client/web-client.go @@ -5,6 +5,7 @@ package main import ( + "context" "flag" "log" "net/http" @@ -20,6 +21,7 @@ var ( func main() { flag.Parse() + ctx := context.Background() s := new(tsnet.Server) defer s.Close() @@ -30,7 +32,7 @@ func main() { } // Serve the Tailscale web client. - ws, cleanup := web.NewServer(web.ServerOpts{ + ws, cleanup := web.NewServer(ctx, web.ServerOpts{ DevMode: *devMode, LocalClient: lc, })