clientupdate.go 38 KB

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