ipn/ipnlocal: periodically run auto-updates when "offline" (#12118)

When the client is disconnected from control for any reason (typically
just turned off), we should still attempt to update if auto-updates are
enabled. This may help users who turn tailscale on infrequently for
accessing resources.

RELNOTE: Apply auto-updates even if the node is down or disconnected
from the coordination server.

Updates #12117

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
pull/12341/head
Andrew Lytvynov 3 weeks ago committed by GitHub
parent 2f2f588c80
commit bc4c8b65c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,60 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || windows
package ipnlocal
import (
"context"
"time"
"tailscale.com/clientupdate"
"tailscale.com/ipn"
)
func (b *LocalBackend) stopOfflineAutoUpdate() {
if b.offlineAutoUpdateCancel != nil {
b.logf("offline auto-update: stopping update checks")
b.offlineAutoUpdateCancel()
b.offlineAutoUpdateCancel = nil
}
}
func (b *LocalBackend) maybeStartOfflineAutoUpdate(prefs ipn.PrefsView) {
if !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 !clientupdate.CanAutoUpdate() {
return
}
if b.offlineAutoUpdateCancel != nil {
// Already running.
return
}
ctx, cancel := context.WithCancel(context.Background())
b.offlineAutoUpdateCancel = cancel
b.logf("offline auto-update: starting update checks")
go b.offlineAutoUpdate(ctx)
}
const offlineAutoUpdateCheckPeriod = time.Hour
func (b *LocalBackend) offlineAutoUpdate(ctx context.Context) {
t := time.NewTicker(offlineAutoUpdateCheckPeriod)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
}
if err := b.startAutoUpdate("offline auto-update"); err != nil {
b.logf("offline auto-update: failed: %v", err)
}
}
}

@ -0,0 +1,18 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(linux || windows)
package ipnlocal
import (
"tailscale.com/ipn"
)
func (b *LocalBackend) stopOfflineAutoUpdate() {
// Not supported on this platform.
}
func (b *LocalBackend) maybeStartOfflineAutoUpdate(prefs ipn.PrefsView) {
// Not supported on this platform.
}

@ -4,7 +4,6 @@
package ipnlocal
import (
"bytes"
"crypto/x509"
"encoding/json"
"encoding/pem"
@ -307,60 +306,11 @@ func handleC2NUpdatePost(b *LocalBackend, w http.ResponseWriter, r *http.Request
return
}
// Check if update was already started, and mark as started.
if !b.trySetC2NUpdateStarted() {
res.Err = "update already started"
return
}
defer func() {
// Clear the started flag if something failed.
if res.Err != "" {
b.setC2NUpdateStarted(false)
}
}()
cmdTS, err := findCmdTailscale()
if err != nil {
res.Err = fmt.Sprintf("failed to find cmd/tailscale binary: %v", err)
return
}
var ver struct {
Long string `json:"long"`
}
out, err := exec.Command(cmdTS, "version", "--json").Output()
if err != nil {
res.Err = fmt.Sprintf("failed to find cmd/tailscale binary: %v", err)
return
}
if err := json.Unmarshal(out, &ver); err != nil {
res.Err = "invalid JSON from cmd/tailscale version --json"
return
}
if ver.Long != version.Long() {
res.Err = "cmd/tailscale version mismatch"
return
}
cmd := tailscaleUpdateCmd(cmdTS)
buf := new(bytes.Buffer)
cmd.Stdout = buf
cmd.Stderr = buf
b.logf("c2n: running %q", strings.Join(cmd.Args, " "))
if err := cmd.Start(); err != nil {
res.Err = fmt.Sprintf("failed to start cmd/tailscale update: %v", err)
if err := b.startAutoUpdate("c2n"); err != nil {
res.Err = err.Error()
return
}
res.Started = true
// Run update asynchronously and respond that it started.
go func() {
if err := cmd.Wait(); err != nil {
b.logf("c2n: update command failed: %v, output: %s", err, buf)
} else {
b.logf("c2n: update complete")
}
b.setC2NUpdateStarted(false)
}()
}
func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {

@ -4,6 +4,7 @@
package ipnlocal
import (
"bytes"
"cmp"
"context"
"encoding/base64"
@ -20,6 +21,7 @@ import (
"net/netip"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
@ -291,6 +293,13 @@ type LocalBackend struct {
// capForcedNetfilter is the netfilter that control instructs Linux clients
// to use, unless overridden locally.
capForcedNetfilter string
// offlineAutoUpdateCancel stops offline auto-updates when called. It
// should be used via stopOfflineAutoUpdate and
// maybeStartOfflineAutoUpdate. It is nil when offline auto-updates are
// note running.
//
//lint:ignore U1000 only used in Linux and Windows builds in autoupdate.go
offlineAutoUpdateCancel func()
// ServeConfig fields. (also guarded by mu)
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
@ -3327,6 +3336,14 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
b.logf("failed to save new controlclient state: %v", err)
}
if newp.AutoUpdate.Apply.EqualBool(true) {
if b.state != ipn.Running {
b.maybeStartOfflineAutoUpdate(newp.View())
}
} else {
b.stopOfflineAutoUpdate()
}
unlock.UnlockEarly()
if oldp.ShieldsUp() != newp.ShieldsUp || hostInfoChanged {
@ -4347,6 +4364,12 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
}
b.pauseOrResumeControlClientLocked()
if newState == ipn.Running {
b.stopOfflineAutoUpdate()
} else {
b.maybeStartOfflineAutoUpdate(prefs)
}
unlock.UnlockEarly()
// prefs may change irrespective of state; WantRunning should be explicitly
@ -6661,3 +6684,55 @@ func longLatDistance(fromLat, fromLong, toLat, toLong float64) float64 {
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadiusMeters * c
}
// startAutoUpdate triggers an auto-update attempt. The actual update happens
// asynchronously. If another update is in progress, an error is returned.
func (b *LocalBackend) startAutoUpdate(logPrefix string) (retErr error) {
// Check if update was already started, and mark as started.
if !b.trySetC2NUpdateStarted() {
return errors.New("update already started")
}
defer func() {
// Clear the started flag if something failed.
if retErr != nil {
b.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
b.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 {
b.logf("%s: update command failed: %v, output: %s", logPrefix, err, buf)
} else {
b.logf("%s: update attempt complete", logPrefix)
}
b.setC2NUpdateStarted(false)
}()
return nil
}

Loading…
Cancel
Save