clientupdate.go 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package clientupdate implements tailscale client update for all supported
  4. // platforms. This package can be used from both tailscaled and tailscale
  5. // binaries.
  6. package clientupdate
  7. import (
  8. "archive/tar"
  9. "bufio"
  10. "bytes"
  11. "compress/gzip"
  12. "context"
  13. "encoding/json"
  14. "errors"
  15. "fmt"
  16. "io"
  17. "maps"
  18. "net/http"
  19. "os"
  20. "os/exec"
  21. "path"
  22. "path/filepath"
  23. "regexp"
  24. "runtime"
  25. "strconv"
  26. "strings"
  27. "github.com/google/uuid"
  28. "tailscale.com/clientupdate/distsign"
  29. "tailscale.com/types/logger"
  30. "tailscale.com/util/cmpver"
  31. "tailscale.com/util/winutil"
  32. "tailscale.com/version"
  33. "tailscale.com/version/distro"
  34. )
  35. const (
  36. CurrentTrack = ""
  37. StableTrack = "stable"
  38. UnstableTrack = "unstable"
  39. )
  40. func versionToTrack(v string) (string, error) {
  41. _, rest, ok := strings.Cut(v, ".")
  42. if !ok {
  43. return "", fmt.Errorf("malformed version %q", v)
  44. }
  45. minorStr, _, ok := strings.Cut(rest, ".")
  46. if !ok {
  47. return "", fmt.Errorf("malformed version %q", v)
  48. }
  49. minor, err := strconv.Atoi(minorStr)
  50. if err != nil {
  51. return "", fmt.Errorf("malformed version %q", v)
  52. }
  53. if minor%2 == 0 {
  54. return "stable", nil
  55. }
  56. return "unstable", nil
  57. }
  58. // Arguments contains arguments needed to run an update.
  59. type Arguments struct {
  60. // Version can be a specific version number or one of the predefined track
  61. // constants:
  62. //
  63. // - CurrentTrack will use the latest version from the same track as the
  64. // running binary
  65. // - StableTrack and UnstableTrack will use the latest versions of the
  66. // corresponding tracks
  67. //
  68. // Leaving this empty is the same as using CurrentTrack.
  69. Version string
  70. // Logf is a logger for update progress messages.
  71. Logf logger.Logf
  72. // Stdout and Stderr should be used for output instead of os.Stdout and
  73. // os.Stderr.
  74. Stdout io.Writer
  75. Stderr io.Writer
  76. // Confirm is called when a new version is available and should return true
  77. // if this new version should be installed. When Confirm returns false, the
  78. // update is aborted.
  79. Confirm func(newVer string) bool
  80. // PkgsAddr is the address of the pkgs server to fetch updates from.
  81. // Defaults to "https://pkgs.tailscale.com".
  82. PkgsAddr string
  83. // ForAutoUpdate should be true when Updater is created in auto-update
  84. // context. When true, NewUpdater returns an error if it cannot be used for
  85. // auto-updates (even if Updater.Update field is non-nil).
  86. ForAutoUpdate bool
  87. }
  88. func (args Arguments) validate() error {
  89. if args.Confirm == nil {
  90. return errors.New("missing Confirm callback in Arguments")
  91. }
  92. if args.Logf == nil {
  93. return errors.New("missing Logf callback in Arguments")
  94. }
  95. return nil
  96. }
  97. type Updater struct {
  98. Arguments
  99. track string
  100. // Update is a platform-specific method that updates the installation. May be
  101. // nil (not all platforms support updates from within Tailscale).
  102. Update func() error
  103. }
  104. func NewUpdater(args Arguments) (*Updater, error) {
  105. up := Updater{
  106. Arguments: args,
  107. }
  108. if up.Stdout == nil {
  109. up.Stdout = os.Stdout
  110. }
  111. if up.Stderr == nil {
  112. up.Stderr = os.Stderr
  113. }
  114. var canAutoUpdate bool
  115. up.Update, canAutoUpdate = up.getUpdateFunction()
  116. if up.Update == nil {
  117. return nil, errors.ErrUnsupported
  118. }
  119. if args.ForAutoUpdate && !canAutoUpdate {
  120. return nil, errors.ErrUnsupported
  121. }
  122. switch up.Version {
  123. case StableTrack, UnstableTrack:
  124. up.track = up.Version
  125. case CurrentTrack:
  126. if version.IsUnstableBuild() {
  127. up.track = UnstableTrack
  128. } else {
  129. up.track = StableTrack
  130. }
  131. default:
  132. var err error
  133. up.track, err = versionToTrack(args.Version)
  134. if err != nil {
  135. return nil, err
  136. }
  137. }
  138. if up.Arguments.PkgsAddr == "" {
  139. up.Arguments.PkgsAddr = "https://pkgs.tailscale.com"
  140. }
  141. return &up, nil
  142. }
  143. type updateFunction func() error
  144. func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
  145. switch runtime.GOOS {
  146. case "windows":
  147. return up.updateWindows, true
  148. case "linux":
  149. switch distro.Get() {
  150. case distro.Synology:
  151. // Synology updates use our own pkgs.tailscale.com instead of the
  152. // Synology Package Center. We should eventually get to a regular
  153. // release cadence with Synology Package Center and use their
  154. // auto-update mechanism.
  155. return up.updateSynology, false
  156. case distro.Debian: // includes Ubuntu
  157. return up.updateDebLike, true
  158. case distro.Arch:
  159. if up.archPackageInstalled() {
  160. // Arch update func just prints a message about how to update,
  161. // it doesn't support auto-updates.
  162. return up.updateArchLike, false
  163. }
  164. return up.updateLinuxBinary, true
  165. case distro.Alpine:
  166. return up.updateAlpineLike, true
  167. case distro.Unraid:
  168. // Unraid runs from memory, updates must be installed via the Unraid
  169. // plugin manager to be persistent.
  170. // TODO(awly): implement Unraid updates using the 'plugin' CLI.
  171. return nil, false
  172. case distro.QNAP:
  173. return up.updateQNAP, true
  174. }
  175. switch {
  176. case haveExecutable("pacman"):
  177. if up.archPackageInstalled() {
  178. // Arch update func just prints a message about how to update,
  179. // it doesn't support auto-updates.
  180. return up.updateArchLike, false
  181. }
  182. return up.updateLinuxBinary, true
  183. case haveExecutable("apt-get"): // TODO(awly): add support for "apt"
  184. // The distro.Debian switch case above should catch most apt-based
  185. // systems, but add this fallback just in case.
  186. return up.updateDebLike, true
  187. case haveExecutable("dnf"):
  188. return up.updateFedoraLike("dnf"), true
  189. case haveExecutable("yum"):
  190. return up.updateFedoraLike("yum"), true
  191. case haveExecutable("apk"):
  192. return up.updateAlpineLike, true
  193. }
  194. // If nothing matched, fall back to tarball updates.
  195. if up.Update == nil {
  196. return up.updateLinuxBinary, true
  197. }
  198. case "darwin":
  199. switch {
  200. case version.IsMacAppStore():
  201. // App store update func just opens the store page, it doesn't
  202. // support auto-updates.
  203. return up.updateMacAppStore, false
  204. case version.IsMacSysExt():
  205. // Macsys update func kicks off Sparkle. Auto-updates are done by
  206. // Sparkle.
  207. return up.updateMacSys, false
  208. default:
  209. return nil, false
  210. }
  211. case "freebsd":
  212. return up.updateFreeBSD, true
  213. }
  214. return nil, false
  215. }
  216. // Update runs a single update attempt using the platform-specific mechanism.
  217. //
  218. // On Windows, this copies the calling binary and re-executes it to apply the
  219. // update. The calling binary should handle an "update" subcommand and call
  220. // this function again for the re-executed binary to proceed.
  221. func Update(args Arguments) error {
  222. if err := args.validate(); err != nil {
  223. return err
  224. }
  225. up, err := NewUpdater(args)
  226. if err != nil {
  227. return err
  228. }
  229. return up.Update()
  230. }
  231. func (up *Updater) confirm(ver string) bool {
  232. switch cmpver.Compare(version.Short(), ver) {
  233. case 0:
  234. up.Logf("already running %v version %v; no update needed", up.track, ver)
  235. return false
  236. case 1:
  237. up.Logf("installed %v version %v is newer than the latest available version %v; no update needed", up.track, version.Short(), ver)
  238. return false
  239. }
  240. if up.Confirm != nil {
  241. return up.Confirm(ver)
  242. }
  243. return true
  244. }
  245. const synoinfoConfPath = "/etc/synoinfo.conf"
  246. func (up *Updater) updateSynology() error {
  247. if up.Version != "" {
  248. return errors.New("installing a specific version on Synology is not supported")
  249. }
  250. if err := requireRoot(); err != nil {
  251. return err
  252. }
  253. // Get the latest version and list of SPKs from pkgs.tailscale.com.
  254. dsmVersion := distro.DSMVersion()
  255. osName := fmt.Sprintf("dsm%d", dsmVersion)
  256. arch, err := synoArch(runtime.GOARCH, synoinfoConfPath)
  257. if err != nil {
  258. return err
  259. }
  260. latest, err := latestPackages(up.track)
  261. if err != nil {
  262. return err
  263. }
  264. spkName := latest.SPKs[osName][arch]
  265. if spkName == "" {
  266. return fmt.Errorf("cannot find Synology package for os=%s arch=%s, please report a bug with your device model", osName, arch)
  267. }
  268. if !up.confirm(latest.SPKsVersion) {
  269. return nil
  270. }
  271. up.cleanupOldDownloads(filepath.Join(os.TempDir(), "tailscale-update*", "*.spk"))
  272. // Download the SPK into a temporary directory.
  273. spkDir, err := os.MkdirTemp("", "tailscale-update")
  274. if err != nil {
  275. return err
  276. }
  277. pkgsPath := fmt.Sprintf("%s/%s", up.track, spkName)
  278. spkPath := filepath.Join(spkDir, path.Base(pkgsPath))
  279. if err := up.downloadURLToFile(pkgsPath, spkPath); err != nil {
  280. return err
  281. }
  282. // Install the SPK. Run via nohup to allow install to succeed when we're
  283. // connected over tailscale ssh and this parent process dies. Otherwise, if
  284. // you abort synopkg install mid-way, tailscaled is not restarted.
  285. cmd := exec.Command("nohup", "synopkg", "install", spkPath)
  286. // Don't attach cmd.Stdout to Stdout because nohup will redirect that into
  287. // nohup.out file. synopkg doesn't have any progress output anyway, it just
  288. // spits out a JSON result when done.
  289. out, err := cmd.CombinedOutput()
  290. if err != nil {
  291. if dsmVersion == 6 && bytes.Contains(out, []byte("error = [290]")) {
  292. return fmt.Errorf("synopkg install failed: %w\noutput:\n%s\nplease make sure that packages from 'Any publisher' are allowed in the Package Center (Package Center -> Settings -> Trust Level -> Any publisher)", err, out)
  293. }
  294. return fmt.Errorf("synopkg install failed: %w\noutput:\n%s", err, out)
  295. }
  296. if dsmVersion == 6 {
  297. // DSM6 does not automatically restart the package on install. Do it
  298. // manually.
  299. cmd := exec.Command("nohup", "synopkg", "start", "Tailscale")
  300. out, err := cmd.CombinedOutput()
  301. if err != nil {
  302. return fmt.Errorf("synopkg start failed: %w\noutput:\n%s", err, out)
  303. }
  304. }
  305. return nil
  306. }
  307. // synoArch returns the Synology CPU architecture matching one of the SPK
  308. // architectures served from pkgs.tailscale.com.
  309. func synoArch(goArch, synoinfoPath string) (string, error) {
  310. // Most Synology boxes just use a different arch name from GOARCH.
  311. arch := map[string]string{
  312. "amd64": "x86_64",
  313. "386": "i686",
  314. "arm64": "armv8",
  315. }[goArch]
  316. if arch == "" {
  317. // Here's the fun part, some older ARM boxes require you to use SPKs
  318. // specifically for their CPU. See
  319. // https://github.com/SynoCommunity/spksrc/wiki/Synology-and-SynoCommunity-Package-Architectures
  320. // for a complete list.
  321. //
  322. // Some CPUs will map to neither this list nor the goArch map above, and we
  323. // don't have SPKs for them.
  324. cpu, err := parseSynoinfo(synoinfoPath)
  325. if err != nil {
  326. return "", fmt.Errorf("failed to get CPU architecture: %w", err)
  327. }
  328. switch cpu {
  329. case "88f6281", "88f6282", "hi3535", "alpine", "armada370",
  330. "armada375", "armada38x", "armadaxp", "comcerto2k", "monaco":
  331. arch = cpu
  332. default:
  333. return "", fmt.Errorf("unsupported Synology CPU architecture %q (Go arch %q), please report a bug at https://github.com/tailscale/tailscale/issues/new/choose", cpu, goArch)
  334. }
  335. }
  336. return arch, nil
  337. }
  338. func parseSynoinfo(path string) (string, error) {
  339. f, err := os.Open(path)
  340. if err != nil {
  341. return "", err
  342. }
  343. defer f.Close()
  344. // Look for a line like:
  345. // unique="synology_88f6282_413j"
  346. // Extract the CPU in the middle (88f6282 in the above example).
  347. s := bufio.NewScanner(f)
  348. for s.Scan() {
  349. l := s.Text()
  350. if !strings.HasPrefix(l, "unique=") {
  351. continue
  352. }
  353. parts := strings.SplitN(l, "_", 3)
  354. if len(parts) != 3 {
  355. return "", fmt.Errorf(`malformed %q: found %q, expected format like 'unique="synology_$cpu_$model'`, path, l)
  356. }
  357. return parts[1], nil
  358. }
  359. return "", fmt.Errorf(`missing "unique=" field in %q`, path)
  360. }
  361. func (up *Updater) updateDebLike() error {
  362. if err := requireRoot(); err != nil {
  363. return err
  364. }
  365. if err := exec.Command("dpkg", "--status", "tailscale").Run(); err != nil && isExitError(err) {
  366. // Tailscale was not installed via apt, update via tarball download
  367. // instead.
  368. return up.updateLinuxBinary()
  369. }
  370. ver, err := requestedTailscaleVersion(up.Version, up.track)
  371. if err != nil {
  372. return err
  373. }
  374. if !up.confirm(ver) {
  375. return nil
  376. }
  377. if updated, err := updateDebianAptSourcesList(up.track); err != nil {
  378. return err
  379. } else if updated {
  380. up.Logf("Updated %s to use the %s track", aptSourcesFile, up.track)
  381. }
  382. cmd := exec.Command("apt-get", "update",
  383. // Only update the tailscale repo, not the other ones, treating
  384. // the tailscale.list file as the main "sources.list" file.
  385. "-o", "Dir::Etc::SourceList=sources.list.d/tailscale.list",
  386. // Disable the "sources.list.d" directory:
  387. "-o", "Dir::Etc::SourceParts=-",
  388. // Don't forget about packages in the other repos just because
  389. // we're not updating them:
  390. "-o", "APT::Get::List-Cleanup=0",
  391. )
  392. if out, err := cmd.CombinedOutput(); err != nil {
  393. return fmt.Errorf("apt-get update failed: %w; output:\n%s", err, out)
  394. }
  395. for i := 0; i < 2; i++ {
  396. out, err := exec.Command("apt-get", "install", "--yes", "--allow-downgrades", "tailscale="+ver).CombinedOutput()
  397. if err != nil {
  398. if !bytes.Contains(out, []byte(`dpkg was interrupted`)) {
  399. return fmt.Errorf("apt-get install failed: %w; output:\n%s", err, out)
  400. }
  401. up.Logf("apt-get install failed: %s; output:\n%s", err, out)
  402. up.Logf("running dpkg --configure tailscale")
  403. out, err = exec.Command("dpkg", "--force-confdef,downgrade", "--configure", "tailscale").CombinedOutput()
  404. if err != nil {
  405. return fmt.Errorf("dpkg --configure tailscale failed: %w; output:\n%s", err, out)
  406. }
  407. continue
  408. }
  409. break
  410. }
  411. return nil
  412. }
  413. const aptSourcesFile = "/etc/apt/sources.list.d/tailscale.list"
  414. // updateDebianAptSourcesList updates the /etc/apt/sources.list.d/tailscale.list
  415. // file to make sure it has the provided track (stable or unstable) in it.
  416. //
  417. // If it already has the right track (including containing both stable and
  418. // unstable), it does nothing.
  419. func updateDebianAptSourcesList(dstTrack string) (rewrote bool, err error) {
  420. was, err := os.ReadFile(aptSourcesFile)
  421. if err != nil {
  422. return false, err
  423. }
  424. newContent, err := updateDebianAptSourcesListBytes(was, dstTrack)
  425. if err != nil {
  426. return false, err
  427. }
  428. if bytes.Equal(was, newContent) {
  429. return false, nil
  430. }
  431. return true, os.WriteFile(aptSourcesFile, newContent, 0644)
  432. }
  433. func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent []byte, err error) {
  434. trackURLPrefix := []byte("https://pkgs.tailscale.com/" + dstTrack + "/")
  435. var buf bytes.Buffer
  436. var changes int
  437. bs := bufio.NewScanner(bytes.NewReader(was))
  438. hadCorrect := false
  439. commentLine := regexp.MustCompile(`^\s*\#`)
  440. pkgsURL := regexp.MustCompile(`\bhttps://pkgs\.tailscale\.com/((un)?stable)/`)
  441. for bs.Scan() {
  442. line := bs.Bytes()
  443. if !commentLine.Match(line) {
  444. line = pkgsURL.ReplaceAllFunc(line, func(m []byte) []byte {
  445. if bytes.Equal(m, trackURLPrefix) {
  446. hadCorrect = true
  447. } else {
  448. changes++
  449. }
  450. return trackURLPrefix
  451. })
  452. }
  453. buf.Write(line)
  454. buf.WriteByte('\n')
  455. }
  456. if hadCorrect || (changes == 1 && bytes.Equal(bytes.TrimSpace(was), bytes.TrimSpace(buf.Bytes()))) {
  457. // Unchanged or close enough.
  458. return was, nil
  459. }
  460. if changes != 1 {
  461. // No changes, or an unexpected number of changes (what?). Bail.
  462. // They probably editted it by hand and we don't know what to do.
  463. return nil, fmt.Errorf("unexpected/unsupported %s contents", aptSourcesFile)
  464. }
  465. return buf.Bytes(), nil
  466. }
  467. func (up *Updater) archPackageInstalled() bool {
  468. err := exec.Command("pacman", "--query", "tailscale").Run()
  469. return err == nil
  470. }
  471. func (up *Updater) updateArchLike() error {
  472. // Arch maintainer asked us not to implement "tailscale update" or
  473. // auto-updates on Arch-based distros:
  474. // https://github.com/tailscale/tailscale/issues/6995#issuecomment-1687080106
  475. return errors.New(`individual package updates are not supported on Arch-based distros, only full-system updates are: https://wiki.archlinux.org/title/System_maintenance#Partial_upgrades_are_unsupported.
  476. you can use "pacman --sync --refresh --sysupgrade" or "pacman -Syu" to upgrade the system, including Tailscale.`)
  477. }
  478. const yumRepoConfigFile = "/etc/yum.repos.d/tailscale.repo"
  479. // updateFedoraLike updates tailscale on any distros in the Fedora family,
  480. // specifically anything that uses "dnf" or "yum" package managers. The actual
  481. // package manager is passed via packageManager.
  482. func (up *Updater) updateFedoraLike(packageManager string) func() error {
  483. return func() (err error) {
  484. if err := requireRoot(); err != nil {
  485. return err
  486. }
  487. if err := exec.Command(packageManager, "info", "--installed", "tailscale").Run(); err != nil && isExitError(err) {
  488. // Tailscale was not installed via yum/dnf, update via tarball
  489. // download instead.
  490. return up.updateLinuxBinary()
  491. }
  492. defer func() {
  493. if err != nil {
  494. err = fmt.Errorf(`%w; you can try updating using "%s upgrade tailscale"`, err, packageManager)
  495. }
  496. }()
  497. ver, err := requestedTailscaleVersion(up.Version, up.track)
  498. if err != nil {
  499. return err
  500. }
  501. if !up.confirm(ver) {
  502. return nil
  503. }
  504. if updated, err := updateYUMRepoTrack(yumRepoConfigFile, up.track); err != nil {
  505. return err
  506. } else if updated {
  507. up.Logf("Updated %s to use the %s track", yumRepoConfigFile, up.track)
  508. }
  509. cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
  510. cmd.Stdout = up.Stdout
  511. cmd.Stderr = up.Stderr
  512. if err := cmd.Run(); err != nil {
  513. return err
  514. }
  515. return nil
  516. }
  517. }
  518. // updateYUMRepoTrack updates the repoFile file to make sure it has the
  519. // provided track (stable or unstable) in it.
  520. func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) {
  521. was, err := os.ReadFile(repoFile)
  522. if err != nil {
  523. return false, err
  524. }
  525. urlRe := regexp.MustCompile(`^(baseurl|gpgkey)=https://pkgs\.tailscale\.com/(un)?stable/`)
  526. urlReplacement := fmt.Sprintf("$1=https://pkgs.tailscale.com/%s/", dstTrack)
  527. s := bufio.NewScanner(bytes.NewReader(was))
  528. newContent := bytes.NewBuffer(make([]byte, 0, len(was)))
  529. for s.Scan() {
  530. line := s.Text()
  531. // Handle repo section name, like "[tailscale-stable]".
  532. if len(line) > 0 && line[0] == '[' {
  533. if !strings.HasPrefix(line, "[tailscale-") {
  534. return false, fmt.Errorf("%q does not look like a tailscale repo file, it contains an unexpected %q section", repoFile, line)
  535. }
  536. fmt.Fprintf(newContent, "[tailscale-%s]\n", dstTrack)
  537. continue
  538. }
  539. // Update the track mentioned in repo name.
  540. if strings.HasPrefix(line, "name=") {
  541. fmt.Fprintf(newContent, "name=Tailscale %s\n", dstTrack)
  542. continue
  543. }
  544. // Update the actual repo URLs.
  545. if strings.HasPrefix(line, "baseurl=") || strings.HasPrefix(line, "gpgkey=") {
  546. fmt.Fprintln(newContent, urlRe.ReplaceAllString(line, urlReplacement))
  547. continue
  548. }
  549. fmt.Fprintln(newContent, line)
  550. }
  551. if bytes.Equal(was, newContent.Bytes()) {
  552. return false, nil
  553. }
  554. return true, os.WriteFile(repoFile, newContent.Bytes(), 0644)
  555. }
  556. func (up *Updater) updateAlpineLike() (err error) {
  557. if up.Version != "" {
  558. return errors.New("installing a specific version on Alpine-based distros is not supported")
  559. }
  560. if err := requireRoot(); err != nil {
  561. return err
  562. }
  563. if err := exec.Command("apk", "info", "--installed", "tailscale").Run(); err != nil && isExitError(err) {
  564. // Tailscale was not installed via apk, update via tarball download
  565. // instead.
  566. return up.updateLinuxBinary()
  567. }
  568. defer func() {
  569. if err != nil {
  570. err = fmt.Errorf(`%w; you can try updating using "apk upgrade tailscale"`, err)
  571. }
  572. }()
  573. out, err := exec.Command("apk", "update").CombinedOutput()
  574. if err != nil {
  575. return fmt.Errorf("failed refresh apk repository indexes: %w, output:\n%s", err, out)
  576. }
  577. out, err = exec.Command("apk", "info", "tailscale").CombinedOutput()
  578. if err != nil {
  579. return fmt.Errorf("failed checking apk for latest tailscale version: %w, output:\n%s", err, out)
  580. }
  581. ver, err := parseAlpinePackageVersion(out)
  582. if err != nil {
  583. return fmt.Errorf(`failed to parse latest version from "apk info tailscale": %w`, err)
  584. }
  585. if !up.confirm(ver) {
  586. return nil
  587. }
  588. cmd := exec.Command("apk", "upgrade", "tailscale")
  589. cmd.Stdout = up.Stdout
  590. cmd.Stderr = up.Stderr
  591. if err := cmd.Run(); err != nil {
  592. return fmt.Errorf("failed tailscale update using apk: %w", err)
  593. }
  594. return nil
  595. }
  596. func parseAlpinePackageVersion(out []byte) (string, error) {
  597. s := bufio.NewScanner(bytes.NewReader(out))
  598. for s.Scan() {
  599. // The line should look like this:
  600. // tailscale-1.44.2-r0 description:
  601. line := strings.TrimSpace(s.Text())
  602. if !strings.HasPrefix(line, "tailscale-") {
  603. continue
  604. }
  605. parts := strings.SplitN(line, "-", 3)
  606. if len(parts) < 3 {
  607. return "", fmt.Errorf("malformed info line: %q", line)
  608. }
  609. return parts[1], nil
  610. }
  611. return "", errors.New("tailscale version not found in output")
  612. }
  613. func (up *Updater) updateMacSys() error {
  614. return errors.New("NOTREACHED: On MacSys builds, `tailscale update` is handled in Swift to launch the GUI updater")
  615. }
  616. func (up *Updater) updateMacAppStore() error {
  617. // We can't trigger the update via App Store from the sandboxed app. At
  618. // most, we can open the App Store page for them.
  619. up.Logf("Please use the App Store to update Tailscale.\nConsider enabling Automatic Updates in the App Store Settings, if you haven't already.\nOpening the Tailscale app page...")
  620. out, err := exec.Command("open", "https://apps.apple.com/us/app/tailscale/id1475387142").CombinedOutput()
  621. if err != nil {
  622. return fmt.Errorf("can't open the Tailscale page in App Store: %w, output:\n%s", err, string(out))
  623. }
  624. return nil
  625. }
  626. const (
  627. // winMSIEnv is the environment variable that, if set, is the MSI file for
  628. // the update command to install. It's passed like this so we can stop the
  629. // tailscale.exe process from running before the msiexec process runs and
  630. // tries to overwrite ourselves.
  631. winMSIEnv = "TS_UPDATE_WIN_MSI"
  632. // winExePathEnv is the environment variable that is set along with
  633. // winMSIEnv and carries the full path of the calling tailscale.exe binary.
  634. // It is used to re-launch the GUI process (tailscale-ipn.exe) after
  635. // install is complete.
  636. winExePathEnv = "TS_UPDATE_WIN_EXE_PATH"
  637. )
  638. var (
  639. verifyAuthenticode func(string) error // or nil on non-Windows
  640. markTempFileFunc func(string) error // or nil on non-Windows
  641. launchTailscaleAsWinGUIUser func(string) error // or nil on non-Windows
  642. )
  643. func (up *Updater) updateWindows() error {
  644. if msi := os.Getenv(winMSIEnv); msi != "" {
  645. // stdout/stderr from this part of the install could be lost since the
  646. // parent tailscaled is replaced. Create a temp log file to have some
  647. // output to debug with in case update fails.
  648. close, err := up.switchOutputToFile()
  649. if err != nil {
  650. up.Logf("failed to create log file for installation: %v; proceeding with existing outputs", err)
  651. } else {
  652. defer close.Close()
  653. }
  654. up.Logf("installing %v ...", msi)
  655. if err := up.installMSI(msi); err != nil {
  656. up.Logf("MSI install failed: %v", err)
  657. return err
  658. }
  659. up.Logf("relaunching tailscale-ipn.exe...")
  660. exePath := os.Getenv(winExePathEnv)
  661. if exePath == "" {
  662. up.Logf("env var %q not passed to installer binary copy", winExePathEnv)
  663. return fmt.Errorf("env var %q not passed to installer binary copy", winExePathEnv)
  664. }
  665. if err := launchTailscaleAsWinGUIUser(exePath); err != nil {
  666. up.Logf("Failed to re-launch tailscale after update: %v", err)
  667. return err
  668. }
  669. up.Logf("success.")
  670. return nil
  671. }
  672. if !winutil.IsCurrentProcessElevated() {
  673. return errors.New(`update must be run as Administrator
  674. you can run the command prompt as Administrator one of these ways:
  675. * right-click cmd.exe, select 'Run as administrator'
  676. * press Windows+x, then press a
  677. * press Windows+r, type in "cmd", then press Ctrl+Shift+Enter`)
  678. }
  679. ver, err := requestedTailscaleVersion(up.Version, up.track)
  680. if err != nil {
  681. return err
  682. }
  683. arch := runtime.GOARCH
  684. if arch == "386" {
  685. arch = "x86"
  686. }
  687. if !up.confirm(ver) {
  688. return nil
  689. }
  690. tsDir := filepath.Join(os.Getenv("ProgramData"), "Tailscale")
  691. msiDir := filepath.Join(tsDir, "MSICache")
  692. if fi, err := os.Stat(tsDir); err != nil {
  693. return fmt.Errorf("expected %s to exist, got stat error: %w", tsDir, err)
  694. } else if !fi.IsDir() {
  695. return fmt.Errorf("expected %s to be a directory; got %v", tsDir, fi.Mode())
  696. }
  697. if err := os.MkdirAll(msiDir, 0700); err != nil {
  698. return err
  699. }
  700. up.cleanupOldDownloads(filepath.Join(msiDir, "*.msi"))
  701. pkgsPath := fmt.Sprintf("%s/tailscale-setup-%s-%s.msi", up.track, ver, arch)
  702. msiTarget := filepath.Join(msiDir, path.Base(pkgsPath))
  703. if err := up.downloadURLToFile(pkgsPath, msiTarget); err != nil {
  704. return err
  705. }
  706. up.Logf("verifying MSI authenticode...")
  707. if err := verifyAuthenticode(msiTarget); err != nil {
  708. return fmt.Errorf("authenticode verification of %s failed: %w", msiTarget, err)
  709. }
  710. up.Logf("authenticode verification succeeded")
  711. up.Logf("making tailscale.exe copy to switch to...")
  712. selfOrig, selfCopy, err := makeSelfCopy()
  713. if err != nil {
  714. return err
  715. }
  716. defer os.Remove(selfCopy)
  717. up.Logf("running tailscale.exe copy for final install...")
  718. cmd := exec.Command(selfCopy, "update")
  719. cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget, winExePathEnv+"="+selfOrig)
  720. cmd.Stdout = up.Stderr
  721. cmd.Stderr = up.Stderr
  722. cmd.Stdin = os.Stdin
  723. if err := cmd.Start(); err != nil {
  724. return err
  725. }
  726. // Once it's started, exit ourselves, so the binary is free
  727. // to be replaced.
  728. os.Exit(0)
  729. panic("unreachable")
  730. }
  731. func (up *Updater) switchOutputToFile() (io.Closer, error) {
  732. var logFilePath string
  733. exePath, err := os.Executable()
  734. if err != nil {
  735. logFilePath = filepath.Join(os.TempDir(), "tailscale-updater.log")
  736. } else {
  737. logFilePath = strings.TrimSuffix(exePath, ".exe") + ".log"
  738. }
  739. up.Logf("writing update output to %q", logFilePath)
  740. logFile, err := os.Create(logFilePath)
  741. if err != nil {
  742. return nil, err
  743. }
  744. up.Logf = func(m string, args ...any) {
  745. fmt.Fprintf(logFile, m+"\n", args...)
  746. }
  747. up.Stdout = logFile
  748. up.Stderr = logFile
  749. return logFile, nil
  750. }
  751. func (up *Updater) installMSI(msi string) error {
  752. var err error
  753. for tries := 0; tries < 2; tries++ {
  754. // TS_NOLAUNCH: don't automatically launch the app after install.
  755. // We will launch it explicitly as the current GUI user afterwards.
  756. cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn", "TS_NOLAUNCH=true")
  757. cmd.Dir = filepath.Dir(msi)
  758. cmd.Stdout = up.Stdout
  759. cmd.Stderr = up.Stderr
  760. cmd.Stdin = os.Stdin
  761. err = cmd.Run()
  762. if err == nil {
  763. break
  764. }
  765. up.Logf("Install attempt failed: %v", err)
  766. uninstallVersion := version.Short()
  767. if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" {
  768. uninstallVersion = v
  769. }
  770. // Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
  771. up.Logf("Uninstalling current version %q for downgrade...", uninstallVersion)
  772. cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn")
  773. cmd.Stdout = up.Stdout
  774. cmd.Stderr = up.Stderr
  775. cmd.Stdin = os.Stdin
  776. err = cmd.Run()
  777. up.Logf("msiexec uninstall: %v", err)
  778. }
  779. return err
  780. }
  781. // cleanupOldDownloads removes all files matching glob (see filepath.Glob).
  782. // Only regular files are removed, so the glob must match specific files and
  783. // not directories.
  784. func (up *Updater) cleanupOldDownloads(glob string) {
  785. matches, err := filepath.Glob(glob)
  786. if err != nil {
  787. up.Logf("cleaning up old downloads: %v", err)
  788. return
  789. }
  790. for _, m := range matches {
  791. s, err := os.Lstat(m)
  792. if err != nil {
  793. up.Logf("cleaning up old downloads: %v", err)
  794. continue
  795. }
  796. if !s.Mode().IsRegular() {
  797. continue
  798. }
  799. if err := os.Remove(m); err != nil {
  800. up.Logf("cleaning up old downloads: %v", err)
  801. }
  802. }
  803. }
  804. func msiUUIDForVersion(ver string) string {
  805. arch := runtime.GOARCH
  806. if arch == "386" {
  807. arch = "x86"
  808. }
  809. track, err := versionToTrack(ver)
  810. if err != nil {
  811. track = UnstableTrack
  812. }
  813. msiURL := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%s-%s.msi", track, ver, arch)
  814. return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}"
  815. }
  816. func makeSelfCopy() (origPathExe, tmpPathExe string, err error) {
  817. selfExe, err := os.Executable()
  818. if err != nil {
  819. return "", "", err
  820. }
  821. f, err := os.Open(selfExe)
  822. if err != nil {
  823. return "", "", err
  824. }
  825. defer f.Close()
  826. f2, err := os.CreateTemp("", "tailscale-updater-*.exe")
  827. if err != nil {
  828. return "", "", err
  829. }
  830. if f := markTempFileFunc; f != nil {
  831. if err := f(f2.Name()); err != nil {
  832. return "", "", err
  833. }
  834. }
  835. if _, err := io.Copy(f2, f); err != nil {
  836. f2.Close()
  837. return "", "", err
  838. }
  839. return selfExe, f2.Name(), f2.Close()
  840. }
  841. func (up *Updater) downloadURLToFile(pathSrc, fileDst string) (ret error) {
  842. c, err := distsign.NewClient(up.Logf, up.PkgsAddr)
  843. if err != nil {
  844. return err
  845. }
  846. return c.Download(context.Background(), pathSrc, fileDst)
  847. }
  848. func (up *Updater) updateFreeBSD() (err error) {
  849. if up.Version != "" {
  850. return errors.New("installing a specific version on FreeBSD is not supported")
  851. }
  852. if err := requireRoot(); err != nil {
  853. return err
  854. }
  855. if err := exec.Command("pkg", "query", "%n", "tailscale").Run(); err != nil && isExitError(err) {
  856. // Tailscale was not installed via pkg and we don't pre-compile
  857. // binaries for it.
  858. return errors.New("Tailscale was not installed via pkg, binary updates on FreeBSD are not supported; please reinstall Tailscale using pkg or update manually")
  859. }
  860. defer func() {
  861. if err != nil {
  862. err = fmt.Errorf(`%w; you can try updating using "pkg upgrade tailscale"`, err)
  863. }
  864. }()
  865. out, err := exec.Command("pkg", "update").CombinedOutput()
  866. if err != nil {
  867. return fmt.Errorf("failed refresh pkg repository indexes: %w, output:\n%s", err, out)
  868. }
  869. out, err = exec.Command("pkg", "rquery", "%v", "tailscale").CombinedOutput()
  870. if err != nil {
  871. return fmt.Errorf("failed checking pkg for latest tailscale version: %w, output:\n%s", err, out)
  872. }
  873. ver := string(bytes.TrimSpace(out))
  874. if !up.confirm(ver) {
  875. return nil
  876. }
  877. cmd := exec.Command("pkg", "upgrade", "-y", "tailscale")
  878. cmd.Stdout = up.Stdout
  879. cmd.Stderr = up.Stderr
  880. if err := cmd.Run(); err != nil {
  881. return fmt.Errorf("failed tailscale update using pkg: %w", err)
  882. }
  883. // pkg does not automatically restart services after upgrade.
  884. out, err = exec.Command("service", "tailscaled", "restart").CombinedOutput()
  885. if err != nil {
  886. return fmt.Errorf("failed to restart tailscaled after update: %w, output:\n%s", err, out)
  887. }
  888. return nil
  889. }
  890. func (up *Updater) updateLinuxBinary() error {
  891. // Root is needed to overwrite binaries and restart systemd unit.
  892. if err := requireRoot(); err != nil {
  893. return err
  894. }
  895. ver, err := requestedTailscaleVersion(up.Version, up.track)
  896. if err != nil {
  897. return err
  898. }
  899. if !up.confirm(ver) {
  900. return nil
  901. }
  902. dlPath, err := up.downloadLinuxTarball(ver)
  903. if err != nil {
  904. return err
  905. }
  906. up.Logf("Extracting %q", dlPath)
  907. if err := up.unpackLinuxTarball(dlPath); err != nil {
  908. return err
  909. }
  910. if err := os.Remove(dlPath); err != nil {
  911. up.Logf("failed to clean up %q: %v", dlPath, err)
  912. }
  913. if err := restartSystemdUnit(context.Background()); err != nil {
  914. if errors.Is(err, errors.ErrUnsupported) {
  915. up.Logf("Tailscale binaries updated successfully.\nPlease restart tailscaled to finish the update.")
  916. } else {
  917. up.Logf("Tailscale binaries updated successfully, but failed to restart tailscaled: %s.\nPlease restart tailscaled to finish the update.", err)
  918. }
  919. } else {
  920. up.Logf("Success")
  921. }
  922. return nil
  923. }
  924. func (up *Updater) downloadLinuxTarball(ver string) (string, error) {
  925. dlDir, err := os.UserCacheDir()
  926. if err != nil {
  927. dlDir = os.TempDir()
  928. }
  929. dlDir = filepath.Join(dlDir, "tailscale-update")
  930. if err := os.MkdirAll(dlDir, 0700); err != nil {
  931. return "", err
  932. }
  933. pkgsPath := fmt.Sprintf("%s/tailscale_%s_%s.tgz", up.track, ver, runtime.GOARCH)
  934. dlPath := filepath.Join(dlDir, path.Base(pkgsPath))
  935. if err := up.downloadURLToFile(pkgsPath, dlPath); err != nil {
  936. return "", err
  937. }
  938. return dlPath, nil
  939. }
  940. func (up *Updater) unpackLinuxTarball(path string) error {
  941. tailscale, tailscaled, err := binaryPaths()
  942. if err != nil {
  943. return err
  944. }
  945. f, err := os.Open(path)
  946. if err != nil {
  947. return err
  948. }
  949. defer f.Close()
  950. gr, err := gzip.NewReader(f)
  951. if err != nil {
  952. return err
  953. }
  954. defer gr.Close()
  955. tr := tar.NewReader(gr)
  956. files := make(map[string]int)
  957. wantFiles := map[string]int{
  958. "tailscale": 1,
  959. "tailscaled": 1,
  960. }
  961. for {
  962. th, err := tr.Next()
  963. if err == io.EOF {
  964. break
  965. }
  966. if err != nil {
  967. return fmt.Errorf("failed extracting %q: %w", path, err)
  968. }
  969. // TODO(awly): try to also extract tailscaled.service. The tricky part
  970. // is fixing up binary paths in that file if they differ from where
  971. // local tailscale/tailscaled are installed. Also, this may not be a
  972. // systemd distro.
  973. switch filepath.Base(th.Name) {
  974. case "tailscale":
  975. files["tailscale"]++
  976. if err := writeFile(tr, tailscale+".new", 0755); err != nil {
  977. return fmt.Errorf("failed extracting the new tailscale binary from %q: %w", path, err)
  978. }
  979. case "tailscaled":
  980. files["tailscaled"]++
  981. if err := writeFile(tr, tailscaled+".new", 0755); err != nil {
  982. return fmt.Errorf("failed extracting the new tailscaled binary from %q: %w", path, err)
  983. }
  984. }
  985. }
  986. if !maps.Equal(files, wantFiles) {
  987. return fmt.Errorf("%q has missing or duplicate files: got %v, want %v", path, files, wantFiles)
  988. }
  989. // Only place the files in final locations after everything extracted correctly.
  990. if err := os.Rename(tailscale+".new", tailscale); err != nil {
  991. return err
  992. }
  993. up.Logf("Updated %s", tailscale)
  994. if err := os.Rename(tailscaled+".new", tailscaled); err != nil {
  995. return err
  996. }
  997. up.Logf("Updated %s", tailscaled)
  998. return nil
  999. }
  1000. func (up *Updater) updateQNAP() (err error) {
  1001. if up.Version != "" {
  1002. return errors.New("installing a specific version on QNAP is not supported")
  1003. }
  1004. if err := requireRoot(); err != nil {
  1005. return err
  1006. }
  1007. defer func() {
  1008. if err != nil {
  1009. err = fmt.Errorf(`%w; you can try updating using "qpkg_cli --add Tailscale"`, err)
  1010. }
  1011. }()
  1012. out, err := exec.Command("qpkg_cli", "--upgradable", "Tailscale").CombinedOutput()
  1013. if err != nil {
  1014. return fmt.Errorf("failed to check if Tailscale is upgradable using qpkg_cli: %w, output: %q", err, out)
  1015. }
  1016. // Output should look like this:
  1017. //
  1018. // $ qpkg_cli -G Tailscale
  1019. // [Tailscale]
  1020. // upgradeStatus = 1
  1021. statusRe := regexp.MustCompile(`upgradeStatus = (\d)`)
  1022. m := statusRe.FindStringSubmatch(string(out))
  1023. if len(m) < 2 {
  1024. return fmt.Errorf("failed to check if Tailscale is upgradable using qpkg_cli, output: %q", out)
  1025. }
  1026. status, err := strconv.Atoi(m[1])
  1027. if err != nil {
  1028. return fmt.Errorf("cannot parse upgradeStatus from qpkg_cli output %q: %w", out, err)
  1029. }
  1030. // Possible status values:
  1031. // 0:can upgrade
  1032. // 1:can not upgrade
  1033. // 2:error
  1034. // 3:can not get rss information
  1035. // 4:qpkg not found
  1036. // 5:qpkg not installed
  1037. //
  1038. // We want status 0.
  1039. switch status {
  1040. case 0: // proceed with upgrade
  1041. case 1:
  1042. up.Logf("no update available")
  1043. return nil
  1044. case 2, 3, 4:
  1045. return fmt.Errorf("failed to check update status with qpkg_cli (upgradeStatus = %d)", status)
  1046. case 5:
  1047. return errors.New("Tailscale was not found in the QNAP App Center")
  1048. default:
  1049. return fmt.Errorf("failed to check update status with qpkg_cli (upgradeStatus = %d)", status)
  1050. }
  1051. // There doesn't seem to be a way to fetch what the available upgrade
  1052. // version is. Use the generic "latest" version in confirmation prompt.
  1053. if up.Confirm != nil && !up.Confirm("latest") {
  1054. return nil
  1055. }
  1056. up.Logf("c2n: running qpkg_cli --add Tailscale")
  1057. cmd := exec.Command("qpkg_cli", "--add", "Tailscale")
  1058. cmd.Stdout = up.Stdout
  1059. cmd.Stderr = up.Stderr
  1060. if err := cmd.Run(); err != nil {
  1061. return fmt.Errorf("failed tailscale update using qpkg_cli: %w", err)
  1062. }
  1063. return nil
  1064. }
  1065. func writeFile(r io.Reader, path string, perm os.FileMode) error {
  1066. if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
  1067. return fmt.Errorf("failed to remove existing file at %q: %w", path, err)
  1068. }
  1069. f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
  1070. if err != nil {
  1071. return err
  1072. }
  1073. defer f.Close()
  1074. if _, err := io.Copy(f, r); err != nil {
  1075. return err
  1076. }
  1077. return f.Close()
  1078. }
  1079. // Var allows overriding this in tests.
  1080. var binaryPaths = func() (tailscale, tailscaled string, err error) {
  1081. // This can be either tailscale or tailscaled.
  1082. this, err := os.Executable()
  1083. if err != nil {
  1084. return "", "", err
  1085. }
  1086. otherName := "tailscaled"
  1087. if filepath.Base(this) == "tailscaled" {
  1088. otherName = "tailscale"
  1089. }
  1090. // Try to find the other binary in the same directory.
  1091. other := filepath.Join(filepath.Dir(this), otherName)
  1092. _, err = os.Stat(other)
  1093. if os.IsNotExist(err) {
  1094. // If it's not in the same directory, try to find it in $PATH.
  1095. other, err = exec.LookPath(otherName)
  1096. }
  1097. if err != nil {
  1098. return "", "", fmt.Errorf("cannot find %q in neither %q nor $PATH: %w", otherName, filepath.Dir(this), err)
  1099. }
  1100. if otherName == "tailscaled" {
  1101. return this, other, nil
  1102. } else {
  1103. return other, this, nil
  1104. }
  1105. }
  1106. func haveExecutable(name string) bool {
  1107. path, err := exec.LookPath(name)
  1108. return err == nil && path != ""
  1109. }
  1110. func requestedTailscaleVersion(ver, track string) (string, error) {
  1111. if ver != "" {
  1112. return ver, nil
  1113. }
  1114. return LatestTailscaleVersion(track)
  1115. }
  1116. // LatestTailscaleVersion returns the latest released version for the given
  1117. // track from pkgs.tailscale.com.
  1118. func LatestTailscaleVersion(track string) (string, error) {
  1119. if track == CurrentTrack {
  1120. if version.IsUnstableBuild() {
  1121. track = UnstableTrack
  1122. } else {
  1123. track = StableTrack
  1124. }
  1125. }
  1126. latest, err := latestPackages(track)
  1127. if err != nil {
  1128. return "", err
  1129. }
  1130. if latest.Version == "" {
  1131. return "", fmt.Errorf("no latest version found for %q track", track)
  1132. }
  1133. return latest.Version, nil
  1134. }
  1135. type trackPackages struct {
  1136. Version string
  1137. Tarballs map[string]string
  1138. TarballsVersion string
  1139. Exes []string
  1140. ExesVersion string
  1141. MSIs map[string]string
  1142. MSIsVersion string
  1143. MacZips map[string]string
  1144. MacZipsVersion string
  1145. SPKs map[string]map[string]string
  1146. SPKsVersion string
  1147. }
  1148. func latestPackages(track string) (*trackPackages, error) {
  1149. url := fmt.Sprintf("https://pkgs.tailscale.com/%s/?mode=json&os=%s", track, runtime.GOOS)
  1150. res, err := http.Get(url)
  1151. if err != nil {
  1152. return nil, fmt.Errorf("fetching latest tailscale version: %w", err)
  1153. }
  1154. defer res.Body.Close()
  1155. var latest trackPackages
  1156. if err := json.NewDecoder(res.Body).Decode(&latest); err != nil {
  1157. return nil, fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
  1158. }
  1159. return &latest, nil
  1160. }
  1161. func requireRoot() error {
  1162. if os.Geteuid() == 0 {
  1163. return nil
  1164. }
  1165. switch runtime.GOOS {
  1166. case "linux":
  1167. return errors.New("must be root; use sudo")
  1168. case "freebsd", "openbsd":
  1169. return errors.New("must be root; use doas")
  1170. default:
  1171. return errors.New("must be root")
  1172. }
  1173. }
  1174. func isExitError(err error) bool {
  1175. var exitErr *exec.ExitError
  1176. return errors.As(err, &exitErr)
  1177. }