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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
|
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package clientupdate enables the client update feature.
package clientupdate
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnext"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/localapi"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/httpm"
"tailscale.com/version"
"tailscale.com/version/distro"
)
func init() {
ipnext.RegisterExtension("clientupdate", newExt)
// C2N
ipnlocal.RegisterC2N("GET /update", handleC2NUpdateGet)
ipnlocal.RegisterC2N("POST /update", handleC2NUpdatePost)
// LocalAPI:
localapi.Register("update/install", serveUpdateInstall)
localapi.Register("update/progress", serveUpdateProgress)
}
func newExt(logf logger.Logf, sb ipnext.SafeBackend) (ipnext.Extension, error) {
return &extension{
logf: logf,
sb: sb,
lastSelfUpdateState: ipnstate.UpdateFinished,
}, nil
}
type extension struct {
logf logger.Logf
sb ipnext.SafeBackend
mu sync.Mutex
// c2nUpdateStatus is the status of c2n-triggered client update.
c2nUpdateStatus updateStatus
prefs ipn.PrefsView
state ipn.State
lastSelfUpdateState ipnstate.SelfUpdateStatus
selfUpdateProgress []ipnstate.UpdateProgress
// offlineAutoUpdateCancel stops offline auto-updates when called. It
// should be used via stopOfflineAutoUpdate and
// maybeStartOfflineAutoUpdate. It is nil when offline auto-updates are
// not running.
//
//lint:ignore U1000 only used in Linux and Windows builds in autoupdate.go
offlineAutoUpdateCancel func()
}
func (e *extension) Name() string { return "clientupdate" }
func (e *extension) Init(h ipnext.Host) error {
h.Hooks().ProfileStateChange.Add(e.onChangeProfile)
h.Hooks().BackendStateChange.Add(e.onBackendStateChange)
// TODO(nickkhyl): remove this after the profileManager refactoring.
// See tailscale/tailscale#15974.
// This same workaround appears in feature/portlist/portlist.go.
profile, prefs := h.Profiles().CurrentProfileState()
e.onChangeProfile(profile, prefs, false)
return nil
}
func (e *extension) Shutdown() error {
e.stopOfflineAutoUpdate()
return nil
}
func (e *extension) onBackendStateChange(newState ipn.State) {
e.mu.Lock()
defer e.mu.Unlock()
e.state = newState
e.updateOfflineAutoUpdateLocked()
}
func (e *extension) onChangeProfile(profile ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) {
e.mu.Lock()
defer e.mu.Unlock()
e.prefs = prefs
e.updateOfflineAutoUpdateLocked()
}
func (e *extension) updateOfflineAutoUpdateLocked() {
want := e.prefs.Valid() && e.prefs.AutoUpdate().Apply.EqualBool(true) &&
e.state != ipn.Running && e.state != ipn.Starting
cur := e.offlineAutoUpdateCancel != nil
if want && !cur {
e.maybeStartOfflineAutoUpdateLocked(e.prefs)
} else if !want && cur {
e.stopOfflineAutoUpdateLocked()
}
}
type updateStatus struct {
started bool
}
func (e *extension) clearSelfUpdateProgress() {
e.mu.Lock()
defer e.mu.Unlock()
e.selfUpdateProgress = make([]ipnstate.UpdateProgress, 0)
e.lastSelfUpdateState = ipnstate.UpdateFinished
}
func (e *extension) GetSelfUpdateProgress() []ipnstate.UpdateProgress {
e.mu.Lock()
defer e.mu.Unlock()
res := make([]ipnstate.UpdateProgress, len(e.selfUpdateProgress))
copy(res, e.selfUpdateProgress)
return res
}
func (e *extension) DoSelfUpdate() {
e.mu.Lock()
updateState := e.lastSelfUpdateState
e.mu.Unlock()
// don't start an update if one is already in progress
if updateState == ipnstate.UpdateInProgress {
return
}
e.clearSelfUpdateProgress()
e.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateInProgress, ""))
up, err := clientupdate.NewUpdater(clientupdate.Arguments{
Logf: func(format string, args ...any) {
e.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateInProgress, fmt.Sprintf(format, args...)))
},
})
if err != nil {
e.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFailed, err.Error()))
return
}
err = up.Update()
if err != nil {
e.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFailed, err.Error()))
} else {
e.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFinished, "tailscaled did not restart; please restart Tailscale manually."))
}
}
// serveUpdateInstall sends a request to the LocalBackend to start a Tailscale
// self-update. A successful response does not indicate whether the update
// succeeded, only that the request was accepted. Clients should use
// serveUpdateProgress after pinging this endpoint to check how the update is
// going.
func serveUpdateInstall(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
if r.Method != httpm.POST {
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
return
}
b := h.LocalBackend()
ext, ok := ipnlocal.GetExt[*extension](b)
if !ok {
http.Error(w, "clientupdate extension not found", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusAccepted)
go ext.DoSelfUpdate()
}
// serveUpdateProgress returns the status of an in-progress Tailscale self-update.
// This is provided as a slice of ipnstate.UpdateProgress structs with various
// log messages in order from oldest to newest. If an update is not in progress,
// the returned slice will be empty.
func serveUpdateProgress(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
if r.Method != httpm.GET {
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
return
}
b := h.LocalBackend()
ext, ok := ipnlocal.GetExt[*extension](b)
if !ok {
http.Error(w, "clientupdate extension not found", http.StatusInternalServerError)
return
}
ups := ext.GetSelfUpdateProgress()
json.NewEncoder(w).Encode(ups)
}
func (e *extension) pushSelfUpdateProgress(up ipnstate.UpdateProgress) {
e.mu.Lock()
defer e.mu.Unlock()
e.selfUpdateProgress = append(e.selfUpdateProgress, up)
e.lastSelfUpdateState = up.Status
}
func handleC2NUpdateGet(b *ipnlocal.LocalBackend, w http.ResponseWriter, r *http.Request) {
e, ok := ipnlocal.GetExt[*extension](b)
if !ok {
http.Error(w, "clientupdate extension not found", http.StatusInternalServerError)
return
}
e.logf("c2n: GET /update received")
res := e.newC2NUpdateResponse()
res.Started = e.c2nUpdateStarted()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func handleC2NUpdatePost(b *ipnlocal.LocalBackend, w http.ResponseWriter, r *http.Request) {
e, ok := ipnlocal.GetExt[*extension](b)
if !ok {
http.Error(w, "clientupdate extension not found", http.StatusInternalServerError)
return
}
e.logf("c2n: POST /update received")
res := e.newC2NUpdateResponse()
defer func() {
if res.Err != "" {
e.logf("c2n: POST /update failed: %s", res.Err)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}()
if !res.Enabled {
res.Err = "not enabled"
return
}
if !res.Supported {
res.Err = "not supported"
return
}
// Do not update if we have active inbound SSH connections. Control can set
// force=true query parameter to override this.
if r.FormValue("force") != "true" && b.ActiveSSHConns() > 0 {
res.Err = "not updating due to active SSH connections"
return
}
if err := e.startAutoUpdate("c2n"); err != nil {
res.Err = err.Error()
return
}
res.Started = true
}
func (e *extension) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
e.mu.Lock()
defer e.mu.Unlock()
// If NewUpdater does not return an error, we can update the installation.
//
// Note that we create the Updater solely to check for errors; we do not
// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
var upPref ipn.AutoUpdatePrefs
if e.prefs.Valid() {
upPref = e.prefs.AutoUpdate()
}
return tailcfg.C2NUpdateResponse{
Enabled: envknob.AllowsRemoteUpdate() || upPref.Apply.EqualBool(true),
Supported: feature.CanAutoUpdate() && !version.IsMacSysExt(),
}
}
func (e *extension) c2nUpdateStarted() bool {
e.mu.Lock()
defer e.mu.Unlock()
return e.c2nUpdateStatus.started
}
func (e *extension) setC2NUpdateStarted(v bool) {
e.mu.Lock()
defer e.mu.Unlock()
e.c2nUpdateStatus.started = v
}
func (e *extension) trySetC2NUpdateStarted() bool {
e.mu.Lock()
defer e.mu.Unlock()
if e.c2nUpdateStatus.started {
return false
}
e.c2nUpdateStatus.started = true
return true
}
// findCmdTailscale looks for the cmd/tailscale that corresponds to the
// currently running cmd/tailscaled. It's up to the caller to verify that the
// two match, but this function does its best to find the right one. Notably, it
// doesn't use $PATH for security reasons.
func findCmdTailscale() (string, error) {
self, err := os.Executable()
if err != nil {
return "", err
}
var ts string
switch runtime.GOOS {
case "linux":
if self == "/usr/sbin/tailscaled" || self == "/usr/bin/tailscaled" {
ts = "/usr/bin/tailscale"
}
if self == "/usr/local/sbin/tailscaled" || self == "/usr/local/bin/tailscaled" {
ts = "/usr/local/bin/tailscale"
}
switch distro.Get() {
case distro.QNAP:
// The volume under /share/ where qpkg are installed is not
// predictable. But the rest of the path is.
ok, err := filepath.Match("/share/*/.qpkg/Tailscale/tailscaled", self)
if err == nil && ok {
ts = filepath.Join(filepath.Dir(self), "tailscale")
}
case distro.Unraid:
if self == "/usr/local/emhttp/plugins/tailscale/bin/tailscaled" {
ts = "/usr/local/emhttp/plugins/tailscale/bin/tailscale"
}
}
case "windows":
ts = filepath.Join(filepath.Dir(self), "tailscale.exe")
case "freebsd", "openbsd":
if self == "/usr/local/bin/tailscaled" {
ts = "/usr/local/bin/tailscale"
}
default:
return "", fmt.Errorf("unsupported OS %v", runtime.GOOS)
}
if ts != "" && regularFileExists(ts) {
return ts, nil
}
return "", errors.New("tailscale executable not found in expected place")
}
func tailscaleUpdateCmd(cmdTS string) *exec.Cmd {
defaultCmd := exec.Command(cmdTS, "update", "--yes")
if runtime.GOOS != "linux" {
return defaultCmd
}
if _, err := exec.LookPath("systemd-run"); err != nil {
return defaultCmd
}
// When systemd-run is available, use it to run the update command. This
// creates a new temporary unit separate from the tailscaled unit. When
// tailscaled is restarted during the update, systemd won't kill this
// temporary update unit, which could cause unexpected breakage.
//
// We want to use a few optional flags:
// * --wait, to block the update command until completion (added in systemd 232)
// * --pipe, to collect stdout/stderr (added in systemd 235)
// * --collect, to clean up failed runs from memory (added in systemd 236)
//
// We need to check the version of systemd to figure out if those flags are
// available.
//
// The output will look like:
//
// systemd 255 (255.7-1-arch)
// +PAM +AUDIT ... other feature flags ...
systemdVerOut, err := exec.Command("systemd-run", "--version").Output()
if err != nil {
return defaultCmd
}
parts := strings.Fields(string(systemdVerOut))
if len(parts) < 2 || parts[0] != "systemd" {
return defaultCmd
}
systemdVer, err := strconv.Atoi(parts[1])
if err != nil {
return defaultCmd
}
if systemdVer >= 236 {
return exec.Command("systemd-run", "--wait", "--pipe", "--collect", cmdTS, "update", "--yes")
} else if systemdVer >= 235 {
return exec.Command("systemd-run", "--wait", "--pipe", cmdTS, "update", "--yes")
} else if systemdVer >= 232 {
return exec.Command("systemd-run", "--wait", cmdTS, "update", "--yes")
} else {
return exec.Command("systemd-run", cmdTS, "update", "--yes")
}
}
func regularFileExists(path string) bool {
fi, err := os.Stat(path)
return err == nil && fi.Mode().IsRegular()
}
// startAutoUpdate triggers an auto-update attempt. The actual update happens
// asynchronously. If another update is in progress, an error is returned.
func (e *extension) startAutoUpdate(logPrefix string) (retErr error) {
// Check if update was already started, and mark as started.
if !e.trySetC2NUpdateStarted() {
return errors.New("update already started")
}
defer func() {
// Clear the started flag if something failed.
if retErr != nil {
e.setC2NUpdateStarted(false)
}
}()
cmdTS, err := findCmdTailscale()
if err != nil {
return fmt.Errorf("failed to find cmd/tailscale binary: %w", err)
}
var ver struct {
Long string `json:"long"`
}
out, err := exec.Command(cmdTS, "version", "--json").Output()
if err != nil {
return fmt.Errorf("failed to find cmd/tailscale binary: %w", err)
}
if err := json.Unmarshal(out, &ver); err != nil {
return fmt.Errorf("invalid JSON from cmd/tailscale version --json: %w", err)
}
if ver.Long != version.Long() {
return fmt.Errorf("cmd/tailscale version %q does not match tailscaled version %q", ver.Long, version.Long())
}
cmd := tailscaleUpdateCmd(cmdTS)
buf := new(bytes.Buffer)
cmd.Stdout = buf
cmd.Stderr = buf
e.logf("%s: running %q", logPrefix, strings.Join(cmd.Args, " "))
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start cmd/tailscale update: %w", err)
}
go func() {
if err := cmd.Wait(); err != nil {
e.logf("%s: update command failed: %v, output: %s", logPrefix, err, buf)
} else {
e.logf("%s: update attempt complete", logPrefix)
}
e.setC2NUpdateStarted(false)
}()
return nil
}
func (e *extension) stopOfflineAutoUpdate() {
e.mu.Lock()
defer e.mu.Unlock()
e.stopOfflineAutoUpdateLocked()
}
func (e *extension) stopOfflineAutoUpdateLocked() {
if e.offlineAutoUpdateCancel == nil {
return
}
e.logf("offline auto-update: stopping update checks")
e.offlineAutoUpdateCancel()
e.offlineAutoUpdateCancel = nil
}
// e.mu must be held
func (e *extension) maybeStartOfflineAutoUpdateLocked(prefs ipn.PrefsView) {
if !prefs.Valid() || !prefs.AutoUpdate().Apply.EqualBool(true) {
return
}
// AutoUpdate.Apply field in prefs can only be true for platforms that
// support auto-updates. But check it here again, just in case.
if !feature.CanAutoUpdate() {
return
}
// On macsys, auto-updates are managed by Sparkle.
if version.IsMacSysExt() {
return
}
if e.offlineAutoUpdateCancel != nil {
// Already running.
return
}
ctx, cancel := context.WithCancel(context.Background())
e.offlineAutoUpdateCancel = cancel
e.logf("offline auto-update: starting update checks")
go e.offlineAutoUpdate(ctx)
}
const offlineAutoUpdateCheckPeriod = time.Hour
func (e *extension) offlineAutoUpdate(ctx context.Context) {
t := time.NewTicker(offlineAutoUpdateCheckPeriod)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
}
if err := e.startAutoUpdate("offline auto-update"); err != nil {
e.logf("offline auto-update: failed: %v", err)
}
}
}
|