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);
///
/// Reverse-connect (server-initiated) configuration. When
/// is true the driver flips the
/// transport direction: instead of dialling the upstream server, it opens a TCP
/// listener on and waits for the
/// upstream server to initiate the connection ("ReverseHello"). Required for
/// OT-DMZ deployments where the firewall only permits outbound traffic from the
/// plant network — the upstream server reaches out, the gateway listens.
///
public ReverseConnectOptions ReverseConnect { get; init; } = new();
}
///
/// Driver knobs for OPC UA reverse-connect (server-initiated) sessions. Mirrors the
/// SDK's Opc.Ua.Client.ReverseConnectManager surface but expressed as plain
/// config so the driver can decide listener-mode vs dial-mode at startup.
///
///
///
/// Direction: in conventional OPC UA the client opens the TCP connection
/// to the server. Reverse-connect inverts this — the server initiates a TCP
/// connection to a listener the client exposes, then sends a ReverseHello
/// message naming itself; the client picks the right session config and continues
/// the OPC UA handshake on the inbound socket. Critical for OT-DMZ networks where
/// the plant firewall only allows outbound traffic.
///
///
/// Singleton listener: a single ReverseConnectManager per process
/// keyed on multiplexes inbound connections across
/// driver instances. Two drivers configured with the same listener URL share one
/// underlying TCP socket; the manager dispatches by the upstream's reported
/// ServerUri. See ReverseConnectListener.
///
///
///
/// When true, the driver opens a listener at and
/// waits for the upstream server to initiate the session. When false
/// (default), the driver uses the conventional dial path against
/// .
///
///
/// Local listener URL the SDK binds when reverse-connect is enabled. Typically
/// opc.tcp://0.0.0.0:4844 to accept on every interface, or pinned to a
/// specific NIC for multi-homed gateways. Required when
/// is true.
///
///
/// The upstream server's ApplicationUri the driver expects to see in the
/// ReverseHello. The SDK passes this as the serverUri filter to
/// WaitForConnectionAsync so connections from a different upstream are ignored
/// — important when the listener is shared across multiple drivers and several
/// upstreams might dial in. Leave null to accept the first connection regardless
/// (only safe when exactly one upstream targets the listener).
///
public sealed record ReverseConnectOptions(
bool Enabled = false,
string? ListenerUrl = null,
string? ExpectedServerUri = null);
///
/// 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,
}