2705f16670
v2-ci / unit-tests (push) Waiting to run
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
117 lines
7.3 KiB
Markdown
117 lines
7.3 KiB
Markdown
# 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:
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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 kill`s 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.
|