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 <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy
2023-10-11 14:35:22 -04:00
committed by Sonia Appasamy
parent 93aa8a8cff
commit 89953b015b
14 changed files with 211 additions and 12 deletions

View File

@@ -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
ipn/ipnlocal/web.go Normal file
View File

@@ -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
ipn/ipnlocal/web_stub.go Normal file
View File

@@ -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() {}