-
-
Save lavacano/a678e65d31df9bec344e572461ed3e10 to your computer and use it in GitHub Desktop.
Disable WAN Interface on CARP Backup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/local/bin/php | |
<?php | |
declare(strict_types=1); | |
/* | |
* OPNsense HA Failover Script - FINAL CORRECTED VERSION | |
* Makes targeted changes for WAN, Tunnels, and Services. | |
* Controls services that are not CARP-aware. | |
* Correctly handles default route on backup node. | |
* Fixed: CARP service remains stopped on backup nodes (passive state). | |
*/ | |
require_once 'config.inc'; | |
require_once 'interfaces.inc'; | |
require_once 'util.inc'; | |
require_once 'system.inc'; | |
require_once 'filter.inc'; | |
use OPNsense\Core\Backend; | |
use OPNsense\Core\Config; | |
// ========================================================================= | |
// == Configuration | |
// ========================================================================= | |
const IFKEY = 'wan'; | |
const TBROKER_IFKEY = 'opt1'; | |
const WAN_IP_V4 = '69.420.xxx.yyy'; | |
const WAN_SUBNET_V4 = '27'; | |
const WAN_GW_NAME = 'WAN_STATIC'; | |
const TBROKER_GW_NAME = 'TUNNELBROKER_TUNNELV6'; | |
const LAN_VIP_V4 = '10.10.10.1'; | |
const LAN_VIP_V6 = '2600:1337:10::1'; | |
const LOCK_FILE = '/tmp/wan_failover.lock'; | |
const BACKUP_GUARD_FILE = '/tmp/ha_in_backup_state'; | |
const IP_NONE = 'none'; | |
const INTERFACE_SETTLE_TIME = 3; | |
const GATEWAY_SETTLE_TIME = 5; | |
// Services that should only run on the MASTER node (not CARP-aware) | |
const HA_CONTROLLED_SERVICES = [ | |
'lldpd', | |
'snmpd', | |
'igmpproxy' | |
]; | |
// ========================================================================= | |
// == Core Functions | |
// ========================================================================= | |
function log_failover(string $message, int $priority = LOG_NOTICE): void | |
{ | |
log_msg("WAN Failover: {$message}", $priority); | |
} | |
function acquire_lock() | |
{ | |
$lock_handle = fopen(LOCK_FILE, 'w'); | |
if (!$lock_handle || !flock($lock_handle, LOCK_EX | LOCK_NB)) { | |
log_failover('Could not acquire lock file. Another instance may be running.', LOG_WARNING); | |
exit(1); | |
} | |
return $lock_handle; | |
} | |
function setup_shutdown_handler($lock_handle): void | |
{ | |
register_shutdown_function(function () use ($lock_handle) { | |
if (is_resource($lock_handle)) { | |
flock($lock_handle, LOCK_UN); | |
fclose($lock_handle); | |
} | |
@unlink(LOCK_FILE); | |
}); | |
} | |
function validate_arguments(array $argv): array | |
{ | |
$subsystem = $argv[1] ?? ''; | |
$type = $argv[2] ?? ''; | |
if (!in_array($type, ['MASTER', 'BACKUP'], true)) { | |
exit(0); | |
} | |
if (!preg_match('/^[a-z0-9_]+@\S+$/i', $subsystem)) { | |
log_failover("Malformed subsystem argument: '{$subsystem}'.", LOG_WARNING); | |
exit(0); | |
} | |
return [$subsystem, $type]; | |
} | |
function configure_single_interface(Backend $backend, string $ifkey): bool | |
{ | |
log_failover("Configuring interface '{$ifkey}'..."); | |
try { | |
$result = $backend->configdRun("interface reconfigure {$ifkey}"); | |
if ($result === null) { | |
log_failover("Failed to reconfigure interface '{$ifkey}'", LOG_WARNING); | |
return false; | |
} | |
sleep(INTERFACE_SETTLE_TIME); | |
log_failover("Interface '{$ifkey}' configured successfully."); | |
return true; | |
} catch (Exception $e) { | |
log_failover("Exception configuring '{$ifkey}': " . $e->getMessage(), LOG_ERR); | |
return false; | |
} | |
} | |
function control_services(Backend $backend, bool $is_master): void | |
{ | |
$action = $is_master ? 'start' : 'stop'; | |
log_failover("Performing '{$action}' for HA controlled services..."); | |
foreach (HA_CONTROLLED_SERVICES as $service) { | |
try { | |
$result = $backend->configdRun("service {$action} {$service}"); | |
if ($result === null) { | |
log_failover("Failed to {$action} service '{$service}'", LOG_WARNING); | |
} else { | |
log_failover("Service '{$service}' {$action} command sent successfully."); | |
} | |
} catch (Exception $e) { | |
log_failover("Exception with service '{$service}': " . $e->getMessage(), LOG_ERR); | |
} | |
} | |
} | |
function configure_gateway_monitoring(Backend $backend): bool | |
{ | |
log_failover("Configuring gateway monitoring..."); | |
try { | |
$result = $backend->configdRun("system gateway configure"); | |
if ($result === null) { | |
log_failover("Failed to configure gateway monitoring via backend", LOG_WARNING); | |
// Fall back to system routing configuration | |
system_routing_configure(); | |
} | |
sleep(GATEWAY_SETTLE_TIME); | |
log_failover("Gateway monitoring configured successfully."); | |
return true; | |
} catch (Exception $e) { | |
log_failover("Exception configuring gateway monitoring: " . $e->getMessage(), LOG_ERR); | |
return false; | |
} | |
} | |
function remove_default_routes(): void | |
{ | |
log_failover("Removing default routes from backup node..."); | |
// Remove IPv4 default route | |
$ipv4_result = mwexec('/sbin/route delete -inet default 2>/dev/null'); | |
if ($ipv4_result === 0) { | |
log_failover("IPv4 default route removed successfully."); | |
} else { | |
log_failover("IPv4 default route removal failed or no route existed."); | |
} | |
// Remove IPv6 default route | |
$ipv6_result = mwexec('/sbin/route delete -inet6 default 2>/dev/null'); | |
if ($ipv6_result === 0) { | |
log_failover("IPv6 default route removed successfully."); | |
} else { | |
log_failover("IPv6 default route removal failed or no route existed."); | |
} | |
} | |
// ========================================================================= | |
// == State Handlers | |
// ========================================================================= | |
function handle_master_transition(Backend $backend): bool | |
{ | |
global $config; | |
log_msg('WAN Failover: MASTER transition starting...', LOG_NOTICE); | |
// Remove the guard file if it exists (re-arm the backup script) | |
if (file_exists(BACKUP_GUARD_FILE)) { | |
@unlink(BACKUP_GUARD_FILE); | |
log_failover("Removed backup guard file - re-arming failover script."); | |
} | |
// Stop CARP to prevent flapping during interface configuration | |
log_failover("Temporarily stopping CARP service..."); | |
$backend->configdRun("service stop carp"); | |
sleep(2); // Give the service a moment to stop | |
// 1. Set interface configs for MASTER state | |
$config['interfaces'][IFKEY]['enable'] = '1'; | |
$config['interfaces'][IFKEY]['ipaddr'] = WAN_IP_V4; | |
$config['interfaces'][IFKEY]['subnet'] = WAN_SUBNET_V4; | |
$config['interfaces'][IFKEY]['gateway'] = WAN_GW_NAME; | |
if (!empty(TBROKER_IFKEY)) { | |
$config['interfaces'][TBROKER_IFKEY]['enable'] = '1'; | |
} | |
// 2. Write the config changes to disk | |
$config_instance = Config::getInstance(); | |
$config_instance->lock(); | |
try { | |
if (write_config('WAN Failover: MASTER transition') === false) { | |
log_failover('Failed to write configuration', LOG_ERR); | |
return false; | |
} | |
} finally { | |
$config_instance->unlock(); | |
} | |
// 3. Apply interface changes | |
$success = true; | |
if (!configure_single_interface($backend, IFKEY)) { | |
$success = false; | |
} | |
if (!empty(TBROKER_IFKEY)) { | |
if (!configure_single_interface($backend, TBROKER_IFKEY)) { | |
log_failover('Tunnel interface configuration failed, but continuing...', LOG_WARNING); | |
} | |
} | |
// 4. Start services that should only run on MASTER | |
control_services($backend, true); | |
// 5. Configure routing and gateway monitoring | |
system_routing_configure(); | |
sleep(5); // Wait for routing table to settle | |
configure_gateway_monitoring($backend); | |
// 6. Restart CARP now that all configurations are stable | |
log_failover("Restarting CARP service..."); | |
$backend->configdRun("service start carp"); | |
log_failover('MASTER transition complete.'); | |
return $success; | |
} | |
function handle_backup_transition(Backend $backend): bool | |
{ | |
global $config; | |
// Check if the guard file exists. If so, we're already in BACKUP state. | |
if (file_exists(BACKUP_GUARD_FILE)) { | |
log_failover("Guard file exists - already in BACKUP state. Exiting to prevent loop."); | |
return true; | |
} | |
log_msg('WAN Failover: BACKUP transition starting...', LOG_NOTICE); | |
// 1. Stop services that should only run on MASTER | |
control_services($backend, false); | |
// Stop CARP to prevent it from reacting to the interface going down | |
log_failover("Stopping CARP service to maintain passive state..."); | |
$backend->configdRun("service stop carp"); | |
sleep(2); | |
// 2. Set interface configs for BACKUP state | |
$config['interfaces'][IFKEY]['ipaddr'] = IP_NONE; | |
unset($config['interfaces'][IFKEY]['enable']); | |
unset($config['interfaces'][IFKEY]['gateway']); | |
if (!empty(TBROKER_IFKEY)) { | |
unset($config['interfaces'][TBROKER_IFKEY]['enable']); | |
} | |
// 3. Write the config changes to disk | |
$config_instance = Config::getInstance(); | |
$config_instance->lock(); | |
try { | |
if (write_config('WAN Failover: BACKUP transition') === false) { | |
log_failover('Failed to write configuration', LOG_ERR); | |
return false; | |
} | |
} finally { | |
$config_instance->unlock(); | |
} | |
// 4. Apply interface changes | |
$success = true; | |
if (!configure_single_interface($backend, IFKEY)) { | |
$success = false; | |
} | |
if (!empty(TBROKER_IFKEY)) { | |
if (!configure_single_interface($backend, TBROKER_IFKEY)) { | |
log_failover('Tunnel interface configuration failed, but continuing...', LOG_WARNING); | |
} | |
} | |
// 5. Explicitly remove the default route to prevent WAN traffic leakage | |
remove_default_routes(); | |
// Create the guard file to prevent the script from running again | |
if (touch(BACKUP_GUARD_FILE)) { | |
log_failover("Created backup guard file to prevent script loops."); | |
} else { | |
log_failover("Failed to create backup guard file.", LOG_WARNING); | |
} | |
// DO NOT RESTART CARP HERE - let the node remain passive | |
log_failover('BACKUP transition complete. CARP service remains stopped to maintain passive state.'); | |
return $success; | |
} | |
// ========================================================================= | |
// == Main Execution | |
// ========================================================================= | |
$lock_handle = acquire_lock(); | |
setup_shutdown_handler($lock_handle); | |
[$subsystem, $type] = validate_arguments($argv); | |
try { | |
$backend = new Backend(); | |
$success = ($type === 'MASTER') ? handle_master_transition($backend) : handle_backup_transition($backend); | |
exit($success ? 0 : 1); | |
} catch (Exception $e) { | |
log_failover('Unexpected error: ' . $e->getMessage(), LOG_CRIT); | |
exit(1); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/local/bin/php | |
<?php | |
/* | |
* OPNsense HA Failover Script for Single WAN IP (v2.9) | |
* | |
* v2.9 - 2025-07-12 | |
* - Added optional, independent service management for Unbound DNS, AdGuard Home, | |
* and the DHCP Daemon. | |
* | |
* NOTE! - set System > Settings > Tunables > net.inet.carp.init_delay = 60 | |
* Delay CARP startup for 60 seconds to allow interfaces to stabilize. | |
*/ | |
// #################### CONFIGURATION #################### | |
$ifkey = 'wan'; | |
$tbroker_ifkey = 'opt1'; | |
// For STATIC: set your IP and subnet. For DHCP: set ipaddr to 'dhcp'. | |
$wan_ip_v4 = 'A.B.C.100'; | |
$wan_subnet_v4 = '27'; | |
// Names of the gateways to manage | |
$wan_gw_name = 'WAN_UPLINK_GW'; | |
$tbroker_gw_name = 'IPV6_TUNNEL_GW'; | |
// LAN CARP VIP for backup node internet access | |
$lan_vip_v4 = '10.10.10.1'; | |
$lan_vip_v6 = '2001:db8:388a:10::1'; | |
// --- Service Management Toggles --- | |
$manage_dhcpd = true; // Manage the ISC DHCPv4 Server (dhcpd) | |
$manage_unbound = true; // Manage Unbound DNS | |
$manage_adguard = false; // Manage AdGuard Home (non-standard plugin) | |
$verbose_logging = true; | |
$lock_file = '/tmp/wan_failover.lock'; | |
// ####################################################### | |
require_once("config.inc"); | |
require_once("interfaces.inc"); | |
require_once("util.inc"); | |
require_once("system.inc"); | |
function log_failover($message, $priority = LOG_NOTICE){ | |
global $verbose_logging; | |
if ($verbose_logging || $priority >= LOG_WARNING) { | |
log_msg("WAN Failover: " . $message, $priority); | |
} | |
} | |
function configure_gateway($gw_name, $interface, $is_disabled){ | |
global $config; | |
if (empty($gw_name) || !isset($config['gateways']['gateway_item']) || !is_array($config['gateways']['gateway_item'])) { | |
return; | |
} | |
foreach ($config['gateways']['gateway_item'] as &$gateway) { | |
if (isset($gateway['name']) && $gateway['name'] == $gw_name) { | |
$gateway['interface'] = $interface; | |
if ($is_disabled) { | |
$gateway['disabled'] = true; | |
log_failover("Gateway '{$gw_name}' marked as DISABLED on interface '{$interface}'."); | |
} else { | |
unset($gateway['disabled']); | |
log_failover("Gateway '{$gw_name}' marked as ENABLED on interface '{$interface}'."); | |
} | |
break; | |
} | |
} | |
} | |
function handle_master_transition($ifkey, $subsystem){ | |
global $config, $wan_ip_v4, $wan_subnet_v4, $wan_gw_name, $tbroker_gw_name, $tbroker_ifkey, $manage_dhcpd, $manage_unbound, $manage_adguard; | |
log_msg("WAN Failover: CARP MASTER event on {$subsystem}. Starting transition...", LOG_NOTICE); | |
// STAGE 1: Enable interfaces | |
log_failover("MASTER - Stage 1: Enabling interfaces."); | |
config_read_array(); | |
$config['interfaces'][$ifkey]['enable'] = true; | |
if (!empty($tbroker_gw_name)) { | |
$config['interfaces'][$tbroker_ifkey]['enable'] = true; | |
} | |
write_config("WAN Failover: Master - Stage 1", false); | |
interface_configure(false, $ifkey, true, false); | |
if (!empty($tbroker_gw_name)) { | |
interface_configure(false, $tbroker_ifkey, true, false); | |
} | |
// STAGE 2: Enable and assign gateways | |
log_failover("MASTER - Stage 2: Enabling and assigning gateways."); | |
config_read_array(); | |
configure_gateway($wan_gw_name, $ifkey, false); | |
if (!empty($tbroker_gw_name)) { | |
configure_gateway($tbroker_gw_name, $tbroker_ifkey, false); | |
} | |
write_config("WAN Failover: Master - Stage 2", false); | |
system_routing_configure(); | |
// STAGE 3: Configure WAN IP | |
log_failover("MASTER - Stage 3: Configuring WAN IP and linking gateway."); | |
config_read_array(); | |
$config['interfaces'][$ifkey]['ipaddr'] = $wan_ip_v4; | |
if ($wan_ip_v4 !== 'dhcp' && !empty($wan_subnet_v4)) { | |
$config['interfaces'][$ifkey]['subnet'] = $wan_subnet_v4; | |
} | |
$config['interfaces'][$ifkey]['gateway'] = $wan_gw_name; | |
write_config("WAN Failover: Master - Stage 3", false); | |
interface_configure(false, $ifkey, true, false); | |
system_routing_configure(); | |
// STAGE 4: Reset TunnelBroker gateway | |
if (!empty($tbroker_gw_name)) { | |
log_failover("MASTER - Stage 4: Resetting TunnelBroker gateway."); | |
sleep(1); | |
config_read_array(); | |
configure_gateway($tbroker_gw_name, $tbroker_ifkey, true); | |
write_config("WAN Failover: Master - Stage 4a (Disable Tunnel)", false); | |
system_routing_configure(); | |
sleep(1); | |
config_read_array(); | |
configure_gateway($tbroker_gw_name, $tbroker_ifkey, false); | |
write_config("WAN Failover: Master - Stage 4b (Enable Tunnel)", false); | |
system_routing_configure(); | |
} | |
// Finalize: Restart services on the new MASTER node. | |
if ($manage_dhcpd) { | |
log_failover("Restarting DHCPD service on MASTER node."); | |
mwexecf('/usr/local/sbin/pluginctl -s dhcpd restart'); | |
} | |
if ($manage_unbound) { | |
log_failover("Restarting Unbound DNS service on MASTER node."); | |
mwexecf('/usr/local/sbin/pluginctl -s unbound restart'); | |
} | |
if ($manage_adguard) { | |
log_failover("Restarting AdGuard Home service on MASTER node."); | |
mwexecf('/usr/local/etc/rc.d/adguardhome restart'); | |
} | |
log_failover("MASTER transition complete."); | |
return true; | |
} | |
function handle_backup_transition($ifkey, $subsystem){ | |
global $config, $lan_vip_v4, $lan_vip_v6, $wan_gw_name, $tbroker_gw_name, $tbroker_ifkey, $manage_dhcpd, $manage_unbound, $manage_adguard; | |
log_msg("WAN Failover: CARP BACKUP event on {$subsystem}. Disabling WAN and gateways.", LOG_NOTICE); | |
// Stop services on the new BACKUP node. | |
if ($manage_dhcpd) { | |
log_failover("Stopping DHCPD service on BACKUP node."); | |
mwexecf('/usr/local/sbin/pluginctl -s dhcpd stop'); | |
} | |
if ($manage_unbound) { | |
log_failover("Stopping Unbound DNS service on BACKUP node."); | |
mwexecf('/usr/local/sbin/pluginctl -s unbound stop'); | |
} | |
if ($manage_adguard) { | |
log_failover("Stopping AdGuard Home service on BACKUP node."); | |
mwexecf('/usr/local/etc/rc.d/adguardhome stop'); | |
} | |
$real_wan_if = get_real_interface($ifkey); | |
if (!empty($real_wan_if)) { | |
log_failover("Forcing physical interface {$real_wan_if} down."); | |
mwexecf('/sbin/ifconfig %s down', [$real_wan_if]); | |
} | |
log_failover("Killing states on interface '{$ifkey}'."); | |
mwexecf('/sbin/pfctl -i %s -F states', [$ifkey]); | |
config_read_array(); | |
$config['interfaces'][$ifkey]['ipaddr'] = 'none'; | |
unset($config['interfaces'][$ifkey]['enable']); | |
unset($config['interfaces'][$ifkey]['gateway']); | |
if (!empty($tbroker_gw_name)) { | |
unset($config['interfaces'][$tbroker_ifkey]['enable']); | |
} | |
configure_gateway($wan_gw_name, $ifkey, true); | |
if (!empty($tbroker_gw_name)) { | |
configure_gateway($tbroker_gw_name, $tbroker_ifkey, true); | |
} | |
if (write_config("WAN Failover: Set to BACKUP", false) === false) { | |
log_failover("FATAL: Failed to write configuration to disk.", LOG_ERR); | |
return false; | |
} | |
log_failover("Applying light interface configurations..."); | |
interface_configure(false, $ifkey, false, false); | |
if (!empty($tbroker_gw_name)) { | |
interface_configure(false, $tbroker_ifkey, false, false); | |
} | |
$lan_if = null; | |
$target_vip = !empty($lan_vip_v4) ? $lan_vip_v4 : $lan_vip_v6; | |
if (empty($target_vip)) { | |
return true; | |
} | |
foreach ($config['virtualip']['vip'] as $vip) { | |
if (isset($vip['subnet']) && $vip['subnet'] == $target_vip) { | |
$lan_if = $vip['interface']; | |
break; | |
} | |
} | |
if ($lan_if) { | |
$real_lan_if = get_real_interface($lan_if); | |
log_failover("Found LAN VIP on interface '{$lan_if}'. Rerouting default gateways."); | |
if (!empty($lan_vip_v4)) { | |
$gw_v4 = ['gateway' => $lan_vip_v4, 'if' => $real_lan_if]; | |
system_default_route($gw_v4, 'inet'); | |
} | |
if (!empty($lan_vip_v6)) { | |
$gw_v6 = ['gateway' => $lan_vip_v6, 'if' => $real_lan_if]; | |
system_default_route($gw_v6, 'inet6'); | |
} | |
} | |
return true; | |
} | |
// --- Main Execution --- | |
register_shutdown_function(function () use ($lock_file) { | |
if (file_exists($lock_file)) { | |
unlink($lock_file); | |
} | |
}); | |
$lock_handle = fopen($lock_file, 'w'); | |
if ($lock_handle === false || !flock($lock_handle, LOCK_EX | LOCK_NB)) { | |
exit(1); | |
} | |
$subsystem = !empty($argv[1]) ? $argv[1] : ''; | |
$type = !empty($argv[2]) ? $argv[2] : ''; | |
if (!in_array($type, ['MASTER', 'BACKUP'])) { | |
exit(0); | |
} | |
if (!strstr($subsystem, '@')) { | |
exit(0); | |
} | |
$success = false; | |
if ($type === "MASTER") { | |
$success = handle_master_transition($ifkey, $subsystem); | |
} else { | |
$success = handle_backup_transition($ifkey, $subsystem); | |
} | |
if ($success) { | |
exit(0); | |
} else { | |
exit(1); | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment