clientupdate_windows.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Windows-specific stuff that can't go in clientupdate.go because it needs
  4. // x/sys/windows.
  5. package clientupdate
  6. import (
  7. "errors"
  8. "fmt"
  9. "io"
  10. "os"
  11. "os/exec"
  12. "path"
  13. "path/filepath"
  14. "runtime"
  15. "strings"
  16. "time"
  17. "github.com/google/uuid"
  18. "golang.org/x/sys/windows"
  19. "tailscale.com/util/winutil"
  20. "tailscale.com/util/winutil/authenticode"
  21. )
  22. const (
  23. // winMSIEnv is the environment variable that, if set, is the MSI file for
  24. // the update command to install. It's passed like this so we can stop the
  25. // tailscale.exe process from running before the msiexec process runs and
  26. // tries to overwrite ourselves.
  27. winMSIEnv = "TS_UPDATE_WIN_MSI"
  28. // winExePathEnv is the environment variable that is set along with
  29. // winMSIEnv and carries the full path of the calling tailscale.exe binary.
  30. // It is used to re-launch the GUI process (tailscale-ipn.exe) after
  31. // install is complete.
  32. winExePathEnv = "TS_UPDATE_WIN_EXE_PATH"
  33. // winVersionEnv is the environment variable that is set along with
  34. // winMSIEnv and carries the version of tailscale that is being installed.
  35. // It is used for logging purposes.
  36. winVersionEnv = "TS_UPDATE_WIN_VERSION"
  37. // updaterPrefix is the prefix for the temporary executable created by [makeSelfCopy].
  38. updaterPrefix = "tailscale-updater"
  39. )
  40. func makeSelfCopy() (origPathExe, tmpPathExe string, err error) {
  41. selfExe, err := os.Executable()
  42. if err != nil {
  43. return "", "", err
  44. }
  45. f, err := os.Open(selfExe)
  46. if err != nil {
  47. return "", "", err
  48. }
  49. defer f.Close()
  50. f2, err := os.CreateTemp("", updaterPrefix+"-*.exe")
  51. if err != nil {
  52. return "", "", err
  53. }
  54. if err := markTempFileWindows(f2.Name()); err != nil {
  55. return "", "", err
  56. }
  57. if _, err := io.Copy(f2, f); err != nil {
  58. f2.Close()
  59. return "", "", err
  60. }
  61. return selfExe, f2.Name(), f2.Close()
  62. }
  63. func markTempFileWindows(name string) error {
  64. name16 := windows.StringToUTF16Ptr(name)
  65. return windows.MoveFileEx(name16, nil, windows.MOVEFILE_DELAY_UNTIL_REBOOT)
  66. }
  67. const certSubjectTailscale = "Tailscale Inc."
  68. func verifyAuthenticode(path string) error {
  69. return authenticode.Verify(path, certSubjectTailscale)
  70. }
  71. func (up *Updater) updateWindows() error {
  72. if msi := os.Getenv(winMSIEnv); msi != "" {
  73. // stdout/stderr from this part of the install could be lost since the
  74. // parent tailscaled is replaced. Create a temp log file to have some
  75. // output to debug with in case update fails.
  76. close, err := up.switchOutputToFile()
  77. if err != nil {
  78. up.Logf("failed to create log file for installation: %v; proceeding with existing outputs", err)
  79. } else {
  80. defer close.Close()
  81. }
  82. up.Logf("installing %v ...", msi)
  83. if err := up.installMSI(msi); err != nil {
  84. up.Logf("MSI install failed: %v", err)
  85. return err
  86. }
  87. up.Logf("success.")
  88. return nil
  89. }
  90. if !winutil.IsCurrentProcessElevated() {
  91. return errors.New(`update must be run as Administrator
  92. you can run the command prompt as Administrator one of these ways:
  93. * right-click cmd.exe, select 'Run as administrator'
  94. * press Windows+x, then press a
  95. * press Windows+r, type in "cmd", then press Ctrl+Shift+Enter`)
  96. }
  97. ver, err := requestedTailscaleVersion(up.Version, up.Track)
  98. if err != nil {
  99. return err
  100. }
  101. arch := runtime.GOARCH
  102. if arch == "386" {
  103. arch = "x86"
  104. }
  105. if !up.confirm(ver) {
  106. return nil
  107. }
  108. tsDir := filepath.Join(os.Getenv("ProgramData"), "Tailscale")
  109. msiDir := filepath.Join(tsDir, "MSICache")
  110. if fi, err := os.Stat(tsDir); err != nil {
  111. return fmt.Errorf("expected %s to exist, got stat error: %w", tsDir, err)
  112. } else if !fi.IsDir() {
  113. return fmt.Errorf("expected %s to be a directory; got %v", tsDir, fi.Mode())
  114. }
  115. if err := os.MkdirAll(msiDir, 0700); err != nil {
  116. return err
  117. }
  118. up.cleanupOldDownloads(filepath.Join(msiDir, "*.msi"))
  119. pkgsPath := fmt.Sprintf("%s/tailscale-setup-%s-%s.msi", up.Track, ver, arch)
  120. msiTarget := filepath.Join(msiDir, path.Base(pkgsPath))
  121. if err := up.downloadURLToFile(pkgsPath, msiTarget); err != nil {
  122. return err
  123. }
  124. up.Logf("verifying MSI authenticode...")
  125. if err := verifyAuthenticode(msiTarget); err != nil {
  126. return fmt.Errorf("authenticode verification of %s failed: %w", msiTarget, err)
  127. }
  128. up.Logf("authenticode verification succeeded")
  129. up.Logf("making tailscale.exe copy to switch to...")
  130. up.cleanupOldDownloads(filepath.Join(os.TempDir(), updaterPrefix+"-*.exe"))
  131. selfOrig, selfCopy, err := makeSelfCopy()
  132. if err != nil {
  133. return err
  134. }
  135. defer os.Remove(selfCopy)
  136. up.Logf("running tailscale.exe copy for final install...")
  137. cmd := exec.Command(selfCopy, "update")
  138. cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget, winExePathEnv+"="+selfOrig, winVersionEnv+"="+ver)
  139. cmd.Stdout = up.Stderr
  140. cmd.Stderr = up.Stderr
  141. cmd.Stdin = os.Stdin
  142. if err := cmd.Start(); err != nil {
  143. return err
  144. }
  145. // Once it's started, exit ourselves, so the binary is free
  146. // to be replaced.
  147. os.Exit(0)
  148. panic("unreachable")
  149. }
  150. func (up *Updater) installMSI(msi string) error {
  151. var err error
  152. for tries := 0; tries < 2; tries++ {
  153. // msiexec.exe requires exclusive access to the log file, so create a dedicated one for each run.
  154. installLogPath := up.startNewLogFile("tailscale-installer", os.Getenv(winVersionEnv))
  155. up.Logf("Install log: %s", installLogPath)
  156. cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/norestart", "/qn", "/L*v", installLogPath)
  157. cmd.Dir = filepath.Dir(msi)
  158. cmd.Stdout = up.Stdout
  159. cmd.Stderr = up.Stderr
  160. cmd.Stdin = os.Stdin
  161. err = cmd.Run()
  162. switch err := err.(type) {
  163. case nil:
  164. // Success.
  165. return nil
  166. case *exec.ExitError:
  167. // For possible error codes returned by Windows Installer, see
  168. // https://web.archive.org/web/20250409144914/https://learn.microsoft.com/en-us/windows/win32/msi/error-codes
  169. switch windows.Errno(err.ExitCode()) {
  170. case windows.ERROR_SUCCESS_REBOOT_REQUIRED:
  171. // In most cases, updating Tailscale should not require a reboot.
  172. // If it does, it might be because we failed to close the GUI
  173. // and the installer couldn't replace tailscale-ipn.exe.
  174. // The old GUI will continue to run until the next reboot.
  175. // Not ideal, but also not a retryable error.
  176. up.Logf("[unexpected] reboot required")
  177. return nil
  178. case windows.ERROR_SUCCESS_REBOOT_INITIATED:
  179. // Same as above, but perhaps the device is configured to prompt
  180. // the user to reboot and the user has chosen to reboot now.
  181. up.Logf("[unexpected] reboot initiated")
  182. return nil
  183. case windows.ERROR_INSTALL_ALREADY_RUNNING:
  184. // The Windows Installer service is currently busy.
  185. // It could be our own install initiated by user/MDM/GP, another MSI install or perhaps a Windows Update install.
  186. // Anyway, we can't do anything about it right now. The user (or tailscaled) can retry later.
  187. // Retrying now will likely fail, and is risky since we might uninstall the current version
  188. // and then fail to install the new one, leaving the user with no Tailscale at all.
  189. //
  190. // TODO(nickkhyl,awly): should we check if this is actually a downgrade before uninstalling the current version?
  191. // Also, maybe keep retrying the install longer if we uninstalled the current version due to a failed install attempt?
  192. up.Logf("another installation is already in progress")
  193. return err
  194. }
  195. default:
  196. // Everything else is a retryable error.
  197. }
  198. up.Logf("Install attempt failed: %v", err)
  199. uninstallVersion := up.currentVersion
  200. if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" {
  201. uninstallVersion = v
  202. }
  203. uninstallLogPath := up.startNewLogFile("tailscale-uninstaller", uninstallVersion)
  204. // Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
  205. up.Logf("Uninstalling current version %q for downgrade...", uninstallVersion)
  206. up.Logf("Uninstall log: %s", uninstallLogPath)
  207. cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn", "/L*v", uninstallLogPath)
  208. cmd.Stdout = up.Stdout
  209. cmd.Stderr = up.Stderr
  210. cmd.Stdin = os.Stdin
  211. err = cmd.Run()
  212. up.Logf("msiexec uninstall: %v", err)
  213. }
  214. return err
  215. }
  216. func msiUUIDForVersion(ver string) string {
  217. arch := runtime.GOARCH
  218. if arch == "386" {
  219. arch = "x86"
  220. }
  221. track, err := versionToTrack(ver)
  222. if err != nil {
  223. track = UnstableTrack
  224. }
  225. msiURL := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%s-%s.msi", track, ver, arch)
  226. return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}"
  227. }
  228. func (up *Updater) switchOutputToFile() (io.Closer, error) {
  229. var logFilePath string
  230. exePath, err := os.Executable()
  231. if err != nil {
  232. logFilePath = up.startNewLogFile(updaterPrefix, os.Getenv(winVersionEnv))
  233. } else {
  234. // Use the same suffix as the self-copy executable.
  235. suffix := strings.TrimSuffix(strings.TrimPrefix(filepath.Base(exePath), updaterPrefix), ".exe")
  236. logFilePath = up.startNewLogFile(updaterPrefix, os.Getenv(winVersionEnv)+suffix)
  237. }
  238. up.Logf("writing update output to: %s", logFilePath)
  239. logFile, err := os.Create(logFilePath)
  240. if err != nil {
  241. return nil, err
  242. }
  243. up.Logf = func(m string, args ...any) {
  244. fmt.Fprintf(logFile, m+"\n", args...)
  245. }
  246. up.Stdout = logFile
  247. up.Stderr = logFile
  248. return logFile, nil
  249. }
  250. // startNewLogFile returns a name for a new log file.
  251. // It cleans up any old log files with the same baseNamePrefix.
  252. func (up *Updater) startNewLogFile(baseNamePrefix, baseNameSuffix string) string {
  253. baseName := fmt.Sprintf("%s-%s-%s.log", baseNamePrefix,
  254. time.Now().Format("20060102T150405"), baseNameSuffix)
  255. dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs")
  256. if err := os.MkdirAll(dir, 0700); err != nil {
  257. up.Logf("failed to create log directory: %v", err)
  258. return filepath.Join(os.TempDir(), baseName)
  259. }
  260. // TODO(nickkhyl): preserve up to N old log files?
  261. up.cleanupOldDownloads(filepath.Join(dir, baseNamePrefix+"-*.log"))
  262. return filepath.Join(dir, baseName)
  263. }