The Problem with PPPoE and Active/Passive Clusters
PPPoE isn’t like a regular WAN connection — only one device can hold the authenticated session at a time. In a standard OPNsense CARP cluster, both nodes share a virtual IP and the master handles all traffic. That model works beautifully for regular interfaces, but PPPoE introduces an extra constraint: whichever node is passive needs to actively drop its PPPoE session, not just stop using it. If both nodes try to hold a session simultaneously, you’ll hit authentication conflicts, stale connections, and reconnection failures after failover.
The good news is that OPNsense has had the plumbing for this since version 24.1. A hook script at /usr/local/etc/rc.syshook.d/carp/20-ppp watches CARP state transitions and tears down the PPPoE session on any node that transitions to backup. The bad news is that wiring it all up correctly requires understanding a subtlety that the official documentation glosses over entirely — which interface the CARP address actually lives on.
Understanding the Interface Stack
Before touching any settings, get this mental model clear. Your WAN connectivity flows through three distinct layers:
1. The physical interface (igb0 or similar) — this is the raw Ethernet port with a cable running to your ONT or the switch port bridging your cluster to the ONT. It carries no IP address. It does nothing except move frames.
2. The VLAN interface (igb0_vlan911 in our case) — most ISPs that use PPPoE also require a VLAN tag on the WAN link. VLAN 911 is common, though it varies by provider. This tagged sub-interface sits on top of the physical port and is where things start to get real: it needs its own IP address, and crucially, it’s where your CARP virtual IP must live.
3. The PPPoE interface — OPNsense’s WAN interface, configured to dial out over the VLAN interface above. This is where your ISP credentials go, and it’s what ultimately gets you a public IP and internet access.
The mistake that catches most people — and that several guides get wrong — is placing the static IP and CARP address on the physical interface rather than the VLAN interface. The CARP hook scripts need to find a CARP address on the same interface that PPPoE is running over. If that interface is your VLAN, that’s where CARP must be. Putting it on the physical port instead will leave you with failover that doesn’t work, or worse, appears to work in testing but fails silently in production.
Configuration Walkthrough
Step 1 — Assign the Physical Interface
In Interfaces → Assignments, add the raw Ethernet port (e.g. igb0) as a new interface. A name like WAN_Port is descriptive enough. Don’t assign an IP address here — this interface is purely a parent device for the VLAN you’re about to create.
Step 2 — Create the VLAN Interface
Head to Interfaces → Devices → VLAN and add a new VLAN:
- Parent device: your physical WAN port (e.g.
igb0) - VLAN tag: whatever your ISP requires — in our setup, that’s 911
- Description: something readable, like
WAN_VLAN911
OPNsense will create a logical interface named igb0_vlan911. Every subsequent step references this interface, not the physical one.
Step 3 — Assign and Give the VLAN Interface an IP
Back in Interfaces → Assignments, assign igb0_vlan911 to a new interface — call it WAN_VLAN or similar.
Open that interface’s configuration page and set the IPv4 type to Static IPv4. Each firewall node gets its own address on this subnet:
- Master node: e.g.
192.168.18.2/24 - Backup node: e.g.
192.168.18.3/24
These are internal transfer network addresses — they only need to be reachable between your firewalls and the ONT, not from the internet.
Step 4 — Add the CARP Virtual IP on the VLAN Interface
This is the step that everything else depends on. Go to Interfaces → Virtual IPs → Settings and create a new CARP entry:
- Interface:
WAN_VLAN— your VLAN 911 interface. Not the physical port. - IP address: a third address on the same subnet, e.g.
192.168.18.10/24 - VHID group and password: identical on both nodes
The virtual IP address itself isn’t used by PPPoE at all — it’s not your dial-up address and it won’t appear in your ISP’s routing table. Its only job is to give the CARP subsystem something to monitor on the right interface. When this CARP address transitions to BACKUP, the hook script fires and disconnects the PPPoE session on that node. If CARP is watching the wrong interface, that chain reaction never happens.
Step 5 — Enable Dial-up Disconnect on Failover
In System → High Availability → Settings, enable Disconnect dialup interfaces.
This is the switch that activates the 20-ppp hook script. Without it, CARP will fail over your virtual IPs but leave the PPPoE session running on the backup node — which will cause chaos.
Step 6 — Configure the WAN Interface for PPPoE
Open Interfaces → WAN and set the following:
- IPv4 Configuration Type: PPPoE
- IPv6 Configuration Type: DHCPv6 (if your ISP provides IPv6)
- On OPNsense 24 and earlier: enter your ISP username and password under the PPPoE configuration section
- If using DHCPv6: enable
Use IPv4 connectivityand set your prefix delegation size to match what your ISP allocates
Save the page, then follow the link that appears: “Click here for additional PPP-specific configuration options.”
Step 7 — Point PPPoE at the VLAN Interface
The Point-to-Point Devices screen is where the other common mistake happens. Under Link interface(s), you must select igb0_vlan911 — the VLAN interface — not igb0.
If you select the physical interface, PPPoE frames will go out untagged and your ISP’s equipment will reject them. The session will never establish.
On OPNsense 25 and later, the username and password fields have moved to this screen rather than the WAN interface page — set them here instead.
Gotchas Worth Knowing About
The CARP INIT State Gap
On OPNsense versions before 25.7.8, the 20-ppp hook script only responds to CARP transitions into BACKUP — not into INIT. This matters because when you manually select Temporarily Disable CARP in the virtual IP status page, CARP enters INIT rather than BACKUP. The result is that your PPPoE session keeps running when you expect it to drop, which makes testing failover scenarios frustrating.
If you’re on an older version and need to test cleanly, a one-line patch to the script adds INIT handling. As of 25.7.8, this has been merged upstream and no patch is needed.