param( [string]$InstallRoot = "C:\Program Files\ProxyWarden\sing-box", [string]$ServiceName = "ProxyWardenSingBox", [string]$ConfigSource = "C:\ProgramData\ProxyWarden\generated\sing-box-config.json", [switch]$PlanOnly, [switch]$Force, [switch]$Uninstall ) $ErrorActionPreference = "Stop" $SingBoxReleaseApi = "https://api.github.com/repos/SagerNet/sing-box/releases/latest" $WinSwReleaseApi = "https://api.github.com/repos/winsw/winsw/releases/latest" $WrapperFile = "$ServiceName.exe" $ConfigFile = "config.json" function New-Result { param( [bool]$Success, [string]$Action, [bool]$Changed, [string]$Message, [hashtable]$Details = @{} ) [ordered]@{ success = $Success action = $Action changed = $Changed message = $Message details = $Details } | ConvertTo-Json -Depth 8 } function Test-IsAdministrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Get-NativeArchitecture { $processor = Get-CimInstance Win32_Processor | Select-Object -First 1 if ($null -ne $processor -and $processor.Architecture -eq 12) { return "arm64" } if ([Environment]::Is64BitOperatingSystem) { return "amd64" } return "386" } function Get-WinSwArchitecture { param([string]$Arch) if ($Arch -eq "arm64") { return "arm64" } if ($Arch -eq "386") { return "x86" } return "x64" } function Invoke-Download { param([string]$Uri, [string]$Path) Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $Path -Headers @{ "User-Agent" = "proxywarden" } } function Select-Asset { param( [object[]]$Assets, [string]$Pattern, [string]$Label ) $asset = $Assets | Where-Object { $_.name -match $Pattern } | Select-Object -First 1 if ($null -eq $asset) { throw "Не найден release asset для $Label по шаблону $Pattern." } return $asset } function Test-SafeInstallRoot { param([string]$Path) $full = [System.IO.Path]::GetFullPath($Path).TrimEnd("\") $leaf = Split-Path -Leaf $full $parent = Split-Path -Parent $full if ($leaf -ne "sing-box") { return $false } return $parent -match "\\ProxyWarden$|\\proxywarden$" } function Backup-File { param([string]$Path) if (Test-Path -LiteralPath $Path) { $backup = "$Path.bak" Copy-Item -LiteralPath $Path -Destination $backup -Force return $backup } return $null } function Write-Utf8NoBomFile { param( [string]$Path, [string]$Value ) $encoding = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText($Path, $Value, $encoding) } function Write-WinSwConfig { param( [string]$Root, [string]$Name ) $xmlPath = Join-Path $Root "$Name.xml" $logDir = Join-Path $Root "logs" New-Item -ItemType Directory -Path $logDir -Force | Out-Null $xml = @" $Name ProxyWarden Local sing-box Local sing-box runtime managed by ProxyWarden. %BASE%\sing-box.exe run -c "%BASE%\config.json" %BASE%\logs 10485760 4 "@ Write-Utf8NoBomFile -Path $xmlPath -Value $xml return $xmlPath } function Stop-And-Uninstall-Service { param( [string]$Root, [string]$Name ) $wrapper = Join-Path $Root "$Name.exe" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($null -ne $service -and $service.Status -ne "Stopped") { Stop-Service -Name $Name -Force -ErrorAction SilentlyContinue $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($null -ne $service) { try { $service.WaitForStatus("Stopped", [TimeSpan]::FromSeconds(15)) } catch {} } } if (Test-Path -LiteralPath $wrapper) { Push-Location $Root try { & $wrapper uninstall | Out-Null } finally { Pop-Location } } $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($null -ne $service) { sc.exe delete $Name | Out-Null } } try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $installRootFull = [System.IO.Path]::GetFullPath($InstallRoot) $details = @{ installRoot = $installRootFull serviceName = $ServiceName configSource = $ConfigSource singboxReleaseApi = $SingBoxReleaseApi winswReleaseApi = $WinSwReleaseApi planOnly = [bool]$PlanOnly uninstall = [bool]$Uninstall } if ($PlanOnly) { $details.items = @( @{ id = "sing-box-binary"; name = "sing-box.exe"; source = $SingBoxReleaseApi; target = (Join-Path $installRootFull "sing-box.exe") }, @{ id = "winsw-wrapper"; name = $WrapperFile; source = $WinSwReleaseApi; target = (Join-Path $installRootFull $WrapperFile) }, @{ id = "windows-service"; name = $ServiceName; target = "Windows Service" }, @{ id = "config"; name = $ConfigFile; source = $ConfigSource; target = (Join-Path $installRootFull $ConfigFile) } ) New-Result -Success $true -Action "install-singbox.plan" -Changed $false -Message "Local sing-box install plan is ready." -Details $details exit 0 } if (-not (Test-IsAdministrator)) { New-Result -Success $false -Action "install-singbox" -Changed $false -Message "Administrator rights are required." -Details $details exit 1 } if ($Uninstall) { if (-not (Test-SafeInstallRoot -Path $installRootFull)) { New-Result -Success $false -Action "uninstall-singbox" -Changed $false -Message "Unsafe InstallRoot for recursive uninstall." -Details $details exit 2 } Stop-And-Uninstall-Service -Root $installRootFull -Name $ServiceName if (Test-Path -LiteralPath $installRootFull) { Remove-Item -LiteralPath $installRootFull -Recurse -Force } New-Result -Success $true -Action "uninstall-singbox" -Changed $true -Message "Local sing-box service and install folder were removed." -Details $details exit 0 } $changed = $false New-Item -ItemType Directory -Path $installRootFull -Force | Out-Null $workDir = Join-Path ([System.IO.Path]::GetTempPath()) ("proxywarden-singbox-" + [guid]::NewGuid().ToString("N")) $extractDir = Join-Path $workDir "extract" New-Item -ItemType Directory -Path $extractDir -Force | Out-Null try { $arch = Get-NativeArchitecture $winswArch = Get-WinSwArchitecture -Arch $arch $details.architecture = $arch $details.winswArchitecture = $winswArch $singboxRelease = Invoke-RestMethod -Uri $SingBoxReleaseApi -Headers @{ "User-Agent" = "proxywarden" } $singboxAsset = Select-Asset $singboxRelease.assets "windows-$arch\.zip$" "sing-box" $singboxZip = Join-Path $workDir $singboxAsset.name Invoke-Download $singboxAsset.browser_download_url $singboxZip Expand-Archive -LiteralPath $singboxZip -DestinationPath $extractDir -Force $singboxExe = Get-ChildItem -LiteralPath $extractDir -Recurse -Filter "sing-box.exe" | Select-Object -First 1 if ($null -eq $singboxExe) { throw "В архиве sing-box не найден sing-box.exe." } Copy-Item -LiteralPath $singboxExe.FullName -Destination (Join-Path $installRootFull "sing-box.exe") -Force $changed = $true $winswRelease = Invoke-RestMethod -Uri $WinSwReleaseApi -Headers @{ "User-Agent" = "proxywarden" } $winswAsset = Select-Asset $winswRelease.assets "WinSW-$winswArch\.exe$" "WinSW" Invoke-Download $winswAsset.browser_download_url (Join-Path $installRootFull $WrapperFile) $changed = $true $configTarget = Join-Path $installRootFull $ConfigFile $backupPath = Backup-File -Path $configTarget if ($backupPath) { $details.backupPath = $backupPath } if (Test-Path -LiteralPath $ConfigSource) { Copy-Item -LiteralPath $ConfigSource -Destination $configTarget -Force } elseif (-not (Test-Path -LiteralPath $configTarget)) { Write-Utf8NoBomFile -Path $configTarget -Value '{"log":{"level":"info","timestamp":true},"inbounds":[],"outbounds":[{"type":"direct","tag":"direct"}],"route":{"final":"direct"}}' } $xmlPath = Write-WinSwConfig -Root $installRootFull -Name $ServiceName $details.configPath = $configTarget $details.wrapperConfigPath = $xmlPath if ($Force) { Stop-And-Uninstall-Service -Root $installRootFull -Name $ServiceName } Push-Location $installRootFull try { $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue if ($null -eq $service) { & ".\$WrapperFile" install if ($LASTEXITCODE -ne 0) { throw "WinSW install завершился с кодом $LASTEXITCODE." } $changed = $true } & ".\$WrapperFile" start if ($LASTEXITCODE -ne 0) { Start-Service -Name $ServiceName -ErrorAction Stop } } finally { Pop-Location } } finally { if (Test-Path -LiteralPath $workDir) { Remove-Item -LiteralPath $workDir -Recurse -Force -ErrorAction SilentlyContinue } } New-Result -Success $true -Action "install-singbox" -Changed $changed -Message "Local sing-box service is installed and started." -Details $details } catch { New-Result -Success $false -Action "install-singbox" -Changed $false -Message $_.Exception.Message exit 1 }