Переглянути джерело

ipn/{ipnlocal,ipnstate,localapi}: add localapi endpoints for client self-update (#10188)

* ipn/{ipnlocal,ipnstate,localapi}: add localapi endpoints for client self-update

Updates #10187.

Signed-off-by: Naman Sood <[email protected]>

* depaware

Updates #10187.

Signed-off-by: Naman Sood <[email protected]>

* address review feedback

Signed-off-by: Naman Sood <[email protected]>

---------

Signed-off-by: Naman Sood <[email protected]>
Naman Sood 2 роки тому
батько
коміт
e57fd9cda4
4 змінених файлів з 152 додано та 3 видалено
  1. 1 1
      cmd/tailscaled/depaware.txt
  2. 55 2
      ipn/ipnlocal/local.go
  3. 23 0
      ipn/ipnstate/ipnstate.go
  4. 73 0
      ipn/localapi/localapi.go

+ 1 - 1
cmd/tailscaled/depaware.txt

@@ -228,7 +228,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         tailscale.com/client/tailscale                               from tailscale.com/derp+
         tailscale.com/client/tailscale/apitype                       from tailscale.com/ipn/ipnlocal+
         tailscale.com/client/web                                     from tailscale.com/ipn/ipnlocal
-        tailscale.com/clientupdate                                   from tailscale.com/ipn/ipnlocal
+        tailscale.com/clientupdate                                   from tailscale.com/ipn/ipnlocal+
         tailscale.com/clientupdate/distsign                          from tailscale.com/clientupdate
         tailscale.com/cmd/tailscaled/childproc                       from tailscale.com/ssh/tailssh+
         tailscale.com/control/controlbase                            from tailscale.com/control/controlclient+

+ 55 - 2
ipn/ipnlocal/local.go

@@ -34,6 +34,7 @@ import (
 	"gvisor.dev/gvisor/pkg/tcpip"
 	"tailscale.com/appc"
 	"tailscale.com/client/tailscale/apitype"
+	"tailscale.com/clientupdate"
 	"tailscale.com/control/controlclient"
 	"tailscale.com/control/controlknobs"
 	"tailscale.com/doctor"
@@ -265,8 +266,10 @@ type LocalBackend struct {
 	directFileDoFinalRename bool // false on macOS, true on several NAS platforms
 	componentLogUntil       map[string]componentLogState
 	// c2nUpdateStatus is the status of c2n-triggered client update.
-	c2nUpdateStatus updateStatus
-	currentUser     ipnauth.WindowsToken
+	c2nUpdateStatus     updateStatus
+	currentUser         ipnauth.WindowsToken
+	selfUpdateProgress  []ipnstate.UpdateProgress
+	lastSelfUpdateState ipnstate.SelfUpdateStatus
 
 	// ServeConfig fields. (also guarded by mu)
 	lastServeConfJSON   mem.RO              // last JSON that was parsed into serveConfig
@@ -374,6 +377,8 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
 		loginFlags:          loginFlags,
 		clock:               clock,
 		activeWatchSessions: make(set.Set[string]),
+		selfUpdateProgress:  make([]ipnstate.UpdateProgress, 0),
+		lastSelfUpdateState: ipnstate.UpdateFinished,
 	}
 
 	netMon := sys.NetMon.Get()
@@ -5539,6 +5544,54 @@ 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) {

+ 23 - 0
ipn/ipnstate/ipnstate.go

@@ -22,6 +22,7 @@ import (
 	"tailscale.com/types/ptr"
 	"tailscale.com/types/views"
 	"tailscale.com/util/dnsname"
+	"tailscale.com/version"
 )
 
 //go:generate go run tailscale.com/cmd/cloner  -clonefunc=false -type=TKAFilteredPeer
@@ -710,3 +711,25 @@ type DebugDERPRegionReport struct {
 	Warnings []string
 	Errors   []string
 }
+
+type SelfUpdateStatus string
+
+const (
+	UpdateFinished   SelfUpdateStatus = "UpdateFinished"
+	UpdateInProgress SelfUpdateStatus = "UpdateInProgress"
+	UpdateFailed     SelfUpdateStatus = "UpdateFailed"
+)
+
+type UpdateProgress struct {
+	Status  SelfUpdateStatus `json:"status,omitempty"`
+	Message string           `json:"message,omitempty"`
+	Version string           `json:"version,omitempty"`
+}
+
+func NewUpdateProgress(ps SelfUpdateStatus, msg string) UpdateProgress {
+	return UpdateProgress{
+		Status:  ps,
+		Message: msg,
+		Version: version.Short(),
+	}
+}

+ 73 - 0
ipn/localapi/localapi.go

@@ -27,6 +27,7 @@ import (
 	"time"
 
 	"tailscale.com/client/tailscale/apitype"
+	"tailscale.com/clientupdate"
 	"tailscale.com/envknob"
 	"tailscale.com/health"
 	"tailscale.com/hostinfo"
@@ -125,6 +126,9 @@ var handler = map[string]localAPIHandler{
 	"watch-ipn-bus":               (*Handler).serveWatchIPNBus,
 	"whois":                       (*Handler).serveWhoIs,
 	"query-feature":               (*Handler).serveQueryFeature,
+	"update/check":                (*Handler).serveUpdateCheck,
+	"update/install":              (*Handler).serveUpdateInstall,
+	"update/progress":             (*Handler).serveUpdateProgress,
 }
 
 var (
@@ -2418,6 +2422,75 @@ func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusNoContent)
 }
 
+// serveUpdateCheck returns the ClientVersion from Status, which contains
+// information on whether an update is available, and if so, what version,
+// *if* we support auto-updates on this platform. If we don't, this endpoint
+// always returns a ClientVersion saying we're running the newest version.
+// Effectively, it tells us whether serveUpdateInstall will be able to install
+// an update for us.
+func (h *Handler) serveUpdateCheck(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "GET" {
+		http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	_, err := clientupdate.NewUpdater(clientupdate.Arguments{
+		ForAutoUpdate: true,
+	})
+
+	if err != nil {
+		// if we don't support auto-update, just say that we're up to date
+		if errors.Is(err, errors.ErrUnsupported) {
+			json.NewEncoder(w).Encode(tailcfg.ClientVersion{RunningLatest: true})
+		} else {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+		return
+	}
+
+	cv := h.b.StatusWithoutPeers().ClientVersion
+	// ipnstate.Status documentation notes that ClientVersion may be nil on some
+	// platforms where this information is unavailable. In that case, return a
+	// ClientVersion that says we're up to date, since we have no information on
+	// whether an update is possible.
+	if cv == nil {
+		cv = &tailcfg.ClientVersion{RunningLatest: true}
+	}
+
+	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 != "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 != "GET" {
+		http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	ups := h.b.GetSelfUpdateProgress()
+
+	json.NewEncoder(w).Encode(ups)
+}
+
 var (
 	metricInvalidRequests = clientmetric.NewCounter("localapi_invalid_requests")