Refactor proxy routing and session management

This commit is contained in:
2026-07-08 00:09:38 +03:00
parent c5bdb10445
commit b45dd2ae05
26 changed files with 5193 additions and 307 deletions

View File

@@ -1,13 +1,19 @@
param(
[string]$InstallRoot = "C:\Program Files\VpnProxy\sing-box",
[string]$BinaryPath = "",
[string]$ServiceName = "VpnProxySingBox",
[string]$ConfigSource = "C:\ProgramData\VpnProxy\generated\sing-box-config.json",
[switch]$PlanOnly,
[switch]$Force
[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,
@@ -23,7 +29,7 @@ function New-Result {
changed = $Changed
message = $Message
details = $Details
} | ConvertTo-Json -Depth 6
} | ConvertTo-Json -Depth 8
}
function Test-IsAdministrator {
@@ -32,6 +38,48 @@ function Test-IsAdministrator {
$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) {
@@ -42,16 +90,92 @@ function Backup-File {
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 = $InstallRoot
binaryPath = $BinaryPath
installRoot = $installRootFull
serviceName = $ServiceName
configSource = $ConfigSource
singboxReleaseApi = $SingBoxReleaseApi
winswReleaseApi = $WinSwReleaseApi
planOnly = [bool]$PlanOnly
uninstall = [bool]$Uninstall
}
if ($PlanOnly) {
New-Result -Success $true -Action "install-singbox" -Changed $false -Message "Local sing-box install plan is ready." -Details $details
$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
}
@@ -60,36 +184,86 @@ try {
exit 1
}
if ([string]::IsNullOrWhiteSpace($BinaryPath) -or -not (Test-Path -LiteralPath $BinaryPath)) {
New-Result -Success $false -Action "install-singbox" -Changed $false -Message "BinaryPath is required and must point to sing-box.exe." -Details $details
exit 2
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
if (-not (Test-Path -LiteralPath $InstallRoot)) {
New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null
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
}
$configPath = Join-Path $InstallRoot "config.json"
$backupPath = Backup-File -Path $configPath
if ($backupPath) {
$details.backupPath = $backupPath
}
$markerPath = Join-Path $InstallRoot "install-singbox.marker.json"
if ((-not (Test-Path -LiteralPath $markerPath)) -or $Force) {
@{
component = "singbox"
binaryPath = $BinaryPath
serviceName = $ServiceName
installedAt = (Get-Date).ToString("o")
} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $markerPath -Encoding UTF8
$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
}
}
$details.markerPath = $markerPath
New-Result -Success $true -Action "install-singbox" -Changed $changed -Message "Local sing-box install boundary completed." -Details $details
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