Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<ProjectReference Include="..\..\Libraries\Opc.Ua.Configuration\Opc.Ua.Configuration.csproj" />
<ProjectReference Include="..\..\Libraries\Opc.Ua.PubSub\Opc.Ua.PubSub.csproj" />
<ProjectReference Include="..\..\Libraries\Opc.Ua.PubSub.Udp\Opc.Ua.PubSub.Udp.csproj" />
<ProjectReference Include="..\..\Libraries\Opc.Ua.PubSub.Eth\Opc.Ua.PubSub.Eth.csproj" />
<ProjectReference Include="..\..\Libraries\Opc.Ua.PubSub.Mqtt\Opc.Ua.PubSub.Mqtt.csproj" />
<ProjectReference Include="..\..\Libraries\Opc.Ua.PubSub.Adapter\Opc.Ua.PubSub.Adapter.csproj" />
</ItemGroup>
Expand Down
32 changes: 28 additions & 4 deletions Applications/ConsoleReferencePubSubClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,11 @@ private static async Task<int> RunPublisherAsync(
.AddUdpTransport()
.AddSecurityKeyProvider(SampleSecurity.CreateKeyProvider())
.AddDataSetSource(PublisherConfigurationBuilder.DataSetName, sampleSource);
if (profile != PublisherProfile.UdpUadp)
if (profile == PublisherProfile.EthUadp)
{
publisher.AddEthTransport();
}
else if (profile != PublisherProfile.UdpUadp)
{
publisher.AddMqttTransport();
}
Expand Down Expand Up @@ -434,7 +438,11 @@ private static async Task<int> RunSubscriberAsync(
sp => new ConsoleLoggingSink(
sp.GetRequiredService<ILoggerFactory>()
.CreateLogger<ConsoleLoggingSink>()));
if (profile != SubscriberProfile.UdpUadp)
if (profile == SubscriberProfile.EthUadp)
{
subscriber.AddEthTransport();
}
else if (profile != SubscriberProfile.UdpUadp)
{
subscriber.AddMqttTransport();
}
Expand Down Expand Up @@ -705,6 +713,9 @@ private static bool TryParsePublisherProfile(string? text, out PublisherProfile
case "mqtt-json":
profile = PublisherProfile.MqttJson;
return true;
case "eth-uadp":
profile = PublisherProfile.EthUadp;
return true;
default:
profile = PublisherProfile.UdpUadp;
return false;
Expand All @@ -724,6 +735,9 @@ private static bool TryParseSubscriberProfile(string? text, out SubscriberProfil
case "mqtt-json":
profile = SubscriberProfile.MqttJson;
return true;
case "eth-uadp":
profile = SubscriberProfile.EthUadp;
return true;
default:
profile = SubscriberProfile.UdpUadp;
return false;
Expand Down Expand Up @@ -821,7 +835,12 @@ public enum PublisherProfile
/// <summary>
/// MQTT broker transport with JSON message mapping.
/// </summary>
MqttJson = 2
MqttJson = 2,

/// <summary>
/// Ethernet (Layer 2) transport with UADP message mapping.
/// </summary>
EthUadp = 3
}

/// <summary>
Expand All @@ -842,7 +861,12 @@ public enum SubscriberProfile
/// <summary>
/// MQTT broker transport with JSON message mapping.
/// </summary>
MqttJson = 2
MqttJson = 2,

/// <summary>
/// Ethernet (Layer 2) transport with UADP message mapping.
/// </summary>
EthUadp = 3
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
using System;
using Opc.Ua;
using Opc.Ua.PubSub.Configuration;
using Opc.Ua.PubSub.Eth;

namespace Quickstarts.ConsoleReferencePubSubClient
{
Expand All @@ -44,14 +45,18 @@ public static class PublisherConfigurationBuilder
{
public const string DataSetName = "Simple";
public const string DefaultUdpEndpoint = "opc.udp://239.0.0.1:4840";
public const string DefaultEthEndpoint = "opc.eth://01-00-5E-7F-00-01";
public const string DefaultMqttEndpoint = "mqtt://localhost:1883";
private const string MqttQueueName = "Quickstarts/Reference/Simple";

public static string DefaultEndpointFor(PublisherProfile profile)
{
return profile == PublisherProfile.UdpUadp
? DefaultUdpEndpoint
: DefaultMqttEndpoint;
return profile switch
{
PublisherProfile.UdpUadp => DefaultUdpEndpoint,
PublisherProfile.EthUadp => DefaultEthEndpoint,
_ => DefaultMqttEndpoint
};
}

public static PubSubConfigurationDataType Build(
Expand All @@ -62,7 +67,9 @@ public static PubSubConfigurationDataType Build(
ushort dataSetWriterId,
int intervalMs)
{
bool udp = profile == PublisherProfile.UdpUadp;
// UDP and Ethernet are datagram transports (no broker queue);
// the MQTT profiles use broker transport settings instead.
bool udp = profile is PublisherProfile.UdpUadp or PublisherProfile.EthUadp;

// UADP message security (SignAndEncrypt) is wired for the UADP
// profiles via the shared StaticSecurityKeyProvider. The JSON
Expand All @@ -72,6 +79,7 @@ public static PubSubConfigurationDataType Build(
string transportProfileUri = profile switch
{
PublisherProfile.UdpUadp => Profiles.PubSubUdpUadpTransport,
PublisherProfile.EthUadp => EthProfiles.PubSubEthUadpTransport,
PublisherProfile.MqttUadp => Profiles.PubSubMqttUadpTransport,
PublisherProfile.MqttJson => Profiles.PubSubMqttJsonTransport,
_ => throw new ArgumentOutOfRangeException(nameof(profile))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
using System;
using Opc.Ua;
using Opc.Ua.PubSub.Configuration;
using Opc.Ua.PubSub.Eth;

namespace Quickstarts.ConsoleReferencePubSubClient
{
Expand All @@ -45,14 +46,18 @@ public static class SubscriberConfigurationBuilder
public const string ReaderName = "Reader 1";
public const string DataSetName = "Simple";
public const string DefaultUdpEndpoint = "opc.udp://239.0.0.1:4840";
public const string DefaultEthEndpoint = "opc.eth://01-00-5E-7F-00-01";
public const string DefaultMqttEndpoint = "mqtt://localhost:1883";
private const string MqttQueueName = "Quickstarts/Reference/Simple";

public static string DefaultEndpointFor(SubscriberProfile profile)
{
return profile == SubscriberProfile.UdpUadp
? DefaultUdpEndpoint
: DefaultMqttEndpoint;
return profile switch
{
SubscriberProfile.UdpUadp => DefaultUdpEndpoint,
SubscriberProfile.EthUadp => DefaultEthEndpoint,
_ => DefaultMqttEndpoint
};
}

public static PubSubConfigurationDataType Build(
Expand All @@ -62,7 +67,9 @@ public static PubSubConfigurationDataType Build(
ushort writerGroupIdFilter,
ushort dataSetWriterIdFilter)
{
bool udp = profile == SubscriberProfile.UdpUadp;
// UDP and Ethernet are datagram transports (no broker queue);
// the MQTT profiles use broker transport settings instead.
bool udp = profile is SubscriberProfile.UdpUadp or SubscriberProfile.EthUadp;

// UADP message security (SignAndEncrypt) is wired for the UADP
// profiles via the shared StaticSecurityKeyProvider. The JSON
Expand All @@ -72,6 +79,7 @@ public static PubSubConfigurationDataType Build(
string transportProfileUri = profile switch
{
SubscriberProfile.UdpUadp => Profiles.PubSubUdpUadpTransport,
SubscriberProfile.EthUadp => EthProfiles.PubSubEthUadpTransport,
SubscriberProfile.MqttUadp => Profiles.PubSubMqttUadpTransport,
SubscriberProfile.MqttJson => Profiles.PubSubMqttJsonTransport,
_ => throw new ArgumentOutOfRangeException(nameof(profile))
Expand Down
6 changes: 4 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!--
PacketDotNet and SharpPcap are native-code dependencies of
Opc.Ua.Core.Diagnostics. They dynamically load libpcap on
Linux/macOS and Npcap on Windows. Per SDL native-code policy:
Opc.Ua.Core.Diagnostics, Opc.Ua.PubSub.Diagnostics and (for the
opt-in WithPcap() backend, net8.0+ only) Opc.Ua.PubSub.Eth. They
dynamically load libpcap on Linux/macOS and Npcap on Windows. Per
SDL native-code policy:
- Versions are pinned (no floating range)
- SBOM tracking is required for every release
- Subscribe to upstream CVE feeds:
Expand Down
1 change: 1 addition & 0 deletions Docs/Profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ machinery and conformance unit semantics are defined by
[Part 7 §4.3](https://reference.opcfoundation.org/specs/OPC-10000-7/v1.05.06/4.3).

- **[PubSub UDP UADP](http://opcfoundation.org/UA-Profile/Transport/pubsub-udp-uadp)** — UDP transport with UADP message encoding.
- **[PubSub Ethernet UADP](http://opcfoundation.org/UA-Profile/Transport/pubsub-eth-uadp)** — Ethernet (Layer 2) transport with UADP message encoding (see [PubSub transports](PubSub.md#transports)).
- **[PubSub MQTT UADP](http://opcfoundation.org/UA-Profile/Transport/pubsub-mqtt-uadp)** — MQTT transport with UADP message encoding.
- **[PubSub MQTT JSON](http://opcfoundation.org/UA-Profile/Transport/pubsub-mqtt-json)** — MQTT transport with JSON message encoding.
- **Datagram-v2 connection profile** —
Expand Down
67 changes: 66 additions & 1 deletion Docs/PubSub.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
- Multi-TFM: `netstandard2.1`, `net48`, `net472`, `net8.0` (LTS), `net9.0`, `net10.0` (LTS).
- Native AOT clean — both reference samples publish with zero
`IL2026` / `IL3050` warnings.
- Transports: **UDP** (uni/multi/broadcast), **DTLS over UDP** (`opc.dtls://`, unicast UADP), and **MQTT** (3.1.1 + 5.0).
- Transports: **UDP** (uni/multi/broadcast), **DTLS over UDP** (`opc.dtls://`, unicast UADP), **MQTT** (3.1.1 + 5.0), and **Ethernet** (`opc.eth://`, Layer 2 UADP with 802.1Q VLAN).
- Encodings: **UADP** ([§7.2.4](https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/7.2.4))
and **JSON** ([§7.2.5](https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/7.2.5))
with `Verbose` / `Compact` / `RawData` modes.
Expand Down Expand Up @@ -397,6 +397,9 @@ only makes sense together with the PubSub feature:
unicast / multicast / broadcast.
- `IPubSubBuilder.AddMqttTransport(Action<MqttConnectionOptions>?)` —
MQTT 3.1.1 + 5.0 via MQTTnet.
- `IPubSubBuilder.AddEthTransport(Action<EthTransportOptions>?)` —
Ethernet Layer 2 (`opc.eth://`); chain `.WithPcap()` for the
SharpPcap (libpcap / Npcap) backend.

Server-side address space — see
[Server-side address space](#server-side-address-space):
Expand Down Expand Up @@ -425,6 +428,68 @@ broadcast. The transport honours the
| `MessageRepeatDelay` | Delay between repeats; receivers deduplicate using `SequenceNumber`. |


### Ethernet / UADP (`opc.eth://`)

Implemented in `Opc.Ua.PubSub.Eth`. Wire profile
[`PubSub Ethernet UADP`](http://opcfoundation.org/UA-Profile/Transport/pubsub-eth-uadp)
(`Opc.Ua.PubSub.Eth.EthProfiles.PubSubEthUadpTransport`). OPC UA PubSub NetworkMessages are carried directly inside raw Ethernet II frames (no IP, no UDP), identified by the OPC Foundation EtherType `0xB62C`, with optional IEEE 802.1Q VLAN tagging. The existing UADP message encoding and message-level PubSub security are reused — only the transport binding is new.

**Addressing.** The connection address is a `NetworkAddressUrlDataType.Url` of the form `opc.eth://<mac>[?vid=<0-4095>&pcp=<0-7>]`. The destination MAC accepts the hyphen form (`01-00-5E-7F-00-01`), the colon form (`01:00:5E:7F:00:01`), and the bare twelve hexadecimal digit form (`01005E7F0001`); the legacy `opc.eth://<mac>:<vid>.<pcp>` suffix is also accepted. The MAC is classified as unicast, multicast (I/G bit set), or broadcast (all ones); multicast / broadcast addresses cause the receive backend to join the corresponding group.

```
opc.eth://01-00-5E-7F-00-01 # multicast, untagged
opc.eth://01-00-5E-7F-00-01?vid=5&pcp=6 # multicast, VLAN 5, priority 6
opc.eth://FF-FF-FF-FF-FF-FF # broadcast
opc.eth://00-11-22-33-44-55 # unicast
```

**Frame backends (provider model).** Raw Layer-2 frame I/O is platform-specific and privileged — there is no cross-platform BCL raw-Ethernet support. The transport never touches a socket directly; it resolves the backend through an injectable `IEthernetFrameChannelFactory` and owns the Ethernet / VLAN framing itself.

| Backend | Platforms | Notes |
| ------- | --------- | ----- |
| Native (default) | Linux (`AF_PACKET`), macOS (BPF) | libc P/Invoke, no managed dependency, NativeAOT-compatible. Requires `CAP_NET_RAW` / root (Linux) or BPF device access (macOS). |
| SharpPcap (`WithPcap()`) | Linux, macOS, **Windows** | libpcap / Npcap via SharpPcap. Opt-in; requires the native capture library installed. |
| In-memory loopback | any | Deterministic, privilege-free; used by the tests and for local diagnostics. |

On Windows the default native factory throws `PlatformNotSupportedException`; register the SharpPcap backend with `WithPcap()` or inject a custom `IEthernetFrameChannelFactory`. The native and in-memory backends are NativeAOT / trim clean; the SharpPcap backend lives in the same package with its SharpPcap-touching members isolated via `[UnconditionalSuppressMessage]` (compiled `net8.0+` because PacketDotNet has no `netstandard` asset), and an AOT smoke test in `Opc.Ua.Aot.Tests` verifies it runs under NativeAOT.

```csharp
services.AddOpcUa().AddPubSub(pubsub => pubsub
.AddPublisher()
.AddEthTransport(options =>
{
options.PreferredNetworkInterface = "eth0";
options.DefaultVlanId = 5;
options.DefaultPriority = 6;
}));

// SharpPcap backend (for example Windows with Npcap installed):
services.AddOpcUa().AddPubSub(pubsub => pubsub
.AddSubscriber()
.AddEthTransport()
.WithPcap());
```

`AddEthTransport` also accepts an `IConfiguration` / `IConfigurationSection` (default section `OpcUa:PubSub:Eth`). `EthTransportOptions`:

| Option | Default | Meaning |
| ------ | ------- | ------- |
| `ReceiveQueueCapacity` | 1024 | Bounded receive queue depth (frames). |
| `MaxFrameSize` | 1522 | Maximum accepted frame size (standard Ethernet + 802.1Q tag); raise for jumbo frames. |
| `PreferredNetworkInterface` | `null` | NIC name fallback when the address does not name an interface. |
| `DefaultVlanId` | `null` | VLAN id applied when the address URL omits one. |
| `DefaultPriority` | `null` | 802.1Q priority applied when the address URL omits one. |
| `Promiscuous` | `false` | Promiscuous receive (multicast is received via group membership without it). |
| `DiscoveryAnnounceRate` | 0 | Cyclic discovery announcement rate (ms); 0 disables. |
| `DiscoveryMulticastAddress` | `null` | Destination MAC for discovery announcements; defaults to the data destination MAC. |

`EthernetDatagramTransport` implements `IPubSubDiscoveryAnnouncementTransport`: when `DiscoveryAnnounceRate` is non-zero, announcements are sent to `DiscoveryMulticastAddress` (or the data destination MAC when unset).

Notes: only UADP encoding is defined for the Ethernet mapping (no JSON over `opc.eth://`); frames exceeding `MaxFrameSize` (the link MTU) cannot be sent, so enable UADP chunking or raise the MTU; the native AF_PACKET / BPF backends are exercised by opt-in / manual tests only (they need privileges and real hardware), while CI uses the in-memory loopback backend.

**Security.** The Ethernet mapping provides **no transport-level authentication, integrity, or confidentiality** (unlike `opc.dtls://`): raw Layer 2 frames are unauthenticated and unencrypted, and any node on the broadcast / VLAN domain can sniff, inject, replay, or spoof them. Always configure **message-level PubSub security** (`SecurityMode = SignAndEncrypt` with a SecurityGroup / SKS), exactly as for UDP — the transport applies the same inbound security gate. The transport logs a prominent **warning** when a connection is opened with `SecurityMode = None`. Run the process with the **least privilege** required for raw L2 access — on Linux grant the `CAP_NET_RAW` capability to the binary (`setcap cap_net_raw+ep`) rather than running as root; `Promiscuous` mode is off by default and broadens the receive exposure when enabled. The in-memory loopback backend delivers every frame to all peers on its bus (no destination filtering) and is a test / diagnostic double only — it must not be relied on as a security or isolation boundary. SharpPcap (+ PacketDotNet) are pinned native dependencies tracked under the repository's SDL native-code policy (see `Directory.Packages.props`).


### DTLS / UADP (`opc.dtls://`)

`Opc.Ua.PubSub.Udp` also implements the Part 14 §7.3.2.4 DTLS transport for
Expand Down
3 changes: 2 additions & 1 deletion Docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ Here is a list of available documentation for different topics:
* [AuthorizationService](AuthorizationService.md) - Modern Part 12 `StartRequestToken` / `FinishRequestToken`, `ITokenIssuer`, and GDS token issuance.
* [Fuzz testing](../Fuzzing/Fuzzing.md) - SharpFuzz + afl-fuzz + libFuzzer integration. Three areas: `Encoders` (Binary/JSON/XML decoders, built-in type readers, parser entry points), `Certificates` (`X509CRL`, X509 extension parsers, `PEMReader`, `Pkcs10CertificationRequest`, ASN.1 helpers), and `Network` (UA-SC framing via `Opc.Ua.Core.Diagnostics` + internal `TcpMessageParsers` seam on `Opc.Ua.Core`). The [`fuzz-tester`](../.github/agents/fuzz-tester.agent.md) custom agent drives the whole toolchain autonomously: it detects OS-available engines, runs them in parallel, fixes novel findings per repo guidelines, adds the failing input as a regression asset, and pushes one commit per fix until the user says stop.
* [KeyCredentialService](KeyCredentialService.md) - Pull, Push, and experimental bridge guidance for Part 12 KeyCredential flows.
* [PubSub (Part 14)](PubSub.md) - Publisher/subscriber support library: architecture, fluent builder, transports (UDP / MQTT 3.1.1 + 5.0), encodings (UADP / JSON), security, and server-side address space.
* [PubSub (Part 14)](PubSub.md) - Publisher/subscriber support library: architecture, fluent builder, transports (UDP / MQTT 3.1.1 + 5.0 / Ethernet Layer 2), encodings (UADP / JSON), security, and server-side address space.
* [Migration sub-doc](migrate/2.0.x/pubsub.md) - 1.5.378 → 2.0 breaking API, transport, JSON, and field-encoding changes, plus the compatibility matrix.
* [Ethernet transport](PubSub.md#transports) - Layer 2 PubSub (`opc.eth://`, EtherType `0xB62C`, 802.1Q VLAN) with native AF_PACKET / BPF, SharpPcap, and in-memory backends.
* [External server adapter](PubSub.md#binding-pubsub-to-an-external-opc-ua-server-client-session-adapters) - Bind PubSub publishers, subscribers, and Action responders to an external OPC UA server through `ManagedSession`.
* [Dependency Injection extensions](DependencyInjection.md) - `AddPubSub`, `AddPubSubPublisher`, `AddPubSubSubscriber`, `AddPubSubSecurityKeyServiceClient/Server`, `AddPubSubAddressSpace`.
* [Profiles](Profiles.md#pubsub-transports) - Datagram-v2, SKS pull / push, AES-128/256-CTR security facets.
Expand Down
Loading
Loading