Compare commits
2 Commits
awly/cli-j
...
will/sonia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11b70e46b8 | ||
|
|
7f3599748a |
@@ -32,12 +32,14 @@ import (
|
||||
"tailscale.com/licenses"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// Server is the backend server for a Tailscale web client.
|
||||
type Server struct {
|
||||
logf logger.Logf
|
||||
lc *tailscale.LocalClient
|
||||
timeNow func() time.Time
|
||||
|
||||
@@ -47,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.
|
||||
@@ -136,14 +139,20 @@ type ServerOpts struct {
|
||||
// TimeNow optionally provides a time function.
|
||||
// time.Now is used as default.
|
||||
TimeNow func() time.Time
|
||||
|
||||
Logf logger.Logf
|
||||
}
|
||||
|
||||
// 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{}
|
||||
}
|
||||
s = &Server{
|
||||
logf: opts.Logf,
|
||||
devMode: opts.DevMode,
|
||||
lc: opts.LocalClient,
|
||||
cgiMode: opts.CGIMode,
|
||||
@@ -153,8 +162,13 @@ func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
|
||||
if s.timeNow == nil {
|
||||
s.timeNow = time.Now
|
||||
}
|
||||
if s.logf == nil {
|
||||
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
|
||||
|
||||
// Create handler for "/api" requests with CSRF protection.
|
||||
// We don't require secure cookies, since the web client is regularly used
|
||||
@@ -166,13 +180,26 @@ func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
|
||||
// For the login client, we don't serve the full web client API,
|
||||
// only the login endpoints.
|
||||
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
|
||||
s.lc.IncrementCounter(context.Background(), "web_login_client_initialization", 1)
|
||||
metric = "web_login_client_initialization"
|
||||
} else {
|
||||
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveAPI))
|
||||
s.lc.IncrementCounter(context.Background(), "web_client_initialization", 1)
|
||||
metric = "web_client_initialization"
|
||||
}
|
||||
|
||||
return s, cleanup
|
||||
// Report metric in separate go routine with 5 second timeout.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
go func() {
|
||||
defer cancel()
|
||||
s.lc.IncrementCounter(ctx, metric, 1)
|
||||
}()
|
||||
|
||||
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.
|
||||
@@ -645,7 +672,7 @@ func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
mp.Prefs.WantRunning = true
|
||||
mp.Prefs.AdvertiseRoutes = routes
|
||||
log.Printf("Doing edit: %v", mp.Pretty())
|
||||
s.logf("Doing edit: %v", mp.Pretty())
|
||||
|
||||
if _, err := s.lc.EditPrefs(r.Context(), mp); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
@@ -661,9 +688,9 @@ func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
if postData.ForceLogout {
|
||||
logout = true
|
||||
}
|
||||
log.Printf("tailscaleUp(reauth=%v, logout=%v) ...", reauth, logout)
|
||||
s.logf("tailscaleUp(reauth=%v, logout=%v) ...", reauth, logout)
|
||||
url, err := s.tailscaleUp(r.Context(), st, postData)
|
||||
log.Printf("tailscaleUp = (URL %v, %v)", url != "", err)
|
||||
s.logf("tailscaleUp = (URL %v, %v)", url != "", err)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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)
|
||||
|
||||
126
ipn/ipnlocal/web.go
Normal file
126
ipn/ipnlocal/web.go
Normal file
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user