namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient; /// /// OPC UA Client (gateway) driver configuration. Bound from DriverConfig JSON at /// driver-host registration time. Models the settings documented in /// docs/v2/driver-specs.md §8. /// /// /// 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. /// public sealed class OpcUaClientDriverOptions { /// /// Remote OPC UA endpoint URL, e.g. opc.tcp://plc.internal:4840. Convenience /// shortcut for a single-endpoint deployment — equivalent to setting /// to a list with this one URL. When both are provided, /// the list wins and is ignored. /// public string EndpointUrl { get; init; } = "opc.tcp://localhost:4840"; /// /// Ordered list of candidate endpoint URLs for failover. The driver tries each in /// order at 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 /// as a single-URL shortcut. /// public IReadOnlyList EndpointUrls { get; init; } = []; /// /// 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 which governs steady-state operations. /// public TimeSpan PerEndpointConnectTimeout { get; init; } = TimeSpan.FromSeconds(3); /// /// Optional discovery URL pointing at a Local Discovery Server (LDS) or a server's /// own discovery endpoint. When set, the driver runs FindServers + /// GetEndpoints against this URL during /// and prepends the discovered endpoint URLs to the failover candidate list. When /// is empty (and only is set as /// a fallback), the discovered URLs replace the candidate list entirely so a /// discovery-driven deployment can be configured without specifying any endpoints /// up front. Discovery failures are non-fatal — the driver logs and falls back to the /// statically configured candidates. /// /// /// /// FindServers requires SecurityMode=None on the discovery channel per the /// OPC UA spec — discovery is unauthenticated even when the data channel uses /// Sign or SignAndEncrypt. The driver opens the discovery channel /// unsecured regardless of ; only the resulting data /// session is bound to the configured policy. /// /// /// Endpoints returned by discovery are filtered to those matching /// + before being added to /// the candidate list, so a discovery sweep against a multi-policy server only /// surfaces endpoints the driver could actually connect to. /// /// public string? DiscoveryUrl { get; init; } /// /// Security policy to require when selecting an endpoint. Either a /// enum constant or a free-form string (for /// forward-compatibility with future OPC UA policies not yet in the enum). /// Matched against EndpointDescription.SecurityPolicyUri suffix — the driver /// connects to the first endpoint whose policy name matches AND whose mode matches /// . When set to /// the driver picks any unsecured endpoint regardless of policy string. /// public OpcUaSecurityPolicy SecurityPolicy { get; init; } = OpcUaSecurityPolicy.None; /// Security mode. public OpcUaSecurityMode SecurityMode { get; init; } = OpcUaSecurityMode.None; /// Authentication type. public OpcUaAuthType AuthType { get; init; } = OpcUaAuthType.Anonymous; /// User name (required only for ). public string? Username { get; init; } /// Password (required only for ). public string? Password { get; init; } /// /// Filesystem path to the user-identity certificate (PFX/PEM). Required when /// is . The driver /// loads the cert + private key, which the remote server validates against its /// TrustedUserCertificates 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). /// public string? UserCertificatePath { get; init; } /// /// Optional password that unlocks when the PFX is /// protected. PEM files generally have their password on the adjacent key file; this /// knob only applies to password-locked PFX. /// public string? UserCertificatePassword { get; init; } /// Server-negotiated session timeout. Default 120s per driver-specs.md §8. public TimeSpan SessionTimeout { get; init; } = TimeSpan.FromSeconds(120); /// Client-side keep-alive interval. public TimeSpan KeepAliveInterval { get; init; } = TimeSpan.FromSeconds(5); /// Initial reconnect delay after a session drop. public TimeSpan ReconnectPeriod { get; init; } = TimeSpan.FromSeconds(5); /// /// When true, the driver accepts any self-signed / untrusted server certificate. /// Dev-only — must be false in production so MITM attacks against the opc.tcp /// channel fail closed. /// public bool AutoAcceptCertificates { get; init; } = false; /// /// 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. /// public string ApplicationUri { get; init; } = "urn:localhost:OtOpcUa:GatewayClient"; /// /// 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. /// public string SessionName { get; init; } = "OtOpcUa-Gateway"; /// Connect + per-operation timeout. public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(10); /// /// Root NodeId to mirror. Default null = ObjectsFolder (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. /// public string? BrowseRoot { get; init; } /// /// Cap on total nodes discovered during DiscoverAsync. 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. /// public int MaxDiscoveredNodes { get; init; } = 10_000; /// /// 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. /// public int MaxBrowseDepth { get; init; } = 10; /// /// Per-subscription tuning knobs applied when the driver creates data + alarm /// subscriptions on the upstream session. Defaults preserve the previous hard-coded /// values so existing deployments see no behaviour change. /// public OpcUaSubscriptionDefaults Subscriptions { get; init; } = new(); /// /// Server-certificate validation knobs applied during the /// CertificateValidator.CertificateValidation callback. Surfaces explicit /// handling for revoked certs (always rejected, never auto-accepted), unknown /// revocation status (rejected only when /// is set), SHA-1 signature rejection, and minimum RSA key size. Defaults preserve /// existing behaviour wherever possible — the one tightening is /// =true /// since SHA-1 is spec-deprecated for OPC UA. /// public OpcUaCertificateValidationOptions CertificateValidation { get; init; } = new(); /// /// Curation rules applied to the upstream address space during /// DiscoverAsync. Lets operators trim the mirrored tree to the subset their /// downstream clients actually need, rename namespace URIs so the local-side metadata /// stays consistent across upstream-server swaps, and override the default /// "Remote" root folder name. Defaults are empty / null which preserves the /// pre-curation behaviour exactly — empty include = include all. /// public OpcUaClientCurationOptions Curation { get; init; } = new(); /// /// When true, DiscoverAsync runs an additional pass that walks the upstream /// TypesFolder (i=86) — ObjectTypes (i=88), VariableTypes /// (i=89), DataTypes (i=90), ReferenceTypes (i=91) — and projects the /// discovered type-definition nodes into the local address space via /// IAddressSpaceBuilder.RegisterTypeNode. Default false — opt-in so /// existing deployments don't suddenly see a flood of type nodes after upgrade. Enable /// when downstream clients need the upstream type system to render structured values or /// decode custom event fields. /// /// /// /// The type-mirror pass uses Session.FetchTypeTreeAsync on each of the four /// root type nodes so the SDK's local TypeTree cache is populated efficiently (one /// batched browse per root rather than per-node round trips). This PR ships the /// structural mirror only — every type node is registered with its identity, /// super-type chain, and IsAbstract flag, but structured-type binary encodings are /// NOT primed. (The OPCFoundation SDK removed /// ISession.LoadDataTypeSystem(NodeId, CancellationToken) from the public /// surface in 1.5.378+; loading binary encodings now requires per-node walks of /// HasEncoding + dictionary nodes which is tracked as a follow-up.) Clients /// that need structured-type decoding can still consume /// Variant<ExtensionObject> on the wire. /// /// /// + /// still apply to the type /// walk; paths are slash-joined under their root (e.g. /// "ObjectTypes/BaseObjectType/SomeType"). Most operators want all types so /// empty include = include all is the right default. /// /// public bool MirrorTypeDefinitions { get; init; } = false; /// /// When true (default), the driver subscribes to /// BaseModelChangeEventType + GeneralModelChangeEventType on the /// upstream Server node (i=2253) at the end of . /// When the upstream advertises a topology change, the driver coalesces events over /// and triggers a re-import (equivalent to calling /// ReinitializeAsync) so the locally-mirrored address space tracks the upstream. /// /// /// /// The re-import path acquires the same _gate that read / write / browse / /// subscribe paths use, which means there's a brief browse-gap (≈ the upstream /// DiscoverAsync duration) during which downstream calls block on the /// driver's gate. Operators can disable the watch when the upstream topology is /// known-static and the gap isn't acceptable. /// /// public bool WatchModelChanges { get; init; } = true; /// /// Coalescing window for upstream ModelChangeEvent notifications. The first /// event in a window starts the timer; further events extend it; when the timer /// fires the driver runs one re-import regardless of how many events arrived. Default /// 5 seconds — long enough to absorb a bulk topology edit on the upstream server, /// short enough that single-node adds re-import promptly. /// public TimeSpan ModelChangeDebounce { get; init; } = TimeSpan.FromSeconds(5); } /// /// Selective import + namespace remap rules for the OPC UA Client driver. Pure local /// filtering inside BrowseRecursiveAsync + EnrichAndRegisterVariablesAsync; /// no new SDK calls. /// /// /// /// Glob semantics: patterns are matched against the slash-joined BrowseName /// segments accumulated during the browse pass (e.g. "Server/Diagnostics/SessionsDiagnosticsArray"). /// Two wildcards are supported — * matches any sequence of characters /// (including empty / slashes) and ? matches exactly one character. No /// character classes, no **, no escapes — keep the surface tight so the doc /// + behaviour stay simple. /// /// /// Empty = include all (existing behaviour). /// wins over when both match. /// Folders pruned by the rules are skipped wholesale — their descendants don't get /// browsed, which keeps the wire cost down on large servers. /// /// /// /// Glob patterns matched against the BrowsePath segment list. Empty = include all /// (default — preserves pre-curation behaviour). /// /// /// Glob patterns matched against the BrowsePath segment list. Wins over /// — useful for "include everything under Plant/* /// except Plant/Diagnostics" rules. /// /// /// Upstream-namespace-URI → local-namespace-URI translation table applied to the /// FullName field of DriverAttributeInfo when registering variables. /// The driver's stored FullName swaps the prefix before persisting so downstream /// clients see the remapped URI. Lookup is case-sensitive — match the upstream URI /// exactly. Defaults to empty (no remap). /// /// /// Replaces the default "Remote" folder name at the top of the mirrored tree. /// Useful when multiple OPC UA Client drivers are aggregated and operators need to /// distinguish them in the local browse tree. Default null = use "Remote". /// public sealed record OpcUaClientCurationOptions( IReadOnlyList? IncludePaths = null, IReadOnlyList? ExcludePaths = null, IReadOnlyDictionary? NamespaceRemap = null, string? RootAlias = null); /// /// Knobs governing the server-certificate validation callback. Plumbed onto /// rather than the top-level /// options to keep cert-related config grouped together. /// /// /// /// CRL discovery: the OPC UA SDK reads CRL files automatically from the /// crl/ sub-directory of each cert store (own, trusted, issuers). Drop the /// issuer's .crl in that folder and the SDK picks it up — no driver-side wiring /// required. When the directory is absent or empty, the SDK reports /// BadCertificateRevocationUnknown, which this driver gates with /// . /// /// /// /// Reject server certificates whose signature uses SHA-1. Default true — SHA-1 was /// deprecated by the OPC UA spec and is treated as a hard fail in production. Flip to /// false only for short-term interop with legacy controllers. /// /// /// When the SDK can't determine revocation status (no CRL present, or stale CRL), /// reject the cert if true; allow if false. Default false — many /// plant deployments don't run CRL infrastructure, and a hard-fail default would break /// them on first connection. Set true in environments with a managed PKI. /// /// /// Minimum RSA key size (bits) accepted. Certs with shorter keys are rejected. Default /// 2048 matches the current OPC UA spec floor; raise to 3072 or 4096 for stricter /// deployments. Non-RSA keys (ECC) bypass this check. /// public sealed record OpcUaCertificateValidationOptions( bool RejectSHA1SignedCertificates = true, bool RejectUnknownRevocationStatus = false, int MinimumCertificateKeySize = 2048); /// /// Tuning surface for OPC UA subscriptions created by . /// Lifted from the per-call hard-coded literals so operators can tune publish cadence, /// keep-alive ratio, and alarm-vs-data prioritisation without recompiling the driver. /// Defaults match the original hard-coded values (KeepAlive=10, Lifetime=1000, /// MaxNotifications=0 unlimited, Priority=0, MinPublishingInterval=50ms). /// /// /// Number of consecutive empty publish cycles before the server sends a keep-alive /// response. Default 10 — high enough to suppress idle traffic, low enough that the /// client notices a stalled subscription within ~5x the publish interval. /// /// /// Number of consecutive missed publish responses before the server tears down the /// subscription. Must be ≥3× per OPC UA spec; default 1000 /// gives ~100 keep-alives of slack which is conservative on flaky networks. /// /// /// Cap on notifications returned per publish response. 0 = unlimited (the OPC UA /// spec sentinel). Lower this to bound publish-message size on bursty servers. /// /// /// Subscription priority for data subscriptions (0..255). Higher = scheduled ahead of /// lower. Default 0 matches the SDK's default for ordinary tag subscriptions. /// /// /// Floor (ms) applied to publishingInterval requests. Sub-floor values are /// clamped up so wire-side negotiations don't waste round-trips on intervals the server /// will only round up anyway. Default 50ms. /// /// /// Subscription priority for the alarm subscription (0..255). Higher than /// by default (1 vs 0) so alarm publishes aren't starved during /// data-tag bursts. /// public sealed record OpcUaSubscriptionDefaults( int KeepAliveCount = 10, uint LifetimeCount = 1000, uint MaxNotificationsPerPublish = 0, byte Priority = 0, int MinPublishingIntervalMs = 50, byte AlarmsPriority = 1); /// OPC UA message security mode. public enum OpcUaSecurityMode { None, Sign, SignAndEncrypt, } /// /// OPC UA security policies recognized by the driver. Maps to the standard /// http://opcfoundation.org/UA/SecurityPolicy# URI suffixes the SDK uses for /// endpoint matching. /// /// /// and are deprecated per OPC UA /// spec v1.04 — they remain in the enum only for brownfield interop with older servers. /// Prefer , , or /// for new deployments. /// public enum OpcUaSecurityPolicy { /// No security. Unsigned, unencrypted wire. None, /// Deprecated (OPC UA 1.04). Retained for legacy server interop. Basic128Rsa15, /// Deprecated (OPC UA 1.04). Retained for legacy server interop. Basic256, /// Recommended baseline for current deployments. Basic256Sha256, /// Current OPC UA policy; AES-128 + SHA-256 + RSA-OAEP. Aes128_Sha256_RsaOaep, /// Current OPC UA policy; AES-256 + SHA-256 + RSA-PSS. Aes256_Sha256_RsaPss, } /// User authentication type sent to the remote server. public enum OpcUaAuthType { Anonymous, Username, Certificate, }