1e1fad76c4
v2-ci / unit-tests (push) Has been cancelled
Since #808 an UNS folder with no placement in the generation is denied (fail closed), so the matrix's ad hoc "Eq" folder made every UNS case fail with BadUserAccessDenied on the candidate server. The fixture now supplies unsFolderScopes (+ the alarm's tagReferencingEquipment) as a deployed generation does. An unplaced "Unplaced" UNS folder is kept as a negative control: the Shared client leg requires it denied against a #808+ server with the ACL on. No server authorization code changed. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
100 lines
6.1 KiB
Markdown
100 lines
6.1 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). 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.
|