param( [string]$SshUser = "dohertj2", [string]$SshHost = "10.100.0.35", [string]$TargetHost = "10.100.0.48", [int]$ListenPort = 32568, [int]$TargetPort = 32568, [ValidateSet("history", "event")] [string]$Scenario = "history", [string]$TagName = "OtOpcUaParityTest_001.Counter", [int]$LookbackMinutes = 1440, [int]$MaxRows = 1, [string]$OutputPath = $null ) $ErrorActionPreference = "Stop" $repoRoot = Split-Path -Parent $PSScriptRoot $stamp = Get-Date -Format "yyyyMMdd-HHmmss" if ([string]::IsNullOrWhiteSpace($OutputPath)) { $OutputPath = Join-Path $repoRoot "docs\reverse-engineering\system-boundary-via-debian-relay-$Scenario-$stamp.ndjson" } $localPy = Join-Path $env:TEMP "histsdk-simple-relay-$stamp.py" $remotePy = "/tmp/histsdk-simple-relay-$stamp.py" $remoteLog = "/tmp/histsdk-simple-relay-$stamp.log" $sshTarget = "$SshUser@$SshHost" $tcpOwnerLog = Join-Path $env:TEMP "histsdk-tcp-owner-$stamp.ndjson" $relaySource = @' import argparse, selectors, socket, sys, time parser = argparse.ArgumentParser() parser.add_argument("--listen-port", type=int, default=32568) parser.add_argument("--target", required=True) parser.add_argument("--target-port", type=int, default=32568) args = parser.parse_args() sel = selectors.DefaultSelector() peers = {} def log(text): print(time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), text, flush=True) def close_pair(sock): peer = peers.pop(sock, None) if peer is None: return peers.pop(peer, None) for item in (sock, peer): try: sel.unregister(item) except Exception: pass try: item.close() except Exception: pass def relay(sock): peer = peers.get(sock) if peer is None: return try: data = sock.recv(65536) except Exception as exc: log("recv_error " + repr(exc)) close_pair(sock) return if not data: close_pair(sock) return try: peer.sendall(data) except Exception as exc: log("send_error " + repr(exc)) close_pair(sock) def accept(listener): client, address = listener.accept() client.setblocking(False) upstream = socket.create_connection((args.target, args.target_port), timeout=8) upstream.setblocking(False) peers[client] = upstream peers[upstream] = client sel.register(client, selectors.EVENT_READ, relay) sel.register(upstream, selectors.EVENT_READ, relay) log("connect " + repr(address)) listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listener.bind(("0.0.0.0", args.listen_port)) listener.listen(16) listener.setblocking(False) sel.register(listener, selectors.EVENT_READ, accept) log("listening") while True: for key, _ in sel.select(timeout=1): key.data(key.fileobj) '@ Push-Location $repoRoot try { Set-Content -LiteralPath $localPy -Value $relaySource -Encoding UTF8 scp -q $localPy "${sshTarget}:$remotePy" $remoteCommand = "nohup python3 $remotePy --listen-port $ListenPort --target $TargetHost --target-port $TargetPort > $remoteLog 2>&1 < /dev/null & echo `$!" $relayPid = (ssh -o BatchMode=yes -o ConnectTimeout=8 $sshTarget $remoteCommand).Trim() Start-Sleep -Seconds 1 "--- relay startup log ---" | Tee-Object -FilePath $OutputPath -Append | Out-Host ssh -o BatchMode=yes -o ConnectTimeout=8 $sshTarget "cat $remoteLog 2>/dev/null || true" | Tee-Object -FilePath $OutputPath -Append | Out-Host $monitorJob = Start-Job -ScriptBlock { param($RemoteAddress, $RemotePort, $LogPath) $deadline = (Get-Date).AddSeconds(45) while ((Get-Date) -lt $deadline) { Get-NetTCPConnection -RemoteAddress $RemoteAddress -RemotePort $RemotePort -ErrorAction SilentlyContinue | ForEach-Object { $processName = $null try { $processName = (Get-Process -Id $_.OwningProcess -ErrorAction Stop).ProcessName } catch { $processName = $null } [pscustomobject]@{ TimestampUtc = (Get-Date).ToUniversalTime().ToString("O") LocalAddress = $_.LocalAddress LocalPort = $_.LocalPort RemoteAddress = $_.RemoteAddress RemotePort = $_.RemotePort State = $_.State.ToString() OwningProcess = $_.OwningProcess ProcessName = $processName } | ConvertTo-Json -Compress } | Add-Content -LiteralPath $LogPath Start-Sleep -Milliseconds 50 } } -ArgumentList $SshHost, $ListenPort, $tcpOwnerLog powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Attach-NativeTraceHarnessSystemBoundaryCapture.ps1 ` -Scenario $Scenario ` -ServerName $SshHost ` -TagName $TagName ` -LookbackMinutes $LookbackMinutes ` -MaxRows $MaxRows ` -PreLoadSleepSeconds 10 ` -OutputPath $OutputPath Stop-Job -Job $monitorJob -ErrorAction SilentlyContinue Receive-Job -Job $monitorJob -ErrorAction SilentlyContinue | Out-Null Remove-Job -Job $monitorJob -Force -ErrorAction SilentlyContinue "--- windows tcp owner monitor ---" | Tee-Object -FilePath $OutputPath -Append | Out-Host Get-Content -LiteralPath $tcpOwnerLog -ErrorAction SilentlyContinue | Sort-Object -Unique | Tee-Object -FilePath $OutputPath -Append | Out-Host "--- relay final log ---" | Tee-Object -FilePath $OutputPath -Append | Out-Host ssh -o BatchMode=yes -o ConnectTimeout=8 $sshTarget "cat $remoteLog 2>/dev/null || true" | Tee-Object -FilePath $OutputPath -Append | Out-Host } finally { try { if ($relayPid) { ssh -o BatchMode=yes $sshTarget "kill $relayPid 2>/dev/null || true; rm -f $remotePy $remoteLog" | Out-Null } } catch { Write-Warning "Relay cleanup failed: $_" } Remove-Item -LiteralPath $localPy -ErrorAction SilentlyContinue Remove-Item -LiteralPath $tcpOwnerLog -ErrorAction SilentlyContinue Pop-Location }