Files
sing-box/experimental/boxdd/update_windows.go
T
2026-07-14 21:33:15 +08:00

268 lines
8.1 KiB
Go

//go:build windows
package main
import (
"bytes"
"fmt"
"path/filepath"
"strings"
"unsafe"
"github.com/sagernet/sing-box/common/badversion"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox"
E "github.com/sagernet/sing/common/exceptions"
"github.com/tailscale/go-winio"
"golang.org/x/sys/windows"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
updateInstallerDesktop = `winsta0\default`
updateProductName = "sing-box"
seTcbPrivilege = "SeTcbPrivilege"
seAssignPrimaryToken = "SeAssignPrimaryTokenPrivilege"
seIncreaseQuota = "SeIncreaseQuotaPrivilege"
)
func (d *Daemon) installUpdate(identity peerIdentity, installerPath string) (*InstallUpdateResponse, error) {
if installerPath == "" {
return nil, status.Error(codes.InvalidArgument, "missing update installer path")
}
platform := d.platform.(*windowsPlatformInterface)
platform.updateAccess.Lock()
if platform.updateInProgress {
platform.updateAccess.Unlock()
return nil, status.Error(codes.Aborted, "update installation is already in progress")
}
platform.updateInProgress = true
platform.updateAccess.Unlock()
started := false
defer func() {
if started {
return
}
platform.updateAccess.Lock()
platform.updateInProgress = false
platform.updateAccess.Unlock()
}()
installer, err := openLockedExecutable(installerPath)
if err != nil {
return nil, E.Cause(err, "open update installer")
}
defer windows.CloseHandle(installer)
installerFinalPath, err := finalWindowsPath(installer)
if err != nil {
return nil, E.Cause(err, "resolve update installer")
}
installerIdentity, err := windowsExecutableIdentity(installerFinalPath)
if err != nil {
return nil, status.Error(codes.InvalidArgument, E.Cause(err, "read update installer identity").Error())
}
if installerIdentity.productName != updateProductName {
return nil, status.Error(codes.InvalidArgument, "update executable is not a sing-box installer")
}
err = validateNSISExecutable(installerFinalPath)
if err != nil {
return nil, status.Error(codes.InvalidArgument, E.Cause(err, "update executable is not a sing-box installer").Error())
}
installerSigner, err := authenticodeSigner(installerFinalPath, installer)
if err != nil {
return nil, E.Cause(err, "authenticate update installer")
}
if !bytes.Equal(installerSigner, platform.daemonSigner) {
return &InstallUpdateResponse{Result: InstallUpdateResult_INSTALL_UPDATE_RESULT_SIGNER_MISMATCH}, nil
}
if !badversion.IsValid(installerIdentity.version) {
return nil, status.Error(codes.InvalidArgument, "update installer has an invalid version")
}
if !libbox.CompareSemver(installerIdentity.version, C.Version) {
return &InstallUpdateResponse{Result: InstallUpdateResult_INSTALL_UPDATE_RESULT_NOT_NEWER}, nil
}
installerProcess, err := launchUpdateInstaller(installerFinalPath, identity.SessionID)
if err != nil {
return nil, E.Cause(err, "launch update installer")
}
started = true
go platform.waitUpdateInstaller(installerProcess)
return &InstallUpdateResponse{Result: InstallUpdateResult_INSTALL_UPDATE_RESULT_STARTED}, nil
}
func (p *windowsPlatformInterface) waitUpdateInstaller(process windows.Handle) {
defer windows.CloseHandle(process)
_, _ = windows.WaitForSingleObject(process, windows.INFINITE)
p.updateAccess.Lock()
p.updateInProgress = false
p.updateAccess.Unlock()
}
type windowsExecutableVersionIdentity struct {
productName string
version string
}
type windowsVersionTranslation struct {
language uint16
codePage uint16
}
func windowsExecutableIdentity(path string) (windowsExecutableVersionIdentity, error) {
var zero windows.Handle
informationSize, err := windows.GetFileVersionInfoSize(path, &zero)
if err != nil {
return windowsExecutableVersionIdentity{}, err
}
information := make([]byte, informationSize)
err = windows.GetFileVersionInfo(path, 0, informationSize, unsafe.Pointer(&information[0]))
if err != nil {
return windowsExecutableVersionIdentity{}, err
}
var translationsPointer *windowsVersionTranslation
var translationsSize uint32
err = windows.VerQueryValue(
unsafe.Pointer(&information[0]),
`\VarFileInfo\Translation`,
unsafe.Pointer(&translationsPointer),
&translationsSize,
)
if err != nil {
return windowsExecutableVersionIdentity{}, E.Cause(err, "query version translations")
}
if translationsPointer == nil || translationsSize == 0 || translationsSize%uint32(unsafe.Sizeof(windowsVersionTranslation{})) != 0 {
return windowsExecutableVersionIdentity{}, E.New("invalid version translations")
}
translations := unsafe.Slice(
translationsPointer,
int(translationsSize/uint32(unsafe.Sizeof(windowsVersionTranslation{}))),
)
for _, translation := range translations {
productName, productErr := windowsVersionString(information, translation, "ProductName")
if productErr != nil {
continue
}
version, versionErr := windowsVersionString(information, translation, "FileVersion")
if versionErr != nil {
continue
}
return windowsExecutableVersionIdentity{
productName: strings.TrimSpace(productName),
version: strings.TrimSpace(version),
}, nil
}
return windowsExecutableVersionIdentity{}, E.New("missing product name or file version")
}
func windowsVersionString(information []byte, translation windowsVersionTranslation, name string) (string, error) {
query := fmt.Sprintf(`\StringFileInfo\%04X%04X\%s`, translation.language, translation.codePage, name)
var valuePointer *uint16
var valueLength uint32
err := windows.VerQueryValue(
unsafe.Pointer(&information[0]),
query,
unsafe.Pointer(&valuePointer),
&valueLength,
)
if err != nil {
return "", err
}
if valuePointer == nil || valueLength == 0 {
return "", E.New("empty version string")
}
return windows.UTF16ToString(unsafe.Slice(valuePointer, int(valueLength))), nil
}
func launchUpdateInstaller(installerPath string, sessionID uint32) (windows.Handle, error) {
var processToken windows.Token
err := windows.OpenProcessToken(
windows.CurrentProcess(),
windows.TOKEN_DUPLICATE|windows.TOKEN_QUERY,
&processToken,
)
if err != nil {
return 0, E.Cause(err, "open daemon process token")
}
defer processToken.Close()
var installerToken windows.Token
err = windows.DuplicateTokenEx(
processToken,
windows.TOKEN_ALL_ACCESS,
nil,
windows.SecurityImpersonation,
windows.TokenPrimary,
&installerToken,
)
if err != nil {
return 0, E.Cause(err, "duplicate daemon process token")
}
defer installerToken.Close()
var installerProcess windows.Handle
err = winio.RunWithPrivileges(
[]string{seTcbPrivilege, seAssignPrimaryToken, seIncreaseQuota},
func() error {
err = windows.SetTokenInformation(
installerToken,
windows.TokenSessionId,
(*byte)(unsafe.Pointer(&sessionID)),
uint32(unsafe.Sizeof(sessionID)),
)
if err != nil {
return E.Cause(err, "set update installer session")
}
installerProcess, err = createUpdateInstallerProcess(installerToken, installerPath)
return err
},
)
return installerProcess, err
}
func createUpdateInstallerProcess(token windows.Token, installerPath string) (windows.Handle, error) {
applicationName, err := windows.UTF16PtrFromString(installerPath)
if err != nil {
return 0, err
}
commandLine, err := windows.UTF16FromString(windows.ComposeCommandLine([]string{
installerPath,
"--updated",
"/S",
"--force-run",
}))
if err != nil {
return 0, err
}
desktop, err := windows.UTF16PtrFromString(updateInstallerDesktop)
if err != nil {
return 0, err
}
workingDirectory, err := windows.UTF16PtrFromString(filepath.Dir(installerPath))
if err != nil {
return 0, err
}
startupInformation := windows.StartupInfo{
Cb: uint32(unsafe.Sizeof(windows.StartupInfo{})),
Desktop: desktop,
}
var processInformation windows.ProcessInformation
err = windows.CreateProcessAsUser(
token,
applicationName,
&commandLine[0],
nil,
nil,
false,
windows.CREATE_NEW_PROCESS_GROUP|windows.CREATE_UNICODE_ENVIRONMENT,
nil,
workingDirectory,
&startupInformation,
&processInformation,
)
if err != nil {
return 0, err
}
windows.CloseHandle(processInformation.Thread)
return processInformation.Process, nil
}