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 <sonia@tailscale.com>
pull/9019/head
Sonia Appasamy 1 year ago committed by Sonia Appasamy
parent 3b7ebeba2e
commit f3077c6ab5

@ -20,6 +20,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
"tailscale.com/client/tailscale" "tailscale.com/client/tailscale"
@ -56,6 +57,18 @@ type Server struct {
cgiMode bool cgiMode bool
cgiPath string cgiPath string
apiHandler http.Handler // csrf-protected api handler 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. // ServerOpts contains options for constructing a new Server.
@ -74,7 +87,8 @@ type ServerOpts struct {
} }
// NewServer constructs a new Tailscale web client server. // 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 { if opts.LocalClient == nil {
opts.LocalClient = &tailscale.LocalClient{} opts.LocalClient = &tailscale.LocalClient{}
} }
@ -97,6 +111,15 @@ func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false)) csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
s.apiHandler = csrfProtect(&api{s: s}) 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) s.lc.IncrementCounter(context.Background(), "web_client_initialization", 1)
return s, cleanup return s, cleanup
} }
@ -105,6 +128,58 @@ func init() {
tmpls = template.Must(template.New("").ParseFS(embeddedFS, "*")) 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. // ServeHTTP processes all requests for the Tailscale web client.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// some platforms where the client runs have their own authentication // some platforms where the client runs have their own authentication

@ -78,7 +78,7 @@ func runWeb(ctx context.Context, args []string) error {
return fmt.Errorf("too many non-flag arguments: %q", args) 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, DevMode: webArgs.dev,
CGIMode: webArgs.cgi, CGIMode: webArgs.cgi,
LocalClient: &localClient, LocalClient: &localClient,

@ -5,6 +5,7 @@
package main package main
import ( import (
"context"
"flag" "flag"
"log" "log"
"net/http" "net/http"
@ -20,6 +21,7 @@ var (
func main() { func main() {
flag.Parse() flag.Parse()
ctx := context.Background()
s := new(tsnet.Server) s := new(tsnet.Server)
defer s.Close() defer s.Close()
@ -30,7 +32,7 @@ func main() {
} }
// Serve the Tailscale web client. // Serve the Tailscale web client.
ws, cleanup := web.NewServer(web.ServerOpts{ ws, cleanup := web.NewServer(ctx, web.ServerOpts{
DevMode: *devMode, DevMode: *devMode,
LocalClient: lc, LocalClient: lc,
}) })

Loading…
Cancel
Save