fe2a6db786
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>
296 lines
19 KiB
Markdown
296 lines
19 KiB
Markdown
# Crate topology
|
|
|
|
## Workspace layout
|
|
|
|
```
|
|
rust/
|
|
Cargo.toml workspace root
|
|
Cargo.lock
|
|
rust-toolchain.toml 1.85, stable (matches workspace.package.rust-version)
|
|
crates/
|
|
mxaccess-codec/ pure protocol codec, no I/O
|
|
mxaccess-galaxy/ Galaxy SQL resolver (tiberius)
|
|
mxaccess-rpc/ DCE/RPC + NTLMv2 + OXID + OBJREF
|
|
mxaccess-callback/ INmxSvcCallback RPC server
|
|
mxaccess-nmx/ INmxService2 client
|
|
mxaccess-asb-nettcp/ net.tcp framing: MC-NMF + MC-NBFX/NBFS binary message encoder
|
|
(NetTcpBinding default — see src/MxAsbClient/MxAsbDataClient.cs:660-685)
|
|
mxaccess-asb/ IASBIDataV2 client
|
|
mxaccess/ async session + Transport trait + public API
|
|
examples/ `cargo run --example` only resolves examples
|
|
connect-write-read.rs owned by a specific crate, so the public-facing
|
|
subscribe.rs examples live under the top-level `mxaccess`
|
|
subscribe-buffered.rs crate and are invoked with `-p mxaccess`.
|
|
asb-subscribe.rs
|
|
recovery.rs
|
|
multi-tag.rs
|
|
secured-write.rs
|
|
mxaccess-compat/ LMXProxyServer-shaped facade (optional)
|
|
tests/
|
|
fixtures/ copy of ../captures/0NN-frida-* (junctions are Windows-only and don't survive `git clone` cross-platform; symlinks need Developer Mode on Windows — copy is the portable default)
|
|
```
|
|
|
|
The workspace lives at `c:\Users\dohertj2\Desktop\mxaccess\rust\` (sibling of `src/`) per the `CLAUDE.md` directive. The .NET tooling does not look there; cargo treats it as the workspace root.
|
|
|
|
## Dependency graph
|
|
|
|
```
|
|
+----------------+
|
|
| mxaccess-codec |
|
|
+----------------+
|
|
^ ^
|
|
| |
|
|
+-----------+ +-----------+
|
|
| |
|
|
+---------------+ +-------------------+
|
|
| mxaccess-rpc | | mxaccess-asb-nettcp |
|
|
+---------------+ +-------------------+
|
|
^ ^
|
|
| |
|
|
+-------+---------+ +-------+--------+
|
|
| mxaccess-callback| | mxaccess-asb |
|
|
+------------------+ +----------------+
|
|
^ ^
|
|
| |
|
|
+-------+----------+ |
|
|
| mxaccess-nmx | |
|
|
+------------------+ |
|
|
^ +-------------------+ |
|
|
| | mxaccess-galaxy | |
|
|
| +-------------------+ |
|
|
| ^ |
|
|
+--------------+-------------------+
|
|
|
|
|
+---------------+
|
|
| mxaccess | (top-level async API)
|
|
+---------------+
|
|
^
|
|
|
|
|
+-----------------+
|
|
| mxaccess-compat | (LMXProxyServer shape)
|
|
+-----------------+
|
|
```
|
|
|
|
No cycles. ASB and NMX paths never depend on each other. The `mxaccess-nmx → mxaccess-galaxy` arrow is feature-gated behind `galaxy-resolver` (default-on); consumers building NMX with a custom `Resolver` can drop it.
|
|
|
|
## Per-crate detail
|
|
|
|
### `mxaccess-codec`
|
|
|
|
| | |
|
|
|---|---|
|
|
| Role | Pure encoder/decoder for NMX wire types and ASB variant |
|
|
| Targets | All Rust targets *theoretically* (codec is pure, no platform-bound deps); Linux/macOS support is a **stretch goal**, gated on `MX_LIVE` integration tests against a remote AVEVA install. See `60-roadmap.md` and `70-risks-and-open-questions.md` Q3. |
|
|
| Public deps | `bytes`, `byteorder`, `uuid`, `widestring`, `thiserror` |
|
|
| Private deps | `proptest` (dev) |
|
|
| Optional features | `serde` (derives `Serialize`/`Deserialize` on public types) |
|
|
| Tests | Round-trip every captured fixture; proptest generators for primitives; cross-implementation parity vs `dotnet run --project src\MxNativeCodec.Tests` |
|
|
|
|
### `mxaccess-galaxy`
|
|
|
|
| | |
|
|
|---|---|
|
|
| Role | Galaxy Repository SQL resolver: tag → metadata, user → identity |
|
|
| Targets | All Rust targets, but Linux integrated-security is a stretch goal — see auth note below |
|
|
| Deps | `mxaccess-codec`, `tokio`, `tokio-util`, `futures-util`, `thiserror`, `tracing` (`tiberius` is now an optional dep, see below) |
|
|
| Optional features | `galaxy-resolver` (default-off; pulls `tiberius` and exposes the SQL-backed resolver. Consumers that only need NMX/ASB with a custom `Resolver` impl can leave this off and avoid pulling TDS, native-tls/rustls, and the `winauth` stack). `auth-windows` (default-on for Windows when `galaxy-resolver` is on; selects `tiberius`'s `winauth` SSPI feature for integrated security against domain-joined SQL Server). On Linux, `auth-windows` does **not** apply: integrated security against an MSSQL Galaxy DB requires `tiberius`'s `integrated-auth-gssapi` feature plus a configured Kerberos KDC and `krb5.conf` on the client. Galaxy databases in practice are domain-joined Windows boxes using NTLM/Kerberos integrated auth, so Linux clients without an MIT/Heimdal stack will fail to authenticate; flagged as a **stretch goal** and tracked in `70-risks-and-open-questions.md`. SQL-login fallback is always available cross-platform. |
|
|
| Tests | Mock SQL fixtures; live integration test gated on `MX_GALAXY_DB` env var |
|
|
|
|
The .NET reference keeps `GalaxyRepositoryTagResolver.cs` inside the `MxNativeClient` namespace (`src/MxNativeClient/GalaxyRepositoryTagResolver.cs:4`). Splitting it into `mxaccess-galaxy` is a Rust-side improvement, not a porting fault: the resolver's only inputs are SQL connection options, its only output is `MxReferenceHandle` (a `mxaccess-codec` type), and the Rust trait `Resolver` is exposed by `mxaccess-nmx` so consumers can plug in their own implementation. With `galaxy-resolver` feature-gated, `mxaccess-nmx` does not transitively pull `tiberius` for consumers who do not need it.
|
|
|
|
**Resolver input contract — `tag_name`-form only.** The Galaxy DB carries two distinct name fields per object: `tag_name` (the runtime read/write name, e.g. `DelmiaReceiver_001`) and `contained_name` (the hierarchy-browsing path, e.g. `TestMachine_001.DelmiaReceiver`). These are **asymmetric and cannot be used interchangeably** — `wwtools/grdb/README.md` calls this out as a critical distinction. `GalaxyRepositoryTagResolver.ResolveSql` keys on `g.tag_name = @objectTagName`; passing a contained-name will silently miss. The Rust `Resolver` trait takes a `tag_name`-form `&str` (e.g. `"TestObject.TestInt"` resolves the `TestObject` tag plus the `TestInt` attribute on it). If a future consumer needs contained-name → tag-name translation, add it as a separate translator that calls `wwtools/grdb/queries/hierarchy.sql`-style logic; **do not** mix the two paths inside `mxaccess-galaxy`.
|
|
|
|
**Galaxy schema version probe.** R10 in `70-risks-and-open-questions.md` flags older Galaxy schema layouts as untested. `wwtools/grdb/` confirms a `dbo.schema_version` table exists with `version_number` / `version_string` / `cdi_version` columns. The Rust resolver should query this at session construction and fail loud (`ConfigError::Galaxy { reason: format!("schema version {version_string} is outside tested range") }`) if the version is outside the proven set.
|
|
|
|
### `mxaccess-rpc`
|
|
|
|
| | |
|
|
|---|---|
|
|
| Role | DCE/RPC PDU codec + NTLMv2 + OBJREF + OXID resolution + RemQI |
|
|
| Targets | `x86_64-pc-windows-msvc` (primary), `x86_64-pc-windows-gnu`, `x86_64-unknown-linux-gnu` (NTLM-only paths) |
|
|
| Deps | `mxaccess-codec`, `bytes`, `byteorder`, `tokio`, `hmac`, `md-5`, `rc4`, `rand`, `uuid`, `thiserror`, `tracing` (all crypto crates pinned to the `digest 0.11`/`cipher 0.5` generation per the workspace dependency table) |
|
|
| Optional features | `windows-com` (default-on Windows; pulls `windows` for `GUID`/`ObjRef` helpers) |
|
|
| Tests | Unit tests for NTLM message construction + PDU framing; integration tests against captured ObjectExporter responses |
|
|
|
|
### `mxaccess-callback`
|
|
|
|
| | |
|
|
|---|---|
|
|
| Role | TCP listener + RPC server for `INmxSvcCallback` and `IRemUnknown` |
|
|
| Deps | `mxaccess-rpc`, `mxaccess-codec`, `tokio`, `futures-util`, `tracing`, `thiserror` |
|
|
| Tests | Unit test that exercises the dispatch table with synthetic Bind/Request PDUs |
|
|
|
|
### `mxaccess-nmx`
|
|
|
|
| | |
|
|
|---|---|
|
|
| Role | `INmxService2` client + raw NMX session façade. Exposes a `Resolver` trait so consumers can plug in any tag-handle resolver. |
|
|
| Deps | `mxaccess-codec`, `mxaccess-rpc`, `mxaccess-callback`, `tokio`, `tracing`, `thiserror` |
|
|
| Optional features | `galaxy-resolver` (default-on; pulls `mxaccess-galaxy` and re-exports its SQL-backed `Resolver` impl. Off → `mxaccess-nmx` builds without `tiberius`/TDS, and the consumer supplies their own `Resolver`.) |
|
|
| Tests | Round-trip TransferData fixtures; live probe gated on `MX_LIVE` |
|
|
|
|
### `mxaccess-asb-nettcp`
|
|
|
|
| | |
|
|
|---|---|
|
|
| Role | net.tcp framing layer. Implements MC-NMF (.NET Message Framing) + MC-NBFX/NBFS (.NET Binary XML / dictionary string table) — the default binary message encoder for `NetTcpBinding`. Reference WCF construction in `src/MxAsbClient/MxAsbDataClient.cs:660-685` is `new NetTcpBinding(SecurityMode.None)` with no encoder override, which selects `BinaryMessageEncodingBindingElement` by default — i.e. *not* SOAP/XML on the wire. The previous name `mxaccess-asb-soap` was a misnomer. |
|
|
| Visibility | Workspace-internal crate, published alongside the rest of the workspace (no `publish = false` — Cargo refuses `cargo publish` of `mxaccess-asb` if a path-dep here lacks a published version). |
|
|
| Deps | `bytes`, `tokio`, `[an MC-NBFX/NBFS impl — TODO: evaluate `wcf-binary` crate or hand-roll a dictionary-table codec]`, `quick-xml` (only for the small ASB control-plane XML payloads such as `request.ToXml()` at `src/MxAsbClient/AsbSystemAuthenticator.cs:79`, *not* for net.tcp framing), `flate2`, `aes`, `hmac`, `md-5`, `sha1` (note crate rename — `sha-1` is deprecated upstream, `sha1` is the maintained successor), `sha2`, `pbkdf2`, `num-bigint`, `rand`, `tracing`. All RustCrypto crates are pinned to the `digest 0.11`/`cipher 0.5` generation; see workspace `[workspace.dependencies]` table. |
|
|
| Tests | Unit tests for net.tcp/MC-NMF framing + DH handshake against captured payloads from `AsbMessageDumpBehavior` |
|
|
|
|
### `mxaccess-asb`
|
|
|
|
| | |
|
|
|---|---|
|
|
| Role | `IASBIDataV2` client |
|
|
| Deps | `mxaccess-codec`, `mxaccess-asb-nettcp`, `tokio`, `tracing`, `thiserror` |
|
|
| Optional features | `dpapi` (default-on for Windows targets) — see `SecretProvider` below |
|
|
| Tests | Round-trip Variant fixtures; live probe gated on `MX_LIVE` |
|
|
|
|
`mxaccess-asb` always exposes a `SecretProvider` trait (single fallible `async fn fetch(&self) -> Result<Zeroizing<Vec<u8>>, Error>`) that the ASB authenticator calls to obtain the shared secret used for the DH-passphrase derivation (`src/MxAsbClient/AsbSystemAuthenticator.cs:28, 134-142` — the secret is mandatory for the handshake; without it ASB cannot authenticate). The trait is **always present** — not feature-gated — so consumers can plug in any source (env var, file, KeyVault, hardcoded test fixture). The `dpapi` feature provides a default Windows-only implementation that reads the secret via `windows::Win32::Security::Cryptography::CryptUnprotectData`. With `dpapi=off`, the crate still compiles and works; the consumer must provide a `SecretProvider` impl explicitly, otherwise `Session::builder()` fails at construction time with `Error::ConfigurationIncomplete { missing: "secret_provider" }`.
|
|
|
|
### `mxaccess`
|
|
|
|
| | |
|
|
|---|---|
|
|
| Role | Public async API: `Session`, `Subscription`, `Transport` trait |
|
|
| Deps | `mxaccess-codec`, `tokio`, `tokio-util`, `futures-util`, `tracing`, `thiserror`, `async-trait`, `arc-swap` (for cheap clones of session state) |
|
|
| Optional features | `nmx` (default-on Windows; pulls `mxaccess-nmx`), `asb` (default-on; pulls `mxaccess-asb`), `metrics` (optional `metrics` instrumentation), `serde` (forwards to codec) |
|
|
| Tests | Integration tests gated on env vars; in-memory `Transport` for unit tests |
|
|
|
|
### `mxaccess-compat`
|
|
|
|
| | |
|
|
|---|---|
|
|
| Role | `LMXProxyServer`-shaped methods on top of `Session` |
|
|
| Deps | `mxaccess` |
|
|
| Tests | Method-equivalence tests against captured `MxNativeCompatibilityServer` outputs |
|
|
|
|
## Build / test commands
|
|
|
|
To be added to `CLAUDE.md` "Common commands" once `rust/` exists:
|
|
|
|
```powershell
|
|
# Workspace-wide
|
|
cargo build --workspace
|
|
cargo test --workspace
|
|
cargo clippy --workspace -- -D warnings
|
|
cargo fmt --check
|
|
|
|
# Single crate
|
|
cargo build -p mxaccess-codec
|
|
cargo test -p mxaccess-codec
|
|
|
|
# Live integration tests (require AVEVA install + Galaxy DB)
|
|
$env:MX_LIVE = "1"
|
|
$env:MX_GALAXY_DB = "Server=localhost;Database=Galaxy;Integrated Security=True;TrustServerCertificate=True"
|
|
$env:MX_NMX_HOST = "localhost"
|
|
cargo test -p mxaccess --features live -- --ignored
|
|
|
|
# Examples (live under `crates/mxaccess/examples/`; `-p mxaccess` is required because
|
|
# `cargo run --example` only resolves examples that belong to a specific crate)
|
|
cargo run -p mxaccess --example connect-write-read
|
|
cargo run -p mxaccess --example subscribe -- --tag TestChildObject.TestInt
|
|
cargo run -p mxaccess --example asb-subscribe -- --tag TestChildObject.TestInt
|
|
```
|
|
|
|
## Toolchain & MSRV
|
|
|
|
- MSRV is **1.85**, set both in `rust-toolchain.toml` and in `[workspace.package].rust-version`. Both must stay in lock-step; CI fails if they drift. 1.85 is the floor required by the pinned RustCrypto generation (`digest 0.11` / `cipher 0.5` family — `aes 0.9`, `hmac 0.13`, `md-5 0.11`, `sha1 0.11`, `pbkdf2 0.13`) and by the latest `uuid 1.x`; lowering it forces an older crypto generation that conflicts on the resolved `digest`/`cipher` traits.
|
|
- Edition **2024** (stable since Rust 1.85, 2025-02). Since MSRV is already 1.85, edition 2024 is a free upgrade.
|
|
- `rustfmt` default config + 100-column lines committed.
|
|
- `clippy` with `-D warnings` in CI.
|
|
- `clippy::unwrap_used`, `clippy::expect_used` set to deny in `mxaccess`, `mxaccess-codec`, `mxaccess-rpc`, `mxaccess-nmx`, `mxaccess-asb`. Allowed in tests via `#[cfg(test)]` overrides. UTF-16LE name decoding in `mxaccess-codec` (`MxReferenceHandle` parsing) must use a fallible helper that maps `String::from_utf16` errors into a typed codec error rather than `.unwrap()`-ing — there is no panicking decode path on the hot wire-parse surface.
|
|
|
|
## Feature gates summary
|
|
|
|
| Feature | Default | Crate | Effect |
|
|
|---|---|---|---|
|
|
| `nmx` | yes (Windows) | `mxaccess` | Enables `NmxTransport`, pulls `mxaccess-nmx` |
|
|
| `asb` | yes | `mxaccess` | Enables `AsbTransport`, pulls `mxaccess-asb` |
|
|
| `metrics` | no | `mxaccess` | Emits `metrics` counters/histograms |
|
|
| `serde` | no | `mxaccess`, `mxaccess-codec` | Derives `Serialize`/`Deserialize` |
|
|
| `dpapi` | yes (Windows) | `mxaccess-asb` | Provides the Windows DPAPI default `SecretProvider` impl. Off → consumer must supply their own `SecretProvider` (the trait is always present). |
|
|
| `galaxy-resolver` | yes | `mxaccess-nmx` | Pulls `mxaccess-galaxy` and exposes the SQL-backed `Resolver`. Off → `mxaccess-nmx` ships without `tiberius`/TDS; consumer supplies a custom `Resolver`. |
|
|
| `auth-windows` | yes (Windows) | `mxaccess-galaxy` | Integrated security (SSPI/`winauth`) for SQL Server. Windows-only; Linux integrated security is a separate stretch goal that requires `tiberius`'s `integrated-auth-gssapi` feature + a configured Kerberos KDC. SQL-login fallback works cross-platform without this feature. |
|
|
| `windows-com` | yes (Windows) | `mxaccess-rpc` | Uses `windows` crate for GUID/IID helpers |
|
|
| `live` (test-only) | no | `mxaccess`, `mxaccess-nmx`, `mxaccess-asb` | Enables tests that hit a live AVEVA install |
|
|
|
|
## Workspace `Cargo.toml` skeleton
|
|
|
|
```toml
|
|
[workspace]
|
|
resolver = "2"
|
|
members = [
|
|
"crates/mxaccess-codec",
|
|
"crates/mxaccess-galaxy",
|
|
"crates/mxaccess-rpc",
|
|
"crates/mxaccess-callback",
|
|
"crates/mxaccess-nmx",
|
|
"crates/mxaccess-asb-nettcp",
|
|
"crates/mxaccess-asb",
|
|
"crates/mxaccess",
|
|
"crates/mxaccess-compat",
|
|
]
|
|
|
|
[workspace.package]
|
|
edition = "2024"
|
|
license = "MIT" # resolved 2026-05-05; LICENSE at repo root
|
|
repository = "https://github.com/<org>/mxaccess"
|
|
rust-version = "1.85"
|
|
|
|
[workspace.dependencies]
|
|
bytes = "1"
|
|
byteorder = "1"
|
|
uuid = { version = "1", features = ["v4", "v7"] }
|
|
widestring = "1"
|
|
thiserror = "1"
|
|
tokio = { version = "1", features = ["net", "io-util", "rt-multi-thread", "sync", "time", "macros"] }
|
|
tokio-util = { version = "0.7", features = ["codec"] }
|
|
futures-util = "0.3"
|
|
tracing = "0.1"
|
|
async-trait = "0.1"
|
|
# RustCrypto generation: digest 0.11 / cipher 0.5 line. All crates here are pinned to that
|
|
# generation so the resolved `digest` / `cipher` graph is coherent. Bumping any one of these
|
|
# to the older 0.10/0.12 line will fail to build — pin the generation, not the individual
|
|
# versions. This generation requires `rust-version = "1.85"` (set below).
|
|
hmac = "0.13"
|
|
md-5 = "0.11"
|
|
sha1 = "0.11" # crate renamed from `sha-1` (deprecated) to `sha1` upstream
|
|
sha2 = "0.11"
|
|
rc4 = "0.2" # latest published; on the cipher 0.5 trait reform.
|
|
rand = "0.8"
|
|
quick-xml = "0.36" # ASB control-plane XML payloads only (e.g. `request.ToXml()` at
|
|
# `src/MxAsbClient/AsbSystemAuthenticator.cs:79`); not used for
|
|
# net.tcp wire framing.
|
|
aes = "0.9"
|
|
flate2 = "1"
|
|
pbkdf2 = "0.13"
|
|
num-bigint = "0.4" # NOTE: review.md [MAJOR] flags this as not constant-time. The DH
|
|
# private exponent is long-lived (`AsbSystemAuthenticator.cs:153-166`),
|
|
# so a side-channel-leaky `mod_exp` is a security regression vs. an
|
|
# opportunity. Tracked as an explicit follow-up in `70-risks-and-open-questions.md`
|
|
# to swap to `crypto-bigint` constant-time `mod_exp` once the wire
|
|
# round-trips against captured DH handshakes.
|
|
tiberius = { version = "0.12", features = ["chrono", "tds73"] }
|
|
# `windows` 0.62 is the current line; 0.58 → 0.62 has breaking renames in
|
|
# `Win32_System_Rpc` and `Win32_Security_Cryptography` so designing against an older
|
|
# pin wastes work.
|
|
windows = { version = "0.62", features = [
|
|
"Win32_Foundation",
|
|
"Win32_System_Com",
|
|
"Win32_Security_Cryptography",
|
|
"Win32_System_Rpc",
|
|
] }
|
|
proptest = "1"
|
|
metrics = "0.23"
|
|
serde = { version = "1", features = ["derive"] }
|
|
arc-swap = "1"
|
|
```
|
|
|
|
Pin minor versions in CI; keep workspace-level dependency table consistent across crates.
|
|
|
|
## License
|
|
|
|
**MIT** (resolved 2026-05-05; see `70-risks-and-open-questions.md` Q2). `LICENSE` file lives at the project root (`c:\Users\dohertj2\Desktop\mxaccess\LICENSE`). All crate `Cargo.toml`s inherit `license = "MIT"` via `workspace.package` so each crate publishes correctly. Workspace deps listed above are MIT/Apache-2.0 compatible; MIT alone satisfies every dep's downstream license obligation. The `windows` crate proxy/stub IDL re-emissions are flagged for legal review only if vendored from Microsoft headers — not applicable to typical `windows-rs`-generated code.
|