Files
UA-.NETStandard/docs/Transports.md
Julius PfrommerandJulius Pfrommer 2fb20c06a2 Use standardized OPC UA WebSocket transport profile URIs (#4133)
The WSS transport constants used the unregistered uawss- prefix for the
UA-SecureConversation/UA-Binary and UA-JSON profiles.

Use the profile URIs registered by the OPC Foundation:

https://profiles.opcfoundation.org/profile/254
https://profiles.opcfoundation.org/profile/253

The WebSocket mappings and sub-protocols are specified in OPC UA Part 6
section 7.5.2:

https://reference.opcfoundation.org/Core/Part6/v105/docs/7.5.2

Update the transport-profile helpers, regression tests, and
documentation accordingly so endpoint discovery publishes identifiers
recognized by conforming OPC UA clients and servers.

Tests: dotnet test tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj -f
net10.0 --filter FullyQualifiedName~TransportProfileHelpersTests (39
passed)

# Description

_Describe the changes here to communicate to the maintainers why they
should accept this pull request. By default - this will become the
Commit message after merging and thus define history._

## Related Issues

_Reference all GitHub issues this PR addresses. If there is no issue
yet, open one and link it here._

_If this is a relatively large or complex change, a design must have
been discussed in the related tracking issue and signed off (which
becomes the Architectural Decision Record (ADR))._

- Fixes #github-issue-number, ...

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

Co-authored-by: Julius Pfrommer <julius.pfrommer@o6-automation.com>
2026-07-31 09:39:11 +02:00

451 lines
21 KiB
Markdown

# Transport Profiles
This document is the practical companion to [`Profiles.md`](Profiles.md) and
describes how the OPC UA .NET Standard stack ships each of the transport
profiles defined in OPC UA Part 6 §7. It is aimed at developers who need
to host or connect to OPC UA servers over something other than the
default `opc.tcp://` transport — for example to traverse firewalls or
to integrate with web-based tooling.
## Transport profile matrix
| Profile | URL scheme | Wire format | UA Secure Conversation | Security modes |
|---|---|---|---|---|
| `uatcp-uasc-uabinary` | `opc.tcp://` | UA Binary | yes | `None`, `Sign`, `SignAndEncrypt` |
| `https-uabinary` | `opc.https://`, `https://` | UA Binary in HTTP body (`application/octet-stream`) | yes (one chunk per POST) | `None`, `Sign`, `SignAndEncrypt` |
| `https-uajson` | `opc.https://`, `https://` | UA JSON in HTTP body (`application/opcua+uajson`) | **no** — TLS only | `None` only |
| [`https-uajson-openapi`](https://profiles.opcfoundation.org/profile/2338) | `opc.https://`, `https://` | OpenAPI Mapping (Part 6 §G.3): per-service `POST /<service>` with body = `<Service>Request` JSON (`application/json; encoding=compact\|verbose`) | **no** — TLS only | `None` only |
| `wss-uasc-uabinary` | `opc.wss://`, `wss://` | UA Binary in WebSocket binary frame (sub-protocol `opcua+uacp`) | yes | `None`, `Sign`, `SignAndEncrypt` |
| `wss-uajson` | `opc.wss://`, `wss://` | UA JSON in WebSocket text frame (sub-protocol `opcua+uajson`) | **no** — TLS only | `None` only |
| [`wss-uajson-openapi`](https://profiles.opcfoundation.org/profile/2339) | `opc.wss://`, `wss://` | OpenAPI Mapping over WebSocket text frame (sub-protocol `opcua+openapi` / `opcua+openapi+<accesstoken>`) | **no** — TLS only | `None` only |
The JSON / OpenAPI profiles do not negotiate a UA SecureChannel; transport
security is provided exclusively by the surrounding TLS connection.
Servers MUST advertise these endpoints with `MessageSecurityMode.None`
and `SecurityPolicyUri = None`. The `https-uajson` and WSS JSON
profiles use the Compact (reversible) flavour mandated by
Part 6 §5.4.9 (`JsonEncoderOptions.Compact`). The OpenAPI profiles
select between Compact (default, mandatory) and Verbose via
the `application/json; encoding=compact|verbose` media-type parameter
on `Content-Type` / `Accept` — see [`WebApi.md`](WebApi.md) for the
full mapping table.
The OPC UA Profiles surface exposes the two OpenAPI profile URIs as
`Profiles.HttpsOpenApiTransport` (profile/2338) and
`Profiles.WssOpenApiTransport` (profile/2339); the `HttpsServiceHost`
emits the HTTPS twin as a discovery-only `EndpointDescription`
alongside each `SecurityMode.None` HTTPS-binary endpoint so discovery
clients see the OpenAPI route without hard-coding the URL.
## Assembly layout
* **`Opc.Ua.Core`** (this is what every Server / Client application
references):
- `IUaSCByteTransport` — the runtime transport boundary used by the
UA Secure Conversation channel pipeline.
- `TcpByteTransport`, `TcpByteTransportFactory` — TCP implementation.
- `TcpTransportChannel` + `TcpTransportChannelFactory` — client-side
channel for `opc.tcp://`.
- `HttpsTransportChannel` + `HttpsTransportChannelFactory`,
`OpcHttpsTransportChannelFactory` — polymorphic client-side channel
that handles both `https-uabinary` and `https-uajson` based on the
endpoint's `TransportProfileUri`.
* **`Opc.Ua.Bindings.Https`** (Kestrel-dependent; pulled in
automatically when an HTTPS or WSS endpoint is configured):
- `HttpsTransportListener` — Kestrel-hosted listener that dispatches
requests on `(Upgrade, ContentType)` to the binary / JSON / WSS
handlers.
- `HttpsTransportListenerFactory`, `OpcHttpsTransportListenerFactory`
— listener factories for `https://` and `opc.https://`.
- `WssTransportListenerFactory`, `OpcWssTransportListenerFactory` —
listener factories for `wss://` and `opc.wss://`.
- `WebSocketClientByteTransport` + `WebSocketClientByteTransportFactory`,
`WssTransportChannel`, `WssTransportChannelFactory`,
`OpcWssTransportChannelFactory` — client-side WSS+uacp.
- `WssJsonTransportChannel`, `WssJsonTransportChannelFactory` —
client-side WSS+uajson (per-request `ClientWebSocket`).
- `JsonRequestMapper` — shared JSON encode / decode helper used by
both server handlers.
For both WSS variants the HTTPS / WSS factories live in
`Opc.Ua.Bindings.Https`; consumers register them via the
`AddHttpsTransport()` / `AddWssTransport()` DI extensions on
`IOpcUaBuilder`, or by constructing a `DefaultTransportBindingRegistry`
and calling `RegisterListenerFactory` / `RegisterChannelFactory`
directly for non-DI hosts.
`AddHttpsTransport(options => ...)` is available as a one-shot registration
for the Kestrel HTTPS stack. It registers HTTPS and WSS by default and can
attach the WebApi binding and authentication callbacks in the same fluent
chain. `AddWebApiTransport()` and `AddKestrelOpcTcpTransport()` are
order-independent with the underlying HTTPS/TCP registrations.
## Server-side configuration
Servers declare endpoints via `ApplicationConfiguration` →
`ServerConfiguration` → `BaseAddresses`. Each base address yields a
single listener for its scheme.
If a discovery or hosted server has a non-TCP base address but the matching
transport binding has not been registered, startup throws a clear error that
names the missing scheme and suggested `Add*Transport()` method.
```xml
<BaseAddresses>
<ua:String>opc.tcp://localhost:62541/MyServer</ua:String>
<ua:String>opc.https://localhost:62540/MyServer</ua:String>
<ua:String>opc.wss://localhost:62543/MyServer</ua:String>
</BaseAddresses>
```
* The `opc.tcp` listener uses the lightweight raw-socket
`TcpTransportListener` by default (no ASP.NET Core dependency). An
opt-in alternative shipped in `Opc.Ua.Bindings.Https` (`net8.0`+) is
described below.
* The `opc.https` and `opc.wss` listeners both share a single
`HttpsTransportListener` per `(host, port)` — Kestrel routes the
inbound HTTP request to the right handler based on the
`Upgrade` / `Content-Type` headers. Concretely:
- `POST` with `Content-Type: application/octet-stream` →
`https-uabinary` handler.
- `POST` with `Content-Type: application/opcua+uajson` →
`https-uajson` handler.
- HTTP Upgrade with `Sec-WebSocket-Protocol: opcua+uacp` →
full UASC WebSocket session.
- HTTP Upgrade with `Sec-WebSocket-Protocol: opcua+uajson` →
request/response JSON WebSocket session.
### Security configuration
The HTTPS and WSS listeners share the same TLS configuration via
`HttpsConnectionAdapterOptions`. By default the listener serves
`SecurityMode.None`; setting
`ServerConfiguration.HttpsMutualTls = true` requires the client to
present a TLS certificate which is matched against the
`ClientCertificate` field of the UASC `OpenSecureChannelRequest`.
The JSON sub-profiles (`https-uajson` and the WSS `opcua+uajson`
sub-protocol) only accept `SecurityMode.None` regardless of the
configured security policies — see Part 6 §7.4.5 / §7.5.2 for the
spec rationale.
## Client-side usage
The client API is the standard `Session` + `EndpointDescription` flow;
the transport is selected automatically from
`EndpointDescription.TransportProfileUri`:
```csharp
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var application = new ApplicationInstance(telemetry)
{
ApplicationName = "MyClient",
ApplicationType = ApplicationType.Client
};
ApplicationConfiguration configuration = await application
.Build("urn:localhost:MyClient", "urn:localhost:product")
.AsClient()
.AddSecurityConfiguration("CN=MyClient")
.Create()
.ConfigureAwait(false);
// 1) Direct connect to a known opc.wss endpoint (binary UASC):
EndpointDescription wssEndpoint = CoreClientUtils.SelectEndpoint(
application,
"opc.wss://server.example.com:62543/MyServer",
useSecurity: true);
ISession session = await Session.CreateAsync(
configuration, new ConfiguredEndpoint(null, wssEndpoint), false,
"WssSession", 60_000, null, null).ConfigureAwait(false);
// 2) HTTPS-JSON endpoint (Security Mode None):
var jsonEndpoint = new EndpointDescription
{
EndpointUrl = "opc.https://server.example.com:62540/MyServer",
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None,
TransportProfileUri = Profiles.HttpsJsonTransport
};
ISession jsonSession = await Session.CreateAsync(
configuration, new ConfiguredEndpoint(null, jsonEndpoint), false,
"JsonSession", 60_000, null, null).ConfigureAwait(false);
```
A few notes:
* The `opc.wss://` scheme is the OPC UA alias; the stack normalises it
to `wss://` (the IETF scheme) and falls back to
`Utils.UaWebSocketsDefaultPort` (4843) when no explicit port is
supplied.
* The WSS client requires the server to select the requested WebSocket
sub-protocol. If the server returns anything else the connection
fails with `BadNotConnected`.
* The WSS-JSON client opens a fresh `ClientWebSocket` per request
(simplest correct behaviour). A pooled / persistent-WebSocket
variant can be added without changing the public shape.
* The HTTPS client (binary or JSON) reuses a single `HttpClient` per
channel; the encoding is selected from
`EndpointDescription.TransportProfileUri` at request time.
## Discovery
`GetEndpoints` and `FindServers` return the `EndpointDescription` list
declared by the listener for each base address. For TCP, HTTPS-binary,
and WSS+uacp the description includes the correct `TransportProfileUri`
(`uatcp-uasc-uabinary`, `https-uabinary`, `wss-uasc-uabinary`
respectively). The JSON sub-protocols are reachable on the same URL as
their binary counterparts and clients select them explicitly via the
`Content-Type` header (HTTPS) or the `Sec-WebSocket-Protocol` header
(WSS). The HTTPS / WSS service hosts also emit a discovery-only
**JSON-twin** `EndpointDescription` (`TransportProfileUri =
Profiles.HttpsJsonTransport` / `Profiles.UaWssJsonTransport`,
`SecurityMode = None`) for every `SecurityMode = None` binary endpoint,
so JSON-aware clients see the profile in `GetEndpoints` without needing
to construct the description themselves. The twin is discovery-only
(no separate listener) — the existing binary listener handles content
negotiation.
## Opt-in: Kestrel-hosted `opc.tcp`
By default the stack ships **two** listener implementations for
`opc.tcp://`:
| Package | Listener | When to use |
| --- | --- | --- |
| `Opc.Ua.Core` (default) | `TcpTransportListener` | Raw `Socket` + `SocketAsyncEventArgs`. No ASP.NET Core dependency. The right choice for trimmed / AOT deployments and for environments that already avoid pulling in `Microsoft.AspNetCore.App`. |
| `Opc.Ua.Bindings.Https` (opt-in, `net8.0`+) | `KestrelTcpTransportListener` | Hosts `opc.tcp://` on Kestrel via `Microsoft.AspNetCore.Connections.ConnectionHandler`. Lets a single `IHost` serve `opc.tcp`, `opc.https`, and `opc.wss` so consumers manage one HTTP-style runtime, share TLS plumbing, and share observability middleware. |
The Kestrel-TCP listener uses the same `IUaSCByteTransport` runtime
boundary as the raw-socket listener, so the UASC channel pipeline is
unchanged. It supports the full feature set the raw-socket listener
does, including:
* forward (server) and **reverse-connect** listener modes,
* per-listener TLS certificate hot-update via
`ITransportListenerCertificateRotation`,
* discovery (`EndpointDescription` emission via the shared
`TcpServiceHost` base).
Swap it in by registering the factory through the DI container
**before** opening any `opc.tcp` listener (typically at application
startup):
```csharp
// Standalone non-DI consumers:
DefaultTransportBindingRegistry registry =
DefaultTransportBindingRegistry.WithDefaultTcp();
registry.RegisterListenerFactory(new KestrelTcpTransportListenerFactory());
// Pass to ServerBase (via ctor / TransportBindings setter) before StartAsync.
// Microsoft.Extensions.DependencyInjection consumers:
services
.AddOpcUa()
.AddOpcTcpTransport() // raw-socket opc.tcp default
.AddKestrelOpcTcpTransport(); // last-writer-wins: overrides with Kestrel
```
`AddKestrelOpcTcpTransport()` installs an `ITransportBindingConfigurator`
that runs after `AddOpcTcpTransport()`, so the registry resolves the
Kestrel listener factory for `opc.tcp://`. The same pattern applies to
`AddHttpsTransport()` / `AddWssTransport()` (HTTPS / WSS) and any custom
binding registered via `AddCustomTransport<TListener, TChannel>()`.
> **`AddOpcTcpTransport()` is optional.** `AddOpcUa()` already seeds the mandatory raw-socket `opc.tcp` listener and channel factories into the transport binding registry, so clients and servers reach `opc.tcp://` without it. Call it only to be explicit; the `Add*Transport()` extensions above still override the seeded defaults (last-writer-wins per URI scheme) whether or not it was called.
The package targets net8.0+ only (the ASP.NET Core `ConnectionContext`
API surface used to bridge `Socket`-like semantics — `LocalEndPoint`,
`RemoteEndPoint`, `ConnectionClosed` — is not consistently available
on older TFMs). Consumers on net472 / netstandard2.x continue to use
the raw-socket default.
## Implementing a custom byte transport
The OPC UA Secure Conversation (UASC) binary channel pipeline talks to
the wire through a narrow byte-level abstraction —
[`IUaSCByteTransport`](../src/Opc.Ua.Core/Stack/Tcp/IUaSCByteTransport.cs).
It is the public extension point for plugging in custom transports
beyond the built-in TCP and WebSocket implementations.
### When you need this
Use cases that are good fits for a custom byte transport:
- **Named pipes / Unix domain sockets** for fast loopback IPC.
- **QUIC / HTTP/3** to replace the TCP transport's congestion behaviour.
- **In-process bridges** for unit tests or co-located server/client pairs
that want to skip the network stack entirely.
- **Tunnels** (SSH, custom L4) that wrap the UASC chunks in another
framing.
Use cases that should NOT use this:
- A new sub-protocol that changes UASC framing — that lives inside the
channel implementation, not the transport.
- HTTPS-binary or HTTPS-JSON variants — those are handled in
`Opc.Ua.Bindings.Https` outside the UASC pipeline.
### Contract summary
```csharp
public interface IUaSCByteTransport
{
string Implementation { get; } // diagnostic id
TransportChannelFeatures Features { get; } // optional capabilities
EndPoint? LocalEndpoint { get; } // may be null
EndPoint? RemoteEndpoint { get; } // may be null
ValueTask ConnectAsync(Uri url, CancellationToken ct);
ValueTask SendChunkAsync(ReadOnlyMemory<byte> chunk, CancellationToken ct);
ValueTask SendChunkAsync(BufferCollection buffers, CancellationToken ct);
ValueTask<ArraySegment<byte>> ReceiveChunkAsync(CancellationToken ct);
void Close();
}
public interface IUaSCByteTransportFactory
{
string Implementation { get; }
IUaSCByteTransport Create(
BufferManager bufferManager,
int receiveBufferSize,
ITelemetryContext telemetry);
}
```
Each Send / Receive operates on **exactly one** complete UASC
`MessageChunk` (Part 6 §6.7.2). The channel pipeline above the transport
owns chunk-level framing — your job is to move chunk bytes across the
wire.
### Implementation checklist
- [ ] Buffer ownership. Buffers returned by `ReceiveChunkAsync` are
rented from the supplied `BufferManager`; the caller (the UASC
channel) returns them via `BufferManager.ReturnBuffer` once the
chunk has been processed. Do **not** pool or reuse the same array
across receives.
- [ ] Idempotent `Close`. The channel may dispose a transport from
multiple paths (normal shutdown, fatal error, channel-state race);
every implementation MUST treat `Close()` as a no-op when already
closed.
- [ ] Cancellation. `ReceiveChunkAsync` should observe the cancellation
token while awaiting peer data — long-lived sessions rely on this
to tear down cleanly.
- [ ] Error mapping. Map transport-layer errors to
[`ServiceResultException`](../src/Opc.Ua.Core/Types/Result/ServiceResultException.cs)
with the matching `StatusCodes.BadXxx` (`BadConnectionClosed`,
`BadTcpMessageTypeInvalid`, `BadTcpMessageTooLarge`, …) so the
channel can route them through normal UA fault paths.
- [ ] Vectored Send. The `BufferCollection` overload is called by the
channel for chunks that span multiple buffers (typical for the
asymmetric handshake). If your transport does not support
vectored writes, concatenate the segments into a single buffer
before sending (this is what `WebSocketByteTransportBase` does).
- [ ] Client vs server. Client transports implement `ConnectAsync` to
dial outbound; server transports built from an
already-accepted connection should throw `NotSupportedException`
from `ConnectAsync` (this is the contract enforced by
`UaSCUaBinaryClientChannel`).
### Wiring the transport into the channel pipeline
#### Client side
Implement [`IUaSCByteTransportFactory`](../src/Opc.Ua.Core/Stack/Tcp/IUaSCByteTransport.cs)
and hand the factory to a subclass of
`UaSCUaBinaryTransportChannel`:
```csharp
internal sealed class MyTransportFactory : IUaSCByteTransportFactory
{
public string Implementation => "UA-MY";
public IUaSCByteTransport Create(BufferManager bm, int rxSize, ITelemetryContext tel)
=> new MyByteTransport(bm, rxSize, tel);
}
public sealed class MyTransportChannel : UaSCUaBinaryTransportChannel
{
public MyTransportChannel(ITelemetryContext telemetry)
: base(new MyTransportFactory(), telemetry)
{
}
}
public sealed class MyTransportChannelFactory : ITransportChannelFactory
{
public string UriScheme => "opc.my";
public ITransportChannel Create(ITelemetryContext telemetry)
=> new MyTransportChannel(telemetry);
}
```
If your transport needs channel-level state (TLS validator, client cert,
custom credentials) plumbed in, override
`UaSCUaBinaryTransportChannel.OnSettingsSaved(TransportChannelSettings,
ChannelQuotas)` — that hook fires after the channel binds settings but
before it tries to connect. The WSS implementation uses this pattern;
see `WssTransportChannel.OnSettingsSaved` for a reference.
#### Server side
Implement [`ITransportListener`](../src/Opc.Ua.Core/Stack/Transport/ITransportListener.cs)
plus an `ITransportListenerFactory`. For each accepted connection,
construct your transport directly (not through the
`IUaSCByteTransportFactory`, which is client-side only), then hand it to
a `TcpServerChannel` via `Attach(channelId, transport)`. The WSS server
path in `HttpsTransportListener.AcceptWebSocketAsync` is the canonical
template.
#### Registering by URL scheme
Install your custom listener and channel factories into the host's
`ITransportBindingRegistry`. The simplest path uses the DI extension:
```csharp
services
.AddOpcUa()
.AddCustomTransport<MyCustomListenerFactory, MyCustomChannelFactory>();
```
Both factories are resolved from the `IServiceProvider` (so they may
have constructor-injected dependencies) and installed under the URI
scheme reported by `MyCustomListenerFactory.UriScheme`. Non-DI consumers
construct a `DefaultTransportBindingRegistry` and call
`RegisterListenerFactory` / `RegisterChannelFactory` directly, then hand
the registry to `ServerBase` (via `TransportBindings`) or to
`ReverseConnectManager.TransportBindings`.
### Worked example
[`InProcessTransport`](../src/Opc.Ua.Core/Stack/Tcp/InProcessTransport.cs)
is the public reference implementation that ships in `Opc.Ua.Core`. It
uses only public API: two paired transports communicate over a pair of
in-memory `System.Threading.Channels.Channel<byte[]>` (one per
direction). Use it directly for unit tests or co-located client/server
pairs that want to skip the network stack entirely, or as a
copy-paste starting point for a custom transport.
```csharp
using Opc.Ua.Bindings;
var buffers = new BufferManager("inproc", 8192, telemetry);
(InProcessTransport client, InProcessTransport server) =
InProcessTransport.CreatePair(buffers, receiveBufferSize: 8192, telemetry);
await client.SendChunkAsync(payload, ct).ConfigureAwait(false);
ArraySegment<byte> received = await server.ReceiveChunkAsync(ct).ConfigureAwait(false);
```
Note that `InProcessTransport.ConnectAsync` throws
`NotSupportedException` — the pair is created up front via
`CreatePair`. A real network transport would implement `ConnectAsync`
to dial the wire.
## See also
* [`Profiles.md`](Profiles.md) — supported profiles, security policies,
message encodings.
* [`MigrationGuide.md`](MigrationGuide.md) — note on the breaking
removal of `IMessageSocket` and the new `IUaSCByteTransport`
contract.
* OPC UA Part 6 §7.4 (HTTPS) / §7.5 (WebSockets) for the wire-format
specification.