@echo off setlocal set "TEST_BAT_PATH=%~f0" powershell -NoProfile -ExecutionPolicy Bypass -Command "$path=$env:TEST_BAT_PATH; $raw=Get-Content -Raw -LiteralPath $path; $marker='# POWERSHELL'; $start=$raw.LastIndexOf($marker); if($start -lt 0){ throw 'PowerShell marker not found' }; $ps=$raw.Substring($start + $marker.Length); Set-Location -LiteralPath (Split-Path -Parent $path); Invoke-Expression $ps" exit /b %ERRORLEVEL% # POWERSHELL $ErrorActionPreference = 'Stop' if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { $PSNativeCommandUseErrorActionPreference = $false } $forceBuild = if ($env:FORCE_BUILD) { $env:FORCE_BUILD } else { '0' } $verbose = if ($env:VERBOSE) { $env:VERBOSE } else { '0' } $composeBase = @('compose', '--env-file', '.env', '-f', 'compose.yml') function Write-TaggedLine { param([string] $Line) if ($Line.StartsWith('[PASS]')) { Write-Host $Line -ForegroundColor Green } elseif ($Line.StartsWith('[NO]') -or $Line.StartsWith('reason:')) { Write-Host $Line -ForegroundColor Red } elseif ($Line.StartsWith('[RUN]')) { Write-Host $Line -ForegroundColor Cyan } elseif ($Line.StartsWith('[INFO]')) { Write-Host $Line -ForegroundColor DarkCyan } } function Invoke-Docker { param([string[]] $Arguments) $previousErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $output = & docker @Arguments 2>&1 $code = $LASTEXITCODE } finally { $ErrorActionPreference = $previousErrorActionPreference } $lines = @( $output | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_.ToString() } } | Where-Object { $_ -and $_ -ne 'System.Management.Automation.RemoteException' } ) [pscustomobject]@{ Code = $code Lines = $lines } } function Print-FilteredOutput { param([string[]] $Lines) foreach ($line in $Lines) { if ($verbose -eq '1') { Write-Host $line } else { Write-TaggedLine $line } } } function Print-FailureDiagnostics { param([string[]] $Lines) $hasNoLine = $Lines | Where-Object { $_.StartsWith('[NO]') } | Select-Object -First 1 if (-not $hasNoLine) { Write-Host '--- raw docker output ---' -ForegroundColor Yellow $Lines | Select-Object -Last 80 | ForEach-Object { Write-Host $_ } } } function Build-Image { Write-Host '' Write-TaggedLine '[RUN] Build image' $sourceFiles = Get-ChildItem -Recurse -File scripts, rootfs, Dockerfile, compose.yml, .env, .env.example $env:SOURCE_VERSION = ($sourceFiles | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1).LastWriteTimeUtc.Ticks $dockerArgs = $composeBase + @('build', '--quiet') if ($forceBuild -eq '1') { $dockerArgs += '--no-cache' } $result = Invoke-Docker $dockerArgs if ($result.Code -ne 0) { Write-TaggedLine '[NO] Build image' $result.Lines | ForEach-Object { Write-Host $_ } exit $result.Code } Write-TaggedLine '[PASS] Build image' if ($verbose -eq '1') { $result.Lines | ForEach-Object { Write-Host $_ } } } function Run-Case { param( [string] $Title, [string] $Service, [string] $Script ) Write-Host '' Write-TaggedLine "[RUN] $Title" $casePath = Join-Path (Get-Location) ('.test-case-{0}.sh' -f ([guid]::NewGuid().ToString('N'))) Set-Content -LiteralPath $casePath -Value $Script -NoNewline -Encoding Ascii try { $volumeArg = "${casePath}:/tmp/test-case.sh:ro" $dockerArgs = $composeBase + @('run', '--rm', '--no-deps', '-T', '--volume', $volumeArg, $Service, 'bash', '/tmp/test-case.sh') $result = Invoke-Docker $dockerArgs } finally { Remove-Item -LiteralPath $casePath -Force -ErrorAction SilentlyContinue } Print-FilteredOutput $result.Lines if ($result.Code -ne 0) { Print-FailureDiagnostics $result.Lines } return $result.Code } if (-not (Test-Path -LiteralPath '.env')) { Write-TaggedLine '[NO] .env exists' Write-TaggedLine 'reason: create Ubuntu\.env from Ubuntu\.env.example and set XRAY_URL.' exit 1 } $tests = @( @{ Title = 'IPv6 default off' Service = 'ipv6-default-off' Script = @' set -euo pipefail if curl --noproxy '*' -6 -fsSIL --connect-timeout 5 --max-time 10 http://google.com >/tmp/curl.out 2>/tmp/curl.err; then echo '[NO] IPv6 request is blocked by default' echo 'reason: IPv6 request unexpectedly succeeded' exit 1 fi echo '[PASS] IPv6 request is blocked by default' '@ } @{ Title = 'IPv6 enabled' Service = 'ipv6-enabled' Script = @' set -euo pipefail if [ -r /proc/sys/net/ipv6/conf/all/disable_ipv6 ]; then value="$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6)" if [ "${value}" != "0" ]; then echo '[NO] IPv6 is allowed by container policy' echo "reason: disable_ipv6=${value}" exit 1 fi fi echo '[PASS] IPv6 is allowed by container policy' '@ } @{ Title = 'Proxy on' Service = 'proxy-on' Script = @' set -euo pipefail sleep 3 if ! iptables -t nat -S XRAY_OUTPUT >/tmp/iptables.out 2>/tmp/iptables.err; then echo '[NO] transparent proxy iptables chain exists' echo "reason: $(sed -n '1p' /tmp/iptables.err)" exit 1 fi if ! grep -q -- "--to-ports ${XRAY_REDIRECT_PORT:-12345}" /tmp/iptables.out; then echo '[NO] transparent proxy redirect rule exists' echo "reason: XRAY_OUTPUT has no REDIRECT to ${XRAY_REDIRECT_PORT:-12345}" exit 1 fi echo '[PASS] transparent proxy redirect rule exists' if curl --noproxy '*' -fsSIL --connect-timeout 10 --max-time 20 http://google.com >/tmp/curl.out 2>/tmp/curl.err; then echo '[PASS] HTTP request through transparent proxy' else echo '[NO] HTTP request through transparent proxy' echo "reason: $(sed -n '1p' /tmp/curl.err)" exit 1 fi '@ } @{ Title = 'Proxy off' Service = 'proxy-off' Script = @' set -euo pipefail if pgrep -x xray >/dev/null 2>&1; then echo '[NO] xray is not running' echo 'reason: xray process exists' exit 1 fi echo '[PASS] xray is not running' if curl --noproxy '*' -fsSIL --connect-timeout 3 --max-time 3 http://google.com >/tmp/curl.out 2>/tmp/curl.err; then echo '[INFO] HTTP request without xray succeeded directly' exit 0 fi echo '[PASS] HTTP request without xray is blocked' '@ } ) Build-Image $failed = $false foreach ($test in $tests) { $code = Run-Case -Title $test.Title -Service $test.Service -Script $test.Script if ($code -ne 0) { $failed = $true } } Write-Host '' if ($failed) { Write-TaggedLine '[NO] one or more tests failed' exit 1 } Write-TaggedLine '[PASS] all tests passed' exit 0