$hasErrors = $false $rootDir = Split-Path $PSScriptRoot $listsDir = Join-Path $rootDir "lists" $utilsDir = Join-Path $rootDir "utils" $resultsDir = Join-Path $utilsDir "test results" if (-not (Test-Path $resultsDir)) { New-Item -ItemType Directory -Path $resultsDir | Out-Null } # Define functions early function Get-IpsetStatus { $listFile = Join-Path $listsDir "ipset-all.txt" if (-not (Test-Path $listFile)) { return "none" } $lineCount = (Get-Content $listFile | Measure-Object -Line).Lines if ($lineCount -eq 0) { return "any" } $hasDummy = Get-Content $listFile | Select-String -Pattern "203\.0\.113\.113/32" -Quiet if ($hasDummy) { return "none" } else { return "loaded" } } function Set-IpsetMode { param([string]$mode) $listFile = Join-Path $listsDir "ipset-all.txt" $backupFile = Join-Path $listsDir "ipset-all.test-backup.txt" if ($mode -eq "any") { # Always backup current file (even if none) if (Test-Path $listFile) { Copy-Item $listFile $backupFile -Force } else { # If none, create empty backup "" | Out-File $backupFile -Encoding UTF8 } # Make file empty "" | Out-File $listFile -Encoding UTF8 } elseif ($mode -eq "restore") { if (Test-Path $backupFile) { Move-Item $backupFile $listFile -Force } } } trap { Write-Host "[ERROR] Script interrupted. Restoring ipset..." -ForegroundColor Red if ($originalIpsetStatus -and $originalIpsetStatus -ne "any") { Set-IpsetMode -mode "restore" } Remove-Item -Path $ipsetFlagFile -ErrorAction SilentlyContinue break } function New-OrderedDict { New-Object System.Collections.Specialized.OrderedDictionary } function Add-OrSet { param($dict, $key, $val) if ($dict.Contains($key)) { $dict[$key] = $val } else { $dict.Add($key, $val) } } # Convert raw target value to structured target (supports PING:ip for ping-only targets) function Convert-Target { param( [string]$Name, [string]$Value ) if ($Value -like "PING:*") { $ping = $Value -replace '^PING:\s*', '' $url = $null $pingTarget = $ping } else { $url = $Value $pingTarget = $url -replace "^https?://", "" -replace "/.*$", "" } return (New-Object PSObject -Property @{ Name = $Name Url = $url PingTarget = $pingTarget }) } # DPI checker defaults (override via MONITOR_* env vars like in monitor.ps1) $dpiTimeoutSeconds = 5 $dpiRangeBytes = 65536 $dpiMaxParallel = 8 $dpiCustomHost = $env:MONITOR_HOST if ($env:MONITOR_TIMEOUT) { [int]$dpiTimeoutSeconds = $env:MONITOR_TIMEOUT } if ($env:MONITOR_RANGE) { [int]$dpiRangeBytes = $env:MONITOR_RANGE } if ($env:MONITOR_MAX_PARALLEL) { [int]$dpiMaxParallel = $env:MONITOR_MAX_PARALLEL } function Get-DpiSuite { # Suite sourced from https://github.com/hyperion-cs/dpi-checkers (Apache-2.0 license) # Original copyright retained from dpi-checkers repository $url = "https://hyperion-cs.github.io/dpi-checkers/ru/tcp-16-20/suite.v2.json" try { (Invoke-RestMethod -Uri $url -TimeoutSec $dpiTimeoutSeconds) | Select-Object ` @{n='Id'; e={$_.id}}, @{n='Provider'; e={$_.provider}}, @{n='Country'; e={$_.country}}, @{n='Host'; e={$_.host}} } catch { Write-Host "[WARN] Fetch dpi suite failed." -ForegroundColor Yellow @() } } function Build-DpiTargets { param( [string]$CustomHost ) $suite = Get-DpiSuite $targets = @() if ($CustomHost) { $targets += @{ Id = "CUSTOM"; Provider = "Custom"; Country = "💡"; Host = $CustomHost } } else { foreach ($entry in $suite) { $targets += @{ Id = $entry.Id; Country = $entry.Country; Provider = $entry.Provider; Host = $entry.Host } } } return $targets } function Invoke-DpiSuite { param( [array]$Targets, [int]$TimeoutSeconds, [int]$RangeBytes, [int]$MaxParallel ) $tests = @( @{ Label = "HTTP"; Args = @("--http1.1") }, @{ Label = "TLS1.2"; Args = @("--tlsv1.2", "--tls-max", "1.2") }, @{ Label = "TLS1.3"; Args = @("--tlsv1.3", "--tls-max", "1.3") } ) $rangeSpec = "0-$($RangeBytes - 1)" $warnDetected = $false Write-Host "[INFO] Targets: $($Targets.Count) (custom URL overrides suite). Range: $rangeSpec bytes; Timeout: $($TimeoutSeconds)s" -ForegroundColor Cyan Write-Host "[INFO] Starting DPI TCP 16-20 checks (parallel: $MaxParallel)..." -ForegroundColor DarkGray $runspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxParallel) $runspacePool.Open() $payload = New-Object byte[] $RangeBytes [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($payload) $payloadFile = New-TemporaryFile [IO.File]::WriteAllBytes($payloadFile, $payload) $scriptBlock = { param($payloadFile, $target, $tests, $rangeSpec, $TimeoutSeconds) $warned = $false $lines = @() foreach ($test in $tests) { $curlArgs = @( "--range", $rangeSpec, "-m", $TimeoutSeconds, "-w", "%{http_code} %{size_upload} %{size_download} %{time_total}", "-o", "NUL", "-X", "POST", "--data-binary", "@$payloadFile", "-s" ) + $test.Args + @("https://$($target.Host)") $output = $payload | curl.exe @curlArgs 2>&1 $exit = $LASTEXITCODE $text = ($output | Out-String).Trim() $code = "NA" $upBytes = 0 $downBytes = 0 $time = -1 if ($text -match '^(?\d{3})\s+(?\d+)\s+(?\d+)\s+(?