Files
mxaccess/design/30-crate-topology.md
T
Joseph Doherty fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
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>
2026-05-05 06:21:00 -04:00

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.