clientupdate: manually restart Windows GUI after update (#9906)

When updating via c2n, `tailscale.exe update` runs from `tailscaled.exe`
which runs as SYSTEM. The MSI installer does not start the GUI when
running as SYSTEM. This results in Tailscale just existing on
auto-update, which is ungood.

Instead, always ask the MSI installer to not launch the GUI (via
`TS_NOLAUNCH` argument) and launch it manually with a token from the
current logged in user. The token code was borrowed from
d9081d6ba2/net/dns/wsl_windows.go (L207-L232)

Also, make some logging changes so that these issues are easier to debug
in the future.

Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
pull/9933/head
Andrew Lytvynov 11 months ago committed by GitHub
parent e9956419f6
commit e561f1ce61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -631,24 +631,53 @@ func (up *Updater) updateMacAppStore() error {
return nil return nil
} }
// winMSIEnv is the environment variable that, if set, is the MSI file for the const (
// update command to install. It's passed like this so we can stop the // winMSIEnv is the environment variable that, if set, is the MSI file for
// tailscale.exe process from running before the msiexec process runs and tries // the update command to install. It's passed like this so we can stop the
// to overwrite ourselves. // tailscale.exe process from running before the msiexec process runs and
const winMSIEnv = "TS_UPDATE_WIN_MSI" // tries to overwrite ourselves.
winMSIEnv = "TS_UPDATE_WIN_MSI"
// winExePathEnv is the environment variable that is set along with
// winMSIEnv and carries the full path of the calling tailscale.exe binary.
// It is used to re-launch the GUI process (tailscale-ipn.exe) after
// install is complete.
winExePathEnv = "TS_UPDATE_WIN_EXE_PATH"
)
var ( var (
verifyAuthenticode func(string) error // or nil on non-Windows verifyAuthenticode func(string) error // or nil on non-Windows
markTempFileFunc func(string) error // or nil on non-Windows markTempFileFunc func(string) error // or nil on non-Windows
launchTailscaleAsWinGUIUser func(string) error // or nil on non-Windows
) )
func (up *Updater) updateWindows() error { func (up *Updater) updateWindows() error {
if msi := os.Getenv(winMSIEnv); msi != "" { 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) up.Logf("installing %v ...", msi)
if err := up.installMSI(msi); err != nil { if err := up.installMSI(msi); err != nil {
up.Logf("MSI install failed: %v", err) up.Logf("MSI install failed: %v", err)
return err return err
} }
up.Logf("relaunching tailscale-ipn.exe...")
exePath := os.Getenv(winExePathEnv)
if exePath == "" {
up.Logf("env var %q not passed to installer binary copy", winExePathEnv)
return fmt.Errorf("env var %q not passed to installer binary copy", winExePathEnv)
}
if err := launchTailscaleAsWinGUIUser(exePath); err != nil {
up.Logf("Failed to re-launch tailscale after update: %v", err)
return err
}
up.Logf("success.") up.Logf("success.")
return nil return nil
} }
@ -691,7 +720,7 @@ func (up *Updater) updateWindows() error {
up.Logf("authenticode verification succeeded") up.Logf("authenticode verification succeeded")
up.Logf("making tailscale.exe copy to switch to...") up.Logf("making tailscale.exe copy to switch to...")
selfCopy, err := makeSelfCopy() selfOrig, selfCopy, err := makeSelfCopy()
if err != nil { if err != nil {
return err return err
} }
@ -699,7 +728,7 @@ func (up *Updater) updateWindows() error {
up.Logf("running tailscale.exe copy for final install...") up.Logf("running tailscale.exe copy for final install...")
cmd := exec.Command(selfCopy, "update") cmd := exec.Command(selfCopy, "update")
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget) cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget, winExePathEnv+"="+selfOrig)
cmd.Stdout = up.Stderr cmd.Stdout = up.Stderr
cmd.Stderr = up.Stderr cmd.Stderr = up.Stderr
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
@ -712,10 +741,35 @@ func (up *Updater) updateWindows() error {
panic("unreachable") panic("unreachable")
} }
func (up *Updater) switchOutputToFile() (io.Closer, error) {
var logFilePath string
exePath, err := os.Executable()
if err != nil {
logFilePath = filepath.Join(os.TempDir(), "tailscale-updater.log")
} else {
logFilePath = strings.TrimSuffix(exePath, ".exe") + ".log"
}
up.Logf("writing update output to %q", 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
}
func (up *Updater) installMSI(msi string) error { func (up *Updater) installMSI(msi string) error {
var err error var err error
for tries := 0; tries < 2; tries++ { for tries := 0; tries < 2; tries++ {
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn") // TS_NOLAUNCH: don't automatically launch the app after install.
// We will launch it explicitly as the current GUI user afterwards.
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn", "TS_NOLAUNCH=true")
cmd.Dir = filepath.Dir(msi) cmd.Dir = filepath.Dir(msi)
cmd.Stdout = up.Stdout cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr cmd.Stderr = up.Stderr
@ -724,6 +778,7 @@ func (up *Updater) installMSI(msi string) error {
if err == nil { if err == nil {
break break
} }
up.Logf("Install attempt failed: %v", err)
uninstallVersion := version.Short() uninstallVersion := version.Short()
if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" { if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" {
uninstallVersion = v uninstallVersion = v
@ -753,30 +808,30 @@ func msiUUIDForVersion(ver string) string {
return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}" return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}"
} }
func makeSelfCopy() (tmpPathExe string, err error) { func makeSelfCopy() (origPathExe, tmpPathExe string, err error) {
selfExe, err := os.Executable() selfExe, err := os.Executable()
if err != nil { if err != nil {
return "", err return "", "", err
} }
f, err := os.Open(selfExe) f, err := os.Open(selfExe)
if err != nil { if err != nil {
return "", err return "", "", err
} }
defer f.Close() defer f.Close()
f2, err := os.CreateTemp("", "tailscale-updater-*.exe") f2, err := os.CreateTemp("", "tailscale-updater-*.exe")
if err != nil { if err != nil {
return "", err return "", "", err
} }
if f := markTempFileFunc; f != nil { if f := markTempFileFunc; f != nil {
if err := f(f2.Name()); err != nil { if err := f(f2.Name()); err != nil {
return "", err return "", "", err
} }
} }
if _, err := io.Copy(f2, f); err != nil { if _, err := io.Copy(f2, f); err != nil {
f2.Close() f2.Close()
return "", err return "", "", err
} }
return f2.Name(), f2.Close() return selfExe, f2.Name(), f2.Close()
} }
func (up *Updater) downloadURLToFile(pathSrc, fileDst string) (ret error) { func (up *Updater) downloadURLToFile(pathSrc, fileDst string) (ret error) {

@ -7,13 +7,20 @@
package clientupdate package clientupdate
import ( import (
"os/exec"
"os/user"
"path/filepath"
"syscall"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
"tailscale.com/util/winutil"
"tailscale.com/util/winutil/authenticode" "tailscale.com/util/winutil/authenticode"
) )
func init() { func init() {
markTempFileFunc = markTempFileWindows markTempFileFunc = markTempFileWindows
verifyAuthenticode = verifyTailscale verifyAuthenticode = verifyTailscale
launchTailscaleAsWinGUIUser = launchTailscaleAsGUIUser
} }
func markTempFileWindows(name string) error { func markTempFileWindows(name string) error {
@ -26,3 +33,25 @@ const certSubjectTailscale = "Tailscale Inc."
func verifyTailscale(path string) error { func verifyTailscale(path string) error {
return authenticode.Verify(path, certSubjectTailscale) return authenticode.Verify(path, certSubjectTailscale)
} }
func launchTailscaleAsGUIUser(exePath string) error {
exePath = filepath.Join(filepath.Dir(exePath), "tailscale-ipn.exe")
var token windows.Token
if u, err := user.Current(); err == nil && u.Name == "SYSTEM" {
sessionID := winutil.WTSGetActiveConsoleSessionId()
if sessionID != 0xFFFFFFFF {
if err := windows.WTSQueryUserToken(sessionID, &token); err != nil {
return err
}
defer token.Close()
}
}
cmd := exec.Command(exePath)
cmd.SysProcAttr = &syscall.SysProcAttr{
Token: syscall.Token(token),
HideWindow: true,
}
return cmd.Start()
}

Loading…
Cancel
Save