mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
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>
This commit is contained in:
parent
3b7ebeba2e
commit
f3077c6ab5
@ -20,6 +20,7 @@
|
||||
"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
|
||||
|
@ -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,
|
||||
|
@ -5,6 +5,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -20,6 +21,7 @@
|
||||
|
||||
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,
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user