clientupdate_windows.go 9.9 KB

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