103 lines
2.5 KiB
PHP
103 lines
2.5 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
// SPDX-License-Identifier: BSD-2-Clause
|
||
|
|
// SPDX-FileCopyrightText: 2026 ms2k
|
||
|
|
|
||
|
|
namespace OPNsense\NordVPN;
|
||
|
|
|
||
|
|
class Settings
|
||
|
|
{
|
||
|
|
public object $general;
|
||
|
|
public object $managed;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->general = (object)['countries' => ''];
|
||
|
|
$this->managed = (object)[
|
||
|
|
'server_country' => 'NL',
|
||
|
|
'server_hostname' => 'nl.example.invalid',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$root = dirname(__DIR__);
|
||
|
|
foreach (['NordApi', 'EndpointStore', 'StatusStore', 'SystemRunner', 'TunnelHealth', 'ManagedConfiguration', 'Reconciler'] as $class) {
|
||
|
|
require_once $root . '/src/opnsense/mvc/app/library/OPNsense/NordVPN/' . $class . '.php';
|
||
|
|
}
|
||
|
|
|
||
|
|
class TestConfiguration extends ManagedConfiguration
|
||
|
|
{
|
||
|
|
private Settings $testSettings;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->testSettings = new Settings();
|
||
|
|
}
|
||
|
|
|
||
|
|
public function settings(): Settings
|
||
|
|
{
|
||
|
|
return $this->testSettings;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function interfaceName(): ?string
|
||
|
|
{
|
||
|
|
return 'wg0';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class AvailableHealth extends TunnelHealth
|
||
|
|
{
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
public function check(string $interface): array
|
||
|
|
{
|
||
|
|
return ['available' => true, 'last_handshake' => 1000];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class FailOnceRunner extends SystemRunner
|
||
|
|
{
|
||
|
|
public int $firewallCalls = 0;
|
||
|
|
|
||
|
|
public function applyFirewall(): void
|
||
|
|
{
|
||
|
|
$this->firewallCalls++;
|
||
|
|
if ($this->firewallCalls === 1) {
|
||
|
|
throw new \RuntimeException('Synthetic firewall failure.');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$path = sys_get_temp_dir() . '/nordvpn-retry-' . bin2hex(random_bytes(8)) . '.json';
|
||
|
|
$status = new StatusStore($path);
|
||
|
|
$runner = new FailOnceRunner();
|
||
|
|
$reconciler = new Reconciler(
|
||
|
|
new NordApi(static fn() => []),
|
||
|
|
new TestConfiguration(),
|
||
|
|
new AvailableHealth(),
|
||
|
|
$status,
|
||
|
|
$runner
|
||
|
|
);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$reconciler->run();
|
||
|
|
throw new \RuntimeException('The first firewall failure was not reported.');
|
||
|
|
} catch (\RuntimeException $exception) {
|
||
|
|
if ($exception->getMessage() !== 'Synthetic firewall failure.') {
|
||
|
|
throw $exception;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (empty($status->read()['firewall_pending'])) {
|
||
|
|
throw new \RuntimeException('The failed firewall apply was not retained as pending.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$reconciler->run();
|
||
|
|
if ($runner->firewallCalls !== 2 || !empty($status->read()['firewall_pending'])) {
|
||
|
|
throw new \RuntimeException('The pending firewall apply was not completed on retry.');
|
||
|
|
}
|
||
|
|
|
||
|
|
unlink($path);
|
||
|
|
echo "ok - reconciliation retries a failed firewall apply\n";
|