Special-purpose servers
Three services don't fit the "classic OPC UA server with the test address space" mold: the discovery server, the Security Key Service, and the PubSub publisher. Each is the only target for its specific tests.
These don't expose the standard test address space. Each one exists for a specific protocol feature you'd otherwise have to mock.
Discovery server (opcua-discovery, port 4844)
endpoint: opc.tcp://localhost:4844
(note: no resource path)
policy: None, Basic256Sha256
mode: None, SignAndEncrypt
auth: Anonymous
A dedicated OPC UA Discovery Server. Not a regular server —
it does not expose TestServer and has no application
address space.
What it serves
| Service | Use case |
|---|---|
FindServers |
List servers that have registered with discovery |
GetEndpoints |
Get endpoints of this discovery server itself |
How tests use it
None of the classic test servers in docker-compose.yml set
OPCUA_DISCOVERY_URL, and TestServerApp does not call
RegisterServer / RegisterServer2 against any discovery
endpoint. So the discovery server does not maintain a
registry of the other suite services — FindServers() returns
only what the discovery server has registered for itself
(typically a single entry describing the discovery endpoint).
Treat opcua-discovery as a target for exercising the
FindServers and GetEndpoints calls themselves (wire shape,
error handling, security policy selection) — not as a working
registry for the rest of the suite.
A discovery test typically:
- Connects to
opc.tcp://localhost:4844anonymously. - Calls
GetEndpoints("opc.tcp://localhost:4844"). - Asserts the response carries
None/NoneandBasic256Sha256/SignAndEncryptendpoints. - Calls
FindServers()with no filter — asserts the response shape and the presence of the discovery server's own entry.
Note the lack of resource path — discovery endpoints are
served at the bare URL, no /UA/TestServer suffix.
Security Key Service (opcua-sks, port 4851)
endpoint: opc.tcp://localhost:4851/UA/TestServer
policy: None
mode: None
auth: Anonymous
extra: OPCUA_ENABLE_SKS=true
A classic OPC UA server with one extra feature: the
GetSecurityKeys method from OPC UA Part 14 §8.4.2. Used by
PubSub subscribers to obtain group keys.
The method
NodeIds:
- Object:
ns=1;s=TestServer/SecurityKeyService - Method:
ns=1;s=TestServer/SecurityKeyService/GetSecurityKeys
Signature:
GetSecurityKeys(
securityGroupId: String,
startingTokenId: UInt32,
requestedKeyCount: UInt32,
) returns (
securityPolicyUri: String,
firstTokenId: UInt32,
keys: ByteString[],
timeToNextKey: Duration,
keyLifetime: Duration,
)
Default key layout
For policy PubSub-Aes256-CTR, each keys[i] ByteString is 68
bytes:
| Bytes | Contents | Default |
|---|---|---|
| 0-31 | HMAC-SHA256 signing key | 32 × 0x01 |
| 32-63 | AES-256 encrypting key | 32 × 0x02 |
| 64-67 | 4-byte key nonce | 0x03 0x03 0x03 0x03 |
All values overridable via OPCUA_SKS_* environment variables —
see Security Key Service.
Limits — test-only
- Single hardcoded group (
OPCUA_SKS_GROUP_ID, defaulttest-group). - Unknown
securityGroupIdreturnsBadNotFound. - No caller authentication — anyone reachable on 4851 can fetch keys.
- No rotation scheduling — keys are static for the lifetime of the process.
Real-world SKS deployments do all of the above. This service exists to give subscriber-side code a real round-trip target, not to be a reference SKS.
PubSub publisher (opcua-pubsub, UDP 14850 host)
endpoint: opc.udp://127.0.0.1:14850
codebase: src/TestPublisher/ (separate)
transport: UADP over UDP
mode: None (unsecured)
A UA-.NETStandard PubSub publisher. Emits a deterministic
DataSet every 500 ms over UDP UADP.
DataSet shape
| Field | Type | Value |
|---|---|---|
counter |
UInt32 | Monotonic counter. Internal _counter initial is 0 but it is incremented before the first publish, so the first wire value is 1. |
timestamp |
DateTime | UTC publish time |
value |
Double | sin(counter × π / 20) |
Headers
| Header | Default |
|---|---|
PublisherId |
100 |
WriterGroupId |
1 |
DataSetWriterId |
1 |
| Publish interval | 500 ms |
All defaults overridable via OPCUA_* env vars — see
PubSub publisher.
Why two services?
PubSub natively uses multicast — which doesn't traverse the
Docker Desktop VM boundary reliably across platforms. The
publisher sends unicast to a socat relay on a shared
compose bridge; the relay re-emits each packet to
host.docker.internal:14850, which Docker Desktop and Docker
Engine both bridge to the physical host.
opcua-pubsub ── pubsub-net ──► opcua-pubsub-relay ── host.docker.internal:14850 ──► subscriber
Subscribers on the host listen on 127.0.0.1:14850 and don't
know the relay exists.
How tests use it
A subscriber-side test:
- Opens a UDP socket on
127.0.0.1:14850. - Receives UADP NetworkMessages.
- Decodes them per
OPCFoundation.NetStandard.Opc.Ua.PubSubwire format. - Asserts the
counterincrements, thevalueis a sine wave, the timestamps are monotonic.
The published headers (PublisherId, WriterGroupId,
DataSetWriterId) are also part of the assertion surface — they
exercise the subscriber's demux logic.
Where to read next
- Security Key Service — the SKS in detail.
- PubSub publisher — the publisher in detail.