Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled

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>
This commit is contained in:
Joseph Doherty
2026-05-05 06:21:00 -04:00
parent 43733699b0
commit fe2a6db786
3849 changed files with 352975 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net481</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Reference Include="ASBIDataV2Adapter">
<HintPath>C:\Windows\Microsoft.NET\assembly\GAC_MSIL\ASBIDataV2Adapter\v4.0_1.0.0.0__23106a86e706d0ae\ASBIDataV2Adapter.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesCommon">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesCommon.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesCommonDataContracts">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesCommonDataContracts.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesContractIAuthenticateASB">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesContractIAuthenticateASB.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesContractIData">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesContractIData.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesContractIDataV2">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesContractIDataV2.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesProxyIData">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesProxyIData.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesProxyIDataV2">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesProxyIDataV2.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceModel.Discovery" />
</ItemGroup>
</Project>
+305
View File
@@ -0,0 +1,305 @@
using System;
using System.Text;
using System.Linq;
using ArchestrAServices.Contract;
using ArchestrAServices.Proxy;
using ArchestrAServices.ASBIDataV2Contract;
using V2ItemIdentity = ArchestrAServices.ASBIDataV2Contract.ItemIdentity;
using V2ItemRegistration = ArchestrAServices.ASBIDataV2Contract.ItemRegistration;
using V2ItemStatus = ArchestrAServices.ASBIDataV2Contract.ItemStatus;
using V2RuntimeValue = ArchestrAServices.ASBIDataV2Contract.RuntimeValue;
using V2Variant = ArchestrAServices.ASBIDataContract.V2.Variant;
using V2VariantFactory = ArchestrAServices.ASBIDataContract.V2.VariantFactory;
using V2WriteValue = ArchestrAServices.ASBIDataV2Contract.WriteValue;
namespace AsbProxyProbe;
internal static class Program
{
private static int Main(string[] args)
{
string[] accessNames = GetStrings(args, "--access");
if (accessNames.Length == 0)
{
accessNames =
[
"ZB",
"ZB2",
"Default_ZB_MxDataProvider",
"Default_ZB2_MxDataProvider",
"Galaxy",
"localhost",
];
}
bool connect = HasFlag(args, "--connect");
bool register = HasFlag(args, "--register");
bool unregister = HasFlag(args, "--unregister");
bool read = HasFlag(args, "--read");
int? writeInt = GetInt(args, "--write-int");
string tag = GetString(args, "--tag") ?? "TestChildObject.TestInt";
Console.WriteLine($"process=x64:{Environment.Is64BitProcess}");
Console.WriteLine($"tag={tag}");
if (HasFlag(args, "--dump-register-payload"))
{
DumpRegisterPayload(tag);
return 0;
}
if (writeInt.HasValue)
{
Console.WriteLine($"write_int={writeInt.Value}");
}
foreach (string accessName in accessNames)
{
ProbeAccessName(accessName, connect, register, unregister, read, tag, writeInt);
}
return 0;
}
private static void ProbeAccessName(string accessName, bool connect, bool register, bool unregister, bool read, string tag, int? writeInt)
{
Console.WriteLine($"access={accessName}");
try
{
var response = ASBDataV2Proxy.FindIDataEndpoint(accessName, DiscoveryScope.Global);
int count = response?.Endpoints?.Count ?? 0;
Console.WriteLine($"idata_v2_endpoints={count}");
if (response?.Endpoints is not null)
{
for (int i = 0; i < response.Endpoints.Count; i++)
{
var endpoint = response.Endpoints[i];
Console.WriteLine($"idata_v2_endpoint[{i}].address={endpoint.Address?.Uri}");
Console.WriteLine($"idata_v2_endpoint[{i}].listen={string.Join(",", endpoint.ListenUris.Select(uri => uri.ToString()))}");
Console.WriteLine($"idata_v2_endpoint[{i}].contracts={string.Join(",", endpoint.ContractTypeNames.Select(name => name.ToString()))}");
Console.WriteLine($"idata_v2_endpoint[{i}].scopes={string.Join(",", endpoint.Scopes.Select(uri => uri.ToString()))}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"idata_v2_discovery_error={ex.GetType().FullName}: {ex.Message}");
}
try
{
object? proxy = IDataProxySelector.SelectProxyForLatestEndpoint(accessName, new AsbMxDataSettings(), out string selectorError);
Console.WriteLine($"selector_proxy={proxy?.GetType().FullName ?? "<null>"}");
Console.WriteLine($"selector_error={selectorError.Replace(Environment.NewLine, " ")}");
if (connect && proxy is ASBDataV2Proxy v2)
{
bool connected = v2.Connect(out string connectError);
Console.WriteLine($"connect_v2={connected}");
Console.WriteLine($"connect_v2_error={connectError.Replace(Environment.NewLine, " ")}");
Console.WriteLine($"connect_v2_state={v2.ChannelState}");
if (connected)
{
var result = v2.PublishWriteComplete(out var completeWrites);
Console.WriteLine($"publish_write_complete_error=0x{result.ErrorCode:X8}");
Console.WriteLine($"publish_write_complete_count={completeWrites?.Length ?? 0}");
V2ItemIdentity? registeredItem = null;
if (register)
{
registeredItem = ProbeRegister(v2, tag);
}
if (read)
{
ProbeRead(v2, tag);
}
if (writeInt.HasValue)
{
ProbeWriteInt(v2, tag, writeInt.Value);
ProbeRead(v2, tag);
result = v2.PublishWriteComplete(out completeWrites);
Console.WriteLine($"publish_write_complete_after_write_error=0x{result.ErrorCode:X8}");
Console.WriteLine($"publish_write_complete_after_write_count={completeWrites?.Length ?? 0}");
if (completeWrites is not null)
{
for (int i = 0; i < completeWrites.Length; i++)
{
Console.WriteLine($"publish_write_complete_after_write[{i}]=handle:{completeWrites[i].WriteHandle} handle_specified:{completeWrites[i].WriteHandleSpecified} status_items:{completeWrites[i].Status?.Length ?? 0}");
PrintStatuses($"publish_write_complete_after_write[{i}].status", completeWrites[i].Status);
}
}
}
if (unregister)
{
ProbeUnregister(v2, tag, registeredItem);
}
}
}
else if (connect && proxy is ASBDataProxy v1)
{
bool connected = v1.Connect(out string connectError);
Console.WriteLine($"connect_v1={connected}");
Console.WriteLine($"connect_v1_error={connectError.Replace(Environment.NewLine, " ")}");
}
}
catch (Exception ex)
{
Console.WriteLine($"selector_error_exception={ex.GetType().FullName}: {ex.Message}");
}
}
private static V2ItemIdentity? ProbeRegister(ASBDataV2Proxy proxy, string tag)
{
V2ItemIdentity[] items = [CreateAbsoluteItem(tag)];
var result = proxy.RegisterItems(out V2ItemStatus[] statuses, out V2ItemRegistration[] capabilities, RequireId: true, RegisterOnly: true, items);
Console.WriteLine($"register_error=0x{result.ErrorCode:X8} status=0x{result.Status:X8} specific=0x{result.SpecificErrorCode:X8}");
PrintStatuses("register_status", statuses);
if (capabilities is not null)
{
for (int i = 0; i < capabilities.Length; i++)
{
Console.WriteLine($"register_capability[{i}]=id:{capabilities[i].Id} id_specified:{capabilities[i].IdSpecified} write_capability:{capabilities[i].WriteCapability} write_specified:{capabilities[i].WriteCapabilitySpecified}");
}
}
return statuses is { Length: > 0 } ? statuses[0].Item : null;
}
private static void ProbeUnregister(ASBDataV2Proxy proxy, string tag, V2ItemIdentity? registeredItem)
{
V2ItemIdentity[] items = [registeredItem ?? CreateAbsoluteItem(tag)];
var result = proxy.UnregisterItems(out V2ItemStatus[] statuses, items);
Console.WriteLine($"unregister_error=0x{result.ErrorCode:X8} status=0x{result.Status:X8} specific=0x{result.SpecificErrorCode:X8}");
PrintStatuses("unregister_status", statuses);
}
private static void ProbeRead(ASBDataV2Proxy proxy, string tag)
{
V2ItemIdentity[] items = [CreateAbsoluteItem(tag)];
var result = proxy.Read(out V2ItemStatus[] statuses, out V2RuntimeValue[] values, items);
Console.WriteLine($"read_error=0x{result.ErrorCode:X8} status=0x{result.Status:X8} specific=0x{result.SpecificErrorCode:X8}");
PrintStatuses("read_status", statuses);
if (values is not null)
{
for (int i = 0; i < values.Length; i++)
{
V2RuntimeValue value = values[i];
PrintVariant($"read_value[{i}]", value.Value);
Console.WriteLine($"read_value[{i}].timestamp={value.Timestamp:o} timestamp_specified={value.TimestampSpecified}");
Console.WriteLine($"read_value[{i}].status_count={value.Status.Count} status_payload_len={value.Status.Payload?.Length ?? 0}");
}
}
}
private static void ProbeWriteInt(ASBDataV2Proxy proxy, string tag, int value)
{
V2ItemIdentity[] items = [CreateAbsoluteItem(tag)];
V2WriteValue[] values =
[
new V2WriteValue
{
Value = V2VariantFactory.MakeVariantValue(value),
Comment = "AsbProxyProbe write-int",
},
];
const uint writeHandle = 0xA5B20001;
var result = proxy.Write(out V2ItemStatus[] statuses, items, values, writeHandle);
Console.WriteLine($"write_error=0x{result.ErrorCode:X8} status=0x{result.Status:X8} specific=0x{result.SpecificErrorCode:X8} handle=0x{writeHandle:X8}");
PrintStatuses("write_status", statuses);
}
private static V2ItemIdentity CreateAbsoluteItem(string tag)
{
return new V2ItemIdentity
{
Type = (ushort)ArchestrAServices.ASBIDataV2Contract.ItemIdentityType.Name,
ReferenceType = (ushort)ArchestrAServices.ASBIDataV2Contract.ItemReferenceType.Absolute,
Name = tag,
ContextName = string.Empty,
};
}
private static void DumpRegisterPayload(string tag)
{
V2ItemIdentity[] items = [CreateAbsoluteItem(tag)];
using var stream = new System.IO.MemoryStream();
var writer = new System.IO.BinaryWriter(stream);
items[0].WriteArrayToStream(items, ref writer);
byte[] payload = stream.ToArray();
Console.WriteLine($"register_payload_len={payload.Length}");
Console.WriteLine($"register_payload_b64={Convert.ToBase64String(payload)}");
}
private static void PrintStatuses(string prefix, V2ItemStatus[]? statuses)
{
if (statuses is null)
{
return;
}
for (int i = 0; i < statuses.Length; i++)
{
V2ItemStatus status = statuses[i];
Console.WriteLine($"{prefix}[{i}]=item:{status.Item.Name} id:{status.Item.Id} id_specified:{status.Item.IdSpecified} error:0x{status.ErrorCode:X8} error_specified:{status.ErrorCodeSpecified} status_count:{status.Status.Count} status_payload_len:{status.Status.Payload?.Length ?? 0}");
}
}
private static void PrintVariant(string prefix, V2Variant variant)
{
string preview = string.Empty;
if (variant.Payload is { Length: > 0 })
{
preview = variant.Type switch
{
0 when variant.Payload.Length >= 1 => variant.Payload[0].ToString(System.Globalization.CultureInfo.InvariantCulture),
1 when variant.Payload.Length >= 1 => ((char)variant.Payload[0]).ToString(),
2 when variant.Payload.Length >= 2 => BitConverter.ToInt16(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
3 when variant.Payload.Length >= 2 => BitConverter.ToUInt16(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
4 when variant.Payload.Length >= 4 => BitConverter.ToInt32(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
5 when variant.Payload.Length >= 4 => BitConverter.ToUInt32(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
6 when variant.Payload.Length >= 8 => BitConverter.ToInt64(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
7 when variant.Payload.Length >= 8 => BitConverter.ToUInt64(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
8 when variant.Payload.Length >= 4 => BitConverter.ToSingle(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
9 when variant.Payload.Length >= 8 => BitConverter.ToDouble(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
10 => Encoding.Unicode.GetString(variant.Payload),
11 when variant.Payload.Length >= 8 => DateTime.FromFileTimeUtc(BitConverter.ToInt64(variant.Payload, 0)).ToString("o"),
17 when variant.Payload.Length >= 1 => BitConverter.ToBoolean(variant.Payload, 0).ToString(),
18 when variant.Payload.Length >= 1 => ((sbyte)variant.Payload[0]).ToString(System.Globalization.CultureInfo.InvariantCulture),
_ => BitConverter.ToString(variant.Payload).Replace("-", string.Empty),
};
}
Console.WriteLine($"{prefix}=type:{variant.Type} length:{variant.Length} payload_len:{variant.Payload?.Length ?? 0} preview:{preview}");
}
private static string[] GetStrings(string[] args, string name)
{
string prefix = name + "=";
return args
.Where(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.Select(arg => arg.Substring(prefix.Length))
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToArray();
}
private static string? GetString(string[] args, string name)
{
string prefix = name + "=";
return args.FirstOrDefault(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))?.Substring(prefix.Length);
}
private static int? GetInt(string[] args, string name)
{
string? value = GetString(args, name);
return int.TryParse(value, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int result)
? result
: null;
}
private static bool HasFlag(string[] args, string name)
{
return args.Any(arg => arg.Equals(name, StringComparison.OrdinalIgnoreCase));
}
}