Files
mxaccess/design/followups.md
T
Joseph Doherty 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>
2026-05-05 07:47:42 -04:00

7.5 KiB

Followups

Open work items deferred during /loop iterations. Triaged at the top of every iteration. New items are appended under ## Open; resolved items move to ## Resolved with a date + commit hash.

Open

F1 — NTLM consumer-layer helpers (workstation default + from_env constructor)

Severity: P3 Source: M2 wave 1, crates/mxaccess-rpc/src/ntlm.rs Why deferred: The .NET reference's Environment.MachineName default for workstation and its FromEnvironment() constructor (ManagedNtlmClientContext.cs:38, :41-49) read host state and env vars — both side effects that don't belong in a pure codec module. The constructor takes workstation: Option<&str> so callers can wire either later. Resolves when: M2 wave 2 transport (or the M2 example connect-nmx.rs) wires NtlmClientContext::new(.., Some(hostname()?)) and provides a small from_env helper at the consumer layer.

F2 — NTLM verify_signature path + constant-time MAC compare (server-to-client direction)

Severity: P2 Source: M2 wave 1, crates/mxaccess-rpc/src/ntlm.rs Why deferred: The .NET ManagedNtlmClientContext only implements client-to-server signing (cs:30,124); there is no implementation of server-to-client sign/seal keys or verify_signature. Both are needed when the callback exporter receives a signed inbound frame from NmxSvc.exe, but no such fixture exists yet. Resolves when: M2 wave 3 (callback exporter) captures an INmxSvcCallback::StatusReceived frame with an auth_value trailer per design/60-roadmap.md:56 (DoD #3) and a fixture lands under tests/fixtures/m2-status-frame/. Add subtle = "2" and gate the byte compare behind ConstantTimeEq at the same time.

F3 — Cross-domain NTLM Type1/2/3 fixture

Severity: P2 Source: M2 wave 1, crates/mxaccess-rpc/src/ntlm.rs Why deferred: All current NTLM fixtures are single-domain (the local AVEVA install). Tracked separately in design/70-risks-and-open-questions.md R8 (P1 risk) and the open-evidence-gaps table. Resolves when: A multi-domain AVEVA test harness lands and a successful cross-domain authenticate round-trip captures Type1/2/3 bytes. Notes: this clears R8.

F4 — BindAck / AlterContextResponse body parser

Severity: P2 Source: M2 wave 1, crates/mxaccess-rpc/src/pdu.rs Why deferred: The .NET reference (DceRpcPdu.cs:217-262) parses Bind and AlterContext into the same struct but does not decode the corresponding response body (result list + secondary address). The Rust port's BindPdu::decode accepts BindAck packet type but does not interpret the body. The negotiated transfer syntax — needed before opnum dispatch — is currently inferred from request-side context. Resolves when: A captured BindAck frame from captures/013-loopback-subscribe-scalars/nmx-stream-*.bin is decoded and the body shape is documented in docs/Loopback-Protocol-Findings.md.

F5 — Captured DCE/RPC bind-frame fixture round-trip

Severity: P2 Source: M2 wave 1, crates/mxaccess-rpc/src/pdu.rs Why deferred: Existing PDU tests build hand-constructed [C706]-conformant frames. A capture-driven round-trip (extract bind/alter PDUs from captures/013-loopback-subscribe-scalars/nmx-stream-*.bin, decode → encode → assert byte-identical) would be stronger evidence of parity with the live wire. Resolves when: Bytes from that capture are extracted into tests/fixtures/m2-pdu/ and the round-trip test lands.

F6 — Port ComObjRefProvider.cs (OBJREF emitter via Win32 CoMarshalInterface)

Severity: P2 Source: M2 wave 1, crates/mxaccess-rpc/src/objref.rs Why deferred: The provider is a wrapper around ole32::CoMarshalInterface / IStream / GlobalLock / GlobalSize. It needs windows-rs, which is currently behind the windows-com feature in mxaccess-rpc/Cargo.toml. The pure-Rust parser stands alone for the inbound activation-response path that M2 wave 1 needs. Resolves when: windows-rs is wired into mxaccess-rpc (M2 wave 3 callback exporter needs to publish its own OBJREF for IRemUnknown / INmxSvcCallback registration) and an emitter port lands behind the windows-com feature.

F9 — ObjectExporterClient.cs ResolveOxid wrapper methods

Severity: P2 (was P1 — downgraded after DceRpcTcpClient transport landed) Source: M2 wave 2, crates/mxaccess-rpc/src/object_exporter.rs Why deferred: The transport prerequisite (DceRpcTcpClient) is now ported in crates/mxaccess-rpc/src/transport.rs. What remains is two thin wrapper methods that wire the codec to the transport: resolve_oxid_unauthenticated(addr, oxid, protseqs) -> Result<ResolveOxidResult, _> (mirrors ObjectExporterClient.cs:14-30) and resolve_oxid_with_managed_ntlm_packet_integrity(addr, oxid, protseqs, ntlm) -> Result<ResolveOxidResult, _> (mirrors cs:66-81). The two SSPI variants (ResolveOxidWithNtlmConnect at cs:32-47 and ResolveOxidWithNtlmPacketIntegrity at cs:49-64) are .NET-specific (System.Net.Security.SspiClientContext) and explicitly out of scope. Resolves when: Both wrapper methods land, calling DceRpcTcpClient::connect/bind/call_bound against IObjectExporter opnum 0 and parsing via parse_resolve_oxid_result / parse_resolve_oxid_failure.

F10 — IObjectExporter::ResolveOxid2 (opnum 4) body codec

Severity: P2 Source: M2 wave 2, crates/mxaccess-rpc/src/object_exporter.rs Why deferred: ObjectExporterMessages.cs only models opnum 0 (ResolveOxid). Opnum 4 (ResolveOxid2) has a different response shape — it adds a COMVERSION plus an AuthnHnt[] array. The .NET reference does not exercise this path, so there's no executable spec to mirror. Resolves when: Either a [MS-DCOM] §3.1.2.5.1.4-derived layout is verified against a captured ResolveOxid2 exchange, or the .NET reference grows a ParseResolveOxid2* helper.

F11 — IRemUnknown::RemAddRef and RemRelease body codecs

Severity: P2 Source: M2 wave 2, crates/mxaccess-rpc/src/rem_unknown.rs Why deferred: RemUnknownMessages.cs declares the opnums (:9-10) but does not implement encoders/decoders. The Rust port matches that exactly per "port what is already proven." Resolves when: The .NET reference adds bodies for opnums 4 / 5 (or a captured frame establishes the on-wire shape). At that point port them into rem_unknown.rs alongside the existing RemQueryInterface codec.

Resolved

F7 — Consolidate Guid type across mxaccess-rpc

Resolved: 2026-05-05 in this iteration's commit. Guid was hoisted from objref::Guid into the new shared crate::guid::Guid module. objref and pdu now re-export from there; M2 wave 2's orpc, object_exporter, and rem_unknown import it directly. The OXID-resolve dual-string decoder additionally needs an owned protocol label (format!("protseq_0x{:04x}", tower_id) per ObjectExporterMessages.cs:120) — ComDualStringEntry::protocol was upgraded from &'static str to Cow<'static, str> to support both decoders without the agent's interim Box::leak workaround.

F8 — RpcError is duplicated across objref and pdu modules

Resolved: 2026-05-05 in this iteration's commit. RpcError was hoisted into the new shared crate::error::RpcError module as a single union of all wave 1 variants plus a generic Decode { offset, reason: &'static str, buffer_len } variant for the wave 2 ORPC parsers' one-off failures. objref and pdu re-export from there; M2 wave 2's orpc, object_exporter, and rem_unknown use it directly.