VRRP
VRRP gives a LAN a gateway address that survives the loss of the router holding it. Two or more routers share a virtual IP; one is Active (Master) and answers for it, the others wait. If the Active router dies, a Backup takes the address over, and the hosts on the segment never change their default gateway.
Ze implements RFC 9568 (VRRPv3, IPv4 and IPv6) by default, and RFC 3768 (VRRPv2, IPv4 only) as a per-group opt-in for talking to older equipment.
Status: Experimental. The implementation is complete, its wire format is asserted against golden frames under QEMU, and it interoperates with keepalived 2.3.1 across election, node-death failover, and graceful-stop scenarios (including virtual-MAC ownership of the virtual IP), but it has not yet been proven in production. See RFC status.
The Docker interop lab runs the vrrp-mastership-keepalived scenario
(./le integration interop). Ze at priority 200 and keepalived 2.3.1 at 100 contend
for one virtual IP on a shared segment, VRID 10, VRRPv3 pinned on both sides
(keepalived speaks v2 by default, and RFC 9568 Section 7.1 has a v3 router discard
a v2 advertisement, so the two would never see each other). Every assertion reads
which container owns the address with ip -o -f inet addr inside that container,
never from ze's own output.
| Phase | What it asserts |
|---|---|
| 1 | Ze holds the virtual IP alone, continuously, for longer than keepalived's Active_Down_Interval. Held over a window and not read at an instant: a build whose SendAdvert returned without sending passed the instantaneous form. |
| 2 | Ze gets SIGTERM and keepalived takes the address over inside its own Active_Down_Interval (3 x 1s + Skew_Time = 3.61s at priority 100). Only ze's RFC 9568 Section 6.4.3 shutdown Priority 0 advertisement collapses that wait, so the bound is an assertion about ze rather than about a timeout. |
| 3 | Ze returns, preempts, and keepalived releases. The election runs again rather than latching. |
Split brain is its own assertion: two owners is what a scenario watching only its own side would call a pass.
The IPv6 unsolicited Neighbor Advertisement burst on promotion is resolved on the caller's goroutine, because netlink sockets are created in the calling thread's network namespace and a lazy resolution on the announcer worker saw the wrong one. A compile-time check pins that path: a type assertion cannot report a break, so the break is a build error instead.
Configuration
A VRRP group lives under the interface unit's address family, next to the addresses it backs:
interface {
backend netlink;
ethernet eth0 {
unit 0 {
ipv4 {
address [ 192.0.2.251/24 ];
vrrp {
group uplink {
vrid 10;
virtual-address [ 192.0.2.1 ];
priority 200;
}
}
}
}
}
}
group uplink is a name you choose, for your own readability: it names the
group in show vrrp output, in log lines, and in the group metric label. It
carries no protocol meaning. The protocol identity is vrid, which is
mandatory, must be 1..255, and must match on every router in the group.
The two routers in a group differ only in priority. Give the router you want
to win the higher number.
| Leaf | Default | Meaning |
|---|---|---|
vrid |
none (mandatory) | Virtual Router ID, 1..255. The same value on every router in the group. |
virtual-address |
none (at least one) | The address(es) the group backs. A leaf-list: [ 192.0.2.1 192.0.2.2 ]. |
priority |
100 | 1..254. Highest wins. 255 is reserved for the address owner and is assigned by ze, never by you. |
preempt |
true | Whether a higher-priority router takes mastership back when it returns. |
preempt-delay-seconds |
0 | 0..3600. How long a returning higher-priority router waits before preempting. Useful when a router's uplinks converge more slowly than it boots, so it does not take the gateway back before it can forward. |
accept-mode |
false | VRRPv3 only. Whether a non-owner Active accepts packets addressed to the virtual IP. False is the RFC default and means a ping to the virtual IP gets no reply from a non-owner. Forwarding, ARP and Neighbor Discovery are unaffected. |
advertise-interval-milliseconds |
1000 | How often the Active router advertises. |
version |
3 | 2 opts this group into VRRPv2. |
track |
absent | Interfaces whose loss lowers the priority this group advertises. See Tracking an interface. |
Tracking an interface
A group can watch other interfaces and lower the priority it advertises while one of them is down. That is how a router hands the gateway over when an uplink fails, rather than only when VRRP itself stops.
vrrp {
group uplink {
vrid 10;
virtual-address [ 192.0.2.1 ];
priority 200;
track {
interface eth1 {
priority-decrement 150;
}
}
}
}
While eth1 is down this router advertises 50 instead of 200, so a Backup
configured at 100 takes the gateway. When eth1 returns, the router advertises
200 again and takes it back (subject to preempt).
| Leaf | Default | Meaning |
|---|---|---|
interface <name> |
none | The interface to watch. Up to 16 for one group. The name is an interface from the interface tree, or a kernel device name. |
priority-decrement |
none (mandatory) | 1..254. The priority subtracted while this interface is down. |
Four rules decide what tracking does:
- Decrements add up. Two tracked interfaces down cost the sum of their decrements.
- The result never falls below 1. RFC 9568 Section 5.2.4 keeps a Backup router in 1..254, and 0 says the Active router stopped participating, so a decrement at or past the configured priority leaves this router at 1.
- A name Ze cannot resolve counts as DOWN. An uplink Ze cannot find is not carrying traffic. Ze logs the resolver error, so a typo shows up in the log rather than in an unexplained priority.
- Tracking is refused on the address owner. That router advertises 255 (RFC 9568 Section 5.2.4), so no decrement could take effect. Ze rejects the configuration rather than accepting it and ignoring it.
Ze reads the tracked interface's OPERATIONAL state and nothing else. An
interface that is up while it blackholes traffic still counts as up. show vrrp
reports effective-priority beside the configured priority, and lists the
tracked interfaces that are down now under tracked-down.
Ze tracks an interface. It does not track a route or run a health-check script: both need machinery Ze does not have, and neither is configurable.
Choosing a version
VRRPv3 is the default and is what you want unless something else on the segment
cannot speak it. Set version 2 per group for legacy peers:
vrrp {
group legacy {
vrid 20;
version 2;
virtual-address [ 192.0.2.5 ];
advertise-interval-milliseconds 2000;
}
}
VRRPv2 is IPv4-only and has no accept-mode; ze rejects a config that combines
them rather than ignoring the leaf. The two versions also encode the advertise
interval differently, which constrains what you can ask for:
| Version | Interval encoding | Valid advertise-interval-milliseconds |
|---|---|---|
| 3 | centiseconds | any multiple of 10 |
| 2 | whole seconds | any multiple of 1000 |
Ze refuses an interval it cannot put on the wire instead of silently rounding it, because a rounded interval is a timing mismatch you would only discover as a flapping election.
RFC 3768's authentication types are deliberately not implemented: RFC 9568 Section 9 removed them, as they protect against no real attack.
IPv6
An IPv6 group's first virtual address must be the link-local one, because RFC 9568 Section 5.2.9 makes the first address the advert's source identity:
ipv6 {
vrrp {
group uplink-v6 {
vrid 10;
virtual-address [ fe80::1 2001:db8::1 ];
priority 200;
}
}
}
IPv4 and IPv6 groups are independent state machines even when they share a
vrid, so a group of each on one unit is normal and correct.
IPv6 needs none of the ARP-flux sysctls IPv4 needs: Neighbor Solicitation for
the virtual IP targets its solicited-node multicast group, which only the
virtual-MAC macvlan joins (the parent does not hold the virtual IP), so the
parent never competes to answer. Ze does disable Duplicate Address Detection on
that macvlan (accept_dad=0): a virtual IP lives on one router at a time, so DAD
would only add a tentative window during which the address is unreachable right
after a promotion. IPv6 interoperability is verified against keepalived under
QEMU (election, node-death failover, and virtual-MAC resolution of the virtual
IP).
The address owner
If a group's virtual address equals an address configured on the unit, that
router owns the address and ze assigns it priority 255 automatically
(RFC 9568 Section 5.2.4). The owner always wins. Do not set priority 255
yourself; ze rejects it, because the owner is derived from the addresses, and a
hand-set 255 on a non-owner would claim an authority the router does not have.
The virtual MAC
Each group gets its own macvlan device carrying the RFC virtual MAC:
| Family | Virtual MAC |
|---|---|
| IPv4 | 00:00:5e:00:01:{vrid} |
| IPv6 | 00:00:5e:00:02:{vrid} |
This is why failover works without the hosts noticing. The virtual MAC moves with the virtual IP, so a host's ARP or neighbour entry for the gateway stays valid: the address it points at simply arrives from a different router. Ze additionally sends gratuitous ARP (IPv4) or unsolicited Neighbor Advertisements (IPv6) on promotion so switches relearn the port immediately.
The macvlan is created and destroyed with the group. It belongs to the plugin, not to your config, and you do not declare it.
It hangs off the device the interface's hardware selector names. If you pin an
interface to a NIC with mac/match, or alias it with os-name, the virtual
router lives on THAT NIC, and the kernel name it currently wears does not
matter. Move the card to another slot, let the kernel rename it, and the group
follows on the next commit. A group on a VLAN-tagged unit lives on the tag of
the selected device, not on the tag of the name you chose.
The device label on ze's VRRP metrics and state-change events names that same
resolved device. On an interface with no selector nothing changes, because the
logical name and the kernel name are the same string. On a selected interface
the label carries the NIC's kernel name rather than the name you chose, so a
dashboard or alert keyed on the old value follows it.
A selector that names no present device, or that more than one device answers, runs no virtual router at all. Ze does not fall back to a device that merely shares the interface's name: that device is not the link you asked to protect. The group starts when the selector answers exactly one device, and the daemon logs the reason while it does not. That log line says whether the group will retry: a group that never started waits for the next configuration apply.
A group that is ALREADY running is never stopped by a resolution failure. Ze cannot tell "this NIC is gone" from "I could not read the interface list this second", and treating the second as the first would fail a working master over for a transient error. A running group keeps the device it is on, and the ordinary liveness check decides whether it advertises there: if the NIC really did go away, the group stands down and comes back by itself when the NIC returns, with no commit from you.
A host on the segment that resolves the virtual IP gets the virtual MAC, not
the router's real MAC, so failover is transparent at L2: the address moves to a
different router without the host's ARP or neighbour entry changing. Achieving
this on Linux, where the macvlan's parent holds a real address in the same
subnet, requires the macvlan to be the sole ARP responder for the virtual IP.
Ze arranges that automatically when it creates the macvlan (you configure
nothing): the device is created in macvlan private mode, the virtual IP is
installed with the parent's subnet prefix, and a small set of arp_ignore,
arp_filter, and rp_filter sysctls stop the parent answering for the virtual
IP while letting the macvlan answer with the virtual MAC. This mirrors what
keepalived's use_vmac does, and ze restores the sysctls it changed when the
last group on an interface goes away. This behaviour is verified against
keepalived under QEMU (ze-test's VRRP interop lab).
A few consequences of this mechanism are worth knowing:
- Ze owns the parent's
arp_ignore,arp_filter, andrp_filterwhile a group is active. It re-asserts them on every config apply, so if you also set those knobs on the same interface unit, ze's values win for as long as VRRP runs there. Do not rely on a conflicting manual setting on a VRRP interface. net.ipv4.conf.all.rp_filteris set to 0 (host-wide) while any IPv4 group is active, because a per-devicerp_filtercannot go below theallvalue. Ze restores the previous value when the last group is removed, but a hard kill (SIGKILL, power loss) skips that cleanup and leaves it at 0. This matches keepalived, which never restores it.- The address owner is a special case. When the virtual IP equals a real address on the unit, that address already lives on the parent, so it keeps answering with the parent's real MAC (ze installs it as a host route on the macvlan to avoid a duplicate subnet route). Virtual-MAC ownership does not apply to the owner, which never fails the address over anyway.
- First-resolution race (IPv4 only). The very first host to ARP for the
virtual IP right as a router becomes Active can, for one resolution, cache the
parent's real MAC before the macvlan takes over; it converges to the virtual
MAC on its next resolution. keepalived's
use_vmacbehaves identically. IPv6 has no such race (Neighbor Solicitation is not a broadcast).
Operating
| Command | Shows |
|---|---|
show vrrp |
Every group: state, priority, vrid, virtual addresses. |
show vrrp interface name eth0 |
Only the groups on one interface. |
show vrrp statistics |
Per-group counters, including rejected packets and why. |
clear vrrp statistics |
Resets those counters. |
State changes are logged as vrrp: state change with from, to, and a
reason, so a failover leaves a record of what triggered it.
Keep the gateway reachable while Ze stops
Inspect the active and live VRRP state, stop the higher-priority Ze router, and prove keepalived takes the same reachable VIP.
Read the demonstration transcript
An operator needs to stop the active router without changing the default gateway on every host.
$ ze config show demos/terminal/vrrp-failover/ze.conf interface ethernet eth0 unit 0 ipv4 vrrp group gateway
The daemon configuration shows VRID 10, VIP 192.0.2.1, priority 200, and the advertisement interval.
$ grep -E 'interface|virtual_router_id|priority|192.0.2.1' /src/demos/terminal/vrrp-failover/keepalived.conf
The peer is configured as BACKUP on the same interface, VRID, and VIP with a lower priority of 100.
$ ze cli -c 'show vrrp' | ze pipe yaml
The complete live state shows Ze is master.
$ ip -n vrrp-ze -o addr show | grep 192.0.2.1 | tr -s ' ' | cut -d' ' -f2,4
The kernel shows the VIP on Ze's RFC virtual-MAC interface.
$ ze-demo run vrrp-failover proof-show
$ ze-demo run vrrp-failover proof
The recording runs the compiled proof that stops Ze, removes its namespace, inspects the VIP on keepalived, and sends two probes.
The final kernel output shows 192.0.2.1 on keepalived's `vrrp.10` interface, and both probes succeed after failover.
For Prometheus:
| Metric | Meaning |
|---|---|
ze_vrrp_state{device,group,vrid,family} |
Current state of each group. |
ze_vrrp_transitions_total |
State transitions, for spotting a flapping group. |
Requirements and limits
A ping to the virtual IP gets no reply unless you ask for one. RFC 9568
Section 6.4.3 says a non-owner Active router must not accept packets addressed to
the virtual IP, and accept-mode defaults to false, so ze installs a drop rule
for the virtual addresses while the group is Active. Forwarding is unaffected:
hosts using the virtual IP as their default gateway keep working, the router
still answers ARP and Neighbor Discovery for it, and IPv6 Neighbor Solicitations
and Advertisements are never dropped. Set accept-mode true if you monitor the
gateway by pinging its virtual IP. The router that owns the address as a real
interface address accepts on it whatever the leaf says. The rules appear in
show firewall ruleset under the vrrp table.
Tracking watches an interface, not a route or a script. track interface
lowers the advertised priority while a watched interface is down, which is the
failover Junos, Nokia and VyOS drive the same way. Route tracking and
health-check-script tracking are not implemented: a tracked route needs a watch
keyed on a prefix, and a health check needs a script runner with its own timers
and security surface. Apart from tracking, failover is driven by VRRP's own
triggers: loss of the Active router's adverts, carrier loss on the VRRP
interface, or a graceful stop.
Netlink backend only. VRRP needs macvlan devices and raw sockets, which the VPP backend does not provide. A VPP-backed config carrying a VRRP group is rejected at validation rather than started in a degraded state.
Raw socket privileges. Ze needs CAP_NET_RAW and CAP_NET_ADMIN to send
adverts (IP protocol 112) and to manage the macvlan.
Multicast must reach the segment. Adverts go to 224.0.0.18 (IPv4) or
ff02::12 (IPv6) with TTL/hop-limit 255, and RFC 9568 requires receivers to
discard anything arriving with a lower TTL. A device that rewrites the TTL or
filters link-local multicast will break the election.
Diagnostics
ze doctor runs the same verifier a commit runs, so the two can never disagree.
| Code | Meaning |
|---|---|
doctor-vrrp-config-invalid |
A cross-leaf rule was broken (for example accept-mode with version 2, or track on the address-owner group). |
doctor-vrrp-backend-unusable |
The tree configures VRRP on a backend that cannot run it. |
doctor-vrrp-raw-socket |
The raw socket VRRP needs cannot be opened, usually missing CAP_NET_RAW. |
Run ze explain <code> for the remediation.