diff --git a/design/followups.md b/design/followups.md index b715102..589f4ee 100644 --- a/design/followups.md +++ b/design/followups.md @@ -111,10 +111,24 @@ Between each publish: wait for the crate to be indexed before the next one's `ca **The OBJREF binding is correct:** `DESKTOP-6JL3KKO[]` with `port` from `tokio::net::TcpListener::bind(0.0.0.0:0)`. Windows Firewall is OFF on all profiles. The hand-rolled exporter accepts connections; NmxSvc just refuses to use it. -**Hypotheses (each needs verification):** -1. NmxSvc validates callback OBJREFs through Windows DCOM (`CoUnmarshalInterface` or similar) before registering them — and the hand-rolled blob fails that validation, surfacing as `RPC_S_SERVER_UNAVAILABLE` because COM interprets it as "the named server is unreachable". -2. The OBJREF carries fields (e.g. specific `STDOBJREF.flags`, security bindings, or authn-hint values) NmxSvc requires that the hand-rolled builder doesn't set correctly. Comparing the byte-by-byte structure shows identical layout to .NET's hand-rolled OBJREF — but the same .NET hand-rolled OBJREF also fails. So this isn't a Rust-vs-.NET layout drift, it's an architecture-vs-NmxSvc gap. -3. The NmxSvc version on this dev machine has stricter callback validation than the reference development version targeted by `MxNativeClient`'s original architecture. (NmxSvc release notes / version unknown at this point.) +**Path C investigation (2026-05-06).** Captured the OBJREF byte structure from both paths via the .NET probe: + +| Field | DCOM-marshalled (works) | Hand-rolled (fails) | +|---|---|---| +| Total size | 338 bytes | 162 bytes | +| `std_flags` | `0x0A80` (SORF_OXRES4+OXRES6+OXRES8) | `0x280` (SORF_OXRES4+OXRES6) | +| `std_public_refs` | 5 | 5 | +| `std_oxid` / `std_oid` / `std_ipid` | random per session | random per session | +| ncacn_ip_tcp bindings | 4 (DESKTOP-6JL3KKO, 10.100.0.48, 2x IPv6 link-local) — **no ports** | 1 (DESKTOP-6JL3KKO[]) — **with port** | +| Security bindings | 7 | 7 | + +Tried setting `std_flags = 0x0A80` on the hand-rolled OBJREF (matching the DCOM-marshalled flag bits): **RegisterEngine2 still fails with the same 1722.** Reverted. + +**Updated diagnosis.** The likely cause is that NmxSvc, on receiving RegisterEngine2 with a callback OBJREF, does its own SCM-side OXID resolution: it calls `IObjectExporter::ResolveOxid` against the local SCM at `127.0.0.1:135` to get the bindings for the OBJREF's OXID, then dials those bindings. Our hand-rolled OXID is **never registered with the local SCM**, so the resolution fails and NmxSvc returns `RPC_S_SERVER_UNAVAILABLE` (1722) — matching the symptom and the sub-second timing (no TCP-dial-back attempt to our listener happens at all). + +DCOM marshalling fixes this because `CoMarshalInterface` internally registers the OXID with RPCSS, so NmxSvc's SCM-side ResolveOxid succeeds. The bindings carry no port because RPCSS-side resolution returns the dynamic port from the Windows DCOM stub layer. + +This makes Path A the architecturally correct fix: the callback exporter must be a DCOM-managed object (registered with RPCSS) for NmxSvc to accept the callback. The hand-rolled-listener-with-explicit-port-in-OBJREF approach used by both the Rust port and the .NET reference's `ManagedCallbackExporter` doesn't satisfy NmxSvc's callback validation. **Three resolution paths (each substantial):** diff --git a/rust/crates/mxaccess-callback/src/exporter.rs b/rust/crates/mxaccess-callback/src/exporter.rs index 33594f0..903e73d 100644 --- a/rust/crates/mxaccess-callback/src/exporter.rs +++ b/rust/crates/mxaccess-callback/src/exporter.rs @@ -210,9 +210,13 @@ impl CallbackExporter { /// Build a callback OBJREF to publish back to the AVEVA service. /// /// Mirrors `ManagedCallbackExporter.CreateCallbackObjRef` - /// (`cs:44-54`): the IID is `INmxSvcCallback`, `std_flags = 0x280`, + /// (`cs:44-54`): the IID is `INmxSvcCallback`, /// `public_refs = 5`, OXID/OID/IPID come from `self.identities`, and /// the single string binding is `"[]"`. + /// + /// `std_flags = 0x280` — `SORF_OXRES4 | SORF_OXRES6` (= `0x80 | + /// 0x200`). Mirrors the .NET reference's `ManagedCallbackExporter` + /// (`cs:48`). #[must_use] pub fn create_callback_objref(&self, hostname: &str) -> Vec { let binding = format!("{hostname}[{port}]", port = self.local_addr.port());