Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
MXAccess reverse-engineering analysis
Executive summary
The primary ArchestrA.MXAccess.dll is not a protocol implementation. It is a
.NET primary interop assembly generated from LMXPROXYLib. All useful methods
on LMXProxyServerClass are extern COM calls. The actual implementation is
native, 32-bit, and registered as an in-process COM server:
net48 x86 caller
-> ArchestrA.MXAccess.dll
-> COM CLSID {C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}
-> C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
-> C:\Program Files (x86)\Common Files\ArchestrA\Framework\Bin\Lmx.dll
-> C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll
-> C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe
-> runtime engines over NMX
That is why the current OPC UA interface is constrained to .NET Framework and x86. The binding itself is not forcing .NET 4.8; the 32-bit COM/native stack is. The cleanest modern replacement is not "port the interop assembly"; it is either:
- keep a 32-bit sidecar process and expose a modern IPC API to .NET 10/Rust, or
- implement the lower LMX/NMX client protocol directly from the native type libraries plus runtime traces.
Option 2 is possible but not complete from static decompilation alone because
the proprietary message bodies are assembled inside native code, not in
ArchestrA.MXAccess.dll.
Evidence
Managed DLL
ArchestrA.MXAccess.dll:
- File path:
C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll - Assembly:
ArchestrA.MxAccess, Version=3.2.0.0, PublicKeyToken=23106a86e706d0ae - File description/product:
Assembly imported from type library 'LMXPROXYLib'. - Decompilation contains only COM-imported interfaces, structs, enums, delegates, and event sink helpers.
LMXProxyServerClassmethods are allruntime managed internalcall, meaning the CLR dispatches them through COM; there is no managed implementation body to port.
COM registration
LMXProxy.LMXProxyServer is registered only in the 32-bit COM registry view:
HKCR\Wow6432Node\CLSID\{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}
(default) = LMXProxyServer Class
InprocServer32 = C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
ThreadingModel = Apartment
ProgID = LMXProxy.LMXProxyServer.1
VersionIndependentProgID = LMXProxy.LMXProxyServer
TypeLib = {C36ECF28-3EF3-4528-9843-81D7FDC86328}
Type libraries:
{77E896EC-B3E3-4DE4-B96F-F32570164211}\3.2
LMXProxy 3.2 Type Library
C:\Program Files (x86)\ArchestrA\Framework\Bin\MXAccess32.tlb
{C36ECF28-3EF3-4528-9843-81D7FDC86328}\1.0
LMXProxy 1.0 Type Library
C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
{C36ECF28-3EF3-4528-9843-81D7FDC86328}\2.0
LMXProxy 2.0 Type Library
C:\Program Files (x86)\ArchestrA\Framework\Bin\MXAccess20.tlb
Native binaries
| Binary | Role inferred from registration/imports/strings | Architecture |
|---|---|---|
LmxProxy.dll |
Public MXAccess COM wrapper. Implements LMXProxy.LMXProxyServer. Imports licensing wrapper and COM/OLE Automation APIs. |
PE32 x86 |
Lmx.dll |
Main local message exchange implementation. Exposes many COM objects including IDataClient, IDataConsumer, Nmx, bootstrap, runtime object, and data variant surfaces. |
PE32 x86 |
NmxAdptr.dll |
In-process adapter over NMX service. Imports Winsock and exposes WonderWare.Nmx.CNmxAdapter. |
PE32 x86 |
NmxSvc.exe |
Out-of-process NMX service. Registered as NmxSvc.NmxService; listens on TCP/UDP port 5026 on this machine. |
PE32 x86 |
NmxSvcps.dll |
MIDL proxy/stub for NMX service COM interfaces. | PE32 x86 |
LmxProxy.dll exports only standard COM DLL exports:
DllCanUnloadNow
DllGetClassObject
DllRegisterServer
DllUnregisterServer
This means there is no flat C ABI to P/Invoke from modern .NET or Rust.
LmxProxy.dll imports LicAPINativeWrapper.dll, including:
CreateClientConnection
AddLicenseRequestInfo
AcquireLicense
GetLicenseAcquisitionError
ReleaseLicense
GetDeviceIdentity
Any native replacement has to account for AVEVA licensing behavior, either by using a supported client surface or by keeping a licensed sidecar.
Runtime model inferred from MXAccess
The current high-level API is handle based:
Register(clientName)returns anhLMXServerHandle.AddItem(handle, fullReference)resolves a Galaxy/runtime attribute and returns an item handle.AdviseorAdviseSupervisorysubscribes to updates.- Updates arrive through COM connection-point events:
OnDataChange,OnWriteComplete, andOperationComplete. Write/Write2submit writes and completion is reported asynchronously.UnAdvise,RemoveItem, andUnregisterclean up.
The existing OPC UA host uses the canonical pattern correctly:
- All COM work runs on one STA thread.
- Reads are implemented as transient subscribe -> first
OnDataChange-> unsubscribe because MXAccess exposes no direct synchronous read. - Writes are submitted with
Writeand then matched withOnWriteComplete. - Subscriptions use
AdviseSupervisory.
Lower LMX data-client surface
Lmx.dll exposes a richer COM interface named IDataClient. It is still x86 COM,
but it is closer to the underlying data model than LMXProxyServer:
void Initialize(string namespaceName);
IntPtr Connect(string endPointUri, ulong timeout, ref _IUserToken userToken, out uint clientId);
IArchestrAResult[] Connect2(...);
long CreateSubscription2(...);
IArchestrAResult[] RegisterItems2(IItemIdentity2[] items, ...);
IArchestrAResult[] AddMonitoredItems2(long subscriptionId, IMonitoredItem2[] items, ...);
IArchestrAResult[] Publish2(long subscriptionId, out DataChangeUpdate[] updates, out uint resultCount);
IArchestrAResult[] Read2(IItemIdentity2[] items, out DataChangeUpdate[] updates, out uint updatesCount);
IArchestrAResult[] Write2(WriteRequest2[] writes, ...);
Important structs:
struct IItemIdentity2 {
ushort ReferenceType;
ushort type;
string ContextName;
string Name;
ulong id;
}
struct IMonitoredItem2 {
byte Active;
ushort ReferenceType;
ushort type;
string ContextName;
string Name;
ulong id;
ulong SampleInterval;
ulong TimeDeadband;
}
struct DataChangeUpdate {
ulong ItemId;
int StatusCode;
uint HighDateTime;
uint LowDateTime;
int quality;
IDataVariant value;
}
struct IDataVariant {
ushort type;
int length;
byte[] Payload;
}
This surface suggests AVEVA has a newer OPC UA-like internal data client in the
LMX layer. It may be a better reverse-engineering target than LMXProxyServer,
but it is not registered as a 64-bit component on this machine. It still goes
through the x86 native stack.
NMX service surface
NmxSvc.exe is registered as an out-of-process COM local server:
CLSID {AE24BD51-2E80-44CC-905B-E5446C942BEB}
LocalServer32 = "C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe"
ProgID = NmxSvc.NmxService.1
Key INmxService2 methods:
void RegisterEngine2(int localEngineId, string engineName, int version, INmxSvcCallback callback);
void UnRegisterEngine(int localEngineId);
void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte msgBody);
void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
void RemoveSubscriberEngine(...);
void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
void GetPartnerVersion(int galaxyId, int platformId, int engineId, out int version);
NmxAdptr.dll exposes WonderWare.Nmx.CNmxAdapter as an in-process COM adapter:
CLSID {42DB0511-28BE-11D3-80C0-00104B5F96A7}
InprocServer32 = C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll
ProgID = WonderWare.Nmx.CNmxAdapter.1
ThreadingModel = Apartment
Key INmx4 methods:
void Initialize2(int platformId, int engineId, int version, int queueExtent);
void InitializeAnonymous2(out int platformId, out int engineId, int version);
void PutRequest2(int galaxyId, int platformId, int engineId, byte priority, byte type, int size, ref byte data, out int requestHandle);
void GetResponse2(byte type, out int responseCode, out int requestHandle, out int size, IntPtr data);
void PutMessageNoReply(...);
void GetPartnerVersion(...);
Static strings identify NMX message categories:
NMXMTYPE_CONNECT_INFO
NMXMTYPE_CONNECT_INFO_REQ
NMXMTYPE_ENGINE_DATA
NMXMTYPE_GET_HEARTBEAT_RATE
NMXMTYPE_PLATFORM_HEARTBEAT
NMXMTYPE_SET_HEARTBEAT_RATE
NMXMTYPE_VERSION_ERROR
NMXMTYPE_HEARTBEAT_DISCONNECT_REQ
On this machine, the running NmxSvc.exe process is listening on:
TCP 10.100.0.48:5026
UDP 10.100.0.48:5026
That gives a network tracing target for the direct-native path.
Why direct x64 COM is blocked
There are two different issues:
ArchestrA.MXAccess.dllis a .NET Framework-era interop assembly. Modern .NET can consume many COM interfaces, but the generated event provider and TLB-imported types are built for the old COM interop shape.- The actual COM class is an x86 in-process DLL. A 64-bit process cannot load
LmxProxy.dll, regardless of whether the caller is .NET Framework, .NET 10, or Rust.
An out-of-process COM local server can cross bitness, but LMXProxyServer is
not registered as a local server. It is an in-proc DLL.
Implementation options
Option A: Keep and formalize the 32-bit sidecar
This is the shortest reliable path.
Build or keep a small x86 process that owns:
- COM apartment initialization.
LMXProxy.LMXProxyServerlifetime.- register/add/advise/write/unadvise cleanup.
- reconnect and subscription replay.
Expose a stable IPC API to the modern service:
- named pipes for same-machine .NET/Rust services,
- gRPC over named pipes or loopback TCP if cross-language tooling matters,
- protobuf messages for
Read,Write,Subscribe,Unsubscribe, and status events.
This is effectively what the existing OtOpcUaGalaxyHost / proxy work has moved
toward. It removes the OPC UA server's net48/x86 constraint without attempting
to clone AVEVA's internal protocol.
Option B: Use lower LMX COM interfaces from an x86 sidecar
Instead of the high-level LMXProxyServer, use Lmx.dll's IDataClient /
IDataConsumer APIs inside the sidecar. Potential benefits:
- direct
Read2API rather than transient subscribe for reads, - batch register/read/write/monitor operations,
- explicit
IDataVariantpayloads, - closer alignment with OPC UA-like status codes.
Risks:
- less documented than MXAccess,
- still x86 COM,
- endpoint URI, namespace, token setup, and licensing behavior need live probing.
Option C: Direct NMX protocol client in Rust or .NET
This is the true native replacement path.
Required work:
- Discover galaxy/platform/engine ids for target attributes.
- Reconstruct NMX message envelope layout used by
PutRequest2andNmxSvc.TransferData. - Reconstruct LMX item registration, subscription, write, and completion message bodies.
- Reproduce heartbeat/version negotiation.
- Reproduce security/licensing behavior or find a supported token flow.
- Validate behavior across platform stopped/off-scan, engine restart, deploy, security-denied write, and repository-busy cases.
Static type-library analysis gives method names and high-level message routes, but not the binary message schemas. Those schemas are assembled in native code. The next step for this option is live tracing.
Proposed tracing plan
-
Build a minimal x86 MXAccess harness that:
- registers as a known client name,
- adds one known good tag and one bad tag,
- subscribes with
AdviseSupervisory, - writes a toggled value,
- unregisters cleanly.
-
Capture process activity:
- Process Monitor filters for
LmxProxy.dll,Lmx.dll,NmxAdptr.dll,NmxSvc.exe. - ETW or API Monitor for COM method calls if available.
- Wireshark or
netsh traceon TCP/UDP5026.
- Process Monitor filters for
-
Correlate high-level actions to low-level packets:
RegisterAddItemAdviseSupervisory- first data change
WriteOnWriteCompleteUnAdvise/RemoveItem
-
Repeat against:
- local object attribute,
- remote platform attribute,
- stopped engine,
- security-denied write,
- secured/verified write.
-
Use
NmxAdptr.INmx4as a stepping stone:- call
InitializeAnonymous2, - issue controlled
PutRequest2from an x86 harness, - compare buffers and responses with MXAccess traffic.
- call
Current recommendation
For production, do not block the OPC UA server on a full native NMX clone. The low-risk path is:
- keep the x86 MXAccess host as the vendor-bound adapter,
- make the main OPC UA interface .NET 10 or Rust,
- communicate through a narrow, well-tested IPC contract,
- continue reverse-engineering
IDataClientand NMX in parallel as an R&D path.
The direct native path may still be worth pursuing, but it should be treated as a protocol-reconstruction project with packet captures and failure-mode testing, not as a decompile-only port.
Artifacts produced
Generated files in this project folder:
analysis/decompiled-mxaccess/
analysis/interop/
analysis/decompiled-interop/
Installed tools used:
dotnet tool install --global ilspycmd
python pefile
Windows SDK TlbImp.exe