[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$GameImage,
[Parameter(Mandatory)][string]$Dumpcap,
[Parameter(Mandatory)][string]$OutputDirectory,
[ValidateRange(1, 120)][int]$Minutes = 30,
[ValidateRange(8, 1024)][int]$MaxTotalMiB = 256,
[switch]$KeepWaitingForGame
)
$ErrorActionPreference = 'Stop'
$taskImage = (Resolve-Path -LiteralPath $GameImage).Path
$taskDumpcap = (Resolve-Path -LiteralPath $Dumpcap).Path
$taskOutput = [IO.Path]::GetFullPath($OutputDirectory)
[IO.Directory]::CreateDirectory($taskOutput) | Out-Null
$taskStopFile = Join-Path $taskOutput 'stop.request'
$taskManifest = Join-Path $taskOutput 'ownership.jsonl'
$taskStatus = Join-Path $taskOutput 'status.json'
$taskDeadline = [DateTimeOffset]::UtcNow.AddMinutes($Minutes)
$taskChild = $null
$taskSegments = 0
function Write-TaskStatus([string]$State, [string]$Message) {
@{ state=$State; message=$Message; updatedAt=[DateTimeOffset]::UtcNow; supervisorPid=$PID; gameImage=$taskImage;
segments=$taskSegments; outputDirectory=$taskOutput; stopFile=$taskStopFile; deadline=$taskDeadline;
scope='Passive Npcap capture of observed game TCP/UDP local ports. Port filters are not OS process isolation; ownership is sampled per segment. TLS remains encrypted.'
} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $taskStatus -Encoding utf8
}
try {
Write-TaskStatus 'starting' 'Discovering active capture interfaces; no game injection or network configuration changes.'
$taskUpGuids = @(Get-NetAdapter | Where-Object Status -eq 'Up' | ForEach-Object { $_.InterfaceGuid.ToString().Trim('{}').ToUpperInvariant() })
$taskInterfaceLines = @(& $taskDumpcap -D 2>&1 | ForEach-Object { $_.ToString() })
if ($LASTEXITCODE -ne 0) { throw 'dumpcap could not enumerate Npcap interfaces.' }
$taskInterfaces = @($taskInterfaceLines | ForEach-Object {
if ($_ -match '^\d+\.\s+(\S+)') {
$taskInterfaceName = $Matches[1]
if ($taskInterfaceName -eq '\Device\NPF_Loopback') { $taskInterfaceName }
elseif ($taskInterfaceName -match '\{[^}]+\}') {
if ($taskUpGuids -contains $Matches[0].Trim('{}').ToUpperInvariant()) { $taskInterfaceName }
}
}
})
if ($taskInterfaces.Count -eq 0) { throw 'No active capture interface is available.' }
while ([DateTimeOffset]::UtcNow -lt $taskDeadline -and -not (Test-Path -LiteralPath $taskStopFile)) {
$taskGames = @(Get-Process -Name EliteDangerous64 -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $taskImage })
if ($taskGames.Count -eq 0 -and $KeepWaitingForGame) { Write-TaskStatus 'waiting' 'Waiting for the selected game installation to restart.'; Start-Sleep -Seconds 3; continue }
if ($taskGames.Count -ne 1) { Write-TaskStatus 'stopped' 'The selected game instance is no longer unique or has exited.'; break }
$taskGamePid = $taskGames[0].Id
$taskTcp = @(Get-NetTCPConnection -OwningProcess $taskGamePid -ErrorAction SilentlyContinue | Where-Object { $_.RemotePort -gt 0 -and $_.LocalPort -gt 0 } | Select-Object LocalAddress,LocalPort,RemoteAddress,RemotePort,State)
$taskUdp = @(Get-NetUDPEndpoint -OwningProcess $taskGamePid -ErrorAction SilentlyContinue | Select-Object LocalAddress,LocalPort)
$taskClauses = @($taskTcp | ForEach-Object {
'(tcp and ((src host {0} and src port {1}) or (dst host {0} and dst port {1})))' -f $_.LocalAddress,$_.LocalPort
}) + @($taskUdp | ForEach-Object {
if ($_.LocalAddress -in @('0.0.0.0','::')) { '(udp port {0})' -f $_.LocalPort }
else { '(udp and ((src host {0} and src port {1}) or (dst host {0} and dst port {1})))' -f $_.LocalAddress,$_.LocalPort }
})
if ($taskClauses.Count -eq 0) { Write-TaskStatus 'waiting' 'The game has no observed network ports.'; Start-Sleep -Seconds 3; continue }
$taskBytes = (Get-ChildItem -LiteralPath $taskOutput -Filter '*.pcapng' -File | Measure-Object Length -Sum).Sum
if ($taskBytes -ge $MaxTotalMiB * 1MB) { Write-TaskStatus 'stopped' 'Capture size limit reached; existing evidence preserved.'; break }
$taskFilter = (@($taskClauses | Sort-Object -Unique) -join ' or ')
$taskSegments++
$taskPrefix = Join-Path $taskOutput ('game-{0:0000}.pcapng' -f $taskSegments)
$taskDuration = [Math]::Max(1, [Math]::Min(30, [Math]::Floor(($taskDeadline - [DateTimeOffset]::UtcNow).TotalSeconds)))
$taskStart = [Diagnostics.ProcessStartInfo]::new($taskDumpcap)
$taskStart.UseShellExecute = $false
$taskStart.CreateNoWindow = $true
$taskStart.RedirectStandardError = $true
$taskStart.RedirectStandardOutput = $true
foreach ($taskInterface in $taskInterfaces) { $taskStart.ArgumentList.Add('-i'); $taskStart.ArgumentList.Add($taskInterface); $taskStart.ArgumentList.Add('-f'); $taskStart.ArgumentList.Add($taskFilter) }
foreach ($taskArg in @('-q','-s','0','-a',"duration:$taskDuration",'-a','files:2','-b','filesize:4096','-b','files:2','-w',$taskPrefix)) { $taskStart.ArgumentList.Add($taskArg) }
$taskChild = [Diagnostics.Process]::Start($taskStart)
$taskReadError = $taskChild.StandardError.ReadToEndAsync()
$taskReadOutput = $taskChild.StandardOutput.ReadToEndAsync()
@{observedAt=[DateTimeOffset]::UtcNow; gamePid=$taskGamePid; dumpcapPid=$taskChild.Id; segment=$taskSegments; prefix=$taskPrefix; interfaces=$taskInterfaces; filter=$taskFilter; tcp=$taskTcp; udp=$taskUdp} | ConvertTo-Json -Depth 6 -Compress | Add-Content -LiteralPath $taskManifest -Encoding utf8
Write-TaskStatus 'capturing' 'Capturing an observed game port window; encrypted traffic is not decoded.'
$taskChild.WaitForExit()
$taskErrorText = $taskReadError.GetAwaiter().GetResult()
$null = $taskReadOutput.GetAwaiter().GetResult()
if ($taskChild.ExitCode -ne 0) { throw "dumpcap exited $($taskChild.ExitCode): $taskErrorText" }
$taskChild.Dispose(); $taskChild = $null
}
Write-TaskStatus 'stopped' 'Capture completed or stop requested. All evidence remains in the output directory.'
}
catch { Write-TaskStatus 'failed' $_.Exception.Message; throw }
finally {
# Only this script's own child may be stopped. Never touch the game or another capture process.
if ($null -ne $taskChild) {
try { if (-not $taskChild.HasExited) { $taskChild.WaitForExit(35000) | Out-Null } } finally { $taskChild.Dispose() }
}
}
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$GameImage,
[Parameter(Mandatory)][string]$Dumpcap,
[Parameter(Mandatory)][string]$OutputDirectory,
[ValidateRange(1, 120)][int]$Minutes = 30,
[ValidateRange(8, 1024)][int]$MaxTotalMiB = 256,
[switch]$KeepWaitingForGame
)
$ErrorActionPreference = 'Stop'
$taskImage = (Resolve-Path -LiteralPath $GameImage).Path
$taskDumpcap = (Resolve-Path -LiteralPath $Dumpcap).Path
$taskOutput = [IO.Path]::GetFullPath($OutputDirectory)
[IO.Directory]::CreateDirectory($taskOutput) | Out-Null
$taskStopFile = Join-Path $taskOutput 'stop.request'
$taskManifest = Join-Path $taskOutput 'ownership.jsonl'
$taskStatus = Join-Path $taskOutput 'status.json'
$taskDeadline = [DateTimeOffset]::UtcNow.AddMinutes($Minutes)
$taskChild = $null
$taskSegments = 0
function Write-TaskStatus([string]$State, [string]$Message) {
@{ state=$State; message=$Message; updatedAt=[DateTimeOffset]::UtcNow; supervisorPid=$PID; gameImage=$taskImage;
segments=$taskSegments; outputDirectory=$taskOutput; stopFile=$taskStopFile; deadline=$taskDeadline;
scope='Passive Npcap capture of observed game TCP/UDP local ports. Port filters are not OS process isolation; ownership is sampled per segment. TLS remains encrypted.'
} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $taskStatus -Encoding utf8
}
try {
Write-TaskStatus 'starting' 'Discovering active capture interfaces; no game injection or network configuration changes.'
$taskUpGuids = @(Get-NetAdapter | Where-Object Status -eq 'Up' | ForEach-Object { $_.InterfaceGuid.ToString().Trim('{}').ToUpperInvariant() })
$taskInterfaceLines = @(& $taskDumpcap -D 2>&1 | ForEach-Object { $_.ToString() })
if ($LASTEXITCODE -ne 0) { throw 'dumpcap could not enumerate Npcap interfaces.' }
$taskInterfaces = @($taskInterfaceLines | ForEach-Object {
if ($_ -match '^\d+\.\s+(\S+)') {
$taskInterfaceName = $Matches[1]
if ($taskInterfaceName -eq '\Device\NPF_Loopback') { $taskInterfaceName }
elseif ($taskInterfaceName -match '\{[^}]+\}') {
if ($taskUpGuids -contains $Matches[0].Trim('{}').ToUpperInvariant()) { $taskInterfaceName }
}
}
})
if ($taskInterfaces.Count -eq 0) { throw 'No active capture interface is available.' }
while ([DateTimeOffset]::UtcNow -lt $taskDeadline -and -not (Test-Path -LiteralPath $taskStopFile)) {
$taskGames = @(Get-Process -Name EliteDangerous64 -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $taskImage })
if ($taskGames.Count -eq 0 -and $KeepWaitingForGame) { Write-TaskStatus 'waiting' 'Waiting for the selected game installation to restart.'; Start-Sleep -Seconds 3; continue }
if ($taskGames.Count -ne 1) { Write-TaskStatus 'stopped' 'The selected game instance is no longer unique or has exited.'; break }
$taskGamePid = $taskGames[0].Id
$taskTcp = @(Get-NetTCPConnection -OwningProcess $taskGamePid -ErrorAction SilentlyContinue | Where-Object { $_.RemotePort -gt 0 -and $_.LocalPort -gt 0 } | Select-Object LocalAddress,LocalPort,RemoteAddress,RemotePort,State)
$taskUdp = @(Get-NetUDPEndpoint -OwningProcess $taskGamePid -ErrorAction SilentlyContinue | Select-Object LocalAddress,LocalPort)
$taskClauses = @($taskTcp | ForEach-Object {
'(tcp and ((src host {0} and src port {1}) or (dst host {0} and dst port {1})))' -f $_.LocalAddress,$_.LocalPort
}) + @($taskUdp | ForEach-Object {
if ($_.LocalAddress -in @('0.0.0.0','::')) { '(udp port {0})' -f $_.LocalPort }
else { '(udp and ((src host {0} and src port {1}) or (dst host {0} and dst port {1})))' -f $_.LocalAddress,$_.LocalPort }
})
if ($taskClauses.Count -eq 0) { Write-TaskStatus 'waiting' 'The game has no observed network ports.'; Start-Sleep -Seconds 3; continue }
$taskBytes = (Get-ChildItem -LiteralPath $taskOutput -Filter '*.pcapng' -File | Measure-Object Length -Sum).Sum
if ($taskBytes -ge $MaxTotalMiB * 1MB) { Write-TaskStatus 'stopped' 'Capture size limit reached; existing evidence preserved.'; break }
$taskFilter = (@($taskClauses | Sort-Object -Unique) -join ' or ')
$taskSegments++
$taskPrefix = Join-Path $taskOutput ('game-{0:0000}.pcapng' -f $taskSegments)
$taskDuration = [Math]::Max(1, [Math]::Min(30, [Math]::Floor(($taskDeadline - [DateTimeOffset]::UtcNow).TotalSeconds)))
$taskStart = [Diagnostics.ProcessStartInfo]::new($taskDumpcap)
$taskStart.UseShellExecute = $false
$taskStart.CreateNoWindow = $true
$taskStart.RedirectStandardError = $true
$taskStart.RedirectStandardOutput = $true
foreach ($taskInterface in $taskInterfaces) { $taskStart.ArgumentList.Add('-i'); $taskStart.ArgumentList.Add($taskInterface); $taskStart.ArgumentList.Add('-f'); $taskStart.ArgumentList.Add($taskFilter) }
foreach ($taskArg in @('-q','-s','0','-a',"duration:$taskDuration",'-a','files:2','-b','filesize:4096','-b','files:2','-w',$taskPrefix)) { $taskStart.ArgumentList.Add($taskArg) }
$taskChild = [Diagnostics.Process]::Start($taskStart)
$taskReadError = $taskChild.StandardError.ReadToEndAsync()
$taskReadOutput = $taskChild.StandardOutput.ReadToEndAsync()
@{observedAt=[DateTimeOffset]::UtcNow; gamePid=$taskGamePid; dumpcapPid=$taskChild.Id; segment=$taskSegments; prefix=$taskPrefix; interfaces=$taskInterfaces; filter=$taskFilter; tcp=$taskTcp; udp=$taskUdp} | ConvertTo-Json -Depth 6 -Compress | Add-Content -LiteralPath $taskManifest -Encoding utf8
Write-TaskStatus 'capturing' 'Capturing an observed game port window; encrypted traffic is not decoded.'
$taskChild.WaitForExit()
$taskErrorText = $taskReadError.GetAwaiter().GetResult()
$null = $taskReadOutput.GetAwaiter().GetResult()
if ($taskChild.ExitCode -ne 0) { throw "dumpcap exited $($taskChild.ExitCode): $taskErrorText" }
$taskChild.Dispose(); $taskChild = $null
}
Write-TaskStatus 'stopped' 'Capture completed or stop requested. All evidence remains in the output directory.'
}
catch { Write-TaskStatus 'failed' $_.Exception.Message; throw }
finally {
# Only this script's own child may be stopped. Never touch the game or another capture process.
if ($null -ne $taskChild) {
try { if (-not $taskChild.HasExited) { $taskChild.WaitForExit(35000) | Out-Null } } finally { $taskChild.Dispose() }
}
}