clientupdate.go 40 KB

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