Repository navigation
Send UDP multicast on the network interface the transport joins - #4711
Conversation
UdpDatagramTransport joined multicast groups on the resolved network interface (the connection address's NetworkInterface, the PreferredNetworkInterface option, or the first operational interface), but it never set IP_MULTICAST_IF / IPV6_MULTICAST_IF. Multicast data and discovery announcements therefore left through the operating system's default multicast route. On a computer with several network interfaces, a publisher configured for one network sent on another, and subscribers on the configured network missed the messages (OPC 10000-14 7.3.2.2, NetworkAddressDataType.NetworkInterface). ConfigureSocket now sets the multicast send interface to the resolved interface for IPv4 and IPv6 sockets, including the separate socket that sends discovery announcements for loopback-bound connections. A failure is logged at Debug level, like the other socket options. Behavior change: without a configured interface, multicast now leaves through the first operational non-loopback interface, which is the interface that subscribers join on, instead of the default multicast route. Tests (UdpDatagramTransportMulticastInterfaceTests): - The IPv4 and IPv6 egress options equal the resolved interface. Before the fix they were 0.0.0.0 and 0. - A publisher and subscriber pinned to the loopback interface exchange a payload; a raw control sender distinguishes a host without loopback multicast from a publisher that sends elsewhere. Red on Linux before the fix. Windows also delivers local copies sent on other interfaces, so there the test only confirms the exchange. Validation: Opc.Ua.PubSub.Udp.Tests net10.0 237/237 (Windows and a Linux container), net48 232/232; Opc.Ua.PubSub.Tests net10.0 1490 passed, 1 skipped; Release build of Opc.Ua.PubSub.Udp for all target frameworks with 0 warnings. The one-computer exchange in docs/PubSub.md delivers DataSets with the documented warning once, verified together with the separate sample KeyNonce fix that master currently needs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟢 Approval recommended
The implementation is focused and well tested; only minor IPv6 logging wording remains.
1 open finding
What changed in this PR
Pins UDP multicast egress to the resolved network interface, aligning publishers with subscriber group membership.
Changes:
- Configures IPv4/IPv6 multicast send interfaces.
- Adds multicast interface regression tests.
- Updates API and PubSub documentation.
| File | Description |
|---|---|
UdpDatagramTransportMulticastInterfaceTests.cs |
Tests egress options and loopback delivery. |
UdpTransportOptions.cs |
Documents interface behavior. |
UdpNetworkInterfaceResolver.cs |
Clarifies resolver semantics. |
UdpDatagramTransport.cs |
Configures multicast egress and logging. |
docs/PubSub.md |
Updates multicast guidance. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Code coverage✅ Coverage gate passed.
Uncovered changed lines
Coverage is above the recorded baseline - consider ratcheting Thresholds live in |
Review feedback: - UdpDatagramTransport.SocketForTest is now InnerSocket. - The message logged when setting the multicast send interface fails no longer names IP_MULTICAST_IF, because the same handler reports IPv6 (IPV6_MULTICAST_IF) failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #4711 +/- ##
=======================================
Coverage 82.83% 82.83%
=======================================
Files 2267 2267
Lines 345803 345826 +23
Branches 60271 60275 +4
=======================================
+ Hits 286437 286460 +23
+ Misses 39272 39267 -5
- Partials 20094 20099 +5
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…4712) # Description `MetaDataPublisher` encoded the DataSetMetaData announcement itself and called `IPubSubTransport.SendAsync` directly. That bypassed the connection's security wrapper and its fail-closed check. A WriterGroup configured for `SignAndEncrypt` therefore announced its DataSet layout (field names, types, version) unsigned and unencrypted. A secured subscriber dropped the announcement (`Dropping unsecured inbound frame ... requiring SignAndEncrypt`, documented as an expected warning in docs/PubSub.md), and any listener could read it. OPC 10000-14 §6.2.5.2 applies the WriterGroup's SecurityMode to the NetworkMessages it publishes, and §7.2.4.6.3 carries the security of a discovery announcement in its SecurityHeader. Confirmed on the wire: the documented `udp-uadp` exchange sent secured DataSetMessages plus one DiscoveryResponse (announcement type 2, DataSetMetaData) with ExtendedFlags1 `0x81`, meaning the SecurityHeader bit was clear. Review feedback widened the scope to every UADP discovery message of a secured connection, and to the diagnostics of secured messages. Fix: - `MetaDataPublisher` builds the announcement and hands it to the connection (`PubSubConnection.SendMetaDataAnnouncementAsync`). - One encode-and-secure step (`PubSubConnection.EncodeOutboundAsync`) now serves data messages, discovery requests and responses, and the announcements that UDP and Ethernet send to their transport-specific discovery destination (ApplicationInformation, PublisherEndpoints, PubSubConnection). On a connection configured for message security, it applies the SecurityMode (`Sign`, or `SignAndEncrypt`) to UADP messages and refuses to send any message it cannot secure. - Before, a secured connection dropped its own discovery requests, so a secured subscriber could not ask for metadata, writer configuration, or endpoints. It also refused metadata and writer-configuration probe responses, and sent the transport-specific announcements in plaintext. - A discovery response or announcement that cannot be secured or sent is logged (`DiscoveryMessageNotSent`, Warning) and dropped. Securing announcements makes them depend on a current key; without this, a key provider with no key yet, for example before a Security Key Service delivers the first key, made `StartAsync` fail, and a probe response that could not be secured ended the receive loop. Requests sent through `RequestDiscoveryAsync` still throw to the caller. - Secured messages are counted in `SentNetworkMessages` and `SentDataSetMessages` like unsecured ones. Before, they bypassed the encoder that counts them, so secured data messages were never counted. - `UadpDiscoveryCoder` can encode a discovery message with the SecurityHeader bit set, and reports the security boundary after the PublisherId (and DataSetClassId), which is where `UadpDecoder.TryReadOuterPrefix` ends the prefix. `UadpEncoder.EncodeWithSecurityBoundary` accepts discovery messages. - The receive path is unchanged: it already unwraps secured discovery frames and registers authenticated metadata. - Not changed: the MQTT last-will status message is registered with the broker when the connection opens and published later, so it cannot carry a current SecurityHeader; it stays unsecured. - docs/PubSub.md no longer lists the unsecured-frame warning. It describes how the SecurityMode applies to announcements and discovery (`Sign` authenticates; `SignAndEncrypt` also encrypts) and the last-will exception. Tests: - `MetaDataPublisherTests.UadpPathSecuresTheAnnouncementOfASecuredWriterGroupAsync`: the announcement of a SignAndEncrypt WriterGroup carries a SecurityHeader, does not contain the DataSet name in cleartext, and unwraps and decodes with the subscriber's keys. Before the fix, the announcement had no SecurityHeader. - `MetaDataPublisherTests.SecuredNetworkMessagesAreCountedAsSentAsync`: before the fix, `SentNetworkMessages` read 0 on a secured connection. - `PubSubDiscoveryTests.SecuredSubscriberDiscoversSecuredPublisherWithoutPlaintextFramesAsync`: a secured subscriber and a secured publisher, connected through an in-memory transport that also sends announcements to a separate destination, discover metadata, writer configuration, and endpoints, and no frame lacks a SecurityHeader. Before the fix, the metadata request was dropped. With only the announcement change reverted, the PublisherEndpoints response was dropped. - `PubSubDiscoveryTests.StartupAnnouncementsWithoutACurrentKeyDoNotFailTheStartAsync` and `ReceiveLoopSurvivesADiscoveryResponseThatCannotBeSecuredAsync`: both failed before the fail-soft change. - `UadpDiscoveryTests.SecuredDiscoveryMessagesSplitWhereTheReceiverExpects`: for requests and responses, with and without a DataSetClassId, the secured encoding differs from the unsecured one only by the SecurityHeader flag, and its boundary equals the receiver's prefix length. - `UadpDiscoveryTests.EncodeWithSecurityBoundaryRejectsAMessageItCannotSecure`. ### Validation | Command | Result | | --- | --- | | `dotnet test tests/Opc.Ua.PubSub.Tests -f net10.0 -p:CustomTestTarget=net10.0` | 1498 passed, 1 skipped (the UDP multicast loopback discovery test, which this host skips) | | `dotnet test tests/Opc.Ua.PubSub.Tests -f net48 -p:CustomTestTarget=net48` | 1498 passed, 1 skipped | | `dotnet test` on net10.0 for `Opc.Ua.PubSub.Udp.Tests`, `.Eth.Tests`, `.Mqtt.Tests`, `.Server.Tests`, `fuzzing/Opc.Ua.PubSub.Fuzz.Tests` | 234, 55, 157, 177, 149 passed | | `dotnet test` on net10.0 for `Opc.Ua.PubSub.Kafka.Tests`, `.Adapter.Tests`, `.Diagnostics.Tests`, `Opc.Ua.Redundancy.PubSub.Tests` (on 9417ccb) | 92 (1 skipped), 152, 53 (1 skipped), 45 passed | | `tests\Opc.Ua.Aot.Tests\bin\Debug\net10.0\Opc.Ua.Aot.Tests.exe --treenode-filter "/*/*/PubSubAotTests/*"` (JIT; the AOT job failed on 94d4391) | 7 of 7 passed, including `StartsAndStopsPublisher_UdpUadp` | | `dotnet build src/Opc.Ua.PubSub -c Release` (all target frameworks) | 0 warnings | | The docs/PubSub.md first exchange, run verbatim, with #4710 applied (before the review changes) | The announcement is secured on the wire (ExtendedFlags1 `0x91`, 268 bytes); 14 DataSets; no unsecured-frame warning; the subscriber logs `Registered inbound metadata for writer 1` | | After merging master (#4710, #4711, #4716), on 83567ce: `dotnet build src/Opc.Ua.PubSub -c Release` | 0 warnings | | Same merge: `dotnet test tests/Opc.Ua.PubSub.Tests -f net10.0`; the changed fixtures on net48; `dotnet test tests/Opc.Ua.PubSub.Udp.Tests -f net10.0` | 1498 passed, 1 skipped; 132 passed, 1 skipped; 237 passed | | Same merge: `Opc.Ua.Aot.Tests.exe --treenode-filter "/*/*/PubSubAotTests/*"` (JIT) | 8 of 8 passed, including #4710's `SamplePublisherDeliversSecuredDataSetToSampleSubscriberAsync` | | Same merge: the docs/PubSub.md first exchange, run verbatim on `opc.udp://127.0.0.1:4840`, then the publisher alone against a UDP listener | 9 DataSets in 10 seconds and no warning on either side; the listener saw 9 DataSetMessages and 1 DiscoveryResponse (the startup announcement), all with the SecurityHeader bit set | ## Related Issues - No tracking issue. Found while verifying the PubSub first-exchange documentation (follow-up to #4555). ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] 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. - [x] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Description
UdpDatagramTransportjoined multicast groups on the resolved network interface: the connection address'sNetworkInterface, thePreferredNetworkInterfaceoption, or the first operational interface. It never setIP_MULTICAST_IF/IPV6_MULTICAST_IF, though, so multicast data and discovery announcements left through the operating system's default multicast route. On a computer with several network interfaces, a publisher configured for one network sent on another, and subscribers on the configured network missed the messages (OPC 10000-14 §7.3.2.2,NetworkAddressDataType.NetworkInterface).ConfigureSocketnow sets the multicast send interface to the resolved interface for IPv4 and IPv6 sockets. That includes the separate socket that sends discovery announcements for loopback-bound connections. A failure is logged at Debug level, like the other socket options.Behavior change: without a configured interface, multicast now leaves through the first operational non-loopback interface, which is the interface subscribers join on, instead of the default multicast route.
docs/PubSub.mdand the XML docs ofPreferredNetworkInterfaceandUdpNetworkInterfaceResolverdescribe this and recommend naming the interface on multi-homed computers.Tests (
UdpDatagramTransportMulticastInterfaceTests):0.0.0.0and0.Validation
dotnet test tests/Opc.Ua.PubSub.Udp.Tests -f net10.0 -p:CustomTestTarget=net10.0 --filter "FullyQualifiedName~UdpDatagramTransportMulticastInterfaceTests", with the fix temporarily disabledExpected: <192.168.178.101> But was: <0.0.0.0>,Expected: 4 But was: 0)mcr.microsoft.com/dotnet/sdk:10.0,dotnet test Opc.Ua.PubSub.Udp.Tests.dll), fix disabled, 5 runsdotnet test tests/Opc.Ua.PubSub.Udp.Tests -f net10.0 -p:CustomTestTarget=net10.0dotnet test tests/Opc.Ua.PubSub.Udp.Tests -f net48 -p:CustomTestTarget=net48dotnet test tests/Opc.Ua.PubSub.Tests -f net10.0 -p:CustomTestTarget=net10.0dotnet build src/Opc.Ua.PubSub.Udp -c Release(net10.0, net9.0, net8.0, net48)Related Issues
Checklist