Client-side listener for OPC UA Reverse Connect (Part 6 §7.1.2.3). Extension of php-opcua/opcua-client. The server dials out, the client accepts; everything after the opening ReverseHello frame is the standard UA-TCP protocol.
Reverse Connect inverts who initiates the underlying TCP connection. It is the pattern of choice when the server is behind NAT or a one-way firewall: the device dials outward, your PHP application listens for the incoming socket, validates the announced ServerUri, then proceeds with the normal UA-TCP handshake — secure channel, session, service calls — exactly as if you had connected the regular way.
What you can do with it:
- Bind a listener on any host/port and accept inbound
RHEframes from one or many announcing servers - Validate each frame against an explicit whitelist of trusted
ServerUrivalues before letting the UA-TCP pipeline touch the socket - Hand the live socket to the standard
ClientBuildervia the v4.4.0TcpTransport::fromConnectedSocket()seam, then use the resultingClientlike any other - Observe the flow with three PSR-14 events —
ReverseHelloReceived,ReverseConnectAccepted,ReverseConnectRejected— and a PSR-3 logger - Cover failure modes explicitly with four typed exceptions (parse, validation, timeout, plumbing) rooted in a single base class
Pure PHP, no native extensions, no event loop dependency.
Note: This package only ships the listener-side machinery. The OPC UA server you talk to must support Reverse Connect on its end and be told (via configuration, MQTT, an HTTPS callback — whatever fits your topology) to dial back to the listener's host and port. For integration testing the
uanetstandard-test-suitev1.4.0+ exposes two Method nodes —StartReverseConnectandStopReverseConnect— that let a regular client trigger the outbound dial.
|
The only seam in the core package is the |
composer require php-opcua/opcua-client-ext-reverse-connectuse PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\ExtReverseConnect\ReverseConnectClientFactory;
use PhpOpcua\Client\ExtReverseConnect\ReverseConnectListener;
use PhpOpcua\Client\ExtReverseConnect\ReverseHelloValidator;
use PhpOpcua\Client\Security\SecurityMode;
use PhpOpcua\Client\Security\SecurityPolicy;
$listener = new ReverseConnectListener(
bindHost: '0.0.0.0',
bindPort: 4841,
validator: new ReverseHelloValidator(['urn:my-edge-gateway:server']),
);
$listener->listen();
$session = $listener->accept(timeoutSeconds: 30.0);
$client = (new ReverseConnectClientFactory())->buildClient(
$session,
static fn (ClientBuilder $b) => $b
->setSecurityPolicy(SecurityPolicy::None)
->setSecurityMode(SecurityMode::None),
);
$value = $client->read('ns=2;s=Demo.Counter');
echo $value->getValue() . PHP_EOL;
$client->disconnect();
$listener->close();That's it. Bind, accept, build, use, disconnect — five calls and you are reading values through a socket the server opened.
use PhpOpcua\Client\ExtReverseConnect\ReverseHelloValidator;
$validator = new ReverseHelloValidator([
'urn:gateway:plc-42',
'urn:gateway:plc-43',
]);
// The listener uses ensureAccepted() internally; you can call it
// yourself on captured frames in tests or audit pipelines.
$validator->ensureAccepted($message); // throws ReverseConnectRejectedException
$validator->isAccepted($message); // bool — non-throwing variantAn empty whitelist refuses every incoming frame — the validator is fail-secure by default.
use PhpOpcua\Client\ExtReverseConnect\Event\ReverseConnectAccepted;
use PhpOpcua\Client\ExtReverseConnect\Event\ReverseConnectRejected;
use PhpOpcua\Client\ExtReverseConnect\Event\ReverseHelloReceived;
$listener = new ReverseConnectListener(
bindHost: '0.0.0.0',
bindPort: 4841,
validator: $validator,
dispatcher: $yourPsr14Dispatcher,
);
// Listeners observe parseable RHEs, accepted sessions, and rejections.
class AuditHandler {
public function onReceived(ReverseHelloReceived $event): void { /* ... */ }
public function onAccepted(ReverseConnectAccepted $event): void { /* ... */ }
public function onRejected(ReverseConnectRejected $event): void {
Log::warning("Rejected {$event->message->serverUri}: {$event->reason}");
}
}Zero overhead when no dispatcher is provided — events are not constructed at all.
use PhpOpcua\Client\Types\BuiltinType;
use PhpOpcua\Client\Types\NodeId;
use PhpOpcua\Client\Types\Variant;
$trigger = (new ClientBuilder())
->setSecurityPolicy(SecurityPolicy::None)
->setSecurityMode(SecurityMode::None)
->connect('opc.tcp://localhost:4840/UA/TestServer');
$trigger->call(
NodeId::string(2, 'TestServer/ReverseConnect'),
NodeId::string(2, 'TestServer/ReverseConnect/StartReverseConnect'),
[
new Variant(BuiltinType::String, 'host.docker.internal'),
new Variant(BuiltinType::UInt16, $listenerPort),
],
);In production the trigger is whatever you already have — HTTPS callback, MQTT push, a CLI on the gateway. The listener does not care how the server learned where to dial.
$client = (new ReverseConnectClientFactory())->buildClient(
$session,
static fn (ClientBuilder $b) => $b
->setSecurityPolicy(SecurityPolicy::Basic256Sha256)
->setSecurityMode(SecurityMode::SignAndEncrypt)
->setClientCertificate('/certs/client.pem', '/certs/client.key', '/certs/ca.pem')
->setUserCredentials('operator', 'secret'),
);Reverse Connect inverts the TCP direction; it does not change the OPC UA security model. Configure the builder inside the $configure closure the same way you would for a classic outbound client. Do not call setTransport() inside the closure — the factory has already wired one from the inherited socket.
while ($keepRunning) {
try {
$session = $listener->accept(timeoutSeconds: 5.0);
} catch (ReverseConnectTimeoutException) {
continue;
} catch (ReverseConnectRejectedException $e) {
Log::warning("Rejected reverse-connect from {$e->rejectedMessage->serverUri}: {$e->getMessage()}");
continue;
} catch (ReverseHelloParseException $e) {
Log::warning("Malformed RHE: {$e->getMessage()}");
continue;
}
handleSession($session);
}
$listener->close();- Tiny, focused surface — six classes, four exceptions, three events. Everything else is
php-opcua/opcua-client. - No event loop dependency — bounded
accept(timeoutSeconds)overstream_select(). Plug into your own loop or run a CLI worker. - Fail-secure validator — empty whitelist refuses every frame; case-sensitive
ServerUrimatch;opc.tcp://scheme enforced on the announced endpoint. - PSR everywhere — PSR-3 logger and PSR-14 dispatcher both optional. No global state. No service locator.
- Cross-platform — pure PHP streams API; no Unix domain sockets, no FFI, no native extensions. Tested on Linux, macOS, and Windows.
- Reuses the full core — same
Client, same security stack, same modules, same DTOs asopcua-client. The transport seam isTcpTransport::fromConnectedSocket(); the rest of the pipeline is untouched. - Thoroughly tested — 44 unit tests + 4 end-to-end integration tests against UA-.NETStandard via
uanetstandard-test-suitev1.4.0+.
The published version of the documentation lives at https://www.php-opcua.com/dev/components. The Markdown sources ship under docs/:
| Section | Covers |
|---|---|
| Getting started — Overview · Installation · Quick start | What it is, how to install, first listener |
| Concepts — How Reverse Connect works | Wire format, lifecycle, security model |
| API — Listener · Validator · Factory · Events | Constructor arguments, methods, rejection rules |
| Recipes — Docker host networking | extra_hosts + 0.0.0.0 bind pattern |
| Reference — Exceptions | Every exception, cause, and catch strategy |
./vendor/bin/pest # everything
./vendor/bin/pest tests/Unit/ # unit only (44 tests, no external deps)
./vendor/bin/pest tests/Integration/ --group=integration # E2E (requires uanetstandard-test-suite v1.4.0+)The integration suite expects the opcua-no-security service from uanetstandard-test-suite running on opc.tcp://localhost:4840 with extra_hosts: ["host.docker.internal:host-gateway"] configured (already the case in v1.4.0+).
| Package | Description |
|---|---|
| opcua-client | Pure PHP OPC UA client (the core this extension hooks into) |
| opcua-client-ext-pubsub | OPC UA PubSub Subscriber — UDP + UADP + JSON |
| opcua-cli | CLI tool — browse, read, write, watch, discover endpoints, manage certificates |
| opcua-client-nodeset | Pre-generated PHP types from 51 OPC Foundation companion specifications |
| laravel-opcua | Laravel integration — service provider, facade, config |
| uanetstandard-test-suite | Docker-based OPC UA test servers (UA-.NETStandard) for integration testing |
Have questions, ideas, or want to share what you've built? Join the GitHub Discussions.
Contributions welcome — see CONTRIBUTING.md in the core repository for the shared code style and workflow.
See CHANGELOG.md.