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>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# WCF Open2 Evidence
|
||||
|
||||
Command:
|
||||
|
||||
```powershell
|
||||
dotnet run --no-build --project tools\AVEVA.Historian.ReverseEngineering -- wcf-open2 localhost 32568
|
||||
```
|
||||
|
||||
Confirmed:
|
||||
|
||||
- Native `CHistoryConnectionWCF.OpenConnection2` serializes a byte buffer and
|
||||
calls `HistoryServiceContract.IHistoryServiceContract2.OpenConnection2`, WCF
|
||||
operation `Open2`.
|
||||
- The managed contract for `Open2` is reachable through the fully managed
|
||||
WCF/MDAS binding.
|
||||
- The version-1 open buffer layout from `CServiceUtility.SaveOpenConnectionParams`
|
||||
is:
|
||||
- `ushort` packet version `1`
|
||||
- strings encoded as `uint32` UTF-16 character count followed by UTF-16LE bytes
|
||||
without a null terminator
|
||||
- `uint32` process id
|
||||
- `uint32` password byte length followed by password bytes
|
||||
- `byte` client type
|
||||
- `ushort` client version
|
||||
- `uint32` connection mode
|
||||
- default metadata namespace: flag byte plus three empty encoded strings
|
||||
- Empty credential attempts now return a native error buffer instead of
|
||||
`InvalidPacketVersion`.
|
||||
|
||||
Observed sanitized result:
|
||||
|
||||
- Empty credential modes return error bytes `04 AB 00 00 00`.
|
||||
- Interpreted as native custom error type `4`, code `171`, this maps to
|
||||
`AuthenticationFailed` in `ArchestrA.HistorianAccessError.ErrorValue`.
|
||||
- Integrated-security mode `1026` with WCF transport security fails before the
|
||||
operation call with `ProtocolException: The requested upgrade is not
|
||||
supported by 'net.tcp://localhost:32568/Hist'`. This proves the server's MDAS
|
||||
`/Hist` endpoint is not using standard WCF transport-security upgrade
|
||||
semantics.
|
||||
- The same version-1 integrated-security buffer succeeds when sent to
|
||||
`net.tcp://localhost:32568/Hist-Integrated` through the managed WCF Windows
|
||||
transport-security binding. The response has a 32-byte output buffer and no
|
||||
error buffer. Do not commit the output bytes; they are treated as an
|
||||
ephemeral session artifact.
|
||||
- Client-version values `0`, `4`, and `11` all return the same 32-byte output
|
||||
shape for this version-1 request.
|
||||
- The successful 32-byte output is not yet decoded. `Close(1)` returns native
|
||||
return code `4`; `Retr.IsOriginalAllowed(1)` also returns native return code
|
||||
`4`. `Close2` candidates derived from the output buffer all fail with native
|
||||
error code `51`: first 16 bytes as .NET Guid, first 16 bytes as RFC4122 Guid,
|
||||
first 16 bytes as hex/base64, and all 32 bytes as hex/base64. Therefore the
|
||||
managed session-open path is known, but the usable client handle/session-token
|
||||
decoding is still unknown.
|
||||
- Decompiled `CClientInfo.DeserializeOpenConnectionOutParams` expects a
|
||||
different native-consumable output shape: first byte must be packet version
|
||||
`2` or `3`, followed by a 4-byte field, a 16-byte field, an 8-byte field, and
|
||||
for version `3` another 8-byte field. The 32-byte output from the current
|
||||
version-1 managed request does not match that shape.
|
||||
- Earlier integrated-security probing through the no-security MDAS binding could
|
||||
trigger server-side `ServiceSecurityContext.WindowsIdentity` null-reference
|
||||
failures because that binding does not establish a Windows security context.
|
||||
- The server message `Failed to get user token... check that the username has
|
||||
been added to Historian Users` is authentication/configuration evidence, not
|
||||
packet-version evidence. It indicates the supplied Windows/user credential is
|
||||
not authorized as a Historian user on the target server.
|
||||
- The managed harness now decodes these five-byte buffers as:
|
||||
- byte `0`: native error type
|
||||
- bytes `1..4`: little-endian unsigned error code
|
||||
- known names for codes `1`, `73`, and `171`
|
||||
|
||||
Interpretation:
|
||||
|
||||
- `Hist.Open2` byte-buffer framing is confirmed for the legacy version-1 path.
|
||||
- Fully managed integrated Windows session-open is confirmed for the local
|
||||
2020 Historian when the client uses `/Hist-Integrated`.
|
||||
- The remaining blocker is decoding the successful 32-byte `Open2` output into
|
||||
the handle/token shape used by close, status, retrieval, and query calls.
|
||||
|
||||
## Native integrated-auth baseline
|
||||
|
||||
Reverse-engineering script:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Test-AahClientManagedOpen.ps1 -HostName localhost -Port 32568 -IntegratedSecurity -ConnectionWaitSeconds 15
|
||||
```
|
||||
|
||||
Sanitized result:
|
||||
|
||||
- `Success = true`
|
||||
- `ConnectedToServer = true`
|
||||
- `ConnectionPending = false`
|
||||
- `ConnectionErrorOccurred = false`
|
||||
- `ErrorCode = Success`
|
||||
|
||||
The native wrapper returns from `OpenConnection` before the connection is fully
|
||||
settled, so scripts must poll `GetConnectionStatus` before treating the open as
|
||||
usable.
|
||||
|
||||
Reverse-engineering read smoke:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Test-AahClientManagedReadIntegrated.ps1 -TagName __codex_missing_probe_tag__ -LookbackMinutes 1 -MaxRows 1 -ConnectionWaitSeconds 15
|
||||
```
|
||||
|
||||
Sanitized result:
|
||||
|
||||
- `OpenSuccess = true`
|
||||
- `ConnectedToServer = true`
|
||||
- `StartQuerySuccess = false`
|
||||
- `StartQueryErrorCode = TagNotFound`
|
||||
- `StartQueryErrorDescription = error = 127 (Tag not found)`
|
||||
|
||||
This is useful positive evidence: the native integrated-auth path reaches the
|
||||
real Historian query service and fails only because the deliberately fake probe
|
||||
tag does not exist.
|
||||
|
||||
Known historized tags can be discovered from the local Galaxy Repository:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Find-GalaxyHistorizedTags.ps1 -Limit 5
|
||||
```
|
||||
|
||||
Observed candidates:
|
||||
|
||||
- `OtOpcUaParityTest_001.Counter`
|
||||
- `TestMachine_001.TestHistoryValue`
|
||||
- `TestMachine_002.TestHistoryValue`
|
||||
- `TestMachine_003.TestHistoryValue`
|
||||
- `TestMachine_004.TestHistoryValue`
|
||||
|
||||
Using `OtOpcUaParityTest_001.Counter` over a 1440-minute lookback through
|
||||
`aahClientManaged.dll` returned a successful one-row native query:
|
||||
|
||||
- `StartQuerySuccess = true`
|
||||
- `RowCount = 1`
|
||||
- `StartDateTime = 2026-04-30T11:00:29.4340342Z`
|
||||
- `EndDateTime = 2026-04-30T11:00:29.4340342Z`
|
||||
- `Quality = 133`
|
||||
- `OpcQuality = 192`
|
||||
- `QualityDetail = 248`
|
||||
- `Value = 0`
|
||||
- `PercentGood = 100`
|
||||
Reference in New Issue
Block a user