Files
lmxopcua/docs/OpcUaServer.md
Joseph Doherty 969b0847a1 docs: update path references for module-folder reorganization
Rewrite src/ and tests/ project paths in docs, CLAUDE.md, README.md, and
test-fixture READMEs to the new module-folder layout (Core/Server/Drivers/
Client/Tooling). References to retired v1 projects (Galaxy.Host/Proxy/Shared,
the legacy monolithic test projects) are left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 02:10:29 -04:00

8.2 KiB

OPC UA Server

The OPC UA server component (src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs) hosts the OPC UA stack and exposes one browsable subtree per registered driver. The server itself is driver-agnostic — Galaxy/MXAccess, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client are all plugged in as IDriver implementations via the capability interfaces in src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/.

Composition

OtOpcUaServer subclasses the OPC Foundation StandardServer and wires:

  • A DriverHost (src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs) which registers drivers and holds the per-instance IDriver references.
  • One DriverNodeManager per registered driver (src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs), constructed in CreateMasterNodeManager. Each manager owns its own namespace URI (urn:OtOpcUa:{DriverInstanceId}) and exposes the driver as a subtree under the standard Objects folder.
  • A CapabilityInvoker (src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs) per driver instance, keyed on (DriverInstanceId, HostName, DriverCapability) against the shared DriverResiliencePipelineBuilder. Every Read/Write/Discovery/Subscribe/HistoryRead/AlarmSubscribe call on the driver flows through this invoker so the Polly pipeline (retry / timeout / breaker / bulkhead) applies. The OTOPCUA0001 Roslyn analyzer enforces the wrapping at compile time.
  • An IUserAuthenticator (LDAP in production, injected stub in tests) for UserName token validation in the ImpersonateUser hook.
  • Optional AuthorizationGate + NodeScopeResolver (Phase 6.2) that sit in front of every dispatch call. In lax mode the gate passes through when the identity lacks LDAP groups so existing integration tests keep working; strict mode (Authorization:StrictMode = true) denies those cases.

OtOpcUaServer.DriverNodeManagers exposes the materialized list so the hosting layer can walk each one post-start and call GenericDriverNodeManager.BuildAddressSpaceAsync(manager) — the manager is passed as its own IAddressSpaceBuilder.

Configuration

Server wiring used to live in appsettings.json. It now flows from the SQL Server Config DB: ServerInstance + DriverInstance + Tag + NodeAcl rows are published as a generation via sp_PublishGeneration and loaded into the running process by the generation applier. The Admin UI (Blazor Server, docs/v2/admin-ui.md) is the operator surface — drafts accumulate edits; sp_ComputeGenerationDiff drives the DiffViewer preview; a UNS drag-reorder carries a DraftRevisionToken so Confirm re-checks against the current draft and returns 409 if it advanced (decision #161). See docs/v2/config-db-schema.md for the schema.

Environmental knobs that aren't per-tenant (bind address, port, PKI path) still live in appsettings.json on the Server project; everything tenant-scoped moved to the Config DB.

Transport

The server binds one TCP endpoint per ServerInstance (default opc.tcp://0.0.0.0:4840). The ApplicationConfiguration is built programmatically in the OpcUaApplicationHost — there are no UA XML files. Security profiles (None, Basic256Sha256-Sign, Basic256Sha256-SignAndEncrypt) are resolved from the ServerInstance.Security JSON at startup; the default profile is still None for backward compatibility. User token policies (Anonymous, UserName) are attached based on whether LDAP is configured. See docs/security.md for hardening.

Session impersonation

OtOpcUaServer.OnImpersonateUser handles the three token types:

  • AnonymousIdentityToken → default anonymous UserIdentity.
  • UserNameIdentityTokenIUserAuthenticator.AuthenticateAsync validates the credential (LdapUserAuthenticator in production). On success, the resolved display name + LDAP-derived roles are wrapped in a RoleBasedIdentity that implements IRoleBearer. DriverNodeManager.OnWriteValue reads these roles via context.UserIdentity is IRoleBearer and applies WriteAuthzPolicy per write.
  • Anything else → BadIdentityTokenInvalid.

The Phase 6.2 AuthorizationGate runs on top of this baseline: when configured it consults the cluster's permission trie (loaded from NodeAcl rows) using the session's UserAuthorizationState and can deny Read / HistoryRead / Write / Browse independently per tag. See docs/v2/acl-design.md.

Dispatch

Every service call the stack hands to DriverNodeManager is translated to the driver's capability interface and routed through CapabilityInvoker:

Service Capability Invoker method
Read IReadable.ReadAsync ExecuteAsync(DriverCapability.Read, host, …)
Write IWritable.WriteAsync ExecuteWriteAsync(host, isIdempotent, …) — honors WriteIdempotentAttribute (#143)
CreateMonitoredItems / DeleteMonitoredItems ISubscribable.SubscribeAsync/UnsubscribeAsync ExecuteAsync(DriverCapability.Subscribe, host, …)
HistoryRead (raw / processed / at-time / events) IHistoryProvider.*Async ExecuteAsync(DriverCapability.HistoryRead, host, …)
ConditionRefresh / Acknowledge IAlarmSource.*Async via AlarmSurfaceInvoker (fans out per host)

The host name fed to the invoker comes from IPerCallHostResolver.ResolveHost(fullReference) when the driver implements it (multi-host drivers: AB CIP, Modbus with per-device options). Single-host drivers fall back to DriverInstanceId, preserving pre-Phase-6.1 pipeline-key semantics (decision #144).

Redundancy

Redundancy.Enabled = true on the ServerInstance activates the RedundancyCoordinator + ServiceLevelCalculator (src/Server/ZB.MOM.WW.OtOpcUa.Server/Redundancy/). Standard OPC UA redundancy nodes (Server/ServerRedundancy/RedundancySupport, ServerUriArray, Server/ServiceLevel) are populated on startup; ServiceLevel recomputes whenever any driver's DriverHealth changes. The apply-lease mechanism prevents two instances from concurrently applying a generation. See docs/Redundancy.md.

Server class hierarchy

OtOpcUaServer extends StandardServer

  • CreateMasterNodeManager — Iterates _driverHost.RegisteredDriverIds, builds one DriverNodeManager per driver with its own CapabilityInvoker + resilience options (tier from DriverTypeRegistry, per-instance JSON overrides from DriverInstance.ResilienceConfig via DriverResilienceOptionsParser). The managers are wrapped in a MasterNodeManager with no additional core managers.
  • OnServerStarted — Hooks SessionManager.ImpersonateUser for LDAP auth. Redundancy + server-capability population happens via OpcUaApplicationHost.
  • LoadServerProperties — Manufacturer OtOpcUa, Product OtOpcUa.Server, ProductUri urn:OtOpcUa:Server.

ServerCapabilities

OpcUaApplicationHost populates Server/ServerCapabilities with StandardUA2017, en locale, 100 ms MinSupportedSampleRate, 4 MB message caps, and per-operation limits (1000 per Read/Write/Browse/TranslateBrowsePaths/MonitoredItems/HistoryRead; 0 for MethodCall/NodeManagement/HistoryUpdate).

Certificate handling

Certificate stores default to %LOCALAPPDATA%\OPC Foundation\pki\ (directory-based):

Store Path suffix
Own pki/own
Trusted issuers pki/issuer
Trusted peers pki/trusted
Rejected pki/rejected

Security.AutoAcceptClientCertificates (default true) and RejectSHA1Certificates (default true) are honored. The server certificate is always created — even for None-only deployments — because UserName token encryption needs it.

Key source files

  • src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.csStandardServer subclass + ImpersonateUser hook
  • src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs — per-driver CustomNodeManager2 + dispatch surface
  • src/Server/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs — programmatic ApplicationConfiguration + lifecycle
  • src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs — driver registration
  • src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs — Polly pipeline entry point
  • src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/ — Phase 6.2 permission trie + evaluator
  • src/Server/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs — stack-to-evaluator bridge