Guide

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:

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:

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.

Replayable Ze terminal lab

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.

Reproducible
Ze 26.08.311 minute 48 secondsLinux namespace labTerminalZe recorder Plain-text transcript
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.