Skip to content

Continue C# parity: TCP, live discovery, and vendor CPA interoperability - #14

Merged
masarray merged 180 commits into
mainfrom
agent/phase-4c-tcp-live-discovery
Aug 10, 2026
Merged

masarray merged 180 commits into
mainfrom
agent/phase-4c-tcp-live-discovery

Conversation

@masarray

@masarray masarray commented Aug 6, 2026 •

Copy link
Copy Markdown
Owner

Scope

This draft PR now delivers Phase 4C and Phase 4C.1:

  • built-in non-blocking Windows Winsock and POSIX TCP transport;
  • bounded live MMS discovery;
  • C#-compatible live-ied-model-v1 hierarchy and deterministic fingerprint;
  • C#↔C++ parity tooling; and
  • repeatable physical read-only interoperability evidence runners.

TCP and live discovery

  • IPv4/IPv6 resolution, TCP_NODELAY, keepalive, partial-send, peer-close, deadlines, and std::stop_token cancellation.
  • Bounded GetNameList pagination for domains, variables, and named-variable lists.
  • Optional GetVariableAccessAttributes, GetNamedVariableListAttributes, and read-only RCB attribute probes.
  • Partial evidence and diagnostics are retained when optional probes fail.

Phase 4C.1 live-model parity

  • Normalize LN$FC$DO$DA... into Logical Device / Logical Node / Data Object / Data Attribute hierarchy.
  • Preserve MMS/user references, functional constraints, MMS/SCL type evidence, DataSets, and BRCB/URCB state.
  • Resolve direct leaf and nested TypeSpecification evidence.
  • Infer IED identity, logical-device aliases, and CDC conservatively with explicit confidence.
  • Emit the C# oracle schema through ariec61850_live_discover --model-json.
  • Emit deterministic manifests and fingerprints through --manifest.
  • Compare C# and C++ models with scripts/compare-live-model-parity.py.

Current interoperability hardening

  • Align the Presentation CPA decoder with vendor responses that include an optional responding-presentation-selector (0x83) before the context-result list.
  • Make IP/hostname the normal CLI input; --ied-name remains an optional model identity override only.
  • Continue porting the C# live IED identity resolver and physical-I/O behavior before claiming full parity.

Physical read-only interoperability

  • scripts/run-live-readonly-interop.py reconnects and performs a fresh discovery per cycle.
  • PowerShell and Bash wrappers are included.
  • Per-cycle live models, optional C# parity reports, and interop-summary.json are retained.
  • Acceptance requires non-empty coverage, stable fingerprints, accepted warning policy, and optional C# parity success.

Read-only safety boundary

The discovery path constructs only GetNameList, GetVariableAccessAttributes, GetNamedVariableListAttributes, and Read. It does not construct MMS Write, control, GI, RCB reservation/enable, dynamic DataSet mutation, or file-service requests. The scripted regression decodes every emitted discovery request and rejects service tag 5.

Validation completed locally

  • GCC and Clang strict warning validation of the new model/reference implementation: passed.
  • Multi-translation-unit header/link validation: passed.
  • Clang ASan/UBSan model smoke: passed.
  • TCP loopback, cancellation, GCC/Clang strict checks, and sanitizer smoke from Phase 4C: passed.
  • Python parity and interoperability scripts: py_compile passed.
  • Deterministic simulated three-cycle interoperability run: passed with a stable fingerprint.
  • Windows MSVC full CTest reported by the maintainer: 18/18 passed before the current vendor-CPA compatibility patch.

Acceptance still pending

No physical IED result is claimed. Before merge:

  1. run repository GCC/Clang/MSVC and Security/Evidence workflows;
  2. run full branch CTest after each interoperability patch;
  3. execute the evidence runner against external IEC 61850 client/KM_Looptest and a reachable physical IED;
  4. complete a primary-vendor ten-cycle run plus timeout/reconnect evidence;
  5. port and validate the complete C# IED identity resolver and basic/full CLI discovery split;
  6. record large-model pagination where available; and
  7. accept C#↔C++ parity against the same IED configuration.

The PR remains draft until those external acceptance gates are complete.

@masarray masarray closed this Aug 6, 2026
@masarray masarray reopened this Aug 6, 2026
masarray added 27 commits August 8, 2026 12:37
@masarray
masarray marked this pull request as ready for review August 10, 2026 21:52
@masarray
masarray merged commit bdf6c59 into main Aug 10, 2026
9 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eee6c16e76

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/mms/tcp_transport.cpp
Comment on lines +478 to +480
if (received > 0) {
bytes.resize(static_cast<std::size_t>(received));
return bytes;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Drain buffered TPKTs before performing another recv

When one TCP read contains multiple TPKT frames—which TCP explicitly permits—this returns the whole coalesced chunk. MmsAssociationRuntime::receive_application_payload() stops after the first complete application payload and leaves remaining frames in tpkt_decoder_, but its next iteration or call invokes transport_.receive() before trying to pop those buffered frames. A response coalesced behind an information report therefore remains unread while the runtime waits for new socket data and eventually times out; drain the decoder before blocking on the socket.

Useful? React with 👍 / 👎.

Comment on lines +57 to +64
const bool has_presentation_accept =
contains_octet(presentation, 0x31U) ||
contains_octet(presentation, 0xA0U);
const bool has_acse_aare = contains_octet(presentation, 0x61U);
const bool has_user_information = contains_octet(presentation, 0xBEU);
const bool has_mms_initiate_response = contains_octet(presentation, 0xA9U);

if (!has_presentation_accept || !has_acse_aare) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse the AARE result before accepting compatibility responses

When strict CPA/AARE decoding fails because of a vendor-specific or malformed field, this fallback treats the mere presence of byte values 0x31/0xA0 and 0x61 anywhere in Session user data as acceptance. A rejected AARE still contains the 0x61 AARE tag and normally sits inside those Presentation markers, so such a response can incorrectly transition the runtime to associated; the compatibility path must minimally parse and verify the AARE result instead of scanning for octets.

Useful? React with 👍 / 👎.

Comment thread src/mms/tcp_transport.cpp
Comment on lines +326 to +327
const int resolve_result = getaddrinfo(
endpoint.host.c_str(), port.c_str(), &hints, &raw_addresses);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound hostname resolution by the connect deadline

For hostname endpoints under slow or unavailable DNS, getaddrinfo() is a synchronous blocking call and neither the supplied deadline nor the stop token is observed until it returns. Consequently the advertised connect timeout and cancellation behavior can be exceeded indefinitely before the nonblocking socket logic begins; use cancellable/asynchronous resolution or otherwise include resolution in the bounded operation.

Useful? React with 👍 / 👎.

Comment thread src/mms/tcp_transport.cpp
Comment on lines +184 to +188
if (read_ready) {
FD_SET(socket, &read_set);
}
if (write_ready) {
FD_SET(socket, &write_set);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid FD_SET overflow for high POSIX descriptors

On POSIX processes whose file-descriptor limit is raised above FD_SETSIZE, socket() can return a descriptor of 1024 or greater; passing it to FD_SET writes outside the fixed-size fd_set, causing undefined behavior or memory corruption. This library can therefore fail in services with many open descriptors even though the socket itself was created successfully; use poll/ppoll or reject descriptors outside the supported range before calling FD_SET.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant