-
-
Save oLeDfrEeZe/5a417737d9e3fb034b0c8a31dcf8c8be 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 | |
| require_once("config.inc"); | |
| require_once("interfaces.inc"); | |
| require_once("util.inc"); | |
| require_once("system.inc"); | |
| /* | |
| * WARNING: | |
| * This is vibe-coded / AI-generated code. | |
| * Review and test it carefully before using it in production. | |
| * | |
| * WHAT THIS SCRIPT DOES: | |
| * - Reacts to CARP MASTER / BACKUP / INIT events | |
| * - Activates the WAN interface on MASTER | |
| * - Forces the WAN interface inactive on BACKUP / INIT | |
| * | |
| * IMPORTANT: | |
| * This script does NOT mark the interface as "disabled" in the OPNsense GUI. | |
| * It only changes the runtime state of the interface and related processes. | |
| * | |
| * YOU MUST ADAPT THE PLACEHOLDERS BELOW TO MATCH YOUR OWN SETUP. | |
| */ | |
| $subsystem = $argv[1] ?? ''; | |
| $type = $argv[2] ?? ''; | |
| /* | |
| * REQUIRED CONFIGURATION | |
| * Replace all placeholder values below before using this script. | |
| */ | |
| $trigger_subsystem = 'REPLACE_WITH_CARP_TRIGGER_SUBSYSTEM'; // Example: 01@vlan01 | |
| $ifkey = 'REPLACE_WITH_INTERFACE_KEY'; // Example: wan | |
| $gw_name = 'REPLACE_WITH_GATEWAY_NAME'; // Example: WAN_DHCP | |
| $lockfile = '/tmp/wan_carp_ha.lock'; | |
| /* | |
| * OPTIONAL TIMING TUNING | |
| * Adjust these only if your system needs slower or faster timing. | |
| */ | |
| const WAIT_MASTER_RECONFIG = 2; | |
| const WAIT_MASTER_IP = 15; | |
| const WAIT_MASTER_DHCP_FALL = 5; | |
| const WAIT_BACKUP_SETTLE = 10; | |
| function run_cmd($cmd, $log_prefix = '') | |
| { | |
| $out = []; | |
| exec($cmd . ' 2>&1', $out, $rc); | |
| $msg = trim(implode(" | ", $out)); | |
| if ($rc !== 0) { | |
| log_error(($log_prefix ?: 'cmd') . " rc={$rc} cmd={$cmd}" . ($msg !== '' ? " output={$msg}" : '')); | |
| } elseif ($msg !== '') { | |
| log_msg(($log_prefix ?: 'cmd') . " ok cmd={$cmd} output={$msg}"); | |
| } | |
| return [$rc, $msg]; | |
| } | |
| function get_ipv4_list($ifname) | |
| { | |
| $out = trim(shell_exec("/sbin/ifconfig " . escapeshellarg($ifname) . " | /usr/bin/awk '/inet / {print \$2}'")); | |
| return array_values(array_filter(array_map('trim', explode("\n", $out)))); | |
| } | |
| function get_ipv6_list($ifname) | |
| { | |
| $out = trim(shell_exec("/sbin/ifconfig " . escapeshellarg($ifname) . " | /usr/bin/awk '/inet6 / && \$2 !~ /^fe80:/ {print \$2}'")); | |
| return array_values(array_filter(array_map('trim', explode("\n", $out)))); | |
| } | |
| function has_ipv4($ifname) | |
| { | |
| return !empty(get_ipv4_list($ifname)); | |
| } | |
| /* | |
| * Use a unique helper name to avoid collisions with OPNsense core functions. | |
| */ | |
| function wan_carp_is_process_running($pattern) | |
| { | |
| $out = []; | |
| exec('/bin/pgrep -f ' . escapeshellarg($pattern) . ' >/dev/null 2>&1', $out, $rc); | |
| return $rc === 0; | |
| } | |
| function wait_for_ipv4($ifname, $timeout = WAIT_MASTER_IP) | |
| { | |
| for ($i = 0; $i < $timeout; $i++) { | |
| if (has_ipv4($ifname)) { | |
| return true; | |
| } | |
| sleep(1); | |
| } | |
| return false; | |
| } | |
| function wait_until_backup_quiet($real_if, $gw_name, $timeout = WAIT_BACKUP_SETTLE) | |
| { | |
| for ($i = 0; $i < $timeout; $i++) { | |
| $has_v4 = !empty(get_ipv4_list($real_if)); | |
| $has_v6 = !empty(get_ipv6_list($real_if)); | |
| $dhcp4 = wan_carp_is_process_running("dhclient.*{$real_if}"); | |
| $dhcp6 = wan_carp_is_process_running("dhcp6c.*{$real_if}"); | |
| $dp = wan_carp_is_process_running("dpinger.*{$gw_name}"); | |
| if (!$has_v4 && !$has_v6 && !$dhcp4 && !$dhcp6 && !$dp) { | |
| return true; | |
| } | |
| sleep(1); | |
| } | |
| return false; | |
| } | |
| function backup_enforce_down($ifkey, $real_if, $gw_name, $tag = 'backup') | |
| { | |
| run_cmd("/usr/bin/pkill -f " . escapeshellarg("dhclient.*{$real_if}") . " || true", "{$tag}.kill_dhclient"); | |
| run_cmd("/usr/bin/pkill -f " . escapeshellarg("dhcp6c.*{$real_if}") . " || true", "{$tag}.kill_dhcp6c"); | |
| run_cmd("/usr/bin/pkill -f " . escapeshellarg("dpinger.*{$gw_name}") . " || true", "{$tag}.kill_dpinger"); | |
| run_cmd("/usr/local/sbin/configctl interface linkup stop " . escapeshellarg($ifkey), "{$tag}.linkup_stop"); | |
| run_cmd("/sbin/ifconfig " . escapeshellarg($real_if) . " down", "{$tag}.if_down"); | |
| foreach (get_ipv4_list($real_if) as $ip) { | |
| run_cmd("/sbin/ifconfig " . escapeshellarg($real_if) . " -alias " . escapeshellarg($ip), "{$tag}.del_v4"); | |
| } | |
| foreach (get_ipv6_list($real_if) as $ip6) { | |
| run_cmd("/sbin/ifconfig " . escapeshellarg($real_if) . " inet6 " . escapeshellarg($ip6) . " delete", "{$tag}.del_v6"); | |
| } | |
| } | |
| if (!in_array($type, ['MASTER', 'BACKUP', 'INIT'], true)) { | |
| log_msg("CARP '{$type}' event unknown from source '{$subsystem}'"); | |
| exit(1); | |
| } | |
| /* | |
| * Ignore unrelated CARP events. | |
| */ | |
| if ($subsystem !== $trigger_subsystem) { | |
| exit(0); | |
| } | |
| $real_if = get_real_interface($ifkey); | |
| if (empty($real_if)) { | |
| log_error("Could not resolve real interface for '{$ifkey}'"); | |
| exit(1); | |
| } | |
| /* | |
| * Prevent overlapping runs. | |
| */ | |
| $fh = fopen($lockfile, 'c'); | |
| if ($fh === false || !flock($fh, LOCK_EX | LOCK_NB)) { | |
| log_error("Could not acquire lock {$lockfile}, skipping '{$type}' for '{$ifkey}'"); | |
| exit(1); | |
| } | |
| log_msg("CARP {$type} on {$subsystem}: managing {$ifkey} ({$real_if})"); | |
| if ($type === 'MASTER') { | |
| run_cmd("/usr/local/sbin/configctl interface reconfigure " . escapeshellarg($ifkey), "master.reconfigure"); | |
| sleep(WAIT_MASTER_RECONFIG); | |
| run_cmd("/usr/local/sbin/configctl interface linkup start " . escapeshellarg($ifkey), "master.linkup_start"); | |
| if (!wait_for_ipv4($real_if)) { | |
| log_error("MASTER: no IPv4 on {$real_if} after reconfigure/linkup, trying DHCP fallback"); | |
| run_cmd("/sbin/ifconfig " . escapeshellarg($real_if) . " up", "master.if_up"); | |
| run_cmd("/sbin/dhclient " . escapeshellarg($real_if), "master.dhclient"); | |
| if (!wait_for_ipv4($real_if, WAIT_MASTER_DHCP_FALL)) { | |
| log_error("MASTER: DHCP fallback did not acquire IPv4 on {$real_if}"); | |
| } | |
| } | |
| if (has_ipv4($real_if)) { | |
| log_msg("MASTER: {$ifkey} is active on {$real_if}"); | |
| } else { | |
| log_error("MASTER: {$ifkey} still has no IPv4 on {$real_if}"); | |
| } | |
| } else { | |
| $phase = ($type === 'INIT') ? 'init' : 'backup'; | |
| backup_enforce_down($ifkey, $real_if, $gw_name, $phase . '1'); | |
| if (!wait_until_backup_quiet($real_if, $gw_name)) { | |
| log_msg(strtoupper($phase) . ": first quiet-check timed out on {$real_if}, running second enforcement round"); | |
| } | |
| backup_enforce_down($ifkey, $real_if, $gw_name, $phase . '2'); | |
| if (wait_until_backup_quiet($real_if, $gw_name, 3)) { | |
| log_msg(strtoupper($phase) . ": {$ifkey} forced inactive on {$real_if}"); | |
| } else { | |
| log_error(strtoupper($phase) . ": {$ifkey} may not be fully inactive on {$real_if}"); | |
| } | |
| } | |
| flock($fh, LOCK_UN); | |
| fclose($fh); | |
| exit(0); | |
| ?> |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
WARNING: This is vibe-coded / AI-generated code. Review it carefully before using it in production.
Tested and working for me on OPNsense 26.1.7.
This is an adjusted fork of an OPNsense CARP WAN failover hook for setups where a DHCP WAN should only be active on the current CARP MASTER.
The script only reacts to a specific CARP trigger interface / subsystem that must be configured manually for the local setup. It does not mark the interface as disabled in the OPNsense GUI, because it only changes the runtime state and does not toggle the persistent “Enable interface” setting.
However, you can still see the effect on the Dashboard and in the gateway monitoring views, because the script stops dpinger for the configured gateway and forces the interface inactive on BACKUP/INIT. On the BACKUP node, the affected gateway should therefore appear offline / down there even though the interface itself is not shown as disabled in the interface configuration page.