feature/clientupdate: move clientupdate to a modular feature, disabled for tsnet

Updates #12614

Change-Id: I5f685dec84a5396b7c2b66f2788ae3d286e1ddc6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-09-26 16:41:26 -07:00
committed by Brad Fitzpatrick
parent 69c79cb9f3
commit 038cdb4640
24 changed files with 749 additions and 609 deletions
-65
View File
@@ -1,65 +0,0 @@
// 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"
"tailscale.com/version"
)
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
}
// On macsys, auto-updates are managed by Sparkle.
if version.IsMacSysExt() {
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)
}
}
}
-18
View File
@@ -1,18 +0,0 @@
// 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.
}
-190
View File
@@ -5,23 +5,16 @@ package ipnlocal
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"time"
"tailscale.com/clientupdate"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/sockstats"
"tailscale.com/posture"
@@ -34,7 +27,6 @@ import (
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/ptype"
"tailscale.com/version"
"tailscale.com/version/distro"
)
// c2nHandlers maps an HTTP method and URI path (without query parameters) to
@@ -60,10 +52,6 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
// SSH
req("/ssh/usernames"): handleC2NSSHUsernames,
// Auto-updates.
req("GET /update"): handleC2NUpdateGet,
req("POST /update"): handleC2NUpdatePost,
// Device posture.
req("GET /posture/identity"): handleC2NPostureIdentityGet,
@@ -337,50 +325,6 @@ func handleC2NSetNetfilterKind(b *LocalBackend, w http.ResponseWriter, r *http.R
w.WriteHeader(http.StatusNoContent)
}
func handleC2NUpdateGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
b.logf("c2n: GET /update received")
res := b.newC2NUpdateResponse()
res.Started = b.c2nUpdateStarted()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func handleC2NUpdatePost(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
b.logf("c2n: POST /update received")
res := b.newC2NUpdateResponse()
defer func() {
if res.Err != "" {
b.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.sshServer != nil && b.sshServer.NumActiveConns() > 0 {
res.Err = "not updating due to active SSH connections"
return
}
if err := b.startAutoUpdate("c2n"); err != nil {
res.Err = err.Error()
return
}
res.Started = true
}
func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
b.logf("c2n: GET /posture/identity received")
@@ -423,137 +367,3 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
// 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.
prefs := b.Prefs().AutoUpdate()
return tailcfg.C2NUpdateResponse{
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply.EqualBool(true),
Supported: clientupdate.CanAutoUpdate() && !version.IsMacSysExt(),
}
}
func (b *LocalBackend) c2nUpdateStarted() bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.c2nUpdateStatus.started
}
func (b *LocalBackend) setC2NUpdateStarted(v bool) {
b.mu.Lock()
defer b.mu.Unlock()
b.c2nUpdateStatus.started = v
}
func (b *LocalBackend) trySetC2NUpdateStarted() bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.c2nUpdateStatus.started {
return false
}
b.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()
}
+17 -138
View File
@@ -6,7 +6,6 @@
package ipnlocal
import (
"bytes"
"cmp"
"context"
"crypto/sha256"
@@ -25,7 +24,6 @@ import (
"net/netip"
"net/url"
"os"
"os/exec"
"reflect"
"runtime"
"slices"
@@ -40,7 +38,6 @@ import (
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/clientupdate"
"tailscale.com/control/controlclient"
"tailscale.com/control/controlknobs"
"tailscale.com/drive"
@@ -302,22 +299,11 @@ type LocalBackend struct {
notifyWatchers map[string]*watchSession // by session ID
lastStatusTime time.Time // status.AsOf value of the last processed status update
componentLogUntil map[string]componentLogState
// c2nUpdateStatus is the status of c2n-triggered client update.
c2nUpdateStatus updateStatus
currentUser ipnauth.Actor
currentUser ipnauth.Actor
selfUpdateProgress []ipnstate.UpdateProgress
lastSelfUpdateState ipnstate.SelfUpdateStatus
// capForcedNetfilter is the netfilter that control instructs Linux clients
// to use, unless overridden locally.
capForcedNetfilter string // TODO(nickkhyl): move to nodeBackend
// 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
@@ -433,10 +419,6 @@ func (b *LocalBackend) NetMon() *netmon.Monitor {
return b.sys.NetMon.Get()
}
type updateStatus struct {
started bool
}
type metrics struct {
// advertisedRoutes is a metric that reports the number of network routes that are advertised by the local node.
// This informs the user of how many routes are being advertised by the local node, excluding exit routes.
@@ -517,8 +499,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
em: newExpiryManager(logf, sys.Bus.Get()),
loginFlags: loginFlags,
clock: clock,
selfUpdateProgress: make([]ipnstate.UpdateProgress, 0),
lastSelfUpdateState: ipnstate.UpdateFinished,
captiveCtx: captiveCtx,
captiveCancel: nil, // so that we start checkCaptivePortalLoop when Running
needsCaptiveDetection: make(chan bool),
@@ -1127,7 +1107,6 @@ func (b *LocalBackend) Shutdown() {
defer cancel()
b.sockstatLogger.Shutdown(ctx)
}
b.stopOfflineAutoUpdate()
b.unregisterSysPolicyWatch()
if cc != nil {
@@ -3412,7 +3391,7 @@ func (b *LocalBackend) onTailnetDefaultAutoUpdate(au bool) {
// can still manually enable auto-updates on this node.
return
}
if clientupdate.CanAutoUpdate() {
if buildfeatures.HasClientUpdate && feature.CanAutoUpdate() {
b.logf("using tailnet default auto-update setting: %v", au)
prefsClone := prefs.AsStruct()
prefsClone.AutoUpdate.Apply = opt.NewBool(au)
@@ -4100,7 +4079,12 @@ func (b *LocalBackend) checkFunnelEnabledLocked(p *ipn.Prefs) error {
}
func (b *LocalBackend) checkAutoUpdatePrefsLocked(p *ipn.Prefs) error {
if p.AutoUpdate.Apply.EqualBool(true) && !clientupdate.CanAutoUpdate() {
if !buildfeatures.HasClientUpdate {
if p.AutoUpdate.Apply.EqualBool(true) {
return errors.New("Auto-update support is disabled in this build")
}
}
if p.AutoUpdate.Apply.EqualBool(true) && !feature.CanAutoUpdate() {
return errors.New("Auto-updates are not supported on this platform.")
}
return nil
@@ -4552,14 +4536,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
b.resetAlwaysOnOverrideLocked()
}
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 {
@@ -5467,12 +5443,6 @@ 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
@@ -6611,6 +6581,15 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
return cc.DoNoiseRequest(req)
}
// ActiveSSHConns returns the number of active SSH connections,
// or 0 if SSH is not linked into the binary or available on the platform.
func (b *LocalBackend) ActiveSSHConns() int {
if b.sshServer == nil {
return 0
}
return b.sshServer.NumActiveConns()
}
func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
b.mu.Lock()
defer b.mu.Unlock()
@@ -6941,54 +6920,6 @@ func (b *LocalBackend) DebugBreakDERPConns() error {
return b.MagicConn().DebugBreakDERPConns()
}
func (b *LocalBackend) pushSelfUpdateProgress(up ipnstate.UpdateProgress) {
b.mu.Lock()
defer b.mu.Unlock()
b.selfUpdateProgress = append(b.selfUpdateProgress, up)
b.lastSelfUpdateState = up.Status
}
func (b *LocalBackend) clearSelfUpdateProgress() {
b.mu.Lock()
defer b.mu.Unlock()
b.selfUpdateProgress = make([]ipnstate.UpdateProgress, 0)
b.lastSelfUpdateState = ipnstate.UpdateFinished
}
func (b *LocalBackend) GetSelfUpdateProgress() []ipnstate.UpdateProgress {
b.mu.Lock()
defer b.mu.Unlock()
res := make([]ipnstate.UpdateProgress, len(b.selfUpdateProgress))
copy(res, b.selfUpdateProgress)
return res
}
func (b *LocalBackend) DoSelfUpdate() {
b.mu.Lock()
updateState := b.lastSelfUpdateState
b.mu.Unlock()
// don't start an update if one is already in progress
if updateState == ipnstate.UpdateInProgress {
return
}
b.clearSelfUpdateProgress()
b.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateInProgress, ""))
up, err := clientupdate.NewUpdater(clientupdate.Arguments{
Logf: func(format string, args ...any) {
b.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateInProgress, fmt.Sprintf(format, args...)))
},
})
if err != nil {
b.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFailed, err.Error()))
}
err = up.Update()
if err != nil {
b.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFailed, err.Error()))
} else {
b.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFinished, "tailscaled did not restart; please restart Tailscale manually."))
}
}
// ObserveDNSResponse passes a DNS response from the PeerAPI DNS server to the
// App Connector to enable route discovery.
func (b *LocalBackend) ObserveDNSResponse(res []byte) error {
@@ -7603,58 +7534,6 @@ func isAllowedAutoExitNodeID(polc policyclient.Client, exitNodeID tailcfg.Stable
return true // no policy configured; allow all exit nodes
}
// 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
}
// srcIPHasCapForFilter is called by the packet filter when evaluating firewall
// rules that require a source IP to have a certain node capability.
//
+3 -3
View File
@@ -30,10 +30,10 @@ import (
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc"
"tailscale.com/appc/appctest"
"tailscale.com/clientupdate"
"tailscale.com/control/controlclient"
"tailscale.com/drive"
"tailscale.com/drive/driveimpl"
"tailscale.com/feature"
_ "tailscale.com/feature/condregister/portmapper"
"tailscale.com/health"
"tailscale.com/hostinfo"
@@ -3710,7 +3710,7 @@ func TestOnTailnetDefaultAutoUpdate(t *testing.T) {
// On platforms that don't support auto-update we can never
// transition to auto-updates being enabled. The value should
// remain unchanged after onTailnetDefaultAutoUpdate.
if !clientupdate.CanAutoUpdate() {
if !feature.CanAutoUpdate() {
want = tt.before
}
if got := b.pm.CurrentPrefs().AutoUpdate().Apply; got != want {
@@ -5455,7 +5455,7 @@ func TestEnableAutoUpdates(t *testing.T) {
})
// Enabling may fail, depending on which environment we are running this
// test in.
wantErr := !clientupdate.CanAutoUpdate()
wantErr := !feature.CanAutoUpdate()
gotErr := err != nil
if gotErr != wantErr {
t.Fatalf("enabling auto-updates: got error: %v (%v); want error: %v", gotErr, err, wantErr)
+2 -2
View File
@@ -13,8 +13,8 @@ import (
"slices"
"strings"
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/health"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnext"
@@ -674,7 +674,7 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
// cause any EditPrefs calls to fail (other than disabling auto-updates).
//
// Reset AutoUpdate.Apply if we detect such invalid prefs.
if savedPrefs.AutoUpdate.Apply.EqualBool(true) && !clientupdate.CanAutoUpdate() {
if savedPrefs.AutoUpdate.Apply.EqualBool(true) && !feature.CanAutoUpdate() {
savedPrefs.AutoUpdate.Apply.Clear()
}
+3 -2
View File
@@ -12,7 +12,8 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"tailscale.com/clientupdate"
_ "tailscale.com/clientupdate" // for feature registration side effects
"tailscale.com/feature"
"tailscale.com/health"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
@@ -464,7 +465,7 @@ func TestProfileManagement(t *testing.T) {
wantCurProfile = "user@2.example.com"
checkProfiles(t)
if !clientupdate.CanAutoUpdate() {
if !feature.CanAutoUpdate() {
t.Logf("Save an invalid AutoUpdate pref value")
prefs := pm.CurrentPrefs().AsStruct()
prefs.AutoUpdate.Apply.Set(true)
+2 -35
View File
@@ -27,8 +27,8 @@ import (
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health/healthmsg"
"tailscale.com/hostinfo"
@@ -120,8 +120,6 @@ var handler = map[string]LocalAPIHandler{
"status": (*Handler).serveStatus,
"suggest-exit-node": (*Handler).serveSuggestExitNode,
"update/check": (*Handler).serveUpdateCheck,
"update/install": (*Handler).serveUpdateInstall,
"update/progress": (*Handler).serveUpdateProgress,
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
"usermetrics": (*Handler).serveUserMetrics,
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
@@ -1897,7 +1895,7 @@ func (h *Handler) serveUpdateCheck(w http.ResponseWriter, r *http.Request) {
return
}
if !clientupdate.CanAutoUpdate() {
if !feature.CanAutoUpdate() {
// if we don't support auto-update, just say that we're up to date
json.NewEncoder(w).Encode(tailcfg.ClientVersion{RunningLatest: true})
return
@@ -1915,37 +1913,6 @@ func (h *Handler) serveUpdateCheck(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(cv)
}
// 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 (h *Handler) serveUpdateInstall(w http.ResponseWriter, r *http.Request) {
if r.Method != httpm.POST {
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusAccepted)
go h.b.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 (h *Handler) serveUpdateProgress(w http.ResponseWriter, r *http.Request) {
if r.Method != httpm.GET {
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
return
}
ups := h.b.GetSelfUpdateProgress()
json.NewEncoder(w).Encode(ups)
}
// serveDNSOSConfig serves the current system DNS configuration as a JSON object, if
// supported by the OS.
func (h *Handler) serveDNSOSConfig(w http.ResponseWriter, r *http.Request) {