mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 15:11:53 +08:00
# Description Remove stock phrasing and correct misleading or inconsistent technical documentation. The initial two passes covered 367 Markdown files, followed by an interactive review of 169 editorial flags. The U5 interpretation originally deferred in that review has since received the upstream Mantis answer recorded by #4843; remaining server/spec follow-ups stay explicit in the CTT log. - Correct package responsibilities, migration steps, API names, ownership, protocol descriptions and links. Keep bundled migration guides synchronized. Preserve historical conclusions as historical records with separate re-check notes. - Provide at least two rendered TOC levels throughout `docs/`. Convert appropriate architecture figures to Mermaid without changing code examples, transcripts or historical quotations. - Address the October 19 feedback with concrete setup/migration instructions, corrected certificate paths and PubSub names, shorter repeated caveats, consistent XML comments, and a full review of the Concepts guide. Retain #4839's API corrections. - Integrate #4841's runtime/performance guidance and review incoming/recent prose. Preserve dated measurements rather than claiming new benchmark runs; clarify allocation ratios, startup-only processor settings, JIT warm-up and the benchmark commit's scope. Missing raw measurement artifacts and repeat ranges remain tracked by #4842. - Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log conflict using the updated Mantis states and OPC answers, retaining historical framing and re-checks for C28/C32/C43. - Address the latest naming/index feedback in `adb5bf693`: rename the guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the provider and DataType-declaration contracts into the conversion guide and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and `samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di, FluentApi, ISA95, Lds and PubSub; update solution entries, project references, Docker/CI paths, test fixture paths and documentation links with exact casing. **WoT guide scope.** This branch retains the detailed conversion guide that #4758 had replaced with an anchor stub. Conversion limits, verified document sets, preservation rules and provider ownership now live together in `WotNodeSetConversion.md`; binding/runtime guidance remains in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger prose was shortened, the oversized argument-table cell moved into its section, and existing anchors preserved. **SDK policy is a separate, explicit user-directed change.** `global.json` sets a stable minimum of `10.0.400`, `rollForward: latestMajor`, and `allowPrerelease: false`. Newer major SDKs are permitted; CI's explicit SDK pins are unchanged. README, GettingStarted, DeveloperGuide and Recycling instructions reflect the possible analyzer differences. The image-contract regression fix accepts compatible stable SDK images instead of requiring exact equality with the floor. The latest four rename/index threads are resolved after publication. Three older threads remain open: universal obsolete-removal timing, unconditional nonce-strength wording, and a blanket Vision NativeAOT claim. Existing implementation/evidence constraints are explained in their inline replies. The two-level TOCs remain as requested. ## Validation - `git diff --check` and staged diff checks: passed. The untouched upstream AutoID NodeSet retains its provenance bytes, including two existing license-header trailing spaces. - `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled migration documents and manifests synchronized. - Documentation/path validation: **101** documents retain two rendered TOC levels (**2,228** entries), with idempotent formatting. **52** new relative links and their anchors passed exact-case checks. No tracked references to the old guide/sample paths remain. Both relocated provider-contract sections are preserved in the destination. - `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects and the existing FluentApi/Lds/PubSub projects. Updated solution and pump ProjectReference paths also passed exact-case existence checks. - Latest focused commands, run from the repository root: ```powershell # Passed: 61 channel send-order and reconnect tests. dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal # Passed: 5 pump datasheet tests; builds the renamed pump sample. dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal # Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models. dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal # Passed: build, zero warnings/errors. dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal # Passed: dependent sample-test project build, zero warnings/errors. dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal ``` Earlier focused integration checks passed 2,620 tests across AutoID, event queues/scheduling, source generation, MCP/package profiles and WoT. Both reviewed Mermaid diagrams rendered locally without network access. These checks are not a full solution run. The full solution has **not** been rerun for this review. Earlier `dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did not pass: three AOT projects rejected VSTest and 20 Vision tests failed during graphics initialization with `OutOfMemoryException`. Remaining long-running tests were stopped at the user's request. A focused Vision rerun passed 558 tests and reproduced those 20 failures. The earlier `dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was blocked by the former SDK pin and was not retried after the policy change. The user approved proceeding with those limitations documented; current remote CI is not claimed green. ## Related Issues - Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and diagnostic-tool documentation refresh. #4841 supplies documentation and earlier results, not all newly requested measurements. - #4758 explains the conversion guide's earlier consolidation; the current provider contracts are now consolidated into `WotNodeSetConversion.md`. - #4839 supplies the reviewed API corrections; #4843 supplies the updated CTT issue record. - The original prose cleanup and SDK policy were user-requested without a source issue. No new runtime architecture is introduced by these documentation edits. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. --------- Co-authored-by: Roman Ettlinger <romanett98@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
650 lines
33 KiB
Markdown
650 lines
33 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)
|
|
- [Committed session bindings](#committed-session-bindings)
|
|
- [Incomplete-message resource limits](#incomplete-message-resource-limits)
|
|
- [Assembly layout](#assembly-layout)
|
|
- [Server-side configuration](#server-side-configuration)
|
|
- [Security configuration](#security-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)
|
|
- [When you need this](#when-you-need-this)
|
|
- [Contract summary](#contract-summary)
|
|
- [Implementation checklist](#implementation-checklist)
|
|
- [Wiring the transport into the channel pipeline](#wiring-the-transport-into-the-channel-pipeline)
|
|
- [Worked example](#worked-example)
|
|
- [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 `IServerSessionBindingProvider` to their listeners.
|
|
This read-only server lookup is separate from the client's
|
|
[`ISessionBindingProvider`](SessionBindings.md), which captures a generation-bound
|
|
dispatch client through `CreateBindingAsync`. Server hosts register custom lookup
|
|
implementations as `IServerSessionBindingProvider`; no shared interface combines
|
|
these capabilities.
|
|
The preview server lookup rename changes its public type identity: custom hosts
|
|
must update implementations and DI registrations and rebuild. It does not change
|
|
the lookup semantics or add members to existing session and transport interfaces.
|
|
`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`** (referenced by every Server / Client application):
|
|
- `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`** (optional, Kestrel-dependent; requires
|
|
an explicit package reference and transport registration):
|
|
- `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.
|
|
|
|
Binary WSS SecureChannels participate in application peer-trust revalidation.
|
|
A committed `Peers` TrustList or CRL change closes only channels whose complete
|
|
peer chain no longer validates, including outbound reverse connections, without
|
|
restarting the shared HTTPS host. Ordinary request-scoped HTTPS remains governed
|
|
by its TLS validation rather than this application-channel sweep.
|
|
|
|
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 `ManagedSessionBuilder` flow; the
|
|
transport is selected automatically from the endpoint URL scheme and
|
|
`EndpointDescription.TransportProfileUri`:
|
|
|
|
```csharp
|
|
ITelemetryContext telemetry = DefaultTelemetry.Create(builder => builder.AddConsole());
|
|
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")
|
|
.CreateAsync()
|
|
.ConfigureAwait(false);
|
|
|
|
// 1) Connect to a known opc.wss endpoint (binary UASC):
|
|
ManagedSession session = await new ManagedSessionBuilder(configuration, telemetry)
|
|
.UseEndpoint("opc.wss://server.example.com:62543/MyServer")
|
|
.ConnectAsync(ct)
|
|
.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
|
|
};
|
|
ManagedSession jsonSession = await new ManagedSessionBuilder(configuration, telemetry)
|
|
.UseEndpoint(new ConfiguredEndpoint(null, jsonEndpoint))
|
|
.ConnectAsync(ct)
|
|
.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 (as `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.
|