| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- //go:build !ios && !android && !ts_omit_webclient
- package ipnlocal
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net"
- "net/http"
- "net/netip"
- "sync"
- "time"
- "tailscale.com/client/local"
- "tailscale.com/client/web"
- "tailscale.com/net/netutil"
- "tailscale.com/tailcfg"
- "tailscale.com/tsconst"
- "tailscale.com/types/logger"
- "tailscale.com/util/backoff"
- "tailscale.com/util/mak"
- )
- const webClientPort = tsconst.WebListenPort
- // 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 local.Client to use to connect
- // to the localapi for this tailscaled instance.
- // If nil, a default is used.
- lc *local.Client
- }
- // ConfigureWebClient configures b.web prior to use.
- // Specifially, it sets b.web.lc to the provided local.Client.
- // If provided as nil, b.web.lc is cleared out.
- func (b *LocalBackend) ConfigureWebClient(lc *local.Client) {
- 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() {
- nm := b.currentNode().NetMap()
- if nm == nil {
- return
- }
- addrs := nm.GetAddresses()
- for _, pfx := range addrs.All() {
- addrPort := netip.AddrPortFrom(pfx.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
- }
|