Skip to content
Merged
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
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
A pure-PHP client implementation of strongSwan's [VICI protocol](https://github.com/strongswan/strongswan/blob/master/src/libcharon/plugins/vici/README.md). Use it from PHP to monitor, configure, and control the IKE daemon `charon`.

- Covers every command and event documented in the VICI README.
- Pluggable transport: Unix domain socket (default) or TCP, plus a generic `StreamTransport` for injection and testing.
- Pluggable transport: Unix domain socket (default) or TCP, plus a generic `StreamTransport` for injection and testing. An opt-in `ReconnectingTransport` wrapper recovers from charon restarts on Unix sockets.
- Blocking `Session` for commands, plus an `EventListener` for long-running event subscriptions.
- Streaming list commands (`list-sas`, `list-conns`, ...) expose event streams as PHP generators.
- Fully typed, PHPStan level 8 clean, zero runtime dependencies.
Expand Down Expand Up @@ -68,6 +68,33 @@ $stream = stream_socket_client('unix:///run/strongswan/charon.vici');
$session = new Session(new StreamTransport($stream, readTimeout: 10.0));
```

### Long-lived connections (Unix socket reconnect)

For daemons or `while (true)` loops where charon may restart and recreate the Unix socket file, use `ReconnectingTransport`. It reconnects automatically when a single `send()`, `receive()`, or `hasData()` call fails with `ConnectionException`:

```php
use Bk203\Vici\Session;
use Bk203\Vici\Transport\ReconnectingTransport;

$session = new Session(new ReconnectingTransport(
path: '/var/run/charon.vici',
readTimeout: 30.0,
));

while (true) {
sleep(60);
$info = $session->version();
}
```

`ReconnectingTransport` is opt-in; `new Session()` alone still uses a plain `UnixSocketTransport` with no automatic recovery.

**v1 limitations**

- Retry covers one transport I/O call. Multi-packet commands (`streamedRequest()`, `EventListener::listen()`) can still fail mid-operation; catch `ConnectionException` and restart the command or listener loop.
- Daemon-side `EVENT_REGISTER` state is not replayed after reconnect. Re-register events or wait for a future Session-level restore helper.
- `TimeoutException` is not retried (slow charon is not treated as a dead socket).

## Common workflows

### Load a connection
Expand Down
98 changes: 98 additions & 0 deletions src/Transport/ReconnectingTransport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace Bk203\Vici\Transport;

use Bk203\Vici\Exception\ConnectionException;

/**
* Transport wrapper that reconnects to a Unix VICI socket after connection
* failures. Intended for long-lived loops where charon may restart and
* recreate the socket file.
*/
final class ReconnectingTransport implements TransportInterface
{
private UnixSocketTransport $inner;

public function __construct(
public readonly string $path = UnixSocketTransport::DEFAULT_PATH,
public readonly float $connectTimeout = 5.0,
public readonly ?float $readTimeout = 30.0,
public readonly int $maxReconnectAttempts = 3,
public readonly int $reconnectDelayMs = 200,
) {
$this->inner = new UnixSocketTransport(
$this->path,
$this->connectTimeout,
$this->readTimeout,
);
}

public function send(string $bytes): void
{
$this->withReconnect(function (UnixSocketTransport $inner) use ($bytes): void {
$inner->send($bytes);
});
}

public function receive(?float $timeout = null): string
{
return $this->withReconnect(static fn (UnixSocketTransport $inner): string => $inner->receive($timeout));
}

public function hasData(float $timeout = 0.0): bool
{
return $this->withReconnect(static fn (UnixSocketTransport $inner): bool => $inner->hasData($timeout));
}

public function isConnected(): bool
{
return $this->inner->isConnected();
}

public function close(): void
{
$this->inner->close();
}

public function getStream()
{
return $this->inner->getStream();
}

/**
* @template T
*
* @param callable(UnixSocketTransport): T $operation
*
* @return T
*/
private function withReconnect(callable $operation): mixed
{
try {
return $operation($this->inner);
} catch (ConnectionException $first) {
$last = $first;

for ($attempt = 0; $attempt < $this->maxReconnectAttempts; $attempt++) {
usleep($this->reconnectDelayMs * 1000);

try {
$this->inner->reconnect();
} catch (ConnectionException $e) {
$last = $e;
continue;
}

try {
return $operation($this->inner);
} catch (ConnectionException $e) {
$last = $e;
}
}

throw $last;
}
}
}
28 changes: 21 additions & 7 deletions src/Transport/SocketTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ final public function hasData(float $timeout = 0.0): bool
$ready = @stream_select($read, $write, $except, $sec, $usec);

if ($ready === false) {
throw new ConnectionException('stream_select() failed on VICI transport.');
$this->connectionFailed('stream_select() failed on VICI transport.');
}

return $ready > 0;
Expand All @@ -96,6 +96,20 @@ final public function getStream()
return \is_resource($this->stream) ? $this->stream : null;
}

protected function invalidate(): void
{
$this->close();
}

/**
* @return never
*/
protected function connectionFailed(string $message): void
{
$this->invalidate();
throw new ConnectionException($message);
}

private function writeAll(string $data): void
{
$stream = $this->requireStream();
Expand All @@ -110,9 +124,9 @@ private function writeAll(string $data): void
throw new TimeoutException('Timed out writing to VICI socket.');
}
if (feof($stream)) {
throw new ConnectionException('VICI socket closed during write.');
$this->connectionFailed('VICI socket closed during write.');
}
throw new ConnectionException('Failed to write to VICI socket.');
$this->connectionFailed('Failed to write to VICI socket.');
}
$written += $chunk;
}
Expand All @@ -137,7 +151,7 @@ private function readAll(int $length, ?float $timeout): string
$usec = (int) round(($remaining - $sec) * 1_000_000);
$ready = @stream_select($read, $write, $except, $sec, $usec);
if ($ready === false) {
throw new ConnectionException('stream_select() failed on VICI transport.');
$this->connectionFailed('stream_select() failed on VICI transport.');
}
if ($ready === 0) {
throw new TimeoutException('Timed out reading from VICI socket.');
Expand All @@ -148,11 +162,11 @@ private function readAll(int $length, ?float $timeout): string
\assert($need > 0);
$chunk = @fread($stream, $need);
if ($chunk === false) {
throw new ConnectionException('Failed to read from VICI socket.');
$this->connectionFailed('Failed to read from VICI socket.');
}
if ($chunk === '') {
if (feof($stream)) {
throw new ConnectionException('VICI socket closed during read.');
$this->connectionFailed('VICI socket closed during read.');
}
$meta = stream_get_meta_data($stream);
if ($meta['timed_out']) {
Expand All @@ -173,7 +187,7 @@ private function readAll(int $length, ?float $timeout): string
private function requireStream()
{
if (!\is_resource($this->stream)) {
throw new ConnectionException('VICI transport is not connected.');
$this->connectionFailed('VICI transport is not connected.');
}
return $this->stream;
}
Expand Down
27 changes: 23 additions & 4 deletions src/Transport/UnixSocketTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,27 @@ public function __construct(
$this->connect();
}

private function connect(): void
public function reconnect(): void
{
$this->close();
$this->connect();
}

protected function connect(): void
{
$deadline = microtime(true) + $this->connectTimeout;
while (!file_exists($this->path) && microtime(true) < $deadline) {
usleep(100_000);
}

$remaining = max(0.0, $deadline - microtime(true));
$errno = 0;
$errstr = '';
$stream = @stream_socket_client(
'unix://' . $this->path,
$errno,
$errstr,
$this->connectTimeout,
$remaining,
\STREAM_CLIENT_CONNECT,
);

Expand All @@ -43,13 +55,20 @@ private function connect(): void
));
}

$this->applyStreamOptions($stream);
$this->stream = $stream;
}

/**
* @param resource $stream
*/
private function applyStreamOptions($stream): void
{
stream_set_blocking($stream, true);
if ($this->readTimeout !== null) {
$sec = (int) floor($this->readTimeout);
$usec = (int) round(($this->readTimeout - $sec) * 1_000_000);
stream_set_timeout($stream, $sec, $usec);
}

$this->stream = $stream;
}
}
Loading
Loading