#!/usr/local/bin/php
<?php
/**
 * Author 2025 Etienne Girault <etienne.girault@gmail.com>
 * OPNsense CARP event script
 * - Enables/disables the WAN interface only when needed
 * - Avoids reapplying config when CARP triggers multiple times
 */

require_once("config.inc");
require_once("interfaces.inc");
require_once("util.inc");
require_once("system.inc");

// Read CARP event arguments
$subsystem = !empty($argv[1]) ? $argv[1] : '';
$type = !empty($argv[2]) ? $argv[2] : '';

// Accept only MASTER/BACKUP events
if (!in_array($type, ['MASTER', 'BACKUP'])) {
    // Ignore CARP INIT, DEMOTED, etc.
    exit(0);
}

// Validate subsystem name format, expected pattern: <ifname>@<vhid>
if (!preg_match('/^[a-z0-9_]+@\S+$/i', $subsystem)) {
    log_error("Malformed subsystem argument: '{$subsystem}'.");
    exit(0);
}

// Only react to the primary VHID
list($vhid, $iface) = explode('@', $subsystem);
$primary_vhid = '1'; //

if ($vhid !== $primary_vhid) {
    exit(0); // ignore events from other VHIDs
}

// Interface key to manage
$ifkey = 'wan';
$real_if = get_real_interface('wan');
// Fallback gateway name
$gw_name = 'LAN_GW';
// Determine whether WAN interface is currently enabled
$ifkey_enabled = !empty($config['interfaces'][$ifkey]['enable']) ? true : false;
// Lock file to prevent interface flapping
$lock_file = '/tmp/carp_wan_disable_lock';
$lock_default_age = 5;
$lock_max_age = 10;

// MASTER event
if ($type === "MASTER") {
    // Enable WAN only if it's currently disabled
    if (!$ifkey_enabled) {
        // Check if lock file is present
        if (file_exists($lock_file)) {
            $lock_age = time() - (int)file_get_contents($lock_file);
            if ($lock_age < $lock_max_age) {
                log_msg("CARP event: WAN disable lock present ({$lock_age}s old), waiting...");
                $elapsed = 0;
                while (file_exists($lock_file) && $elapsed < 5000) {
                    usleep(500000);
                    $elapsed += 500;
                }
            } else {
                log_msg("CARP event: removing stale WAN disable lock.");
                @unlink($lock_file);
            }
        }
        log_msg("CARP event: switching to '$type', enabling interface '$ifkey'.", LOG_WARNING);
        $config['interfaces'][$ifkey]['enable'] = '1';
        write_config("enable interface '$ifkey' due CARP event '$type'", false);
        interface_configure(false, $ifkey, false, false);
    } else {
        log_msg("CARP event: already 'MASTER' for interface '$ifkey', nothing to do.");
    }

// BACKUP event
} else {
    // Disable WAN only if it's currently enabled
    if ($ifkey_enabled) {
        log_msg("CARP event: switching to '$type', disabling interface '$ifkey'.", LOG_WARNING);
        unset($config['interfaces'][$ifkey]['enable']);
        write_config("disable interface '$ifkey' due CARP event '$type'", false);
        interface_configure(false, $ifkey, false, false);
        // Remove WAN default gateway
        mwexec("/sbin/route delete default");
        foreach ($config['OPNsense']['Gateways']['gateway_item'] as $gw) {
            if ($gw['name'] === $gw_name) {
                $gw_ip = $gw['gateway'];
                break;
            }
        }
        // Shutdown WAN interface
        mwexec("/sbin/ifconfig {$real_if} down")
        // Add fallback default gateway
        mwexec("/sbin/route add default {$gw_ip}");
        // Create lock file
        file_put_contents($lock_file, time());
        sleep($lock_default_age);
        @unlink($lock_file);
    } else {
        log_msg("CARP event: already '$type' for interface '$ifkey', nothing to do.");
    }
}