Skip to content

Instantly share code, notes, and snippets.

@x-stp
Forked from spali/10-wancarp
Last active November 11, 2025 19:47
Show Gist options
  • Select an option

  • Save x-stp/8f19e45d804644ee2a0c39e2bf26ec8b to your computer and use it in GitHub Desktop.

Select an option

Save x-stp/8f19e45d804644ee2a0c39e2bf26ec8b to your computer and use it in GitHub Desktop.
Disable WAN Interface on CARP Backup
#!/usr/local/bin/php
<?php
declare(strict_types=1);
/* opnsense carp hook: toggle wan (+optional others) on master/backup
* based on kronenpj (e90258f12f7a40c4f38a23b609b3288b) and spali (2da4f23e488219504b2ada12ac59a7dc)
* pepijn van der stap, neosecurity.nl - 2025-11-11
*/
require_once "config.inc";
require_once "system.inc";
require_once "interfaces.inc";
require_once "interfaces.lib.inc";
require_once "util.inc";
const LOG_PREFIX = "[wan-carp]";
const LOCK_FILE = "/var/run/10-wancarp.lock";
$IFACES = ["wan"]; // e.g. ['wan','opt2']
$ONLY_SUBSYSTEM = ""; // e.g. "40@vlan009" (vhid@if). leave '' to react to all
$REWRITE_ROUTE_ON_BACKUP = false; // true => backup defaults via lan vip
$LAN_VIP = ""; // e.g. '10.10.10.1' (used only if ^ true)
$RESTART_ROUTING_ON_MASTER = false; // true => pluginctl routing restart after master
$RESTART_DELAY_SECONDS = 3;
$subsystem = $argv[1] ?? "";
$type = $argv[2] ?? "";
/* avoid races */
$lock = @fopen(LOCK_FILE, "c");
if ($lock && !@flock($lock, LOCK_EX | LOCK_NB)) {
carp_log("lock busy; skip '$subsystem'");
exit(75);
}
/* validate */
if (!in_array($type, ["MASTER", "BACKUP", "INIT"], true)) {
carp_log("unknown event '$type' from '$subsystem'");
exit(64);
}
if (strpos($subsystem, "@") === false) {
carp_log("invalid subsystem '$subsystem'");
exit(64);
}
if ($ONLY_SUBSYSTEM !== "" && $subsystem !== $ONLY_SUBSYSTEM) {
cleanup_lock($lock);
exit(0);
}
if ($type === "INIT") {
cleanup_lock($lock);
exit(0);
}
/* pick existing target ifaces, prevent ghosting config branches */
$config["interfaces"] = $config["interfaces"] ?? [];
$targets = [];
foreach (array_values(array_unique($IFACES)) as $k) {
if (
isset($config["interfaces"][$k]) &&
is_array($config["interfaces"][$k])
) {
$targets[] = $k;
} else {
carp_log("alias '$k' not configured; skipping");
}
}
if (!$targets) {
cleanup_lock($lock);
exit(0);
}
/* use first target (usually 'wan') as idempotency probe */
$probe = $targets[0];
$enabled = ($config["interfaces"][$probe]["enable"] ?? null) === "1";
if ($type === "MASTER") {
if ($enabled) {
carp_log("MASTER duplicate; already enabled ($subsystem)");
cleanup_lock($lock);
exit(0);
}
foreach ($targets as $ifkey) {
carp_log("MASTER on '$subsystem': enable '$ifkey'");
$config["interfaces"][$ifkey]["enable"] = "1";
if_flag($ifkey, "up");
interface_configure(false, $ifkey, true, true);
}
write_config(LOG_PREFIX . " master transition ($subsystem)", false);
if ($RESTART_ROUTING_ON_MASTER) {
sleep((int) $RESTART_DELAY_SECONDS);
mwexec("/usr/local/sbin/pluginctl -s routing restart 2>&1", $rc);
if ($rc !== 0) {
carp_log("routing restart rc=$rc after master ($subsystem)");
}
}
cleanup_lock($lock);
exit(0);
}
if ($type === "BACKUP") {
if (!$enabled) {
carp_log("BACKUP duplicate; already disabled ($subsystem)");
cleanup_lock($lock);
exit(0);
}
foreach ($targets as $ifkey) {
carp_log("BACKUP on '$subsystem': disable '$ifkey'");
interface_reset($ifkey); // dhclient/pppoe/states...
unset($config["interfaces"][$ifkey]["enable"]);
interface_configure(false, $ifkey, true, false);
if_flag($ifkey, "down");
}
write_config(LOG_PREFIX . " backup transition ($subsystem)", false);
if ($REWRITE_ROUTE_ON_BACKUP && filter_var($LAN_VIP, FILTER_VALIDATE_IP)) {
rewrite_default_route_to($LAN_VIP);
}
cleanup_lock($lock);
exit(0);
}
/* helpers */
function carp_log(string $msg): void
{
log_error(LOG_PREFIX . " " . $msg);
}
function cleanup_lock($lock): void
{
if ($lock) {
@flock($lock, LOCK_UN);
@fclose($lock);
}
}
function if_flag(string $ifkey, string $flag): void
{
if (function_exists("legacy_interface_flags")) {
legacy_interface_flags($ifkey, $flag);
return;
}
$dev = get_real_interface($ifkey);
if ($dev) {
mwexec(
"/sbin/ifconfig " .
escapeshellarg($dev) .
" " .
escapeshellarg($flag) .
" 2>&1",
$rc,
);
if ($rc !== 0) {
carp_log("ifconfig $flag failed on '$dev' rc=$rc");
}
}
}
function rewrite_default_route_to(string $vip): void
{
mwexec("/sbin/route del default 2>&1", $r1);
mwexec("/sbin/route add default " . escapeshellarg($vip) . " 2>&1", $r2);
if ($r1 !== 0 || $r2 !== 0) {
carp_log("route rewrite rc={$r1}/{$r2}");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment