Repair managed gateway interface resolution

This commit is contained in:
ms2k 2026-08-03 23:48:07 +09:00
commit aaf5d18f58
4 changed files with 160 additions and 0 deletions

View file

@ -44,6 +44,7 @@ class ServiceController extends ApiControllerBase
]);
$runner->applyFirewall();
$runner->applyWireGuard();
$configuration->repairGatewayInterface();
$runner->applyRouting();
$runner->applyFirewall();
sleep(5);

View file

@ -140,6 +140,28 @@ class ManagedConfiguration
return $instance === null ? null : (string)$instance->interface;
}
public function repairGatewayInterface(): bool
{
$interface = Config::getInstance()->object()->interfaces->{self::INTERFACE} ?? null;
if ($interface === null) {
return false;
}
if (empty((string)$interface->internal_dynamic)) {
throw new \RuntimeException('The NordVPN interface identifier is already in use.');
}
$device = $this->interfaceName();
if ($device === null || $device === '') {
throw new \RuntimeException('The managed WireGuard interface is unavailable.');
}
if ((string)$interface->if === $device && (string)$interface->virtual !== '1') {
return false;
}
$interface->if = $device;
unset($interface->virtual);
Config::getInstance()->save(['description' => 'Repair NordVPN gateway interface']);
return true;
}
public function settings(): Settings
{
return $this->settings;

View file

@ -44,6 +44,10 @@ class Reconciler
'message' => 'Bootstrap is required.',
]);
}
if ($this->configuration->repairGatewayInterface()) {
$this->runner->applyRouting();
$this->runner->applyFirewall();
}
$endpointsChanged = false;
if ((string)($settings->general->exempt_wireguard ?? '0') === '1') {
@ -83,6 +87,7 @@ class Reconciler
}
$this->configuration->updateCandidate($candidate);
$this->runner->applyWireGuard();
$this->configuration->repairGatewayInterface();
($this->pause)(5);
$health = $this->health->check((string)$this->configuration->interfaceName());
if ($health['available']) {

View file

@ -43,6 +43,11 @@ class TestConfiguration extends ManagedConfiguration
{
return 'wg0';
}
public function repairGatewayInterface(): bool
{
return false;
}
}
class AvailableHealth extends TunnelHealth
@ -70,6 +75,133 @@ class FailOnceRunner extends SystemRunner
}
}
class RepairConfiguration extends TestConfiguration
{
public function repairGatewayInterface(): bool
{
return true;
}
}
class RepairRunner extends SystemRunner
{
public int $routingCalls = 0;
public int $firewallCalls = 0;
public function applyRouting(): void
{
$this->routingCalls++;
}
public function applyFirewall(): void
{
$this->firewallCalls++;
}
}
class FailoverConfiguration extends TestConfiguration
{
public bool $candidateUpdated = false;
public int $repairCalls = 0;
public function updateCandidate(array $candidate): void
{
$this->candidateUpdated = true;
}
public function repairGatewayInterface(): bool
{
if (!$this->candidateUpdated) {
return false;
}
$this->candidateUpdated = false;
$this->repairCalls++;
return true;
}
}
class RecoveringHealth extends TunnelHealth
{
private int $checks = 0;
public function __construct()
{
}
public function check(string $interface): array
{
$this->checks++;
return ['available' => $this->checks > 1, 'last_handshake' => $this->checks > 1 ? 1000 : 0];
}
}
class FailoverRunner extends SystemRunner
{
public int $wireGuardCalls = 0;
public int $routingCalls = 0;
public int $firewallCalls = 0;
public function applyWireGuard(): void
{
$this->wireGuardCalls++;
}
public function applyRouting(): void
{
$this->routingCalls++;
}
public function applyFirewall(): void
{
$this->firewallCalls++;
}
}
$repairPath = sys_get_temp_dir() . '/nordvpn-repair-' . bin2hex(random_bytes(8)) . '.json';
$repairRunner = new RepairRunner();
(new Reconciler(
new NordApi(static fn() => []),
new RepairConfiguration(),
new AvailableHealth(),
new StatusStore($repairPath),
$repairRunner
))->run();
if ($repairRunner->routingCalls !== 1 || $repairRunner->firewallCalls !== 2) {
throw new \RuntimeException('Gateway interface repair was not applied.');
}
unlink($repairPath);
echo "ok - reconciliation applies gateway interface repair\n";
$failoverPath = sys_get_temp_dir() . '/nordvpn-failover-' . bin2hex(random_bytes(8)) . '.json';
$failoverConfiguration = new FailoverConfiguration();
$failoverRunner = new FailoverRunner();
$publicKey = base64_encode(str_repeat("\0", 32));
(new Reconciler(
new NordApi(static fn() => [[
'hostname' => 'nl2.example.invalid',
'station' => '198.51.100.2',
'locations' => [['country' => ['code' => 'NL']]],
'technologies' => [[
'id' => 35,
'pivot' => ['status' => 'online'],
'metadata' => [['name' => 'public_key', 'value' => $publicKey]],
]],
]]),
$failoverConfiguration,
new RecoveringHealth(),
new StatusStore($failoverPath),
$failoverRunner,
static fn($seconds) => null
))->run();
if ($failoverRunner->wireGuardCalls !== 1 ||
$failoverConfiguration->repairCalls !== 1 ||
$failoverRunner->routingCalls !== 1 ||
$failoverRunner->firewallCalls !== 1) {
throw new \RuntimeException('Server failover did not repair the gateway interface.');
}
unlink($failoverPath);
echo "ok - server failover repairs the gateway interface\n";
$path = sys_get_temp_dir() . '/nordvpn-retry-' . bin2hex(random_bytes(8)) . '.json';
$status = new StatusStore($path);
$runner = new FailOnceRunner();