## Summary
### Documentation
- Stage the documentation index as a learning path: a first application,
independent client, server, and PubSub paths, then data and services,
companion models, production, advanced integration, and draft or
experimental capabilities.
- Add Getting started (repository samples or NuGet packages), OPC UA
concepts, Your first information model, and a production readiness
checklist.
- Add Native AOT application publishing, a first PubSub publisher and
subscriber exchange, and the UDP transport options.
- Add or expand contents navigation throughout `docs/`, and keep
contributor documentation separate.
- Correct outdated SDK examples and capability guidance, consolidate
repeated explanations into canonical guides, and repair local links and
heading structure.
- Rewrite What's New in 2.0 as a concise feature overview with short
linked details; keep migration instructions in the Migration Guide.
- Expand the feature catalogue with specification IDs, GDS, LDS, the OPC
10000-21 onboarding registrar, ISA-95, xRegistry, OpenUSD, and
sample-local models; distinguish draft status, explain crypto
pluggability, and bring performance guidance forward.
- Address review feedback on wording, active monitored items during
shadow reload, and the `Samples.md` filename and references.
- Regenerate the `opcua-v20-migration` plugin's bundled copy of the
migration guides so it matches the edited `docs/migrate/2.0.x` guides.
- Merge the latest `master`, preserving upstream resource-isolation
guidance within the task-based documentation index.
- Retain deferred Lingity findings where appropriate; this PR does not
claim that every document meets the Lingity score thresholds.
### Fixes found while verifying the guides
Each fix has regression tests that fail without it.
- **Client:** V2 subscriptions compared their lifetime with the
operation timeout instead of the session timeout, and logged a spurious
warning. `ISubscriptionEngineContext`, which is new in 2.0, gains a
`SessionTimeout` property; implementations must add it.
- **PubSub:** a subscriber now registers the `DataSetMetaData`
configured on its readers (OPC 10000-14 6.2.9.4), and the UADP decoder
decodes DataSetMessages that carry no ConfigurationVersion. Before, a
secured subscriber of the reference sample received no DataSets.
- **Source generator:** a ModelDesign method that declares its arguments
inline, without a method type, produced code that did not compile; it
now uses `MethodState`. The generated code of existing models is
unchanged.
- **MinimalClient sample:** waits for its first data-change notification
before it disconnects.
### Known issues, not addressed here
- UDP multicast: the publisher does not send on the interface that the
subscriber joins, and `MulticastLoopback` defaults to false. The PubSub
guide uses unicast for the exchange on one computer.
- The UDP metadata announcement is sent unsecured on secured writer
groups; a secured subscriber drops it with a warning.
- A server does not start in globalization-invariant mode, because
`ResourceManager` creates `CultureInfo("en-US")`. The Native AOT guide
cautions against the mode.
## Validation
- `git diff --check` and
`./.azurepipelines/validate-migration-plugin.ps1` (15 bundled migration
docs validated, plugin manifests synchronized).
- Local Markdown links, anchors, file-link casing, and contents lists
checked across all 87 documents under `docs/`. The index links every
top-level guide and every target of the previous index.
- Both Getting started routes, Your first information model, the PubSub
exchange, and the Native AOT publish of the Getting started client and
server were run as documented. The NuGet route and the tutorial use the
public `2.0.0-preview.5` packages.
- The changed libraries build for all target frameworks in Release
without warnings.
- Tests on .NET 10: Opc.Ua.SourceGeneration.Tests 199 passed;
Opc.Ua.SourceGeneration.Core.Tests 3,977 passed, 8 skipped;
Opc.Ua.Server.Tests `Fluent` category 621 passed; Opc.Ua.Client.Tests
subscription-engine tests 70 passed; Opc.Ua.PubSub.Tests 1,374 passed, 2
skipped; PubSub fuzz replay 145 passed. The new and affected tests also
pass on .NET Framework 4.8.
- Earlier merge validation: 174 resource-isolation tests passed on .NET
10.
- The full solution suite was not run locally.
Review and validation artifacts are maintained outside the repository.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
18 KiB
Reverse Connect
Overview
The stack supports Reverse Connect through these components:
- The C# stack implements the
ReverseHellomessage for clients and servers. - The server library provides server-initiated connections through
ReverseConnectServer. The regular dependency-injection server uses this support automatically. Configuration options control client locations and timeouts, and the API lets applications manage client connections. - The client library provides
ReverseConnectManagerto acceptReverseHellomessages. Applications can register for connections by callback or wait for a message matching a server endpoint and application URI. Optional server-URI and endpoint-URL filters let multiple clients share one listener. - The Console Reference Server supports reverse connect through its configuration XML.
- The Console Reference Client can initiate a reverse connection through command-line options.
- The Aggregation Server supports incoming and outgoing reverse connections.
Contents
- Overview
- Reverse Connect Handshake
- Sharing a listener across multiple Servers
- Server-side dependency injection
- Dependency-injection lifecycle
- Configuration Extensions
- WSS reverse-connect
- Kestrel-hosted opc.tcp reverse-connect
- Known limitations and issues
Reverse Connect Handshake
More details on the reverse connect handshake can be found in the OPC UA spec Part 6, Establishing a connection.
The ReverseHello message allows Servers behind firewalls to initiate communication with Clients. This requires that the Server be pre-configured with the location of the Client. The Server adds a configuration option that can be used to initiate a ReverseHello with the Client running behind a firewall.
Once the reverse connection is established, the Server will automatically re-establish the connection if it is closed. Most servers continue sending ReverseHello messages after a client connects.
This implementation offers three behaviors: continue sending messages, allow
only one connection, or stop sending after the server reaches its maximum
session count. In single-connection mode, a rejected connection suspends
further messages for a configurable timeout. A client rejects an unsupported
or unwanted connection with BadTcpMessageTypeInvalid.
To accept reverse connections, configure a client ReverseConnectManager to
invoke registered callbacks or hold incoming connections open for a
configurable timeout. An application can register a callback to accept or
reject requests, or start a connection and wait for a matching
ReverseHello. If the manager already holds an open connection to the
server, the client can establish the session without waiting.
The client transport implements ITransportListener and routes incoming
connections through ReverseConnectionManager. One client port can accept
ReverseHello messages from multiple servers. Register callbacks for a
specific server URI, endpoint URL, or any incoming connection.
When a client receives ReverseHello, its callback receives an
ITransportWaitingConnection. Pass this connection object to the session
connect API to create a session, much like connecting with an endpoint URL.
The client uses the open socket to send Hello back to the server and
continue secure-channel establishment.
Alternatively, call the Connect API with a configured
ReverseConnectionManager. The client waits for an incoming connection and
establishes the session if it arrives before the timeout expires. Existing
applications can adopt this pattern without changing their broader
connection flow.
If no client responds to ReverseHello, or the client rejects it, the
channel closes with BadTcpMessageTypeInvalid. The server should interpret
this as an indication that the client is not configured to accept that
server's reverse connections.
After accepting the connection, a client that needs a secure channel must
call GetEndpoints to retrieve the server certificate. The client then
closes that channel and waits for the server to reconnect. On reconnection,
the client can use the cached security information to connect securely.
An optimized server can send ReverseHello immediately after GetEndpoints
to reduce this delay.
Server auto-reconnect is essential because clients close the socket when
the SecureChannel closes. The specification requires a server to stop
reconnecting if it receives BadTcpMessageTypeInvalid. Older peers that do
not support ReverseHello return this error. A client can also return it
when a user rejects the connection.
In this implementation, a server configured for a single connection waits
for an extended, configurable timeout before sending another ReverseHello.
In other modes, the server continues sending ReverseHello messages at the
configured interval.
Sharing a listener across multiple Servers
A reverse-connect listener remains bound for the lifetime of its ReverseConnectManager. Seeing the listener port remain in the LISTENING state after a Session is established is expected. The listening socket accepts additional transport connections while each accepted socket is handed to the Session that claimed its ReverseHello message. Dispose the manager when the listener should be released.
Use one shared ReverseConnectManager for all Servers that connect to the same Client URL. Register or wait for each Server separately by using its Server EndpointUrl and, preferably, its ServerUri. The fluent dependency-injection integration registers the manager as a singleton.
A ManagedSession needs that manager for recovery as well as initial connection.
A waiting ITransportWaitingConnection supplied directly to CreateAsync is
single-use. Without a ReverseConnectManager, recovery fails through the configured
reconnect policy instead of obtaining a fresh connection; it never silently
switches to an outbound connection.
await using var manager = new ReverseConnectManager(telemetry);
manager.AddEndpoint(new Uri("opc.tcp://client-host:65300"));
await manager.StartServiceAsync(
new ReverseConnectClientConfiguration
{
HoldTime = 15000,
WaitTimeout = 20000
},
cancellationToken).ConfigureAwait(false);
Task<ITransportWaitingConnection> serverA = manager.WaitForConnectionAsync(
new Uri("opc.tcp://server-a:4840"),
"urn:example:server-a",
cancellationToken);
Task<ITransportWaitingConnection> serverB = manager.WaitForConnectionAsync(
new Uri("opc.tcp://server-b:4840"),
"urn:example:server-b",
cancellationToken);
await Task.WhenAll(serverA, serverB).ConfigureAwait(false);
Pass each returned ITransportWaitingConnection to the session factory for the corresponding Server.
HoldTime controls how long an incoming ReverseHello that does not match any registration yet is held open before it is rejected. This matters when several Servers connect before the application has registered a waiting connection for each of them: every held connection waits for the remainder of its own hold time, also when a registration for a different Server arrives in the meantime. A Server whose hold time expires without a matching registration is rejected and reconnects on its next ReverseHello interval. A committed stop or dispose releases the held connections immediately.
StartServiceAsync validates and prepares the complete listener set before it changes a running service. If activation fails after existing listeners have stopped, the manager recreates and reopens the previous configuration. Cancellation cleans partially initialized candidates and either preserves or restores the prior service. Use await manager.StopServiceAsync(...) for an explicit stop and await manager.DisposeAsync() (or await using) for teardown.
The synchronous StartService, RegisterWaitingConnection, and Dispose APIs remain as obsolete compatibility wrappers. New code should use StartServiceAsync, RegisterWaitingConnectionAsync, and DisposeAsync; the compatibility wrappers may block a caller thread.
Server-side dependency injection
The regular AddServer(...) host uses
DependencyInjectionStandardServer, which derives from
ReverseConnectServer. Configured outbound reverse connections start
and stop with the hosted server. Existing DI hooks for node managers,
session and subscription services, identity, and startup tasks coexist
with reverse connect; an application does not need a server subclass.
Queued callbacks from disposed or superseded retry timers are ignored,
including during startup-failure cleanup.
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua.Server.Hosting;
services.AddOpcUa()
.AddServer(options =>
{
options.ApplicationName = "PlantServer";
options.EndpointUrls.Add("opc.tcp://localhost:4840/PlantServer");
})
.AddReverseConnect(options =>
{
options.ConnectIntervalMs = 15000;
options.Clients.Add(new ServerReverseConnectClientOptions
{
EndpointUrl = "opc.tcp://client.example.com:65300",
Timeout = 30000,
MaxSessionCount = 1,
Enabled = true
});
});
AddReverseConnect(...) configures OpcUaServerOptions.ReverseConnect.
The projection into ServerConfiguration.ReverseConnect is unchanged,
and the same options can be bound from OpcUa:Server:ReverseConnect.
On server configuration reload, removing the ReverseConnect section or
emptying its clients removes only configuration-owned connections. Connections
added through AddReverseConnection retain their settings and are not
overwritten by a configured client with the same endpoint URL.
When ConfigurationFile or ConfigurationStream supplies the
application configuration, that document is authoritative. The
ReverseConnect options and AddReverseConnect(...) shortcut do not
overwrite it. Use ConfigureLoadedConfiguration to change loaded
settings; this example retains the file's clients and changes only
their retry interval:
services.AddOpcUa().AddServer(options =>
{
options.ConfigurationFile = "PlantServer.Config.xml";
options.ConfigureLoadedConfiguration = configuration =>
{
var reverseConnect = configuration.ServerConfiguration.ReverseConnect
?? throw new InvalidOperationException(
"Configure reverse-connect clients in the XML file.");
reverseConnect.ConnectInterval = 20000;
};
});
The ordinary StandardServer is unchanged. Applications constructing
servers manually still choose ReverseConnectServer explicitly when
they need server-initiated connections; setting reverse-connect
configuration alone does not add that behavior to StandardServer.
See Dependency Injection
for the complete options shape and other hosted-server customization.
Dependency-injection lifecycle
AddClient(...) registers one singleton ReverseConnectManager, an IReverseConnectConfigurationProvider, and an internal hosted service. In a .NET Generic Host, the hosted service eagerly opens configured listeners during host startup and closes them during host shutdown. In a plain ServiceCollection without a running host, WaitForConnectionAsync and RegisterWaitingConnectionAsync call EnsureStartedAsync lazily. Resolving the manager itself never blocks on listener startup.
services
.AddOpcUa()
.AddClient(options =>
{
options.Configuration = applicationConfiguration;
options.ReverseConnect = new ClientReverseConnectOptions
{
HoldTimeMs = 15000,
WaitTimeoutMs = 20000
};
options.ReverseConnect.ClientEndpointUrls.Add(
"opc.tcp://client-host:65300");
});
Applications can replace the default pass-through provider to asynchronously validate or transform the effective listener configuration before any active listener is stopped:
services.AddSingleton<IReverseConnectConfigurationProvider, MyProvider>();
Providers run outside the manager's lifecycle gate. Provider exceptions reject the candidate while the current service remains active. The former protected OnUpdateConfiguration hooks were removed; custom configuration logic belongs in IReverseConnectConfigurationProvider.
Do not create a separate manager for each Server when those managers use the same local listener URL. Only one listener can bind a given host and port. An invalid URL reports BadTcpEndpointUrlInvalid, an unsupported transport retains its transport-specific status, and a bind or listener-open failure reports BadNoCommunication. Startup diagnostics identify the affected endpoint URLs, and listeners opened by a failed attempt are closed instead of allowing a later connection wait to time out.
This behavior follows OPC UA Part 6, 7.1.3, which defines a separate transport connection for each reverse connection and requires Servers to maintain an available socket to each configured Client. OPC UA Part 12, 4.4.2 defines one or more Client URLs that allow Servers to connect. Clients shall validate the ServerUri and EndpointUrl as described in OPC UA Part 2, 6.14.
Configuration Extensions
This configuration sample shows the configuration setting for a reverse connection on port 65300.
The Server configuration extension to connect to one or more reverse connect clients:
</ServerConfiguration>
...
<ReverseConnect>
<Clients>
<ReverseConnectClient>
<EndpointUrl>opc.tcp://localhost:65300</EndpointUrl>
<Timeout>30000</Timeout>
</ReverseConnectClient>
</Clients>
<ConnectInterval>15000</ConnectInterval>
<ConnectTimeout>30000</ConnectTimeout>
<RejectTimeout>60000</RejectTimeout>
</ReverseConnect>
</ServerConfiguration>
The Client configuration extension to allow incoming connections for one or more reverse connect servers:
<ClientConfiguration>
...
<ReverseConnect>
<ClientEndpoints>
<ClientEndpoint>
<EndpointUrl>opc.tcp://localhost:65300</EndpointUrl>
</ClientEndpoint>
</ClientEndpoints>
<HoldTime>15000</HoldTime>
<WaitTimeout>20000</WaitTimeout>
</ReverseConnect>
</ClientConfiguration>
WSS reverse-connect (opc.wss://)
The WSS reverse-connect path covers the same two halves as the TCP path but layered over TLS + WebSocket:
-
Server-side outbound —
HttpsTransportListener.CreateReverseConnectionopens an outboundClientWebSocketto the configured client URI, wraps the resultingWebSocketin aWebSocketClientByteTransport, and drives the reverse-hello handshake via the sameTcpServerChannel.BeginReverseConnect(... IUaSCByteTransport ...)overload the TCP path uses. The server'sReverseConnectconfiguration block acceptsopc.wss://...URIs identical to the TCP form above. -
Client-side listener —
ReverseConnectHost.CreateListeneraccepts an optional TLS certificate (and validator) pair foropc.wss://reverse-connect endpoints. The host wires the cert into the Kestrel-backedHttpsTransportListenerviasettings.ReverseConnectListener = true, dispatches each accepted WSS upgrade to aTcpReverseConnectChannel, and firesConnectionWaitingviaTransferListenerChannelAsyncwhen theReverseHelloarrives. The application takes ownership of theITransportWaitingConnectionexactly as it would for the TCP path.
ReverseConnectManager.AddEndpoint gained an additive overload to
support TLS-terminating listeners:
// Old: works for opc.tcp:// (no TLS state needed at bind time).
manager.AddEndpoint(new Uri("opc.tcp://localhost:65300"));
// New: also works for opc.wss:// because the CertificateManager is
// supplied at AddEndpoint time (m_appConfig is otherwise only set
// when StartService runs - too late for WSS listeners that need the
// server certificate at bind time).
manager.AddEndpoint(new Uri("opc.wss://localhost:65300"), config);
await manager.StartServiceAsync(config, cancellationToken)
.ConfigureAwait(false);
The original single-parameter AddEndpoint(Uri) is unchanged for
back-compat (opc.tcp consumers do not need the new overload).
Kestrel-hosted opc.tcp reverse-connect (opt-in)
The Opc.Ua.Bindings.Https package serves the opc.tcp
listener from a Kestrel IHost instead of raw Socket + SAEA (an
opt-in alternative on net8.0+ to the default raw-socket listener that
ships in Opc.Ua.Core). It
supports the full forward AND reverse-connect listener modes the
raw-socket TcpTransportListener does. To use it, install the binding
via DI before opening the listener:
// Microsoft.Extensions.DependencyInjection consumers:
services
.AddOpcUa()
.AddOpcTcpTransport() // raw-socket opc.tcp default
.AddKestrelOpcTcpTransport(); // overrides with Kestrel (last-writer-wins)
// Non-DI consumers (e.g. test fixtures):
DefaultTransportBindingRegistry registry =
DefaultTransportBindingRegistry.WithDefaultTcp();
registry.RegisterListenerFactory(new KestrelTcpTransportListenerFactory());
// Forward the registry to:
// - ServerBase via the new ctor overload or .TransportBindings setter
// (server-side reverse-connect outbound and forward listeners),
// - ReverseConnectManager.TransportBindings (client-side reverse-connect
// listener: every AddEndpoint(Uri,...) call constructs a
// ReverseConnectHost that resolves the right factory from this registry).
The default raw-socket implementation stays available for
deployments that want to avoid the ASP.NET Core dependency in
Opc.Ua.Core.
Known limitations and issues
- Only a limited number of samples is available yet, the Reference Server, the Aggregation Server and the Console server and client.