summaryrefslogtreecommitdiffhomepage
path: root/ipn/ipnlocal/web_client.go
blob: 37dba93d0a49bdbcd79cde881b624e8ac2c3c149 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Copyright (c) Tailscale Inc & contributors
// 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
}