mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-04 23:45:34 +00:00
bd534b971a
This change removes the existing debug-web-client localapi endpoint and replaces it with functions passed directly to the web.ServerOpts when constructing a web.ManageServerMode client. The debug-web-client endpoint previously handled making noise requests to the control server via the /machine/webclient/ endpoints. The noise requests must be made from tailscaled, which has the noise connection open. But, now that the full client is served from tailscaled, we no longer need to proxy this request over the localapi. Updates tailscale/corp#14335 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
206 lines
6.1 KiB
Go
206 lines
6.1 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !ios && !android
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"sync"
|
|
"time"
|
|
|
|
"tailscale.com/client/tailscale"
|
|
"tailscale.com/client/web"
|
|
"tailscale.com/logtail/backoff"
|
|
"tailscale.com/net/netutil"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/mak"
|
|
)
|
|
|
|
const webClientPort = web.ListenPort
|
|
|
|
// webClient holds state for the web interface for managing this
|
|
// tailscale instance. The web interface is not used by default,
|
|
// but initialized by calling LocalBackend.WebClientGetOrInit.
|
|
type webClient struct {
|
|
mu sync.Mutex // protects webClient fields
|
|
|
|
server *web.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
|
|
}
|
|
|
|
// ConfigureWebClient configures b.web prior to use.
|
|
// Specifially, it sets b.web.lc to the provided LocalClient.
|
|
// If provided as nil, b.web.lc is cleared out.
|
|
func (b *LocalBackend) ConfigureWebClient(lc *tailscale.LocalClient) {
|
|
b.webClient.mu.Lock()
|
|
defer b.webClient.mu.Unlock()
|
|
b.webClient.lc = lc
|
|
}
|
|
|
|
// webClientGetOrInit gets or initializes the web server for managing
|
|
// this tailscaled instance.
|
|
// s is always non-nil if err is empty.
|
|
func (b *LocalBackend) webClientGetOrInit() (s *web.Server, err error) {
|
|
if !b.ShouldRunWebClient() {
|
|
return nil, errors.New("web client not enabled for this device")
|
|
}
|
|
|
|
b.webClient.mu.Lock()
|
|
defer b.webClient.mu.Unlock()
|
|
if b.webClient.server != nil {
|
|
return b.webClient.server, nil
|
|
}
|
|
|
|
b.logf("webClientGetOrInit: initializing web ui")
|
|
if b.webClient.server, err = web.NewServer(web.ServerOpts{
|
|
Mode: web.ManageServerMode,
|
|
LocalClient: b.webClient.lc,
|
|
Logf: b.logf,
|
|
NewAuthURL: b.newWebClientAuthURL,
|
|
WaitAuthURL: b.waitWebClientAuthURL,
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("web.NewServer: %w", err)
|
|
}
|
|
|
|
b.logf("webClientGetOrInit: started web ui")
|
|
return b.webClient.server, nil
|
|
}
|
|
|
|
// WebClientShutdown shuts down any running b.webClient servers and
|
|
// clears out b.webClient state (besides the b.webClient.lc field,
|
|
// which is left untouched because required for future web startups).
|
|
// WebClientShutdown obtains the b.mu lock.
|
|
func (b *LocalBackend) webClientShutdown() {
|
|
b.mu.Lock()
|
|
for ap, ln := range b.webClientListeners {
|
|
ln.Close()
|
|
delete(b.webClientListeners, ap)
|
|
}
|
|
b.mu.Unlock()
|
|
|
|
b.webClient.mu.Lock() // webClient struct uses its own mutext
|
|
server := b.webClient.server
|
|
b.webClient.server = nil
|
|
b.webClient.mu.Unlock() // release lock before shutdown
|
|
if server != nil {
|
|
server.Shutdown()
|
|
b.logf("WebClientShutdown: shut down web ui")
|
|
}
|
|
}
|
|
|
|
// handleWebClientConn serves web client requests.
|
|
func (b *LocalBackend) handleWebClientConn(c net.Conn) error {
|
|
webServer, err := b.webClientGetOrInit()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s := http.Server{Handler: webServer}
|
|
return s.Serve(netutil.NewOneConnListener(c, nil))
|
|
}
|
|
|
|
// updateWebClientListenersLocked creates listeners on the web client port (5252)
|
|
// for each of the local device's Tailscale IP addresses. This is needed to properly
|
|
// route local traffic when using kernel networking mode.
|
|
func (b *LocalBackend) updateWebClientListenersLocked() {
|
|
if b.netMap == nil {
|
|
return
|
|
}
|
|
|
|
addrs := b.netMap.GetAddresses()
|
|
for i := range addrs.LenIter() {
|
|
addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), webClientPort)
|
|
if _, ok := b.webClientListeners[addrPort]; ok {
|
|
continue // already listening
|
|
}
|
|
|
|
sl := b.newWebClientListener(context.Background(), addrPort, b.logf)
|
|
mak.Set(&b.webClientListeners, addrPort, sl)
|
|
|
|
go sl.Run()
|
|
}
|
|
}
|
|
|
|
// newWebClientListener returns a listener for local connections to the built-in web client
|
|
// used to manage this Tailscale instance.
|
|
func (b *LocalBackend) newWebClientListener(ctx context.Context, ap netip.AddrPort, logf logger.Logf) *localListener {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
return &localListener{
|
|
b: b,
|
|
ap: ap,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
logf: logf,
|
|
|
|
handler: b.handleWebClientConn,
|
|
bo: backoff.NewBackoff("webclient-listener", logf, 30*time.Second),
|
|
}
|
|
}
|
|
|
|
// newWebClientAuthURL talks to the control server to create a new auth
|
|
// URL that can be used to validate a browser session to manage this
|
|
// tailscaled instance via the web client.
|
|
func (b *LocalBackend) newWebClientAuthURL(ctx context.Context, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error) {
|
|
return b.doWebClientNoiseRequest(ctx, "", src)
|
|
}
|
|
|
|
// waitWebClientAuthURL connects to the control server and blocks
|
|
// until the associated auth URL has been completed by its user,
|
|
// or until ctx is canceled.
|
|
func (b *LocalBackend) waitWebClientAuthURL(ctx context.Context, id string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error) {
|
|
return b.doWebClientNoiseRequest(ctx, id, src)
|
|
}
|
|
|
|
// doWebClientNoiseRequest handles making the "/machine/webclient"
|
|
// noise requests to the control server for web client user auth.
|
|
//
|
|
// It either creates a new control auth URL or waits for an existing
|
|
// one to be completed, based on the presence or absence of the
|
|
// provided id value.
|
|
func (b *LocalBackend) doWebClientNoiseRequest(ctx context.Context, id string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error) {
|
|
nm := b.NetMap()
|
|
if nm == nil || !nm.SelfNode.Valid() {
|
|
return nil, errors.New("[unexpected] no self node")
|
|
}
|
|
dst := nm.SelfNode.ID()
|
|
var noiseURL string
|
|
if id != "" {
|
|
noiseURL = fmt.Sprintf("https://unused/machine/webclient/wait/%d/to/%d/%s", src, dst, id)
|
|
} else {
|
|
noiseURL = fmt.Sprintf("https://unused/machine/webclient/init/%d/to/%d", src, dst)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", noiseURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := b.DoNoiseRequest(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("failed request: %s", body)
|
|
}
|
|
var authResp *tailcfg.WebClientAuthResponse
|
|
if err := json.Unmarshal(body, &authResp); err != nil {
|
|
return nil, err
|
|
}
|
|
return authResp, nil
|
|
}
|