mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 09:06:37 +08:00
61edaa1ed0
# Description Drops support for **netstandard2.1** and **.NET Framework 4.7.2**. Libraries now target `net48`, `net8.0`, `net9.0` and `net10.0`. `Opc.Ua.Types` and `Opc.Ua.SourceGeneration.Core` still also target `netstandard2.0`, but only because the Roslyn source generators load them. There is no general netstandard2.0 support. **Preprocessor directives** (112 files) - Every `#if`/`#elif` mentioning `NETSTANDARD2_1*`, `NET472*`, `NET_STANDARD_TESTS`, `SKIP_ECC_CERTIFICATE_REQUEST_SIGNING` or `NETSTANDARD*` was evaluated against the remaining TFM set (net48, net8/9/10, plus netstandard2.0 inside `Opc.Ua.Types`/`tools`) and simplified. Dead branches were removed. - Example: `NETSTANDARD2_1 || NET8_0_OR_GREATER` → `NET8_0_OR_GREATER`. - Example: `NET8_0_OR_GREATER || NET472_OR_GREATER` → removed (always true). - Netstandard-only code was removed: `UnixFileModeShim`, the CRDT reflection path in `ReplicatedGossipOptions.CreateReaderOptions`, and the netstandard2.1-only test variants. **Custom test logic** - `targets.props` drops the `net472`, `netstandard2.0` and `netstandard2.1` `CustomTestTarget` profiles, `NetStandardTests` and `SKIP_ECC_CERTIFICATE_REQUEST_SIGNING`. Supported values are now `net48`, `net8.0`, `net9.0`, `net10.0`. - The `NET_STANDARD_TESTS` define is removed from 29 test projects. - The "build netstandard shells as net10.0" overrides are removed from 18 projects. - `RestrictForLegacyTfm` now only applies to `net48`. - `tests/customtest.bat` is updated to the new values. **Build / packaging / CI** - MSBuild TFM conditions are simplified: Kestrel package choice, MQTTnet v4 (net48 only), BouncyCastle, Di/OpenUsd/Vision/Stress test excludes, and `System.IO.FileSystem.AccessControl` (dropped from Core). - The MigrationAnalyzer nuspec drops its net472/netstandard2.1 lib groups. The Azure signing lists are updated. - `get-ci-matrix.ps1` removes the net472/netstandard test profiles (including the PR-gating `linux-netstandard2.1`) and the matching build legs. **Breaking API change (2.0 preview):** removed `HttpsTransportListener.IsWssTransportSupported` and `ReplicatedGossipOptions.IsGossipStateDecodingSupported`. They only returned `false` on netstandard2.1 and are now always `true`. The test guards that used them were removed as well. **Docs** - NuGet READMEs, `README.md`, `DeveloperGuide.md`, `PubSub.md`, the certificate/ECC/identity docs and the 2.0 migration guide (plus its `.agents` skill mirror) are updated. - The migration guide notes that 4.7.2 consumers must move to 4.8 and netstandard2.1 consumers to net8.0+. ### Reviewer notes - The `System.IO.FileSystem.AccessControl` `PackageVersion` pin is left in `Directory.Packages.props`. It has no direct reference any more, but transitive pinning is on, so it was kept to avoid shifting resolved versions. - The `#if` rewrite was done mechanically by a truth-table evaluator. The diff in the `.cs` files is limited to directive lines, removed dead branches, and stale-comment updates. ### Verified locally - `dotnet build UA.slnx -p:CustomTestTarget=net48`: 0 errors. - `dotnet build UA.slnx -p:CustomTestTarget=net10.0`: 0 errors. - Default multi-target builds (no `CustomTestTarget`) of Opc.Ua.Types, Bindings.Https, PubSub.Mqtt and Redundancy.Server succeeded. - net10.0 tests: - Opc.Ua.Redundancy.Server.Tests: 840/840 passed. - Opc.Ua.Types.Tests: 11363 passed, 1 skipped. - ModelDependencyScanner tests: 20/20 passed. - The full test suite was **not** run locally. ## Related Issues - None ## Checklist - [ ] 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. - [ ] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] 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. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
631 lines
31 KiB
Markdown
631 lines
31 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.
|
|
|
|
## Contents
|
|
|
|
- [Transport profile matrix](#transport-profile-matrix)
|
|
- [Assembly layout](#assembly-layout)
|
|
- [Server-side configuration](#server-side-configuration)
|
|
- [Client-side usage](#client-side-usage)
|
|
- [Discovery](#discovery)
|
|
- [Opt-in: Kestrel-hosted `opc.tcp`](#opt-in-kestrel-hosted-opctcp)
|
|
- [Implementing a custom byte transport](#implementing-a-custom-byte-transport)
|
|
- [See also](#see-also)
|
|
|
|
## 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.
|
|
|
|
Discovery retains binary, JSON and OpenAPI endpoints even when their URL and
|
|
security settings are identical. `GetEndpoints` profile filters select only
|
|
the requested profiles, including when translating an alternate hostname.
|
|
Managed client channels likewise include the effective transport profile in
|
|
their sharing key: equal URLs do not allow different wire encodings to share
|
|
an underlying channel.
|
|
|
|
The managed channel manager retains its own client certificate and chain.
|
|
Open attempts borrow independent snapshots, and each installed transport has
|
|
separate references until it is closed. Closing or rotating one channel does
|
|
not retire keys still used by another channel or an in-flight open.
|
|
Managed sessions prefer the active certificate registry over an older configured
|
|
store entry; unmanaged sessions retain explicit configured-store selection.
|
|
A session opened on a managed transport uses that transport's
|
|
owned certificate snapshot, including during recreation after rotation; a
|
|
later registry update cannot substitute a different key into that handshake.
|
|
Reconnects recheck certificate generations before reactivating sessions and
|
|
reporting readiness. Publishing identical certificate/issuer material does not
|
|
force another transport replacement; a changed issuer chain still does.
|
|
Rotation that supersedes successful work does not consume the failed-attempt
|
|
retry allowance. Reconnect time budgets and shutdown cancellation still apply.
|
|
|
|
UA-TCP reconnect hands the new connection to the retained channel without
|
|
closing it when the temporary handshake channel is retired. ECC and RSA-DH
|
|
handoffs move the owned ephemeral nonce objects, including the private key;
|
|
public nonce bytes cannot reconstruct them. The retained token's secret is
|
|
used when deriving the replacement keys. Continued sequence numbers are
|
|
checked against the retained channel, not treated as a new secure channel.
|
|
A rejected or failed handoff closes the new connection and releases its
|
|
unadopted token rather than restarting an orphaned receive loop. The peer
|
|
certificate remains available through failure auditing. Receive loops
|
|
have separate cancellation lifetimes; a retiring connection cannot stop its
|
|
replacement. Connection admission reserves capacity before invoking channel
|
|
callbacks, retires idle channels outside the listener lock, and closes rejected
|
|
sockets. Invalid response sequences fail pending requests promptly with
|
|
`BadSecurityChecksFailed`; diagnostics retain `BadSequenceNumberInvalid`.
|
|
|
|
Raw TCP, Kestrel TCP, and UACP WebSocket listeners honor the configured
|
|
`ConnectionRateLimiter` before allocating a channel or accepting a WebSocket
|
|
upgrade. Kestrel and UACP WebSocket listeners reserve their configured
|
|
`MaxChannelCount` capacity atomically, including pending admissions. The
|
|
reservation follows the physical transport through a reverse-connect handoff
|
|
and is released on closure, cancellation, failed attachment, or listener stop.
|
|
The host retains ownership of a supplied limiter. These limits are not
|
|
per-tenant fairness policies, and HTTP/JSON requests are not counted as UASC
|
|
channels.
|
|
|
|
### Committed session bindings
|
|
|
|
Managed servers expose `ISessionBindingProvider` to their listeners.
|
|
`SessionManager` maintains distinct committed session/channel membership:
|
|
reactivation does not add another session, transfer moves only that session's
|
|
binding, and closure, timeout cleanup, or shutdown removes it. Resource
|
|
eligibility no longer depends on counting activation and close responses.
|
|
|
|
`TryGetSessionContext` provides an immutable `SessionBindingContext` after a
|
|
read-only lookup of a live, activated, unexpired session and its transport
|
|
binding. It does not refresh session activity or perform service authorization.
|
|
Full service validation is still required; re-query after queueing and compare
|
|
snapshot identity and activation sequence before reusing classification.
|
|
Sequences belong to a live session instance, so a restored session's snapshot
|
|
must not be confused with the earlier instance. Anonymous sessions do not
|
|
establish trusted tenants, and identity keys must not become unbounded metric
|
|
labels.
|
|
|
|
`StandardServer` uses its current session manager's optional capability by
|
|
default. A custom manager without that capability grants no managed session
|
|
classification. Core-only standalone channels without a provider retain
|
|
legacy response-count hints for compatibility; those hints are not authoritative
|
|
membership and must not be used to grant trusted reservations.
|
|
|
|
### Incomplete-message resource limits
|
|
|
|
UA Secure Conversation channels enforce message-size and chunk-count limits
|
|
including the incoming chunk, before retaining it. Exceeding a limit releases
|
|
the partial message and closes the channel; no final chunk is required to
|
|
trigger cleanup. A valid Abort chunk is not additional message payload: it
|
|
discards the partial message without consuming its size, chunk-count, or
|
|
reassembly-budget allowance. The channel stays open; a client reports the
|
|
peer's abort status for that request.
|
|
|
|
On server channels, `ChannelLifetime` also bounds assembly of an incomplete
|
|
message from its first retained chunk. Further chunks or other activity do not
|
|
restart that deadline. Channels sharing listener quotas and a clock share one
|
|
assembly timer, checked at half-lifetime intervals. Idle channels do not take
|
|
the partial-message lock during that check. The timer is released when the last
|
|
channel leaves the scope. This also works in bindings without an inactivity
|
|
sweep. For assembly deadlines, a zero or negative `ChannelLifetime` uses the
|
|
30-second transport default rather than disabling cleanup; other quota values
|
|
are not changed. Expiry attempts an ERR carrying `BadTimeout` before closing,
|
|
and cleanup still completes if that error cannot be sent.
|
|
Completed or discarded messages clear their assembly state,
|
|
and subsequent messages receive a fresh deadline. This applies before an OPC UA
|
|
session is created and to the UA-TCP, Kestrel TCP, and WebSocket bindings using
|
|
the server-channel pipeline.
|
|
|
|
Per-channel limits alone do not bound memory across many connections.
|
|
`ChunkReassemblyBudget` counts the actual arrays retained for intermediate
|
|
chunks across all listeners of a server. Its default is **64 MiB** for the
|
|
reference server, with half reserved as headroom for channels carrying activated
|
|
sessions. Reservations are released on completion, replacement, abort, fault, or
|
|
closure. Final chunks and normal response allocations are not charged.
|
|
|
|
A rejected chunk releases the partial message, reports
|
|
`BadTcpNotEnoughResources` when an error response can be sent, and closes the
|
|
channel permanently. General buffer allocation remains unrestricted by default.
|
|
See [reassembly-budget configuration](RateLimiting.md#incomplete-messages) for
|
|
sizing and scope. `MaxSessionCount` does not limit connections that have not
|
|
created a session.
|
|
|
|
Hello negotiation also updates TCP and WebSocket receive-buffer sizes. The
|
|
Kestrel pipe transport rents for the actual chunk size instead of the listener
|
|
maximum, avoiding unnecessary retained capacity for small chunks.
|
|
|
|
## 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 a specialized server builder, or by constructing a
|
|
`DefaultTransportBindingRegistry` and calling `RegisterListenerFactory` /
|
|
`RegisterChannelFactory` directly for non-DI hosts. Applications must
|
|
reference `OPCFoundation.NetStandard.Opc.Ua.Bindings.Https` directly; Server
|
|
and GDS packages do not pull the optional binding transitively.
|
|
|
|
`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.
|
|
|
|
All HTTPS and WSS client bindings, including WebApi, require the TLS certificate
|
|
to match the endpoint hostname before invoking a configured OPC UA certificate
|
|
validator. Trusting a certificate or its issuer does not bypass hostname
|
|
verification. This also applies to TLS-only JSON bindings.
|
|
|
|
All HTTP, WebSocket and WebApi dispatch paths forward the observed remote IP
|
|
address for authentication lockout accounting. Unsecured clients cannot reset
|
|
that bucket by opening a new connection or changing their ApplicationUri.
|
|
When a transport cannot supply a peer address, the fallback bucket is
|
|
session-local, never shared by every client of a listener.
|
|
|
|
## 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 (`opcua+uajson`) 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.
|
|
|
|
The WSS OpenAPI client (`opcua+openapi`) keeps a persistent connection and
|
|
multiplexes requests, so an outstanding Publish does not block other services.
|
|
It assigns unique, nonzero wire request handles for that connection while
|
|
preserving caller-owned request objects and caller-visible response handles.
|
|
Concurrent requests must use distinct caller handles; a handle can be reused
|
|
after completion, cancellation or timeout. The shared `ClientBase` caller-handle
|
|
sequence can wrap; it is separate from the connection-local wire allocator.
|
|
Terminal requests release their pending state without waiting for a server reply, and late replies cannot
|
|
complete a newer request.
|
|
|
|
An attributable response-decoding failure, such as exceeding `MaxArrayLength`,
|
|
fails only that request. Other pending requests and subsequent requests keep
|
|
using the connection. Transport/framing failures and responses that cannot be
|
|
attributed to an issued wire handle still close it.
|
|
|
|
Caller cancellation and request timeout cancel send-lock admission and the
|
|
pending response, not an already-started WebSocket send. An admitted send uses
|
|
the connection's shutdown token and finishes independently of the caller;
|
|
shutdown drains it before disposing the socket.
|
|
|
|
`CancelRequest.RequestHandle` is translated to the wire handle of the currently
|
|
outstanding request with that caller handle. A retired or unknown target maps
|
|
to a reserved, never-issued wire handle, so it cannot cancel an unrelated request.
|
|
Wire handles never wrap: exhaustion closes the connection with
|
|
`BadConnectionClosed` and requires reopening it. Shutdown closes operation
|
|
admission, cancels queued sends and close operations, and drains admitted
|
|
socket users before disposing connection resources.
|
|
|
|
HTTPS binary, JSON, and OpenAPI clients obtain response headers before
|
|
buffering the body. A positive `MaxMessageSize` limits the actual body bytes, including
|
|
chunked responses or misleading `Content-Length` values. An oversized
|
|
response fails with `BadResponseTooLarge`; the reader consumes at most one
|
|
byte beyond the limit to detect the overflow. HTTP error responses are
|
|
disposed without buffering their bodies.
|
|
|
|
The request deadline and caller cancellation remain active while the
|
|
response body is being read. The binary/JSON transport reports socket
|
|
connection failures as `BadNotConnected`, socket or request timeouts as
|
|
`BadRequestTimeout`, and other HTTP failures as `BadUnknownResponse`, while
|
|
preserving the original exception for diagnostics. HTTP 429/503 retain
|
|
their `BadServerTooBusy` and `Retry-After` handling.
|
|
|
|
## 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 net48 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.Types/Utils/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.
|