summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/web/web.go18
-rw-r--r--cmd/tailscale/cli/web.go8
-rw-r--r--cmd/tailscaled/depaware.txt15
-rw-r--r--ipn/ipnlocal/local.go2
-rw-r--r--ipn/ipnlocal/web.go126
-rw-r--r--ipn/localapi/localapi.go25
-rw-r--r--tsnet/example/web-client/web-client.go7
7 files changed, 191 insertions, 10 deletions
diff --git a/client/web/web.go b/client/web/web.go
index 43707821a..ffabe5f92 100644
--- a/client/web/web.go
+++ b/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
@@ -189,7 +193,13 @@ func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
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.
diff --git a/cmd/tailscale/cli/web.go b/cmd/tailscale/cli/web.go
index 7e5cfddf5..437cd85f3 100644
--- a/cmd/tailscale/cli/web.go
+++ b/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 {
diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt
index 3ae8c6bef..30d13e511 100644
--- a/cmd/tailscaled/depaware.txt
+++ b/cmd/tailscaled/depaware.txt
@@ -100,6 +100,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
@@ -133,6 +135,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+
@@ -268,6 +274,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+
tailscale.com/net/flowtrack from tailscale.com/net/packet+
💣 tailscale.com/net/interfaces from tailscale.com/control/controlclient+
+ tailscale.com/net/memnet from tailscale.com/ipn/ipnlocal
tailscale.com/net/netaddr from tailscale.com/ipn+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
@@ -339,7 +346,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+
@@ -469,6 +476,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+
@@ -483,6 +491,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+
@@ -527,6 +536,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+
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index ad36fd37b..bd80442b8 100644
--- a/ipn/ipnlocal/local.go
+++ b/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
@@ -635,6 +636,7 @@ func (b *LocalBackend) Shutdown() {
b.sshServer.Shutdown()
b.sshServer = nil
}
+ b.webShutdownLocked()
b.closePeerAPIListenersLocked()
if b.debugSink != nil {
b.e.InstallCaptureHook(nil)
diff --git a/ipn/ipnlocal/web.go b/ipn/ipnlocal/web.go
new file mode 100644
index 000000000..8ab9eefd7
--- /dev/null
+++ b/ipn/ipnlocal/web.go
@@ -0,0 +1,126 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package ipnlocal
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "net/http"
+ "sync"
+
+ "tailscale.com/client/tailscale"
+ "tailscale.com/client/web"
+ "tailscale.com/envknob"
+ "tailscale.com/net/memnet"
+)
+
+// 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
+
+ // webServer maintains its own localapi server and localclient connected to it
+ localAPIListener net.Listener // in-memory, used by lc
+ localAPIServer *http.Server
+ lc *tailscale.LocalClient
+
+ wg sync.WaitGroup
+}
+
+// WebOrInit gets or initializes the web interface for
+// managing this tailscaled instance.
+func (b *LocalBackend) WebOrInit(localapiHandler http.Handler) (_ *web.Server, err error) {
+ if !envknob.Bool("TS_DEBUG_WEB_UI") {
+ return nil, errors.New("web ui flag unset")
+ }
+
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ if b.web.ws != nil {
+ return b.web.ws, nil
+ }
+
+ lal := memnet.Listen("local-tailscaled.sock:80")
+ b.web.localAPIListener = lal
+ b.web.localAPIServer = &http.Server{Handler: localapiHandler}
+ b.web.lc = &tailscale.LocalClient{Dial: lal.Dial}
+
+ go func() {
+ if err := b.web.localAPIServer.Serve(lal); err != nil {
+ b.logf("localapi serve error: %v", err)
+ }
+ }()
+
+ b.logf("WebOrInit: 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 nil, fmt.Errorf("web.NewServer: %w", err)
+ }
+
+ // Start up the server.
+ b.web.wg.Add(1)
+ go func() {
+ defer b.web.wg.Done()
+ addr := ":5252"
+ b.web.httpServer = &http.Server{
+ Addr: addr,
+ Handler: http.HandlerFunc(b.web.ws.ServeHTTP),
+ }
+ b.logf("WebOrInit: serving web ui on %s", addr)
+ if err := b.web.httpServer.ListenAndServe(); err != nil {
+ if err != http.ErrServerClosed {
+ b.logf("[unexpected] WebOrInit: %v", err)
+ }
+ }
+ }()
+
+ b.logf("WebOrInit: started web ui")
+ return b.web.ws, 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).
+func (b *LocalBackend) WebShutdown() {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ b.webShutdownLocked()
+}
+
+// webShutdownLocked 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).
+//
+// b.mu must be held.
+func (b *LocalBackend) webShutdownLocked() {
+ if b.web.ws != nil {
+ b.web.ws.Shutdown()
+ }
+ if b.web.httpServer != nil {
+ if err := b.web.httpServer.Shutdown(context.Background()); err != nil {
+ b.logf("[unexpected] webShutdownLocked: %v", err)
+ }
+ }
+ if b.web.localAPIServer != nil {
+ if err := b.web.localAPIServer.Shutdown(context.Background()); err != nil {
+ b.logf("[unexpected] webShutdownLocked: %v", err)
+ }
+ }
+ if b.web.localAPIListener != nil {
+ b.web.localAPIListener.Close()
+ }
+ b.web.ws = nil
+ b.web.httpServer = nil
+ b.web.wg.Wait()
+ b.logf("webShutdownLocked: shut down web ui")
+}
diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go
index 4483efdd3..861523313 100644
--- a/ipn/localapi/localapi.go
+++ b/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:
@@ -2181,6 +2182,30 @@ 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 r.Method != httpm.POST {
+ http.Error(w, "use POST", http.StatusMethodNotAllowed)
+ return
+ }
+ switch r.URL.Path {
+ case "/localapi/v0/web/start":
+ _, err := h.b.WebOrInit(h)
+ if 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
diff --git a/tsnet/example/web-client/web-client.go b/tsnet/example/web-client/web-client.go
index 903088f23..6ed802d92 100644
--- a/tsnet/example/web-client/web-client.go
+++ b/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 {