From fcd087dad2f76525204adcd7f1018bf1f2422f0d Mon Sep 17 00:00:00 2001 From: Alan Jowett Date: Wed, 12 Aug 2026 12:11:56 -0700 Subject: [PATCH 1/8] Add ShadowSocketProxy iperf benchmark workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../scripts/run-shadowsocketproxy-iperf.ps1 | 290 ++++++++++++++++++ .github/workflows/shadowsocketproxy-iperf.yml | 145 +++++++++ docs/shadowsocketproxy-iperf.md | 45 +++ 3 files changed, 480 insertions(+) create mode 100644 .github/scripts/run-shadowsocketproxy-iperf.ps1 create mode 100644 .github/workflows/shadowsocketproxy-iperf.yml create mode 100644 docs/shadowsocketproxy-iperf.md diff --git a/.github/scripts/run-shadowsocketproxy-iperf.ps1 b/.github/scripts/run-shadowsocketproxy-iperf.ps1 new file mode 100644 index 00000000..d1966755 --- /dev/null +++ b/.github/scripts/run-shadowsocketproxy-iperf.ps1 @@ -0,0 +1,290 @@ +param( + [Parameter(Mandatory = $true)][string]$TargetPeer, + [Parameter(Mandatory = $true)][string]$TargetAddress, + [Parameter(Mandatory = $true)][string]$ShadowRoot, + [Parameter(Mandatory = $true)][string]$Distribution, + [string]$Iperf3WslPath = '/usr/bin/iperf3', + [string]$Iperf3WindowsPath = 'C:\_work\iperf3\iperf3.exe', + [int]$Duration = 30, + [int]$Runs = 3, + [string]$UdpRates = '100M,1G', + [switch]$SkipProxy +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Invoke-WslChecked { + param([Parameter(Mandatory = $true)][string[]]$Arguments) + $output = & wsl.exe @Arguments + if ($LASTEXITCODE -ne 0) { + throw "WSL command failed: wsl.exe $($Arguments -join ' ')" + } + return (($output | Out-String).Trim() -replace "`0", '') +} + +function ConvertTo-WslPath { + param([Parameter(Mandatory = $true)][string]$WindowsPath) + $fullPath = (Resolve-Path -LiteralPath $WindowsPath).Path + if ($fullPath -notmatch '^([A-Za-z]):\\(.*)$') { + throw "Expected a drive-qualified Windows path: $fullPath" + } + return "/mnt/$($Matches[1].ToLowerInvariant())/$($Matches[2] -replace '\\', '/')" +} + +function Wait-TcpPort { + param( + [Parameter(Mandatory = $true)][string]$Address, + [Parameter(Mandatory = $true)][int]$Port + ) + for ($attempt = 0; $attempt -lt 30; $attempt++) { + if (Test-NetConnection -ComputerName $Address -Port $Port -InformationLevel Quiet) { + return + } + Start-Sleep -Milliseconds 500 + } + throw "TCP port $Address`:$Port did not become ready" +} + +function Wait-WindowsTcpListener { + param([Parameter(Mandatory = $true)][int]$Port) + for ($attempt = 0; $attempt -lt 30; $attempt++) { + if (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) { + return + } + Start-Sleep -Milliseconds 500 + } + throw "No Windows TCP listener appeared on port $Port" +} + +function Start-ShadowProxy { + param( + [Parameter(Mandatory = $true)][string]$ControlBinary, + [Parameter(Mandatory = $true)][string]$HostBinary, + [Parameter(Mandatory = $true)][string]$BpfObject, + [Parameter(Mandatory = $true)][string]$WorkDirectory + ) + + $route = Invoke-WslChecked @('-d', $Distribution, '--', 'ip', 'route', 'show', 'default') -split '\s+' + if ($route.Count -lt 5) { + throw 'Unable to discover the WSL default gateway and interface' + } + $gateway = $route[2] + $interface = $route[4] + $controlWsl = ConvertTo-WslPath $ControlBinary + $bpfWsl = ConvertTo-WslPath $BpfObject + $secret = [Convert]::ToHexString([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) + $identity = "ssp-iperf-$([guid]::NewGuid().ToString('N'))" + $controlPort = 50051 + $proxyPort = 15000 + $proxyAddress = "${gateway}:$proxyPort" + $controlStdout = Join-Path $WorkDirectory 'control.stdout.log' + $controlStderr = Join-Path $WorkDirectory 'control.stderr.log' + $proxyStdout = Join-Path $WorkDirectory 'host-proxy.stdout.log' + $proxyStderr = Join-Path $WorkDirectory 'host-proxy.stderr.log' + $qdiscCreated = $false + $controlProcess = $null + $proxyProcess = $null + + try { + $qdisc = Invoke-WslChecked @('-d', $Distribution, '-u', 'root', '--', 'tc', 'qdisc', 'show', 'dev', $interface) + if ($qdisc -notmatch '(?m)^qdisc clsact ') { + Invoke-WslChecked @('-d', $Distribution, '-u', 'root', '--', 'tc', 'qdisc', 'add', 'dev', $interface, 'clsact') | Out-Null + $qdiscCreated = $true + } + + $controlArguments = @( + '-d', $Distribution, '-u', 'root', '--', 'env', + "SSP_LISTEN_ADDR=127.0.0.1:$controlPort", + 'SSP_TC_HOOK_LAYOUT=wsl', + "SSP_TLS_PSK_IDENTITY=$identity", + "SSP_TLS_PSK_SECRET=$secret", + $controlWsl + ) + $controlProcess = Start-Process wsl.exe -PassThru -WindowStyle Hidden ` + -RedirectStandardOutput $controlStdout -RedirectStandardError $controlStderr ` + -ArgumentList $controlArguments + Wait-TcpPort -Address '127.0.0.1' -Port $controlPort + + $proxyArguments = @( + '--listen', $proxyAddress, + '--control-endpoint', "https://127.0.0.1:$controlPort", + '--psk-identity', $identity, + '--psk-secret', $secret, + '--bpf-elf', $bpfWsl, + '--interface', $interface, + '--udp-idle-timeout-secs', ([Math]::Max(60, $Duration + 30)) + ) + $proxyProcess = Start-Process $HostBinary -PassThru -WindowStyle Hidden ` + -RedirectStandardOutput $proxyStdout -RedirectStandardError $proxyStderr ` + -ArgumentList $proxyArguments + # Windows may not be able to connect back through the WSL gateway address, + # even though WSL applications can reach the host-owned listener. + Wait-WindowsTcpListener -Port $proxyPort + if ($proxyProcess.HasExited) { + throw "ShadowSocketProxy host process exited with code $($proxyProcess.ExitCode)" + } + + return [pscustomobject]@{ + Gateway = $gateway + Interface = $interface + ControlProcess = $controlProcess + ProxyProcess = $proxyProcess + QdiscCreated = $qdiscCreated + ControlStdout = $controlStdout + ControlStderr = $controlStderr + ProxyStdout = $proxyStdout + ProxyStderr = $proxyStderr + Distribution = $Distribution + } + } + catch { + if (Test-Path -LiteralPath $controlStderr) { Get-Content -LiteralPath $controlStderr } + if (Test-Path -LiteralPath $proxyStderr) { Get-Content -LiteralPath $proxyStderr } + if ($null -ne $proxyProcess -and -not $proxyProcess.HasExited) { Stop-Process -Id $proxyProcess.Id -Force } + if ($null -ne $controlProcess -and -not $controlProcess.HasExited) { Stop-Process -Id $controlProcess.Id -Force } + if ($qdiscCreated) { + Invoke-WslChecked @('-d', $Distribution, '-u', 'root', '--', 'tc', 'qdisc', 'del', 'dev', $interface, 'clsact') | Out-Null + } + throw + } +} + +function Stop-ShadowProxy { + param([Parameter(Mandatory = $true)]$State) + if ($null -ne $State.ProxyProcess -and -not $State.ProxyProcess.HasExited) { + Stop-Process -Id $State.ProxyProcess.Id -Force + } + if ($null -ne $State.ControlProcess -and -not $State.ControlProcess.HasExited) { + Stop-Process -Id $State.ControlProcess.Id -Force + } + if ($State.QdiscCreated) { + Invoke-WslChecked @('-d', $State.Distribution, '-u', 'root', '--', 'tc', 'qdisc', 'del', 'dev', $State.Interface, 'clsact') | Out-Null + } +} + +function Invoke-Iperf { + param( + [Parameter(Mandatory = $true)][string]$Scenario, + [Parameter(Mandatory = $true)][string[]]$Arguments, + [Parameter(Mandatory = $true)][string]$OutputPath + ) + $wslArguments = @('-d', $Distribution, '--', $Iperf3WslPath) + $Arguments + $json = Invoke-WslChecked -Arguments $wslArguments + $json | Out-File -LiteralPath $OutputPath -Encoding utf8 -Force + try { + $parsed = $json | ConvertFrom-Json + [pscustomobject]@{ + Scenario = $Scenario + Output = $OutputPath + SentBps = $parsed.end.sum_sent.bits_per_second + ReceivedBps = $parsed.end.sum_received.bits_per_second + LostPercent = $parsed.end.sum.lost_percent + JitterMs = $parsed.end.sum.jitter_ms + } + } + catch { + throw "iperf3 did not produce valid JSON for $Scenario. Raw output saved to $OutputPath" + } +} + +$workDirectory = Join-Path (Get-Location) 'shadowsocketproxy-iperf' +New-Item -ItemType Directory -Force -Path $workDirectory | Out-Null +$session = $null +$serverProcessId = $null +$proxyState = $null + +try { + $shadowRootPath = (Resolve-Path -LiteralPath $ShadowRoot).Path + $controlBinary = Join-Path $shadowRootPath 'target\release\shadow-socket-proxy-control' + $hostBinary = Join-Path $shadowRootPath 'target\release\shadow-socket-proxy-host.exe' + $bpfObject = Join-Path $shadowRootPath 'crates\bpf\shadow-socket-proxy.bpf.o' + foreach ($path in @($controlBinary, $hostBinary, $bpfObject)) { + if (-not (Test-Path -LiteralPath $path)) { + throw "Required ShadowSocketProxy artifact is missing: $path" + } + } + + Invoke-WslChecked @('-d', $Distribution, '--', 'test', '-x', $Iperf3WslPath) | Out-Null + $modulePath = Join-Path $PSScriptRoot 'performance_utilities.psm1' + Import-Module $modulePath -Force + $session = Create-Session -PeerName $TargetPeer -RemotePSConfiguration 'PowerShell.7' + $remoteDirectory = Split-Path -Parent $Iperf3WindowsPath + Invoke-Command -Session $session -ScriptBlock { + param($directory, $iperfPath) + if (-not (Test-Path -LiteralPath $directory)) { + New-Item -ItemType Directory -Path $directory -Force | Out-Null + } + if (-not (Test-Path -LiteralPath $iperfPath)) { + throw "iperf3 is missing on target peer: $iperfPath" + } + Get-NetFirewallRule -DisplayName 'netperf-iperf3-*' -ErrorAction SilentlyContinue | + Remove-NetFirewallRule -ErrorAction SilentlyContinue + New-NetFirewallRule -DisplayName 'netperf-iperf3-tcp' -Direction Inbound -Protocol TCP -LocalPort 5201 -Action Allow | Out-Null + New-NetFirewallRule -DisplayName 'netperf-iperf3-udp' -Direction Inbound -Protocol UDP -LocalPort 5201 -Action Allow | Out-Null + $process = Start-Process -FilePath $iperfPath -ArgumentList @('-s') -PassThru -WindowStyle Hidden + return $process.Id + } -ArgumentList $remoteDirectory, $Iperf3WindowsPath | ForEach-Object { + if ($_ -is [int]) { $script:serverProcessId = $_ } + } + if ($null -eq $serverProcessId) { + throw 'Unable to start iperf3 on the target peer' + } + + $results = [System.Collections.Generic.List[object]]::new() + $runArguments = @( + @('-c', $TargetAddress, '-t', $Duration, '-J'), + @('-c', $TargetAddress, '-t', $Duration, '-P', '4', '-J') + ) + foreach ($run in 1..$Runs) { + foreach ($args in $runArguments) { + $name = if ($args -contains '-P') { 'tcp-parallel' } else { 'tcp' } + $path = Join-Path $workDirectory "baseline-$name-run$run.json" + $results.Add((Invoke-Iperf -Scenario "baseline-$name-run$run" -Arguments $args -OutputPath $path)) + } + foreach ($rate in ($UdpRates -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })) { + $args = @('-c', $TargetAddress, '-u', '-b', $rate, '-t', $Duration, '-J') + $path = Join-Path $workDirectory "baseline-udp-$rate-run$run.json" + $results.Add((Invoke-Iperf -Scenario "baseline-udp-$rate-run$run" -Arguments $args -OutputPath $path)) + } + } + + if (-not $SkipProxy) { + $proxyState = Start-ShadowProxy ` + -ControlBinary $controlBinary ` + -HostBinary $hostBinary ` + -BpfObject $bpfObject ` + -WorkDirectory $workDirectory + + foreach ($run in 1..$Runs) { + foreach ($args in $runArguments) { + $name = if ($args -contains '-P') { 'tcp-parallel' } else { 'tcp' } + $path = Join-Path $workDirectory "proxy-$name-run$run.json" + $results.Add((Invoke-Iperf -Scenario "proxy-$name-run$run" -Arguments $args -OutputPath $path)) + } + foreach ($rate in ($UdpRates -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })) { + $args = @('-c', $TargetAddress, '-u', '-b', $rate, '-t', $Duration, '-J') + $path = Join-Path $workDirectory "proxy-udp-$rate-run$run.json" + $results.Add((Invoke-Iperf -Scenario "proxy-udp-$rate-run$run" -Arguments $args -OutputPath $path)) + } + } + } + + $results | ConvertTo-Json -Depth 4 | Out-File -LiteralPath (Join-Path $workDirectory 'summary.json') -Encoding utf8 -Force +} +finally { + if ($null -ne $proxyState) { + Stop-ShadowProxy -State $proxyState + } + if ($null -ne $session) { + if ($null -ne $serverProcessId) { + Invoke-Command -Session $session -ScriptBlock { + param($processId) + Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue + Get-NetFirewallRule -DisplayName 'netperf-iperf3-*' -ErrorAction SilentlyContinue | + Remove-NetFirewallRule -ErrorAction SilentlyContinue + } -ArgumentList $serverProcessId -ErrorAction SilentlyContinue + } + Remove-PSSession -Session $session -ErrorAction SilentlyContinue + } +} diff --git a/.github/workflows/shadowsocketproxy-iperf.yml b/.github/workflows/shadowsocketproxy-iperf.yml new file mode 100644 index 00000000..335efa12 --- /dev/null +++ b/.github/workflows/shadowsocketproxy-iperf.yml @@ -0,0 +1,145 @@ +name: shadowsocketproxy-iperf + +on: + workflow_dispatch: + inputs: + shadow_ref: + description: 'ShadowSocketProxy branch, tag, or commit' + required: false + default: 'main' + type: string + target_peer: + description: 'Windows peer that runs iperf3 server' + required: false + default: 'netperf-peer' + type: string + target_address: + description: 'Optional IPv4 address; otherwise resolve target_peer on the runner' + required: false + default: '' + type: string + distribution: + description: 'Installed WSL distribution on the runner' + required: false + default: 'Ubuntu' + type: string + duration: + description: 'iperf3 duration per run in seconds' + required: false + default: '30' + type: string + runs: + description: 'Number of repetitions per scenario' + required: false + default: '3' + type: string + udp_rates: + description: 'Comma-separated UDP target rates' + required: false + default: '100M,1G' + type: string + skip_proxy: + description: 'Run baseline only' + required: false + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: shadowsocketproxy-iperf-${{ inputs.target_peer || 'target' }} + cancel-in-progress: true + +jobs: + benchmark: + name: ShadowSocketProxy iperf3 benchmark + runs-on: + - self-hosted + - lab + - os-windows-2022 + - x64 + - wsl + - ebpf + + steps: + - name: Checkout netperf + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 + + - name: Prepare workspace + shell: pwsh + run: | + $workspace = "${{ github.workspace }}" + if ((Get-Location).Path -ne $workspace) { + throw "Unexpected working directory: $((Get-Location).Path)" + } + foreach ($path in @("$workspace\ShadowSocketProxy", "$workspace\shadowsocketproxy-iperf")) { + if (Test-Path -LiteralPath $path) { + Remove-Item -LiteralPath $path -Recurse -Force + } + } + New-Item -ItemType Directory -Force -Path "$workspace\results" | Out-Null + + - name: Checkout ShadowSocketProxy + shell: pwsh + run: | + git clone --no-checkout --filter=blob:none https://github.com/Alan-Jowett/ShadowSocketProxy.git "${{ github.workspace }}\ShadowSocketProxy" + git -C "${{ github.workspace }}\ShadowSocketProxy" fetch --depth 1 origin "${{ inputs.shadow_ref }}" + git -C "${{ github.workspace }}\ShadowSocketProxy" checkout --detach FETCH_HEAD + + - name: Validate WSL prerequisites + shell: pwsh + run: | + $distribution = "${{ inputs.distribution }}" + wsl.exe -d $distribution -- uname -a + wsl.exe -d $distribution -- sh -lc "command -v cargo && command -v tc && command -v iperf3" + wsl.exe -d $distribution -u root -- sh -lc 'iface=$(ip route show default | awk "{print $5}"); tc qdisc show dev "$iface"' + + - name: Build ShadowSocketProxy in WSL + shell: pwsh + working-directory: ${{ github.workspace }}\ShadowSocketProxy + run: | + $wslRoot = "/mnt/c/$((Get-Location).Path.Substring(3) -replace '\\','/')" + wsl.exe -d "${{ inputs.distribution }}" -- bash -lc "cd '$wslRoot' && make -C crates/bpf clean all && cargo build --locked --release -p shadow-socket-proxy-control --features tls-psk" + if ($LASTEXITCODE -ne 0) { throw "WSL ShadowSocketProxy build failed" } + + - name: Build Windows host proxy + shell: pwsh + working-directory: ${{ github.workspace }}\ShadowSocketProxy + run: | + cargo build --locked --release -p shadow-socket-proxy-host --features tls-psk + if ($LASTEXITCODE -ne 0) { throw "Windows host proxy build failed" } + + - name: Run baseline and proxy benchmark + shell: pwsh + run: | + $targetAddress = "${{ inputs.target_address }}" + if ([string]::IsNullOrWhiteSpace($targetAddress)) { + $targetAddress = [System.Net.Dns]::GetHostAddresses("${{ inputs.target_peer }}") | + Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork } | + Select-Object -First 1 -ExpandProperty IPAddressToString + } + if ([string]::IsNullOrWhiteSpace($targetAddress)) { + throw "Could not resolve an IPv4 address for target peer '${{ inputs.target_peer }}'" + } + Write-Host "Using target peer ${{ inputs.target_peer }} at $targetAddress" + $params = @{ + TargetPeer = "${{ inputs.target_peer }}" + TargetAddress = $targetAddress + ShadowRoot = "${{ github.workspace }}\ShadowSocketProxy" + Distribution = "${{ inputs.distribution }}" + Duration = [int]"${{ inputs.duration }}" + Runs = [int]"${{ inputs.runs }}" + UdpRates = "${{ inputs.udp_rates }}" + } + if ("${{ inputs.skip_proxy }}" -eq "true") { + $params.SkipProxy = $true + } + & "${{ github.workspace }}\.github\scripts\run-shadowsocketproxy-iperf.ps1" @params + + - name: Upload benchmark results + if: always() + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 + with: + name: shadowsocketproxy-iperf-results + path: ${{ github.workspace }}\shadowsocketproxy-iperf diff --git a/docs/shadowsocketproxy-iperf.md b/docs/shadowsocketproxy-iperf.md new file mode 100644 index 00000000..a35f2ce6 --- /dev/null +++ b/docs/shadowsocketproxy-iperf.md @@ -0,0 +1,45 @@ +# ShadowSocketProxy iperf3 benchmark + +The `shadowsocketproxy-iperf` workflow compares outbound TCP and UDP traffic +from WSL2 to a Windows peer with and without ShadowSocketProxy. + +## Pair provisioning + +The GitHub Actions runner must be a Windows 2022 self-hosted runner with the +`wsl` and `ebpf` labels. Provision it before running the workflow: + +- Install WSL2 and a pinned Ubuntu distribution. +- Install Rust 1.96.1, `cargo`, `tc`, and `iperf3` inside WSL. +- Confirm the WSL kernel permits TC/BPF attachment. +- Install a pinned Windows `iperf3.exe` on the target peer. +- Enable PowerShell 7 remoting between the runner and target peer. + +The target peer must allow TCP and UDP port 5201. The workflow creates +temporary firewall rules with the `netperf-iperf3-*` prefix and removes them +after the run. + +The default target is `netperf-peer`. Its IPv4 address is resolved on the +runner, matching the existing eBPF performance workflows; an address can be +provided explicitly when DNS is unavailable. + +## Measurements + +Each run executes single-stream TCP, four-stream TCP, and UDP tests for every +configured UDP rate. The baseline runs before the proxy is started. The proxy +case starts the WSL control service and Windows host proxy, attaches the BPF +program, and repeats the same matrix. + +JSON results and proxy logs are uploaded as the +`shadowsocketproxy-iperf-results` artifact. Use at least three repetitions and +compare throughput, UDP loss, jitter, and CPU or tracing data from the runner. + +## Local smoke checks + +The benchmark driver can be syntax-checked without a peer: + +```powershell +[void][scriptblock]::Create((Get-Content -Raw .github\scripts\run-shadowsocketproxy-iperf.ps1)) +``` + +A complete benchmark requires the paired Windows peer and cannot be completed +on a single workstation. From 0a79e2832ee836887a0a29d289e079fe92455862 Mon Sep 17 00:00:00 2001 From: Alan Jowett Date: Wed, 12 Aug 2026 12:12:53 -0700 Subject: [PATCH 2/8] Run benchmark workflow from pull requests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/shadowsocketproxy-iperf.yml | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/shadowsocketproxy-iperf.yml b/.github/workflows/shadowsocketproxy-iperf.yml index 335efa12..b7742b31 100644 --- a/.github/workflows/shadowsocketproxy-iperf.yml +++ b/.github/workflows/shadowsocketproxy-iperf.yml @@ -43,6 +43,13 @@ on: required: false default: false type: boolean + pull_request: + branches: + - main + paths: + - '.github/workflows/shadowsocketproxy-iperf.yml' + - '.github/scripts/run-shadowsocketproxy-iperf.ps1' + - 'docs/shadowsocketproxy-iperf.md' permissions: contents: read @@ -51,6 +58,16 @@ concurrency: group: shadowsocketproxy-iperf-${{ inputs.target_peer || 'target' }} cancel-in-progress: true +env: + SHADOW_REF: ${{ inputs.shadow_ref || 'main' }} + TARGET_PEER: ${{ inputs.target_peer || 'netperf-peer' }} + TARGET_ADDRESS: ${{ inputs.target_address || '' }} + DISTRIBUTION: ${{ inputs.distribution || 'Ubuntu' }} + DURATION: ${{ inputs.duration || '30' }} + RUNS: ${{ inputs.runs || '3' }} + UDP_RATES: ${{ inputs.udp_rates || '100M,1G' }} + SKIP_PROXY: ${{ inputs.skip_proxy || false }} + jobs: benchmark: name: ShadowSocketProxy iperf3 benchmark @@ -84,13 +101,13 @@ jobs: shell: pwsh run: | git clone --no-checkout --filter=blob:none https://github.com/Alan-Jowett/ShadowSocketProxy.git "${{ github.workspace }}\ShadowSocketProxy" - git -C "${{ github.workspace }}\ShadowSocketProxy" fetch --depth 1 origin "${{ inputs.shadow_ref }}" + git -C "${{ github.workspace }}\ShadowSocketProxy" fetch --depth 1 origin "$env:SHADOW_REF" git -C "${{ github.workspace }}\ShadowSocketProxy" checkout --detach FETCH_HEAD - name: Validate WSL prerequisites shell: pwsh run: | - $distribution = "${{ inputs.distribution }}" + $distribution = $env:DISTRIBUTION wsl.exe -d $distribution -- uname -a wsl.exe -d $distribution -- sh -lc "command -v cargo && command -v tc && command -v iperf3" wsl.exe -d $distribution -u root -- sh -lc 'iface=$(ip route show default | awk "{print $5}"); tc qdisc show dev "$iface"' @@ -100,7 +117,7 @@ jobs: working-directory: ${{ github.workspace }}\ShadowSocketProxy run: | $wslRoot = "/mnt/c/$((Get-Location).Path.Substring(3) -replace '\\','/')" - wsl.exe -d "${{ inputs.distribution }}" -- bash -lc "cd '$wslRoot' && make -C crates/bpf clean all && cargo build --locked --release -p shadow-socket-proxy-control --features tls-psk" + wsl.exe -d "$env:DISTRIBUTION" -- bash -lc "cd '$wslRoot' && make -C crates/bpf clean all && cargo build --locked --release -p shadow-socket-proxy-control --features tls-psk" if ($LASTEXITCODE -ne 0) { throw "WSL ShadowSocketProxy build failed" } - name: Build Windows host proxy @@ -113,26 +130,26 @@ jobs: - name: Run baseline and proxy benchmark shell: pwsh run: | - $targetAddress = "${{ inputs.target_address }}" + $targetAddress = $env:TARGET_ADDRESS if ([string]::IsNullOrWhiteSpace($targetAddress)) { - $targetAddress = [System.Net.Dns]::GetHostAddresses("${{ inputs.target_peer }}") | + $targetAddress = [System.Net.Dns]::GetHostAddresses($env:TARGET_PEER) | Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork } | Select-Object -First 1 -ExpandProperty IPAddressToString } if ([string]::IsNullOrWhiteSpace($targetAddress)) { - throw "Could not resolve an IPv4 address for target peer '${{ inputs.target_peer }}'" + throw "Could not resolve an IPv4 address for target peer '$env:TARGET_PEER'" } - Write-Host "Using target peer ${{ inputs.target_peer }} at $targetAddress" + Write-Host "Using target peer $env:TARGET_PEER at $targetAddress" $params = @{ - TargetPeer = "${{ inputs.target_peer }}" + TargetPeer = $env:TARGET_PEER TargetAddress = $targetAddress ShadowRoot = "${{ github.workspace }}\ShadowSocketProxy" - Distribution = "${{ inputs.distribution }}" - Duration = [int]"${{ inputs.duration }}" - Runs = [int]"${{ inputs.runs }}" - UdpRates = "${{ inputs.udp_rates }}" + Distribution = $env:DISTRIBUTION + Duration = [int]$env:DURATION + Runs = [int]$env:RUNS + UdpRates = $env:UDP_RATES } - if ("${{ inputs.skip_proxy }}" -eq "true") { + if ($env:SKIP_PROXY -eq "true") { $params.SkipProxy = $true } & "${{ github.workspace }}\.github\scripts\run-shadowsocketproxy-iperf.ps1" @params From 2c75fe1d3d5e9458a1ae64b59e2887dbb64fc305 Mon Sep 17 00:00:00 2001 From: Alan Jowett Date: Wed, 12 Aug 2026 12:15:53 -0700 Subject: [PATCH 3/8] Use existing lab runner labels Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/shadowsocketproxy-iperf.yml | 1 - docs/shadowsocketproxy-iperf.md | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/shadowsocketproxy-iperf.yml b/.github/workflows/shadowsocketproxy-iperf.yml index b7742b31..9ea9e317 100644 --- a/.github/workflows/shadowsocketproxy-iperf.yml +++ b/.github/workflows/shadowsocketproxy-iperf.yml @@ -76,7 +76,6 @@ jobs: - lab - os-windows-2022 - x64 - - wsl - ebpf steps: diff --git a/docs/shadowsocketproxy-iperf.md b/docs/shadowsocketproxy-iperf.md index a35f2ce6..85ed9900 100644 --- a/docs/shadowsocketproxy-iperf.md +++ b/docs/shadowsocketproxy-iperf.md @@ -6,7 +6,8 @@ from WSL2 to a Windows peer with and without ShadowSocketProxy. ## Pair provisioning The GitHub Actions runner must be a Windows 2022 self-hosted runner with the -`wsl` and `ebpf` labels. Provision it before running the workflow: +existing `lab`, `x64`, and `ebpf` labels. Provision it before running the +workflow: - Install WSL2 and a pinned Ubuntu distribution. - Install Rust 1.96.1, `cargo`, `tc`, and `iperf3` inside WSL. From 1f741792b0d13d89e33db051757ef19ace3a92ae Mon Sep 17 00:00:00 2001 From: Alan Jowett Date: Wed, 12 Aug 2026 12:18:53 -0700 Subject: [PATCH 4/8] Use standard Windows lab runner labels Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/shadowsocketproxy-iperf.yml | 1 - docs/shadowsocketproxy-iperf.md | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/shadowsocketproxy-iperf.yml b/.github/workflows/shadowsocketproxy-iperf.yml index 9ea9e317..77233997 100644 --- a/.github/workflows/shadowsocketproxy-iperf.yml +++ b/.github/workflows/shadowsocketproxy-iperf.yml @@ -76,7 +76,6 @@ jobs: - lab - os-windows-2022 - x64 - - ebpf steps: - name: Checkout netperf diff --git a/docs/shadowsocketproxy-iperf.md b/docs/shadowsocketproxy-iperf.md index 85ed9900..5a91b1ff 100644 --- a/docs/shadowsocketproxy-iperf.md +++ b/docs/shadowsocketproxy-iperf.md @@ -6,8 +6,7 @@ from WSL2 to a Windows peer with and without ShadowSocketProxy. ## Pair provisioning The GitHub Actions runner must be a Windows 2022 self-hosted runner with the -existing `lab`, `x64`, and `ebpf` labels. Provision it before running the -workflow: +existing `lab` and `x64` labels. Provision it before running the workflow: - Install WSL2 and a pinned Ubuntu distribution. - Install Rust 1.96.1, `cargo`, `tc`, and `iperf3` inside WSL. From 0f3ea08943126237aaca0131a4fc0afc87666c3c Mon Sep 17 00:00:00 2001 From: Alan Jowett Date: Wed, 12 Aug 2026 12:32:55 -0700 Subject: [PATCH 5/8] Use checkout action for ShadowSocketProxy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/shadowsocketproxy-iperf.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/shadowsocketproxy-iperf.yml b/.github/workflows/shadowsocketproxy-iperf.yml index 77233997..bfec06a4 100644 --- a/.github/workflows/shadowsocketproxy-iperf.yml +++ b/.github/workflows/shadowsocketproxy-iperf.yml @@ -96,11 +96,12 @@ jobs: New-Item -ItemType Directory -Force -Path "$workspace\results" | Out-Null - name: Checkout ShadowSocketProxy - shell: pwsh - run: | - git clone --no-checkout --filter=blob:none https://github.com/Alan-Jowett/ShadowSocketProxy.git "${{ github.workspace }}\ShadowSocketProxy" - git -C "${{ github.workspace }}\ShadowSocketProxy" fetch --depth 1 origin "$env:SHADOW_REF" - git -C "${{ github.workspace }}\ShadowSocketProxy" checkout --detach FETCH_HEAD + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 + with: + repository: Alan-Jowett/ShadowSocketProxy + ref: ${{ env.SHADOW_REF }} + path: ShadowSocketProxy + fetch-depth: 1 - name: Validate WSL prerequisites shell: pwsh From 8fcee5ca3b4d152e11d91686d84a248c3f7074ca Mon Sep 17 00:00:00 2001 From: Alan Jowett Date: Wed, 12 Aug 2026 12:34:13 -0700 Subject: [PATCH 6/8] Discover installed WSL distribution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/shadowsocketproxy-iperf.yml | 17 ++++++++++++++--- docs/shadowsocketproxy-iperf.md | 5 ++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/shadowsocketproxy-iperf.yml b/.github/workflows/shadowsocketproxy-iperf.yml index bfec06a4..06bea470 100644 --- a/.github/workflows/shadowsocketproxy-iperf.yml +++ b/.github/workflows/shadowsocketproxy-iperf.yml @@ -19,9 +19,9 @@ on: default: '' type: string distribution: - description: 'Installed WSL distribution on the runner' + description: 'Installed WSL distribution; blank selects the first installed distribution' required: false - default: 'Ubuntu' + default: '' type: string duration: description: 'iperf3 duration per run in seconds' @@ -62,7 +62,7 @@ env: SHADOW_REF: ${{ inputs.shadow_ref || 'main' }} TARGET_PEER: ${{ inputs.target_peer || 'netperf-peer' }} TARGET_ADDRESS: ${{ inputs.target_address || '' }} - DISTRIBUTION: ${{ inputs.distribution || 'Ubuntu' }} + DISTRIBUTION: ${{ inputs.distribution || '' }} DURATION: ${{ inputs.duration || '30' }} RUNS: ${{ inputs.runs || '3' }} UDP_RATES: ${{ inputs.udp_rates || '100M,1G' }} @@ -107,6 +107,17 @@ jobs: shell: pwsh run: | $distribution = $env:DISTRIBUTION + if ([string]::IsNullOrWhiteSpace($distribution)) { + $distribution = (& wsl.exe --list --quiet | Out-String) -replace "`0", '' + $distribution = ($distribution -split "`r?`n" | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Select-Object -First 1).Trim() + } + if ([string]::IsNullOrWhiteSpace($distribution)) { + throw 'No WSL distribution is installed on the runner' + } + "DISTRIBUTION=$distribution" >> $env:GITHUB_ENV + Write-Host "Using WSL distribution: $distribution" wsl.exe -d $distribution -- uname -a wsl.exe -d $distribution -- sh -lc "command -v cargo && command -v tc && command -v iperf3" wsl.exe -d $distribution -u root -- sh -lc 'iface=$(ip route show default | awk "{print $5}"); tc qdisc show dev "$iface"' diff --git a/docs/shadowsocketproxy-iperf.md b/docs/shadowsocketproxy-iperf.md index 5a91b1ff..474e2811 100644 --- a/docs/shadowsocketproxy-iperf.md +++ b/docs/shadowsocketproxy-iperf.md @@ -8,7 +8,7 @@ from WSL2 to a Windows peer with and without ShadowSocketProxy. The GitHub Actions runner must be a Windows 2022 self-hosted runner with the existing `lab` and `x64` labels. Provision it before running the workflow: -- Install WSL2 and a pinned Ubuntu distribution. +- Install WSL2 and a pinned Linux distribution. - Install Rust 1.96.1, `cargo`, `tc`, and `iperf3` inside WSL. - Confirm the WSL kernel permits TC/BPF attachment. - Install a pinned Windows `iperf3.exe` on the target peer. @@ -22,6 +22,9 @@ The default target is `netperf-peer`. Its IPv4 address is resolved on the runner, matching the existing eBPF performance workflows; an address can be provided explicitly when DNS is unavailable. +The workflow uses the first installed WSL distribution when no distribution is +specified. Set the `distribution` input to pin a particular distribution. + ## Measurements Each run executes single-stream TCP, four-stream TCP, and UDP tests for every From 6212a006e470db6229c5a365c5979ce1d1ddeca0 Mon Sep 17 00:00:00 2001 From: Alan Jowett Date: Wed, 12 Aug 2026 12:35:45 -0700 Subject: [PATCH 7/8] Report missing WSL distribution clearly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/shadowsocketproxy-iperf.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/shadowsocketproxy-iperf.yml b/.github/workflows/shadowsocketproxy-iperf.yml index 06bea470..280d6616 100644 --- a/.github/workflows/shadowsocketproxy-iperf.yml +++ b/.github/workflows/shadowsocketproxy-iperf.yml @@ -108,13 +108,20 @@ jobs: run: | $distribution = $env:DISTRIBUTION if ([string]::IsNullOrWhiteSpace($distribution)) { - $distribution = (& wsl.exe --list --quiet | Out-String) -replace "`0", '' - $distribution = ($distribution -split "`r?`n" | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - Select-Object -First 1).Trim() + $wslList = ((& wsl.exe --list --quiet 2>$null | Out-String) -replace "`0", '') + $candidates = $wslList -split "`r?`n" | + ForEach-Object { $_.Trim() } | + Where-Object { $_ -match '^[A-Za-z0-9][A-Za-z0-9._-]*$' } + foreach ($candidate in $candidates) { + & wsl.exe -d $candidate -- true 2>$null + if ($LASTEXITCODE -eq 0) { + $distribution = $candidate + break + } + } } if ([string]::IsNullOrWhiteSpace($distribution)) { - throw 'No WSL distribution is installed on the runner' + throw 'No usable WSL distribution is installed on the runner; provision WSL2 with a Linux distribution before running this benchmark' } "DISTRIBUTION=$distribution" >> $env:GITHUB_ENV Write-Host "Using WSL distribution: $distribution" From 352bf552608cd4e56e7182ad4e3a12144b813ef2 Mon Sep 17 00:00:00 2001 From: Alan Jowett Date: Wed, 12 Aug 2026 12:37:13 -0700 Subject: [PATCH 8/8] Require provisioned WSL benchmark runners Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/shadowsocketproxy-iperf.yml | 1 + docs/shadowsocketproxy-iperf.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/shadowsocketproxy-iperf.yml b/.github/workflows/shadowsocketproxy-iperf.yml index 280d6616..82c6769a 100644 --- a/.github/workflows/shadowsocketproxy-iperf.yml +++ b/.github/workflows/shadowsocketproxy-iperf.yml @@ -76,6 +76,7 @@ jobs: - lab - os-windows-2022 - x64 + - wsl steps: - name: Checkout netperf diff --git a/docs/shadowsocketproxy-iperf.md b/docs/shadowsocketproxy-iperf.md index 474e2811..cdeb30b2 100644 --- a/docs/shadowsocketproxy-iperf.md +++ b/docs/shadowsocketproxy-iperf.md @@ -6,7 +6,8 @@ from WSL2 to a Windows peer with and without ShadowSocketProxy. ## Pair provisioning The GitHub Actions runner must be a Windows 2022 self-hosted runner with the -existing `lab` and `x64` labels. Provision it before running the workflow: +existing `lab` and `x64` labels plus a `wsl` label. Provision it before running +the workflow: - Install WSL2 and a pinned Linux distribution. - Install Rust 1.96.1, `cargo`, `tc`, and `iperf3` inside WSL.