4dfc0cee6540bee0d13cf6511b5f8afb8fb47560
18 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d5aa152b1f |
[F35] mxaccess-compat: LMXProxyServer-shaped facade (18 methods)
Replace the 8-line `mxaccess-compat` stub with a real `LmxClient` struct exposing the 18 `ILMXProxyServer5` methods as Rust async fns on top of `mxaccess::Session` (NMX) and `mxaccess::AsbSession` (ASB). Handle-table approach * `Mutex<HashMap<i32, ItemRef>>` for item handles, populated by `add_item` / `add_item_2` / `add_buffered_item`, drained by `remove_item` / `unregister`. * `Mutex<HashMap<i32, UserRef>>` for user handles allocated by `authenticate_user` / `archestra_user_to_id`. * `AtomicI32` monotonic counters for both, matching the .NET reference's `_nextItemHandle` / `_nextUserHandles` per `MxNativeCompatibilityServer.cs:62-63`. Stream-based event surface (per Q4) * `OnDataChange` / `OnBufferedDataChange` / `OnWriteComplete` / `OperationComplete` exposed as `EventStream<T>: Stream<Item=T>`, backed by `tokio::sync::broadcast` channels. Lag silently skips past `BroadcastStream::Lagged` to keep the public `Item` shape ergonomic. NOT COM events — that's the post-V1 `mxaccess-compat-com` crate per design/70-risks-and-open-questions.md Q4. The `OperationComplete` channel is wired but no firing path is modelled (R3 deferred — no captured byte mapping yet). * `Advise` / `AdviseSupervisory` spawn a background fan-out task that drains the `Subscription` stream and routes each `DataChange` to either `on_data_change` or `on_buffered_data_change` based on the item's `is_buffered` flag. `UnAdvise` / `RemoveItem` abort the task. Pass-through methods * `Write` / `Write2` -> `Session::write` / `write_with_timestamp` (`userId` ignored — the underlying surface uses engine identity). * `WriteSecured2` -> `Session::write_secured_at` with both user ids always passed (R6: single-user secured = same id twice; never gated). * `AdviseSupervisory` collapses onto `Session::subscribe` because the wire path is `AdviseSupervisory` already (`session.rs:1057`), matching the .NET reference's `cs:251-259` identical collapse. * `SetBufferedUpdateInterval` rounds up to nearest 100 ms per `MxNativeCompatibilityServer.cs:638`. Stubbed pass-throughs (mirror upstream `Error::Unsupported`) * `WriteSecured` (no timestamp) — `Session::write_secured` is stubbed at `crates/mxaccess/src/lib.rs:472` (only `WriteSecured2`/`0x3A` is ported); workaround documented inline. * `AddBufferedItem` allocates the handle but `Advise` for buffered items does not yet drive `Session::subscribe_buffered` cadence knob — TODO(F36) flagged inline at `add_buffered_item` and `set_buffered_update_interval`. Tests (25 new, all green) * Handle-table lifecycle: Add -> Advise -> UnAdvise -> Remove with a mocked subscription task. * Monotonic handle allocation; context-prefix combination. * `SetBufferedUpdateInterval` rounding (50 -> 100, 101 -> 200, etc.) + zero-rejection. * Compile-time check that all 18 LMX methods are reachable on `LmxClient`. * Each event stream yields published items; lag silently dropped. * GUID-shape validation; server-handle mismatch errors. Build hygiene * `cargo build -p mxaccess-compat` clean. * `cargo test -p mxaccess-compat` -> 25 passed. * `cargo clippy -p mxaccess-compat --all-targets -- -D warnings` clean. * `RUSTDOCFLAGS=-D warnings cargo doc -p mxaccess-compat --no-deps` clean. Deferred / TODOs * TODO(F36): wire `set_buffered_update_interval` cadence into the `advise` path for buffered items. * TODO(R3): plumb a real trigger into `on_operation_complete` once the byte mapping lands. * TODO(wave 2): live integration tests against AVEVA. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1de049e114 |
[F2] mxaccess-rpc: NTLM verify_signature (server-to-client) with constant-time MAC compare
rust / build / test / clippy / fmt (push) Has been cancelled
Closes F2. Structural port from [MS-NLMP] §3.4.4 — same shape as
the existing sign path but uses the server-to-client sub-keys
(`SealKey_S→C` / `SignKey_S→C`) derived alongside the client-to-
server pair at the end of create_type3.
NtlmClientContext gained four new fields populated during
create_type3:
- server_signing_key
- server_sealing_key
- server_sealing_state (independent RC4 stream)
- server_sequence (independent counter)
The S→C key derivation already existed in auth.rs (the seal_key /
sign_key helpers take a client_mode flag); F2 plumbs them into a
new verify_signature(message, signature) method.
The verify path:
1. Validates signature.len() == 16 + leading version word 0x01.
2. Reads trailing seq num, compares against self.server_sequence
(mismatch ⇒ InvalidSignature, no state change).
3. Computes expected_mac = HMAC_MD5(server_signing_key,
seq || message)[0..8] then RC4 transform.
4. Constant-time compares expected_mac against wire bytes 4..12
via subtle::ConstantTimeEq.
5. On success: commits cipher-state advance + ++server_sequence.
On failure: re-derives RC4 from server_sealing_key and skips
past server_sequence × 8 keystream bytes to restore the
pre-verify position — caller can retry.
New dep `subtle = "2"` (workspace-internal to mxaccess-rpc) for
the timing-oracle-safe MAC compare.
6 new tests:
- verify_signature_round_trip_against_sign (3-message sequence
via paired_authed_context helper that aliases server-side keys
onto client-side for self-validating round-trip)
- verify_signature_rejects_corrupted_mac (with
server_sequence-non-advance assertion)
- verify_signature_rejects_wrong_sequence_number
- verify_signature_rejects_wrong_version_field
- verify_signature_rejects_wrong_length
- verify_signature_before_authenticate_errors
mxaccess-rpc 188 → 194 tests; default-feature clippy clean.
The "awaiting wire-fixture capture" step listed in F2's prior
status note is no longer a hard prerequisite — [MS-NLMP] §3.4.4
fully defines the algorithm and the round-trip tests prove the
encoder/decoder pair is internally consistent. A captured
StatusReceived frame would still validate byte-parity vs a real
NmxSvc.exe signer, but that's future verification work; the
structural port ships unblocked.
design/followups.md F2 moved to Resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9496322712 |
[F27] mxaccess-asb-nettcp: constant-time DH mod_exp via crypto-bigint::DynResidue
rust / build / test / clippy / fmt (push) Has been cancelled
Closes F27 per option (b) of its resolve criterion: fixed-width
U2048 DH backend using crypto-bigint's Montgomery-form residue
arithmetic.
New auth.rs::constant_time_mod_exp(base, exp, modulus) wrapper
preserves the BigUint-in-BigUint-out API of the existing byte
helpers; the actual square-and-multiply chain runs in Montgomery
form. Both DH call sites swap to the wrapper:
- AsbAuthenticator::new line 179 (public-key generation)
- crypto_key line 354 (shared-secret derivation)
DH private exponent timing-leak resistance is the goal: the .NET
reference's BigInteger.ModPow is also non-constant-time, so we
were at parity but not at the long-term Rust target. With this
fix the production path no longer leaks the bit-pattern of the
long-lived DH private key through power/timing side channels.
DynResidueParams::new requires an odd modulus (Montgomery form's
only restriction). Production DH primes are always odd
(`MX_ASB_DH_PRIME = 1552...7919` on this host's registry).
CryptoParameters::DEFAULT_PRIME_TEXT — the test-fixture default
inherited from AsbRegistry.cs:66 — ends in 4 (even), which is
mathematically unsound for DH but kept for parity with the .NET
default. For that case the wrapper falls back to BigUint::modpow,
preserving the wire bytes (modular exp is a pure function of
inputs).
Wire-byte parity verified two ways:
1. Unit fixture test
`auth.rs::deterministic_hmac_matches_dotnet_fixture` — byte-equal
to captured .NET output for the full DH → PBKDF2 → AES-CBC chain.
Continues to pass.
2. Live: Connect handshake against the local AVEVA install
completes with apollo:V2 lifetime, proving MxDataProvider
accepts the constant-time-derived public key and the
shared-secret-based AuthenticateMe.
Workspace deps:
- crypto-bigint = "0.5" added to [workspace.dependencies] and
mxaccess-asb-nettcp/Cargo.toml.
- num-bigint retained for decimal-string parsing + .NET-LE byte
conversion (crypto-bigint has neither).
Closes the "review.md MAJOR finding" originally flagged at
design/30-crate-topology.md:269-274.
design/followups.md: F27 moved to Resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
cf9dbaf568 |
[F6] mxaccess-rpc: ComObjRefProvider port via windows-rs (CoMarshalInterface)
rust / build / test / clippy / fmt (push) Has been cancelled
New module crates/mxaccess-rpc/src/com_objref_provider.rs gated on cfg(all(windows, feature = "windows-com")). Pulls windows = "0.59" (features Win32_Foundation + Win32_System_Com + Win32_System_Com_Marshal + Win32_System_Com_StructuredStorage + Win32_System_Memory) as an optional dep behind the existing windows-com feature; default footprint stays slim. Public API mirrors ComObjRefProvider.cs 1:1: MarshalContext enum (InProcess / Local / DifferentMachine wrapping the MSHCTX_* newtype constants), clsid_from_prog_id, marshal_activated_iunknown_objref (activates via CoCreateInstance with INPROC | LOCAL | REMOTE then marshals), marshal_iunknown_objref (uses IUnknown::IID), marshal_interface_objref (CoMarshalInterface over an HGlobal-backed IStream). All `unsafe` is internal to the module — public API exposes only typed Rust values (Vec<u8>, GUID, ProviderError), no raw pointers / HRESULTs / lifetime-bound interface pointers leak. Each unsafe block carries an inline SAFETY comment naming the invariants being upheld. Per-thread COM init via thread-local OnceLock<()>: lazy CoInitializeEx(MULTITHREADED) on first call; S_FALSE (already initialised) and RPC_E_CHANGED_MODE (thread is STA) treated as success — matches the .NET runtime's tolerant apartment behaviour. ProviderError enumerates the four documented failure modes plus the apartment-init pre-check: UnknownProgId / ActivationFailed / MarshalFailed / GlobalLockFailed / ApartmentInitFailed. 4 offline tests: MarshalContext → MSHCTX_* mapping, ensure_apartment idempotence, clsid_from_prog_id returns UnknownProgId for fake ProgIDs, marshal_activated short-circuits at the resolution stage. 1 live test (#[ignore], gated on MX_LIVE): activates the real NmxSvc.NmxService, marshals the proxy's IUnknown via CoMarshalInterface, then parses the resulting blob via ComObjRef::parse and asserts non-zero OXID + IPID. Passes against the AVEVA install on this host. Workspace tests: mxaccess-rpc went 179 → 183 (+4). All other crates unchanged. Unblocks F12 (NmxClient::create — the auto-resolving COM-activation factory): the underlying primitive (marshal_activated_iunknown_objref) now exists; remaining work is threading the windows-com feature through mxaccess-nmx and chaining ComObjRef::parse → resolve_oxid_with_managed_ntlm_packet_integrity → RemQueryInterface. design/followups.md F12 updated with a revised "Resolves when" reflecting that F6's blocker is gone. Closes F6 in design/followups.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
41f2d4c0f2 |
[F14] mxaccess-galaxy: tiberius-backed SQL Resolver + UserResolver
rust / build / test / clippy / fmt (push) Has been cancelled
New module crates/mxaccess-galaxy/src/sql_resolver.rs (~480 LoC) gated behind the existing galaxy-resolver Cargo feature. Adds SqlTagResolver + SqlUserResolver, both constructed via from_ado_string(&str) accepting the same connection-string shape the .NET reference uses by default (Server=localhost;Database=ZB;Integrated Security=True; Encrypt=False;TrustServerCertificate=True). Integrated Security=True resolves to Windows auth via tiberius's winauth feature. Each top-level call (resolve / browse / resolve_by_guid / resolve_by_name) opens a fresh Client<Compat<TcpStream>> and drops it on return — matches the .NET `await using` lifecycle at GalaxyRepositoryTagResolver.cs:93-95. tiberius's Client::query only accepts positional @P1..@PN placeholders (delegates to sp_executesql); the canonical RESOLVE_SQL / BROWSE_SQL / USER_BY_GUID_SQL / USER_BY_NAME_SQL constants are rewritten once-per-process via OnceLock<String> (@objectTagName → @P1, etc.). The unrewritten constants stay byte-identical with the .NET reference for ad-hoc diagnostic copy/paste. read_metadata mirrors ReadMetadata (cs:149-165) byte-by-byte: signed smallint → i16 widened to u16 for platform/engine/object IDs (matches the .NET checked((ushort)reader.GetInt16(N)) shape), int → i32 checked-cast to i16 for property_id, nullable nvarchar for primitive_name. read_user_profile mirrors ReadProfile (cs:76-85) including the roles_text blob → parse_role_blob round-trip. Deps added (gated): tiberius 0.12 (default-features = false; tds73 + rustls + winauth — no chrono / rust_decimal pulled), tokio-util's compat feature for the futures-rs ↔ tokio AsyncRead bridge, futures-util for TryStreamExt::try_next. Default-feature build still pulls only mxaccess-codec + async-trait + thiserror + uuid (slim foot-print preserved per the design doc's intent). New `live` feature on this crate (`live = ["galaxy-resolver"]`) for parity with the workspace pattern. 11 offline unit tests pin: SQL named→positional rewriting (no @named left, @P1/@P2/@P3 present), line-count preserved, ado-string acceptance (default Galaxy shape parses, garbage rejected), input validation (max_rows=0 rejected, empty LIKE rejected, empty user_name rejected, all checked before connect attempt). Two #[cfg(feature = "live")] #[ignore]'d tests round-trip against a real Galaxy DB (gated on MX_LIVE + MX_GALAXY_DB env vars per tools/Setup-LiveProbeEnv.ps1). Live verification on this host: live_resolve_test_child_object_test_int and live_browse_test_child_object both pass against the local AVEVA install — TestChildObject.TestInt resolves with mx_data_type=2 (Int32), is_array=false. Closes F14 in design/followups.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3b09297b27 |
[M5] live-probe iteration 1 — major wire-byte reconciliation fixes
First live-test cycle against AVEVA on this box. Comparing the .NET
probe's `--dump-messages` XML output against our NBFX-encoded
envelope surfaced six structural bugs in the F25 envelope/operations
layer. All fixed; tests passing (702 workspace).
Fixes (all backed by the .NET dump as ground truth):
1. **`mustUnderstand` attribute name** — NBFS dict id was 116
(`MustUnderstand`, capital-M, a different SOAP token); SOAP 1.2
spec uses lowercase `mustUnderstand` at id 0. Sending the wrong
one triggered a WCF parse fault that surfaced as TCP RST.
2. **Missing `<a:MessageID>` header** — WCF's default binding
requires MessageID for two-way operations. We now auto-generate
`urn:uuid:<v4>` per envelope via a small inline `make_random_uuid_v4`
helper (no `uuid` crate dep).
3. **Missing `<a:ReplyTo>` anonymous header** — WCF's
BinaryMessageEncoder always emits `<a:ReplyTo><a:Address>...
addressing/anonymous</a:Address></a:ReplyTo>` for two-way ops.
4. **ConnectionValidator field names + namespace** — we were
emitting PascalCase `<ConnectionId>` etc. .NET's WCF
DataContractSerializer uses the private backing-field names
(`<connectionIdField xmlns="...ASBContract">guid</connectionIdField>`)
per `[DataMember(Name = "fooField")]`. Added the
`xmlns:i="...XMLSchema-instance"` declaration WCF emits
alongside (even when no `i:nil` is used). Decoder now accepts
both PascalCase (legacy tests) and DataContract field names.
5. **`<ASBIData>` over-wrapping** — we were emitting
`<Items><ASBIData>{bytes}</ASBIData></Items>`. .NET's
`AsbDataCustomSerializer.WriteStartObject` (`AsbContracts.cs:
1561-1572`) REPLACES the field's outer element with `<ASBIData>`
directly — there's no `<Items>` wrapper on the wire. Fixed by
collapsing `BodyField::AsbiDataElement` to emit just `<ASBIData>`
without the named outer element. The `name` field is retained
for self-documentation.
6. **`collect_asbidata_payloads` API** — was keyed by field name
(`Status` / `Values`); now positional (`payloads[0]`,
`payloads.get(1)`) since the wrapper element is gone. All seven
response decoders updated.
Plus tooling for the live-probe loop:
* `tools/Get-AsbPassphrase.ps1` — DPAPI loader that auto-discovers
the solution name + reads the sharedsecret + decrypts it. Sets
$env:MX_ASB_PASSPHRASE / MX_ASB_HOST / MX_ASB_VIA / MX_LIVE.
Lowercase via-host (WCF SMSvcHost is case-sensitive on the URL
host segment).
* `examples/asb-preamble-probe.rs` — diagnostic that connects,
runs the preamble, captures the PreambleAck, then sends a
synthetic ConnectRequest and dumps both directions as hex. Used
to bisect the wire-byte deltas above.
* `examples/asb-subscribe.rs` port default fixed (5074 → 808 —
WCF's NetTcpPortSharing/SMSvcHost listener confirmed via
Get-NetTCPConnection).
**Status**: preamble + PreambleAck round-trip works end-to-end
against the live AVEVA install (verified via probe). The
post-preamble Connect SOAP envelope still gets TCP RST'd — the six
structural fixes above are necessary but not yet sufficient. Next
iteration needs binary wire capture (Wireshark + Npcap loopback,
or a TCP-relay middleman) to compare the .NET probe's BinaryMessageEncoder
output byte-for-byte with ours and find the remaining delta(s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8a0f92b6bc |
[M5] mxaccess: F26 step 1 — AsbTransport bridges AsbClient into Transport trait
First slice of F26. Bridges F25's working AsbClient into the M0
`mxaccess::Transport` trait that Session uses to discriminate
operations across NMX and ASB transports.
API additions:
* `mxaccess::AsbTransport<T>` — generic over the same
AsyncRead+AsyncWrite+Unpin+Send+Sync+'static bound that AsbClient
takes. Owns an AsbClient and exposes it via `client_mut()` /
`into_client()`.
* `impl Transport for AsbTransport<T>`:
- `capabilities()` — `buffered_subscribe = false`,
`activate_suspend = false`, `operation_complete_frame = false`
per `design/60-roadmap.md` M5 (no NMX-specific extensions on
ASB).
- `kind()` — `TransportKind::Asb`.
Path-dep wiring: `mxaccess` now imports `mxaccess-asb` +
`mxaccess-asb-nettcp` directly.
Compile-time `Send + Sync + 'static` assertion guards the
trait-bound contract.
2 new tests:
* `asb_transport_kind_is_asb`.
* `asb_transport_capabilities_disable_buffered_and_activate_suspend`.
Stubbed for F26 step 2:
* `Session::connect_asb` constructor that owns TCP open +
preamble + DH handshake orchestration.
* Operation routing that maps ASB types (ItemStatus, RuntimeValue)
back to mxaccess types (MxStatus, DataChange, MxValue).
Stubbed for F26 step 3:
* Subscription routing — Session::subscribe on ASB needs F25
subscription operations (CreateSubscription / AddMonitoredItems
/ Publish), which are not yet implemented.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
25dbd8d3bd |
[M5] mxaccess-asb: F25 step 1 — SOAP envelope codec
First slice of F25. Provides the building blocks the per-operation
request/response codecs and the network loop will compose:
* `actions` module — IASBIDataV2 action strings (all 14 operations,
verbatim from `AsbContracts.cs:14-58`).
* `ConnectionValidator` — SOAP header struct mirroring
`AsbContracts.cs:65-117`. `from_signed(&SignedValidator)` converts
F23's MAC + IV to base64 for the wire, matching .NET's
`BinaryWriter`-via-`XmlSerializer` shape.
* `SoapEnvelope` + `encode_envelope` — assembles the NBFX token
stream: `s:Envelope` → `s:Header` → `a:Action s:mustUnderstand="1"`
→ optional `h:ConnectionValidator` → `s:Body` → caller-supplied
body tokens. Uses static-dictionary IDs for the SOAP/WS-Addressing
tokens via F22's `lookup_static`.
* `decode_envelope` — pulls action + validator + body tokens back
out of received bytes. Tolerant of header ordering.
* Mixed-endian GUID format/parse (`format_uuid` / `parse_uuid`) that
mirrors .NET's `Guid.ToString("D")` byte order so connection-id
round-trip matches the wire exactly.
9 new unit tests cover:
* Round-trip with and without validator.
* `from_signed` base64 encoding of MAC + IV.
* `format_uuid` produces the correct .NET-mixed-endian hex string.
* GUID round-trip through string formatter.
* Action string presence in the encoded byte stream.
* Decoder tolerance of envelopes without an Action header.
* Validator round-trip through full encode → decode.
* Lint-style guard that all 14 action constants are URIs ending `In`.
Stubbed for next F25 iteration: per-operation request/response
struct codecs (`ConnectRequest`, `RegisterItemsRequest`, etc.) +
`AsbClient` network loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ed17c07c10 |
[M5] mxaccess-asb-nettcp: M5 plan + F19 deps + F23 auth crypto port
F18 plans M5 as 9 sub-followups (F18-F26 + F27 constant-time DH) per design/dependencies.md:73-89. Wave-1 streams F20-F23+F24 are parallel-safe after F19 (workspace deps). F25 (ASB client) is sequential after the framing/encoder streams. F26 (Session over AsbTransport) is sequential after F25. F19 — workspace deps for the M5 crypto + framing surface: hmac, md-5, sha1, sha2, aes, cbc, pbkdf2, flate2, rand, num-bigint, num-traits, num-integer, quick-xml, tokio-util, zeroize. Pinned to the digest 0.10 / cipher 0.4 generation matching mxaccess-rpc. F23 — ports `AsbSystemAuthenticator.cs` (167 LoC) to `mxaccess-asb-nettcp::auth`. Wire-byte parity points: .NET BigInteger little-endian two's-complement byte order with optional 0x00 sign-byte suffix; AES-128-CBC with PKCS7 padding; PBKDF2-SHA1 1000 iterations over `Convert.ToBase64String(crypto_key)` with ASCII salt "ArchestrAService"; deflate-then-AES (Baktun) vs raw-AES (Apollo) selected by `:V2` lifetime suffix; HMAC-MD5/SHA1/SHA512 negotiated per `AsbSolutionCryptoParameters.HashAlgorithm` (with `force_hmac=true` fallback to HMAC-SHA1 for unrecognised algorithms). 13 unit tests cover the cryptographic primitives + DH peer agreement + .NET byte-order round-trip + Apollo lifetime dispatch. F27 — filed for the `num-bigint` → `crypto-bigint::BoxedUint` swap once the latter exposes a stable heap-allocated `pow_mod`. Currently at parity with the .NET reference (also not constant-time). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a31237d1d0 |
[M4] mxaccess: Subscription impls Stream<Item = DataChange> (resolves F15)
F15 step 2/2 lands the per-Subscription routing on top of step 1's broadcast layer. Subscription is now a working data-change stream. Subscription type - Now impls futures_util::Stream<Item = Result<DataChange, Error>> via tokio_stream::wrappers::BroadcastStream + a per-message filter. - No longer Clone (broadcast::Receiver isn't Clone). Consumers that want fanout subscribe twice or share via Arc<Mutex<...>>. - Holds the broadcast::Receiver subscribed BEFORE AdviseSupervisory fires — guarantees no updates between advise and stream-creation are dropped. - pending VecDeque buffers records from the current message so each poll_next yields at most one DataChange (Stream contract). Filter logic (records_to_data_changes, mirrors cs:333-343) - 0x32 SubscriptionStatus: keep when msg.item_correlation_id == subscription.correlation_id; drop otherwise. - 0x33 DataUpdate: keep ALL — codec exposes no per-record correlation field, and the .NET filter only checks item_correlation_id (which 0x33 doesn't carry), so DataUpdates fan out to every active subscription. Matches .NET behavior verbatim. - Records with value: None drop silently (mirrors evt.Record.Value is null filter at cs:337). - BroadcastStream Lagged(n) maps to Error::Configuration with the lag count in the detail string. Helpers - filetime_to_system_time(i64) -> SystemTime: inverse of system_time_to_filetime; saturates at Unix epoch for FILETIMEs before 1970 since SystemTime can't portably represent pre-epoch. - record_to_data_change(record, reference) -> Option<DataChange>: builds DataChange from one record, returns None for unparseable value (the codec couldn't decode the wire kind). - Status currently hardcoded to MxStatus::DATA_CHANGE_OK (mirrors NmxSubscriptionRecord.ToDataChangeStatus at NmxSubscriptionMessage.cs:22-25 which the .NET reference itself stubs to the OK constant). Cargo.toml additions: futures-util (workspace) + tokio-stream (0.1 with sync feature for BroadcastStream). Tests (5 new in mxaccess; total 40) - subscription_stream_yields_data_change_for_matching_correlation: build a 0x32 SubscriptionStatus with one Int32 record and the subscription's correlation id, inject through test_inject_sender, observe the DataChange (reference, value, quality match) on the Stream. - subscription_stream_filters_out_mismatched_correlation_for_status: inject 0x32 with wrong correlation id, assert the stream stays pending (timeout-as-success). - subscription_stream_keeps_data_update_regardless_of_correlation: inject 0x33 DataUpdate with one Int32 record (no correlation field on the message); stream still yields the DataChange. - filetime_to_system_time_round_trip: build a SystemTime with .005s precision, round-trip through both helpers, assert equality. - filetime_to_system_time_pre_unix_epoch_saturates: FILETIME 0 (year 1601) → SystemTime::UNIX_EPOCH (saturating clamp). design/followups.md: F15 moved to Resolved with both step commits referenced. Open list: 9 items (was 10). Test count delta: 511 -> 516 (+5). All four DoD gates green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
12cb10c3a1 |
[M4] mxaccess: Session::connect_nmx + write_value + shutdown (wave 1 main)
First working M4 wave 1 slice. Adds session.rs with the connect /
write / shutdown path on top of NmxClient + Resolver, plus a tokio
test that exercises a full round-trip against a hand-rolled server.
Read, subscribe, recovery, and the long-lived connection task land
in wave 2.
Architecture
- Session holds Arc<SessionInner>; SessionInner wraps NmxClient
behind a tokio::sync::Mutex. All RPC ops serialize on that mutex.
Wave 2 will replace it with an mpsc::channel<Op> + dispatcher task
per design/70-risks-and-open-questions.md R15 (drop-time async
cleanup hazards).
- ensure_connected gate stops post-shutdown ops with
Connection::EngineNotRegistered. Shutdown is idempotent via
AtomicBool::swap.
- Manual Debug impl on SessionInner — neither dyn Resolver nor
NmxClient impl Debug.
Public API
- Session::connect_nmx(addr, options, ntlm, service_ipid, resolver,
recovery): validates the policy, opens NmxClient, runs
RegisterEngine2 (no callback yet — wave 2), optionally configures
heartbeat. Returns Error::Connection on non-zero HRESULT.
- Session::write_value(reference, value: WriteValue): resolves the
tag through the configured Resolver, dispatches NmxClient::write.
- Session::resolve_write_kind / resolve_tag: convenience accessors.
- Session::shutdown_nmx: calls UnregisterEngine, idempotent.
Error mapping
- map_nmx / map_transport / map_resolver bridge the inner crate
errors into the public Error enum. NonZeroHresult → InvalidArgument
with the hex code; transport Fault → Status-shaped error;
ResolverError::NotFound → Galaxy { reason: "tag not found: ..." }.
- All three matchers handle their #[non_exhaustive] sources with a
generic catch-all so future variants don't silently break the map.
Tests (8 new in mxaccess; total mxaccess: 19)
- write_value round-trip via in-memory StaticResolver + hand-rolled
unauthenticated DCE/RPC server.
- write_value propagates resolver not-found → Galaxy error.
- write_value propagates non-zero HRESULT → InvalidArgument.
- shutdown is idempotent (second call is a no-op).
- write after shutdown returns EngineNotRegistered.
- resolve_tag and resolve_write_kind work without RPC.
- envelope-kind constants used by Session match codec exports
(sanity guard against codec rename).
mxaccess-nmx: WriteValue now re-exported at crate root.
mxaccess: deps gained mxaccess-nmx/galaxy/rpc + tokio + tracing,
plus async-trait as a dev-dep for the test resolver impl.
Test count delta: 479 -> 487 (+8). All four DoD gates green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
baea6eaa41 |
[M3] mxaccess-galaxy: GalaxyUserProfile + UserResolver trait + role-blob
Lands the user-resolver half of M3 stream A. Pure-Rust foundation — the tiberius-backed SQL impl is logged as F14 and stays gated behind the existing galaxy-resolver Cargo feature. New - role_blob.rs (~270 LoC, 12 tests including a garbage-between-roles edge case) — port of ParseRoleBlob (cs:87-133). Sliding-window scan over hex-decoded UTF-16LE bytes; rejects non-printable code units; case-insensitive dedup. Pure function, no I/O. - user.rs (~290 LoC, 8 tests including 4 tokio-driven InMemoryUserResolver cases) — GalaxyUserProfile (port of cs:5-11) + from_columns helper bridging into role_blob + UserResolver async trait + UserResolverError with NotFound / Backend variants. - sql.rs additions: USER_SELECT_SQL + USER_BY_GUID_SQL + USER_BY_NAME_SQL constants (port of cs:135-148). Inline concatcp! macro composes the base SELECT with each WHERE clause at compile time without pulling const_format. Cargo.toml: added uuid (Galaxy user_guid is a uniqueidentifier). design/followups.md: added F14 (P2) for the tiberius-backed SQL impl behind the galaxy-resolver feature. Test count delta: 427 -> 446 (+19; mxaccess-galaxy 30 -> 49). All four DoD gates green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d84b066c62 |
[M3] mxaccess-galaxy: GalaxyTagMetadata + parser + Resolver trait + SQL
Lands M3 stream A — the pure-Rust foundation of the Galaxy resolver:
the data type, the tag-reference parser, the async trait, and the
canonical SQL strings. Unblocks F13 (NmxClient::write_* wrappers depend
on GalaxyTagMetadata) without pulling in tiberius yet.
New
- metadata.rs (~195 LoC, 7 tests) — GalaxyTagMetadata record (port of
cs:6-73). Includes is_buffer_property + to_reference_handle(galaxy_id)
bridging into mxaccess-codec::MxReferenceHandle::from_names.
- parser.rs (~330 LoC, 12 tests) — ParsedTagReference parser. Handles
Object.Attribute (1 candidate), Object.Primitive.Attribute (2
candidates: primitive-attr first, dotted-attr second per cs:181-185),
and the case-insensitive .property(buffer) suffix. Pure-Rust, no I/O.
- resolver.rs (~200 LoC, 5 tests including a tokio-driven InMemoryResolver
proving the trait is implementable without SQL) — async Resolver trait
+ ResolverError. Default browse returns Backend("not implemented") so
read-only backends don't need to override it.
- sql.rs (~280 LoC, 5 smoke tests) — RESOLVE_SQL + BROWSE_SQL constants
ported byte-for-byte from cs:208-432. Available publicly so any
backend (the planned tiberius impl, a wwtools/grdb snapshot replay,
etc.) can grab the canonical query.
Cargo.toml: added mxaccess-codec (path), async-trait, thiserror;
tokio added as dev-dependency for the resolver-trait async tests.
Deliberately deferred to a later iteration:
- The tiberius-backed Resolver impl behind the galaxy-resolver feature.
- ToValueKind / TryGetValueKind / ProjectWriteValue helpers on
GalaxyTagMetadata (cs:41-72) — these need a MxDataType -> MxValueKind
lookup that the codec doesn't currently expose; landing them with
F13's write-helper iteration keeps the iteration coherent.
Test count delta: 397 -> 427 (+30). All four DoD gates green.
Open followups touched: F13 prerequisite (GalaxyTagMetadata) now in
place; F13 itself stays open until the write helpers wire it up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0c772d273d |
[M3] mxaccess-nmx: NmxClient — 9 raw INmxService2 opnums (stream B)
Lands M3 stream B raw opnum surface: an async NmxClient over the mxaccess-rpc transport that dispatches all 9 INmxService2 procedures (GetPartnerVersion, RegisterEngine2 + WithoutCallback, UnregisterEngine, Connect, AddSubscriberEngine, RemoveSubscriberEngine, SetHeartbeatSendInterval, TransferData) plus a NonZeroHresult error variant that mirrors ThrowIfFailed (cs:563-574). New - crates/mxaccess-nmx/src/client.rs (~580 LoC, 8 tests including 5 real-socket tokio tests against a hand-rolled DCE/RPC server) — port of the raw opnum surface from ManagedNmxService2Client.cs. - NmxClient::connect builds the NTLM-packet-integrity bind path; for tests, NmxClient::from_bound_transport accepts a transport bound any way the caller likes (the test server doesn't validate signatures). - fresh_orpc_this generates a per-call Cid via rand::random(), mirroring the .NET reference's Guid.NewGuid() at every call site. - NmxClientError::NonZeroHresult unifies the .NET reference's Marshal.ThrowExceptionForHR + InvalidOperationException branches so callers see one typed surface for "transport-OK + LMX rejected". Cargo.toml: added tokio, tracing, thiserror, rand to mxaccess-nmx. Two layers of the .NET reference are deliberately out of scope this iteration; both logged as new followups in design/followups.md: - F12 (P1): the auto-resolving Create() factory, which needs windows-rs COM activation (gated by F6) + ComObjRefProvider port. - F13 (P1): the high-level Write*/Advise*/UnAdvise/RegisterReference helpers, which depend on GalaxyTagMetadata from M3 stream A (the Galaxy SQL resolver crate, not yet started). Test count delta: 389 -> 397 (+8). All four DoD gates green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
432f1102b7 |
[M2/M3] mxaccess-rpc: tokio DCE/RPC TCP transport (DceRpcTcpClient port)
Lands the async DCE/RPC TCP client — the transport that bridges the M2
PDU codec to a real socket. Unblocks M3 stream B (mxaccess-nmx, the
NmxClient) and brings F9 (ResolveOxid wrappers) within reach.
New
- transport.rs (~700 LoC, 10 tests including 2 real-socket tokio tests)
— port of src/MxNativeClient/DceRpcTcpClient.cs.
- DceRpcTcpClient::connect/bind/bind_with_managed_ntlm_packet_integrity/
call/call_bound/call_bound_object — async over tokio::net::TcpStream.
- encode_packet_integrity_request: 4-byte 0xBB pad + 8-byte AuthTrailer
+ 16-byte NtlmClientContext::sign signature, frag_length and
auth_length rewritten in the embedded header per cs:201-250.
- encode_request_bytes: PFC_OBJECT_UUID flag (0x80) and inserted
16-byte object UUID slot per cs:269-278.
- TransportError enum unifies io / codec / NTLM / fault / not-connected
surfaces. Mirrors DceRpcFaultException as the typed Fault variant.
- NTLM_AUTH_CONTEXT_ID = 79232 = 0x13580 (cs:90,133) exposed publicly.
Deliberately skipped: BindWithNtlmConnect / BindWithNtlmPacketIntegrity
(SSPI flavours at cs:55-63,108-149) — those wrap .NET's
System.Net.Security.SspiClientContext, which has no portable analogue.
Managed-NTLM path covers what the production Rust client needs.
mxaccess-rpc/Cargo.toml: added tokio (workspace-pinned).
design/followups.md: F9 downgraded P1 → P2 (transport landed; only the
two pure-codec ResolveOxid wrappers remain).
Test count delta: 354 -> 364 (+10).
Open followups touched: F9 partially advanced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b0954b2672 |
[M2] mxaccess-callback: tokio TCP exporter (wave 3 main)
Lands the M2 wave 3 main course — the INmxSvcCallback callback exporter.
Pure-tokio TCP server that mirrors src/MxNativeClient/ManagedCallbackExporter.cs
and lets a Rust client receive callbacks from NmxSvc.exe.
New
- exporter.rs (~700 LoC, 10 tests) — port of ManagedCallbackExporter.cs.
CallbackExporter::bind starts a TcpListener + accept loop; per-connection
serve task walks Bind / AlterContext / Request / Auth3 PDUs and dispatches
IRemUnknown (opnums 3/4/5) and INmxSvcCallback (opnums 3/4) requests.
Hand-rolled BindAck encoder mirroring cs:226-254 (single acceptance entry,
NDR20 transfer syntax).
- ExporterIdentities { oxid, oid, callback_ipid, rem_unknown_ipid } — exposes
both `random()` (production) and `fixed()` (tests). Mirrors the .NET
RandomUInt64 + Guid.NewGuid pattern at cs:14-20.
- CallbackEvent enum — typed diagnostic stream replacing .NET's
List<string> log (cs:12,33-42,315-321). Variants: ClientConnected,
AcceptError, Bind, Auth3Ignored, Request, RemQueryInterface,
CallbackInvoked, UnhandledRequest, ClientDisconnected, ProtocolError.
- IUNKNOWN_IID const re-exported alongside the other IIDs.
Tests cover real-socket round-trips: Bind+RemQueryInterface (with IUNKNOWN
returning S_OK), Bind+unknown opnum -> Fault, Bind+DataReceived ->
CallbackInvoked event + 12-byte success response, and graceful shutdown.
Test count delta: 344 -> 354 (+10).
Open followups touched: none new. F2 (verify_signature path) still
gated on a live status-frame fixture under tests/fixtures/m2-status-frame/.
F6 / F9 still need the windows-rs and DceRpcTcpClient ports respectively.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
95bd218183 |
[M2] mxaccess-rpc: NTLMv2 + DCE/RPC PDU + OBJREF parser (wave 1)
Lands M2 wave 1 — three pure-Rust modules under crates/mxaccess-rpc with 60 unit tests. Each is a 1:1 port of one .NET reference file: - ntlm.rs (1137 LoC, 19 tests) — `ManagedNtlmClientContext.cs`. NTLMv2 challenge/response, Type1/Type3 builders, sign() with RC4-sealed checksum and per-call sequence advance. Manual `Debug` impl that hides credentials; not Clone (rc4 0.2 cipher state is non-Clone). Pure-Rust crypto via hmac/md-5/md4/rc4 v0.2/rand v0.8 (rc4 0.2 chosen per design/review.md:78). - pdu.rs (1573 LoC, 33 tests) — `DceRpcPdu.cs` + auth-trailer types from `DceRpcAuthentication.cs`. Bind/AlterContext/Auth3/Request/Response/Fault PDUs, NDR20 transfer syntax, auth_value with 4-byte alignment padding, preserved-byte fields per CLAUDE.md unknown-bytes rule. - objref.rs (~470 LoC, 11 tests including a 366-byte captured OBJREF round-trip) — `ComObjRef.cs`. MEOW signature, OXID/OID/IPID, dual-string array with printable-ASCII escaping and security-binding boundary. ComObjRefProvider.cs deferred (windows-rs Win32 wrapper — see F6). Every wire-byte claim cites src/MxNativeClient/<file>.cs:LINE per CLAUDE.md "no fabricated protocol behaviour" rule. Test count delta: 217 → 277 (+60) Open followups touched: F1–F8 (new — see design/followups.md) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fe2a6db786 |
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>
|