mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
fuzzing
1029 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ae34b02938 |
Fuzzing coverage expansion (Full Encoders / Certificates / Network) (#3867)
# Description
Broadens fuzz coverage across the highest-value untrusted-input surfaces
in the stack, refactors the per-area scaffolding so adding a new fuzz
area is a copy-paste-replace operation, and integrates everything into
`UA.slnx`, `Fuzzing/Fuzzing.md`, and `Docs/README.md`.
**Totals**: 143 files, +4961 / −289. Source plan:
`plans/27-fuzzing-coverage-expansion.md` (added in this PR).
### Shared scaffolding (refactor, no behaviour change)
* Extract generic NUnit replay harness from
`Encoders/Fuzz.Tests/EncoderTests.cs` into
`Fuzzing/common/Fuzz.Tests/{FuzzTargetTestsBase,FuzzTargetFunction,TestcaseAsset,TestAssetUtils}.cs`.
* Each area's `Fuzz.Tests` becomes a thin subclass binding its own
`FuzzableCode` type.
* Hardcoded testcase-suffix arrays replaced with `Testcases.*` directory
discovery at runtime.
* New `Fuzzing/scripts/fuzz-menu.ps1`: dynamic target menu enumerating
`FuzzableCode` statics via reflection — no more hardcoded target lists
in `libfuzz.bat` / `aflfuzz.sh`.
### Encoders area hardening
* **Bug fix**: `AflfuzzXmlEncoder` was re-encoding via `JsonEncoder`;
now uses `XmlEncoder` (the `LibfuzzXmlEncoder` variant was already
correct).
* Add idempotent JSON / XML targets mirroring the existing Binary
idempotent pattern.
* Add compact-JSON encoder option coverage (`JsonEncoderOptions.None`
alongside `Verbose`).
* New `FuzzableCode.BuiltInTypes.cs` (48 targets): `NodeId`,
`ExpandedNodeId`, `Variant`, `ExtensionObject`, `DataValue`,
`DiagnosticInfo`, `QualifiedName`, `LocalizedText` × Binary/JSON/XML
decoders.
* Enrich `Encoders.Testcases.cs` seed corpus with `BrowseRequest` +
`WriteRequest`.
* New `Fuzzing/dictionaries/binary.dict`.
### String parsers (folded into Encoders area)
* New `FuzzableCode.Parsers.cs` (12 targets): `NodeId.Parse`,
`ExpandedNodeId.Parse`, `RelativePathFormatter.Parse`,
`QualifiedName.Parse`, `NumericRange.Parse`, `Uuid` round-trip (both
afl-string and libFuzzer-span signatures).
* New `Fuzzing/dictionaries/nodeid.dict`.
### Certificates / CRL / ASN.1 (NEW area)
* New `Fuzzing/Certificates/{Fuzz,Fuzz.Tests,Fuzz.Tools}` referencing
`Opc.Ua.Security.Certificates` + `Opc.Ua.Core`.
* Targets (20 total): `X509CRL(byte[])` → `EnsureDecoded`,
`X509SubjectAltNameExtension` / `X509AuthorityKeyIdentifierExtension` /
`X509CrlNumberExtension` raw-ASN.1 decoders,
`PEMReader.ImportPublicKeysFromPEM` / `ImportRsaPrivateKeyFromPEM`,
`Pkcs10CertificationRequest`, `AsnUtils.ParseX509Blob` + `AsnReader`
sequence/integer helpers.
* `Certificates.Testcases.cs` builds an RSA cert / CRL / CSR with the
existing builders and emits raw DER + PEM seeds.
* New `Fuzzing/dictionaries/asn1.dict` (DER tags, context-class tags,
length-form sentinels, OID prefixes).
### Network area via `Opc.Ua.Bindings.Pcap` (NO Core changes, NEW area)
* New `Fuzzing/Network/{Fuzz,Fuzz.Tests,Fuzz.Tools}`, multi-targeted
`net8.0;net9.0;net10.0` to match the pcap binding matrix (excluded from
net48/net472).
* **Stateless targets**: `OpcUaFrameParser.Process` (TCP segment → UA-SC
chunk splitter), `TcpStreamReassembler.Process` (raw TCP reassembly),
`OfflineSecureChannel.ReadChunk` (UA-SC symmetric decrypt + verify via
`UaSCUaBinaryChannel.ReadSymmetricMessage` → every security profile
covered through the binding), `ServiceCallReassembler.Push` (chunk →
decoded service call).
* **Stateful libFuzzer-only targets**: `MockServerReplay` /
`MockClientReplay` drivers in a separate target list so cheap stateless
targets dominate throughput.
* Deterministic test-only `ChannelKeyMaterial` via
`SecurityPolicies.None`.
* `Network.Testcases.cs` emits synthetic Hello/Ack/Err/Rhe/MSG seeds via
`LoopbackFrameBuilder`. Live handshake recorder is out of scope for this
PR (degraded mode documented).
* `Network.Fuzz` host does **not** instantiate `NicCaptureSource` — the
AFL/libFuzzer process never opens raw sockets.
* New `Fuzzing/dictionaries/uasc.dict` (MSG/OPN/CLO/HEL/ACK/ERR/RHE
magic + chunk-type bytes + security-policy URI prefixes) and `tcp.dict`
(Ethernet/IPv4/TCP framing markers).
### `Opc.Ua.Core` UA-SC parse seam (complements 4a)
* New `Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageParsers.cs` — `internal
static` helpers for the pre-crypto, pre-auth chunk surface NOT covered
by the pcap binding: `TryParseChunkHeader`, `ReadHelloMessage`,
`ReadAcknowledgeMessage`, `ReadErrorMessage`, `ReadReverseHelloMessage`,
`ReadAsymmetricMessageHeader`.
* **No production-code refactor** (Strategy B): the new internal helpers
are an independent parse seam, not a refactor of the entangled private
channel methods. Existing `ProcessHelloMessage` /
`ProcessReverseHelloMessage` / asym-header decode paths are untouched.
Behaviour is byte-identical (verified — Opc.Ua.Core.Tests Tcp subset 7/7
passing).
* Surface via `<InternalsVisibleTo Include="Network.Fuzz" />` (+
matching Tools and Tests entries because the linked
`FuzzableCode.Transport.cs` compiles from all three host projects).
* New `Fuzzing/Network/Fuzz/FuzzableCode.Transport.cs` (12 targets) +
`Fuzzing/Network/Fuzz.Tools/Transport.Testcases.cs` (seeds under
`Testcases.Tcp.*`).
### Integration
* `UA.slnx`: add the 6 new fuzz projects, 4 new shared harness `.cs`
files, 5 new dictionaries, and the new `fuzz-menu.ps1` script — all
under the right `/Fuzzing/<Area>/` solution folders.
* Rewrite `Fuzzing/Fuzzing.md`: areas table, per-area dictionaries,
dynamic target-menu, step-by-step "how to add a new fuzz area" recipe,
Network area's replay-driven seed pipeline + key-material discipline,
pcap binding dependency note.
* Cross-link from `Docs/README.md` to `Fuzzing/Fuzzing.md`.
* Fix `.Order()` → `.OrderBy()` in shared scaffolding
(`.Order(IComparer)` is net7+; the harness compiles on net48/net472 for
the Encoders + Certificates test matrices).
## Verification
| Project | TFM | Tests | Result |
|---|---|---|---|
| `Encoders.Fuzz.Tests` | net10.0 | 4131 | ✅ 0 failed |
| `Certificates.Fuzz.Tests` | net10.0 | 220 | ✅ 0 failed |
| `Network.Fuzz.Tests` | net10.0 | 330 | ✅ 0 failed |
| **Total** | | **4681** | **✅ 0 failed** |
* Full `dotnet build UA.slnx` (all TFMs, including
net48/net472/net8/net9/net10): **0 errors**, warning count unchanged
from baseline.
* `Network.Fuzz` multi-TFM build: **net8.0 + net9.0 + net10.0** all
green.
* `Opc.Ua.Core` build: **0 errors, 0 warnings** after Phase 4b seam
addition.
## Notes for reviewers
* **Base branch**: `channelrefinements2`, not `master`. The pcap binding
(`Stack/Opc.Ua.Bindings.Pcap`) only exists on the channelrefinements
line.
* **Real keylog / key material**: never committed. All Network seed
corpora use deterministic material generated from fixture test
certificates via the binding's own replay helpers.
* **`.der` test assets**: force-added with `git add -f` (consistent with
the existing intentional exception at
`Tests/Opc.Ua.Security.Certificates.Tests/Assets/OPC Foundation Root CA
[...].der`); the repo-wide `*.der` gitignore otherwise excludes them.
* **Out of scope** (documented as fold-ins for future work):
`PcapFileReader` / `PcapNgFileWriter` / `UaKeyLogTextReader` /
`UaKeyLogJsonReader` defensive parser fuzzing; live handshake recorder
for `Network.Testcases.cs`; real loopback replay drivers (current
implementation uses synthetic replay).
## Checklist
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(Fuzz
suites verified on net10.0: 4681/4681 passing. Full UA.slnx build green
on all TFMs. Full repo-wide test run not yet executed on net48 —
pre-existing tests unaffected by these changes; only
`Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageParsers.cs` is new and is gated
behind `<InternalsVisibleTo>`.)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
|
||
|
|
b32a6a73b3 | PR feedback: reword security-bar prose, drop SAST paragraph, collapse empty doc lines, add /// <inheritdoc/> on EncryptedKeyLogStream overrides | ||
|
|
e3e7134d56 |
Merge branch 'channelrefinements' into channelrefinements2
Resolved NodeSetExportTools.cs conflict by adopting base branch's ResolveExportPath / ExportRoot abstraction (configurable via OPCUA_MCP_EXPORT_ROOT env var) and removing my redundant ResolveAndValidateExportPath helper + NodeSetExportPathValidationTests.cs. Base implementation is functionally a superset of F19's hard-coded LocalAppData approach. |
||
|
|
e8afa4237b |
Security hardening: 25-finding pass against SFI / SDL / OPC UA Part 2 baseline
Addresses 25 of 39 findings from a branch security review (assessment 5). CRITICAL: F1 gate MCP key-disclosure tools, F2 0600 keylog/pcap files, F3 per-user BaseFolder, F16 ChannelKeyMaterial IDisposable + ZeroMemory, F17 PcapFileReader MaxPacketBytes (64MB) bound, F18 FlowBuffer MaxBufferBytes (256MB) cap, F19 NodeSet export path validation, F20 ReplaySession listenPort range. HIGH: F4 IPcapAuditSink + LoggerPcapAuditSink + rate-limited frame-capture audit, F5 AES-256-GCM EncryptedKeyLogStream + SessionKeyManager (sibling .key file 0600), F14 IKeyEscrowProvider abstraction + DiskKeyEscrowProvider default (DI TryAddSingleton so consumers can route to KMS), F21 ChannelManagerOptions.MaxChannels (256) cap, F22 CaptureSessionManager.SessionFolder path-traversal guard, F23 PacketDecodeTools pcapPath/keyLogPath validation, F24 replay_pcap speed validation (NaN/Inf/<=0), F25 explicit permissions blocks on .github/workflows. MEDIUM: F6 OnTokenActivated [EditorBrowsable(Never)] + key-disclosure doc, F7 IDiagnosticsChannelMutation accessor + stack-walk caller guard (replaces internal OfflineLoadTokens), F8 PcapOptions.AllowMockClientReplay + AllowedReplayEndpoints, F9 PcapFileWriter / UaKeyLog*Writer rotation (.NNN suffix) with MaxBytes / MaxArtifactsPerSession, F13 keylog-isolation gap acknowledgment doc, F15 HashChainedAuditFileSink (HMAC-chained JSONL ledger, VerifyChain), F11 replay URL scheme allow-list (opc.tcp/opc.tcps/opc.https). LOW/INFO: F10 SBOM/CVE governance comments on PacketDotNet/SharpPcap + Docs/PacketCapture.md, F12 Security model section at top of PacketCapture.md. Tests: 100+ new NUnit tests across Audit/, Capture/, Frame/, KeyLog/, McpServerTools/, Replay/, Stack/Tcp/, DependencyInjection/. All file-mode/HMAC-chain/encryption tests are [Platform(Linux,MacOSX)]-gated where Unix-specific. Build: 0 warnings / 0 errors on Stack/Opc.Ua.Bindings.Pcap for net8.0 and net9.0. net10.0 build is environmentally blocked by a corrupted local SDK 10.0.300 workload manifest (microsoft.net.workload.mono.toolchain.net6 10.0.108 requires Emscripten.net6 >=10.0.108, only 9.0.17 installed); requires 'dotnet workload repair' from an elevated shell to validate the third TFM. No tests were run pending the SDK fix. 5 CA1508/CA2000 false positives suppressed with documented #pragma blocks (ownership transfer in escrow factory / IAsyncEnumerable iterator / unreachable catch path). Deferred to follow-up GitHub issues: 9 MEDIUM (M-SEC-07/09/10/11/12/13/16/22/31 — exception sanitization, reconnect defaults, cert-rotation races, nonce-suppress deprecation, parser hardening) and 10 LOW/INFO (L-SEC-14/15/17/23/24/33/34/35/36/37 — handler cleanup, EventSource endpoint masking, key-log schema validation, EOL deps, SAST). Full reasoning in session plan.md Round 10. |
||
|
|
04f3d12fa8 |
[security] Bound the participant metric tag to per-kind cardinality (MEDIUM-4)
plans/security-assessment.md MEDIUM-4 (unbounded metric cardinality):
opcua.channel.participant.timeout.count and
opcua.channel.participant.recreate.count emit a "participant" tag
whose value was the full per-instance IReconnectParticipant.Id. Two
problems:
- ClientChannelReconnectParticipant uses
"{idPrefix}-{Guid.NewGuid():N}" — the suffix made every instance
permanent in cardinality-retaining metric backends (Prometheus,
OTLP, Application Insights).
- Session.ChannelManager.cs:45 used a bare Guid with NO prefix at
all, so the metric tag was a 32-hex-char string with no way for
operators to bucket the data.
Fix:
- Session.ChannelManager.cs:45 — prefix the GUID with "Session-" so
Session participants follow the same prefix-then-suffix shape as
ClientChannelReconnectParticipant.
- ClientChannelManagerMetrics — add a GetParticipantKind helper
that returns everything before the first '-' (or the full id if
no '-'). Use it in CreateEndpointParticipantTags and
CreateEndpointParticipantSuccessTags so the metric tag stays at
per-kind cardinality ("Session", "Client", …).
- Full per-instance id continues to flow through Activity tags and
EventSource events (separate code path in
ClientChannelManagerDiagnostics) so distributed traces remain
correlatable.
- Docs/Sessions.md callout updated to describe the now-bounded
behavior (was a warning before, now describes the implemented
semantic and the convention required of custom participants).
Build verified: Stack/Opc.Ua.Core, Libraries/Opc.Ua.Client net10.0 —
0 W, 0 E.
|
||
|
|
25dc407917 |
[security] ClientChannelManagerCertRotation: refuse rotation events that cannot be matched to this application's identity (MEDIUM-3)
plans/security-assessment.md MEDIUM-3 (cert-identity confusion):
CertificateIdentifierMatches previously returned true unconditionally
when the ApplicationCertificate had neither Thumbprint nor RawData
configured (the common StorePath + SubjectName configuration). With a
shared CertificateManager, any rotation event whose type matched the
configured certificate type would be adopted — including events
intended for a completely different application that just happens to
share the same manager.
Add a SubjectName-based fallback BEFORE the unconditional accept, and
flip the final fallback to refuse:
1. Match on Thumbprint if configured (existing — unchanged)
2. Match on RawData if configured (existing — unchanged)
3. NEW: match on SubjectName via X509Utils.CompareDistinguishedName
against the old or new certificate's Subject
4. If none of the above are configured, REFUSE the rotation event
and emit a LogWarning telling the operator to configure at least
ApplicationCertificate.SubjectName so cert rotation can match
securely
The SubjectName comparison uses CompareDistinguishedName which is
case-insensitive and order-tolerant, matching how
CertificateValidationCore validates subject identities elsewhere in
the stack.
The static helper now takes an ILogger? parameter so the refusal
warning can be emitted without further plumbing.
Build verified: Stack/Opc.Ua.Core net10.0 — 0 W, 0 E.
|
||
|
|
d0ef235286 |
RetryBudget: swap Reset() write order to avoid stale-tick race
Previous Reset(): Volatile.Write(ref m_started, 0); Interlocked.Exchange(ref m_startTicks, kNotStarted); A concurrent EnsureStarted() observer could see m_started == 0 (taking the "reset, so go start" branch), then read the stale m_startTicks value BEFORE the Interlocked.Exchange landed, and treat the budget as already-started against the pre-reset timestamp. IsExhausted / TryConsume then computed elapsed time from the old origin. Fix: reset m_startTicks first, then m_started. EnsureStarted writes m_started = 1 only after the CAS on m_startTicks succeeds, so resetting m_startTicks first preserves that invariant — no observer can see "started == 1" with a stale tick value. |
||
|
|
2a4f4f6014 |
ManagedTransportChannelLease: non-blocking sync Dispose (deadlock under sync context)
Sync Dispose previously called
DisposeAsyncCore().AsTask().GetAwaiter().GetResult()
which ultimately performs network I/O (TCP FIN handshake) via
ChannelEntry.TearDownAsync → underlying.CloseAsync. On any host that
captures a SynchronizationContext (legacy ASP.NET, WPF, WinForms)
synchronous Dispose on the UI/request thread deadlocks. Even without
a sync context, a thread-pool thread is pinned on network I/O for the
close duration.
New Dispose() body:
- Atomically marks the lease released via
Interlocked.Exchange(ref m_active, 0) (idempotent — second call
short-circuits, so lease accounting cannot double-release)
- Pushes the actual Entry.ReleaseLeaseAsync(this) onto the thread
pool via Task.Run and returns IMMEDIATELY
- Swallows the async failure inside the fire-and-forget (sync Dispose
cannot surface async exceptions)
CloseAsync / DisposeAsyncCore (the async path) are unchanged and still
await the full teardown chain. Documented on Dispose() that callers
needing teardown observability must use CloseAsync().
|
||
|
|
39eb086443 |
ChannelEntry: track tightest IRetryBudget across coalesced joiners; ClampDelayToBudget signals exhaustion
Two related correctness fixes for the channel-reconnect path. 1. ChannelEntry.RequestReconnectAsync coalescing budget When multiple callers concurrently invoke RequestReconnectAsync, the first becomes the "starter" and its budget is passed to the cycle. Subsequent "joiners" were silently dropping their budget parameter — so a joiner with a strict 10-second deadline could not tighten an in-flight cycle that the starter began without one. Fix: add an m_effectiveBudget field guarded by m_lock. Every caller (starter + joiners) updates it via TighterOf(current, candidate), which returns the budget with smaller remaining time (null = "no constraint", so a null joiner never relaxes). The cycle re-reads GetEffectiveBudget() at the top of every iteration AND immediately after the inter-attempt delay, so a late joiner can preempt an in-flight sleep. The finally block clears m_effectiveBudget so the next cycle starts unconstrained. XML doc on RequestReconnectAsync(CancellationToken) describes the coalescing semantics in plain language. 2. ChannelReconnectPolicyBudget.ClampDelayToBudget exhaustion semantics For a custom IChannelReconnectPolicy that does NOT also implement IBudgetAwareChannelReconnectPolicy, when the shared retry budget was exhausted the helper returned the ORIGINAL delay unchanged instead of signaling "stop retrying". Downstream ChannelEntry.RunReconnectCycleAsync independently checks budget.IsExhausted so no extra reconnect attempt was issued, but the bug wasted one delay cycle (up to MaxDelay = 30 s default) before the fault transition was detected. Fix: return Timeout.InfiniteTimeSpan when TryConsume returns false — matching ExponentialBackoffChannelReconnectPolicy.GetDelay. |
||
|
|
03df79835a |
[security] ClientChannelManagerDiagnostics: do not leak inner-exception detail to Activity tags or EventSource
Previously CompleteReconnectActivity, EmitReconnectFailed and
EmitStateChanged passed ServiceResult.ToString() / GetErrorMessage
(which delegated to ToString) to:
- activity?.SetTag("error.message", ...)
- ChannelManagerEventSource.ReconnectFailed / StateChanged
ServiceResult.ToString() serializes StatusCode, SymbolicId,
LocalizedText, AdditionalInfo (often inner-exception messages including
file paths and stack traces) and InnerResult. These tags are exported
to distributed-tracing backends where operators with telemetry-read
access can see internal client/server diagnostics they should not.
Replace with GetSafeErrorMessage(ServiceResult?) which returns ONLY
OPC UA-protocol-level fields:
- error.LocalizedText.Text if present and non-empty
- else error.SymbolicId ?? string.Empty
The "error.status_code" Activity tag (StatusCode only) is preserved —
that is safe to expose. Full ServiceResult contents (AdditionalInfo,
inner .NET exception data) continue to flow through local ILogger debug
logs only; never through Activity tags or EventSource events.
Type-level XML doc on the class is updated to record the contract.
|
||
|
|
5ca95abd0d |
[security] HttpsTransportChannel: refuse factory path when OPC UA cert validation is required
Security-review subagent finding (HIGH, confidence 9/10).
When consumers used the new DI flow services.AddOpcUa().AddClient(...),
the channel took the IHttpClientFactory branch and got back a named
HttpClient configured purely with AddStandardResilienceHandler() — and
the underlying HttpClientHandler was NOT exposed for HttpsTransportChannel
to attach:
- ServerCertificateCustomValidationCallback wired against the OPC UA
CertificateValidator (so the OPC UA trust list was dropped — falling
back to the OS trust chain only)
- The OPC UA application instance client certificate (so OPC UA mTLS
was silently disabled)
- AllowAutoRedirect = false (HTTPS redirects were now followed)
- MaxConnectionsPerServer / MaxRequestContentBufferSize quotas (no
longer enforced at the HTTP layer)
A server presenting a publicly-trusted CA-issued cert (CA misissuance,
domain takeover, compromised CA) would be accepted while the OPC UA
trust list — the proper authority for OPC UA — was ignored.
Fix:
- CanUseHttpClientFactory() now refuses the factory path whenever the
channel quotas carry a CertificateValidator (i.e. whenever OPC UA
server-cert validation is required), unconditionally routing to
CreateDirectHttpClient() which wires the OPC UA server-cert callback
and the OPC UA mTLS client cert.
- CreateHttpClient() emits a one-time LogWarning when a non-Shared
factory was supplied but bypassed for security reasons, so operators
see that the named HttpClient pipeline (e.g. Polly resilience handler)
is NOT applied to this OPC UA HTTPS channel.
- DefaultOpcUaHttpClientFactory.Shared continues to take the direct
path as before (no behavior change for non-DI consumers).
Verified locally:
- Stack/Opc.Ua.Core builds clean on net48 and net10.0 (0 W, 0 E)
- Libraries/Opc.Ua.Client builds clean on net10.0 (0 W, 0 E)
- OpcUaHttpClientFactoryTests on net8.0: 4/4 pass
- Https | Transport | ClientChannelManager filter on net8.0: 112/112 pass
|
||
|
|
9c5f875695 |
Honor ShutdownToken in ChannelEntry reconnect loop (PR #3852 feedback)
@koepalex (copilot finding): When ClientChannelManager.DisposeAsync()
is called while a reconnect cycle is active, the loop continued
forever as a background task. It passed CancellationToken.None to
DelayAsync, EnsureTransportConnectedAsync and NotifyParticipantsAsync,
and never observed the manager's shutdown token. With the default
policy (MaxAttempts = int.MaxValue, infinite budget) the loop never
terminated — it kept retrying with up to 30 s backoff, repeatedly
hitting ObjectDisposedException.
Thread m_host.ShutdownToken through RunReconnectCycleAsync:
- capture shutdownToken at the top of the cycle
- ThrowIfCancellationRequested at the top of every while iteration
- pass shutdownToken to DelayAsync / EnsureTransportConnectedAsync /
NotifyParticipantsAsync
- narrow the per-attempt catch around DelayAsync so it only swallows
OperationCanceledException when shutdown is NOT requested
- re-throw OperationCanceledException from the per-attempt catches
around EnsureTransportConnectedAsync and NotifyParticipantsAsync
when shutdown IS requested, so they don't get reclassified as
transient and re-loop
- also re-throw OCE from the inner catch in
EnsureTransportConnectedAsync's ReconnectAsync branch when ct is
cancelled, so we don't fall through to CreateTransportChannelAsync
during shutdown
The outer try/catch already converts the OCE to tcs.TrySetException
and the finally block records duration metric, completes the activity
and tears down the entry — all on the existing well-tested path.
Production behavior pre-fix was unchanged when shutdown was not
requested; only the disposal path is affected.
Verified locally (net10.0):
- All ClientChannelManager* tests pass (29/29)
- All Channel*/Reconnect* tests pass (73/73)
|
||
|
|
297f71cfbe | Merge branch 'channelrefinements' into channelrefinements2 | ||
|
|
392ad0d3f6 |
Merge origin/master into channelrefinements
Conflict resolved in Libraries/Opc.Ua.Client/Session/ConnectionStateMachine.cs:
adjacent doc-comment edits. Kept channelrefinements' new
<maxTotalReconnectTime> param doc and adopted master's shorter
<see cref="TimeProvider"/> form (System namespace already imported).
Verified post-merge:
- Libraries/Opc.Ua.Client: net10.0 build clean (0 W, 0 E)
- Applications/Quickstarts.Servers: net10.0 build clean (inline NodeId
fix for SecurityGroups/KeyPushTargets in
ReferenceServerConfigurationNodeManager survived auto-merge)
- Tests/Opc.Ua.Client.Tests: net10.0 build clean (only pre-existing
master CA2000 warnings in StaticIssuerKeyResolverTests)
|
||
|
|
a81d981755 |
dotnet format sweep (whitespace, IDE, RCS) + code-style enforcer agent fix (#3865)
# Description
Pure code-style sweep across the repo — no behavioral changes. Drives
`dotnet format` info-level diagnostics down on a large slice of the
codebase (Stack, Libraries, Tests, Tools).
Six commits, applied bottom-up in safety order so each batch leaves the
build green:
1. **`style: fix whitespace and final-newline formatting (WHITESPACE,
FINALNEWLINE)`**
197 hits across ~30 files; pure mechanical cleanup.
2. **`style: apply dotnet format style fixes (IDE rules)`**
IDE-rule batch via `dotnet format style --severity info` —
`var`/explicit type, simplify names, collection expressions, expression
bodies, unused usings, etc.
3. **`style: apply dotnet format analyzers fixes (RCS blank-line
formatting)`**
Roslynator formatting (RCS0xxx) blank-line and punctuation rules.
4. **`style: convert comments to doc comments and inline local vars
(RCS1181, RCS1124)`**
24 files. RCS1181 promotes `// summary`-style comments above
public/internal members to `/// <summary>` doc comments; RCS1124 inlines
locals used exactly once. Generated `Tools/` files were reverted to keep
the source-generator outputs deterministic.
5. **`docs(agent): correct --diagnostics flag form and verify Roslynator
4.x code fixers`**
Updates `.github/agents/opc-ua-codestyle-enforcer.agent.md` with two
corrections discovered while running this sweep:
- `dotnet format --diagnostics "RULE1 RULE2"` (single quoted,
space-separated) silently fixes nothing on `dotnet format` 10.x — it
parses as one unknown ID and exits 0. The correct form is `--diagnostics
RULE1 --diagnostics RULE2 …`, repeated per rule.
- Roslynator 4.15.0 **does** ship code fixers for
RCS1078/1118/1124/1140/1142/1181/1221/1260 (the previous "no auto-fixer"
list was outdated). RCS1140/1142 default to `Hidden` so need `--severity
info` to surface and fire; RCS1078 requires
`roslynator_empty_string_style = field` (already set in
`.editorconfig`). Only NUnit4002/2046/2010 genuinely lack auto-fixers in
this stack.
6. **`style: format new code from master merge (dotnet format sweep)`**
After merging master into the branch (commit `e3ebc8742`), re-ran the
same three-phase format sweep on the 151 .cs files brought in by the
merge, scoped per project via repeated `--include`. 81 files changed;
same rule families as commits 1–4.
- Hand-fixed `Tests/Opc.Ua.MigrationAnalyzer.Tests/AnalyzerHarness.cs`
for `RCS1174` (redundant `async`/`await` removal) because the auto-fixer
didn't apply for that specific case.
- Reverted files that the formatter would break:
- `Libraries/Opc.Ua.Server/Server/ServerInternalData.cs` — multi-TFM
analysis disagreement injected `<<<<<<<` / `=======` / `>>>>>>>`
markers.
-
`Tools/Opc.Ua.MigrationAnalyzer.Core/{Core/Stack/Client/ChannelBase.cs,
Core/Stack/Types/IUserIdentityTokenHandler.cs,
Client/Session/Session.cs}` — IDE0005 removed `using
Opc.Ua.Security.Certificates` that IS used (the files are 1.5.378
reference fixtures for the analyzer).
- `Tests/Opc.Ua.Gds.Tests/PushTest.cs` — IDE0002 over-simplified
`OpcUa.ObjectTypeIds` to `ObjectTypeIds`, which then resolves to a
conflicting type.
### What this PR does **not** touch
- No behavioral or semantic logic changes.
- No `.editorconfig` rule promotions (rules stay at their current
severities; this is a backlog-burndown pass, not a rule tightening).
- Generated source under `Tools/` is intentionally excluded.
- Higher-risk rule families (`RCS1090`/`CA2007` `ConfigureAwait`,
`RCS1249` null-forgiving cleanup, `IDE0270`/`IDE0059`) are left for
follow-up sweeps because they need per-TFM build verification or
semantic review.
## Related Issues
_No tracking issue — this is a non-functional style sweep and the
agent-doc correction is a follow-up to the same work. Happy to open one
if the maintainers prefer._
## Checklist
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage. _(N/A — style-only, no
behavioral change)_
- [x] I have added all necessary documentation. _(agent doc updated with
the two corrections)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. _(verified clean on `net10.0` and `net48`; only
pre-existing `NETSDK1005`/`NETSDK1207` failures on Tools/AOT projects)_
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(build
verified on net10 and net48 per batch; full test suite not yet run
locally — relying on CI)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
---------
Co-authored-by: agent <agent@local>
|
||
|
|
51e7f42bd7 |
Fix SwapFaultedEntryAsync: re-activate released lease before reattach
CI failure (Windows net10 test-windows-latest-Sessions, GitHub run 27218339487): ChannelManagerExhaustionEscalatesAndRecoversWhenServerReturns fails with Opc.Ua.ServiceResultException : Cannot attach a released channel lease. at Opc.Ua.ChannelEntry.ReattachParticipant (...:235) at Opc.Ua.ClientChannelManager.SwapFaultedEntryAsync (...:741) Root cause: when the original ChannelEntry tears down during its Faulted disposal it calls MarkReleased() on every attached lease. On the next manager.ReconnectAsync(channel, ct) call the manager correctly identifies the entry as Faulted and dispatches SwapFaultedEntryAsync to swap to a fresh entry, but the ReattachParticipant guard rejects the now-released lease. The released-state is purely the manager's bookkeeping that the original entry is gone; the lease handle the caller holds is still valid. The swap is conceptually re-establishing the lease against a fresh entry, so re-activating it is the correct semantic. Adds ManagedTransportChannelLease.MarkActiveForSwap() and calls it from SwapFaultedEntryAsync after the fresh entry has opened and before the ReattachParticipant call. This is safe under SwapFaultedEntryAsync's invariants because the caller holds the only lease reference. Verified locally: - net10: ChannelManagerExhaustionEscalatesAndRecoversWhenServerReturns passes 1/1 - net48: ReconnectAsyncSwapsFaultedLeaseEntryAsync (the unit-test variant) passes 3/3 with no race - 9/9 Sessions.Tests ChannelManager filter passes - 1/1 Stress.Tests FaultedEntryRecovery passes |
||
|
|
775ddd7591 | Merge branch 'channelrefinements' into channelrefinements2 | ||
|
|
ce4a1b0b62 |
Fix ClientChannelManager.DelaySwapAsync to honor TimeProvider on net48
CI failure: Tests/Opc.Ua.Client.Tests ReconnectAsyncSwapsFaultedLeaseEntryAsync
times out on net48 because DelaySwapAsync had a #if NET8_0_OR_GREATER
guard that fell through to wall-clock Task.Delay(delay, ct) on
net48/netstandard2.1 — so the test's FakeTimeProvider.Advance(100ms)
could not release the 100ms back-off, and the test hung past its
WaitAsync(5s) bound.
Switch to m_timeProvider.Delay(delay, ct) which uses the repo's
existing Stack/Opc.Ua.Core/Types/Utils/TimeProviderExtensions.cs
polyfill: it delegates to Task.Delay(TimeSpan, TimeProvider, CancellationToken)
on net8+ and uses TimeProvider.CreateTimer-based implementation on
older TFMs. Matches the pattern already used by
DirectoryCertificateStore (line 510, 914) and ChannelEntry (line 470).
Verified locally on net48: ReconnectAsyncSwapsFaultedLeaseEntryAsync
passes 1/1 in ~1s after the fix.
The other CI failure on this PR — Server.Tests:AsyncOnTickIsAwaited /
MultipleOnTickHandlersAllFire on macOS net10.0 — is a pre-existing
macOS timer-tick flake from PR #3808 (commit
|
||
|
|
0597f069eb |
PR feedback: multi-target net8/9/10, wire MaxActiveSessions, restore bindings + async dispose in tests
* Pcap nuget now multi-targets net8.0;net9.0;net10.0 to match PR description claim * Rename CaptureSessionManager.MaxActiveSessions const -> DefaultMaxActiveSessions; add instance property; new 4-arg ctor that accepts an override * AddOpcUaBindingsPcap now passes PcapOptions.MaxActiveSessions through to CaptureSessionManager (was ignored) * Fix stale Opc.Ua.Diagnostics.Pcap doc reference -> Opc.Ua.Bindings.Pcap * Docs/McpServer.md: add stop_replay + list_replays rows to tool tables * PcapBindingsTests + PcapServiceCollectionExtensionsTests: SetUp/TearDown to snapshot+restore the process-wide opc.tcp transport binding * DefaultCaptureSourceFactoryTests: convert 3 sync-over-async tests to async with await using * Add new test asserting PcapOptions.MaxActiveSessions actually flows into the resolved CaptureSessionManager |
||
|
|
f7366ada6f | Merge branch 'channelrefinements' into channelrefinements2 | ||
|
|
81acd947a6 |
Close channel-manager production gaps and eliminate Tests/.../Gaps/
Closes the three carry-forward gaps documented in
Tests/Opc.Ua.Stress.Tests/Channels/Gaps/ (formerly each documented via
an [Explicit] failing test). The realigned tests are relocated to
Channels/Contract/, the [Category("Gaps")] disappears, and the
Gaps/ folder is removed entirely.
G1. Faulted-entry recovery
Already closed implicitly by Phase E SwapFaultedEntryAsync (PR #3852).
Test realigned from "expects BadSecureChannelClosed throw" to
"auto-resets on next ReconnectAsync and reaches Ready" and renamed
FaultedEntryAutoResetsOnNextReconnectAsync. New file location:
Tests/Opc.Ua.Stress.Tests/Channels/Contract/FaultedEntryRecoveryTests.cs.
No production change required; this was a test-vs-design alignment.
G2. Bounded participant timeout
New ParticipantTimeout on IChannelReconnectPolicy. Default
Timeout.InfiniteTimeSpan via DIM on net8+/netstandard2.1; legacy TFMs
opt in via IParticipantTimeoutPolicy (mirrors the existing
IBudgetAwareChannelReconnectPolicy pattern). ExponentialBackoffChannel-
ReconnectPolicy sets a sensible default of 30 seconds.
ChannelEntry.ReactivateParticipantsAsync wraps the participant's
OnReconnectAsync in WaitAsync(timeout, ct); on timeout the participant
is reported as TransientFailure for this cycle (the channel-level
retry policy retries normally; eventually escalates to Faulted via
the existing MaxAttempts path). A new participant.timeout.count
metric is emitted per timeout.
Test realigned from "documents indefinite hang" to "times out within
bounded wait and surfaces as TransientFailure", renamed
HungParticipantTimesOutAfterBoundedWaitAsync, and merged into
Tests/Opc.Ua.Stress.Tests/Channels/Contract/HungParticipantTests.cs.
The sibling Contract/HungParticipantTests.HungParticipantBlocks-
ReconnectIndefinitely is also realigned + renamed to
HungParticipantTimesOutAndOtherParticipantsRecoverAsync. A new
positive contract test (BoundedParticipantTimeoutHonorsTimeoutAsync)
guards against false positives. A new live-server integration test
exercises the timeout via a real ManagedSession in
Tests/Opc.Ua.Sessions.Tests/ChannelManagerSessionLifecycleIntegration-
Tests.cs.
G3. RequiresSessionRecreate plumbing
New RecreateAsync(ct) on IReconnectParticipant. Default no-op via DIM
on net8+/netstandard2.1; legacy TFMs opt in via
IRecreateAwareReconnectParticipant. ChannelEntry's switch on
RequiresSessionRecreate now fire-and-forgets a DispatchRecreate(participant)
task that invokes RecreateAsync; the manager does NOT block its
transition to Ready waiting for the recreate (matches the existing
enum doc that says "the participant is responsible for completing
its own recreation out of band"). New participant.recreate.count
+ participant.recreate.failure.count metrics emit per dispatch.
Session.ChannelManager.cs (the Session's IReconnectParticipant
adapter) overrides RecreateAsync to delegate to the existing
Session.RecreateInPlaceAsync — this is the actual "wired-through"
deliverable flagged as PR #3852 carry-forward.
Test realigned from "documents that nothing is invoked" to "fire-
and-forget invokes RecreateAsync exactly once", renamed
RequiresSessionRecreateInvokesRecreateAsync, moved to
Tests/Opc.Ua.Stress.Tests/Channels/Contract/RecreateDispatchTests.cs.
The 1 remaining [Ignore]d integration test from PR #3852 is replaced
by a positive integration test in
Tests/Opc.Ua.Sessions.Tests/ChannelManagerSessionLifecycleIntegration-
Tests.cs that simulates BadSessionIdInvalid on reactivation.
Cleanup
- GapTestBase.cs deleted; unique helpers folded into ContractTestBase.
- Tests/Opc.Ua.Stress.Tests/Channels/Gaps/ folder deleted.
- [Category("Gaps")] removed from the codebase.
- Tests/Opc.Ua.Stress.Tests/README.md "category overview" loses the
[Explicit] / L5 / Channels/Gaps/ row and the "Add known production
gaps to Channels/Gaps/" guidance bullet.
- Docs/Sessions.md "Participant model" subsection documents the new
RecreateAsync callback and ParticipantTimeout policy property.
- Pre-existing build-blocker in Applications/Quickstarts.Servers/
ReferenceServer/ReferenceServerConfigurationNodeManager.cs
(ObjectIds.SecurityGroups / KeyPushTargets are not on the curated
Stack/Opc.Ua.Types/Internal/ObjectIds.cs surface) is replaced with
inline NodeId literals (i=15443 / i=25440) per the docstring above
the array. This was a separate fix required for the merged tree
to build end-to-end.
Verified locally on net10.0:
- Full UA.slnx Release build (CodeQL parity): 0 errors.
- Tests/Opc.Ua.Stress.Tests Contract filter (faulted recovery,
recreate dispatch, hung participant, participant result
aggregation): 9/9 passed.
- Tests/Opc.Ua.Client.Tests ClientChannelManager filter: 29/29 passed.
- Tests/Opc.Ua.Sessions.Tests ChannelManager filter: 9/9 passed
(was 7/7+1 skipped before; the G3 RequiresSessionRecreate
integration test that was carry-forward [Ignore] is now
implemented and counted as a pass).
- No remaining Gaps namespace / Category("Gaps") / GapTestBase
references anywhere in the tree.
|
||
|
|
b30853856d |
PR feedback: expand single-line summary docs to multi-line form
Per reviewer guidance, all /// <summary>...</summary> comments must use newlines after <summary> and before </summary>. Expanded 36 single-line property summaries across 6 files in Opc.Ua.Bindings.Pcap: - Capture/CaptureSession.cs (9) - Formats/ITraceFormatter.cs (4) - Frame/OpcUaFrameParser.cs (7) - Frame/PcapFileReader.cs (4) - Frame/PcapFileWriter.cs (4) - Frame/TcpStreamReassembler.cs (8) |
||
|
|
311cefa5ff | Merge branch 'channelrefinements2' of https://github.com/OPCFoundation/UA-.NETStandard into channelrefinements2 | ||
|
|
bc840201ab |
Apply dotnet format (whitespace, style, analyzers) + fix CA2000 in tests
- Three-phase dotnet format sweep (whitespace -> style -> analyzers) scoped to files changed since merge-base with channelrefinements (5 projects) - Mostly mechanical: operator wrapping (&& -> end-of-line), fluent StringBuilder chains, using ordering, missing exception XML docs - Fix CA2000 in CapturingMessageSocketTests (using var for inner sockets + TestSinks so they're disposed even if the wrapping CapturingMessageSocket constructor throws) - Document the auto-added PcapDiagnosticsException(string?, int) constructor - Keep DecodeServiceCallsAsync's local var (return await + earlier await foreach triggers RCS1174 if inlined) Verified: - All 5 affected projects build with 0 errors - 3-phase dotnet format --verify-no-changes is clean - 53/53 Opc.Ua.Bindings.Pcap.Tests pass on net10.0 |
||
|
|
f6064e556e | Merge branch 'channelrefinements' into channelrefinements2 | ||
|
|
c23f477bc7 | Merge branch 'master' into channelrefinements | ||
|
|
15eb55c46e |
Cert rotation: force renegotiate (#3562) + auto-detect on client (#3160) (#3849)
# Description Implements the OPC UA Part 12 §7.10.9 contract on the server (fixes #3562) AND the client-side counterpart for immediate detection of own-cert / trust-list / CRL changes (fixes #3160). ## Server side (#3562) Per Part 12 §7.10.9 the server _shall_ "force existing SecureChannels affected by the changes to renegotiate" once the `ApplyChanges` method response has been delivered. Previously the server reloaded the certificate but did not cut affected channels; clients kept using the old cert until natural rekey, and the code carried an explicit `TODO` next to a 1-second `Task.Delay` hack in `ConfigurationNodeManager.ApplyChanges`. Key server changes: * **New optional capability interface** `Opc.Ua.ITransportListenerCertificateRotation` (single method `CloseChannelsForCertificate(Certificate)`) — implemented by `TcpTransportListener` and `HttpsTransportListener`. Custom transport listeners can opt in without breaking `ITransportListener`. * **TCP path**: new internal `TcpListenerChannel.TryCloseForCertificateRotation(oldThumbprint, out globalChannelId)` matches the channel's negotiated `ServerCertificate` by thumbprint, sends `BadCertificateInvalid`, and calls `ChannelFaulted()` to actually close the socket. `TcpTransportListener.CloseChannelsForCertificate` snapshots `m_channels` outside `m_lock` to avoid lock inversion, invokes the per-channel helper, returns cut global channel ids. **The listener socket stays bound** — clients can reconnect immediately. * **HTTPS path**: HTTPS owns the cert at the Kestrel/HTTP.sys listener level, so a `Stop()`+`Start()` cycle is the only practical force-renegotiate. Moved out of `CertificateUpdate` (which now does only the soft fan-out) into `CloseChannelsForCertificate` so the staging event no longer disrupts in-flight requests prematurely. * **`ConfigurationNodeManager.ApplyChanges`** rewritten: - Captures the per-group pre-transaction certificate (preserved across multi-stage transactions per Part 12 §7.10.2) and the staged `UpdateCertificateData`. - Schedules a deferred apply: waits for the response to flush, re-syncs the registry via `CertificateManager.UpdateAsync`, then calls `CloseChannelsForCertificate` on every listener that implements the capability. - **Configurable grace period**: new `IConfigurationNodeManager.ApplyChangesGracePeriod` (default 250 ms, negative clamped to zero). Hosts running over higher-latency administrative links can raise it. - Exposes `IConfigurationNodeManager.DrainPendingApplyChangesAsync(CancellationToken)` for tests/hosts to deterministically await rotation. - Removes the explicit 1-second `Task.Delay` TODO hack. * **`Opc.Ua.Server.ITransportListenerRegistryProvider`** — narrow opt-in interface on `ServerInternalData` exposing the live transport listener list. Lets `ConfigurationNodeManager` drive the fan-out without expanding `IServerInternal` (binary compat for mocked test fixtures). * **Sessions are NOT closed**. Per spec, the cut is on the SecureChannel only; existing `Session.IsSecureChannelValid` + `Session.Activate` rebind handles transfer over the new channel. ## Client side (#3160) The client SDK previously had no automatic detection of changes to its own certificate, trust list, or CRL — users had to call `Session.ReloadInstanceCertificateAsync` manually and trigger a reconnect. The spec-mandated `ApplicationCertificateUpdated`, `TrustListUpdated`, and `CrlUpdated` enum values existed but only the first was being emitted by `CertificateManager`. Key client/shared changes: * **`CertificateManager` now emits `TrustListUpdated` and `CrlUpdated`** from `WriteTrustListAsync` (per mask) and from `TrustListTransaction.CommitAsync` (after the atomic apply). The new internal `NotifyTrustListChanged` also invalidates cached peer/user/HTTPS validation cores so subsequent validations pick up the fresh trust state. * **`ManagedSession.AutoReconnectOnCertificateChange`** opt-in property (default `false` for backwards compat). When `true`, `ManagedSession` subscribes to `CertificateManager.CertificateChanges` on construction and, on the three relevant kinds: - Calls `Session.ReloadInstanceCertificateAsync` on `ApplicationCertificateUpdated` so the next ActivateSession is signed with the rotated client cert. - Triggers a reconnect via the state machine so the new state is honoured within milliseconds rather than at the next `SecurityTokenLifetime` rekey. * **`ManagedSession.ApplicationCertificateChanged`** event surfaces every observed change regardless of the auto-reconnect policy for diagnostics or custom rotation logic. * Subscription is disposed by the existing `Dispose` / `DisposeAsync` paths. ```csharp using var managed = await ManagedSession.CreateAsync( configuration, endpoint, sessionFactory); managed.AutoReconnectOnCertificateChange = true; managed.ApplicationCertificateChanged += (sender, e) => { Console.WriteLine($"observed {e.Kind} on {e.TrustList}"); }; ``` ## Tests | Suite | net10.0 | net48 | |---|---|---| | `TransportListenerCertificateRotationTests` (new, 6 tests) | ✅ | ✅ | | `CertificateManagerTests` (4 new event tests) | ✅ 22/22 | ✅ 4/4 | | `PushCertManagement` | 50/53 ✅ | 50/53 ✅ | | `GDS PushTest` (focused subset) | 15/15 ✅ | 16/16 ✅ | | `Opc.Ua.Server.Tests` (focused subset) | 38/38 ✅ | 38/38 ✅ | | `GDS PushTest` (full) | 313/338 ✅ | — | Skips are pre-existing (cert-type mismatches, optional methods) — no regressions. New integration test `PushTest.ApplyChangesForcesChannelRenegotiateAsync` positively asserts the stale channel fails with a recognized channel-invalidation status after `ApplyChanges`, and that a fresh reconnect succeeds with the rotated cert. ## Documentation - `Docs/CertificateManager.md` — new sections: - "Server-Side Certificate Rotation via Push (OPC UA Part 12 §7.10.9)" — `ITransportListenerCertificateRotation` contract + custom listener example. - "Client-Side Auto-Detection of Certificate Changes (issue #3160)" — `AutoReconnectOnCertificateChange` + `ApplicationCertificateChanged` event + the three covered scenarios. ## Related Issues - Fixes #3562 - Fixes #3160 ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran focused tests locally using the **UA.slnx** projects against .net **framework 4.8** and .net **10**, and all passed. _(Full UA.slnx test run delegated to CI.)_ - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. |
||
|
|
a3a4dc2703 |
Merge origin/capture into channelrefinements2 (round 2)
Brings in three new commits on capture: - |
||
|
|
87dcfc10cf |
Merge origin/capture into channelrefinements2
Brings the packet-capture / dissection / replay engine (Opc.Ua.Bindings.Pcap) onto the channel-refinements branch so it composes cleanly with the central IClientChannelManager work landed in PR #3852 (channelrefinements). Conflicts resolved: - Applications/McpServer/Program.cs: union of both ConfigureServices changes — registers AddOpcUa().AddClient() (channel manager) AND AddOpcUaBindingsPcap() / Formatters / Replay (capture). Channel manager picks up the capture binding automatically via the shared TransportBindings.Channels registry. - Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs: keep both m_sessionCount (channelrefinements2) and m_tokenActivated event (origin/capture). Server-side capture relies on TcpListenerChannel.OnTokenActivated to snapshot per-channel ChannelToken keys for offline decoding. - UA.slnx: keep both Opc.Ua.Di.Tests and Opc.Ua.Bindings.Pcap.Tests project entries. Pre-existing fix folded in (build-blocker introduced earlier on channelrefinements2, not by the capture merge): - Applications/Quickstarts.Servers/ReferenceServer/ReferenceServerConfigurationNodeManager.cs s_optionalPubSubFolders previously referenced ObjectIds.SecurityGroups and ObjectIds.KeyPushTargets, neither of which exists on the curated Stack/Opc.Ua.Types/Internal/ObjectIds.cs surface. Inlined the NodeId literals (i=15443 / i=25440) per the docstring above the array so the merged tree builds end-to-end across all TFMs and every transitive test project (Tests/Opc.Ua.Client.Tests, Tests/Opc.Ua.Sessions.Tests, etc.) compiles. Architectural fit ("take advantage of all the changes you made here"): 1. Pcap integrates with the central channel manager via the global TransportBindings.Channels registry. ClientChannelManager.ctor accepts an injected ITransportChannelBindings? that defaults to the global registry, so the Pcap binding installed by AddOpcUaBindingsPcap is automatically picked up by every channel created through the manager. No code wiring needed — the composition is pure layering. 2. Channel sharing transparently includes capture. When multiple participants share a single IManagedTransportChannel (the F1 refcount + key-equivalence work), the underlying CapturingMessageSocket captures all participant traffic on the same wire, automatically. 3. Transparent reconnect preserves the capture session. The F8 diagnostics events on ClientChannelManager (state-change / close notifications) and the Phase E SwapFaultedEntryAsync swap path produce fresh ChannelEntry instances under the same ManagedChannelKey — IChannelCaptureRegistry is keyed by channel identity, so a swap rolls the active observer into the new entry and the capture session continues recording across the reconnect / faulted-swap cycle. 4. MCP server composes both: services.AddOpcUa().AddClient() (channel manager + DI), services.AddSingleton<OpcUaSessionManager>(), services.AddOpcUaBindingsPcap() (capture binding). OpcUaSessionManager resolves IClientChannelManager from IServiceProvider; the Pcap tools (Tools/PacketCapture/Decode/ReplayTools.cs) resolve CaptureSessionManager and IChannelCaptureRegistry from the same provider. Verified locally on net10.0: - Stack/Opc.Ua.Bindings.Pcap build: 0 warnings, 0 errors. - Libraries/Opc.Ua.Client build (channel manager): 0 warnings, 0 errors. - Tests/Opc.Ua.Bindings.Pcap.Tests: 53/53 passed. - Tests/Opc.Ua.Client.Tests ClientChannelManager filter: 29/29 passed. - Tests/Opc.Ua.Sessions.Tests ChannelManager filter: 7/7 passed, 1 skipped (RequiresSessionRecreate carry-forward). - Applications/McpServer build: 0 warnings, 0 errors. |
||
|
|
117f2212d7 | Simplify token-activated subscription (channel is always ISecureChannel) (CA1508) | ||
|
|
f07501e0ec | Merge origin/master into capture | ||
|
|
788567ad75 | Merge branch 'master' of https://github.com/OPCFoundation/UA-.NETStandard into channelrefinements | ||
|
|
bc4c30034d |
Fix CTT conformance issues #3719, #3720, #3723, #3841 (#3851)
# Description Fixes four CTT (OPC UA Compliance Test Tool) conformance gaps that the existing NUnit parity suite was silently passing while the CTT JS scripts kept failing: | Issue | Area | Failing CTT script | |---|---|---| | #3719 | Base Info Core Structure 2 | `001.js` | | #3720 | Base Info FiniteStateMachine + StateMachine Instance | `initialize.js` + `001.js` | | #3723 | Security None | `007.js` (DoS, partially used channels) | | #3841 | Alarms & Conditions | Acknowledge Test_003, Alarm Test_002, Confirm Test_001 / Test_003 / Err_004, Enable Test_002, Non-Exclusive Limit Test_002 | Across all four, the underlying pattern was the same — the NUnit parity tests were checking weaker invariants (DataType attribute instead of encoded Variant BuiltInType; `Assert.Ignore` instead of `Assert.Fail`; stub `GetEndpoints` instead of the real DoS scenario; method-call + Read instead of subscribe-and-validate-event-payload). So the suite gave false confidence on changes that would have failed CTT. Each commit below both **strengthens the NUnit parity test** to encode the same invariant CTT verifies, **and** fixes the server/model defect that the strengthened test surfaces. ## Commits ### `a1db2984c` Fix #3719 (BaseInfoCoreStructure) - **Test**: `BaseInfoServerTests.ReadConformanceUnitsAsync` now reads the **value** of `i=24101` and asserts `WrappedValue.TypeInfo.BuiltInType == BuiltInType.QualifiedName` AND `ValueRank == ValueRanks.OneDimension` — not just the DataType attribute. - **Test**: `BaseInformationTests.BrowseRequiresMandatoryMethodsAsync` now `Assert.Fail`s (not `Assert.Ignore`) when an expected mandatory method is missing on `i=15443` / `i=25440`. - **Server**: `Server_ServerCapabilities_ConformanceUnits` initialised with `Array.Empty<QualifiedName>().ToArrayOf()` so the encoded Variant carries QualifiedName type info even when empty. - **Server**: Added `AddSecurityGroup` / `RemoveSecurityGroup` (on `i=15443`) and `AddPushTarget` / `RemovePushTarget` (on `i=25440`) method nodes with input/output argument metadata and in-memory handlers in `Quickstarts.Servers/ReferenceServerConfigurationNodeManager`. ### `86d3041e5` Fix #3723 (Security None DoS off-by-one) - **Server**: Correct off-by-one in `Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs` channel eviction: - Condition was `MaxChannelCount == channelCount` (only triggered at exact equality); now `MaxChannelCount > 0 && channelCount >= MaxChannelCount` with a loop that evicts idle channels until the count is below the limit or no more idle channels remain. - **Server**: `UsedBySession` now properly tracks active sessions and clears on `CloseSession` (`TcpServerChannel`, `TcpListenerChannel`). - **Note**: the NUnit reproducer for this issue (`SecurityNoneDosTests`) stays on the `cttunit` branch (PR #3750) because it depends on the `TestFixture.CloseSharedFixtureSessionAsync` helper that only exists in `Tests/Opc.Ua.Conformance.Tests/`. The server fix on this branch is sufficient to make the CTT JS script pass. ### `2c98390c9` Fix #3720 (BaseInfoStateMachine GeneratesEvent) - **Model**: Add `GeneratesEvent` forward reference on `StateMachineType` (i=2299) and `FiniteStateMachineType` (i=2771) → `AuditUpdateStateEventType` (i=2315) in the design XML (`Tools/Opc.Ua.SourceGeneration.Core/Design/StandardTypes.xml`); regenerated `Stack/Opc.Ua.Core/Schema/Opc.Ua.NodeSet2.xml`. - **Server**: Remove the runtime workaround in `DiagnosticsNodeManager` that previously injected this reference at load time — now redundant because the reference ships in the NodeSet at compile time. - **Test**: New `BaseInfoStateMachineInstanceTests.StateMachineTypeHasGeneratesEventReferenceAsync` walks the type hierarchy looking for `GeneratesEvent` with `IncludeSubtypes=true` and asserts the target resolves to a `BaseEventType` subtype (matches the CTT walk). - Per OPC UA Part 16 §4.4.5 / Part 5 §6.4.2. ### `09f551449` Strengthen A&C conformance event tests (#3841) - **Test fixture**: New `AlarmEventCollector` (476 LOC) — subscribes to events on `i=Server` with a full A&C event filter (`Comment`, `Retain`, transition times, all condition-state Ids); per-condition event log; `WaitForEventAsync(conditionId, predicate, timeout)` for synchronisation. Uses async `IAsyncDisposable` cleanup. - **Tests rewritten** to subscribe to events and validate event payload (`Comment.Text`, `Retain`, `TransitionTime`): - `Acknowledge Test_003` — empty comment + locale propagates to event payload; `Retain=false` afterwards - `Alarm Test_002` — full `ActiveState` cycle Active → Normal with Acknowledge + Confirm; every transition event validated - `Confirm Test_001` — unique comment reaches event payload; `ConfirmedState/Id=true` - `Confirm Test_003` — Acknowledge → wait event → Confirm with empty comment - `Confirm Err_004` — **two concurrent sessions**; second Confirm asserts specifically `BadConditionBranchAlreadyConfirmed` (not a generic `BadInvalidState`) - `Enable Test_002` — Disable + Enable via both type method and discovered instance method; NO events during disabled period; `TransitionTime` within call window; `Retain` flips - `Non-Exclusive Limit Test_002` — mirror for `NonExclusiveLimitAlarmType` - **Server fixes** in `Stack/Opc.Ua.Core.Types/State/AcknowledgeableConditionState.cs`, `Quickstarts.Servers/Alarms/AlarmHolders/*`, and `AlarmNodeManager` for: Comment propagation, Retain clearing, TransitionTime updates, and a dedicated ack-only condition instance (`AcknowledgeableConditionHolder`). ## Verification - `dotnet build UA.slnx` — **0 errors**, 42 pre-existing warnings. - `dotnet test Tests/Opc.Ua.InformationModel.Tests` filtered for the new + master `StateMachineGeneratesEvent` / `BaseInfoServer` / `BaseInformation` tests — **6/6 pass**. - `dotnet test Tests/Opc.Ua.History.Tests --filter AlarmsAndConditions` — **141/144 pass**; the 3 occasional failures in `DisableEnableViaTypeAndInstanceMethodNoEventsDuringDisabledAsync` pass when re-run in isolation, consistent with the known A&C suite CI flakiness pattern. ## Related Issues - Fixes #3719 - Fixes #3720 - Fixes #3723 - Fixes #3841 Related: PR #3750 (`cttunit`) carries the same fixes plus the conformance-test-project versions of the strengthened NUnit tests. Either this PR or that one will land first; the other should be rebased afterwards. ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [ ] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
c781514ad5 |
Refactor pcap diagnostics: decorator binding + rename to Opc.Ua.Bindings.Pcap
Three changes addressing the design feedback on PR #3855: 1. Renamed Stack/Opc.Ua.Diagnostics.Pcap -> Stack/Opc.Ua.Bindings.Pcap (and the corresponding test project). PackageId, AssemblyName and RootNamespace updated to Opc.Ua.Bindings.Pcap; references in McpServer, signlists, slnx files, docs, and the interop-tester agent updated; Opc.Ua.Core's InternalsVisibleTo entries updated. 2. Replaced UaSCUaBinaryChannel.FrameCaptureSink + per-call NotifyFrameCaptureSink null check with a transport-binding decorator. New files in Opc.Ua.Bindings.Pcap/Bindings: - IChannelCaptureRegistry / ChannelCaptureRegistry: singleton with a volatile IFrameCaptureSink slot (1 atomic write to switch recording on/off). - CapturingMessageSocket / CapturingMessageSocketFactory: IMessageSocket decorator that taps Send/Receive bytes via a single volatile read of CurrentObserver. - PcapTransportChannelBinding: ITransportChannelFactory decorator that injects the capturing socket factory into every TcpTransportChannel and wires ISecureChannel.OnTokenActivated through to the active observer. - PcapBindings.Install(): non-DI installer for direct TransportBindings.Channels.SetBinding wiring. IFrameCaptureSink moved to Opc.Ua.Core/Stack/Bindings and gained an OnTokenActivated method so the binding can publish key material alongside chunks. UaSCUaBinaryChannel.FrameCaptureSink property and all three NotifyFrameCaptureSink call sites removed - hot path returns to original shape with zero indirection when the binding is not installed. IMessageSocketAsyncEventArgs gains Offset + Count getters (additive). AddOpcUaDiagnosticsPcap/Formatters/Replay extension methods renamed to AddOpcUaBindingsPcap[Formatters|Replay]; the new AddOpcUaBindingsPcap also installs the binding into TransportBindings.Channels. InProcessClientCaptureSource / InProcessServerCaptureSource: dropped reflection-based AttachClientChannel/AttachServerChannel. The source itself implements IFrameCaptureSink and on Start publishes itself as the registry observer; on Stop clears it. McpServer's PacketCaptureTools simplified accordingly (no more per-channel attach loop). 3. Discarded the source-generator suggestion. Confirmed that the dissection path already 100% reuses the source-generated decoders: UaSCUaBinaryChannel.ReadSymmetricMessage for chunk decryption and BinaryDecoder.ReadEncodeable + EncodeableFactory.GlobalFactory (populated by Tools/Opc.Ua.SourceGeneration's DataTypeGenerator) for service-type decoding. Emitting additional dissection code would duplicate what's already there. Validation: - Full UA.slnx builds clean: 0 errors, 2 pre-existing unrelated warnings. - All 53 tests pass on net10.0 (49 existing + 4 new CapturingMessageSocketTests covering hot-path: not-installed/installed-off/installed-on costs, byte equality, exception isolation). - CI command line (dotnet build/test --framework net10.0 --configuration Release /p:CustomTestTarget=net10.0) reproduces clean locally. |
||
|
|
3d11328955 |
Narrow Opc.Ua.Diagnostics.Pcap to net10.0 to match the CI matrix
CI runs the per-test-project jobs with --framework net10.0 /p:CustomTestTarget=net10.0, which restricts every dependency project (Opc.Ua.Core, Client, Server, Configuration, TestFramework, Test.Common) to net10.0 via the shared LibCoreTargetFrameworks/LibTargetFrameworks variables. The new library and its tests previously declared net8.0;net9.0;net10.0, so NuGet restore failed with NU1201 'Project X is not compatible with net8.0...supports net10.0' and the test-ubuntu-latest-Diagnostics.Pcap and test-windows-latest-Diagnostics.Pcap jobs returned exit code 1. Matches the same net10.0-only pattern Opc.Ua.Aot.Tests already uses; the only production consumer of this library (the MCP server) is net10.0 too. The signlist*.txt files are updated to drop the net8.0 and net9.0 paths that no longer exist. Verified locally: dotnet build/test reproducing the CI command line succeeds with 49/49 tests passing. |
||
|
|
ff33cdd18a |
Phase E: auto-reset faulted ChannelEntry on ReconnectAsync(lease, ...)
Per review feedback (ChannelManagerTransparentReconnectIntegrationTests.cs:121 "Implement!"). Adds transparent recovery: when ReconnectAsync is called on a lease whose Entry has transitioned to Faulted or Closed, the manager swaps the lease's Entry reference to a freshly-created one for the same key, preserves the participant, and proceeds with the reconnect cycle. Back-off scales via the existing reconnect-policy GetDelay(SwapCount). - Production: ClientChannelManager.SwapFaultedEntryAsync(lease, ct); ManagedTransportChannelLease.SwapEntry(...) + SwapCount tracking; ChannelEntry.ReattachParticipant(lease, factory) for refcount preservation. - Unit test: ClientChannelManagerManagedTests covers the swap flow with a fake transport. - Integration: ChannelManagerExhaustionEscalatesAndRecoversWhenServerReturns un-Ignored and implemented — verifies the full transparent exhaustion + recovery cycle against a real ManagedSession. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
193fd4e5f4 |
Phase D: refactor ClientChannelManager to single non-partial class + SOLID extractions
Per review feedback (ClientChannelManager.Diagnostics.cs:36): merge the 7 partial files into one non-partial ClientChannelManager and extract facets as separate sealed internal top-level types via narrow IChannelEntryHost / IChannelCertRotationHost host interfaces. Extracted types (each in Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/): - ChannelEntry (promoted from nested; behind IChannelEntryHost seam) - ManagedTransportChannelLease (promoted from nested) - ClientChannel (promoted from nested) - ClientChannelManagerMetrics (promoted from nested) - ClientChannelManagerCertRotation (extracted from partial; behind IChannelCertRotationHost seam) - ClientChannelManagerDiagnostics (extracted from partial) Public methods/events/properties are unchanged. ClientChannelManager-focused Core/Client/Sessions/Stress tests pass, and full Release solution build completes with 0 errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
67e803c6a7 |
Address PR review feedback
* Docs/MigrationGuide.md: drop the new 'Packet capture diagnostics hooks' section. * Stack/Opc.Ua.Core/Stack/Tcp/IFrameCaptureSink.cs -> Stack/Opc.Ua.Core/Stack/Bindings/IFrameCaptureSink.cs. * InProcessCaptureSource: drop sync-over-async (GetAwaiter().GetResult()) on the hot path. Frame and key-material persistence is now fully async via a bounded System.Threading.Channels queue drained by a single background writer task; the IFrameCaptureSink callbacks and OnTokenObserved enqueue non-blocking and never wait on I/O. * Models/Models.cs: split into one file per type (FormatKind, CaptureSourceKind, CaptureSessionState, StartCaptureRequest, CaptureNowRequest, StartReplayRequest, CaptureSessionInfo, NetworkInterfaceInfo). Reformat every <summary>...</summary> block to multi-line per repo convention. * Properties/AssemblyInfo.cs: drop duplicated InternalsVisibleTo (already declared in the csproj <ItemGroup>); keep [assembly: CLSCompliant(false)]. * Opc.Ua.Diagnostics.Pcap.csproj: trim the long marketing-style Description to a single sentence. Validated: UA.slnx build 0 errors; 49/49 pcap library tests pass on net10.0. |
||
|
|
cbd84388d9 |
Add OPC UA packet capture, offline decoding and replay (Opc.Ua.Diagnostics.Pcap)
Adds a new diagnostics library (Stack/Opc.Ua.Diagnostics.Pcap) plus 13 MCP tools that capture OPC UA traffic, decode it offline by reusing the stack's own UaSCUaBinaryChannel, and replay it. Includes additive stack hooks (IFrameCaptureSink, TcpListenerChannel.OnTokenActivated), four capture sources (nic / inproc-client / inproc-server / replay), two keylog formats (.uakeys.json, .uakeys.txt), six output formats (pcap, pcapng, json, csv, text, service-timeline), and mock-server / mock-client replay engines. 49 new tests pass; no regressions in the 163 existing transport tests; full UA.slnx builds clean. |
||
|
|
cf6bd37360 |
Address PR feedback: small-scope cleanup
Applies the safe / minor review feedback on PR #3852. Larger refactors
are scoped into a separate plan posted on the PR for explicit approval
before starting (merge ClientChannelManager partials, merge Channels.Stress
into Opc.Ua.Stress.Tests, move ClientChannelManagerManagedTests out of
Core.Tests, implement the ignored exhaustion-recovery integration test,
fix remaining CA2007 / CA1861 / CA5394 / CA2025 warnings without NoWarn).
Changes in this commit:
- .github/workflows/buildandtest.yml: drop "legacy" wording from the
EXCLUDE comment (Stress suite is opt-in, not legacy).
- Applications/ConsoleReferenceClient/UAClient.cs +
Applications/ConsoleReferenceClient/ClientSamples.cs: replace
Action<IList, IList>? validateResponse with Action<Array, Array>? to
(1) remove the System.Collections import that
|
||
|
|
3a5477b1ea |
Fix CI failures: HTTPS factory regression + parallel build file lock
- HttpsTransportChannel.CanUseHttpClientFactory: the F10 HTTPS migration was auto-routing every HTTPS endpoint through the DefaultOpcUaHttpClientFactory.Shared HttpClient when no client cert or validator was supplied, which BYPASSED the custom ServerCertificateCustomValidationCallback wiring that CreateDirectHttpClient sets up against the OPC UA CertificateValidator. Result: HTTPS tests that rely on self-signed server certs (Sessions, Client.ComplexTypes, PubSub) all failed with HttpRequestException (SSL connection could not be established). The factory path is now opt-in: only callers that explicitly supplied a non-default IOpcUaHttpClientFactory get the shared HttpClient pipeline; otherwise we always fall back to CreateDirectHttpClient which honors the OPC UA TLS validation hooks. - buildandtest.yml: add /p:UseSharedCompilation=false to the build step to avoid the VBCSCompiler file-lock race (CS2012, "Cannot open ... for writing -- file is being used by another process") seen on Channels.Stress and other matrix jobs. |
||
|
|
06fbbb571f |
Add channel-manager stress/chaos test suite (Tests/Opc.Ua.Channels.Stress.Tests)
Layered pyramid (per rubber-duck plan) to exercise every meaningful combination of the channel-manager + session reconnect machinery shipped in PR #3852: L1 — Contract (deterministic, fake transport, runs in every PR): - CoalescingTests, ParticipantResultAggregationTests, RetryBudgetEnforcementTests, HungParticipantTests, LeaseLifecycleTests, GateAndBypassTests, KeyAndSharingTests, CertRotationContractTests, LeakAccuracyTests - 27 tests, all pass. L2 — Integration (in-process server stop/start, runs in every PR): - ServerOutageRecoveryTests, CertRotationLiveTests, FailoverLeaseSwapTests - 6 tests, all pass. L3 — Chaos (TCP proxy with drop/block-accept/stall, nightly category=ChaosTCP): - TransparentReconnectChaosTests, SubscriptionSurvivalChaosTests, AcceptButStallChaosTests, BlockAcceptChaosTests - Seed-driven via ChaosSchedule for reproducibility. L4 — Soak (manual category=Soak): - LongSoakTests (60 min randomized chaos), CombinatorialMatrixTests (engine/transfer/subscribe/sessions matrix), MemoryStabilitySoakTests (30 min memory snapshots). L5 — Known-failing gaps ([Explicit], document carry-forward): - FaultedEntryResetGapTests, SessionRecreatePlumbingGapTests, ParticipantTimeoutGapTests. Infrastructure in Fakes/ and Helpers/: - FakeTransport, FakeChannelBindings, FakeParticipant (configurable fault modes). - ChaosBarrier (deterministic barrier eliminates timing flakiness in coalescing tests). - TcpChaosProxy (~200 lines; DropAllConnectionsAsync, BlockAcceptAsync, StallForwarding). - StressRunner (concurrent workload generator with latency percentiles). - ChaosSchedule + ChaosScheduleRunner (pre-generated deterministic event schedule). - WaitForQuiescence (ForManagerAsync, EntryRefcountReachesAsync, EntryGoneAsync). - MetricsCollector (subscribes to Opc.Ua.ChannelManager EventSource + IMeter). - LeakCounters (snapshot-based refcount/cert/entry validation). CI wiring: - .github/workflows/buildandtest.yml now runs Category=Contract|Integration in PR CI. - .github/workflows/channel-manager-stress-test.yml runs Category=ChaosTCP nightly with random seed. Small production-code tweaks discovered while building tests: - ClientChannelManager.Managed.cs / .Metrics.cs: minor refactors for testability. - Session.cs / Session.ChannelManager.cs / ManagedSession.cs: small adjustments to make IClientChannelManager-aware flows easier to assert. Docs: - Docs/Sessions.md section 4 now has a "Testing the channel manager" subsection pointing at the test categories with run-and-reproduce commands. Build: 0 warnings, 0 errors on net10.0. Tests: 33/33 pass in Category=Contract|Integration. |
||
|
|
0a32452611 | Merge branch 'master' into channelrefinements | ||
|
|
82347dc1da |
Address PR #3852 review feedback
- Move all channel-related types into `Stack/Opc.Ua.Core/Stack/Client/Channels/` subfolder (#13). - Un-obsolete `SessionReconnectHandler` and the SessionExtensions `ReconnectAsync(connection, ct)` / `(channel, ct)` extensions; remove all SRH-related `#pragma warning disable CS0618` suppressions from applications and tests. AttachChannel/DetachChannel remain obsolete (#1, #4). - Remove the shared-budget section from MigrationGuide.md (#3). - Mark `ManagedSession.AttachChannel`/`DetachChannel` `[Obsolete]` so the warning surfaces all the way up the supported API surface (#9). - Break the `<see cref=…>` line in `Session.ChannelManager.cs` to stay under 140 chars (#10). - Move `ChannelManagerSessionFactory` from `Opc.Ua.Gds.Client.Common` into `Opc.Ua.Client` as a public, documented session-factory option (#11). - Audit other client classes for `IClientChannelManager`-aware overloads; add `ISessionFactory`-accepting overloads on `GlobalDiscoveryServerClient`, `LocalDiscoveryServerClient`, `ServerPushConfigurationClient` so any session factory (including `ChannelManagerSessionFactory`) works (#12). - Refactor MCP server (`OpcUaSessionManager`, `Program`) to use `ManagedSession` + DI-resolved `IClientChannelManager`; remove `SessionReconnectHandler` and manual keep-alive reconnect (#2). - ConnectionStateMachine code-style fixes: use named delegate types, collapse multi-line callback properties to single lines, swap `timeProvider`/`maxTotalReconnectTime` constructor param order, fix multi-line declaration (#6, #7, #8). - HTTPS resilience: confirmed `Microsoft.Extensions.Http` and `Microsoft.Extensions.Http.Resilience` 10.6.0 support `net472`/`net48`/`netstandard2.1`; removed `#if NET8_0_OR_GREATER` gating from `ManagedSessionBuilder` and `OpcUaClientBuilderExtensions` and HTTPS csproj package refs (#5). Verification: - `Opc.Ua.Client` + `Opc.Ua.Core` + `Opc.Ua.Gds.Client.Common` build clean (0 warnings, 0 errors). - 56/56 core channel-manager / retry-budget / HTTPS factory tests pass. - 129/129 client (ManagedSession + legacy SRH back-compat + SessionExtensions) tests pass. |
||
|
|
36d67b3d3a |
[Client] Central channel manager with shared, ref-counted transport channels (#3288)
Introduces `IClientChannelManager` as the central registry for client-side transport channels. Sessions and discovery clients now share ref-counted managed channels per (endpoint, reverse-connect identity); reconnect is coalesced and notified to attached participants via a new `IReconnectParticipant` callback interface; service calls are gated by a three-state lifecycle (`TransportReconnecting` -> `TransportConnectedSessionReactivating` -> `Ready`) until reactivation completes. New types in `Stack/Opc.Ua.Core/Stack/Client/`: - `ManagedChannelKey`, `ChannelState`, `ChannelStateChange`, `ParticipantReconnectResult` - `IReconnectParticipant`, `IManagedTransportChannel`, `IClientChannelManager` - `IChannelReconnectPolicy` + `ExponentialBackoffChannelReconnectPolicy` - `IRetryBudget` + `RetryBudget` (shared retry deadline across the two layers) - `ClientChannelManager.Managed/Entry/Lease/Metrics/Diagnostics/CertRotation.cs` partials with refcount, coalesced reconnect, IMeter instruments, ActivitySource + EventSource, and automatic ReconnectAllAsync on certificate rotation. Session integration (Libraries/Opc.Ua.Client): - `Session.CreateAsync(IClientChannelManager, ...)` overload constructs a Session that shares its channel with co-located participants. - `Session` implements `IReconnectParticipant`; reactivation handler distinguishes Reactivated / RequiresSessionRecreate / TransientFailure / FatalForParticipant / FatalForChannel. - `Session.ReconnectAsync(ct)` automatically delegates to the channel manager when wired. - `Session.RecreateInPlaceAsync` swaps managed-channel leases for failover-to-different-endpoint; same-key recreates delegate to manager reconnect; explicit channel/connection callers retain legacy behavior. - KA failure routes through `IClientChannelManager.ReconnectAsync(channel)`. ManagedSession integration: - `WithChannelManager(...)` builder + ctor param. - Two-level retry: channel-mgr handles transparent reconnect; outer `ConnectionStateMachine` + `IReconnectPolicy` only triggers on terminal channel `Faulted`. Outer-state churn suppressed during channel-mgr cycles via `_channelReconnectInProgress` flag wired through `IManagedTransportChannel.StateChanged`. - New `ManagedSession.ChannelStateChanged` event surfaces transparent reconnects to UI / health dashboards. - `ConnectionStateChangedEventArgs.UnderlyingChannelState` populated when outer reconnect was triggered by a channel fault. - Shared `IRetryBudget` propagated to channel-mgr so the two layers enforce a single max-total-time instead of compounding multiplicatively. Discovery/Registration/GDS: - Channel-manager-aware `CreateAsync` overloads on `DiscoveryClient`, `RegistrationClient`, `ServerPushConfigurationClient`, `LocalDiscoveryServerClient`, `GlobalDiscoveryServerClient`. Each registers a minimal `IReconnectParticipant`; co-located sessions and discovery probes targeting the same endpoint share a channel. HTTPS transport (`Stack/Opc.Ua.Bindings.Https/`): - New `IOpcUaHttpClientFactory` abstraction. - HTTPS / OPC-HTTPS channels migrated to `IHttpClientFactory` + `Microsoft.Extensions.Http.Resilience` (10.6.0, AOT-compatible on net10 via source generators; legacy direct-HTTP path retained for older TFMs / no-DI consumers). DI: - `AddOpcUa().AddClient(...)` registers `IClientChannelManager` and the named HTTPS HttpClient + standard resilience handler automatically. - `ManagedSessionBuilder` resolves the manager from DI when present. Obsoletions (functional, `[Obsolete]` with guidance): - `IClientBase.AttachChannel`/`DetachChannel` - `SessionReconnectHandler` - `SessionExtensions.ReconnectAsync(connection, ct)` and `(channel, ct)` legacy overloads Tests (~70 new unit + 4 live-server integration fixtures): - Core: `ClientChannelManagerManagedTests`, `ClientChannelManagerCertRotationTests`, `RetryBudgetTests`, `OpcUaHttpClientFactoryTests` (56/56 pass). - Client: `ManagedSession*Tests` updated (129/129 pass) + back-compat suites pass with `#pragma warning disable CS0618`. - Sessions integration: `ChannelManagerSharing/TransparentReconnect/SessionLifecycle/CertRotation` (6 pass, 2 skipped with documented blockers for faulted-entry reset and RequiresSessionRecreate end-to-end plumbing). Builds clean (0 warnings / 0 errors) across all six TFMs (net472, net48, netstandard2.1, net8.0, net9.0, net10.0). Docs: - `Docs/Sessions.md` new section 4 documents the channel manager, three-state gating, two-level retry with shared `IRetryBudget`, and HTTPS resilience layering. - `Docs/MigrationGuide.md` migration recipes for the four obsoleted API groups. - `Docs/DependencyInjection.md` channel-manager and HTTPS factory registration. |
||
|
|
2966f693b2 |
2.0 Migration Analyzer and codefixer with Agent update (#3820)
Introduces `OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer` — a single NuGet package that ships **three Roslyn components + a runtime shim** to make the 1.5.378 → 2.0 upgrade as close to "install package, run `dotnet format`, remove package" as possible: | DLL | Where | Purpose | |-----|-------|---------| | `Opc.Ua.MigrationAnalyzer.dll` | `analyzers/dotnet/cs/` | 19 Roslyn analyzers (`UA0001`–`UA0022`) covering every breaking-change pattern in [`Docs/MigrationGuide.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md). Built against the stable Roslyn 4.14 analyzer API — loads cleanly in csc.exe (no `Microsoft.CodeAnalysis.Workspaces` reference). | | `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` | `analyzers/dotnet/cs/` | 14 companion `CodeFixProvider`s; loaded only by Workspaces-aware hosts (Visual Studio, `dotnet format analyzers`). | | `Opc.Ua.MigrationAnalyzer.Generator.dll` | `analyzers/dotnet/cs/` | Roslyn `IIncrementalGenerator` that emits per-consumer `internal sealed [Obsolete] class <Name>Collection : List<TElement>` shims for every `<Type>Collection` wrapper the consumer references — including **arbitrary model-compiled `<UserType>Collection` patterns**, not just the 30 well-known built-in ones. | | `Opc.Ua.MigrationAnalyzer.Core.dll` | `lib/<tfm>/` for `net472, net48, netstandard2.1, net8.0, net9.0, net10.0` | 1.5.378 → 2.0 runtime compatibility shim. Re-exposes the obsolete extension surface so 1.5.378-style call sites keep compiling against 2.0 with warnings instead of errors. | ### Migration workflow the package enables 1. **Bump every `OPCFoundation.NetStandard.Opc.Ua.*` PackageReference to `2.0.*-*`.** 2. **Add the migration package** as a build-only dependency: ```xml <PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer" Version="2.0.*-*" PrivateAssets="all" /> ``` 3. **Run `dotnet build`.** The Core shim DLL re-supplies obsolete extension members; the source generator covers `<Type>Collection` references; what remains are `[Obsolete]` warnings + `UA00xx` analyzer diagnostics + (occasionally) `MIG01` for unresolvable model-compiled element types. 4. **Apply auto-fixes** in the IDE or from the command line: ```bash dotnet format analyzers <solution>.sln \ --diagnostics UA0002 UA0003 UA0004 UA0005 UA0006 UA0007 UA0008 \ UA0009 UA0010 UA0012 UA0014 UA0019 UA0020 UA0022 \ --severity warn ``` 5. **Walk the residual** `UA0001` / `UA0011` / `UA0015` / `UA0018` / `UA0021` diagnostics manually — they cover patterns that require human judgement. 6. **Remove the migration package.** You are now on clean 2.0 with no shim dependency. ### Source generator (`Opc.Ua.MigrationAnalyzer.Generator.dll`) Per-consumer Roslyn `IIncrementalGenerator` that emits `.g.cs` files for every `<Name>Collection` reference in the consumer's compilation that fails to bind: - **Catalog override (30 entries)** — pins element types that renamed across the 1.5.378 → 2.0 boundary: `DateTime`→`DateTimeUtc`, `Guid`→`Uuid`, `byte[]`→`ByteString`, `XmlElement`→`Opc.Ua.XmlElement`, plus the primitives and built-in opc types whose element name didn't rename. - **Semantic lookup** — for model-compiled `<UserType>Collection` patterns the model compiler historically emitted for any user-defined complex type. Strips the `Collection` suffix, runs `Compilation.GetSymbolsWithName` against the unique element short name, emits the shim with the fully-qualified type reference. - **`MIG01` diagnostic** — fires when the element type can't be uniquely resolved (zero or multiple candidates). User adds the missing `using` or migrates the site manually. Each generated file emits a class shaped exactly like the 1.5.378 wrapper: ```csharp [global::System.Obsolete("'Int32Collection' was removed in 2.0. Use 'List<int>' or 'ArrayOf<int>' instead. (UA0002)")] internal sealed class Int32Collection : global::System.Collections.Generic.List<int> { public Int32Collection() { } public Int32Collection(int capacity) : base(capacity) { } public Int32Collection(global::System.Collections.Generic.IEnumerable<int> collection) : base(collection) { } public static implicit operator global::Opc.Ua.ArrayOf<int>(Int32Collection? value) => value is null ? default : value.ToArrayOf(); } ``` **Design choice — `internal` (not `public`):** the generated shim never leaks through the consumer's public API surface. Consumers with *public* methods or properties returning a `<Type>Collection` will hit `CS0050: Inconsistent accessibility` — the intended signal that the public surface must migrate to `List<T>` / `ArrayOf<T>` first; internal call sites keep compiling under the shim so iteration is incremental. ### 19-rule analyzer reference | Rule | Replaces | Default | Auto-fix | |------|----------|---------|----------| | `UA0001` | `Utils.Trace` / `Utils.LogX` | Info | No (telemetry plumbing requires manual review) | | `UA0002` | Removed `<Type>Collection` wrappers | Warning | Yes (→ `List<T>` / `ArrayOf<T>`); the source generator keeps the call site compiling in the meantime | | `UA0003` | `x == null` on now-struct built-ins | Warning | Yes (→ `x.IsNull`) | | `UA0004` | `?.` on now-struct built-ins | Warning | Yes (drop the `?`) | | `UA0005` | `byte[]` where `ByteString` is expected | Warning | Yes (append `.ToByteString()`) | | `UA0006` | `new Variant(object\|DateTime\|Guid\|byte[])` | Warning | Yes (→ `Variant.From(...)`) | | `UA0007` | `new NodeId(string)` / `new ExpandedNodeId(string)` | Warning | Yes (→ `Parse`) | | `UA0008` | `Session.Call(..., params object[])` | Warning | Yes (wrap with `Variant.From`) | | `UA0009` | `[DataContract]` / `[DataMember]` on config extensions | Warning | Yes (→ `[DataType]` / `[DataTypeField]`) | | `UA0010` | `using` / `Dispose` on `CertificateIdentifier` / `UserIdentity` / `IUserIdentityTokenHandler` | Warning | Yes (drop the disposable) | | `UA0011` | Sync `IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}` | Info | No (async promotion required) | | `UA0012` | `CertificateFactory.*` static helpers | Warning | Yes (→ instance methods) | | `UA0014` | `DataValue.IsGood(dv)` static helper | Warning | Yes (→ `dv.IsGood`) | | `UA0015` | Sync / APM members on GDS / LDS clients | Info | No (async promotion required) | | `UA0018` | `CertificateIdentifier.Certificate` getter | Info | No (→ `LoadCertificate2Async`) | | `UA0019` | `new DataValue(StatusCode[, ts])` | Warning | Yes (→ object-initializer form) | | `UA0020` | `EncodeableFactory.GlobalFactory` / `Create()` | Warning | Yes (→ `ServiceMessageContext.Factory` / `Fork()`) | | `UA0021` | `CertificateValidator` / `CertificateValidationEventArgs` (structural rename) | Info | No (see migration guide §Certificate validation) | | `UA0022` | `ApplicationConfiguration.CertificateValidator` / `ServerBase.CertificateValidator` property rename | Warning | Yes (→ `.CertificateManager`) | Plus one generator-only diagnostic: | Rule | Source | Default | Triggers | |------|--------|---------|----------| | `MIG01` | Generator | Warning | Generator can't uniquely resolve the element type for a `<Foo>Collection` reference (zero or multiple candidates). The user adds a `using` for the namespace defining `Foo`, or migrates the call site manually. | ### Runtime shim coverage (`Opc.Ua.MigrationAnalyzer.Core.dll`) C# 14 `extension` members so 1.5.378 call sites continue to compile: - **Moved obsolete extensions** the 2.0 libraries no longer carry inline: `NodeId` / `Variant` / `DataValue` null-check helpers, `Session` sync helpers, `Subscription` sync helpers, `ApplicationInstance` helpers, `ServerBase.Start` / `Stop`, `TransportChannel` APM, `ChannelBase` static factory methods, etc. - **New shims for genuinely-removed members**: `EncodeableFactory.GlobalFactory`, `CertificateIdentifier.Certificate` (throws), sync wrappers for `IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}`, sync + APM wrappers for the GDS / LDS client APIs, `GlobalDiscoverySampleServer` 1.5.378-shape ctor (in-tree `Libraries/Opc.Ua.Gds.Server.Common/`). > ⚠ The sync shims wrap their `*Async` counterparts via `Task.Run(…).GetAwaiter().GetResult()`. Intended as a **migration aid only** — port to `await` before shipping. ### Repo layout ``` Tools/Opc.Ua.MigrationAnalyzer/ # analyzer DLL Analyzers/UA00xxAnalyzer.cs # 19 DiagnosticAnalyzer impls Diagnostics/{DiagnosticIds,Descriptors,WellKnownProperties}.cs Helpers/{SymbolExtensions,UaSymbols}.cs Opc.Ua.MigrationAnalyzer.nuspec # ships all 3 analyzer DLLs + 6-TFM Core OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/ # code-fix DLL CodeFixes/UA00xxCodeFix.cs # 14 CodeFixProvider impls Tools/Opc.Ua.MigrationAnalyzer.Generator/ # source generator DLL (NEW) MigrationGenerator.cs # IIncrementalGenerator CollectionShimCatalog.cs # 30-entry element-type overrides Diagnostics/GeneratorDiagnostics.cs # MIG01 Tools/Opc.Ua.MigrationAnalyzer.Core/ # runtime shim (multi-TFM) Client/, Configuration/, Core/, Gds.Client.Common/, Types/, Marker/ Tools/Roslyn.slnx # source-gen + migration projects Tests/Opc.Ua.MigrationAnalyzer.Tests/ # 142 tests Analyzers/UA00xxTests.cs # 107 analyzer + code-fix tests Generators/MigrationGeneratorTests.cs # 35 generator tests (NEW) Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/ # 13 runtime-shim tests ``` ### Documentation - [`Docs/MigrationGuide.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md) — §3 Collection Type Migration gains a tip pointing at the generator so CS0246 is replaced with `[Obsolete]` warnings + UA0002. - [`.github/agents/opcua-v20-migration.agent.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/.github/agents/opcua-v20-migration.agent.md) — Strategy section mentions the three Roslyn components + runtime shim explicitly, plus the MIG01 diagnostic and the "internal-by-design" CS0050 hint. ### Central package pinning `Directory.Packages.props` pins `Microsoft.CodeAnalysis.*` (Common, CSharp, CSharp.Workspaces, Workspaces.Common) to **4.14.0** — the stable analyzer API surface. The earlier 5.3 pin was the *csc-internal* version, which is not safe for analyzer hosts. ### Dogfood story The full 42-sample dogfood against `OPCFoundation/UA-.NETStandard-Samples` surfaced a **critical analyzer-loader bug** in an earlier iteration: the analyzer DLL initially co-shipped its code-fix providers in one assembly, which transitively referenced `Microsoft.CodeAnalysis.Workspaces.dll`. csc.exe's analyzer host ships only `Microsoft.CodeAnalysis.dll` + `CSharp.dll` in its bincore and **silently swallows** load failures — net result: zero diagnostics across all samples. Fix landed in commit `861fa6ee1`: 1. **Split** the assembly: analyzer in `Opc.Ua.MigrationAnalyzer.dll` (Workspaces-free, csc-safe), code-fixes in `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` (Workspaces-aware, IDE-only). 2. **Downgrade** to Roslyn 4.14 — the stable analyzer API. 3. `/p:ReportAnalyzer=true` confirms all 19 analyzers initialise and execute on real consumer code. This PR's new generator was smoke-tested against a minimal consumer that exercises all 30 catalog entries + a model-compiled element type + the unresolvable case: - 30 catalog patterns + model-compiled `MyThingCollection` → all emit shims, fire CS0618 with `(UA0002)` marker - Element-type renames verified: `DateTimeCollection` → `DateTimeUtc`, `ByteStringCollection` → `ByteString`, `GuidCollection` → `Uuid`, etc. - `NeverHeardOfItCollection` → `MIG01` fires with help link to the migration guide Known compatibility gaps (documented in the migration agent): - Legacy WinForms `.Net4` sample projects use pre-SDK MSBuild XML and **ignore** `Directory.Build.targets` `<PackageReference>` injection — consumers in this format must add the migration package to their csproj inline. ### Packaging design notes - Hand-authored nuspec ships 3 DLLs in `analyzers/dotnet/cs/` + 6 TFMs of Core (shim) DLLs under `lib/`. All 6 OPC UA runtime package dependencies are declared per-TFM-group. - `build/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props` is auto-imported by NuGet (file name must match package id exactly) and injects all three analyzer DLLs as `<Analyzer>` items. - `DiagnosticIds.cs` / `DiagnosticDescriptors.cs` / `WellKnownProperties.cs` / `Helpers/*.cs` are **shared via `<Compile Link=…>`** between the analyzer and code-fixer projects to break what would otherwise be a NuGet restore cycle. The generator has no compile dependency on the analyzer; the 30-entry override table is duplicated in `CollectionShimCatalog.cs` (the analyzer table also drives diagnostic messages, so the duplication is small and intentional). - `Opc.Ua.MigrationAnalyzer.Core` is multi-TFM (`net472, net48, netstandard2.1, net8.0, net9.0, net10.0`) and **non-packable on its own**; the parent migration package's nuspec consumes each TFM's DLL by path. |
||
|
|
4756e7c245 |
DI (OPC 10000-100) with samples (Pump, SW Update), fluent API gap closures and model dependency implementation (#3808)
This PR implements the **OPC UA Device Integration (DI, OPC 10000-100)** companion-spec library trio (`Opc.Ua.Di` / `Opc.Ua.Di.Server` / `Opc.Ua.Di.Client`) plus a worked example (`Applications/MinimalPumpServer`) demonstrating the OPC 40223 Pumps companion specification end-to-end with full simulation. It also closes 11 fluent-API gaps in `Opc.Ua.Server.Fluent`, three source-generator bugs, integrates the DI layer into the unified `services.AddOpcUa()` Microsoft.Extensions DI hosting pattern, and adds 250+ new tests. **Worked example — `Applications/MinimalPumpServer/`** - AOT-compatible console host using `Host.CreateApplicationBuilder` + `AddOpcUa()`. - `PumpNodeManager` inherits from `DiNodeManager`; multi-namespace (DI + Machinery + Pumps) via runtime NodeSet2 loading and `ModelLoaderBuilder`. - Demonstrates declarative second-pump creation via `.ConfigureDevicesFor<PumpNodeManager>(...)` and software-package seeding from the same configurator. - Full simulation: pressure, temperatures, power, flow, efficiency, level, cavitation/overheat fault injection, alarm setup, NAMUR-style supervision. **Documentation** - `Docs/Opc.Ua.Di/` — README, DeviceBuilder, Hosting, LockService, SoftwareUpdate, ClientHelpers (6 new developer guides). - `Docs/Opc.Ua.Di/ComplianceMatrix.md` — every DI declaration mapped to In-v1 / Generated / Deferred / Excluded. - `Docs/CompanionSpecLibraries.md` — packaging guide for new companion-spec libraries. - `Docs/FluentApiGaps.md` — historical record of G1–G11. - `Docs/SourceGeneratedNodeManagers.md` updated for richer node managers. - `Docs/plans/StateMachineBuilder.md` — deferred state-machine builder design. |
||
|
|
82993d0b6f |
Allow application certificate lookup to use entries configured with abstract ApplicationCertificateType only as fallback candidates. (#3752)
## Proposed changes Tries to rectify inconsistent behavior: `SecurityConfiguration::BuilldSupportedSecurityPolicies()` advertises RSA policies if ApplicationCertificateType is set as CertificateType, while ApplicationCertificateType is not used when finding actual certificates. Use CertificateIdentifier::SetCertificate(certificate) to recalculate the CertificateType whenever the identifier receives a X509Certificate2. Removed stale if case` if (certType == ObjectTypeIds.ApplicationCertificateType)` from `SecurityConfiguration::FindApplicationCertificateAsync`, since `CertificateIdentifier.MapSecurityPolicyToCertificateTypes(securityPolicy)` never returned 'ApplicationCertificateType' and it was confusing and misleading because it suggested support for abstract ApplicationCertificateType, but normal policy lookup did not actually go through it. Allow application certificate lookup to use entries configured with abstract ApplicationCertificateType as fallback candidates. The fallback loads the candidate certificate first, then validates it against the concrete type required by the selected security policy. 'ApplicationCertificateType' is not a concrete certificate profile and should not be prefered in configuration, even if older configurations may use it. Previously, such entries could be missed during certificate lookup because the selected security policy maps to a concrete type. ## Related Issues - Fixes #3644 ## Types of changes What types of changes does your code introduce? _Put an `x` in the boxes that apply. You can also fill these out after creating the PR._ - [ ] Bugfix (non-breaking change which fixes an issue) - [x] Enhancement (non-breaking change which adds functionality) - [ ] Test enhancement (non-breaking change to increase test coverage) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected, requires version increase of Nuget packages) - [ ] Documentation Update (if none of the other choices apply) ## Checklist _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ - [ ] I have read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf). - [ ] I ran tests locally with my changes, all passed. - [ ] I fixed all failing tests in the CI pipelines. - [ ] I fixed all introduced issues with CodeQL and LGTM. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [ ] I have added necessary documentation (if appropriate). - [ ] Any dependent changes have been merged and published in downstream modules. ## Further comments If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... |
||
|
|
dbc09b8e22 |
Adopt System.TimeProvider across the stack (fixes #2458) (#3829)
# Description Adopt [`System.TimeProvider`](https://learn.microsoft.com/dotnet/api/system.timeprovider) across the stack so that every place that creates timers, runs duration math, or schedules periodic work is mockable, deterministic in tests, and immune to wall-clock changes. The migration touches Core, Core.Types, Server, Client, PubSub, Configuration, Gds (server + client), Lds, and the Quickstarts/Console reference apps. Per-component changes follow the same pattern: 1. Add a `private readonly TimeProvider m_timeProvider;` field. 2. Accept a nullable `TimeProvider? timeProvider = null` as the **last** constructor parameter (new overload; legacy constructor stays for binary compatibility and delegates with `timeProvider: null`). 3. Initialise the field with `m_timeProvider = timeProvider ??= TimeProvider.System;`. 4. Replace `new Timer(...)`, `Stopwatch`, `HiResClock.*`, `DateTime.UtcNow`-based duration math, and `Task.Delay(delay, ct)` in timing loops with the matching `m_timeProvider` API (`CreateTimer`, `GetTimestamp` / `GetElapsedTime`, `GetUtcNow().UtcDateTime`, `Task.Delay(delay, m_timeProvider, ct)`). 5. Wire the provider through DI: `AddOpcUaServerBuilder` / `AddOpcUaClientBuilder` register `TimeProvider.System` via `TryAddSingleton<TimeProvider>()` so tests can swap in `Microsoft.Extensions.Time.Testing.FakeTimeProvider`. `HiResClock` itself is kept (so the `ApplicationConfiguration.DisableHiResClock` schema flag still round-trips), but **every public member** is now `[Obsolete]` with a single shared message pointing to the new migration-guide section. The few internal call sites that still need to set/read `HiResClock` are wrapped with `#pragma warning disable CS0618`. ### Highlights per component - **Core / Stack** — `Opc.Ua.Core`, `Opc.Ua.Core.Types`, channel pipeline (`UaSCBinaryClientChannel`, `ChannelToken`, `UaSCBinaryChannel`), `ApplicationConfiguration`, certificate validators, `JwksIssuerKeyResolver`, `TimeProviderExtensions` polyfills (`GetTimestampMilliseconds`, `GetTickCount`, `CancellationTokenSource(timeout, timeProvider)`, `Task.Delay(delay, provider, ct)` polyfill for older TFMs). - **Server** — session lockout, subscription pipeline (publish/sample/keep-alive/lifetime), `MonitoredItemQueue`, alarm sampling rate, `IntervalRunner`, server diagnostics, configuration state machines, hosted services. - **Client** — `Session`, managed/V2 subscription engines, reconnect handlers, channel keep-alive, periodic publishing, complex-type discovery polling. - **PubSub** — `IntervalRunner` pipeline, message-receive timers, MQTT/UDP keep-alive, publishers/readers. - **Gds / Lds** — `LdsServer`, `LinqApplicationsDatabase` expiry housekeeping, GDS clients (push, pull), `DirectoryCertificateStore`, `ConnectionStateMachine`, classic subscription engine. - **Quickstarts / Console samples** — `SampleNodeManager`, `ReferenceNodeManager`, `AlarmNodeManager`, `BoilerState`, `MemoryBufferState`, `PublishedValuesWrites`. Samples resolve via `(Server as ITimeProviderProvider)?.TimeProvider ?? TimeProvider.System` so existing ctor signatures stay binary-compatible. - **Tests** — `Opc.Ua.PubSub.Tests` adds `IntervalRunnerTests`; existing `SubscriptionTests`, `UaPublisherTests`, and `TestDataSystem` move off `HiResClock`. Added `Microsoft.Extensions.TimeProvider.Testing` package for `FakeTimeProvider` (project-load warning surfaces on net48/net472 only — runtime is .NET 8+). ### Migration guide New section [**Time and Timer abstraction (`TimeProvider`)**](Docs/MigrationGuide.md#time-and-timer-abstraction-timeprovider) documents: - The canonical replacement table (`HiResClock.UtcNow` → `timeProvider.GetUtcNow().UtcDateTime`, `HiResClock.TickCount64` → `timeProvider.GetTimestamp()`, `new Timer` → `timeProvider.CreateTimer`, `Task.Delay(delay, ct)` → `Task.Delay(delay, timeProvider, ct)`, …). - The `TimeProvider? timeProvider = null` + `??=` constructor pattern. - DI wiring and how to swap in `FakeTimeProvider` in tests. - Before/after snippets for `HiResClock.TickCount64`, `HiResClock.UtcNow`, and `new Timer(...)`. ## Related Issues - Fixes #2458 ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. |
||
|
|
0000513cde |
Pluggable user-identity provider model (#3807)
# Description Introduces a pluggable user-identity provider model covering all OPC UA Part 6 §6.5 mechanisms (Anonymous, UserName, X509, IssuedToken/JWT) on both client and server, with full `Microsoft.Extensions.DependencyInjection` integration, plus the Part 12 §9 `AuthorizationService` two-phase token-request flow **including refresh-token rotation**, the Part 12 §8 KeyCredential Push model, and a clean provider model for OPC 10000-12 §7.2 `ApplicationSelfAdmin`. The work is split across phase-by-phase commits so it can be reviewed independently; nothing here changes wire-level behaviour or forces existing applications to migrate. ### Documentation All under `Docs/`: - `IdentityProviders.md` — full developer guide: three-layer model, recipes, configuration reference, "what gets auto-wired" matrix, JWS algorithm table, identity-augmenters section, **"Implementing your own provider"** how-to subsections for Entra / OIDC / Windows / AspNetCore (sibling files merged in per review feedback). - `DependencyInjection.md` — `Identity` sections with `appsettings.json` examples, identity-extension matrix, `WithAuthorizationService` + `WithKeyCredentialPush` + `AddIdentityAugmenter` extensions documented. - `RoleBasedUserManagement.md` — claim-criteria callout describing `IIdentityClaims` probe surface. - `AuthorizationService.md` — Architecture diagram + `IAccessTokenProvider` conceptual narrative restored from master; modern `StartRequestToken` / `FinishRequestToken` flow leads; refresh-token recipe added. - `KeyCredentialService.md` — Push model + Pull vs Push vs Bridge decision matrix + Hybrid (Push + Pull) section; explicit experimental warning for the bridge. - `GDS.md` — KeyCredential Push, `StartRequestToken`, `GdsApplicationSelfAdminProvider`, **shipped Refresh-token support with rotation policy and audit semantics** all documented. - `MigrationGuide.md` — User Identity Providers section listing `[Obsolete]` markers + mechanical recipes; SelfAdmin migration via `IIdentityAugmenter`; `EnableRefreshTokens` default note. - `README.md` index entries. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. |