clientupdate.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. // Copyright (c) Tailscale Inc & contributors
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package clientupdate enables the client update feature.
  4. package clientupdate
  5. import (
  6. "bytes"
  7. "context"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "net/http"
  12. "os"
  13. "os/exec"
  14. "path/filepath"
  15. "runtime"
  16. "strconv"
  17. "strings"
  18. "sync"
  19. "time"
  20. "tailscale.com/clientupdate"
  21. "tailscale.com/envknob"
  22. "tailscale.com/feature"
  23. "tailscale.com/ipn"
  24. "tailscale.com/ipn/ipnext"
  25. "tailscale.com/ipn/ipnlocal"
  26. "tailscale.com/ipn/ipnstate"
  27. "tailscale.com/ipn/localapi"
  28. "tailscale.com/tailcfg"
  29. "tailscale.com/types/logger"
  30. "tailscale.com/util/httpm"
  31. "tailscale.com/version"
  32. "tailscale.com/version/distro"
  33. )
  34. func init() {
  35. ipnext.RegisterExtension("clientupdate", newExt)
  36. // C2N
  37. ipnlocal.RegisterC2N("GET /update", handleC2NUpdateGet)
  38. ipnlocal.RegisterC2N("POST /update", handleC2NUpdatePost)
  39. // LocalAPI:
  40. localapi.Register("update/install", serveUpdateInstall)
  41. localapi.Register("update/progress", serveUpdateProgress)
  42. }
  43. func newExt(logf logger.Logf, sb ipnext.SafeBackend) (ipnext.Extension, error) {
  44. return &extension{
  45. logf: logf,
  46. sb: sb,
  47. lastSelfUpdateState: ipnstate.UpdateFinished,
  48. }, nil
  49. }
  50. type extension struct {
  51. logf logger.Logf
  52. sb ipnext.SafeBackend
  53. mu sync.Mutex
  54. // c2nUpdateStatus is the status of c2n-triggered client update.
  55. c2nUpdateStatus updateStatus
  56. prefs ipn.PrefsView
  57. state ipn.State
  58. lastSelfUpdateState ipnstate.SelfUpdateStatus
  59. selfUpdateProgress []ipnstate.UpdateProgress
  60. // offlineAutoUpdateCancel stops offline auto-updates when called. It
  61. // should be used via stopOfflineAutoUpdate and
  62. // maybeStartOfflineAutoUpdate. It is nil when offline auto-updates are
  63. // not running.
  64. //
  65. //lint:ignore U1000 only used in Linux and Windows builds in autoupdate.go
  66. offlineAutoUpdateCancel func()
  67. }
  68. func (e *extension) Name() string { return "clientupdate" }
  69. func (e *extension) Init(h ipnext.Host) error {
  70. h.Hooks().ProfileStateChange.Add(e.onChangeProfile)
  71. h.Hooks().BackendStateChange.Add(e.onBackendStateChange)
  72. // TODO(nickkhyl): remove this after the profileManager refactoring.
  73. // See tailscale/tailscale#15974.
  74. // This same workaround appears in feature/portlist/portlist.go.
  75. profile, prefs := h.Profiles().CurrentProfileState()
  76. e.onChangeProfile(profile, prefs, false)
  77. return nil
  78. }
  79. func (e *extension) Shutdown() error {
  80. e.stopOfflineAutoUpdate()
  81. return nil
  82. }
  83. func (e *extension) onBackendStateChange(newState ipn.State) {
  84. e.mu.Lock()
  85. defer e.mu.Unlock()
  86. e.state = newState
  87. e.updateOfflineAutoUpdateLocked()
  88. }
  89. func (e *extension) onChangeProfile(profile ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) {
  90. e.mu.Lock()
  91. defer e.mu.Unlock()
  92. e.prefs = prefs
  93. e.updateOfflineAutoUpdateLocked()
  94. }
  95. func (e *extension) updateOfflineAutoUpdateLocked() {
  96. want := e.prefs.Valid() && e.prefs.AutoUpdate().Apply.EqualBool(true) &&
  97. e.state != ipn.Running && e.state != ipn.Starting
  98. cur := e.offlineAutoUpdateCancel != nil
  99. if want && !cur {
  100. e.maybeStartOfflineAutoUpdateLocked(e.prefs)
  101. } else if !want && cur {
  102. e.stopOfflineAutoUpdateLocked()
  103. }
  104. }
  105. type updateStatus struct {
  106. started bool
  107. }
  108. func (e *extension) clearSelfUpdateProgress() {
  109. e.mu.Lock()
  110. defer e.mu.Unlock()
  111. e.selfUpdateProgress = make([]ipnstate.UpdateProgress, 0)
  112. e.lastSelfUpdateState = ipnstate.UpdateFinished
  113. }
  114. func (e *extension) GetSelfUpdateProgress() []ipnstate.UpdateProgress {
  115. e.mu.Lock()
  116. defer e.mu.Unlock()
  117. res := make([]ipnstate.UpdateProgress, len(e.selfUpdateProgress))
  118. copy(res, e.selfUpdateProgress)
  119. return res
  120. }
  121. func (e *extension) DoSelfUpdate() {
  122. e.mu.Lock()
  123. updateState := e.lastSelfUpdateState
  124. e.mu.Unlock()
  125. // don't start an update if one is already in progress
  126. if updateState == ipnstate.UpdateInProgress {
  127. return
  128. }
  129. e.clearSelfUpdateProgress()
  130. e.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateInProgress, ""))
  131. up, err := clientupdate.NewUpdater(clientupdate.Arguments{
  132. Logf: func(format string, args ...any) {
  133. e.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateInProgress, fmt.Sprintf(format, args...)))
  134. },
  135. })
  136. if err != nil {
  137. e.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFailed, err.Error()))
  138. }
  139. err = up.Update()
  140. if err != nil {
  141. e.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFailed, err.Error()))
  142. } else {
  143. e.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFinished, "tailscaled did not restart; please restart Tailscale manually."))
  144. }
  145. }
  146. // serveUpdateInstall sends a request to the LocalBackend to start a Tailscale
  147. // self-update. A successful response does not indicate whether the update
  148. // succeeded, only that the request was accepted. Clients should use
  149. // serveUpdateProgress after pinging this endpoint to check how the update is
  150. // going.
  151. func serveUpdateInstall(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
  152. if r.Method != httpm.POST {
  153. http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
  154. return
  155. }
  156. b := h.LocalBackend()
  157. ext, ok := ipnlocal.GetExt[*extension](b)
  158. if !ok {
  159. http.Error(w, "clientupdate extension not found", http.StatusInternalServerError)
  160. return
  161. }
  162. w.WriteHeader(http.StatusAccepted)
  163. go ext.DoSelfUpdate()
  164. }
  165. // serveUpdateProgress returns the status of an in-progress Tailscale self-update.
  166. // This is provided as a slice of ipnstate.UpdateProgress structs with various
  167. // log messages in order from oldest to newest. If an update is not in progress,
  168. // the returned slice will be empty.
  169. func serveUpdateProgress(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
  170. if r.Method != httpm.GET {
  171. http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
  172. return
  173. }
  174. b := h.LocalBackend()
  175. ext, ok := ipnlocal.GetExt[*extension](b)
  176. if !ok {
  177. http.Error(w, "clientupdate extension not found", http.StatusInternalServerError)
  178. return
  179. }
  180. ups := ext.GetSelfUpdateProgress()
  181. json.NewEncoder(w).Encode(ups)
  182. }
  183. func (e *extension) pushSelfUpdateProgress(up ipnstate.UpdateProgress) {
  184. e.mu.Lock()
  185. defer e.mu.Unlock()
  186. e.selfUpdateProgress = append(e.selfUpdateProgress, up)
  187. e.lastSelfUpdateState = up.Status
  188. }
  189. func handleC2NUpdateGet(b *ipnlocal.LocalBackend, w http.ResponseWriter, r *http.Request) {
  190. e, ok := ipnlocal.GetExt[*extension](b)
  191. if !ok {
  192. http.Error(w, "clientupdate extension not found", http.StatusInternalServerError)
  193. return
  194. }
  195. e.logf("c2n: GET /update received")
  196. res := e.newC2NUpdateResponse()
  197. res.Started = e.c2nUpdateStarted()
  198. w.Header().Set("Content-Type", "application/json")
  199. json.NewEncoder(w).Encode(res)
  200. }
  201. func handleC2NUpdatePost(b *ipnlocal.LocalBackend, w http.ResponseWriter, r *http.Request) {
  202. e, ok := ipnlocal.GetExt[*extension](b)
  203. if !ok {
  204. http.Error(w, "clientupdate extension not found", http.StatusInternalServerError)
  205. return
  206. }
  207. e.logf("c2n: POST /update received")
  208. res := e.newC2NUpdateResponse()
  209. defer func() {
  210. if res.Err != "" {
  211. e.logf("c2n: POST /update failed: %s", res.Err)
  212. }
  213. w.Header().Set("Content-Type", "application/json")
  214. json.NewEncoder(w).Encode(res)
  215. }()
  216. if !res.Enabled {
  217. res.Err = "not enabled"
  218. return
  219. }
  220. if !res.Supported {
  221. res.Err = "not supported"
  222. return
  223. }
  224. // Do not update if we have active inbound SSH connections. Control can set
  225. // force=true query parameter to override this.
  226. if r.FormValue("force") != "true" && b.ActiveSSHConns() > 0 {
  227. res.Err = "not updating due to active SSH connections"
  228. return
  229. }
  230. if err := e.startAutoUpdate("c2n"); err != nil {
  231. res.Err = err.Error()
  232. return
  233. }
  234. res.Started = true
  235. }
  236. func (e *extension) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
  237. e.mu.Lock()
  238. defer e.mu.Unlock()
  239. // If NewUpdater does not return an error, we can update the installation.
  240. //
  241. // Note that we create the Updater solely to check for errors; we do not
  242. // invoke it here. For this purpose, it is ok to pass it a zero Arguments.
  243. var upPref ipn.AutoUpdatePrefs
  244. if e.prefs.Valid() {
  245. upPref = e.prefs.AutoUpdate()
  246. }
  247. return tailcfg.C2NUpdateResponse{
  248. Enabled: envknob.AllowsRemoteUpdate() || upPref.Apply.EqualBool(true),
  249. Supported: feature.CanAutoUpdate() && !version.IsMacSysExt(),
  250. }
  251. }
  252. func (e *extension) c2nUpdateStarted() bool {
  253. e.mu.Lock()
  254. defer e.mu.Unlock()
  255. return e.c2nUpdateStatus.started
  256. }
  257. func (e *extension) setC2NUpdateStarted(v bool) {
  258. e.mu.Lock()
  259. defer e.mu.Unlock()
  260. e.c2nUpdateStatus.started = v
  261. }
  262. func (e *extension) trySetC2NUpdateStarted() bool {
  263. e.mu.Lock()
  264. defer e.mu.Unlock()
  265. if e.c2nUpdateStatus.started {
  266. return false
  267. }
  268. e.c2nUpdateStatus.started = true
  269. return true
  270. }
  271. // findCmdTailscale looks for the cmd/tailscale that corresponds to the
  272. // currently running cmd/tailscaled. It's up to the caller to verify that the
  273. // two match, but this function does its best to find the right one. Notably, it
  274. // doesn't use $PATH for security reasons.
  275. func findCmdTailscale() (string, error) {
  276. self, err := os.Executable()
  277. if err != nil {
  278. return "", err
  279. }
  280. var ts string
  281. switch runtime.GOOS {
  282. case "linux":
  283. if self == "/usr/sbin/tailscaled" || self == "/usr/bin/tailscaled" {
  284. ts = "/usr/bin/tailscale"
  285. }
  286. if self == "/usr/local/sbin/tailscaled" || self == "/usr/local/bin/tailscaled" {
  287. ts = "/usr/local/bin/tailscale"
  288. }
  289. switch distro.Get() {
  290. case distro.QNAP:
  291. // The volume under /share/ where qpkg are installed is not
  292. // predictable. But the rest of the path is.
  293. ok, err := filepath.Match("/share/*/.qpkg/Tailscale/tailscaled", self)
  294. if err == nil && ok {
  295. ts = filepath.Join(filepath.Dir(self), "tailscale")
  296. }
  297. case distro.Unraid:
  298. if self == "/usr/local/emhttp/plugins/tailscale/bin/tailscaled" {
  299. ts = "/usr/local/emhttp/plugins/tailscale/bin/tailscale"
  300. }
  301. }
  302. case "windows":
  303. ts = filepath.Join(filepath.Dir(self), "tailscale.exe")
  304. case "freebsd", "openbsd":
  305. if self == "/usr/local/bin/tailscaled" {
  306. ts = "/usr/local/bin/tailscale"
  307. }
  308. default:
  309. return "", fmt.Errorf("unsupported OS %v", runtime.GOOS)
  310. }
  311. if ts != "" && regularFileExists(ts) {
  312. return ts, nil
  313. }
  314. return "", errors.New("tailscale executable not found in expected place")
  315. }
  316. func tailscaleUpdateCmd(cmdTS string) *exec.Cmd {
  317. defaultCmd := exec.Command(cmdTS, "update", "--yes")
  318. if runtime.GOOS != "linux" {
  319. return defaultCmd
  320. }
  321. if _, err := exec.LookPath("systemd-run"); err != nil {
  322. return defaultCmd
  323. }
  324. // When systemd-run is available, use it to run the update command. This
  325. // creates a new temporary unit separate from the tailscaled unit. When
  326. // tailscaled is restarted during the update, systemd won't kill this
  327. // temporary update unit, which could cause unexpected breakage.
  328. //
  329. // We want to use a few optional flags:
  330. // * --wait, to block the update command until completion (added in systemd 232)
  331. // * --pipe, to collect stdout/stderr (added in systemd 235)
  332. // * --collect, to clean up failed runs from memory (added in systemd 236)
  333. //
  334. // We need to check the version of systemd to figure out if those flags are
  335. // available.
  336. //
  337. // The output will look like:
  338. //
  339. // systemd 255 (255.7-1-arch)
  340. // +PAM +AUDIT ... other feature flags ...
  341. systemdVerOut, err := exec.Command("systemd-run", "--version").Output()
  342. if err != nil {
  343. return defaultCmd
  344. }
  345. parts := strings.Fields(string(systemdVerOut))
  346. if len(parts) < 2 || parts[0] != "systemd" {
  347. return defaultCmd
  348. }
  349. systemdVer, err := strconv.Atoi(parts[1])
  350. if err != nil {
  351. return defaultCmd
  352. }
  353. if systemdVer >= 236 {
  354. return exec.Command("systemd-run", "--wait", "--pipe", "--collect", cmdTS, "update", "--yes")
  355. } else if systemdVer >= 235 {
  356. return exec.Command("systemd-run", "--wait", "--pipe", cmdTS, "update", "--yes")
  357. } else if systemdVer >= 232 {
  358. return exec.Command("systemd-run", "--wait", cmdTS, "update", "--yes")
  359. } else {
  360. return exec.Command("systemd-run", cmdTS, "update", "--yes")
  361. }
  362. }
  363. func regularFileExists(path string) bool {
  364. fi, err := os.Stat(path)
  365. return err == nil && fi.Mode().IsRegular()
  366. }
  367. // startAutoUpdate triggers an auto-update attempt. The actual update happens
  368. // asynchronously. If another update is in progress, an error is returned.
  369. func (e *extension) startAutoUpdate(logPrefix string) (retErr error) {
  370. // Check if update was already started, and mark as started.
  371. if !e.trySetC2NUpdateStarted() {
  372. return errors.New("update already started")
  373. }
  374. defer func() {
  375. // Clear the started flag if something failed.
  376. if retErr != nil {
  377. e.setC2NUpdateStarted(false)
  378. }
  379. }()
  380. cmdTS, err := findCmdTailscale()
  381. if err != nil {
  382. return fmt.Errorf("failed to find cmd/tailscale binary: %w", err)
  383. }
  384. var ver struct {
  385. Long string `json:"long"`
  386. }
  387. out, err := exec.Command(cmdTS, "version", "--json").Output()
  388. if err != nil {
  389. return fmt.Errorf("failed to find cmd/tailscale binary: %w", err)
  390. }
  391. if err := json.Unmarshal(out, &ver); err != nil {
  392. return fmt.Errorf("invalid JSON from cmd/tailscale version --json: %w", err)
  393. }
  394. if ver.Long != version.Long() {
  395. return fmt.Errorf("cmd/tailscale version %q does not match tailscaled version %q", ver.Long, version.Long())
  396. }
  397. cmd := tailscaleUpdateCmd(cmdTS)
  398. buf := new(bytes.Buffer)
  399. cmd.Stdout = buf
  400. cmd.Stderr = buf
  401. e.logf("%s: running %q", logPrefix, strings.Join(cmd.Args, " "))
  402. if err := cmd.Start(); err != nil {
  403. return fmt.Errorf("failed to start cmd/tailscale update: %w", err)
  404. }
  405. go func() {
  406. if err := cmd.Wait(); err != nil {
  407. e.logf("%s: update command failed: %v, output: %s", logPrefix, err, buf)
  408. } else {
  409. e.logf("%s: update attempt complete", logPrefix)
  410. }
  411. e.setC2NUpdateStarted(false)
  412. }()
  413. return nil
  414. }
  415. func (e *extension) stopOfflineAutoUpdate() {
  416. e.mu.Lock()
  417. defer e.mu.Unlock()
  418. e.stopOfflineAutoUpdateLocked()
  419. }
  420. func (e *extension) stopOfflineAutoUpdateLocked() {
  421. if e.offlineAutoUpdateCancel == nil {
  422. return
  423. }
  424. e.logf("offline auto-update: stopping update checks")
  425. e.offlineAutoUpdateCancel()
  426. e.offlineAutoUpdateCancel = nil
  427. }
  428. // e.mu must be held
  429. func (e *extension) maybeStartOfflineAutoUpdateLocked(prefs ipn.PrefsView) {
  430. if !prefs.Valid() || !prefs.AutoUpdate().Apply.EqualBool(true) {
  431. return
  432. }
  433. // AutoUpdate.Apply field in prefs can only be true for platforms that
  434. // support auto-updates. But check it here again, just in case.
  435. if !feature.CanAutoUpdate() {
  436. return
  437. }
  438. // On macsys, auto-updates are managed by Sparkle.
  439. if version.IsMacSysExt() {
  440. return
  441. }
  442. if e.offlineAutoUpdateCancel != nil {
  443. // Already running.
  444. return
  445. }
  446. ctx, cancel := context.WithCancel(context.Background())
  447. e.offlineAutoUpdateCancel = cancel
  448. e.logf("offline auto-update: starting update checks")
  449. go e.offlineAutoUpdate(ctx)
  450. }
  451. const offlineAutoUpdateCheckPeriod = time.Hour
  452. func (e *extension) offlineAutoUpdate(ctx context.Context) {
  453. t := time.NewTicker(offlineAutoUpdateCheckPeriod)
  454. defer t.Stop()
  455. for {
  456. select {
  457. case <-ctx.Done():
  458. return
  459. case <-t.C:
  460. }
  461. if err := e.startAutoUpdate("offline auto-update"); err != nil {
  462. e.logf("offline auto-update: failed: %v", err)
  463. }
  464. }
  465. }