Files
histsdk/scripts/Attach-SystemBoundaryViaDebianRelay.ps1
dohertj2 c95824a65d Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit
Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:

- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass

Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.

Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 06:31:48 -04:00

177 lines
6.2 KiB
PowerShell

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
}