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

19 KiB

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 interchangeablywwtools/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:

# 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

[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.tomls 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.