100 lines
6.2 KiB
PowerShell
100 lines
6.2 KiB
PowerShell
[CmdletBinding()]
|
|
param([switch]$Prepare, [switch]$InstallUpdate)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
|
$app = Join-Path $env:ProgramFiles 'QVPN/app/QVPN.exe'
|
|
$report = Join-Path $repo 'research/installed-startup-verification.json'
|
|
$preparedReport = Join-Path $repo 'research/startup-prepared-verification.json'
|
|
$testDll = Join-Path $repo 'tests/QVPN.Tests/bin/Release/net10.0-windows/QVPN.Tests.dll'
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$administrator = ([Security.Principal.WindowsPrincipal]::new($identity)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
|
|
function Read-Status {
|
|
$raw = & dotnet $testDll --client status
|
|
if ($LASTEXITCODE -ne 0) { throw 'The service status pipe is unavailable' }
|
|
$response = $raw | ConvertFrom-Json
|
|
if (!$response.Ok -or !$response.Status) { throw 'The service status request failed' }
|
|
return $response.Status
|
|
}
|
|
|
|
if ($Prepare) {
|
|
if (!$administrator) { throw 'Preparation requires an administrator token' }
|
|
try {
|
|
$before = Read-Status
|
|
if (!$before.DesiredConnected -or $before.Nodes.Count -eq 0) { throw 'This test requires an existing connected subscription' }
|
|
if ($InstallUpdate) {
|
|
$version = ([xml](Get-Content (Join-Path $repo 'Directory.Build.props') -Raw)).Project.PropertyGroup.Version
|
|
$setup = Join-Path $repo "dist/QVPN-Setup-$version.exe"
|
|
$expected = (Get-Content (Join-Path $repo "dist/QVPN-Setup-$version.sha256") -Raw).Split(' ')[0]
|
|
if ((Get-FileHash -LiteralPath $setup).Hash -ne $expected) { throw 'Installer checksum mismatch' }
|
|
$installer = Start-Process -FilePath $setup -ArgumentList '--silent' -WindowStyle Hidden -PassThru -Wait
|
|
if ($installer.ExitCode -ne 0) { throw "Update failed: $($installer.ExitCode)" }
|
|
}
|
|
$configuration = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\QVPNService'
|
|
if ($configuration.Start -ne 2 -or $configuration.DelayedAutoStart -eq 1) { throw 'The service still uses delayed or non-automatic startup' }
|
|
foreach ($process in (Get-Process -Name QVPN -ErrorAction SilentlyContinue)) {
|
|
if ($process.Path -eq $app) { Stop-Process -Id $process.Id -Force }
|
|
}
|
|
$service = Get-Service QVPNService
|
|
Stop-Service -Name QVPNService
|
|
$service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(30))
|
|
[pscustomobject]@{Prepared=$true;Nodes=$before.Nodes.Count;DesiredConnected=$before.DesiredConnected;DelayedAutoStart=$false;At=[DateTimeOffset]::Now.ToString('o')} |
|
|
ConvertTo-Json | Set-Content -LiteralPath $preparedReport -Encoding utf8
|
|
} catch {
|
|
Start-Service QVPNService -ErrorAction SilentlyContinue
|
|
[pscustomobject]@{Prepared=$false;Failure=$_.Exception.Message} | ConvertTo-Json | Set-Content -LiteralPath $preparedReport -Encoding utf8
|
|
exit 1
|
|
}
|
|
exit 0
|
|
}
|
|
|
|
if ($administrator) { throw 'Run verification from a regular Windows user token; only the preparation helper is elevated' }
|
|
try {
|
|
$arguments = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $PSCommandPath, '-Prepare')
|
|
if ($InstallUpdate) { $arguments += '-InstallUpdate' }
|
|
$helper = Start-Process -FilePath (Join-Path $PSHOME 'pwsh.exe') -Verb RunAs -WindowStyle Hidden -ArgumentList $arguments -PassThru
|
|
$helper.WaitForExit()
|
|
if ($helper.ExitCode -ne 0) { throw 'Elevated preparation did not complete' }
|
|
$prepared = Get-Content -LiteralPath $preparedReport -Raw | ConvertFrom-Json
|
|
if (!$prepared.Prepared) { throw 'Service stop/update preparation failed' }
|
|
|
|
# Capture is read-only and does not trigger service startup.
|
|
$capture = Start-Process -FilePath $app -ArgumentList '--capture-current', (Join-Path $repo 'research/startup-waiting.png') -PassThru -Wait
|
|
if ($capture.ExitCode -ne 1 -or (Get-Service QVPNService).Status -ne 'Stopped') { throw 'Read-only capture unexpectedly started the service' }
|
|
|
|
$timer = [Diagnostics.Stopwatch]::StartNew()
|
|
Start-Process -FilePath $app -ArgumentList '--tray' | Out-Null
|
|
$status = $null
|
|
do {
|
|
Start-Sleep -Seconds 1
|
|
try { $status = Read-Status } catch { $status = $null }
|
|
if ($status -and $status.State -eq 'connected' -and $status.DesiredConnected -and $status.EnginePid) { break }
|
|
} while ($timer.Elapsed.TotalSeconds -lt 60)
|
|
if (!$status -or $status.State -ne 'connected') { throw 'The regular user interface did not automatically start/reconnect the stopped service' }
|
|
if ($status.Nodes.Count -ne $prepared.Nodes) { throw 'The cached subscription was not preserved' }
|
|
$recoverySeconds = [Math]::Round($timer.Elapsed.TotalSeconds,1)
|
|
$http = $null
|
|
foreach ($endpoint in @('https://cp.cloudflare.com/generate_204','https://www.gstatic.com/generate_204')) {
|
|
$code = & curl.exe --silent --show-error --noproxy '*' --max-time 15 --output NUL --write-out '%{http_code}' $endpoint
|
|
if ($LASTEXITCODE -eq 0 -and $code -eq '204') { $http = $code; break }
|
|
}
|
|
if ($http -ne '204') { throw 'System HTTPS through the restored TUN failed on both health endpoints' }
|
|
$capture = Start-Process -FilePath $app -ArgumentList '--capture-current', (Join-Path $repo 'research/startup-connected.png') -PassThru -Wait
|
|
if ($capture.ExitCode -ne 0) { throw 'The connected UI capture failed' }
|
|
[pscustomobject]@{
|
|
Completed=$true;At=[DateTimeOffset]::Now.ToString('o');Version=(Get-Item $app).VersionInfo.FileVersion
|
|
DelayedAutoStart=$false;ClientElevated=$administrator;RegularUserStartedService=$true
|
|
Seconds=$recoverySeconds;State=$status.State;Nodes=$status.Nodes.Count
|
|
KeyEnteredAgain=$false;HttpCode=$http;FullWindowsRebootTested=$false
|
|
} | ConvertTo-Json | Set-Content -LiteralPath $report -Encoding utf8
|
|
Get-Content -LiteralPath $report
|
|
} catch {
|
|
# Restore startup when a verification step fails; this never imports/replaces the key.
|
|
Start-Process -FilePath $app -ArgumentList '--tray' -ErrorAction SilentlyContinue | Out-Null
|
|
[pscustomobject]@{Completed=$false;At=[DateTimeOffset]::Now.ToString('o');Failure=$_.Exception.Message} |
|
|
ConvertTo-Json | Set-Content -LiteralPath $report -Encoding utf8
|
|
Get-Content -LiteralPath $report
|
|
exit 1
|
|
}
|