ソースを参照

ipn/ipnlocal,client/web: add web client to tailscaled

Allows for serving the web interface from tailscaled, with the
ability to start and stop the server via localapi endpoints
(/web/start and /web/stop).

This will be used to run the new full management web client,
which will only be accessible over Tailscale (with an extra auth
check step over noise) from the daemon. This switch also allows
us to run the web interface as a long-lived service in environments
where the CLI version is restricted to CGI, allowing us to manage
certain auth state in memory.

ipn/ipnlocal/web is stubbed out in ipn/ipnlocal/web_stub for
ios builds to satisfy ios restriction from adding "text/template"
and "html/template" dependencies.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <[email protected]>
Sonia Appasamy 2 年 前
コミット
89953b015b

+ 17 - 6
client/web/web.go

@@ -49,8 +49,9 @@ type Server struct {
 	cgiMode    bool
 	pathPrefix string
 
-	assetsHandler http.Handler // serves frontend assets
 	apiHandler    http.Handler // serves api endpoints; csrf-protected
+	assetsHandler http.Handler // serves frontend assets
+	assetsCleanup func()       // called from Server.Shutdown
 
 	// browserSessions is an in-memory cache of browser sessions for the
 	// full management web client, which is only accessible over Tailscale.
@@ -143,7 +144,10 @@ type ServerOpts struct {
 }
 
 // NewServer constructs a new Tailscale web client server.
-func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
+// If err is empty, s is always non-nil.
+// ctx is only required to live the duration of the NewServer call,
+// and not the lifespan of the web server.
+func NewServer(opts ServerOpts) (s *Server, err error) {
 	if opts.LocalClient == nil {
 		opts.LocalClient = &tailscale.LocalClient{}
 	}
@@ -162,7 +166,7 @@ func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
 		s.logf = log.Printf
 	}
 	s.tsDebugMode = s.debugMode()
-	s.assetsHandler, cleanup = assetsHandler(opts.DevMode)
+	s.assetsHandler, s.assetsCleanup = assetsHandler(opts.DevMode)
 
 	var metric string // clientmetric to report on startup
 
@@ -182,14 +186,21 @@ func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
 		metric = "web_client_initialization"
 	}
 
-	// Report metric in separate go routine with 5 second timeout.
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	// Don't block startup on reporting metric.
+	// Report in separate go routine with 5 second timeout.
 	go func() {
+		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 		defer cancel()
 		s.lc.IncrementCounter(ctx, metric, 1)
 	}()
 
-	return s, cleanup
+	return s, nil
+}
+
+func (s *Server) Shutdown() {
+	if s.assetsCleanup != nil {
+		s.assetsCleanup()
+	}
 }
 
 // debugMode returns the debug mode the web client is being run in.

+ 6 - 2
cmd/tailscale/cli/web.go

@@ -80,13 +80,17 @@ 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, err := web.NewServer(web.ServerOpts{
 		DevMode:     webArgs.dev,
 		CGIMode:     webArgs.cgi,
 		PathPrefix:  webArgs.prefix,
 		LocalClient: &localClient,
 	})
-	defer cleanup()
+	if err != nil {
+		log.Printf("tailscale.web: %v", err)
+		return err
+	}
+	defer webServer.Shutdown()
 
 	if webArgs.cgi {
 		if err := cgi.Serve(webServer); err != nil {

+ 12 - 2
cmd/tailscaled/depaware.txt

@@ -95,6 +95,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
    L    github.com/google/nftables/internal/parseexprfunc            from github.com/google/nftables+
    L    github.com/google/nftables/xt                                from github.com/google/nftables/expr+
         github.com/google/uuid                                       from tailscale.com/clientupdate
+        github.com/gorilla/csrf                                      from tailscale.com/client/web
+        github.com/gorilla/securecookie                              from github.com/gorilla/csrf
         github.com/hdevalence/ed25519consensus                       from tailscale.com/tka+
    L 💣 github.com/illarion/gonotify                                 from tailscale.com/net/dns
    L    github.com/insomniacslk/dhcp/dhcpv4                          from tailscale.com/net/tstun
@@ -128,6 +130,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
    L    github.com/pierrec/lz4/v4/internal/lz4errors                 from github.com/pierrec/lz4/v4+
    L    github.com/pierrec/lz4/v4/internal/lz4stream                 from github.com/pierrec/lz4/v4
    L    github.com/pierrec/lz4/v4/internal/xxh32                     from github.com/pierrec/lz4/v4/internal/lz4stream
+        github.com/pkg/errors                                        from github.com/gorilla/csrf
   LD    github.com/pkg/sftp                                          from tailscale.com/ssh/tailssh
   LD    github.com/pkg/sftp/internal/encoding/ssh/filexfer           from github.com/pkg/sftp
    W 💣 github.com/tailscale/certstore                               from tailscale.com/control/controlclient
@@ -149,6 +152,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         github.com/tailscale/goupnp/ssdp                             from github.com/tailscale/goupnp
         github.com/tailscale/hujson                                  from tailscale.com/ipn/conffile
    L 💣 github.com/tailscale/netlink                                 from tailscale.com/wgengine/router+
+        github.com/tailscale/web-client-prebuilt                     from tailscale.com/client/web
      💣 github.com/tailscale/wireguard-go/conn                       from github.com/tailscale/wireguard-go/device+
    W 💣 github.com/tailscale/wireguard-go/conn/winrio                from github.com/tailscale/wireguard-go/conn
      💣 github.com/tailscale/wireguard-go/device                     from tailscale.com/net/tstun+
@@ -219,8 +223,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         tailscale.com                                                from tailscale.com/version
         tailscale.com/atomicfile                                     from tailscale.com/ipn+
   LD    tailscale.com/chirp                                          from tailscale.com/cmd/tailscaled
-        tailscale.com/client/tailscale                               from tailscale.com/derp
+        tailscale.com/client/tailscale                               from tailscale.com/derp+
         tailscale.com/client/tailscale/apitype                       from tailscale.com/ipn/ipnlocal+
+        tailscale.com/client/web                                     from tailscale.com/ipn/ipnlocal
         tailscale.com/clientupdate                                   from tailscale.com/ipn/ipnlocal
         tailscale.com/clientupdate/distsign                          from tailscale.com/clientupdate
         tailscale.com/cmd/tailscaled/childproc                       from tailscale.com/ssh/tailssh+
@@ -251,6 +256,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
    L    tailscale.com/ipn/store/kubestore                            from tailscale.com/ipn/store
         tailscale.com/ipn/store/mem                                  from tailscale.com/ipn/store+
    L    tailscale.com/kube                                           from tailscale.com/ipn/store/kubestore
+        tailscale.com/licenses                                       from tailscale.com/client/web
         tailscale.com/log/filelogger                                 from tailscale.com/logpolicy
         tailscale.com/log/sockstatlog                                from tailscale.com/ipn/ipnlocal
         tailscale.com/logpolicy                                      from tailscale.com/cmd/tailscaled+
@@ -339,7 +345,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
    L 💣 tailscale.com/util/dirwalk                                   from tailscale.com/metrics+
         tailscale.com/util/dnsname                                   from tailscale.com/hostinfo+
         tailscale.com/util/goroutines                                from tailscale.com/ipn/ipnlocal
-        tailscale.com/util/groupmember                               from tailscale.com/ipn/ipnauth
+        tailscale.com/util/groupmember                               from tailscale.com/ipn/ipnauth+
      💣 tailscale.com/util/hashx                                     from tailscale.com/util/deephash
         tailscale.com/util/httphdr                                   from tailscale.com/ipn/ipnlocal+
         tailscale.com/util/httpm                                     from tailscale.com/client/tailscale+
@@ -468,6 +474,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         encoding/base32                                              from tailscale.com/tka+
         encoding/base64                                              from encoding/json+
         encoding/binary                                              from compress/gzip+
+        encoding/gob                                                 from github.com/gorilla/securecookie
         encoding/hex                                                 from crypto/x509+
         encoding/json                                                from expvar+
         encoding/pem                                                 from crypto/tls+
@@ -482,6 +489,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         hash/fnv                                                     from tailscale.com/wgengine/magicsock+
         hash/maphash                                                 from go4.org/mem
         html                                                         from tailscale.com/ipn/ipnlocal+
+        html/template                                                from github.com/gorilla/csrf
         io                                                           from bufio+
         io/fs                                                        from crypto/x509+
         io/ioutil                                                    from github.com/godbus/dbus/v5+
@@ -526,6 +534,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         sync/atomic                                                  from context+
         syscall                                                      from crypto/rand+
         text/tabwriter                                               from runtime/pprof
+        text/template                                                from html/template
+        text/template/parse                                          from html/template+
         time                                                         from compress/gzip+
         unicode                                                      from bytes+
         unicode/utf16                                                from crypto/x509+

+ 4 - 0
cmd/tailscaled/tailscaled.go

@@ -29,6 +29,7 @@ import (
 	"syscall"
 	"time"
 
+	"tailscale.com/client/tailscale"
 	"tailscale.com/cmd/tailscaled/childproc"
 	"tailscale.com/control/controlclient"
 	"tailscale.com/envknob"
@@ -569,6 +570,9 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
 	if root := lb.TailscaleVarRoot(); root != "" {
 		dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
 	}
+	if envknob.Bool("TS_DEBUG_WEB_UI") {
+		lb.SetWebLocalClient(&tailscale.LocalClient{Socket: args.socketpath, UseSocketOnly: args.socketpath != ""})
+	}
 	configureTaildrop(logf, lb)
 	if err := ns.Start(lb); err != nil {
 		log.Fatalf("failed to start netstack: %v", err)

+ 2 - 0
ipn/ipnlocal/local.go

@@ -205,6 +205,7 @@ type LocalBackend struct {
 	httpTestClient *http.Client // for controlclient. nil by default, used by tests.
 	ccGen          clientGen    // function for producing controlclient; lazily populated
 	sshServer      SSHServer    // or nil, initialized lazily.
+	web            webServer
 	notify         func(ipn.Notify)
 	cc             controlclient.Client
 	ccAuto         *controlclient.Auto // if cc is of type *controlclient.Auto
@@ -643,6 +644,7 @@ func (b *LocalBackend) Shutdown() {
 		b.debugSink = nil
 	}
 	b.mu.Unlock()
+	b.WebShutdown()
 
 	if b.sockstatLogger != nil {
 		b.sockstatLogger.Shutdown()

+ 110 - 0
ipn/ipnlocal/web.go

@@ -0,0 +1,110 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !ios && !android
+
+package ipnlocal
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"sync"
+
+	"tailscale.com/client/tailscale"
+	"tailscale.com/client/web"
+	"tailscale.com/envknob"
+)
+
+// webServer holds state for the web interface for managing
+// this tailscale instance. The web interface is not used by
+// default, but initialized by calling LocalBackend.WebOrInit.
+type webServer struct {
+	ws         *web.Server  // or nil, initialized lazily
+	httpServer *http.Server // or nil, initialized lazily
+
+	// lc optionally specifies a LocalClient to use to connect
+	// to the localapi for this tailscaled instance.
+	// If nil, a default is used.
+	lc *tailscale.LocalClient
+
+	wg sync.WaitGroup
+}
+
+// SetWebLocalClient sets the b.web.lc function.
+// If lc is provided as nil, b.web.lc is cleared out.
+func (b *LocalBackend) SetWebLocalClient(lc *tailscale.LocalClient) {
+	b.mu.Lock()
+	defer b.mu.Unlock()
+	b.web.lc = lc
+}
+
+// WebInit initializes the web interface for managing
+// this tailscaled instance. If the web interface is
+// already running, WebInit is a no-op.
+func (b *LocalBackend) WebInit() (err error) {
+	if !envknob.Bool("TS_DEBUG_WEB_UI") {
+		return errors.New("web ui flag unset")
+	}
+
+	b.mu.Lock()
+	defer b.mu.Unlock()
+	if b.web.ws != nil {
+		return nil
+	}
+
+	b.logf("WebInit: initializing web ui")
+	if b.web.ws, err = web.NewServer(web.ServerOpts{
+		// TODO(sonia): allow passing back dev mode flag
+		LocalClient: b.web.lc,
+		Logf:        b.logf,
+	}); err != nil {
+		return fmt.Errorf("web.NewServer: %w", err)
+	}
+
+	// Start up the server.
+	b.web.wg.Add(1)
+	go func() {
+		defer b.web.wg.Done()
+		// TODO(sonia/will): only listen on Tailscale IP addresses
+		addr := ":5252"
+		b.web.httpServer = &http.Server{
+			Addr:    addr,
+			Handler: http.HandlerFunc(b.web.ws.ServeHTTP),
+		}
+		b.logf("WebInit: serving web ui on %s", addr)
+		if err := b.web.httpServer.ListenAndServe(); err != nil {
+			if err != http.ErrServerClosed {
+				b.logf("[unexpected] WebInit: %v", err)
+			}
+		}
+	}()
+
+	b.logf("WebInit: started web ui")
+	return nil
+}
+
+// WebShutdown shuts down any running b.web servers and
+// clears out b.web state (besides the b.web.lc field,
+// which is left untouched because required for future
+// web startups).
+// WebShutdown obtains the b.mu lock.
+func (b *LocalBackend) WebShutdown() {
+	b.mu.Lock()
+	webS := b.web.ws
+	httpS := b.web.httpServer
+	b.web.ws = nil
+	b.web.httpServer = nil
+	b.mu.Unlock() // release lock before shutdown
+	if webS != nil {
+		b.web.ws.Shutdown()
+	}
+	if httpS != nil {
+		if err := b.web.httpServer.Shutdown(context.Background()); err != nil {
+			b.logf("[unexpected] WebShutdown: %v", err)
+		}
+	}
+	b.web.wg.Wait()
+	b.logf("WebShutdown: shut down web ui")
+}

+ 22 - 0
ipn/ipnlocal/web_stub.go

@@ -0,0 +1,22 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build ios || android
+
+package ipnlocal
+
+import (
+	"errors"
+
+	"tailscale.com/client/tailscale"
+)
+
+type webServer struct{}
+
+func (b *LocalBackend) SetWebLocalClient(lc *tailscale.LocalClient) {}
+
+func (b *LocalBackend) WebInit() error {
+	return errors.New("not implemented")
+}
+
+func (b *LocalBackend) WebShutdown() {}

+ 28 - 0
ipn/localapi/localapi.go

@@ -66,6 +66,7 @@ var handler = map[string]localAPIHandler{
 	"file-put/": (*Handler).serveFilePut,
 	"files/":    (*Handler).serveFiles,
 	"profiles/": (*Handler).serveProfiles,
+	"web/":      (*Handler).serveWeb,
 
 	// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
 	// without a trailing slash:
@@ -2233,6 +2234,33 @@ func (h *Handler) serveDebugWebClient(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 }
 
+func (h *Handler) serveWeb(w http.ResponseWriter, r *http.Request) {
+	if !h.PermitWrite {
+		http.Error(w, "access denied", http.StatusForbidden)
+		return
+	}
+	if r.Method != httpm.POST {
+		http.Error(w, "use POST", http.StatusMethodNotAllowed)
+		return
+	}
+	switch r.URL.Path {
+	case "/localapi/v0/web/start":
+		if err := h.b.WebInit(); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		w.WriteHeader(http.StatusOK)
+		return
+	case "/localapi/v0/web/stop":
+		h.b.WebShutdown()
+		w.WriteHeader(http.StatusOK)
+		return
+	default:
+		http.Error(w, "invalid action", http.StatusBadRequest)
+		return
+	}
+}
+
 func defBool(a string, def bool) bool {
 	if a == "" {
 		return def

+ 5 - 2
tsnet/example/web-client/web-client.go

@@ -30,11 +30,14 @@ func main() {
 	}
 
 	// Serve the Tailscale web client.
-	ws, cleanup := web.NewServer(web.ServerOpts{
+	ws, err := web.NewServer(web.ServerOpts{
 		DevMode:     *devMode,
 		LocalClient: lc,
 	})
-	defer cleanup()
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer ws.Shutdown()
 	log.Printf("Serving Tailscale web client on http://%s", *addr)
 	if err := http.ListenAndServe(*addr, ws); err != nil {
 		if err != http.ErrServerClosed {

+ 1 - 0
tstest/integration/tailscaled_deps_test_darwin.go

@@ -11,6 +11,7 @@ import (
 	// transitive deps when we run "go install tailscaled" in a child
 	// process and can cache a prior success when a dependency changes.
 	_ "tailscale.com/chirp"
+	_ "tailscale.com/client/tailscale"
 	_ "tailscale.com/cmd/tailscaled/childproc"
 	_ "tailscale.com/control/controlclient"
 	_ "tailscale.com/derp/derphttp"

+ 1 - 0
tstest/integration/tailscaled_deps_test_freebsd.go

@@ -11,6 +11,7 @@ import (
 	// transitive deps when we run "go install tailscaled" in a child
 	// process and can cache a prior success when a dependency changes.
 	_ "tailscale.com/chirp"
+	_ "tailscale.com/client/tailscale"
 	_ "tailscale.com/cmd/tailscaled/childproc"
 	_ "tailscale.com/control/controlclient"
 	_ "tailscale.com/derp/derphttp"

+ 1 - 0
tstest/integration/tailscaled_deps_test_linux.go

@@ -11,6 +11,7 @@ import (
 	// transitive deps when we run "go install tailscaled" in a child
 	// process and can cache a prior success when a dependency changes.
 	_ "tailscale.com/chirp"
+	_ "tailscale.com/client/tailscale"
 	_ "tailscale.com/cmd/tailscaled/childproc"
 	_ "tailscale.com/control/controlclient"
 	_ "tailscale.com/derp/derphttp"

+ 1 - 0
tstest/integration/tailscaled_deps_test_openbsd.go

@@ -11,6 +11,7 @@ import (
 	// transitive deps when we run "go install tailscaled" in a child
 	// process and can cache a prior success when a dependency changes.
 	_ "tailscale.com/chirp"
+	_ "tailscale.com/client/tailscale"
 	_ "tailscale.com/cmd/tailscaled/childproc"
 	_ "tailscale.com/control/controlclient"
 	_ "tailscale.com/derp/derphttp"

+ 1 - 0
tstest/integration/tailscaled_deps_test_windows.go

@@ -18,6 +18,7 @@ import (
 	_ "golang.org/x/sys/windows/svc/mgr"
 	_ "golang.zx2c4.com/wintun"
 	_ "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
+	_ "tailscale.com/client/tailscale"
 	_ "tailscale.com/cmd/tailscaled/childproc"
 	_ "tailscale.com/control/controlclient"
 	_ "tailscale.com/derp/derphttp"