Reconcile endpoint-qualified firewall policy

This commit is contained in:
ms2k 2026-08-03 01:04:11 +09:00
commit 6ab168e041

View file

@ -4,12 +4,14 @@
// SPDX-FileCopyrightText: 2026 ms2k
require_once __DIR__ . '/../src/opnsense/mvc/app/library/OPNsense/NordVPN/NordApi.php';
require_once __DIR__ . '/../src/opnsense/mvc/app/library/OPNsense/NordVPN/EndpointStore.php';
require_once __DIR__ . '/../src/opnsense/mvc/app/library/OPNsense/NordVPN/EnrollmentInspector.php';
require_once __DIR__ . '/../src/opnsense/mvc/app/library/OPNsense/NordVPN/StatusStore.php';
require_once __DIR__ . '/../src/opnsense/mvc/app/library/OPNsense/NordVPN/SystemRunner.php';
require_once __DIR__ . '/../src/opnsense/mvc/app/library/OPNsense/NordVPN/TunnelHealth.php';
use OPNsense\NordVPN\EnrollmentInspector;
use OPNsense\NordVPN\EndpointStore;
use OPNsense\NordVPN\NordApi;
use OPNsense\NordVPN\StatusStore;
use OPNsense\NordVPN\SystemRunner;
@ -71,6 +73,44 @@ $test('an empty country allowlist queries recommendations directly', static func
$check($api->recommendations([]) === []);
});
$test('endpoint discovery paginates the complete NordLynx address set', static function () use ($check): void {
$requests = [];
$api = new NordApi(static function ($path, $query) use (&$requests): array {
$requests[] = [$path, $query];
if ($query['offset'] === 0) {
return array_map(
static fn($index) => ['station' => sprintf('198.51.%d.%d', intdiv($index, 250), $index % 250 + 1)],
range(0, 499)
);
}
return [['station' => '203.0.113.10'], ['station' => 'invalid']];
});
$addresses = $api->endpointAddresses();
$check(count($addresses) === 501);
$check($requests[0][0] === '/v1/servers');
$check($requests[0][1]['limit'] === 500);
$check($requests[1][1]['offset'] === 500);
$check($requests[0][1]['filters[servers_technologies]'] === 35);
});
$test('endpoint refresh writes an atomic reusable table', static function () use ($check): void {
$directory = sys_get_temp_dir() . '/nordvpn-endpoints-' . bin2hex(random_bytes(8));
$path = $directory . '/endpoints.txt';
$requests = 0;
$api = new NordApi(static function () use (&$requests): array {
$requests++;
return [['station' => '203.0.113.10']];
});
$store = new EndpointStore($path);
$check($store->refreshIfStale($api));
$check($store->available());
$check(file_get_contents($path) === "203.0.113.10\n");
$check(!$store->refreshIfStale($api));
$check($requests === 1);
unlink($path);
rmdir($directory);
});
$test('RFC 1918 warning follows destination selector semantics', static function () use ($check): void {
$inspector = new EnrollmentInspector([
'private' => ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
@ -119,6 +159,19 @@ $test('WireGuard apply follows the OPNsense registration sequence', static funct
]);
});
$test('firewall apply reloads rules before evicting plugin states', static function () use ($check): void {
$commands = [];
$runner = new SystemRunner(static function ($command) use (&$commands): string {
$commands[] = $command;
return '';
});
$runner->applyFirewall();
$check($commands === [
['/usr/local/etc/rc.filter_configure'],
['/sbin/pfctl', '-k', 'label', '-k', SystemRunner::FIREWALL_LABEL],
]);
});
$failures = 0;
foreach ($tests as $name => $body) {
try {
@ -129,4 +182,9 @@ foreach ($tests as $name => $body) {
fwrite(STDERR, "not ok - {$name}: {$exception->getMessage()}\n");
}
}
$command = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(__DIR__ . '/reconciler.php');
passthru($command, $status);
if ($status !== 0) {
$failures++;
}
exit($failures === 0 ? 0 : 1);