Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
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:
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user