432f1102b7
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>
70 lines
7.5 KiB
Markdown
70 lines
7.5 KiB
Markdown
# 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.
|