//go:build windows // Package elevate answers "is this process actually elevated?" and, if not, // can re-exec it through the UAC prompt. // // The manifest asks for asInvoker rather than requireAdministrator: on a // machine with UAC relaxed, or when the parent is already elevated, that means // ipswap starts with zero prompts. The cost is that on a locked-down machine // we start unprivileged and have to offer the relaunch, which is what this // package is for. package elevate import ( "fmt" "os" "path/filepath" "strings" "syscall" "unsafe" "golang.org/x/sys/windows" ) var ( shell32 = syscall.NewLazyDLL("shell32.dll") shellExecuteW = shell32.NewProc("ShellExecuteW") ) const swShowNormal = 1 // IsElevated reports whether the current process token carries administrator // rights right now. // // Membership of the Administrators group is not the same thing: with UAC on, // a member's process gets a filtered token, and `netsh interface ipv4 set // address` then fails with "The requested operation requires elevation." // Checking the token is the only answer that predicts whether an apply works. func IsElevated() bool { return windows.GetCurrentProcessToken().IsElevated() } // RelaunchAsAdmin re-execs the current binary through the UAC prompt and // returns once the new process has been started. The caller is expected to // exit immediately afterwards so the two copies do not both own a tray icon. // // A user who clicks "No" on the UAC prompt produces ERROR_CANCELLED, which is // reported as an error but is not a failure worth a message box. func RelaunchAsAdmin() error { exe, err := os.Executable() if err != nil { return fmt.Errorf("locating the running executable: %w", err) } exe, err = filepath.Abs(exe) if err != nil { return fmt.Errorf("resolving %s: %w", exe, err) } verb, err := syscall.UTF16PtrFromString("runas") if err != nil { return err } file, err := syscall.UTF16PtrFromString(exe) if err != nil { return err } args, err := syscall.UTF16PtrFromString(strings.Join(quoteArgs(os.Args[1:]), " ")) if err != nil { return err } cwd, err := syscall.UTF16PtrFromString(filepath.Dir(exe)) if err != nil { return err } // ShellExecuteW signals failure with a return value of 32 or less. ret, _, callErr := shellExecuteW.Call( 0, uintptr(unsafe.Pointer(verb)), uintptr(unsafe.Pointer(file)), uintptr(unsafe.Pointer(args)), uintptr(unsafe.Pointer(cwd)), swShowNormal, ) if ret <= 32 { if ret == uintptr(windows.ERROR_CANCELLED) { return fmt.Errorf("the elevation prompt was cancelled") } return fmt.Errorf("ShellExecuteW returned %d: %v", ret, callErr) } return nil } // quoteArgs re-quotes arguments for the single command-line string // ShellExecuteW takes, since it does not accept an argv. func quoteArgs(args []string) []string { out := make([]string, 0, len(args)) for _, a := range args { if strings.ContainsAny(a, ` "`) { out = append(out, `"`+strings.ReplaceAll(a, `"`, `\"`)+`"`) } else { out = append(out, a) } } return out }