Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
// Windows-specific stuff that can't go in clientupdate.go because it needs// x/sys/windows.
package clientupdate
import ( "errors" "fmt" "io" "os" "os/exec" "path" "path/filepath" "runtime" "strings" "time"
"github.com/google/uuid" "golang.org/x/sys/windows" "tailscale.com/util/winutil" "tailscale.com/util/winutil/authenticode")
const ( // winMSIEnv is the environment variable that, if set, is the MSI file for // the update command to install. It's passed like this so we can stop the // tailscale.exe process from running before the msiexec process runs and // tries to overwrite ourselves. winMSIEnv = "TS_UPDATE_WIN_MSI" // winVersionEnv is the environment variable that is set along with // winMSIEnv and carries the version of tailscale that is being installed. // It is used for logging purposes. winVersionEnv = "TS_UPDATE_WIN_VERSION" // updaterPrefix is the prefix for the temporary executable created by [makeSelfCopy]. updaterPrefix = "tailscale-updater")
func makeCmdTailscaleCopy() (origPathExe, tmpPathExe string, err error) { srcExe, err := findCmdTailscale() if err != nil { return "", "", err } f, err := os.Open(srcExe) if err != nil { return "", "", err } defer f.Close() f2, err := os.CreateTemp("", updaterPrefix+"-*.exe") if err != nil { return "", "", err } if err := markTempFileWindows(f2.Name()); err != nil { return "", "", err } if _, err := io.Copy(f2, f); err != nil { f2.Close() return "", "", err } return srcExe, f2.Name(), f2.Close()}
// findCmdTailscale returns the path to the binary that should be copied for the update// re-execution. The copy is re-executed with "update" as a subcommand, so it must be// a binary that handles "update" (ie tailscale.exe, not tailscaled.exe)func findCmdTailscale() (string, error) { selfExe, err := os.Executable() if err != nil { return "", err } if strings.EqualFold(filepath.Base(selfExe), "tailscale.exe") { return selfExe, nil } ts := filepath.Join(filepath.Dir(selfExe), "tailscale.exe") if _, err := os.Stat(ts); err != nil { return "", fmt.Errorf("cannot find tailscale.exe alongside %s: %w", selfExe, err) } return ts, nil}
func markTempFileWindows(name string) error { name16 := windows.StringToUTF16Ptr(name) return windows.MoveFileEx(name16, nil, windows.MOVEFILE_DELAY_UNTIL_REBOOT)}
const certSubjectTailscale = "Tailscale Inc."
func verifyAuthenticode(path string) error { return authenticode.Verify(path, certSubjectTailscale)}
func isTSGUIPresent() bool { us, err := os.Executable() if err != nil { return false }
tsgui := filepath.Join(filepath.Dir(us), "tsgui.dll") _, err = os.Stat(tsgui) return err == nil}
func (up *Updater) updateWindows() error { if msi := os.Getenv(winMSIEnv); msi != "" { // stdout/stderr from this part of the install could be lost since the // parent tailscaled is replaced. Create a temp log file to have some // output to debug with in case update fails. close, err := up.switchOutputToFile() if err != nil { up.Logf("failed to create log file for installation: %v; proceeding with existing outputs", err) } else { defer close.Close() }
up.Logf("installing %v ...", msi) if err := up.installMSI(msi); err != nil { up.Logf("MSI install failed: %v", err) return err }
up.Logf("success.") return nil }
if !winutil.IsCurrentProcessElevated() { return errors.New(`update must be run as Administrator
you can run the command prompt as Administrator one of these ways:* right-click cmd.exe, select 'Run as administrator'* press Windows+x, then press a* press Windows+r, type in "cmd", then press Ctrl+Shift+Enter`) } ver, err := requestedTailscaleVersion(up.Version, up.Track) if err != nil { return err } arch := runtime.GOARCH if arch == "386" { arch = "x86" } if !up.confirm(ver) { return nil }
tsDir := filepath.Join(os.Getenv("ProgramData"), "Tailscale") msiDir := filepath.Join(tsDir, "MSICache") if fi, err := os.Stat(tsDir); err != nil { return fmt.Errorf("expected %s to exist, got stat error: %w", tsDir, err) } else if !fi.IsDir() { return fmt.Errorf("expected %s to be a directory; got %v", tsDir, fi.Mode()) } if err := os.MkdirAll(msiDir, 0700); err != nil { return err } up.cleanupOldDownloads(filepath.Join(msiDir, "*.msi"))
qualifiers := []string{ver, arch} // TODO(aaron): Temporary hack so autoupdate still works on winui builds; // remove when we enable winui by default on the unstable track. if isTSGUIPresent() { qualifiers = append(qualifiers, "winui") }
pkgsPath := fmt.Sprintf("%s/tailscale-setup-%s.msi", up.Track, strings.Join(qualifiers, "-")) msiTarget := filepath.Join(msiDir, path.Base(pkgsPath)) if err := up.downloadURLToFile(pkgsPath, msiTarget); err != nil { return err }
up.Logf("verifying MSI authenticode...") if err := verifyAuthenticode(msiTarget); err != nil { return fmt.Errorf("authenticode verification of %s failed: %w", msiTarget, err) } up.Logf("authenticode verification succeeded")
up.Logf("making tailscale.exe copy to switch to...") up.cleanupOldDownloads(filepath.Join(os.TempDir(), updaterPrefix+"-*.exe")) _, cmdTailscaleCopy, err := makeCmdTailscaleCopy() if err != nil { return err } defer os.Remove(cmdTailscaleCopy) up.Logf("running tailscale.exe copy for final install...")
cmd := exec.Command(cmdTailscaleCopy, "update") cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget, winVersionEnv+"="+ver) cmd.Stdout = up.Stderr cmd.Stderr = up.Stderr cmd.Stdin = os.Stdin if err := cmd.Start(); err != nil { return err } // Once it's started, exit ourselves, so the binary is free // to be replaced. os.Exit(0) panic("unreachable")}
func (up *Updater) installMSI(msi string) error { var err error for tries := 0; tries < 2; tries++ { // msiexec.exe requires exclusive access to the log file, so create a dedicated one for each run. installLogPath := up.startNewLogFile("tailscale-installer", os.Getenv(winVersionEnv)) up.Logf("Install log: %s", installLogPath) cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/norestart", "/qn", "/L*v", installLogPath) cmd.Dir = filepath.Dir(msi) cmd.Stdout = up.Stdout cmd.Stderr = up.Stderr cmd.Stdin = os.Stdin err = cmd.Run() switch err := err.(type) { case nil: // Success. return nil case *exec.ExitError: // For possible error codes returned by Windows Installer, see // https://web.archive.org/web/20250409144914/https://learn.microsoft.com/en-us/windows/win32/msi/error-codes switch windows.Errno(err.ExitCode()) { case windows.ERROR_SUCCESS_REBOOT_REQUIRED: // In most cases, updating Tailscale should not require a reboot. // If it does, it might be because we failed to close the GUI // and the installer couldn't replace its executable. // The old GUI will continue to run until the next reboot. // Not ideal, but also not a retryable error. up.Logf("[unexpected] reboot required") return nil case windows.ERROR_SUCCESS_REBOOT_INITIATED: // Same as above, but perhaps the device is configured to prompt // the user to reboot and the user has chosen to reboot now. up.Logf("[unexpected] reboot initiated") return nil case windows.ERROR_INSTALL_ALREADY_RUNNING: // The Windows Installer service is currently busy. // It could be our own install initiated by user/MDM/GP, another MSI install or perhaps a Windows Update install. // Anyway, we can't do anything about it right now. The user (or tailscaled) can retry later. // Retrying now will likely fail, and is risky since we might uninstall the current version // and then fail to install the new one, leaving the user with no Tailscale at all. // // TODO(nickkhyl,awly): should we check if this is actually a downgrade before uninstalling the current version? // Also, maybe keep retrying the install longer if we uninstalled the current version due to a failed install attempt? up.Logf("another installation is already in progress") return err } default: // Everything else is a retryable error. }
up.Logf("Install attempt failed: %v", err) uninstallVersion := up.currentVersion if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" { uninstallVersion = v } uninstallLogPath := up.startNewLogFile("tailscale-uninstaller", uninstallVersion) // Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first. up.Logf("Uninstalling current version %q for downgrade...", uninstallVersion) up.Logf("Uninstall log: %s", uninstallLogPath) cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn", "/L*v", uninstallLogPath) cmd.Stdout = up.Stdout cmd.Stderr = up.Stderr cmd.Stdin = os.Stdin err = cmd.Run() up.Logf("msiexec uninstall: %v", err) } return err}
func msiUUIDForVersion(ver string) string { arch := runtime.GOARCH if arch == "386" { arch = "x86" } track, err := versionToTrack(ver) if err != nil { track = UnstableTrack } msiURL := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%s-%s.msi", track, ver, arch) return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}"}
func (up *Updater) switchOutputToFile() (io.Closer, error) { var logFilePath string exePath, err := os.Executable() if err != nil { logFilePath = up.startNewLogFile(updaterPrefix, os.Getenv(winVersionEnv)) } else { // Use the same suffix as the self-copy executable. suffix := strings.TrimSuffix(strings.TrimPrefix(filepath.Base(exePath), updaterPrefix), ".exe") logFilePath = up.startNewLogFile(updaterPrefix, os.Getenv(winVersionEnv)+suffix) }
up.Logf("writing update output to: %s", logFilePath) logFile, err := os.Create(logFilePath) if err != nil { return nil, err }
up.Logf = func(m string, args ...any) { fmt.Fprintf(logFile, m+"\n", args...) } up.Stdout = logFile up.Stderr = logFile return logFile, nil}
// startNewLogFile returns a name for a new log file.// It cleans up any old log files with the same baseNamePrefix.func (up *Updater) startNewLogFile(baseNamePrefix, baseNameSuffix string) string { baseName := fmt.Sprintf("%s-%s-%s.log", baseNamePrefix, time.Now().Format("20060102T150405"), baseNameSuffix)
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs") if err := os.MkdirAll(dir, 0700); err != nil { up.Logf("failed to create log directory: %v", err) return filepath.Join(os.TempDir(), baseName) }
// TODO(nickkhyl): preserve up to N old log files? up.cleanupOldDownloads(filepath.Join(dir, baseNamePrefix+"-*.log")) return filepath.Join(dir, baseName)}