Refine ProxyWarden routing and config flows
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
param(
|
||||
[string]$InstallRoot = "",
|
||||
[switch]$ForceRemoveWindowsPacketFilter
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function New-Result {
|
||||
param(
|
||||
[bool]$Success,
|
||||
[string]$Message,
|
||||
[hashtable]$Details = @{}
|
||||
)
|
||||
|
||||
[ordered]@{
|
||||
success = $Success
|
||||
message = $Message
|
||||
details = $Details
|
||||
} | ConvertTo-Json -Depth 8 -Compress
|
||||
}
|
||||
|
||||
function Get-FullPath([string]$Path) {
|
||||
return [System.IO.Path]::GetFullPath($Path).TrimEnd("\")
|
||||
}
|
||||
|
||||
function Test-PathInside([string]$Path, [string]$Root) {
|
||||
if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
|
||||
try {
|
||||
$fullPath = Get-FullPath $Path
|
||||
$fullRoot = Get-FullPath $Root
|
||||
return $fullPath.StartsWith($fullRoot + "\", [StringComparison]::OrdinalIgnoreCase)
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-SafeInstallRoot([string]$Root) {
|
||||
if ([string]::IsNullOrWhiteSpace($Root)) {
|
||||
throw "InstallRoot is empty."
|
||||
}
|
||||
|
||||
$full = Get-FullPath $Root
|
||||
if ($full -match "^[A-Za-z]:\\?$") {
|
||||
throw "Refusing to use drive root as InstallRoot: $full"
|
||||
}
|
||||
if ($full -match "\\Windows($|\\)" -or $full -match "\\ProgramData$" -or $full -match "\\Users$") {
|
||||
throw "Refusing unsafe InstallRoot: $full"
|
||||
}
|
||||
|
||||
$knownAppFiles = @(
|
||||
(Join-Path $full "proxywarden.exe"),
|
||||
(Join-Path $full "uninstall.exe"),
|
||||
(Join-Path $full "bundled\cleanup\uninstall-managed-components.ps1")
|
||||
)
|
||||
foreach ($candidate in $knownAppFiles) {
|
||||
if (Test-Path -LiteralPath $candidate) { return $full }
|
||||
}
|
||||
|
||||
throw "InstallRoot does not look like a ProxyWarden install directory: $full"
|
||||
}
|
||||
|
||||
function Resolve-SafeComponentDir([string]$Root, [string]$Leaf) {
|
||||
$componentRoot = Join-Path $Root "components"
|
||||
$path = Join-Path $componentRoot $Leaf
|
||||
$full = Get-FullPath $path
|
||||
$expectedParent = Get-FullPath $componentRoot
|
||||
$actualLeaf = Split-Path -Leaf $full
|
||||
|
||||
if ($actualLeaf -ne $Leaf) {
|
||||
throw "Unexpected component directory leaf: $full"
|
||||
}
|
||||
if (-not $full.StartsWith($expectedParent + "\", [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Component directory is outside ProxyWarden components root: $full"
|
||||
}
|
||||
|
||||
return $full
|
||||
}
|
||||
|
||||
function Read-ComponentMarker([string]$Dir) {
|
||||
$markerPath = Join-Path $Dir "proxywarden-component.json"
|
||||
if (-not (Test-Path -LiteralPath $markerPath)) { return $null }
|
||||
try {
|
||||
return Get-Content -LiteralPath $markerPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-MarkerBool($Marker, [string]$Name) {
|
||||
if ($null -eq $Marker) { return $false }
|
||||
$property = $Marker.PSObject.Properties[$Name]
|
||||
if ($null -eq $property) { return $false }
|
||||
return [bool]$property.Value
|
||||
}
|
||||
|
||||
function Get-ServiceRecord([string]$Name) {
|
||||
$escaped = $Name.Replace("'", "''")
|
||||
return Get-CimInstance Win32_Service -Filter "Name='$escaped'" -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Get-ServiceImagePath($Record) {
|
||||
if ($null -eq $Record -or [string]::IsNullOrWhiteSpace([string]$Record.PathName)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$pathName = ([string]$Record.PathName).Trim()
|
||||
if ($pathName -match '^"([^"]+)"') { return $Matches[1] }
|
||||
if ($pathName -match '^(.+?\.exe)\b') { return $Matches[1].Trim() }
|
||||
return $pathName
|
||||
}
|
||||
|
||||
function Stop-ServiceRecord($Record) {
|
||||
if ($null -eq $Record) { return }
|
||||
|
||||
$service = Get-Service -Name $Record.Name -ErrorAction SilentlyContinue
|
||||
if ($null -ne $service -and $service.Status -ne "Stopped") {
|
||||
Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue
|
||||
$service = Get-Service -Name $Record.Name -ErrorAction SilentlyContinue
|
||||
if ($null -ne $service) {
|
||||
try { $service.WaitForStatus("Stopped", [TimeSpan]::FromSeconds(12)) } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
$recordAfterStop = Get-ServiceRecord $Record.Name
|
||||
if ($null -ne $recordAfterStop -and [int]$recordAfterStop.ProcessId -gt 0) {
|
||||
taskkill.exe /PID ([int]$recordAfterStop.ProcessId) /F | Out-Null
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-ManagedService {
|
||||
param(
|
||||
[string[]]$Names,
|
||||
[string]$InstallRoot,
|
||||
[string]$UninstallExe = ""
|
||||
)
|
||||
|
||||
$removed = @()
|
||||
foreach ($name in $Names) {
|
||||
$record = Get-ServiceRecord $name
|
||||
if ($null -eq $record) { continue }
|
||||
|
||||
$imagePath = Get-ServiceImagePath $record
|
||||
if (-not [string]::IsNullOrWhiteSpace($imagePath) -and -not (Test-PathInside $imagePath $InstallRoot)) {
|
||||
continue
|
||||
}
|
||||
|
||||
Stop-ServiceRecord $record
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($UninstallExe) -and (Test-Path -LiteralPath $UninstallExe)) {
|
||||
Push-Location (Split-Path -Parent $UninstallExe)
|
||||
try { & $UninstallExe uninstall | Out-Null } finally { Pop-Location }
|
||||
}
|
||||
|
||||
$record = Get-ServiceRecord $name
|
||||
if ($null -ne $record) {
|
||||
sc.exe delete $name | Out-Null
|
||||
}
|
||||
$removed += $name
|
||||
}
|
||||
|
||||
return $removed
|
||||
}
|
||||
|
||||
function Remove-SafeDirectory([string]$Path, [string]$Root) {
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return $false }
|
||||
if (-not (Test-PathInside $Path $Root)) {
|
||||
throw "Refusing to remove directory outside InstallRoot: $Path"
|
||||
}
|
||||
Remove-Item -LiteralPath $Path -Recurse -Force
|
||||
return $true
|
||||
}
|
||||
|
||||
function Get-InstalledProgram([string]$Pattern) {
|
||||
$paths = @(
|
||||
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
return Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.DisplayName -match $Pattern } |
|
||||
Select-Object -First 1 DisplayName, DisplayVersion, PSChildName, UninstallString, QuietUninstallString
|
||||
}
|
||||
|
||||
function Resolve-MsiProductCode($Program, [string]$Label) {
|
||||
if ($null -eq $Program) { return $null }
|
||||
if ($Program.PSChildName -match "^\{[0-9A-Fa-f-]{36}\}$") {
|
||||
return $Program.PSChildName
|
||||
}
|
||||
foreach ($candidate in @($Program.QuietUninstallString, $Program.UninstallString)) {
|
||||
if ($candidate -match "\{[0-9A-Fa-f-]{36}\}") {
|
||||
return $Matches[0]
|
||||
}
|
||||
}
|
||||
throw "Could not resolve MSI product code for $Label."
|
||||
}
|
||||
|
||||
function Uninstall-MsiProgram($Program, [string]$Label) {
|
||||
$productCode = Resolve-MsiProductCode $Program $Label
|
||||
if ([string]::IsNullOrWhiteSpace($productCode)) { return $false }
|
||||
|
||||
$logPath = Join-Path ([System.IO.Path]::GetTempPath()) "proxywarden-$Label-uninstall.log"
|
||||
$process = Start-Process -FilePath "msiexec.exe" -ArgumentList @("/x", $productCode, "/qn", "/norestart", "/L*v", $logPath) -Wait -PassThru -WindowStyle Hidden
|
||||
if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010 -and $process.ExitCode -ne 1605) {
|
||||
throw "$Label uninstall exited with code $($process.ExitCode). MSI log: $logPath"
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
try {
|
||||
$details = @{}
|
||||
$root = Assert-SafeInstallRoot $InstallRoot
|
||||
$details.installRoot = $root
|
||||
|
||||
$proxifyreDir = Resolve-SafeComponentDir $root "ProxiFyre"
|
||||
$singboxDir = Resolve-SafeComponentDir $root "sing-box"
|
||||
$proxifyreMarker = Read-ComponentMarker $proxifyreDir
|
||||
$removePacketFilter = [bool]$ForceRemoveWindowsPacketFilter -or (Get-MarkerBool $proxifyreMarker "packetFilterInstalledByProxyWarden")
|
||||
|
||||
$details.removedProxiFyreServices = Remove-ManagedService -Names @("ProxiFyreService", "ProxiFyre") -InstallRoot $root -UninstallExe (Join-Path $proxifyreDir "ProxiFyre.exe")
|
||||
$details.removedSingBoxServices = Remove-ManagedService -Names @("ProxyWardenSingBox") -InstallRoot $root -UninstallExe (Join-Path $singboxDir "ProxyWardenSingBox.exe")
|
||||
$details.removedProxiFyreDir = Remove-SafeDirectory $proxifyreDir $root
|
||||
$details.removedSingBoxDir = Remove-SafeDirectory $singboxDir $root
|
||||
|
||||
if ($removePacketFilter) {
|
||||
$packetFilter = Get-InstalledProgram "Windows Packet Filter|WinpkFilter|NDISAPI"
|
||||
$details.removedWindowsPacketFilter = Uninstall-MsiProgram $packetFilter "windows-packet-filter"
|
||||
} else {
|
||||
$details.removedWindowsPacketFilter = $false
|
||||
}
|
||||
|
||||
New-Result -Success $true -Message "ProxyWarden managed components cleanup completed." -Details $details
|
||||
exit 0
|
||||
} catch {
|
||||
New-Result -Success $false -Message $_.Exception.Message -Details @{}
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
!macro NSIS_HOOK_PREUNINSTALL
|
||||
DetailPrint "ProxyWarden: cleaning managed components"
|
||||
nsExec::ExecToLog 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\bundled\cleanup\uninstall-managed-components.ps1" -InstallRoot "$INSTDIR"'
|
||||
Pop $0
|
||||
DetailPrint "ProxyWarden cleanup exit code: $0"
|
||||
!macroend
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"generatedAt": "2026-07-09T16:13:27.3087159Z",
|
||||
"architectures": [
|
||||
"x64"
|
||||
],
|
||||
"proxifyreRelease": "v2.2.1",
|
||||
"windowsPacketFilterRelease": "v3.6.2",
|
||||
"files": [
|
||||
{
|
||||
"id": "proxifyre-x64",
|
||||
"name": "ProxiFyre-v2.2.1-x64-signed.zip",
|
||||
"sha256": "c38ca1caa68cd730712f5c0911e4240711bf9e7684988ae64ed04ec693cce899",
|
||||
"size": 1372483,
|
||||
"sourceUrl": "https://github.com/wiresock/proxifyre/releases/download/v2.2.1/ProxiFyre-v2.2.1-x64-signed.zip"
|
||||
},
|
||||
{
|
||||
"id": "packet-filter-x64",
|
||||
"name": "Windows.Packet.Filter.3.6.2.1.x64.msi",
|
||||
"sha256": "9c388c0b7f189f7fa98720bae2caecf7d64f30910838b80b438ecf8956b8502c",
|
||||
"size": 819200,
|
||||
"sourceUrl": "https://github.com/wiresock/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi"
|
||||
},
|
||||
{
|
||||
"id": "vc-runtime-x64",
|
||||
"name": "vc_redist.x64.exe",
|
||||
"sha256": "843068991daaa1f73ad9f6239bce4d0f6a07a51f18c37ea2a867e9beca71295c",
|
||||
"size": 18731856,
|
||||
"sourceUrl": "https://aka.ms/vc14/vc_redist.x64.exe"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user