CARP failback (SEC→PRIME) breaks a long-running LAN ping; BACKUP node sources from LAN CARP VIPs #2
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#2
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?
Status: open — root cause not confirmed, but the case has split in two (see §3).
Affects: GARLIC-01 (PRIME) / GARLIC-02 (SEC), OPNsense 26.7.x
Probably not a
10-wancarpdefect — see §4.Migrated from the
ISSUE-carp-failback-lan-vip-arp.mdhandoff doc ondocs/carp-failback-investigation, which was only ever a workaround for not havingteaset up on the other machine. That branch can be deleted once this issue is up.1. Symptom
Continuous ping from a Windows client on the LAN (
192.168.0.x), across CARP failovers:Two load-bearing details, both confirmed:
8.8.8.8-class), not the gateway VIP.Separately, for the record: while SEC is BACKUP it has no internet of its own. That is correct and by design — this topology has a single ISP address, so the BACKUP node has no uplink. Rebooting SEC appears to "restore" its internet only because the boot window briefly brings WAN up before CARP demotes it again. Do not reboot SEC to get it online; use CARP maintenance mode on PRIME so SEC legitimately becomes MASTER.
2. Environment
bc:24:11), virtio NICs.10-wancarpinstalled at/usr/local/etc/rc.syshook.d/carp/10-wancarpon both. Survived the in-place upgrade.vtnet0vtnet1192.168.0.100:00:5e:00:01:01vtnet210.102.0.100:00:5e:00:01:02Script config:
$iface_aliases = ['wan'],$lan_vip = '192.168.0.1',$remove_backup_route = false.The ISP hands the same IPv4 address to both nodes (different MACs). This is why the script exists — only the MASTER may hold WAN up.
HA configuration (verified in GUI): PRIMARY has "Synchronize all states via" set to the Sync VLAN; SECONDARY has it Disabled. ⇒ pfsync state sync is currently inert. PRIMARY multicasts into the sync VLAN; SECONDARY neither sends nor listens.
3. Where the investigation actually stands
Two things are true at once and should not be conflated.
3a. There is a real defect on SEC — a BACKUP node sourcing from a CARP VIP
On PRIME, at exactly the two failbacks and nowhere else:
Both
bc:24:11:…MACs are confirmed to belong to GARLIC-02 (SEC) — one per internal VLAN. So both LAN CARP VIPs moved from their correct CARP virtual MAC to the physical MAC of the node that was demoting to BACKUP.FreeBSD emits
arp: … moved from … to …when it receives an ARP packet claiming a cached IP from a different MAC. That includes ARP requests — the sender-IP field of a request is the sender's own address. So:A BACKUP node must never emit anything sourced from a CARP VIP. This is wrong regardless of what it did or did not break here, and is worth fixing on its own merits. Correlation with the symptom: 2 of 2 on the broken direction, 0 on the working direction.
Likely culprits — anything that can source from a VIP: dpinger gateway monitoring, Unbound/dnsmasq bound to the VIP, a DHCP server, an mDNS/Avahi reflector, monit, or an NTP client.
3b. …but it is probably not what broke the ping
The ping target was external, so ping and browsing both left the client through the same gateway MAC. A poisoned neighbour entry would have killed both. Browsing survived. The ARP anomaly cannot explain the divergence.
What now carries the weight is that recovery was triggered by restarting the process, not by waiting. The network has no idea the ping was restarted — that is a per-flow, client-side signature:
ping -tholds an ICMP handle whose route/interface binding was resolved once, at start. Windows can wedge such a handle across a gateway transition and emitGeneral failure.indefinitely until the process is restarted.One mechanism, both observations — which the ARP story cannot do. This promotes the "Windows
pingquirk" from unlikely (where the original triage had it) to the leading hypothesis. Not yet confirmed; §5 settles it.A second, genuine but transient, factor is in the logs: the promoted node has no WAN address and no default route until DHCP completes:
That is a real outage window on every failback — but it is seconds long and hits browsing equally, so it explains the first few dropped packets, not a failure that persists until the ping is restarted.
4. Scope — this is probably not a
10-wancarpdefect10-wancarponly ever touches thewanalias (vtnet0). It never configures, resets or ARPs onvtnet1/vtnet2. The anomaly in §3a is on the LAN VIPs.The logs show the script behaving correctly on every single event: right enable/disable decisions, idempotency skips firing properly on the duplicate per-VIP CARP events, every
ENDbannerresult=ok.§3a most likely belongs to the class already flagged in the README's "HA note: CARP-aware services" section — a service on SEC that is not CARP-aware. Tracked here because this repo is where the HA behaviour is documented.
5. The decisive test — client-side, costs nothing
At the next failback, while the original ping is still dead, open a second terminal on the Windows client and start a fresh ping to the same external target:
Record at the same moment, in the same window:
General failure.⇒ client-side handle wedge;Request timed out⇒ packets are leaving and not coming back;Destination host unreachable⇒ no usable route/ARP.00-00-5e-00-01-01in the ARP output = correct. Abc-24-11-…MAC = the §3a poisoning did reach the client after all, which would overturn §3b.192.168.0.1) succeeds while the external one fails, the LAN path is fine and the problem is upstream of PRIME — i.e. the DHCP/default-route window, not ARP.Trigger the failback with CARP maintenance mode on PRIME (System → High Availability → Status), not a reboot — that isolates the CARP transition from boot-time noise.
6. Chasing §3a on its own
6a. Run on SEC (GARLIC-02) now — no failover required
Find anything bound to a VIP address rather than an interface address:
Check the usual CARP-unaware suspects are actually CARP-aware or disabled on BACKUP:
Start here — this pair has already been bitten by exactly this once (see
the comment below): Avahi ran on both firewalls reflecting VLAN1↔VLAN102 and
produced network-wide latency that was initially mistaken for a CARP storm.
It was fixed then by ticking that box. The pair has since been upgraded in
place to 26.7, so re-verify the setting survived. Two-minute GUI check, no
failover needed.
6b. Capture the next failback live
On PRIME, before triggering (leave running through the transition):
Looking for: an ARP from SEC's physical MAC with sender-IP = the VIP, and how long the LAN takes to relearn
00:00:5e:00:01:0x. Then identify the emitting process:Fix belongs in service configuration on SEC (make the offending service CARP-aware, or bind it to the interface address rather than the VIP) — not in
10-wancarp.7. Log timeline (2026-09-05)
Reconstructed from
grep -h wancarp /var/log/system/*.logon both nodes plusgrep -hi "moved from"on PRIME.changed=yeschanged=yes— ARP move loggedchanged=yeschanged=yesUPchanged=yes— ARP move loggedchanged=yesTwo secondary observations, both unexplained and both low priority:
UPagain one second later. Something outside the script raised WAN between the two events. Probably shutdown-time interface reconfiguration.8. 26.7 compatibility — verified, no action needed
Checked against the
stable/26.1 → stable/26.7core diff and confirmed with--dryrunon an upgraded node. Recorded so nobody re-does this work. Landed in5fb5793.src/etc/devd/carp.confunchanged (still matches[0-9]+@[0-9a-z_.]++MASTER|BACKUP|INIT),rc.syshookunchanged,actions_interface.conf[carp]action unchanged,rc.syshook.d/carp/still present.interface_configure()gained an optional 5th parameter ($batch = false) that defaults to 26.1 behaviour. The script's four-argument calls are unaffected.NetworkInterfacemodel is<mount>:memory:</mount>and reads/writes through to legacyconfig.xml<interfaces>.$configaliases and theenableflag are untouched.LOWER_UPis new in the flag output (bit 24,IFF_NETLINK_1, ifconfig's netlink path). It reports carrier, not admin state.iface_is_up()'s strictin_array('UP', ..., true)handles it correctly. Do not loosen that to a substring test — see the comment oniface_is_up().'reload' => false. Irrelevant for a physical or virtio WAN.Also discovered along the way:
interfacesis not an XMLRPC-syncable section, so the interfaceenableflag must be set manually on each node. The script header and README both claimed HA config sync carries it. Corrected in70078c7.9. Work items
docs/carp-failback-investigationbranch and itsISSUE-…mdfile once this issue is up. Done — branch deleted locally and on origin atb2e6219; its only unique content was the handoff doc, now superseded by this issue.10. Hypotheses already eliminated
Recorded so they are not re-litigated.
moved fromonvtnet0; no "using my IP" on either node; the one real dual-WAN window is in the working direction.Explicitly deferred — do not implement without new evidence
A proposed change to treat
INITas "not MASTER" (down the interfaces) plus arc.syshook.d/start/boot-time guard. This was designed for a duplicate-WAN-IP boot window hypothesis that the logs disproved (see the table above). TheINITno-op may still be a latent gap, but it is not this bug. Do not implement it as a fix for this issue.Operational context carried over (machine-local notes → issue)
Claude Code's memory is per-machine — it lives in a local
~/.claude/…directory keyed by the repo's absolute path, and does not sync between computers. Work continues on a different PC tomorrow, so the durable parts are transcribed here. This comment is the carry-over; nothing below needs to be rediscovered.Node identities
WAN is
vtnet0, virtio, DHCP, with a software-set shared MAC on both boxes — set in OPNsense, not duplicated on the Proxmox VM NIC hardware. That detail matters if WAN addressing is ever re-examined: the ISP sees one MAC, which is how both nodes get handed the same IPv4 address.Triggering failover safely — read before running §5
Force a transition with Persistent CARP Maintenance Mode (System → High Availability → Status): Enter it on the box you want demoted, then Leave it on that same box. Not a reboot — that keeps the CARP transition clean of boot-time noise, which is what §5 needs.
Persistent maintenance is
sysctl net.inet.carp.demotion= 240.The sysctl is additive on write. So if the GUI ever loses its Leave button and a node is stuck demoted, clear it with:
This has been needed before. Worth knowing before deliberately demoting a node on a firewall you are not physically next to.
10-wancarp --dryrunis safe on a live firewall and makes no changes:CARP passes its arguments as
<vhid>@<interface>(e.g.1@vtnet1) — not the<address>@<vhid>that older documentation claimed.Why Avahi is the prime suspect for §3a, not just one of five
This pair has already had one CARP-unaware-service incident, and it was the mDNS Repeater. Avahi ran on both firewalls reflecting VLAN1↔VLAN102, forming an mDNS (
224.0.0.251) reflection loop that caused network-wide latency and packet loss — initially misdiagnosed as a CARP storm. The fix was the repeater's "Enable CARP Failover" checkbox, which restricts it to the MASTER.That is exactly the shape of §3a: a service on the BACKUP node emitting traffic it has no business emitting on a LAN VIP. The pair has since been upgraded in place 26.1 → 26.7, so the setting is worth re-verifying before anything more elaborate. §6a has been updated to put this first.
General rule established for this pair: any multicast reflector, or any service that must not run on both nodes simultaneously, has to be either CARP-aware or pinned to a single node.
Corroborating an item already in the issue
Boot behaviour is resolved, not open: hard reboots of both the MASTER and the BACKUP were tested and each hands off and reclaims cleanly, because CARP fires
MASTER/BACKUPevents at boot and the runtime-only hook follows them. This independently supports the "Explicitly deferred" note at the bottom of the issue — theINITdefault-deny change is not needed, and should not be implemented as a fix for this bug.Repo state as of this comment
mainis at70078c7, pushed. It carries the config-sync correction (interfacesis not an XMLRPC-syncable section, so the per-interfaceenableflag must be set manually on each node).docs/carp-failback-investigationbranch and itsISSUE-…mdhandoff file have been deleted — this issue supersedes them. Do not go looking for that file tomorrow.Closing — not a bug in this script. Root cause was a misconfiguration on my side.
System → High Availability → Settings → "Synchronize all states via" was configured on the PRIMARY but left Disabled on the SECONDARY. That setting is per-node and is not carried by XMLRPC config sync, so pfsync state synchronisation was inert in both directions even though the primary looked correctly configured.
Setting it to the dedicated CARP sync VLAN on the secondary and re-running the reboot test gives a clean result in both directions: 1 packet lost on failover, 1 on failback, with no other disruption.
The LAN CARP VIP ARP moves noted during the investigation were transition noise, not the cause.
10-wancarpbehaved correctly throughout — every CARP event was handled with the right enable/disable decision, and the idempotency guards worked as intended.26.7 / PHP 8.5 / FreeBSD 15.1 compatibility was verified separately and is documented in the README.
The gap is in the official OPNsense HA documentation, not in this script. No code change required.