Skip to content

DRAFT Add OPC UA data channels and the opc.quic transport - #4240

Draft
marcschier wants to merge 50 commits into
masterfrom
data-channels-quic-experimental
Draft

DRAFT Add OPC UA data channels and the opc.quic transport#4240
marcschier wants to merge 50 commits into
masterfrom
data-channels-quic-experimental

Conversation

@marcschier

@marcschier marcschier commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Description

Experimental. Adds OPC UA data channels — a named, authorized, flow-controlled, bidirectional stream of opaque bytes multiplexed onto a SecureChannel that is already open — together with the opc.quic transport and a message-extension seam in Opc.Ua.Core that carries it.

OPC UA has no streaming primitive. A camera, a microphone, a firmware image or a log tail has to be carried by something designed for something else: Read polling, a Subscription carrying ByteString values, the FileTransfer model, or PubSub alongside the SecureChannel rather than on it. This implements the OPC UA Data Channels errata, which defines that primitive.

Every identifier the feature uses — the STR MessageType, the NodeIds in the 65000 block, the StatusCodes 11001113, the opcua/1 ALPN protocol — is provisional and will change if and when the OPC Foundation assigns final values. The errata is not endorsed by the OPC Foundation.

What is in it

Framing and engine (part of OPCFoundation.NetStandard.Opc.Ua.Core)
The STR MessageChunk, the twelve-byte stream header, seven frame types and five flags, verified byte for byte against the specification's published hex vectors. Serial-number arithmetic with a replay window and bounded GAP runs, per-channel and per-connection credit, a deficit round-robin scheduler with anti-starvation, the per-direction state machine, deadline expiry, and the SequenceNumber budget of §5.1.1.

Services
OpenDataChannel, ModifyDataChannel and CloseDataChannel generated from the model compiler inputs, so they take exactly the path every standard Service takes. Parameter negotiation, Session scoping, direction-aware authorization with re-evaluation, and auditing — served by StandardServer, so a real Client opens a channel through a real Session. Server-initiated offers (Part 4 §6) are not implemented; see Known limitations.

Transports
Inline framing over opc.tcp and opc.wss — a frame is one MessageChunk on the connection the Client already holds. Plus opc.quic (OPCFoundation.NetStandard.Opc.Ua.Bindings.Quic, net9.0+): URL scheme, ALPN negotiation and enforcement, listener, endpoint discovery, reverse connect, certificate rotation, one QUIC stream per channel, and the TLS-peer-to-OPC-UA-peer key binding of §7.6.1.

Carried by the UASC channel itself
UaSCUaBinaryChannel owns the DataChannelManager for its SecureChannel and dispatches STR chunks to it after decrypting, verifying and sequence-checking them, so the engine only ever sees authenticated content. Until EnableDataChannels is called an incoming STR chunk is an unrecognized MessageType and closes the SecureChannel, which is what OPC 10000-6 §6.7.2.2 requires. There is no generic extension abstraction: STR is the only MessageType this adds, and opc.quic needs none of it.

The delicate part is the send. Assigning the SequenceNumber and applying message security stay serialized against Service traffic, because both draw on the same keys and the same counter; the callback an extension passes runs inside that serialization so it can refuse atomically. The write itself is awaited outside it, so a slow peer on a data channel cannot stall Publish.

Design notes worth reading

  • A frame is exactly one MessageChunk. A multi-chunk frame would sit in the existing chunk assembler and block every other Message on the connection — the precise failure a streaming layer exists to avoid.
  • A capable peer never speaks first. No STR frame is transmitted until an OpenDataChannel on that SecureChannel has completed successfully, so a capable and a legacy implementation interoperate with no negotiation and no version bump.
  • Backpressure is per channel. The credit window is the mechanism; a consumer that cannot keep up stalls that stream and nothing else.
  • opc.quic refuses unreliable datagrams with Bad_DeliveryModeUnsupported rather than silently carrying them on a stream: QuicConnection exposes no RFC 9221 datagram API through .NET 10.

Feedback from the specification, back to the specification

Implementing it found two gaps in the draft, both raised upstream:

  • Part 4 §9 assigned no numeric StatusCodes. These travel on the wire in the RESET frame, so two implementations that each invent provisional values cannot interoperate. 11001113 are now pinned in the errata and here.
  • Part 6 §5.7 stated both scheduling obligations for senders only. The symmetric receiver obligation was implied but never binding, and no conformance unit tested it. The errata now states it and adds DCF-039.

The errata also needs one correction this implementation could not follow: OpenDataChannel reuses the transportChannelId parameter name across request and response, which no OPC UA Service does and the model compiler rejects, so the response parameter here is revisedTransportChannelId.

Compatibility

Additive and opt-in. The feature is inert until enabled, and marked [Experimental("DataChannels")] so consumers opt in deliberately — enforced identically on every TFM via an ExperimentalAttribute polyfill. The engine is part of Opc.Ua.Core, which still builds for all six target frameworks, so data channels work on net472 through net10.0. opc.quic is net8.0+, opting into preview features on net8.0 for System.Net.Quic.

Two experimental members were renamed rather than kept as forwarders: TransportChannelFeatures.DataChannelsMessageExtensions, and EnableDataChannels/DataChannels move from UaSCUaBinaryChannel to extension methods in the new package.

OPCFoundation.NetStandard.Opc.Ua.Bindings.Quic was packable but missing from expected-packages.txt; it is added here, which that file says should be a conscious, reviewed act. No other new package ships: the engine is part of Opc.Ua.Core.

Related Issues

No tracking issue yet — this implements an external errata draft rather than a reported defect. Happy to open one to carry the design discussion and act as the ADR if maintainers prefer.

  • Fixes #

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 and read the CONTRIBUTING doc.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • 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.

Test evidence

Suite net10.0 net48
Opc.Ua.Core.DataChannels.Tests 316 passed 242 passed
Opc.Ua.Server.Tests 4072 passed, 5 skipped
Opc.Ua.Sessions.Tests 777 passed, 379 skipped

The last two matter because the data channel code sits on the shared receive and send path that serves all Service traffic — the real regression risk in this change is ordinary MSG/OPN/CLO handling, not data channels.

Every regression test drives the production entry point rather than the component. That is deliberate: a conformance review found several obligations that were implemented correctly, covered by tests, and never invoked from any production path, and the same pattern recurred once more during review.

Known limitations

This is Part 6 clause 5 plus the inline transport, a substantially complete opc.quic, and the Part 4 Service Set. It is not yet a complete implementation of the errata, and the gaps are stated here rather than left to be discovered:

  • Server-initiated offers (Part 4 §6) are not implemented. The registry exists but nothing creates an offer or raises DataChannelOfferedEventType, so TryRedeem can only fail.
  • The Part 3 DataChannelCapabilities model projection is not wired. DataChannelModel builds the values, but the Object is never instantiated under ServerCapabilities, so a Client cannot read the capabilities or discover the feature through the address space. Part 4 §10 makes this a shall.
  • OpenTimeout and PingTimeout are negotiated and carried but not enforced (§5.14), and the revisedLifetime obligation of §5.1.1 is not implemented.
  • Per-Session data channel diagnostics are not exposed.
  • Unreliable datagrams (§7.5) are not implementable on .NET; the Server refuses Unreliable and PartiallyReliable with Bad_DeliveryModeUnsupported, which is what the errata requires.
  • UaSCSecureChannelRegistry is process-global rather than scoped to the listener that owns the channels. Documented with a TODO; scoping it needs a registry instance threaded through the transport bindings.

docs/DataChannels.md carries the full clause-by-clause table, including the rows marked "not wired".

Review fixes

A two-axis review of this branch (coverage, and spec/API completeness) found one security defect and several unwired paths, all fixed on this branch:

  • Authorization was direction-blind. IDataChannelAuthorizer took no direction and the default authorizer always evaluated PermissionType.Read, so a user permitted to read a source could open a SinkToSource channel and write into the Server — the exact failure Part 4 §7.2 names. The interface now carries the direction, the check moved after TryRevise so the negotiated direction is known while still preceding any ChannelId or stream allocation, and each required permission is validated separately, because ValidateRolePermissions treats a combined mask as "any of these".
  • A closed SecureChannel or lost transport now faults the channels riding on it (§5.13). Only the Session-close and identity-change rows of that table had been implemented.
  • AddQuicTransport() now wires the data channel transport, and DependencyInjectionStandardServer resolves the transport, authorizer, auditor and sources from DI.
  • AbortSecureChannel had no caller, and the QUIC implementation closed the whole connection rather than the channels — contradicting §5.11, "a failed stream is not a failed connection".
  • PING rate limiting (§5.11) was enforced on the sending side only. PING is credit-exempt and compels a PONG ahead of queued payload, so a receiver that answers unconditionally is the amplification surface the errata names. The bound now holds on both sides, on data channels and on ChannelId 0.
  • Coverage: Opc.Ua.Bindings.Quic is at 83.6% line; the engine's own coverage rose from 89.8% to 92.9% before it was folded into Opc.Ua.Core. The GAP lifecycle, the receive-side refusals and the public client seam were all at zero and are now covered, and DataChannelIntegrationTests adds the end-to-end Client → Session → Server leg the feature had been missing.

Architecture: the seam is gone

The engine now lives in Opc.Ua.Core (Stack/DataChannels/**) and the
extension seam it used to reach through is deleted, along with the
Opc.Ua.Core.Channels project and its package.

The earlier round argued for keeping the engine outside Core and narrowing
the seam with InternalsVisibleTo. That reasoning was wrong on the point
that mattered: measuring the seam showed Opc.Ua.Bindings.Quic referenced
zero of its symbols, because a QUIC data channel rides its own stream and
never becomes a UASC chunk. A single-implementer abstraction whose one
implementer is Core itself is not a seam, it is indirection — so it is gone
rather than hidden. UaSCUaBinaryChannel owns the DataChannelManager
directly. UaSCSecureChannelRegistry stays public: an application writing
its own IServerDataChannelTransport needs it, as the sample shows.

Three further simplifications came out of that work with no behaviour change:
the SecureChannelMessage struct (one producer, one consumer, never
compared, yet ~60 lines of IEquatable ceremony), the onSecuring
callback together with the SequenceBudget member — Core owns the budget
and does the securing, so it claims the SequenceNumber itself under the same
lock and nothing outside has to reason about that serialization — and
GlobalChannelId, which had no reader.

CI status

Every failing check that was this PR is fixed at the source. Nothing was
skipped, loosened or marked advisory to get there.

Fixed in this round:

  • Core.DataChannels on the Windows agent — 26 QUIC tests failed their
    handshake. Not the Schannel mutual-TLS limitation it resembled: the test
    harnesses bound their listeners to the IPv4 loopback while connecting by
    name, and localhost resolves to ::1 first on those agents, so the
    handshake went where nothing was listening. .NET reports that as an ALPN
    failure ([QUIC] Application layer protocol negotiation error was encountered dotnet/runtime#85412), which points at the wrong layer. The
    harnesses now bind IPv6Any, matching QuicTransportListener.
    MismatchedAlpnListenerIsRefused had been passing for the wrong reason,
    since a connection that never arrives is also refused.
  • code coverage — patch coverage was 60.98% against a 75% floor and an
    80% target. Almost all of it was unmeasured rather than untested: the
    hosted Linux images ship no msquic, so QuicListener.IsSupported was
    false and 61 QUIC tests skipped themselves, leaving the whole of
    Opc.Ua.Bindings.Quic — 1314 of the 1600 uncovered changed lines —
    reading as uncovered. The QUIC transport was effectively untested in CI for
    the whole life of this feature. Installing libmsquic on the Linux leg
    makes those tests run; they needed no changes to pass there. A skip is
    still the honest outcome where msquic is genuinely absent, so
    UA_REQUIRE_QUIC (set by the setup step) turns it into a failure only
    where the runner is meant to provide it, which is what stops this hiding
    again.

Patch coverage 60.98% → 88.02%, gate passing. Every project this PR
touches is above the repo's 80% bar: Opc.Ua.Bindings.Quic 83.6%,
Opc.Ua.Core 83.9%, Opc.Ua.Server 87.3%, Opc.Ua.Client 85.8%,
Opc.Ua.Bindings.Https 81.6%. Whole-report line rate 86.66%.

Also fixed here, though not part of the data channel work: NU1903.
NuGetAudit began failing every restore in the repository once
GHSA-q939-rpr3-3284 was published against SSH.NET 2025.1.0 — a path
traversal in ScpClient.Download, where the client trusts the file names
the remote server sends and can be walked out of the download directory. It
arrives transitively through Testcontainers 4.13.0, which asks for
[2025.1.0, ) and so resolves the lowest, vulnerable version. That broke
CodeQL, both Azure net10.0 builds, build-linux-all-tfm and both Kafka
legs. The patched 2026.0.0 satisfies the same range, so a central pin is
enough and nothing else changes; transitive pinning is already on
repository-wide. It affects master and every other open branch equally and
should ideally land there, but it blocked every check here. Verified:
solution restore clean under the default, net48 and netstandard2.0
pins, and the Kafka tests pass 93/93.

All checks are now green.

One flake was seen along the way and is worth recording, because it is not
this PR and will recur: test-ubuntu-latest-Sessions intermittently fails
after all 777 tests pass, when the global LeakDetectionSetup teardown
asserts an off-by-one certificate leak (created=47529, disposed=47528
one instance out of ~48,000). The leak counter waits up to 60s for the count
to settle and needs 5s of stability, so it is a genuine but rare disposal
race rather than a measurement artefact.

It is pre-existing and unrelated to this branch, which touches neither
Opc.Ua.Sessions, Opc.Ua.Test.Common nor the leak detection helpers:
the identical assertion appears on unrelated branches (for example
marcschier/capture-artifact-path-, 777 passing,
created=47904, disposed=47903), and across recent runs on other branches
it shows up in roughly two of nine. It passed here on re-run, and a local
Debug run of the same suite — the configuration that would print the leaked
certificate's allocation stack — came back clean, so the allocation site was
not identifiable from this branch. Worth chasing separately.

Review follow-up

All nine review comments are addressed.

The largest is the observation on UaSCBinaryClientChannel.cs:893"everywhere we make it look like a generic extension, but we only handle stream". That is right, and checking it decided the design: Opc.Ua.Bindings.Quic references zero of ISecureChannelMessageHost, ISecureChannelMessageExtension, SequenceNumberBudget or the send gate, because a QUIC data channel rides its own stream and never becomes a UASC chunk. The seam had one implementer and no prospect of a second, so the engine moved back into Opc.Ua.Core and the seam is deleted rather than dressed up as general. That also answers the request to rename ISecureChannelMessageExtension.cs after its interface: the file no longer exists. Stack/Tcp drops from 1358 added lines to well under that, and the Opc.Ua.Core.Channels package is gone.

Also in this round: the samples moved under samples/Core; a new ConsoleDataChannelAudio streams a synthesised melody on repeat and plays it back (NAudio, sample-only dependency, WAV fallback where NAudio cannot play); the docs now describe the state of the implementation rather than its history, with two development-diary sections removed; the response-dispatched callback got a correctly named log message on all three listeners instead of borrowing one that said "Failed to send fault response"; opc.quic targets net8.0 with preview features enabled rather than being excluded; and the #if around [Experimental] is gone, replaced by a polyfill — verified to be a real opt-in by confirming a net48 caller without the suppression fails the build with error DataChannels.

marcschier and others added 18 commits July 28, 2026 12:37
The OPC UA Data Channels errata defines a streaming primitive OPC UA does
not have: a named, authorized, flow-controlled, bidirectional stream of
opaque bytes multiplexed onto an existing SecureChannel. Nothing had ever
implemented it, so nothing had pressure-tested it.

This adds the framing layer and the generated Service, DataType and
StatusCode surface it needs.

Generated from the model compiler inputs rather than hand-written, so the
new Services take exactly the path every standard Service takes:

  UA Core Services.xml   OpenDataChannel, ModifyDataChannel,
                         CloseDataChannel, the three enums and the four
                         structures
  StandardTypes.xml      HasDataChannel, IDataChannelSourceType,
                         DataChannelSourceType,
                         DataChannelCapabilitiesType, the three event
                         types and the ServerCapabilities instance
  StandardTypes.csv      NodeIds pinned in the provisional 65000+ block
                         the errata assigns, so a future assignment by the
                         OPC Foundation is a one-line change
  UA Status Codes.xml    the twelve new StatusCodes

KeyValuePair moves from StandardTypes.xml to UA Core Services.xml. It has
to: a ServiceType parameter is resolved against the type dictionary alone,
and DataChannelParametersDataType.ContentParameters is a KeyValuePair
array. The generated type is unchanged, which the whole solution building
against it demonstrates.

The engine lives in Opc.Ua.Core/Stack/DataChannels:

  Framing      the STR MessageChunk, the twelve-byte stream header, the
               optional Deadline, seven frame types, five flags, and a
               decoder that bounds-checks every field against the length
               its own FrameType and flags imply before reading it
  Sequencing   serial arithmetic over 2^32-1 rather than 2^32, because
               zero is excluded from the value space and using 2^32 turns
               the wrap into a spurious gap; the replay window and the
               absolutely bounded GAP-run set
  FlowControl  per-direction channel and connection credit, the zero-start
               bootstrap, and the replenishment obligation without which a
               receiver can legally stall a channel forever
  Scheduling   the send queue, FrameSequenceNumber assignment at enqueue,
               and deadline expiry that reports one GAP run per contiguous
               discard
  DataChannel  the per-direction state machine, including that Paused and
               Closing are both per-direction and that receiving END never
               starts the local drain clock
  Manager      deficit round robin, the connection window, ChannelId
               allocation and the bounded buffering of frames that
               overtake their own OpenDataChannel response

Service traffic keeps its precedence structurally rather than by a second
scheduler: the transport already serializes writes in arrival order, so a
MSG, OPN or CLO chunk that becomes ready while a frame is being written is
admitted immediately after it.

STR dispatch is wired into both channel implementations but is inert until
EnableDataChannels is called, which is what the interoperability rule
requires of a peer that does not implement the errata: a capable peer never
speaks first.

Every new public type carries [Experimental("DataChannels")] under
#if NET8_0_OR_GREATER, matching the existing KeyCredentialBridge pattern.

Verified against the specification's own bytes: the thirteen published hex
vectors decode to the expected fields and re-encode byte for byte, and the
framing and sequencing test assertions carry their DCF and DCP identifiers
in the test names.

Found while implementing: the errata gives OpenDataChannel a
transportChannelId parameter in both the request and the response. No OPC
UA service reuses a parameter name across the two, and the model compiler
enforces it, so the response parameter is revisedTransportChannelId here
and the specification needs the same correction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
The framing layer landed in the previous commit; this adds the layer above
it: what a server does when a client asks for a channel, and the one
security check the QUIC transport cannot be built without.

Negotiation (Part 4 errata 5.1.1). The server revises rather than rejects
wherever it can, because a client that asked for more than it can have
usually wants the largest amount available. Direction and DeliveryMode are
the two exceptions and are rejected outright: silently downgrading to a
stronger guarantee adds unbounded latency to a media channel, and silently
downgrading to a weaker one loses data.

Zero means "no preference" in every numeric member -- except Priority,
where zero is the lowest of the eight real priorities and 255 is the
sentinel. Without that distinction IDataChannelSourceType.Priority could
never take effect. InitialCredit is revised *up* to at least MaxFrameSize,
because a window smaller than one frame is an immediate deadlock: the
channel opens Paused and the first frame can never be sent.

Service handler. Every Service in the set is scoped to both the
SecureChannel and the authorizing Session, not to the SecureChannel alone.
OPC 10000-4 permits several Sessions on one SecureChannel and they share
one ChannelId space; since ChannelIds are allocated monotonically from one
and are therefore trivially guessable, SecureChannel-only scoping would let
one user enumerate and seize another's channels. A channel owned by another
Session returns Bad_DataChannelIdInvalid, indistinguishable from an
unassigned identifier.

Authorization is re-evaluated rather than granted once, because a channel is
long-lived and moves content out of the server continuously and outside the
Service path -- a permission checked only at open is a permission that
cannot be revoked. The SecurityMode floor is enforced by the *server*: a
rule only the attacker is asked to obey is not a rule.

Offers are single-use, scoped to their SecureChannel and expiring, so an
unsubscribed client leaks nothing.

QUIC peer binding (Part 6 errata 7.6.1). The TransportSecured profile rests
on the premise that the TLS connection carrying the frames terminates at the
same application the control stream authenticated. That premise is not
self-evident: any party able to terminate QUIC between the two byte-forwards
the end-to-end-secured control stream so that OpenSecureChannel,
CreateSession and ActivateSession all succeed and every certificate check
passes, while reading, modifying, dropping and injecting every frame in the
clear. Both ends would report a fully authenticated SignAndEncrypt channel.

So the binding is by key, not by name. Equality of an ApplicationUri
subjectAltName is necessary but not sufficient -- CA and GDS implementations
commonly populate that SAN from the requester's own CSR without checking it
against an authoritative registry, so an attacker holding any certificate
from an accepted anchor could otherwise name the victim's ApplicationUri.
The test for DCQ-007 builds exactly that certificate and shows the two are
separated only by the key comparison.

Six more tests, 76 in total, all green. Documented in docs/DataChannels.md,
including what is implemented and what is not.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
Three things the previous two commits left open.

The SequenceNumber budget. OPC 10000-6 6.7.2.4 forbids reusing a
SequenceNumber under one TokenId and discharges that by assuming the token
lifetime is short relative to the chunk rate. Data channels invalidate the
assumption, not the rule: STR frames consume SequenceNumbers at the data
rate of the channel rather than at Service call rate. On a 10 Gbit/s link
minimum-size frames exhaust the 32-bit space in about two and a half
minutes, well inside a one-hour token.

That is not cosmetic. Where a SecurityPolicy derives a per-chunk IV or AEAD
nonce from the SequenceNumber, reuse under one key is a cryptographic
failure; where it does not, replay detection silently degrades, because a
stack checking only "incremented by exactly one" accepts the wrap.

The budget renews early *and* stalls late, because a slow renewal can be
overtaken by a fast channel: SendDataChannelFrameAsync refuses the chunk
that would reuse a value rather than renumbering it. The renewal threshold
is the lesser of 2^30 values and one minute of measured traffic, which is
what makes it work at both ends of the rate range -- a slow channel renews
on the fixed headroom and never renews needlessly, a channel fast enough to
burn 2^30 inside a minute renews on its own rate.

The opc.quic binding, in its own net8.0+ assembly because System.Net.Quic
needs a runtime Opc.Ua.Core cannot require. It carries the UACP
conversation on the first client-initiated bidirectional stream byte for
byte as over opc.tcp -- so it implements IUaSCByteTransport and the UASC
pipeline above it is unchanged -- and adds IMultiplexedByteTransport for
the stream-per-data-channel mapping that the chunk-at-a-time interface
cannot express. RESET is realized as QUIC RESET_STREAM carrying the
StatusCode as its application error code.

Fallback is not permitted to be a downgrade. Blocking UDP on 4840 is a
single firewall rule, so an unconditional fallback would hand an off-path
attacker a downgrade primitive; IsAcceptableFallback refuses any endpoint
weaker than the one required of the QUIC endpoint.

Unreliable datagrams are *not* implementable here and the binding says so
rather than pretending. QuicConnection exposes no RFC 9221 datagram API
through .NET 10, so MaxDatagramSize is zero, SupportsUnreliableDatagrams is
false, and a request for Unreliable or PartiallyReliable is refused with
Bad_DeliveryModeUnsupported -- which is what the errata requires, because
silently carrying them on the channel's stream would deliver a reliability
guarantee the application did not ask for and did not budget latency for.

A loopback suite that runs two managers against each other over an
in-memory transport, encoding and decoding every frame on the way so the
loop exercises the real codec rather than passing objects across. It is
what turns the per-clause unit tests into evidence that the parts compose:
the connection-credit bootstrap actually unblocks both directions,
ascending delivery holds under load, a PING is answered while credit is
exhausted, receiving END really does leave the receiver's own direction
open, and a RESET carrying Good reaches Closed on both peers while a Bad
one reaches Faulted.

Close, Reset and TryPing move from internal to public on DataChannel. An
application holding a channel has to be able to close it; that they were
internal was an oversight the loopback tests exposed immediately.

98 tests on net8.0 and net10.0, 93 on net472, all green. Solution builds
across all six target frameworks with no new warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
The QUIC binding previously had the constants, the fallback rule and the
TLS key binding, but nothing that established a connection. This makes
opc.quic a transport a client can actually connect over: connect, open a
SecureChannel, create a Session, call Services, and carry data channels
on per-channel QUIC streams.

The listener is plumbing rather than a rewrite. Opc.Ua.Bindings.Https
already proves an ITransportListener can live outside Core and drive a
TcpServerChannel through Attach and StartReceiveLoop, so the QUIC listener
accepts a connection, accepts its first bidirectional stream as the control
stream, and hands that to the existing UASC server implementation. Chunking,
security, token renewal and session dispatch are reused untouched.

The binding targets net9.0 and net10.0. System.Net.Quic is still behind
[RequiresPreviewFeatures] on net8.0, and opting in would emit an assembly
attribute that every consumer would have to opt into in turn. Core itself
still builds for all six target frameworks.

ALPN is offered and enforced. A QUIC endpoint serving some other protocol
on the same port is abandoned rather than mistaken for an OPC UA Server.

Data channels over QUIC set HasTransportFlowControl, so no CREDIT frame is
sent or expected and Paused follows QUIC's own blocking; duplicating the
window in two layers gains nothing and deadlocks when the two disagree.
RESET becomes RESET_STREAM carrying the StatusCode as its application
error code.

Unreliable datagrams stay refused with Bad_DeliveryModeUnsupported.
QuicConnection exposes no RFC 9221 datagram API through .NET 10, so
SupportsUnreliableDatagrams is False. Refusing is what the errata requires;
silently carrying an unreliable channel on a reliable stream is not.

Fix a scheduler bug that capped a channel at one quantum per idle tick.
A round's deficit is (Priority + 1) x MaxFrameSize, one frame, and the loop
then waited 20 ms for the next tick, so a saturated channel moved about
fifty frames a second. A round that leaves payload queued now schedules the
next one immediately. The sample measured 0.5 Mbit/s before and 1.3 Gbit/s
after, and ManyFramesDrainWithoutWaitingForTheIdleTick fails if the wake is
dropped again. The unit tests never caught this because none of them
measured time; the bug surfaced the first time the sample was run.

Also wires the DataChannelCapabilities Object, whose absence is precisely
how a server declares it does not support data channels, and adds
samples/ConsoleDataChannelStreaming, which drives the same application code
over --transport tcp and --transport quic against the same server.

103 data channel tests pass on net9.0 and net10.0, 94 on net472 and net8.0
where the QUIC tests do not apply. Every QUIC test is guarded by
QuicConnection.IsSupported so an agent without msquic skips rather than
fails. Opc.Ua.Core.Tests shows no regression at 4134 passing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
Coverage of the data channel and opc.quic code was 49.6%. The existing 103
tests concentrated on framing, sequencing and negotiation -- the parts
easiest to unit test -- while roughly a quarter of the code had no test
referencing it at all. This adds 123 tests across eleven files and takes
line coverage to 87.6%.

The gaps that mattered were not the largest ones. DataChannelServiceHandler
is the whole server-side Open/Modify/CloseDataChannel surface, the most
specification-visible code in the change, and none of its eleven StatusCode
branches were asserted. QuicTransportListener is the accept path, and the
existing QUIC harness built raw System.Net.Quic listeners itself, so the
listener had never run in a test. Both are now covered end to end, which
retires the standing caveat that the listener had never been exercised
through a real server flow.

Measuring first also found a file the reference map had missed entirely:
Stack/Tcp/UaSCBinaryChannel.DataChannels.cs, the inline TCP STR dispatch,
at 0% across 115 lines. It sits outside Stack/DataChannels/ and only the
coverage report caught it.

Writing the tests found three defects, all in code the pre-existing
end-to-end tests had executed without asserting.

QuicTransportListener captured the TLS certificate in the accept callback's
closure, so CertificateUpdate moved the UASC layer to the rotated
certificate while TLS kept presenting the retired one. That breaks the very
key-equality check of Part 6 errata 7.6.1 that the errata exists to enforce,
and leaves a revoked or expired certificate in use until restart. The
callback now reads a field, endpoint descriptions are refreshed the way the
TCP listener already did, and retired certificates are held until close so
an in-flight handshake is never pulled out from under.

QuicConnectionBuilder.ConnectAsync caught only QuicException. An ALPN or
certificate rejection surfaces from the TLS handshake as
AuthenticationException, so it escaped as a raw platform exception and
callers using the stack's catch (ServiceResultException) idiom missed ALPN
failures entirely. It now maps to Bad_SecurityChecksFailed.

DataChannel.TryPing had no channel-state guard although Write guards Closed
and Faulted. On a dead channel it took a sequence number, enqueued a PING,
re-woke the scheduler and latched m_pingOutstanding, so the channel could
later be declared dead by a ping that should never have been sent. TryPing
is public API. It now returns false, matching the Try* contract.

Also guards the discard-path receive credit Release with
!HasTransportFlowControl, matching TryAccount. Over QUIC the two were
asymmetric, so discarded frames accumulated credit that was never accounted.
Inert in practice because the only consumer sits behind the same guard, but
it published a misleading diagnostic.

The tests assert behaviour rather than reachability, because this code has
already shown that the difference matters: the 2600x scheduler throughput
bug fixed earlier on this branch had every line of the scheduling loop
executed by the suite and still shipped, since nothing measured the rate.

226 tests pass on net9.0 and net10.0 and 184 on net472 and net8.0, where
QUIC does not apply. Every QUIC test is guarded by QuicConnection.IsSupported
so an agent without msquic skips rather than fails, and there is deliberately
no build-time coverage gate because a gate would fail exactly those agents.
Opc.Ua.Core.Tests shows no regression at 4134 passing, and the streaming
sample still runs over both framings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
Reviewing the implementation against the errata clause by clause, rather
than against itself, found four divergences. One is a deadlock that would
have stopped every long-lived stream in production.

Connection credit was granted once and never replenished. Part 6 errata
§5.8.2 says a CREDIT frame on a non-zero ChannelId grants ChannelCredit to
that channel "and, if ConnectionCredit is non-zero, that amount to the
connection as well". The sender put the connection grant on the wire and the
encoder wrote it, but the receiver read only ChannelCredit and dropped the
rest: the connection window was reachable solely through the one-shot
EnsureConnectionCreditGranted. On inline framing that window is
MaxCreditPerChannel x MaxDataChannels, 16 MiB by default, after which every
channel stalls permanently with no way to recover. Nothing caught it because
no test and neither sample moved more than a megabyte, and the streams this
errata exists to carry run for hours. The manager now applies the connection
grant when it dispatches a per-channel CREDIT, and faults every channel if
that grant would overflow, since the connection window belongs to no single
channel.

The QUIC Message header sent a zero SecureChannelId. The reasoning was that
the QUIC connection already identifies the SecureChannel, but §5.1 requires
the field to carry the enclosing SecureChannel under both framings, and the
specification's own quic_datagram_unreliable vector carries 41340. The
vectors did not catch this because SpecVectors.QuicPrefix skips the first
twelve bytes, so the codec was checked from the stream header onward and the
message header was never compared against anything. A test now compares the
emitted header to the vector, and asserts the vector's own SecureChannelId is
non-zero so the comparison cannot quietly become vacuous.

A same-key certificate re-issue tore down every connection. The listener
matched on thumbprint, but a re-issue that keeps the key produces a new
thumbprint and an unchanged subjectPublicKeyInfo. Since the binding of §7.6.1
is by key, such a connection remains consistent and §7.6.2 requires it be
left alone; matching on the thumbprint aborted every live media stream on an
ordinary scheduled renewal. The listener now compares the key.

Unsupported frame types and payload on a frame that carries none both mapped
to Bad_DataChannelLimitsExceeded, which describes neither. The errata now
names Bad_DataChannelFrameTypeUnsupported and Bad_DataChannelFrameInvalid for
these two faults, and they are added to the model compiler inputs as
provisional 1111 and 1112.

230 tests pass on net9.0 and net10.0, 186 on net472 and net8.0.
Opc.Ua.Core.Tests shows no regression at 4134 passing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
A conformance review found that the components were right and the wiring
was missing. Several obligations were implemented correctly, covered by
tests, and never invoked from any production path: QuicPeerBinding.Verify
had eight test references and no callers, DataChannelServiceHandler was
never constructed, DataChannelManager.Remove was never called. This
connects them, and fixes what connecting them exposed.

Security
--------

The TLS peer binding of Part 6 errata 7.6.1 now runs. That check is the
whole premise of the TransportSecured profile - it is what proves the TLS
peer is the OPC UA peer - and without it a TLS terminating relay would read
and inject every media frame while both ends reported an authenticated
SignAndEncrypt SecureChannel. The comparison itself moved from
X509Certificate2.GetPublicKey, the raw subjectPublicKey bits, to
PublicKey.ExportSubjectPublicKeyInfo, which is the artifact the clause
names and which additionally covers the AlgorithmIdentifier. A
subjectAltName check against the EndpointUrl host was added beside it.

Certificate rotation now behaves as 7.6.2 requires: a key change closes the
connections bound to the superseded key with a QUIC application
CONNECTION_CLOSE carrying Bad_SecurityChecksFailed rather than per-stream
resets that may not survive the close, connection admission is fenced
against an activation epoch so a handshake in flight cannot be admitted
under a retired key, and a connection is closed when its own bound
certificate becomes revoked, untrusted or expired. A same-key re-issue
still disturbs nothing. TLS resumption is disabled for opc.quic because
.NET exposes no way to invalidate outstanding tickets at activation; the
errata now names that as the conforming fallback.

Correctness
-----------

Securing a STR chunk is now serialized against Service traffic under the
channel's DataLock. Sharing the SequenceNumber sequence, which the errata
already required, also means sharing the symmetric keys and the counter
that produces it, and nothing serialized the two writers. The scheduler
thread and a Service response reached the same HMAC concurrently: on
Windows the CNG hash provider refuses outright and faulted the scheduler
round, and where it did not throw it raced for SequenceNumbers and emitted
duplicates, which a peer is obliged to treat as a replay. Found by running
a Client against a Server, not by any test.

The Session layer rejected the DataChannel request types as an unexpected
RequestType, so a real Client could not reach the Service handler even once
the dispatch was wired. Also found by running it.

MaxDataChannels counted channels that had already ended, so a SecureChannel
refused every new channel after sixteen open-close cycles with none open.
The limit now counts only non-terminal states and ended channels are
released. Their identifiers are never reissued, which is what still lets a
Close on one return Bad_DataChannelClosed rather than
Bad_DataChannelIdInvalid.

DCF-006 and DCF-007 now emit the RESET the errata requires instead of
silently dropping the frame; buffered frames are replayed with their real
FrameType rather than as zero-length DATA, so a buffered RESET still
resets; the unknown-ChannelId buffer is bounded by encoded frame size
rather than payload, so zero-payload frames cannot be queued without limit;
and MaxBitrate is revised against the source instead of always returning 0,
which in the same table means unconstrained. MaxBitrate could not be
revised before because DataChannelParametersDataType had no such field.

QUIC transport
--------------

Data channel direction now maps to QUIC stream type and initiator as 7.4
requires, keyed off the OPC UA role rather than the QUIC role, so reverse
connect inverts the stream types without mis-assigning channels. A
SourceToSink channel allocates a server-initiated unidirectional stream and
returns its id in revisedTransportChannelId; a Client-initiated direction
that omits transportChannelId is refused. opc.tcp is unaffected.

Server side
-----------

StandardServer now serves OpenDataChannel, ModifyDataChannel and
CloseDataChannel, aborts a Session's channels when it closes, raises the
audit events, and drives the authorization recheck on its interval and on
role changes. The sample gained a server mode that opens a data channel
through a real Session over opc.tcp and streams over it, which is what
found the two bugs above; the direct-manager mode is kept, since that is
what found the scheduler throughput bug earlier.

Not fixed, deliberately
-----------------------

The non-downgrading fallback rule of 7.9 has nothing to attach to:
Opc.Ua.Client contains no reference to opc.quic and no cross-scheme
fallback exists anywhere in it, so a Client that never falls back conforms
vacuously. Wiring the predicate would mean first inventing the fallback,
which is a feature rather than a fix. Client-side 0-RTT refusal is likewise
unreachable: .NET exposes no early-data control on QuicClientConnectionOptions.

255 tests pass on net9.0 and net10.0 and 196 on net472 and net8.0.
Opc.Ua.Core.Tests is unchanged at 4134. The one failure in
Opc.Ua.Server.Tests, ConfigureApplicationBuildsSharedClientAndServerConfiguration,
was verified to fail identically on the parent commit and is not a
regression from this change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
The sample now opens a data channel through a real Session against a real
Server over opc.quic as well as opc.tcp, which is what the exercise was
for. Getting there needed two production fixes and one missing seam.

StandardServer created listeners only for schemes in the hardcoded
Utils.DefaultUriSchemes list, so a base address using any other scheme was
silently ignored and no listener was ever created for it. Registering the
QUIC binding therefore appeared to succeed and did nothing. It now derives
the schemes from the base addresses actually configured, which fixes the
general case rather than only opc.quic: any transport binding outside the
built-in set was previously unusable with StandardServer, with no error to
say so.

ServerBase did not map opc.quic to a transport profile, so even a listener
that had been created advertised an endpoint with no ProfileUri.

Neither is reachable from a unit test that constructs a listener directly,
which is why both survived a green suite and surfaced the first time a
Client tried to reach a Server over QUIC.

The missing seam was on the client. The Server reaches its data channel
transport through UseQuicDataChannelTransport; a Client had no equivalent,
so binding its DataChannelManager to the QUIC stream the Server named in
revisedTransportChannelId meant unwrapping the transport by reflection.
QuicClientChannelExtensions is the mirror of the server extension, and
QuicPeerBindingTransport.Inner exposes the connection the 7.6.1 peer
binding wraps, so the unwrapping is part of the contract rather than a
trick.

Both transports verified by running them:

  tcp  : framing Inline, transport chan id 0, 300/300 frames, 0 stalls
  quic : framing Quic,   transport chan id 3, 300/300 frames, 0 stalls

The QUIC transport channel id is the server-initiated unidirectional
stream 7.4 requires for a SourceToSink channel, allocated by the Server and
returned to the Client; the zero on inline framing is correct, since there
is no per-channel stream there. Zero credit stalls on QUIC is also the
expected reading rather than an absence of evidence: QUIC owns the flow
control on that path and no CREDIT frame is exchanged.

255 tests pass on net9.0 and net10.0, 196 on net472 and net8.0.
Opc.Ua.Core.Tests unchanged at 4134.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
Records what the follow-up work changed: the Services are now served by
StandardServer rather than merely implemented, the 7.6.1 peer binding is
invoked on the connect path rather than existing only as tested and
uncalled code, and the sample opens a channel through a real Session over
both transports.

Adds the four defects that running it found and that the suite could not
reach, with why each was invisible to a test - concurrent securing of a STR
chunk against Service traffic, Session refusing the request types, and
StandardServer and ServerBase both ignoring any transport scheme outside
the built-in list.

Coverage is restated at 80.1 percent, down from 87.6 while the test count
rose from 226 to 255. Wiring the uncalled obligations added production code
faster than tests were added for it. It is the more honest figure: the
earlier one measured a smaller body of code, much of which nothing invoked.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
Six findings from a security review of the change set, four of which were
fail-open: they behave correctly against every honest peer and only
misbehave in the presence of an attacker, so nothing in ordinary testing
revealed them.

The shipped default authorizer granted everything. StandardServer fell back
to a permissive IDataChannelAuthorizer that returned true unconditionally,
and no other implementation existed anywhere in the tree. Part 4 §7.2 is
explicit that a Server shall not grant a data channel where it would refuse
a Read of the same content, so any Session - Anonymous included - could
open a channel on a source whose RolePermissions deny it, and because the
same object backs the periodic recheck, revocation never took effect
either. The default now resolves the Session, builds the OperationContext a
Service call would, and asks the owning NodeManager to validate
PermissionType.Read against the source Node, so RolePermissions,
UserRolePermissions and AccessRestrictions all apply without reimplementing
any of them. It fails closed: an unresolvable Session or an error during
validation denies. A source that is not a Node in the AddressSpace has no
permissions to evaluate and remains governed by its registration, matching
what ValidateRolePermissionsAsync already does for unknown Nodes.

Reverse connect performed neither half of the §7.6.1 peer binding. The
forward client path gets it because the transport is wrapped in
QuicPeerBindingTransport; RunReverseConnectAsync built a bare transport, so
QuicPeerBinding.Verify was never reached, and with ServerCertificateValidation
left unset the TLS check degraded to the machine root store rather than the
OPC UA trust list. An on-path attacker holding any certificate chaining to
any CA in the host's store, with a SAN for the configured name, could
therefore terminate TLS and byte-forward the control stream: OpenSecureChannel,
CreateSession and ActivateSession all succeed with genuine certificates,
both ends report an authenticated SignAndEncrypt channel, and every data
channel frame - carrying no UA-SC security under TransportSecured - is
readable and injectable. Reverse connect now uses the application's
certificate validator and runs the two-artifact binding §7.6.2 defines for
the inverted TLS roles.

Per-SecureChannel data channel state was never released. The dictionary is
keyed by SecureChannelId and only cleared at shutdown, and each entry holds
a DataChannelManager with a running scheduler, so a peer that repeatedly
opened and closed a SecureChannel accumulated them without bound. State is
now reaped once no Session remains on that SecureChannel, which is also the
point past which no data channel could be authorized on it anyway.

The negotiated MaxFrameSize was unenforced on receive until a conforming
frame happened to arrive, because the tracking field started at
uint.MaxValue and was only ever lowered by a frame that already fit. A
sender that never set MessageStart kept it unbounded. It now starts at the
negotiated value, and the §5.2 grace window applies only from an actual
ModifyDataChannel reduction. The delivery queue that compounded this over
QUIC - where transport flow control means no credit accounting - is now
bounded with blocking backpressure rather than a reset, so reliable
delivery is preserved and QUIC's own flow control does the throttling.

Pre-authentication QUIC admission state accumulated. The activation-epoch
snapshot is inserted from the TLS handshake callback, before any
authentication, and was only removed for connections that completed the
handshake, so an unauthenticated peer that offered the right ALPN and then
abandoned the handshake left an entry permanently. Entries now expire at
the handshake timeout and hold a certificate reference rather than a copy.

A test that asserted the old reverse-connect behaviour was rewritten rather
than deleted: it configured an accept-all validator and expected rejection,
which only held while the code ignored the configured validator and fell
through to OS trust. It now configures a validator that genuinely refuses,
which is what "fails closed when the peer is untrusted" has to mean.

258 tests pass on net9.0 and net10.0, 197 on net472 and net8.0. The sample
still moves 300/300 frames over both opc.tcp and opc.quic.
Opc.Ua.Core.Tests unchanged at 4134.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
Third security review of the data channels feature. Four findings in the
reference implementation, plus two bugs found while fixing them.

Authorization no longer fails open. The default authorizer treated a
source with no owning NodeManager as authorized, on the reasoning that
registration is a deliberate server-side act. That was wrong:
registration says the source exists, not that this user may read it. It
now denies, and evaluates AccessRestrictions alongside RolePermissions
as MasterNodeManager does for a real Read. Registry-only sources need an
explicit authorizer, which the sample now demonstrates.

The QUIC listener now performs the mutual TLS binding that Part 6
errata 7.6.1 requires, on the normal accept path and not only under
reverse connect. The rule is two obligations, not one: request a client
certificate on every connection, and refuse OpenDataChannel where the
connection completed without one. Collapsing them into a handshake
failure would make the Discovery Services unreachable, since GetEndpoints
runs on a SecurityPolicy None channel that has no certificate to
present. The refusal therefore lands at OpenDataChannel, with
Bad_SecurityChecksFailed, rather than silently degrading to a
Service-only transport.

The SequenceNumber budget now observes every symmetric chunk. MSG, OPN,
CLO and STR all draw from one space, and reuse under one TokenId reuses
the AEAD nonce derived from it. GetNewSequenceNumber is the single
chokepoint, so the hook lives there, and the client brings token renewal
forward when the space runs low instead of waiting on the lifetime timer.

A stream already bound to a data channel can no longer be rebound to
another; it previously returned success.

Two bugs found while fixing the above:

The budget never actually reset. OnTokenActivated zeroed the counter,
but the next synchronization re-observed the channel-lifetime
m_sequenceNumber and restored it, so a per-token budget behaved as a
channel-lifetime one: renewal would latch on permanently and data
channels would eventually stall for good on a long-lived channel. The
budget is now rebased on the counter value at activation.

Wrapping the accept path in QuicPeerBindingTransport silently broke data
channels over QUIC. The wrapper is what the channel holds, and it did
not forward IUaSCSecureChannelBoundTransport, so the QUIC transport
never registered against its SecureChannel and OpenDataChannel fell back
to a Service-only transport. Caught by the sample, not by the unit suite.

Validated: data channel tests 273/273 on net9.0 and net10.0, 203/203 on
net472, net48 and net8.0; Opc.Ua.Core.Tests 4134; Opc.Ua.Server.Tests
3575; Opc.Ua.Client.Tests 2104; and the sample end to end over both
--transport tcp and --transport quic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
UA Secure Conversation requires that symmetric chunks arrive with strictly
increasing SequenceNumbers, and the receiver kills the channel otherwise.
The sending side did not guarantee it. A chunk's SequenceNumber was drawn
inside WriteSymmetricMessage under the securing lock, but the send was
deliberately awaited outside that lock so a slow peer could not block
Service traffic, and TcpByteTransport.SendChunkAsync then serialized the
socket write on a SemaphoreSlim that is not FIFO-fair. Assignment order
and transmission order were therefore decoupled, and chunk N+1 could win
the semaphore ahead of chunk N.

This needs two concurrent writers on one channel to show, which is why it
survived: the data channel scheduler alone stays ordered by luck. Add a
Subscription and the Publish responses supply the second writer, and the
channel dies with

  ProcessStreamMessage - Duplicate sequence number: 13907 <= 13908
  ForceChannelFault ... 'a data channel frame violated the framing rules:
  MalformedHeader'

A per-channel FIFO send gate now spans every path that hands secured
chunks to the transport. A ticket is taken at the instant the
SequenceNumber is drawn and under the same mutual exclusion, and a writer
waits for its turn before writing and releases it afterwards, including
on the failure paths - a stranded ticket would deadlock the channel.
The "secure under the lock, send outside it" property is preserved.

The benchmark that found it is added as --mode benchmark in the streaming
sample. It measures data channel throughput against a competing Publish
load at 10 ms, 100 ms and 1000 ms.

Most of the work in it is refusing to report numbers that look fine and
mean nothing, because this measurement fails quietly in several ways:

- An early version reported that Publish load made the channel twice as
  fast. Every loaded case had received zero notifications: a ManagedSession
  defaults to the newer subscription engine, where a classic Subscription
  is accepted, reports itself Created, reports no item errors, and never
  causes a single Publish request to be sent. The benchmark now counts the
  notifications it actually received and says so when there are none.
- The Server revises a publishing interval below its minimum and rounds it
  to the publishing resolution, both 100 ms by default, so a 10 ms case
  silently becomes a second 100 ms case that agrees with the first for
  entirely the wrong reason. The revised value is reported next to the
  requested one.
- Cases run sequentially on a warming process produce a clean monotonic
  curve that is an artefact of the order. Cases are round-robined inside
  each pass.
- Four medians in a column invite ranking. Where a loaded case overlaps the
  baseline's own run-to-run spread the benchmark states that the ranking is
  not supported by the data.
- The Publish rate under load means nothing without the rate the same
  subscription reaches while the channel is idle, so that control is
  measured and both are reported.

Measured on this machine over loopback: about 768 Mbit/s over opc.quic
against 390 Mbit/s inline over opc.tcp, and no resolvable effect of the
Publish load on either transport in either direction.

One defect is recorded rather than fixed: the credit-stall counter is
incremented over opc.quic, where credit is not in force and CREDIT frames
are neither sent nor expected, so it reports hundreds of thousands of
stalls on the runs with the highest throughput measured. The counter is
wrong, not the transport.

Validation: Opc.Ua.Core.Tests 4134, Opc.Ua.Core.DataChannels.Tests 274,
Opc.Ua.Server.Tests 3575, and the sample end to end over both
--transport tcp and --transport quic with no ordering faults.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
The CreditStalls diagnostic reported 560,000 stalls on opc.quic runs that
simultaneously produced the highest throughput measured anywhere in the
benchmark - roughly double the inline figure. Nothing had stalled.

DataChannelCredit.TryConsume counts a stall whenever the send window is
smaller than the payload, and the window was still consulted over QUIC,
where the transport provides its own per-stream and per-connection flow
control and CREDIT frames are neither sent nor expected. Both the Part 6
errata and this repository's own documentation state the counter should
stay at zero there.

The window is now simply not consulted where HasTransportFlowControl is
true, rather than being consulted and its answer discarded - the latter
is how the defect arose, and it had already been half-applied by seeding
the window with uint.MaxValue, which hid the intent without removing the
counting. The inline path is untouched: over opc.tcp and opc.wss the
credit window is the flow control and its stall counter is a real signal.

The defect was diagnostic only. The QUIC send path already bypassed
credit blocking, so the ignored result never gated a send and no
throughput figure was affected.

Validated: the new regression test fails against the previous code and
passes now; data channel tests 206 on net472, net48 and net8.0 and 276 on
net9.0 and net10.0; Opc.Ua.Core.Tests 4134; and the QUIC benchmark now
reports zero stalls in every case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1a54492-72a7-420f-bbda-38ae8c07cb92
Resolve the Core event ID, internals visibility, and StandardServer startup conflicts while preserving data-channel initialization and master bind-phase freezing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
A review against the OPC UA Data Channels errata found five defects. Four are
implementation faults; two of them also exposed gaps in the draft, which are
proposed separately in the drafts repository.

Validate transportChannelId where the result is visible (Part 6 7.4)

QuicServerDataChannelTransport.BindClientStreamAsync discarded the task
carrying every 7.4 check, so a Client could name the control stream, a stream
it did not initiate, or one already bound, and the Server answered Good and
echoed the value. The checks were right; nothing consumed them. Validation and
stream reservation now run inline where a refusal becomes the Service result.
Only the wait for the stream to materialize stays deferred, because a
peer-initiated QUIC stream is observable only once the peer writes to it and a
Client writes only after it has the response - awaiting it would deadlock the
exchange rather than order it.

Stop one unread channel stalling the SecureChannel (Part 6 5.8)

The delivery queue was bounded in frames but sized from a byte credit, and it
blocked when full. Small frames therefore filled it long before credit ran
out, and the block landed on the reader that also carries MSG, OPN and CLO, so
a slow consumer on one channel stalled every Session, Subscription and Publish
on the connection - the opposite of "stalls that stream and nothing else". The
queue is now bounded by encoded frame bytes, as 7.4 bounds the unknown-ChannelId
buffer and for the same reason, and a peer that exceeds the bound is reset
rather than waited on. This also removes a sync-over-async violation.

Open the channel when the response is dispatched, not before (Part 4 5.1)

OnResponseSent ran before the response object was encoded, so MarkOpen could
wake the scheduler and put a frame for a ChannelId on the wire ahead of the
response naming it. SecureChannelContext gains an optional ResponseDispatched
callback that both listeners invoke after handing the response to the
transport; it is inert when unset. The scheduler additionally refuses to serve
a channel still Opening, which is what the 5.13 state table already said.

Carry inline data channels instead of discarding them (Part 6 5.16)

A Server advertised opc.tcp, accepted OpenDataChannel, and then dropped every
frame through a transport whose send path did nothing - the silent drop 5.16
forbids. InlineServerDataChannelTransport resolves the UASC channel behind the
request and enables the engine on it, so channels ride the connection the
Client already holds. A SecureChannel that can carry no frames is now refused
with Bad_DataChannelTransportUnsupported.

Give Uncertain_DataDiscarded its own identifier

It shared identifier 1111 with Bad_DataChannelFrameTypeUnsupported. The two
differ only in severity bits, so the collision produced valid but wrong
constants instead of a build error; it is now 1113. The generator fails on a
duplicate identifier and a test asserts the emitted constants are unique.

Every regression test added here goes through the production entry point
rather than the component, because that is precisely what the existing
component-level tests missed in all four wiring defects.

Opc.Ua.Core.DataChannels.Tests 284 pass on net10.0 and 212 on net48;
Opc.Ua.Server.Tests 4072 pass; Opc.Ua.Sessions.Tests 776 pass with test
discovery unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
Poll for outbound RESET frames before asserting so the tests no longer race the manager scheduler thread. Strengthen the opening-channel lifecycle guard by granting connection credit before checking for premature DATA frames, and update documented test counts for net10.0 and net48.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
Data channels were welded into Opc.Ua.Core at seven points: the inbound
dispatch reached ReadSymmetricMessage and VerifySequenceNumber, the outbound
path reached DataLock, CurrentToken, WriteSymmetricMessage and the send gate,
and the channel published EnableDataChannels, DataChannels, SequenceBudget and
MaxDataChannelBodySize as its own API. None of that is reachable from another
assembly, so the code could not be lifted out.

This adds the extension point and reroutes the existing code through it,
without moving a single file. A green suite here is what proves the seam is
faithful; the move that follows is then mechanical.

The seam names no data channel concept. ISecureChannelMessageExtension owns a
MessageType that is neither a Service call nor part of establishing the
SecureChannel; ISecureChannelMessageHost is what the channel offers it. A
chunk is decrypted, verified and sequence-checked before the extension sees
it, so an extension never handles content the channel has not authenticated,
and a MessageType with no registered owner stays a protocol error - which is
what OPC 10000-6 6.7.2.2 requires of a receiver that does not implement it.

The send is the delicate part. Assigning the SequenceNumber and applying
message security stay serialized against Service traffic, because both draw on
the same keys and the same counter, and the callback the extension passes runs
inside that serialization so it can refuse the send atomically. That is what
the data channel SequenceNumber budget of Part 6 5.1.1 needs, and it is why
the callback exists rather than a plain "encode then send". The write itself
is still awaited outside the serialization, so a slow peer on an extension
cannot stall Service traffic. The callback is cached, so a frame allocates no
closure on the send path.

The budget splits along the same line: the mechanism, how many SequenceNumbers
this channel has issued under the token in force, belongs to the channel; the
policy of when to stall belongs to the extension.

DataChannelExtension is now the only thing that knows the frame format, and
the protected virtual OnDataChannelProtocolFault it replaces becomes an event,
so which framing rule was broken is still observable - a transport error alone
does not carry it.

Opc.Ua.Core.Channels is scaffolded here but still empty; the engine moves into
it next.

Opc.Ua.Core.DataChannels.Tests 284 pass on net10.0 and 212 on net48;
Opc.Ua.Sessions.Tests 777 pass, which is the suite that would notice a change
to the shared receive and send path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
The engine now consumes the message-extension seam rather than living inside
the assembly that provides it, so Opc.Ua.Core no longer carries a streaming
feature it does not need. Opc.Ua.Core.Channels references Opc.Ua.Core and
nothing else, ships as OPCFoundation.NetStandard.Opc.Ua.Core.Channels, and
targets the same six frameworks as Core so the net48 coverage survives.

What moved: the twenty-one engine files, the data channel half of the UASC
partial - now extension methods, because a partial class cannot span
assemblies - and IServerDataChannelTransport with
InlineServerDataChannelTransport, which turned out to touch only
SecureChannelContext, Profiles and the engine.

What deliberately did not. StandardServer.DataChannels.cs is a partial of
StandardServer, so it cannot move at all; its Service overrides and the
authorizer and auditor that read ServerInternal, MasterNodeManager and node
states are server concerns and stay. Opc.Ua.Bindings.Quic stays its own
net9.0+ package: System.Net.Quic is behind RequiresPreviewFeatures on net8.0,
and it references Opc.Ua.Server, so folding it into a Core.* package would put
that package above the Server layer.

Two things moved the other way, because they are the SecureChannel's own
concerns rather than the feature's. DataChannelSequenceBudget becomes
SequenceNumberBudget in Core: the space is shared by MSG, OPN, CLO and every
extension alike, and the client channel already used it to decide when to
renew a token. Keeping one instance on the channel also removes the second,
redundant budget the seam had briefly introduced. The SecureChannel registry
becomes UaSCSecureChannelRegistry, since mapping an identifier to the channel
that owns it says nothing about data channels.

Core loses EnableDataChannels and DataChannels on both channel types,
MaxDataChannelBodySize, SequenceBudget's data channel name, and the
CoreEventIds.DataChannel blocks; TransportChannelFeatures.DataChannels becomes
MessageExtensions. The feature is [Experimental], so removing rather than
forwarding is allowed. Log EventIds keep their existing numbers in
ChannelsEventIds so an operator's filters keep working across the move.
DataChannelFeature carries the experimental marker in Core, because it marks
the generated Service Set that Clients call, not the engine.

expected-packages.txt gains Core.Channels and Bindings.Quic - the latter was
packable but missing, which the file itself says should be a conscious act.

Opc.Ua.Core.DataChannels.Tests 284 pass on net10.0 and 212 on net48;
Opc.Ua.Server.Tests 4072 and Opc.Ua.Sessions.Tests 777 pass, which is what
covers the shared receive and send path the seam sits on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Code coverage

Coverage gate passed.

Check Result Threshold
✅ Project line rate 86.82% (244000/281052 lines) >= 70.00%
✅ Project branch rate 76.73% >= 60.00%
✅ Patch coverage 88.03% (3610/4101 changed lines) >= 75.00% (> 100 changed lines)
ℹ️ Baseline delta (advisory) +13.22 pp 73.60% recorded
Uncovered changed lines
  • src/Opc.Ua.Bindings.Quic/OpcUaQuicBuilderExtensions.cs: 70
  • src/Opc.Ua.Bindings.Https/Https/HttpsTransportListener.cs: 1737, 1741, 1742
  • src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs: 130, 131, 132, 133, 187, 188, 189, 208, 210, 211, 237, 314, 322, 324, 341, 342, 343, 357, 358, 359, 397, 398, 406, 415, 418, 430, 437, 440, 458, 547, 593, 594, 595, 596, 597
  • src/Opc.Ua.Server/Hosting/DependencyInjectionStandardServer.cs: 81, 86, 91, 96
  • src/Opc.Ua.Bindings.Quic/QuicTransportListener.cs: 115, 120, 121, 122, 276, 279, 311, 312, 350, 363, 440, 467, 486, 511, 533, 548, 561, 589, 596, 633, 644, 649, 681, 683, 684, 691, 693, 769, 772, 783, 788, 814, 815, 816, 818, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 834, 836, 837, 838, 839, 840, 841, 842, 844, 847, 848, 852, 854, 857, 890, 903, 905, 909, 929, 931, 932, 945, 965, 968, 969, 979, 982, 983, 1021, 1037, 1038, 1039, 1040, 1041, 1043, 1045, 1093, 1111, 1125, 1126, 1138, 1140, 1141, 1143, 1194, 1195
  • src/Opc.Ua.Core/Stack/DataChannels/Scheduling/DataChannelSendQueue.cs: 311
  • src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs: 213, 243, 305, 306, 307, 424, 425, 441, 444, 464, 465
  • src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelServiceHandler.cs: 252, 338, 339, 340, 341, 342, 422, 442, 535, 546, 547, 577, 582, 619, 670, 671, 672, 673, 674, 691, 692, 693, 694, 695, 724, 751, 768
  • src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs: 484, 485, 486, 505, 507, 508, 562, 564, 565, 568, 569, 575, 576, 579, 581, 591, 592, 594, 595, 596, 598, 599, 600, 602, 604, 606
  • src/Opc.Ua.Bindings.Quic/QuicClientChannelExtensions.cs: 47, 100, 101, 102, 103, 104, 105
  • src/Opc.Ua.Server/Server/InlineServerDataChannelTransport.cs: 67, 72, 117, 128, 134, 135, 136, 137, 138, 140, 142
  • src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs: 135, 137, 248, 250, 252, 295, 296, 297, 332, 377, 378, 379, 430, 432, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 450, 451, 504, 505, 507, 510, 511, 512, 513, 514, 524, 525, 527, 530, 531, 532, 533, 534, 535, 567, 585, 620, 652, 657, 683, 726
  • src/Opc.Ua.Bindings.Https/Tcp/KestrelTcpTransportListener.cs: 650, 654, 655
  • src/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs: 1427, 1493, 1497, 1498
  • src/Opc.Ua.Bindings.Quic/QuicStandardServerExtensions.cs: 22, 24, 27, 28
  • src/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs: 1127, 1128, 1135
  • src/Opc.Ua.Core/Stack/DataChannels/Services/QuicPeerBinding.cs: 156, 158, 168, 179, 204, 213, 215, 220, 226, 236, 238, 261, 263, 400
  • src/Opc.Ua.Core/Stack/DataChannels/DataChannel.cs: 286, 288, 375, 387, 447, 448, 502, 536, 598, 645, 676, 677, 679, 681, 682, 683, 684, 688, 711, 712, 713, 740, 795, 831, 879, 880, 918, 919, 940, 992, 993, 998, 1000, 1001, 1021, 1030, 1031, 1046, 1193, 1196, 1197, 1199, 1200, 1212
  • src/Opc.Ua.Bindings.Quic/QuicDataChannelTransport.cs: 98, 103, 158, 160, 161, 182, 183, 184, 185, 186, 285, 287, 288, 320, 323, 324, 330, 331, 437, 438, 439, 440, 453, 454, 455, 456, 564, 565, 566, 567, 568, 579, 590
  • src/Opc.Ua.Bindings.Quic/QuicByteTransportFactory.cs: 173, 210, 211, 212, 257, 259, 260, 261, 262, 263, 284, 286, 287, 288, 289, 290, 306, 350, 355, 357, 358, 360, 364, 391, 413, 414, 415, 444, 447, 449, 451, 453, 493, 498, 506, 511, 518, 523, 528, 529, 533, 534, 656, 657, 658, 668, 669, 670
  • src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs: 860, 862
  • src/Opc.Ua.Core/Stack/DataChannels/FlowControl/DataChannelCredit.cs: 260
  • src/Opc.Ua.Bindings.Quic/QuicTransport.cs: 51, 118, 123, 148
  • src/Opc.Ua.Core/Stack/DataChannels/DataChannelManager.cs: 450, 453, 515, 520, 676, 679, 888, 890, 898
  • tools/Opc.Ua.SourceGeneration.Core/Generators/StatusCodesGenerator.cs: 282, 283, 284, 285, 286, 287, 288
  • src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs: 1547, 1549, 1552, 1554, 1556, 1557, 1558, 1559, 1560, 1567, 1568, 1569, 1570, 1571, 1572, 1573
  • src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelNegotiator.cs: 265, 339, 359, 396
  • src/Opc.Ua.Bindings.Quic/QuicServerDataChannelTransport.cs: 32, 40, 41, 42, 43, 48, 49, 50, 51, 152, 153, 160, 162, 193, 201
  • src/Opc.Ua.Bindings.Quic/QuicTransportListenerFactory.cs: 93, 98, 105
  • src/Opc.Ua.Bindings.Quic/QuicTransportChannel.cs: 51, 53
  • src/Opc.Ua.Core/Stack/Server/ServerBase.cs: 503, 504, 505
  • src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs: 692

Coverage is above the recorded baseline - consider ratcheting coverage-thresholds.json.

Thresholds live in coverage-thresholds.json. Whole report before exclusions: line 85.91%, branch 75.87%.

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.18040% with 729 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.90%. Comparing base (978e6d1) to head (0321a9d).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/Opc.Ua.Bindings.Quic/QuicTransportListener.cs 75.78% 91 Missing and 47 partials ⚠️
src/Opc.Ua.Core/Stack/DataChannels/DataChannel.cs 84.79% 44 Missing and 27 partials ⚠️
...c/Opc.Ua.Bindings.Quic/QuicByteTransportFactory.cs 69.95% 48 Missing and 22 partials ⚠️
...pc.Ua.Server/Server/StandardServer.DataChannels.cs 78.83% 51 Missing and 18 partials ⚠️
...c/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs 78.26% 34 Missing and 16 partials ⚠️
...c/Opc.Ua.Bindings.Quic/QuicDataChannelTransport.cs 76.66% 33 Missing and 16 partials ⚠️
...DataChannels/Services/DataChannelServiceHandler.cs 82.23% 27 Missing and 19 partials ⚠️
src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs 65.88% 26 Missing and 3 partials ⚠️
...Ua.Bindings.Quic/QuicServerDataChannelTransport.cs 75.75% 15 Missing and 9 partials ⚠️
...ore/Stack/DataChannels/Services/QuicPeerBinding.cs 66.66% 14 Missing and 5 partials ⚠️
... and 28 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4240      +/-   ##
==========================================
+ Coverage   80.74%   80.90%   +0.16%     
==========================================
  Files        1984     2016      +32     
  Lines      276961   281052    +4091     
  Branches    48060    48733     +673     
==========================================
+ Hits       223625   227391    +3766     
- Misses      36752    36898     +146     
- Partials    16584    16763     +179     
Flag Coverage Δ
actions 80.90% <82.18%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...c/Opc.Ua.Client/Session/ManagedSession.Services.cs 88.62% <100.00%> (+1.18%) ⬆️
...pc.Ua.Core/Security/Constants/SecurityConstants.cs 100.00% <ø> (ø)
...Ua.Core/Stack/Configuration/EndpointDescription.cs 98.50% <100.00%> (+0.06%) ⬆️
....Ua.Core/Stack/DataChannels/DataChannelSettings.cs 100.00% <100.00%> (ø)
...tack/DataChannels/Framing/DataChannelFrameError.cs 100.00% <100.00%> (ø)
....Core/Stack/DataChannels/Model/DataChannelModel.cs 100.00% <100.00%> (ø)
...ack/DataChannels/Sequencing/DataChannelSequence.cs 100.00% <100.00%> (ø)
...c/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs 100.00% <ø> (ø)
src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs 80.78% <100.00%> (+1.07%) ⬆️
src/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs 47.61% <100.00%> (+2.61%) ⬆️
... and 44 more

... and 45 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Addresses the two-axis review of PR #4240 (test coverage and spec/API
completeness). The security fix is the one that matters most.

Security

* Authorization was direction-blind. IDataChannelAuthorizer.IsAuthorizedAsync
  took no direction and the default authorizer always evaluated
  PermissionType.Read, so a user permitted to read a source could open a
  SinkToSource or Bidirectional channel and write into the Server. Part 4
  errata section 7.2 names this exact failure. The interface now carries the
  direction, the call moved to after TryRevise so the negotiated direction is
  known while still preceding any ChannelId or stream allocation, and each
  required permission is validated on its own - ValidateRolePermissions treats
  a combined mask as "any of these", so asking for Read|Write in one call
  would have passed for a Read-only user.

Wiring

* AddQuicTransport() now registers the server-side data channel transport, and
  DependencyInjectionStandardServer resolves the transport, authorizer,
  auditor and sources from DI. UseQuicDataChannelTransport() stays as the
  direct-construction fallback.
* A closed SecureChannel or lost transport now faults every data channel
  riding on it, on both the inline and opc.quic paths (section 5.13). This was
  unimplemented; only the Session-close and identity-change rows of that table
  were covered.
* IServerDataChannelTransport.AbortSecureChannel had no caller and the QUIC
  implementation closed the whole connection, contradicting its own contract
  and section 5.11 ("a failed stream is not a failed connection"). It is now
  invoked when the Server tears down a SecureChannel's state, and tears down
  the data channels rather than the connection.

Tests

* DataChannelIntegrationTests: the end-to-end leg the repo requires and this
  feature lacked. A real Client Session drives Open, Modify and Close against
  a live StandardServer over opc.tcp, and payload crosses the same
  SecureChannel inline. It is the only coverage of the Service dispatch,
  per-SecureChannel state and authorization chain by a request that arrived
  off a socket.
* DataChannelGapTests and DataChannelRefusalTests cover the GAP lifecycle and
  the receive-side refusals, both of which were entirely at zero: the
  section 5.10 protocol error, the section 5.2.1 unbounded-state guard, DATA
  after END, wrong-direction DATA, receive credit exhaustion and credit
  release on discard.
* QuicListenerTests gains the public client seam (QuicClientChannelExtensions
  was 0/21 despite being the API a consumer uses) and the transfer handoff.
* CloseChannelsForCertificateReturnsAffectedChannelIdsOnAKeyChangeAsync never
  called the method it was named after; renamed to what it verifies, and a
  real test of CloseChannelsForCertificateAsync added.
* Two fixed-sleep assertions replaced with waits on an observable scheduler
  round counter, so the negatives are evidence rather than timing.

Coverage: Opc.Ua.Core.Channels 89.8% -> 92.9% line, Opc.Ua.Bindings.Quic
79.5% -> 80.7% line, which clears the repo's 80% bar it previously missed.
311 tests on net10.0 and 237 on net48, up from 284 and 216.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
Comment thread docs/DataChannels.md Outdated
Comment thread docs/DataChannels.md Outdated
Comment thread docs/DataChannels.md Outdated
Comment thread src/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs Outdated
marcschier and others added 2 commits August 11, 2026 22:32
A security review of this branch found no exploitable vulnerability, but
flagged the PONG responder as an unbounded, peer-driven growth path. Reading
the errata back shows it is not merely hardening: section 5.11 states the
obligation and names the attack.

  "PING is exempt from flow control and compels a PONG ahead of queued
   payload, which without a bound is an amplification surface: a peer could
   emit PING at line rate on every open ChannelId and compel the other end to
   answer at line rate ahead of its own traffic, with no window to close
   against it. A peer shall not have more than one unanswered PING
   outstanding per ChannelId, and shall not emit PING on a given ChannelId
   more than once per second. A receiver may discard a PING that violates
   either bound, and may RESET the channel with Bad_DataChannelLimitsExceeded
   if the violation persists."

Only the sending half was implemented (TryPing, TryPingConnection). The
receiving half answered every PING unconditionally, which bounds a
well-behaved peer and leaves a hostile one unbounded - the responder does the
work, so a rule the sender enforces on itself protects nobody.

Both halves are now enforced, on the per-channel path and on ChannelId 0,
which is a ChannelId and was otherwise left as a connection-level
amplification surface once the data channels bounded themselves.

* A PING inside the interval is discarded rather than answered. The prober
  keeps no state on the responder, so a dropped PONG costs it one measurement
  it was not entitled to take.
* A peer that keeps flooding after being ignored MaxPingRateViolations times
  is reset with Bad_DataChannelLimitsExceeded - the errata's "if the violation
  persists". A single burst is absorbed silently.
* The response interval carries a tolerance below the nominal one second.
  The sender measures the bound on its clock and the receiver on its own, so a
  conformant peer still delivers the occasional PING a few milliseconds early;
  without the tolerance ordinary jitter would be punished as a violation.
* "Never answered" is tracked with an explicit flag rather than a zero
  timestamp sentinel, because a TimeProvider is not required to start above
  zero and the bound would silently never engage if it did.

Tests

Five tests in DataChannelRefusalTests: the verbatim Timestamp echo, the
discard, recovery once the interval elapses, the escalation to RESET, and the
control channel. Verified as guards by reverting the production change and
confirming three of them fail.

UnknownChannelBufferIsBoundedByEncodedFrameBytes counted the PONGs that a
replayed PING burst produced as a proxy for how many frames had survived the
unknown-channel buffer. The rate limit correctly invalidates that proxy, so it
now sends DATA frames - which encode to exactly the same size, leaving the
bound arithmetic untouched - and asserts FramesReceived. It measures the
buffer bound it is named for rather than the ping rate limit.

316 tests pass on net10.0 and 242 on net48; Opc.Ua.Server.Tests is unchanged
at 4072 passed, because this sits on the shared receive path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
CI fix

Opc.Ua.Bindings.Quic hardcoded TargetFrameworks net9.0;net10.0 and so did not
take part in the repo's per-TFM pin. A UA.slnx build pinned with
CustomTestTarget builds the rest of the stack for that one TFM, which broke
this project three different ways:

* pinned net10.0 - the project's net9.0 leg referenced a stack built only for
  net10.0, so restore failed with NU1201. This is what failed the ubuntu
  Core.DataChannels job in 32 seconds.
* pinned net472/net48 - the net9.0 leg resolved the net472 assets of
  Opc.Ua.Core and failed with CS0012 on IAsyncDisposable.
* pinned net8.0 - System.Net.Quic is behind RequiresPreviewFeatures there, so
  the project cannot take part at all.

The project now follows the pin when the pin can host it, and otherwise opts
into the existing RestrictForLegacyTfm no-op shape that
samples/ConsoleDataChannelStreaming already uses. That mechanism only knew
about the .NET Framework and netstandard pins - the right floor for a .NET 8+
project - so it gains RestrictForLegacyTfmAdditionalTargets for a project
whose floor is higher. The shipped package shape is unchanged: an unpinned
build still produces net9.0;net10.0.

Verified by running the same matrix the CI legs run. All seven pinned
UA.slnx builds succeed: net10.0, net9.0, net8.0, net48, net472,
netstandard2.0, netstandard2.1.

That matrix also caught a break the earlier per-project validation missed:
SampleDataChannelAuthorizer was never updated for the direction-aware
IDataChannelAuthorizer signature, so the sample did not compile. It now
grants only the SourceToSink direction it actually publishes, which is the
point of the Part 4 errata 7.2 rule it demonstrates.

Seam simplification

The Stack/Tcp change was carrying weight it did not need. Three removals, no
behaviour change:

* SecureChannelMessage is gone. It crossed the boundary with exactly one
  producer and one consumer, was never stored or compared, and carried
  IEquatable, ==, != and GetHashCode purely to satisfy CA1815 - about sixty
  lines of ceremony. OnMessageReceived now takes the three values directly.
* The onSecuring callback is gone, and with it the SequenceBudget member of
  the host interface. It was the subtlest part of the contract - a callback
  that runs inside the send serialization and may veto atomically - and its
  only implementation consumed the budget. Core already owns
  SequenceNumberBudget and is the code doing the securing, so it now claims
  the number itself under the same lock. The guarantee is identical and the
  extension no longer has to reason about the serialization at all.
* GlobalChannelId is gone. It had no reader.

The host interface drops from nine members to six, and Stack/Tcp from 1358
added lines to 1270.

Moving the engine back into Opc.Ua.Core was considered and rejected: it would
remove about 330 of those lines, none of them the hard parts - the FIFO send
gate exists because there is a second writer on one sequence counter, not
because of an assembly boundary - while moving 7241 lines of engine into Core
on every TFM for an experimental feature.

316 tests pass on net10.0 and 242 on net48. Opc.Ua.Server.Tests (4072) and
Opc.Ua.Sessions.Tests (777) are unchanged, which is the check that matters
because the seam sits on the send and receive path of all Service traffic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
@marcschier marcschier changed the title Add OPC UA data channels, the opc.quic transport and a message-extension seam DRAFT Add OPC UA data channels, the opc.quic transport and a message-extension seam Aug 12, 2026
marcschier and others added 2 commits August 12, 2026 08:04
test-ubuntu-latest-SourceGeneration.Core failed on two hardcoded counts:
GetListOfServices() returned 42 rather than 39, and the Session category 7
rather than 4. Both are this PR: OpenDataChannel, ModifyDataChannel and
CloseDataChannel are three new Session-category Services, so the counts moving
by exactly three is the intended behaviour rather than a regression.

The category test now also names the three Services it expects to find, so a
future change to the figure has to say which Service moved in or out of the
category instead of silently renumbering.

Verified locally: the full Opc.Ua.SourceGeneration.Core.Tests suite passes,
3771 tests on net10.0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
…ayer

build-and-push-image (refserver) failed with NETSDK1004: the assets file for
Opc.Ua.Core.Channels was missing. The Dockerfile copies each csproj into the
restore layer by name so that layer caches independently of the sources, and
this PR gave Opc.Ua.Server a reference to the new project without adding it to
that list, so restore never saw it and the later --no-restore publish had no
assets file to read.

Verified locally by building the image end to end with docker build, both the
restore layer on its own and the full publish that CI runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
Comment thread src/Opc.Ua.Client/Session/ManagedSession.Services.cs Outdated
Comment thread src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs
Comment thread src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs Outdated
Comment thread src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj Outdated
The advisory patch coverage gate reported 60.30% against a 75% floor. Most of
the shortfall is structural rather than untested code: Opc.Ua.Bindings.Quic is
29.8% of the changed source lines and 60 of the 316 data channel tests skip on
the CI runner because msquic is not installed there, so that assembly reads
close to zero in the merged report while measuring 80.65% locally, where QUIC
runs.

Two genuine gaps were in that list and are closed here. ManagedSession and
RedundantClientSession each forward the three data channel Services to the
session they wrap, and neither forwarder was exercised: the existing
integration test drives ISession directly, so it never passes through either
facade. A dropped forwarder would be invisible until a caller found the
Service silently doing nothing, and on the redundant facade it would leave a
channel opened against a session the caller can no longer reach after failover.

Both fixtures already assert this obligation for every other Service, so the
new cases join the table-driven passthrough suite rather than standing alone.
The [Experimental("DataChannels")] opt-in is scoped to the new cases with a
pragma rather than switched on for the whole test project, so the attribute
keeps its meaning everywhere else.

Opc.Ua.Client.Tests 2126 pass, Opc.Ua.Redundancy.Client.Tests 123 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
marcschier and others added 3 commits August 12, 2026 11:17
The seam is inline-only. Checking every one of its symbols against
Opc.Ua.Bindings.Quic gives zero references to ISecureChannelMessageHost,
ISecureChannelMessageExtension, SequenceNumberBudget, the send gate and
UaSCSecureChannelRegistry alike - a QUIC data channel rides its own stream and
never becomes a UASC chunk, so none of it applies. The seam therefore exists to
keep the engine out of Core, not to invite other implementers: there is exactly
one, and no prospect of a second.

Publishing it would commit Core to an abstraction with a single implementer for
ever. Core already grants InternalsVisibleTo to its sibling first-party
assemblies - Opc.Ua.Client, Opc.Ua.Bindings.Quic, Opc.Ua.Bindings.Https,
Opc.Ua.Core.Diagnostics - and Opc.Ua.Core.Channels had simply never been added
to that list, which is the only reason the seam had to be public. Adding it
lets the following become internal with no code moving at all:

* ISecureChannelMessageHost and ISecureChannelMessageExtension
* SequenceNumberBudget, and UaSCUaBinaryChannel.SequenceBudget
* RegisterMessageExtension and TryGetMessageExtension
* DataChannelExtension and TryGetDataChannelExtension, which are the inline
  adapter rather than API - a consumer reaches the channels through
  EnableDataChannels and GetDataChannels, which return the engine

UaSCSecureChannelRegistry stays public deliberately. An application writing its
own IServerDataChannelTransport for inline framing needs it to resolve the
channel behind a request, which samples/ConsoleDataChannelStreaming
demonstrates; making it internal would break a real extension point to tidy an
API surface.

The public surface this PR adds to Opc.Ua.Core is now UaSCSecureChannelRegistry
and IUaSCSecureChannelBoundTransport, the latter being a transport extension
point alongside the existing public IUaSCByteTransport.

This captures the main benefit of folding the engine back into Core - not
owing compatibility on a single-implementer abstraction - without moving 4866
lines of experimental engine into Core on every TFM from net472 upward, and
without touching the send gate or the sequence budget, both of which are
inherent to inline framing wherever its code lives.

UA.slnx builds pinned to net10.0, 316 data channel tests pass, and
Opc.Ua.Server.Tests is unchanged at 4072.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
Review feedback on UaSCBinaryClientChannel.cs:893 - "Everywhere we make it
look like a generic extension, but we only handle stream." That is correct, and
checking it against the only other transport settles it: Opc.Ua.Bindings.Quic
references none of ISecureChannelMessageHost, ISecureChannelMessageExtension,
SequenceNumberBudget or the send gate, because a QUIC data channel rides its own
stream and never becomes a UASC chunk. The seam had exactly one implementer and
no prospect of a second; it existed only because the engine sat in another
assembly.

So the engine comes home rather than the seam being dressed up as general. The
destinations are the ones commit cfaa34f recorded as renames:

* Opc.Ua.Core.Channels/DataChannels/** (engine, framing, flow control,
  sequencing, scheduling, model and Services/) to Opc.Ua.Core/Stack/DataChannels
* IServerDataChannelTransport and InlineServerDataChannelTransport back to
  Opc.Ua.Server/Server, where the latter came from
* ChannelsEventIds folded into CoreEventIds, keeping 600/620 so existing log
  filters are unaffected

Deleted outright:

* ISecureChannelMessageExtension.cs, both interfaces. This also answers the
  request to rename that file after its interface: it no longer exists.
* UaSCBinaryChannel.MessageExtensions.cs, replaced by
  UaSCBinaryChannel.DataChannels.cs, which owns the DataChannelManager directly
  and dispatches STR after the existing decrypt, signature and sequence checks.
* DataChannelExtension, the adapter that existed only to bridge assemblies. Its
  IDataChannelTransport half is now a small private InlineDataChannelTransport
  that hands the channel to the engine.
* The Opc.Ua.Core.Channels project, its UA.slnx entry, its expected-packages
  line and its reference from the reference server Dockerfile. The package is
  new in this branch and never shipped, so nothing is owed compatibility.

EnableDataChannels and the channel accessors become ordinary members instead of
extension methods over a published seam, and the framing fault surfaces as
UaSCUaBinaryChannel.DataChannelProtocolFault, so a test no longer has to reach
through a registry to observe which rule was broken.

This supersedes the InternalsVisibleTo narrowing in 18a3077: the types it made
internal are deleted here, and the entry it added to Opc.Ua.Core is removed.
SequenceNumberBudget stays internal, which the move now justifies on its own.
UaSCSecureChannelRegistry stays public because a server writing its own
IServerDataChannelTransport needs it, as the sample does.

Separately, per the review of the QUIC csproj, opc.quic now targets net8.0 as
well and opts into preview features there, so a net8.0 consumer can do the same
rather than being excluded. The three file wide NET9_0_OR_GREATER guards are
gone; only QuicServerConnectionOptions.HandshakeTimeout, which is genuinely
.NET 9+, stays guarded, and the listener's own admission expiry still bounds a
stalled handshake on net8.0.

UA.slnx builds pinned to net10.0 and 316 data channel tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
… net8.0

Nine review comments; two were answered by the previous commit (the generic
extension seam and the file named after it are both gone). The rest:

Sample layout and a second sample

The data channel samples now live under samples/Core, and a new
ConsoleDataChannelAudio joins the throughput benchmark. It stands up a Server
and a Client in one process, synthesises a short melody as 16-bit PCM once at
startup and streams it on repeat over a data channel while the Client plays it
back. Nothing binary enters the repository.

The source writes in real time rather than as fast as the channel will take it,
because a media source is paced by its own clock and writing faster only adds
latency; the progress line reports frames, bytes and credit stalls so a
consumer that cannot keep up is visible rather than silently buffered. Measured
over 31 seconds: 1582 frames, 2725 KiB, zero credit stalls, which is exactly
real time for 20 ms frames.

Playback uses NAudio 2.3.0 (MIT), referenced only by that sample - nothing
under src/ gains a dependency. NAudio's output devices are Windows interfaces
and 2.x has no ALSA or CoreAudio backend, so on Linux and macOS the sample
writes the received stream to a WAV instead and says which it is doing on
startup. The Windows-only paths sit behind OperatingSystem.IsWindows() rather
than a CA1416 suppression.

Documentation

"Where the code lives" describes where the code is rather than where it moved
from, and the same edit removes "rather than in ...", "remains" and the rest of
the change narration. Two sections that were a development diary - the tables
of defects found while writing tests and while running the sample - are gone
entirely: they recorded history rather than the state of the implementation.

Logging

The response-dispatched callback logged through a message that reads "Failed to
send fault response to client", which is not what happened, and on the HTTPS
listener it invented a requestId of 0 to fit that signature. All three
listeners now have a named message that says what actually failed.

net8.0 for the QUIC binding

Rather than excluding net8.0, the binding targets it and enables preview
features, so a net8.0 consumer opts in the same way for System.Net.Quic. The
three file-wide NET9_0_OR_GREATER guards are gone; only
QuicServerConnectionOptions.HandshakeTimeout stays guarded because it is
genuinely .NET 9+, and the listener's own admission expiry still bounds a
stalled handshake there.

Removing the #if around [Experimental]

ExperimentalAttribute is a .NET 8 type, and the repo's rule is to polyfill
rather than fork with #if, so it joins the existing polyfills in
Opc.Ua.Types. Verified that this is a real opt-in and not just something that
compiles: with the suppression removed, a net48 caller of a data channel
Service fails the build with "error DataChannels", which is the same behaviour
as net10.0.

Validated with the matrix CI runs: all seven pinned UA.slnx builds (net10.0,
net9.0, net8.0, net48, net472, netstandard2.0, netstandard2.1), 316 data
channel tests on net10.0 and 242 on net48, Opc.Ua.Server.Tests 4072,
Opc.Ua.Client.Tests 2126, Opc.Ua.Redundancy.Client.Tests 123, and the reference
server Docker image end to end.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
@marcschier
marcschier requested a lite review from Copilot August 12, 2026 14:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

marcschier and others added 4 commits August 12, 2026 18:56
… send path

A data channel send that WriteSymmetricMessage refuses with limitsExceeded
returned without completing the send-gate ticket it had been handed. The
ticket then became the tail every later send chains behind, so MSG, OPN and
CLO for every Session on that SecureChannel stalled for good. Nothing
recovered from it: the scheduler logs a send fault rather than faulting the
channel, so the stall was silent. The ticket is now released on every path
out. Releasing twice is safe because the ticket completes under Interlocked.

MaxDataChannelBodySize approximated the chunk budget instead of computing it,
and missed the cipher block rounding WriteSymmetricMessage applies: a 65535
byte send buffer under a 16 byte block loses its last 15 bytes. A body at the
advertised size therefore spilled into a second chunk, which carries the
Intermediate chunk type that the frame codec rejects, after the SequenceNumber
had already been spent. It now mirrors that arithmetic.

The audio sample wrote to a fixed name under the shared temp directory and
truncated whatever was already there, so a local user could pre-create the
path as a symlink and have the sample overwrite the target. It now writes into
a directory created for the run and opens the file with CreateNew.

Both channel fixes are covered by tests proven to fail without them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
The QUIC harnesses bound their listeners to IPAddress.Loopback while their
clients connected by name. "localhost" resolves to ::1 before 127.0.0.1 on
the CI agents, so the handshake was sent where nothing was listening and 26
tests failed. .NET surfaces a QUIC handshake that never completes as
"Application layer protocol negotiation error", which pointed the
investigation at ALPN rather than at the address the listener was bound to
(dotnet/runtime#85412). The listeners now bind IPv6Any, which is dual stack
and is what QuicTransportListener already does, so the harnesses match the
deployment they stand in for.

MismatchedAlpnListenerIsRefused had been passing for the wrong reason: it
asserts that a connection is refused, and a connection that never arrives is
also refused.

Added a test that connects over IPv6 explicitly, because a host that resolves
IPv4 first never exercises this and would let the same regression back in.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
The QUIC tests have skipped themselves on every CI agent for the whole life
of this feature. .NET reaches QUIC through msquic, which the hosted Linux
images do not ship, so QuicListener.IsSupported was false and 61 tests took
the Assert.Ignore path. The transport was effectively untested in CI, and
because the coverage report is merged from the ubuntu legs, all of
Opc.Ua.Bindings.Quic counted as uncovered: 1314 of the 1600 uncovered changed
lines the coverage gate was complaining about.

Measured in a container on the same image family: without libmsquic 260 pass
and 61 skip; with it 321 pass and none skip. The tests needed no changes to
run on Linux, where QUIC mutual TLS is better supported than on Windows
Server 2022 Schannel.

A silent skip is the honest outcome where msquic is genuinely absent, but not
on a machine that is supposed to have it - that is what hid this. The setup
step sets UA_REQUIRE_QUIC, which turns the skip into a failure, so a future
image or package change cannot quietly return the QUIC binding to zero
coverage. Verified both ways: with the variable set and no msquic the suite
reports 61 failures rather than 61 skips.

The eleven copies of the skip check are now one shared helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
Adds tests for paths the changed-lines report showed uncovered, all of them
the defensive and boundary cases rather than the happy path:

- credit accounting ignores a non-positive length, refuses to replenish
  before anything is released, and saturates rather than wrapping a window
  that a long-lived receiver has driven close to the 32-bit ceiling
- encoding into a buffer too small for the frame is refused, rather than
  putting a truncated frame on the wire that the peer would answer by
  resetting a channel that did nothing wrong
- an empty send queue reports no frame instead of handing out a buffer, and
  expiry leaves the queue untouched when nothing has expired
- negotiation refuses to revise without the capabilities every limit is
  derived from, and an absent modify request is not a mutation
- a CREDIT or GAP that overtakes its OpenDataChannel response replays with
  its own fields intact, not just DATA: a CREDIT replayed without its grant
  leaves the sender blocked, and a GAP replayed without its range reports the
  wrong frames discarded

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
@marcschier marcschier changed the title DRAFT Add OPC UA data channels, the opc.quic transport and a message-extension seam DRAFT Add OPC UA data channels and the opc.quic transport Aug 13, 2026
marcschier and others added 19 commits August 13, 2026 08:09
NuGetAudit began failing every restore in the repository, which took the
solution build, CodeQL and both Kafka test legs with it. SSH.NET 2025.1.0
carries a high severity path traversal: ScpClient.Download trusts the file
and directory names the remote server sends during a recursive download, so
a malicious or man-in-the-middle SCP server can escape the download
directory and overwrite arbitrary files the client can write.

SSH.NET arrives transitively through Testcontainers 4.13.0, which asks for
[2025.1.0, ) and therefore resolves the lowest, vulnerable version. The
patched 2026.0.0 satisfies that same range, so a central pin is enough and no
consumer changes; transitive pinning is already enabled repository-wide. The
pin should be removed once Testcontainers itself requires a fixed version.

This is not part of the data channel work. The advisory was published while
the branch was open and breaks master and every other open branch equally,
but it blocks every check here, so it is fixed rather than waited on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 82983b49-5226-4ca1-a225-1029c6f17e3f
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 82983b49-5226-4ca1-a225-1029c6f17e3f
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 82983b49-5226-4ca1-a225-1029c6f17e3f
Preserve FIFO send-ticket ordering while adopting asynchronous closed-transport completion and security policy registry DI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the new AI sample projects alongside the data-channel sample group in UA.slnx.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b8bfdb6-9b78-4b17-a183-2d8a76419ccc
Adopt the refactored MasterNodeManager service dispatcher while retaining data-channel access restriction validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b8bfdb6-9b78-4b17-a183-2d8a76419ccc
Retain the sample-only NAudio package while adopting the updated test, versioning, Testcontainers, and security-pinned dependency versions from master.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b8bfdb6-9b78-4b17-a183-2d8a76419ccc
Integrate state-machine cause executability reporting and the reverse-connect timeout race fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b8bfdb6-9b78-4b17-a183-2d8a76419ccc
Integrate the latest OpenUSD, WoT, certificate, server, and tooling changes while retaining the branch-specific secure-channel transport extensions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b8bfdb6-9b78-4b17-a183-2d8a76419ccc
Integrate typed child NodeId registration, explicit fluent server roots, the federated OpenUSD demo, and corrected maximum publish timeouts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b8bfdb6-9b78-4b17-a183-2d8a76419ccc
Integrate the concern-based ConfigurationNodeManager split and its certificate-alarm and namespace-metadata coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b8bfdb6-9b78-4b17-a183-2d8a76419ccc
Integrate partial endpoint-filter matching for role management and its documentation and coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b8bfdb6-9b78-4b17-a183-2d8a76419ccc
Integrate WoT Binding 1.1 conformance coverage, generated dependency metadata fixes, and corrected GDS certificate-group type advertisement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b8bfdb6-9b78-4b17-a183-2d8a76419ccc
Preserve configured custom transport schemes while failing startup for missing bindings, and integrate fluent node-manager extensibility and effective monitored-item identities.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0b8bfdb6-9b78-4b17-a183-2d8a76419ccc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants