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 Remove-ManagedFirewallRules { $removed = @() foreach ($name in @("ProxyWarden.ProxiFyre.Inbound", "ProxyWarden.ProxiFyre.Outbound")) { $rule = Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue if ($null -eq $rule) { continue } $rule | Remove-NetFirewallRule -ErrorAction Stop $removed += $name } return $removed } 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.removedProxiFyreFirewallRules = Remove-ManagedFirewallRules $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 }