Integrate Xray 26.9.9 with the sing-box system tunnel and verify Windows routes during health checks. Improve endpoint selection, network recovery, subscription refresh deadlines, diagnostics, packaging, and regression coverage.
94 lines
5.5 KiB
PowerShell
94 lines
5.5 KiB
PowerShell
[CmdletBinding()]
|
|
param([int]$ObserveSeconds = 60)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
|
$dll = Join-Path $repo 'tests/QVPN.Tests/bin/Release/net10.0-windows/QVPN.Tests.dll'
|
|
$reportPath = Join-Path $repo 'research/system-route-verification.json'
|
|
$report = [ordered]@{Completed=$false;Phase='preflight';CapturedAt=[DateTimeOffset]::Now.ToString('o')}
|
|
$injected = $null
|
|
|
|
function Save([string]$phase) {
|
|
$report.Phase=$phase
|
|
$report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $reportPath -Encoding utf8
|
|
}
|
|
function Status {
|
|
$response = & dotnet $dll --client status | ConvertFrom-Json
|
|
if ($LASTEXITCODE -ne 0 -or !$response.Ok) { throw 'QVPN status is unavailable' }
|
|
return $response.Status
|
|
}
|
|
function Remove-InjectedRoute {
|
|
if ($null -eq $script:injected) { return }
|
|
# Only this freshly created ActiveStore /32 is owned by the verifier.
|
|
if ($script:injected.DestinationPrefix -ne '1.1.1.1/32' -or $script:injected.RouteMetric -ne 1) { throw 'Unexpected route identity; refusing cleanup' }
|
|
$script:injected | Remove-NetRoute -Confirm:$false -ErrorAction Stop
|
|
$script:injected=$null
|
|
}
|
|
|
|
try {
|
|
if (!([Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Administrator required' }
|
|
if ($ObserveSeconds -lt 30 -or $ObserveSeconds -gt 600) { throw 'Observation must be between30 and600 seconds' }
|
|
$before=Status
|
|
if ($before.State -ne 'connected' -or !$before.SystemRoutesVerified -or !$before.EnginePid) { throw 'An installed VPN with verified routes is required' }
|
|
$report.Before=$before | Select-Object State,ActiveName,EnginePid,XrayPid,Restarts,SystemRoutesVerified
|
|
if (Get-NetRoute -DestinationPrefix '1.1.1.1/32' -ErrorAction SilentlyContinue) { throw 'A pre-existing control host route must be preserved; test skipped' }
|
|
$physical=Get-NetAdapter -Physical | Where-Object {$_.Status -eq 'Up'} | ForEach-Object {
|
|
Get-NetRoute -InterfaceIndex $_.ifIndex -DestinationPrefix '0.0.0.0/0' -PolicyStore ActiveStore -ErrorAction SilentlyContinue
|
|
} | Where-Object {$_.NextHop -ne '0.0.0.0'} | Sort-Object RouteMetric | Select-Object -First 1
|
|
if (!$physical) { throw 'No physical IPv4 default route found' }
|
|
Save 'inject one temporary public control route'
|
|
$timer=[Diagnostics.Stopwatch]::StartNew()
|
|
$injected=New-NetRoute -DestinationPrefix '1.1.1.1/32' -InterfaceIndex $physical.InterfaceIndex -NextHop $physical.NextHop -RouteMetric 1 -PolicyStore ActiveStore
|
|
$detected=$null; $restarted=$null
|
|
do {
|
|
$status=Status
|
|
if ($status.SystemRoutesVerified -eq $false -and !$detected) {
|
|
$detected=$status
|
|
$report.DetectionSeconds=[Math]::Round($timer.Elapsed.TotalSeconds,2)
|
|
$report.Degraded=$status | Select-Object State,EnginePid,Restarts,SystemRoutesVerified
|
|
if ($status.State -eq 'connected') { throw 'False connected status survived route loss' }
|
|
Save 'route loss detected; await same-server repair'
|
|
}
|
|
if ($status.EnginePid -and $status.EnginePid -ne $before.EnginePid) { $restarted=$status; break }
|
|
Start-Sleep -Milliseconds 200
|
|
} while ($timer.Elapsed.TotalSeconds -lt 50)
|
|
if (!$detected) { throw 'Route bypass was not detected' }
|
|
if (!$restarted) { throw 'Local tunnel repair did not restart the engine' }
|
|
$report.RestartSeconds=[Math]::Round($timer.Elapsed.TotalSeconds,2)
|
|
Remove-InjectedRoute
|
|
Save 'control route restored; verify recovery'
|
|
$timer.Restart()
|
|
do {
|
|
$status=Status
|
|
if ($status.State -eq 'connected' -and $status.SystemRoutesVerified) { break }
|
|
Start-Sleep -Milliseconds 300
|
|
} while ($timer.Elapsed.TotalSeconds -lt 60)
|
|
if ($status.State -ne 'connected' -or !$status.SystemRoutesVerified) { throw 'Verified VPN routes did not recover' }
|
|
if ($status.ActiveId -ne $before.ActiveId -or $status.SelectedId -ne $before.SelectedId) { throw 'A local route fault changed the selected VPN server' }
|
|
$report.RecoverySeconds=[Math]::Round($timer.Elapsed.TotalSeconds,2)
|
|
$report.After=$status | Select-Object State,ActiveName,EnginePid,XrayPid,Restarts,SystemRoutesVerified
|
|
$baseline=$status; $samples=0; $httpsChecks=0
|
|
Save 'passive observation'
|
|
$timer.Restart()
|
|
do {
|
|
$status=Status; $samples++
|
|
if ($status.State -ne 'connected' -or !$status.SystemRoutesVerified -or $status.EnginePid -ne $baseline.EnginePid -or $status.ActiveId -ne $baseline.ActiveId) { throw 'Verified tunnel changed during passive observation' }
|
|
if ($samples % 5 -eq 1) {
|
|
$code=& curl.exe --silent --noproxy '*' --max-time 8 --output NUL --write-out '%{http_code}' 'https://cp.cloudflare.com/generate_204' 2>$null
|
|
if ($LASTEXITCODE -ne 0 -or $code -ne '204') { throw 'System HTTPS failed after route recovery' }
|
|
$httpsChecks++
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ($timer.Elapsed.TotalSeconds -lt $ObserveSeconds)
|
|
$report.Observation=[ordered]@{Seconds=[Math]::Round($timer.Elapsed.TotalSeconds,2);Samples=$samples;SystemHttpsChecks=$httpsChecks;UnplannedRestarts=0;ServerChanges=0}
|
|
$report.RouteCleanupVerified=!(Get-NetRoute -DestinationPrefix '1.1.1.1/32' -ErrorAction SilentlyContinue)
|
|
if (!$report.RouteCleanupVerified) { throw 'Temporary control route still exists' }
|
|
$report.Completed=$true; Save 'complete'
|
|
} catch {
|
|
$report.Failure=$_.Exception.Message
|
|
Save $report.Phase
|
|
exit 1
|
|
} finally {
|
|
Remove-InjectedRoute
|
|
}
|