Role rule: all-INIT VHIDs leave WAN up on a node that lost CARP (dual WAN) #15
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
eric/10-wancarp#15
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
node_role()treats "every counted VHID is INIT" as undecided and takes no action. When the node loses CARP without passing through BACKUP, its WAN stays UP while the peer promotes itself and brings its own WAN up. That is the dual-WAN state this script exists to prevent.Where
10-wancarp,node_role()(around line 474), specifically:INITmeans CARP isn't running on that VIP ...; the peer settles that")INITthe role is undecided, so nothing happens yet")Failure scenario
FreeBSD sets a VHID to INIT when its parent interface loses link or is admin-down (
carp_sc_state()insys/netinet/ip_carp.c, reason "hardware interface down"). It very likely does the same whennet.inet.carp.allow=0, which is what OPNsense's Interfaces → Virtual IPs → Status → Temporarily Disable CARP sets. Confirm that second case on a box.Setup: HA pair, one LAN CARP VIP (VHID 1), WAN is DHCP with no VIP,
$carp_vhids = [](default). This is the common homelab layout.1@vtnet1 INITfires, the script computesrole=undecided, and WAN stays UP.--reconciledoesn't fix it either, because it reaches the same undecided result every minute.With multiple VHIDs and preempt on (OPNsense default), the demotion counter pushes the remaining VHIDs to BACKUP, so "BACKUP wins" already covers that case. The hole is when no counted VHID is MASTER and at least one is INIT, most commonly a single counted VHID.
Testing missed it because maintenance-mode failover demotes the node to BACKUP, not INIT.
Suggested fix
Change the rule so that, among counted VHIDs:
Trade-offs to note in the docs:
rc.bootup's own interface configuration. The live event path (unlike--reconcile) has noexit_on_bootup()guard; decide whether the INIT→BACKUP mapping should skip during boot, e.g. by checkingproduct::getInstance()->booting().INITcurrently maps to$action = ''(around line 709). Decide whether a what-ifINITshould now show the BACKUP branch.Acceptance
ifconfig -ashowing onlycarp: INIT vhid 1 ...:role=BACKUP.carp: MASTER vhid 1+carp: INIT vhid 2: still undecided.[Unreleased]entry under Fixed.Follow-up audit: the boot check never fired
Found in an audit of the fixes against opnsense/core (
3684ed7), fixed in222180a.Finding.
node_booting()testedmethod_exists('product', 'booting'). In core,product(config.inc) has nobooting()method: it is served by__call(), which returns the stored value ornull.method_exists()doesn't see magic methods, so on a real firewall the check was always false and the script treated the box as never booting. An INIT event during boot (every VHID starts INIT) therefore became a live BACKUP run that calledinterface_reset('wan')whilerc.bootupwas bringing WAN up. The following MASTER event would have re-upped it, but that is exactly the clash this fix said it avoided. The harness missed it because itsproductstub declaredbooting()as a real method.Fix.
node_booting()now callsproduct::getInstance()->booting()directly, cast to bool, the same call core'sexit_on_bootup()makes.productstub now uses__call()like core. With that stub, the old script failsall-INIT during boot is undecided and changes nothing; the fixed one passes.boot check product::booting() present; booting=no.(or an error if the class is missing), so the live pre-check in #21 no longer needs a separatephp -rstep. The old pre-check there usedmethod_exists()too and would have printedbool(false)on a healthy box; it has been replaced.The live boot checks in #21 (section C) are the ones that confirm this.