Transport Profiles
July 31, 2026 · View on GitHub
This document is the practical companion to 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 | 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 | 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 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 foropc.tcp://.HttpsTransportChannel+HttpsTransportChannelFactory,OpcHttpsTransportChannelFactory— polymorphic client-side channel that handles bothhttps-uabinaryandhttps-uajsonbased on the endpoint'sTransportProfileUri.
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 forhttps://andopc.https://.WssTransportListenerFactory,OpcWssTransportListenerFactory— listener factories forwss://andopc.wss://.WebSocketClientByteTransport+WebSocketClientByteTransportFactory,WssTransportChannel,WssTransportChannelFactory,OpcWssTransportChannelFactory— client-side WSS+uacp.WssJsonTransportChannel,WssJsonTransportChannelFactory— client-side WSS+uajson (per-requestClientWebSocket).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.
<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.tcplistener uses the lightweight raw-socketTcpTransportListenerby default (no ASP.NET Core dependency). An opt-in alternative shipped inOpc.Ua.Bindings.Https(net8.0+) is described below. - The
opc.httpsandopc.wsslisteners both share a singleHttpsTransportListenerper(host, port)— Kestrel routes the inbound HTTP request to the right handler based on theUpgrade/Content-Typeheaders. Concretely:POSTwithContent-Type: application/octet-stream→https-uabinaryhandler.POSTwithContent-Type: application/opcua+uajson→https-uajsonhandler.- 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:
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 towss://(the IETF scheme) and falls back toUtils.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
ClientWebSocketper 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
HttpClientper channel; the encoding is selected fromEndpointDescription.TransportProfileUriat 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 (
EndpointDescriptionemission via the sharedTcpServiceHostbase).
Swap it in by registering the factory through the DI container
before opening any opc.tcp listener (typically at application
startup):
// 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-socketopc.tcplistener and channel factories into the transport binding registry, so clients and servers reachopc.tcp://without it. Call it only to be explicit; theAdd*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.
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.Httpsoutside the UASC pipeline.
Contract summary
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
ReceiveChunkAsyncare rented from the suppliedBufferManager; the caller (the UASC channel) returns them viaBufferManager.ReturnBufferonce 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 treatClose()as a no-op when already closed. - Cancellation.
ReceiveChunkAsyncshould 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
ServiceResultExceptionwith the matchingStatusCodes.BadXxx(BadConnectionClosed,BadTcpMessageTypeInvalid,BadTcpMessageTooLarge, …) so the channel can route them through normal UA fault paths. - Vectored Send. The
BufferCollectionoverload 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 whatWebSocketByteTransportBasedoes). - Client vs server. Client transports implement
ConnectAsyncto dial outbound; server transports built from an already-accepted connection should throwNotSupportedExceptionfromConnectAsync(this is the contract enforced byUaSCUaBinaryClientChannel).
Wiring the transport into the channel pipeline
Client side
Implement IUaSCByteTransportFactory
and hand the factory to a subclass of
UaSCUaBinaryTransportChannel:
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
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:
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
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.
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— supported profiles, security policies, message encodings.MigrationGuide.md— note on the breaking removal ofIMessageSocketand the newIUaSCByteTransportcontract.- OPC UA Part 6 §7.4 (HTTPS) / §7.5 (WebSockets) for the wire-format specification.