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,163 @@
|
||||
# WCF Contract Evidence
|
||||
|
||||
## Local run evidence
|
||||
|
||||
- `current\aahClient.dll` export inventory ran successfully. SHA256:
|
||||
`77a778988e2d8f2d0e88113f8c8b0788a0ef34fa5134938a353976778144dc83`.
|
||||
- `ArchestrA.HistorianAccess.OpenConnection` succeeded against `localhost:32568`
|
||||
using `HistorianConnectionArgs` with `ConnectionType=Process` and `ReadOnly=true`.
|
||||
- Holding that native connection open produced established TCP sessions from the
|
||||
native PowerShell process to `127.0.0.1:32568`; the server-side listener was
|
||||
owned by `SMSvcHost.exe`, consistent with WCF Net.TCP port sharing.
|
||||
- The managed harness command
|
||||
`dotnet run --no-build --project tools\AVEVA.Historian.ReverseEngineering -- wcf-probe localhost 32568`
|
||||
successfully called `GetV` through fully managed WCF/MDAS:
|
||||
- `net.tcp://localhost:32568/Hist` returned version `11`
|
||||
- `net.tcp://localhost:32568/Retr` returned version `4`
|
||||
- `net.tcp://localhost:32568/Stat` returned version `0`
|
||||
- `net.tcp://localhost:32568/Trx` returned version `2`
|
||||
- `net.tcp://localhost:32568/Storage` did not listen on this local install
|
||||
- `net.tcp://localhost:32568/HistCert` and `/Hist-Integrated` reset when
|
||||
called with the plain managed `GetV` contract, while prefixed variants such
|
||||
as `/HCAP/HistCert` returned `EndpointNotFound`
|
||||
Sanitized output is stored in `docs\reverse-engineering\wcf-probe-localhost.json`.
|
||||
- `netsh trace` and `pktmon` produced ETL files under
|
||||
`%TEMP%\histsdk-captures`, but their converted PCAPNG files contained zero
|
||||
packets. Built-in Windows packet capture is not sufficient for loopback
|
||||
evidence on this machine.
|
||||
- A dedicated managed certificate-binding probe now reaches `HistCert.GetV`
|
||||
through MDAS over WCF Net.TCP transport security:
|
||||
- `net.tcp://localhost:32568/HistCert` returned version `11`
|
||||
- `net.tcp://10.100.0.48:32568/HistCert` initially failed endpoint identity
|
||||
validation because the server certificate presented DNS identity
|
||||
`localhost`
|
||||
- the same remote endpoint returned version `11` when the client supplied
|
||||
endpoint DNS identity `localhost`
|
||||
Sanitized outputs are stored in
|
||||
`docs\reverse-engineering\wcf-cert-probe-localhost-latest.json`,
|
||||
`docs\reverse-engineering\wcf-cert-probe-remote-latest.json`, and
|
||||
`docs\reverse-engineering\wcf-cert-probe-remote-localhost-identity-latest.json`.
|
||||
- The same remote server also accepts the plain managed WCF/MDAS probe on the
|
||||
expected non-security-specific service paths:
|
||||
- `net.tcp://10.100.0.48:32568/Hist` returned version `11`
|
||||
- `net.tcp://10.100.0.48:32568/Retr` returned version `4`
|
||||
- `net.tcp://10.100.0.48:32568/Stat` returned version `0`
|
||||
- `net.tcp://10.100.0.48:32568/Trx` returned version `2`
|
||||
Sanitized output is stored in
|
||||
`docs\reverse-engineering\wcf-probe-remote-latest.json`.
|
||||
- Managed remote `Open2` evidence matches localhost: integrated Windows auth
|
||||
succeeds on `net.tcp://10.100.0.48:32568/Hist-Integrated`, while the same
|
||||
Windows transport binding fails on plain `/Hist` before dispatch. The
|
||||
successful returned handle is accepted by `Retr.IsOriginalAllowed`. Session
|
||||
output bytes and transient handle values are redacted in
|
||||
`docs\reverse-engineering\wcf-open2-remote-latest.json`.
|
||||
- Managed remote `StartQuery2` evidence is still negative but sharper: all 22
|
||||
reconstructed `DataQueryRequest` variants successfully open the integrated
|
||||
session and pass `Retr.IsOriginalAllowed`, then `StartQuery2` returns `false`
|
||||
with zero response and error sizes. The legacy `StartQuery` call returns code
|
||||
`238` for each request and also returns zero response size/no response buffer.
|
||||
Sanitized request hashes are stored in
|
||||
`docs\reverse-engineering\wcf-start-query-remote-latest.json`.
|
||||
- A later bounded managed replay of the first byte-matched full-history
|
||||
candidate used the same integrated open and `Retr.IsOriginalAllowed` path;
|
||||
`StartQuery2` still returned `false` with zero response/error sizes, while
|
||||
legacy `StartQuery` faulted with a server null-reference. This keeps
|
||||
`Open2` as useful connection evidence, but not as a viable replacement for
|
||||
the native `OpenConnection3` session state required by query reads.
|
||||
- Managed wildcard tag browse remains positive evidence for an `Open2`-backed
|
||||
operation: `Retr.StartLikeTagNameSearch` returned `0`, and one
|
||||
`GetLikeTagnames` batch returned the deterministic 66-byte single-tag buffer
|
||||
with SHA-256
|
||||
`2d450a55f392aed0026e9a957fefa3b116aab6ec81912c5d824c6b9a1ff5a4a1`.
|
||||
- Managed remote scalar tag calls also accept the integrated session handle:
|
||||
`Retr.GetTagTypeFromName` returns code `0` and tag type `1` for
|
||||
`OtOpcUaParityTest_001.Counter`; `Retr.IsManualTag` returns code `0` and
|
||||
`false`; legacy `Retr.GetTagInfoFromName` returns `238` with zero metadata
|
||||
bytes. Five `GetTgByNm` tag-name buffer variants and five `GetTg` tag-id
|
||||
buffer variants all return `238`, sequence `0`, and zero output bytes. This
|
||||
suggests the calls are reaching server logic but the metadata-returning
|
||||
contract shape or request buffer is still incomplete. Sanitized output is
|
||||
stored in
|
||||
`docs\reverse-engineering\wcf-tag-info-remote-latest.json`.
|
||||
|
||||
## Decompiled service contracts
|
||||
|
||||
`current\aahClientManaged.dll` contains WCF contracts with namespace `aa`:
|
||||
|
||||
- `HistoryServiceContract.IHistoryServiceContract`
|
||||
- `[ServiceContract(Name = "Hist", Namespace = "aa")]`
|
||||
- `GetInterfaceVersion` as operation `GetV`
|
||||
- `OpenConnection` as operation `Open`
|
||||
- `CloseConnection` as operation `Close`
|
||||
- `ValidateClient` as operation `VldC`
|
||||
- `UpdateClientStatus` as operation `UpdC`
|
||||
- `AddTags` as `AddT`, `RegisterTags` as `RTag`
|
||||
- `AddStreamValues` as `AddS`, `SetClientTimeOut` as `SetT`
|
||||
- `HistoryServiceContract.IHistoryServiceContract2`
|
||||
- `[ServiceContract(Name = "Hist", Namespace = "aa")]`
|
||||
- byte-buffer session open uses `OpenConnection2` as operation `Open2`
|
||||
- extended client status uses `UpdC2` / `UpdC3`
|
||||
- extended write and maintenance calls include `EnsT`, `DelT`, `AddS2`,
|
||||
`ExKey`, `ValCl`, and `GetI`
|
||||
- `RetrievalServiceContract.IRetrievalServiceContract`
|
||||
- `[ServiceContract(Name = "Retr", Namespace = "aa")]`
|
||||
- `StartQuery`, `GetNextQueryResultBuffer`, `EndQuery` use default
|
||||
operation names
|
||||
- tag type/name helpers and tag info calls use default operation names
|
||||
- `RetrievalServiceContract.IRetrievalServiceContract2/3/4`
|
||||
- extended bool/error-buffer variants
|
||||
- SQL/recordset byte stream calls
|
||||
- tag query calls `QTB`, `QTG`, `QTE`
|
||||
- event query calls use default operation names
|
||||
- extended property calls include `GetTgByNm2` and `GetTepByNm`
|
||||
- `StorageServiceContract.IStorageServiceContract`
|
||||
- `[ServiceContract(Name = "Storage", Namespace = "aa")]`
|
||||
- storage/session, metadata, streamed-value, block, snapshot, and delete-tag
|
||||
calls
|
||||
- `StatusServiceContract.IStatusServiceContract`
|
||||
- `[ServiceContract(Name = "Stat", Namespace = "aa")]`
|
||||
- `GetInterfaceVersion` as `GetV`
|
||||
- server time, timezone, DB case sensitivity, and logging use default
|
||||
operation names
|
||||
- `StatusServiceContract.IStatusServiceContract2`
|
||||
- extended status operations include `GetSystemParameter`, `GetTimeZoneNames`,
|
||||
license checks, historian info, and process/ping helpers
|
||||
- ping and historian-info helpers use aliases `PNGS`, `PNGP`, and `GETHI`
|
||||
- `TransactionServiceContract.ITransactionServiceContract`
|
||||
- `[ServiceContract(Name = "Trx", Namespace = "aa")]`
|
||||
- snapshot forwarding and non-streamed value transactions
|
||||
|
||||
`aahMDASEncoder.ClientMessageEncoder` wraps an inner WCF encoder and exposes
|
||||
media/content type `application/x-mdas`. This means the first managed driver
|
||||
transport target should be WCF Net.TCP plus the MDAS content-type encoder, not
|
||||
the earlier speculative raw-frame layer.
|
||||
|
||||
## Current unknowns
|
||||
|
||||
- Endpoint URI paths `net.tcp://{host}:32568/Hist`, `/Retr`, `/Stat`, and
|
||||
`/Trx` are confirmed for `GetV` calls on the local 2020 install.
|
||||
- Relay and local WCF probe evidence also identify security-specific history
|
||||
endpoints `/HistCert` and `/Hist-Integrated`. `/HistCert` is confirmed as a
|
||||
`Hist` contract endpoint when called with MDAS over TLS transport security;
|
||||
`/Hist-Integrated` remains the Windows negotiate endpoint for integrated
|
||||
session open.
|
||||
- Managed `Open2` evidence confirms `/Hist-Integrated` is the correct endpoint
|
||||
for integrated Windows auth. The plain `/Hist` endpoint rejects the Windows
|
||||
transport-security upgrade before dispatching the operation.
|
||||
- The storage contract is confirmed statically, but `/Storage` was not a
|
||||
listening endpoint in the local probe; storage may be routed through
|
||||
session-specific storage/shard endpoints.
|
||||
- `Hist.OpenConnection` reaches server logic, but the native password/session
|
||||
packet encoding is not decoded yet. See `wcf-open-localhost.md`.
|
||||
- `Hist.Open2` is confirmed reachable with a managed version-1 byte buffer.
|
||||
Empty credentials return custom native error `171` (`AuthenticationFailed`),
|
||||
and the harness decodes observed five-byte error buffers as type plus
|
||||
little-endian error code. This confirms framing has progressed past
|
||||
packet-version rejection. See `wcf-open2-localhost.md`.
|
||||
- `Stat` is reachable, but `CStatusConnectionWCF.GetServerTime` is a no-op stub
|
||||
in the decompiled native WCF path and handle-dependent status calls fail with
|
||||
handle `0`. See `wcf-status-localhost.md`.
|
||||
- Query request and response byte-buffer layouts are still proprietary payloads
|
||||
inside WCF operations such as `StartQuery` and `GetNextQueryResultBuffer`.
|
||||
- Write payload layouts remain out of scope until read/query payloads are
|
||||
decoded and fixture-backed.
|
||||
Reference in New Issue
Block a user