The consumer matrix minted a 55-character otua_matrix_ key, which stays under SDK 2's 64-byte RsaEncryptedSecret threshold and so could not show lmxopcua#880 from the consumer side. It now uses the ApiKeySecret.Generate shape (65 characters), as opcua-sdk-compatibility already does. README records the 2026-11-12 run with ScadaBridge on SDK 2.0.1 (a2e9d718): matrix 11/11 x3 modes on83ed880eand412da27c, ACL off and on; 8/8 trust controls fail closed; failover 8/8; a mutation with the SDK default UserName handler fails every baseline leg with BadIdentityTokenInvalid. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
7.3 KiB
Actual ScadaBridge consumer compatibility
This isolated process references the real ScadaBridge OpcUaDataConnection and
RealOpcUaClient, with their own dependency graph. It never loads the candidate
SDK into that process. The verified consumer source is
0e89966da670e289bd875166295bb0e85d8cf8b2, pinned to SDK 1.5.378.106.
Build with an explicit clean consumer checkout:
dotnet build scripts/validation/scadabridge-sdk-compatibility/Probe.csproj \
-c Release -p:ScadaBridgeSourceRoot=/absolute/ScadaBridge
First build the separate application hosts with the parent
scripts/e2e/test-opcua-sdk-compatibility.ps1 harness. Then choose a new output
directory and pass its original and candidate ProcessHost.dll paths:
python3 scripts/validation/scadabridge-sdk-compatibility/run.py \
--baseline-host /absolute/matrix/build-baseline/bin/Release/net10.0/ProcessHost.dll \
--candidate-host /absolute/matrix/build-candidate/bin/Release/net10.0/ProcessHost.dll \
--probe scripts/validation/scadabridge-sdk-compatibility/bin/Release/net10.0/Probe.dll \
--results /absolute/fresh-consumer-evidence
The helper runs each application version with None, Sign and SignAndEncrypt,
plus two trust negative controls per secure profile (server does not trust the
client, client does not trust the server) that must fail closed. None and Sign
are enabled only in their own test fixtures (OPCUA_MATRIX_SERVER_SECURITY=none|sign);
the ordinary compatibility host remains strict SignAndEncrypt. Before each secure
leg the probe's prepare step provisions the consumer identity through
ScadaBridge's own certificate code and exports only its public certificate, which
the runner places in the server's trusted store before the server starts.
OPCUA_MATRIX_ACL=off disables the server's node-ACL check. The fixture's Eq equipment folder is placed in the
authority snapshot (unsFolderScopes, as a deployed generation does since lmxopcua#808, fixed in lmxopcua#888), so
the UNS cases pass with the ACL on and off; an unplaced Unplaced UNS folder is kept as a negative control that the
Shared client leg requires to stay denied with the ACL on. The task key is
generated per process pair, issuer/trusted/rejected stores are task-local, and
server public certificates are explicitly copied. Autoaccept remains false.
Never commit an entire output directory: it contains the fixture's private PKI.
The consumed subset covers API-key activation, raw/UNS scalar and Int32[]
roundtrips, durable hierarchical browse including the native alarm, data quality
and timestamps on raw and UNS subscriptions, alarm filter/ConditionRefresh,
disconnect/reconnect, and (ScadaBridge#125) a Server-object alarm subscription on
the reconnected session whose ConditionRefresh must replay the active native
condition (11 cases per trusted leg). The API key has the production
ApiKeySecret.Generate shape and length, 65 characters (lmxopcua#880): the
55-character otua_matrix_ key used before stayed under SDK 2's 64-byte
RsaEncryptedSecret threshold, so it could not show whether an SDK 2 consumer
still sends the password in a format an SDK 1.5 server decrypts. History
and Part 9 acknowledgment are not exposed by the inspected consumer interfaces;
the parent Shared/CLI matrix covers those application services instead.
ScadaBridge#121 (consumer never configured an application certificate) is
fixed in ScadaBridge ff835447/36c64a6b. Re-run 2026-11-12 with ScadaBridge
424db155: None, Sign and SignAndEncrypt 9/9 against the SDK 1.5.378 baseline
(83ed880e), the SDK 2.0.1 candidate (ebdc5892) and master 182fb353 with
ACL off; all trust negative controls failed closed. The runner retains the
nonzero result and each actual failure; it does not loosen trust or change
expected results to return success. No ScadaBridge production source or user PKI is modified.
Redundancy failover (ScadaBridge#125)
run.py also runs a failover suite (--suites matrix,failover, both by
default; --versions candidate runs one application version only). Each leg
starts two independent servers and runs Probe failover <primary> <backup> <mode> <dir>, which drives ScadaBridge's own DataConnectionActor (the
production primary/backup state machine, the real DataConnectionFactory,
production DataConnectionOptions defaults, FailoverRetryCount = 3). Once the
probe has a steady primary (Good values on a raw and a UNS tag, a write, the
Server-object alarm subscription with its ConditionRefresh) it writes
primary-ready; the runner then loses the primary and writes primary-killed.
The probe must then see Bad quality pushed, "Connected to backup", Good values
whose first value is the backup's own (proving the source), a fresh
ConditionRefresh replaying the native condition, and a write through the backup.
Two loss shapes run per security mode (--failover-modes None,SignAndEncrypt,
--failover-loss kill,freeze):
kill— SIGKILL; the OS closes the primary's sockets, so the client sees the loss at once.freeze— SIGSTOP; the sockets stay open and silent (hung host / dead link), so only the consumer's session keep-alive can detect it.
ScadaBridge does not read ServiceLevel/ServerRedundancy: its failover is
endpoint-level (primary/backup connection details), so two independent fixture
servers exercise exactly what it consumes. rig-failover.sh <Probe.dll> <dir>
runs the same probe against the real local docker-dev pair (central-1 :4840,
central-2 :4841), docker kills central-1 once the primary is steady and
starts it again afterwards. The rig admits anonymous sessions read-only and has
no active native alarm, so that variant checks values and ConditionRefresh
completion only. Local docker-dev only; never point it at a wonder box.
Run 2026-11-12 (ScadaBridge#125) with ScadaBridge 94ae1860 (SDK client
1.5.378.106), ACL off, against the SDK 1.5.378.182 baseline (83ed880e) and
master a28adba2 (SDK 2.0.1): matrix 11/11 in None, Sign and SignAndEncrypt on
both, all 8 trust controls failed closed (0x80130000 / 0x801A0000); failover 8/8
legs (11/11 cases each). Loss-to-backup timings are the same on both versions:
kill about 20 s (three 5 s reconnect attempts, then the backup), freeze about
20 s to detect and 86–130 s to reach the backup, because each reconnect attempt
waits out a connect timeout against the silent primary. rig-failover.sh passed
9/9 against the docker-dev pair (image SDK 2.0.1), central-1 killed, about 20 s
to central-2.
Run 2026-11-12 (ScadaBridge#125) with the consumer itself on SDK 2.0.1
(ScadaBridge 731d1ed7, on main as a2e9d718; the DCL sends UserName passwords in the
legacy RSA format), 65-character key, against the SDK 1.5.378 baseline
(83ed880e) and master 412da27c (SDK 2.0.1): matrix 11/11 in None, Sign and
SignAndEncrypt on both with the ACL off and on, all 8 trust controls failed
closed (0x80130000 / 0x801A0000), failover 8/8 legs. kill reaches the backup
in about 20 s as before; freeze is now detected in about 15 s (SDK 2
keep-alive; about 20 s on the SDK 1.5 client) and reaches the backup in 81–125 s.
A mutation run with the consumer's UserName identity reverted to the SDK 2
default handler fails every trusted leg against the baseline with
BadIdentityTokenInvalid (0x80200000) and passes against master, which is
lmxopcua#880 reproduced from the consumer side.