feat: add windows client installer
This commit is contained in:
33
README.md
33
README.md
@@ -53,6 +53,39 @@ docker compose -f docker-compose.client.yml logs -f
|
||||
docker compose -f docker-compose.client.yml restart
|
||||
```
|
||||
|
||||
## Windows: app proxy client
|
||||
|
||||
Windows mode restores the native workflow for Discord, Vesktop, games, and other apps that do not expose proxy settings.
|
||||
|
||||
Run PowerShell 7 as Administrator. While this branch is being tested, install from `codex-windows-client`:
|
||||
|
||||
```powershell
|
||||
irm https://git.dokops.ru/dokril/vpn-proxy/raw/branch/codex-windows-client/scripts/install-windows-client.ps1 | iex
|
||||
```
|
||||
|
||||
Installer modes:
|
||||
|
||||
- `Full install`: local native `sing-box.exe` on `127.0.0.1:1080` plus ProxiFyre/WinPacketFilter.
|
||||
- `ProxiFyre only`: ProxiFyre/WinPacketFilter only, pointed at an existing SOCKS5 proxy such as `127.0.0.1:8080` or `192.168.50.111:8080`.
|
||||
|
||||
The installer keeps profile data under `C:\Tools\vpn-proxy-windows\data`, so rerunning it can replace app files without deleting saved profiles.
|
||||
|
||||
Local UI:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:3456
|
||||
```
|
||||
|
||||
Recovery commands:
|
||||
|
||||
```powershell
|
||||
& "C:\Tools\vpn-proxy-windows\app\scripts\windows\manage.ps1" -OpenUi
|
||||
& "C:\Tools\vpn-proxy-windows\app\scripts\windows\manage.ps1" -Status
|
||||
& "C:\Tools\vpn-proxy-windows\app\scripts\windows\manage.ps1" -RestartServices
|
||||
```
|
||||
|
||||
The UI manages profiles made of process names, folders, and explicit `.exe` files. It generates ProxiFyre config and restarts ProxiFyre only when the user applies changes.
|
||||
|
||||
---
|
||||
|
||||
# VPN Proxy Gateway
|
||||
|
||||
296
scripts/install-windows-client.ps1
Normal file
296
scripts/install-windows-client.ps1
Normal file
@@ -0,0 +1,296 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
$InstallRoot = $env:VPN_PROXY_WINDOWS_ROOT
|
||||
if ([string]::IsNullOrWhiteSpace($InstallRoot)) { $InstallRoot = "C:\Tools\vpn-proxy-windows" }
|
||||
|
||||
$RepoBranch = $env:VPN_PROXY_WINDOWS_BRANCH
|
||||
if ([string]::IsNullOrWhiteSpace($RepoBranch)) { $RepoBranch = "codex-windows-client" }
|
||||
|
||||
$AppDir = Join-Path $InstallRoot "app"
|
||||
$DataDir = Join-Path $InstallRoot "data"
|
||||
$RuntimeDir = Join-Path $InstallRoot "runtime"
|
||||
$NodeDir = Join-Path $RuntimeDir "node"
|
||||
$SingBoxDir = Join-Path $RuntimeDir "sing-box"
|
||||
$ProxiFyreRoot = $env:PROXIFYRE_ROOT
|
||||
if ([string]::IsNullOrWhiteSpace($ProxiFyreRoot)) { $ProxiFyreRoot = "C:\Tools\ProxiFyre" }
|
||||
|
||||
$RepoZipUrl = "https://git.dokops.ru/dokril/vpn-proxy/archive/$RepoBranch.zip"
|
||||
$SingBoxVersion = "1.12.13"
|
||||
$SingBoxUrl = "https://github.com/SagerNet/sing-box/releases/download/v$SingBoxVersion/sing-box-$SingBoxVersion-windows-amd64.zip"
|
||||
$Headers = @{ "User-Agent" = "vpn-proxy-windows-installer" }
|
||||
|
||||
function Assert-Admin {
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw "Run PowerShell 7 as Administrator"
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-PowerShell7 {
|
||||
if ($PSVersionTable.PSVersion.Major -lt 7) {
|
||||
throw "PowerShell 7 is required"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Arch {
|
||||
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { return "arm64" }
|
||||
if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { return "x64" }
|
||||
return "x86"
|
||||
}
|
||||
|
||||
function Download-File {
|
||||
param([string]$Url, [string]$Destination)
|
||||
Write-Host "Downloading $Url"
|
||||
Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -Headers $Headers
|
||||
Unblock-File -Path $Destination -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Invoke-CheckedProcess {
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[string[]]$ArgumentList,
|
||||
[int[]]$AllowedExitCodes = @(0)
|
||||
)
|
||||
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -Wait -PassThru
|
||||
if ($AllowedExitCodes -notcontains $process.ExitCode) {
|
||||
throw "$FilePath failed with exit code $($process.ExitCode)"
|
||||
}
|
||||
return $process.ExitCode
|
||||
}
|
||||
|
||||
function Get-GitHubReleaseAsset {
|
||||
param(
|
||||
[string]$Repo,
|
||||
[scriptblock]$AssetFilter
|
||||
)
|
||||
|
||||
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -Headers $Headers
|
||||
$asset = @($release.assets | Where-Object $AssetFilter | Select-Object -First 1)
|
||||
if (-not $asset) {
|
||||
throw "No matching release asset found for $Repo"
|
||||
}
|
||||
return $asset[0]
|
||||
}
|
||||
|
||||
function Install-AppFiles {
|
||||
New-Item -ItemType Directory -Force -Path $InstallRoot, $DataDir, $RuntimeDir | Out-Null
|
||||
$zip = Join-Path $env:TEMP "vpn-proxy-windows.zip"
|
||||
$extract = Join-Path $env:TEMP "vpn-proxy-windows-extract"
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $extract -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Download-File -Url $RepoZipUrl -Destination $zip
|
||||
Expand-Archive -Path $zip -DestinationPath $extract -Force
|
||||
$source = Get-ChildItem $extract -Directory | Select-Object -First 1
|
||||
if (-not $source) { throw "Downloaded archive layout is not recognized" }
|
||||
|
||||
if (Test-Path $AppDir) {
|
||||
$backup = "$AppDir.backup"
|
||||
Remove-Item $backup -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Move-Item $AppDir $backup
|
||||
}
|
||||
|
||||
Move-Item $source.FullName $AppDir
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $extract -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Install-NodeRuntime {
|
||||
$existing = Get-Command node -ErrorAction SilentlyContinue
|
||||
if ($existing) { return $existing.Source }
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $RuntimeDir | Out-Null
|
||||
$arch = Get-Arch
|
||||
$index = Invoke-RestMethod -Uri "https://nodejs.org/dist/index.json" -Headers $Headers
|
||||
$release = @($index | Where-Object { $_.lts -ne $false } | Select-Object -First 1)[0]
|
||||
if (-not $release) { throw "Cannot resolve latest Node.js LTS release" }
|
||||
|
||||
$version = [string]$release.version
|
||||
$assetName = "node-$version-win-$arch.zip"
|
||||
$zip = Join-Path $env:TEMP $assetName
|
||||
$extract = Join-Path $env:TEMP "node-windows-extract"
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $extract -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $NodeDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Download-File -Url "https://nodejs.org/dist/$version/$assetName" -Destination $zip
|
||||
Expand-Archive -Path $zip -DestinationPath $extract -Force
|
||||
$nodeSource = Get-ChildItem $extract -Directory | Select-Object -First 1
|
||||
if (-not $nodeSource) { throw "Downloaded Node.js archive layout is not recognized" }
|
||||
Move-Item $nodeSource.FullName $NodeDir
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $extract -Recurse -Force -ErrorAction SilentlyContinue
|
||||
return (Join-Path $NodeDir "node.exe")
|
||||
}
|
||||
|
||||
function Get-NpmCommand {
|
||||
param([string]$NodeCommand)
|
||||
|
||||
$portableNpm = Join-Path (Split-Path -Parent $NodeCommand) "npm.cmd"
|
||||
if (Test-Path $portableNpm) { return $portableNpm }
|
||||
$existing = Get-Command npm -ErrorAction SilentlyContinue
|
||||
if ($existing) { return $existing.Source }
|
||||
throw "npm was not found"
|
||||
}
|
||||
|
||||
function Install-VisualCRedistributable {
|
||||
$arch = Get-Arch
|
||||
$vcArch = if ($arch -eq "arm64") { "arm64" } elseif ($arch -eq "x86") { "x86" } else { "x64" }
|
||||
$exe = Join-Path $env:TEMP "vc_redist.$vcArch.exe"
|
||||
Download-File -Url "https://aka.ms/vs/17/release/vc_redist.$vcArch.exe" -Destination $exe
|
||||
$code = Invoke-CheckedProcess -FilePath $exe -ArgumentList @("/install", "/quiet", "/norestart") -AllowedExitCodes @(0, 3010)
|
||||
if ($code -eq 3010) {
|
||||
Write-Warning "Visual C++ Redistributable requested a reboot"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-WinPacketFilter {
|
||||
$service = Get-Service -Name "ndisrd" -ErrorAction SilentlyContinue
|
||||
if ($service -and $service.Status -eq "Running") {
|
||||
Write-Host "WinPacketFilter driver is already running"
|
||||
return
|
||||
}
|
||||
|
||||
$arch = Get-Arch
|
||||
$assetToken = if ($arch -eq "arm64") { "ARM64" } elseif ($arch -eq "x86") { "x86" } else { "x64" }
|
||||
$asset = Get-GitHubReleaseAsset -Repo "wiresock/ndisapi" -AssetFilter {
|
||||
param($item)
|
||||
$item.name -match "\.msi$" -and $item.name -match $assetToken
|
||||
}
|
||||
|
||||
$msi = Join-Path $env:TEMP $asset.name
|
||||
Download-File -Url $asset.browser_download_url -Destination $msi
|
||||
$code = Invoke-CheckedProcess -FilePath "msiexec.exe" -ArgumentList @("/i", "`"$msi`"", "/qn", "/norestart") -AllowedExitCodes @(0, 3010)
|
||||
if ($code -eq 3010) {
|
||||
Write-Warning "WinPacketFilter requested a reboot before first use"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-ProxiFyre {
|
||||
New-Item -ItemType Directory -Force -Path $ProxiFyreRoot | Out-Null
|
||||
if (Test-Path (Join-Path $ProxiFyreRoot "ProxiFyre.exe")) {
|
||||
Write-Host "ProxiFyre is already installed at $ProxiFyreRoot"
|
||||
return
|
||||
}
|
||||
|
||||
$asset = Get-GitHubReleaseAsset -Repo "wiresock/proxifyre" -AssetFilter {
|
||||
param($item)
|
||||
$item.name -match "\.zip$" -and $item.name -notmatch "source"
|
||||
}
|
||||
|
||||
$zip = Join-Path $env:TEMP $asset.name
|
||||
$extract = Join-Path $env:TEMP "proxifyre-extract"
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $extract -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Download-File -Url $asset.browser_download_url -Destination $zip
|
||||
Expand-Archive -Path $zip -DestinationPath $extract -Force
|
||||
$exe = Get-ChildItem $extract -Recurse -Filter "ProxiFyre.exe" | Select-Object -First 1
|
||||
if (-not $exe) { throw "ProxiFyre.exe was not found in release archive" }
|
||||
|
||||
Copy-Item (Join-Path (Split-Path -Parent $exe.FullName) "*") $ProxiFyreRoot -Recurse -Force
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $extract -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Install-SingBox {
|
||||
New-Item -ItemType Directory -Force -Path $SingBoxDir | Out-Null
|
||||
if (Test-Path (Join-Path $SingBoxDir "sing-box.exe")) { return }
|
||||
|
||||
$zip = Join-Path $env:TEMP "sing-box-windows.zip"
|
||||
$extract = Join-Path $env:TEMP "sing-box-windows-extract"
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $extract -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Download-File -Url $SingBoxUrl -Destination $zip
|
||||
Expand-Archive -Path $zip -DestinationPath $extract -Force
|
||||
$exe = Get-ChildItem $extract -Recurse -Filter "sing-box.exe" | Select-Object -First 1
|
||||
if (-not $exe) { throw "sing-box.exe was not found in archive" }
|
||||
Copy-Item $exe.FullName (Join-Path $SingBoxDir "sing-box.exe") -Force
|
||||
}
|
||||
|
||||
function Select-InstallMode {
|
||||
Write-Host ""
|
||||
Write-Host "Choose install mode:"
|
||||
Write-Host " [1] Full install: local sing-box + ProxiFyre"
|
||||
Write-Host " [2] ProxiFyre only: use existing proxy target"
|
||||
$choice = Read-Host "Mode [1]"
|
||||
if ($choice -eq "2") { return "proxifyre-only" }
|
||||
return "full"
|
||||
}
|
||||
|
||||
function Test-TcpEndpoint {
|
||||
param([string]$HostName, [int]$Port)
|
||||
|
||||
$client = [System.Net.Sockets.TcpClient]::new()
|
||||
try {
|
||||
$task = $client.ConnectAsync($HostName, $Port)
|
||||
if (-not $task.Wait(2000)) { return $false }
|
||||
return $client.Connected
|
||||
} finally {
|
||||
$client.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Write-InitialTargets {
|
||||
param([string]$Mode)
|
||||
|
||||
$targetsPath = Join-Path $DataDir "proxy-targets.json"
|
||||
if (Test-Path $targetsPath) { return }
|
||||
|
||||
if ($Mode -eq "proxifyre-only") {
|
||||
$target = Read-Host "Existing SOCKS5 proxy target host:port"
|
||||
if ($target -notmatch "^([^:]+):(\d+)$") { throw "Expected host:port" }
|
||||
$hostName = $matches[1]
|
||||
$port = [int]$matches[2]
|
||||
if (-not (Test-TcpEndpoint -HostName $hostName -Port $port)) {
|
||||
Write-Warning "Proxy target $target did not accept a TCP connection during install"
|
||||
}
|
||||
@(@{ id = "existing-proxy"; name = "Existing proxy"; protocol = "socks5"; host = $hostName; port = $port }) |
|
||||
ConvertTo-Json -Depth 5 |
|
||||
Set-Content $targetsPath -Encoding UTF8
|
||||
}
|
||||
}
|
||||
|
||||
function Install-NodeDependencies {
|
||||
$node = Install-NodeRuntime
|
||||
$npm = Get-NpmCommand -NodeCommand $node
|
||||
$env:PATH = "$(Split-Path -Parent $node);$env:PATH"
|
||||
Push-Location $AppDir
|
||||
try {
|
||||
& $npm install
|
||||
if ($LASTEXITCODE -ne 0) { throw "npm install failed" }
|
||||
& $npm run build
|
||||
if ($LASTEXITCODE -ne 0) { throw "npm run build failed" }
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Start-Ui {
|
||||
$manage = Join-Path $AppDir "scripts\windows\manage.ps1"
|
||||
Start-Process pwsh -ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$manage`"", "-OpenUi"
|
||||
}
|
||||
|
||||
Assert-Admin
|
||||
Assert-PowerShell7
|
||||
$mode = Select-InstallMode
|
||||
Install-AppFiles
|
||||
Install-NodeDependencies
|
||||
Install-VisualCRedistributable
|
||||
Install-WinPacketFilter
|
||||
Install-ProxiFyre
|
||||
if ($mode -eq "full") { Install-SingBox }
|
||||
Write-InitialTargets -Mode $mode
|
||||
Set-Content -Path (Join-Path $DataDir "windows-state.json") -Encoding UTF8 -Value (@{ installMode = $mode } | ConvertTo-Json)
|
||||
Start-Ui
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "VPN Proxy Windows is installed."
|
||||
Write-Host "UI: http://127.0.0.1:3456"
|
||||
Write-Host "Recovery:"
|
||||
Write-Host "& `"$AppDir\scripts\windows\manage.ps1`" -OpenUi"
|
||||
Write-Host "& `"$AppDir\scripts\windows\manage.ps1`" -Status"
|
||||
Write-Host "& `"$AppDir\scripts\windows\manage.ps1`" -RestartServices"
|
||||
@@ -31,6 +31,7 @@ $env:PROXY_BIND_IP = "127.0.0.1"
|
||||
$env:SING_BOX_CONFIG = Join-Path $Root "runtime\sing-box\config.json"
|
||||
$env:SING_BOX_CACHE = Join-Path $Root "runtime\sing-box\cache.db"
|
||||
$env:WINDOWS_HELPER = Join-Path $AppDir "scripts\windows\helper.ps1"
|
||||
$env:PATH = "$(Join-Path $Root "runtime\sing-box");$env:PATH"
|
||||
|
||||
function Get-NodeCommand {
|
||||
$portable = Join-Path $Root "runtime\node\node.exe"
|
||||
|
||||
Reference in New Issue
Block a user