Files
harbor-net/apps/windows-client/scripts/install-singbox.ps1

271 lines
9.3 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
param(
[string]$InstallRoot = "C:\Program Files\VpnProxy\sing-box",
[string]$ServiceName = "VpnProxySingBox",
[string]$ConfigSource = "C:\ProgramData\VpnProxy\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" = "vpn-proxy-windows-client" }
}
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 "\\VpnProxy$|\\vpn-proxy$"
}
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 = @"
<service>
<id>$Name</id>
<name>VPN Proxy Local sing-box</name>
<description>Local sing-box runtime managed by VPN Proxy Windows client.</description>
<executable>%BASE%\sing-box.exe</executable>
<arguments>run -c "%BASE%\config.json"</arguments>
<logpath>%BASE%\logs</logpath>
<log mode="roll-by-size">
<sizeThreshold>10485760</sizeThreshold>
<keepFiles>4</keepFiles>
</log>
<onfailure action="restart" delay="5 sec"/>
</service>
"@
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()) ("vpn-proxy-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" = "vpn-proxy-windows-client" }
$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" = "vpn-proxy-windows-client" }
$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
}