Skip to content

Instantly share code, notes, and snippets.

@oLeDfrEeZe
Forked from spali/10-wancarp
Last active May 7, 2026 09:38
Show Gist options
  • Select an option

  • Save oLeDfrEeZe/5a417737d9e3fb034b0c8a31dcf8c8be to your computer and use it in GitHub Desktop.

Select an option

Save oLeDfrEeZe/5a417737d9e3fb034b0c8a31dcf8c8be to your computer and use it in GitHub Desktop.
Disable WAN Interface on CARP Backup
#!/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);
?>
@oLeDfrEeZe

oLeDfrEeZe commented Apr 11, 2026

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment