fccffd0a0f
- Endpoint rewrite: the driver now connects to the configured host AND port (scheme match, advertised path kept) instead of the server-advertised URL — fixes NAT/port-map upstreams and lets an in-test proxy carry the session (SDK SelectEndpoint rule). - TcpStallProxy + OpcUaClientDriverStallWireTests: the in-place blip (#670) is now wire-tested on loopback — one Bad, frozen read stamp, same session, identical value republished Good, a post-recovery change arrives, no source-time regression after the SDK's late backlog flush. - Defect found by that test, fixed: a change published right after a stall rode a Publish response the SDK timed out and never republished, leaving the node Good on the pre-change value indefinitely. New trailing reconcile read (Timeout + KeepAliveInterval after recovery) publishes any Good value newer than the last accepted one, through the replay guard's mark. - IDriverConnectionLossSource (Core.Abstractions): opt-in definitive-loss signal. DriverInstanceActor attaches in PreStart / detaches in PostStop and turns a raise into a disconnect only while Connected. OpcUaClient raises once per session on keep-alive BadSessionIdInvalid/BadSessionClosed/BadSessionNotActivated/ BadServerHalted (measured: the SDK never re-opens a halted channel) or a dropped subscription; ambiguous codes stay with the #527 watchdog. - Replay guard: one Information summary per recovery when the window closes ("dropped N late notifications across M refs"), with a timer so quiet subscriptions log it too. - ReconnectPeriod deleted (options, AdminUI form, docs, tests); stored configs that carry reconnectPeriod still parse in the factory, probe and browser. - Docs: OpcUaClient.md, DriverLifecycle.md, driver-specs.md, CLAUDE.md.
278 lines
14 KiB
C#
278 lines
14 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
|
|
|
/// <summary>
|
|
/// OPC UA Client (gateway) driver configuration. Bound from <c>DriverConfig</c> JSON at
|
|
/// driver-host registration time. Models the settings documented in
|
|
/// <c>docs/v2/driver-specs.md</c> §8.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This driver connects to a REMOTE OPC UA server and re-exposes its address space
|
|
/// through the local OtOpcUa server — the opposite direction from the usual "server
|
|
/// exposes PLC data" flow. Tier A (pure managed, OPC Foundation reference SDK); universal
|
|
/// protections cover it.
|
|
/// </remarks>
|
|
// A record (not a plain class) so G-2b's OpcUaClientSecretResolution can produce a
|
|
// credential-resolved copy with a `with` expression — every property keeps its init setter.
|
|
public sealed record OpcUaClientDriverOptions
|
|
{
|
|
/// <summary>
|
|
/// Remote OPC UA endpoint URL, e.g. <c>opc.tcp://plc.internal:4840</c>. Convenience
|
|
/// shortcut for a single-endpoint deployment — equivalent to setting
|
|
/// <see cref="EndpointUrls"/> to a list with this one URL. When both are provided,
|
|
/// the list wins and <see cref="EndpointUrl"/> is ignored.
|
|
/// </summary>
|
|
public string EndpointUrl { get; init; } = "opc.tcp://localhost:4840";
|
|
|
|
/// <summary>
|
|
/// v3 authored raw tags for this driver instance — the deploy artifact (Wave C) populates this
|
|
/// with the cluster's raw OPC UA Client tags. Each <see cref="RawTagEntry"/> carries the tag's
|
|
/// <b>RawPath</b> identity plus a driver <c>TagConfig</c> blob whose <c>nodeId</c> key holds the
|
|
/// upstream OPC UA node id string the driver dials. The driver builds a <c>RawPath → nodeId</c>
|
|
/// resolution table (via <c>NamespaceMap</c>, in the sibling <c>.Session</c> project) from this
|
|
/// list at connect time; a
|
|
/// read/write/subscribe/history reference is always a RawPath resolved through that table.
|
|
/// </summary>
|
|
public IReadOnlyList<RawTagEntry> RawTags { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Ordered list of candidate endpoint URLs for failover. The driver tries each in
|
|
/// order at <c>OpcUaClientDriver.InitializeAsync</c> and on session drop;
|
|
/// the first URL that successfully connects wins. Typical use-case: an OPC UA server
|
|
/// pair running in hot-standby (primary 4840 + backup 4841) where either can serve
|
|
/// the same address space. Leave unset (or empty) to use <see cref="EndpointUrl"/>
|
|
/// as a single-URL shortcut.
|
|
/// </summary>
|
|
public IReadOnlyList<string> EndpointUrls { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Per-endpoint connect-attempt timeout during the failover sweep. Short enough that
|
|
/// cycling through several dead servers doesn't blow the overall init budget, long
|
|
/// enough to tolerate a slow TLS handshake on a healthy server. Applied independently
|
|
/// of <see cref="Timeout"/> which governs steady-state operations.
|
|
/// </summary>
|
|
public TimeSpan PerEndpointConnectTimeout { get; init; } = TimeSpan.FromSeconds(3);
|
|
|
|
/// <summary>
|
|
/// Security policy to require when selecting an endpoint. Either a
|
|
/// <see cref="OpcUaSecurityPolicy"/> enum constant or a free-form string (for
|
|
/// forward-compatibility with future OPC UA policies not yet in the enum).
|
|
/// Matched against <c>EndpointDescription.SecurityPolicyUri</c> suffix — the driver
|
|
/// connects to the first endpoint whose policy name matches AND whose mode matches
|
|
/// <see cref="SecurityMode"/>. When set to <see cref="OpcUaSecurityPolicy.None"/>
|
|
/// the driver picks any unsecured endpoint regardless of policy string.
|
|
/// </summary>
|
|
public OpcUaSecurityPolicy SecurityPolicy { get; init; } = OpcUaSecurityPolicy.None;
|
|
|
|
/// <summary>Security mode.</summary>
|
|
public OpcUaSecurityMode SecurityMode { get; init; } = OpcUaSecurityMode.None;
|
|
|
|
/// <summary>Authentication type.</summary>
|
|
public OpcUaAuthType AuthType { get; init; } = OpcUaAuthType.Anonymous;
|
|
|
|
/// <summary>User name (required only for <see cref="OpcUaAuthType.Username"/>).</summary>
|
|
public string? Username { get; init; }
|
|
|
|
/// <summary>Password (required only for <see cref="OpcUaAuthType.Username"/>).</summary>
|
|
public string? Password { get; init; }
|
|
|
|
/// <summary>
|
|
/// Filesystem path to the user-identity certificate (PFX/PEM). Required when
|
|
/// <see cref="AuthType"/> is <see cref="OpcUaAuthType.Certificate"/>. The driver
|
|
/// loads the cert + private key, which the remote server validates against its
|
|
/// <c>TrustedUserCertificates</c> store to authenticate the session's user token.
|
|
/// Leave unset to use the driver's application-instance certificate as the user
|
|
/// token (not typical — most deployments have a separate user cert).
|
|
/// </summary>
|
|
public string? UserCertificatePath { get; init; }
|
|
|
|
/// <summary>
|
|
/// Optional password that unlocks <see cref="UserCertificatePath"/> when the PFX is
|
|
/// protected. PEM files generally have their password on the adjacent key file; this
|
|
/// knob only applies to password-locked PFX.
|
|
/// </summary>
|
|
public string? UserCertificatePassword { get; init; }
|
|
|
|
/// <summary>Server-negotiated session timeout. Default 120s per driver-specs.md §8.</summary>
|
|
public TimeSpan SessionTimeout { get; init; } = TimeSpan.FromSeconds(120);
|
|
|
|
/// <summary>Client-side keep-alive interval.</summary>
|
|
public TimeSpan KeepAliveInterval { get; init; } = TimeSpan.FromSeconds(5);
|
|
|
|
/// <summary>
|
|
/// When <c>true</c>, the driver accepts any self-signed / untrusted server certificate.
|
|
/// Dev-only — must be <c>false</c> in production so MITM attacks against the opc.tcp
|
|
/// channel fail closed.
|
|
/// </summary>
|
|
public bool AutoAcceptCertificates { get; init; } = false;
|
|
|
|
/// <summary>
|
|
/// Application URI the driver reports during session creation. Must match the
|
|
/// subject-alt-name on the client certificate if one is used, which is why it's a
|
|
/// config knob rather than hard-coded.
|
|
/// </summary>
|
|
public string ApplicationUri { get; init; } = "urn:localhost:OtOpcUa:GatewayClient";
|
|
|
|
/// <summary>
|
|
/// Friendly name sent to the remote server for diagnostics. Shows up in the remote
|
|
/// server's session-list so operators can identify which gateway instance is calling.
|
|
/// </summary>
|
|
public string SessionName { get; init; } = "OtOpcUa-Gateway";
|
|
|
|
/// <summary>Connect + per-operation timeout.</summary>
|
|
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(10);
|
|
|
|
/// <summary>
|
|
/// Root NodeId to mirror. Default <c>null</c> = <c>ObjectsFolder</c> (i=85). Set to
|
|
/// a scoped root to restrict the address space the driver exposes locally — useful
|
|
/// when the remote server has tens of thousands of nodes and only a subset is
|
|
/// needed downstream.
|
|
/// </summary>
|
|
public string? BrowseRoot { get; init; }
|
|
|
|
/// <summary>
|
|
/// Cap on total nodes discovered during <c>DiscoverAsync</c>. Default 10_000 —
|
|
/// bounds memory on runaway remote servers without being so low that normal
|
|
/// deployments hit it. When the cap is reached discovery stops and a warning is
|
|
/// written to the driver health surface; the partially-discovered tree is still
|
|
/// projected into the local address space.
|
|
/// </summary>
|
|
[Range(1, int.MaxValue)]
|
|
public int MaxDiscoveredNodes { get; init; } = 10_000;
|
|
|
|
/// <summary>
|
|
/// Max hierarchical depth of the browse. Default 10 — deep enough for realistic
|
|
/// OPC UA information models, shallow enough that cyclic graphs can't spin the
|
|
/// browse forever.
|
|
/// </summary>
|
|
[Range(1, int.MaxValue)]
|
|
public int MaxBrowseDepth { get; init; } = 10;
|
|
|
|
/// <summary>
|
|
/// Timeout for the AdminUI Test Connect probe, in seconds. The AdminUI clamps to a
|
|
/// 60s server-side maximum; this default is what the form pre-fills for new instances.
|
|
/// </summary>
|
|
[Range(1, 60)]
|
|
public int ProbeTimeoutSeconds { get; init; } = 15;
|
|
|
|
// ---- #273 per-subscription tuning (defaults are the values that used to be hardcoded) ----
|
|
|
|
/// <summary>
|
|
/// Fixed publishing interval for every data-change subscription this driver creates. <c>null</c>
|
|
/// (default) keeps the historical behaviour: the interval the runtime asks for per
|
|
/// <c>SubscribeAsync</c> call, floored at 50 ms. When set it overrides the per-call value (the
|
|
/// 50 ms floor still applies); monitored-item sampling intervals follow it.
|
|
/// </summary>
|
|
public TimeSpan? SubscriptionPublishingInterval { get; init; }
|
|
|
|
/// <summary>
|
|
/// Publish cycles with no data before the server sends a keep-alive (default 10). Must be >= 1.
|
|
/// </summary>
|
|
public uint SubscriptionKeepAliveCount { get; init; } = 10;
|
|
|
|
/// <summary>
|
|
/// Publish cycles with no Publish request before the server drops the subscription (default 1000).
|
|
/// Must be at least 3x <see cref="SubscriptionKeepAliveCount"/> (OPC UA Part 4 §5.13.2).
|
|
/// </summary>
|
|
public uint SubscriptionLifetimeCount { get; init; } = 1000;
|
|
|
|
/// <summary>Max notifications per Publish response; <c>0</c> (default) = server decides.</summary>
|
|
public uint SubscriptionMaxNotificationsPerPublish { get; init; }
|
|
|
|
/// <summary>Relative subscription priority 0-255 (default 0).</summary>
|
|
public byte SubscriptionPriority { get; init; }
|
|
|
|
// ---- #277 ----
|
|
|
|
/// <summary>Certificate-revocation / issuer-store settings. Defaults preserve the SDK behaviour.</summary>
|
|
public OpcUaCertificateValidationOptions CertificateValidation { get; init; } = new();
|
|
|
|
// ---- #278 ----
|
|
|
|
/// <summary>
|
|
/// Optional discovery endpoint (a Local Discovery Server or the server itself), e.g.
|
|
/// <c>opc.tcp://lds.internal:4840</c>. When set, <c>InitializeAsync</c> calls <c>FindServers</c> and
|
|
/// <c>GetEndpoints</c> on it and tries the discovered URLs that match <see cref="SecurityPolicy"/> +
|
|
/// <see cref="SecurityMode"/> <i>after</i> the statically configured <see cref="EndpointUrls"/> /
|
|
/// <see cref="EndpointUrl"/> (pin the server with <see cref="DiscoveryServerUri"/>). A discovery failure is non-fatal: the static list is used. Discovery
|
|
/// itself always runs over an unsecured channel, as the spec requires. <c>null</c> (default) = off.
|
|
/// </summary>
|
|
public string? DiscoveryUrl { get; init; }
|
|
|
|
/// <summary>
|
|
/// ApplicationUri of the one server discovery may resolve to. An LDS usually registers several servers;
|
|
/// with this set only that server's endpoints are used. When unset and discovery finds more than one
|
|
/// server, connect fails with an error listing their ApplicationUris. Discovered endpoints are always
|
|
/// tried AFTER the configured <see cref="EndpointUrls"/>.
|
|
/// </summary>
|
|
public string? DiscoveryServerUri { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server-certificate revocation handling for the OPC UA Client driver (#277). The SDK reads CRLs from
|
|
/// the <c>crl</c> sub-folder of the trusted-peer / trusted-issuer stores; these settings only decide how
|
|
/// a certificate whose revocation status cannot be determined is treated and where the issuer store lives.
|
|
/// </summary>
|
|
public sealed record OpcUaCertificateValidationOptions
|
|
{
|
|
/// <summary>
|
|
/// When <c>true</c>, a server certificate whose revocation status is unknown (no CRL for its issuer, or a
|
|
/// stale one) is rejected. Default <c>false</c> keeps brownfield deployments without CRL infrastructure
|
|
/// connecting. A certificate that IS revoked is always rejected — even with
|
|
/// <see cref="OpcUaClientDriverOptions.AutoAcceptCertificates"/> set.
|
|
/// </summary>
|
|
public bool RejectUnknownRevocationStatus { get; init; }
|
|
|
|
/// <summary>
|
|
/// Directory used as the trusted-issuer certificate store. Its <c>crl/</c> sub-folder is where the SDK
|
|
/// looks for issuer CRLs (<c>.crl</c> files), so this is the knob for pointing the driver at an
|
|
/// operator-managed CRL set. <c>null</c>/empty (default) = <c>%LocalAppData%/OtOpcUa/pki/issuers</c>.
|
|
/// </summary>
|
|
public string? IssuerStorePath { get; init; }
|
|
}
|
|
|
|
/// <summary>OPC UA message security mode.</summary>
|
|
public enum OpcUaSecurityMode
|
|
{
|
|
None,
|
|
Sign,
|
|
SignAndEncrypt,
|
|
}
|
|
|
|
/// <summary>
|
|
/// OPC UA security policies recognized by the driver. Maps to the standard
|
|
/// <c>http://opcfoundation.org/UA/SecurityPolicy#</c> URI suffixes the SDK uses for
|
|
/// endpoint matching.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <see cref="Basic128Rsa15"/> and <see cref="Basic256"/> are <b>deprecated</b> per OPC UA
|
|
/// spec v1.04 — they remain in the enum only for brownfield interop with older servers.
|
|
/// Prefer <see cref="Basic256Sha256"/>, <see cref="Aes128_Sha256_RsaOaep"/>, or
|
|
/// <see cref="Aes256_Sha256_RsaPss"/> for new deployments.
|
|
/// </remarks>
|
|
public enum OpcUaSecurityPolicy
|
|
{
|
|
/// <summary>No security. Unsigned, unencrypted wire.</summary>
|
|
None,
|
|
/// <summary>Deprecated (OPC UA 1.04). Retained for legacy server interop.</summary>
|
|
Basic128Rsa15,
|
|
/// <summary>Deprecated (OPC UA 1.04). Retained for legacy server interop.</summary>
|
|
Basic256,
|
|
/// <summary>Recommended baseline for current deployments.</summary>
|
|
Basic256Sha256,
|
|
/// <summary>Current OPC UA policy; AES-128 + SHA-256 + RSA-OAEP.</summary>
|
|
Aes128_Sha256_RsaOaep,
|
|
/// <summary>Current OPC UA policy; AES-256 + SHA-256 + RSA-PSS.</summary>
|
|
Aes256_Sha256_RsaPss,
|
|
}
|
|
|
|
/// <summary>User authentication type sent to the remote server.</summary>
|
|
public enum OpcUaAuthType
|
|
{
|
|
Anonymous,
|
|
Username,
|
|
Certificate,
|
|
}
|