opcua-client-ext-reverse-connect · master
Docs · Getting started

Quick start

Bind a listener, accept a ReverseHello frame, build a Client from the resulting session, and call Read through the reverse-connected channel.

This walkthrough produces a working reverse-connect client in three explicit steps. It assumes the uanetstandard-test-suite v1.4.0+ stack is running locally on opc.tcp://localhost:4840 — see Installation · Running against the test server.

1 — Open a listener

use PhpOpcua\Client\ExtReverseConnect\ReverseConnectListener;
use PhpOpcua\Client\ExtReverseConnect\ReverseHelloValidator;

$listener = new ReverseConnectListener(
    bindHost: '0.0.0.0',
    bindPort: 0,
    validator: new ReverseHelloValidator(['urn:opcua:testserver:nodes']),
);
$listener->listen();

[, $port] = explode(':', $listener->getBindAddress());
$port = (int) $port;

bindPort: 0 asks the kernel for a free port; getBindAddress() reveals the chosen one. The validator's whitelist is the security boundary of the flow — only servers whose announced ServerUri matches an entry will be accepted.

2 — Trigger the server to dial back

In the test suite, a regular OPC UA client tells the server which host and port to dial:

use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\Security\SecurityMode;
use PhpOpcua\Client\Security\SecurityPolicy;
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');

$folder = NodeId::string(2, 'TestServer/ReverseConnect');
$start  = NodeId::string(2, 'TestServer/ReverseConnect/StartReverseConnect');

$trigger->call($folder, $start, [
    new Variant(BuiltinType::String, 'host.docker.internal'),
    new Variant(BuiltinType::UInt16, $port),
]);

In production the trigger comes from somewhere else — an HTTPS callback, an MQTT message, a configuration push, a CLI invocation on the gateway. The listener does not care how the server learned where to dial.

3 — Accept and build a Client

use PhpOpcua\Client\ExtReverseConnect\ReverseConnectClientFactory;

$session = $listener->accept(timeoutSeconds: 20.0);

$client = (new ReverseConnectClientFactory())->buildClient(
    $session,
    static fn (ClientBuilder $b) => $b
        ->setSecurityPolicy(SecurityPolicy::None)
        ->setSecurityMode(SecurityMode::None),
);

// Use the client exactly like a regularly connected one:
$dataValue = $client->read('ns=2;s=TestServer/DataTypes/Scalar/Int32Value');
echo $dataValue->getValue() . PHP_EOL;     // -100000

$client->disconnect();
$listener->close();

accept() blocks for up to timeoutSeconds waiting for an inbound TCP connection plus the RHE frame. On timeout it throws ReverseConnectTimeoutException; on a frame whose ServerUri is not whitelisted it throws ReverseConnectRejectedException and closes the socket before returning control.

buildClient() returns a fully connected PhpOpcua\Client\Client. The factory wraps the session's socket into a TcpTransport::fromConnectedSocket() and runs the standard ClientBuilder::connect() pipeline; the core's ManagesConnectionTrait::performConnect() skips the redundant TCP connect step when it sees the transport is already connected.

Full runnable example

See examples/exts/reverse-connect/listener.php in the shared examples repository — same three steps, plus an in-process PSR-14 dispatcher that prints every event the flow emits.

What next