#!/bin/sh
# VRRP health check: may THIS router hold the floating IPs?
#
# It may only if it can actually carry the WAN. Without this, VRRP decides
# mastership purely on whether the peer is still advertising -- so a router with
# no WAN at all happily takes the VIPs and blackholes the entire LAN's internet
# while looking perfectly healthy. That is not hypothetical: it is the outage of
# 2026-09-02, reproduced in labsim.
#
# ---------------------------------------------------------------------------
# The first version of this script asked one question: "do I have an address on
# a WAN interface". That is correct for a pair where both routers hold WAN all
# the time. Ours cannot: the 10 gig lease is bound to a cloned MAC and the
# PPPoE line to a single credential, so the WAN follows mastership (see
# vrrp-wan-take). Against that design the old check DEADLOCKS --
#
#     may I be master?  ->  only if I already have WAN
#     do I have WAN?    ->  only if I am master
#
# -- and the backup sits in FAULT for ever. vyos002 sat exactly there, which
# meant the pair could not fail over at all: the safety check had quietly
# removed the redundancy it was protecting.
#
# So the question is now asked in the right order: enforce "must have WAN" only
# on the router that is actually HOLDING the VIPs, and give a new master time to
# bring the WAN up before judging it.
# ---------------------------------------------------------------------------
#
# exit 0 = eligible for MASTER, non-zero = release and let the peer have it.

CONF=/config/vrrp-wan.conf
[ -r "$CONF" ] && . "$CONF"
STATE=/run/vrrp-wan
VIP="${VRRP_WAN_VIP:-192.168.1.1}"

# Seconds a new master may go without any WAN. Sourced from vrrp-wan.conf; the
# fallback is deliberately NOT the old 90. accel-ppp's dead-peer budget is
# lcp-echo-interval(30) x lcp-echo-failure(3) = 90s, so a hard failover into an
# access concentrator that does not replace the stale session lands exactly on
# the boundary: the new master fails its own check, sheds the VIPs, and the peer
# -- in the same position -- does likewise. Both end in FAULT, which is worse
# than the outage this check exists to prevent.
GRACE="${GRACE:-180}"

# A deliberate hand-over lever.
#
# There is no reliable way to MAKE this pair fail over on demand. VyOS offers
# only `restart vrrp`, and neither that nor `systemctl restart keepalived` is
# dependable: with advert_int 1 the peer declares the master dead after ~3.6s,
# and a restart usually finishes inside that window. Measured in labsim -- the
# same command moved mastership on one run and not on the next three. A
# fail-back procedure you cannot trigger on purpose is not a procedure.
#
# Failing the health check IS the supported way to shed mastership: the sync
# group goes FAULT, releases every VIP, and the peer takes over -- the same path
# a genuine WAN loss takes, so the planned drill exercises the real mechanism
# rather than a special case.
#
#   touch /run/vrrp-wan/force-fault    hand over within failure-count*interval
#   rm    /run/vrrp-wan/force-fault    become eligible again (no-preempt keeps
#                                      it BACKUP until the peer hands back)
#
# It lives in /run deliberately: a reboot clears it, so a forgotten drill cannot
# leave a router permanently ineligible.
[ -f /run/vrrp-wan/force-fault ] && exit 1

# Am I holding the VIPs? Asked of REALITY -- is the management VIP actually on
# this box -- and not of a /run marker.
#
# The marker was the first design and it is unsafe: it is written by the VRRP
# transition script, and in labsim that script silently failed to run on a
# promotion (VyOS's keepalived-fifo.py helper stopped delivering while
# keepalived's own notifies kept working). The router then believed it was
# backup, passed this check, and sat holding every VIP with no WAN -- the exact
# outage this script exists to prevent, re-created by trusting the reporter
# instead of the fact.
if [ -z "$(ip -4 -o addr show 2>/dev/null | grep " ${VIP}/")" ]; then
    # Clear the grace stamp on the way down, HERE, not only in the reconciler.
    # The reconciler runs every 30s; this runs every 5s. A promotion that
    # inherited a stamp from an earlier mastership scored grace = hours, failed
    # immediately, and took the sync group to FAULT ~5s after passing -- with the
    # peer already faulted, that left BOTH routers in FAULT and the LAN with no
    # gateway. The stamp must belong to the CURRENT mastership or it is worse
    # than useless.
    rm -f "$STATE/since" 2>/dev/null
    exit 0
fi

# Start the grace clock HERE, the moment mastership is first observed.
#
# It used to be stamped only by vrrp-wan-reconcile, which runs on a 30s timer --
# so a freshly promoted master reached this check with no stamp, scored grace=0,
# failed, and went FAULT before it had any chance to bring the WAN up. The peer
# then found itself alone with no WAN either and did the same. Observed in
# labsim: BOTH routers in FAULT, nobody holding the VIPs, the LAN with no
# gateway at all. That is worse than the outage this script exists to prevent,
# and it would have hit a REAL failover, not just a drill -- the health check
# runs every 5s and the reconciler had not yet ticked.
mkdir -p "$STATE" 2>/dev/null
[ -f "$STATE/since" ] || date +%s > "$STATE/since"

# Master with an address on a WAN interface: healthy.
#
# Deliberately NOT "can I reach the internet" and NOT "do I have a default
# route". During a real ISP outage the route disappears on BOTH routers; a check
# keyed on that would put both into FAULT, nobody would hold the VIPs, and the
# LAN would lose inter-VLAN routing too -- turning an internet outage into a
# total one. A DHCP lease survives an ISP outage, so an address still
# distinguishes "this box structurally cannot route" from "the internet is down
# right now", which is the distinction that matters.
# Any WAN counts. Requiring the 10 gig specifically would fault a healthy master
# during a genuine 10 gig outage and turn a degraded state into a total one --
# the same reasoning as the default-route note above. Which one satisfied it is
# recorded for the operator and the test harness, but does not affect the verdict.
for ifc in bond0.53 pppoe0; do
    if ip -4 addr show dev "$ifc" 2>/dev/null | grep -q 'inet '; then
        echo "$ifc" > "$STATE/wan" 2>/dev/null
        exit 0
    fi
done
rm -f "$STATE/wan" 2>/dev/null

# Master, no WAN yet, still within the grace window: DHCP negotiation and PPPoE
# dial-up take real time, and the ISP has to accept the cloned MAC arriving on a
# different port. Failing here would demote the new master before it ever had a
# chance, and hand the VIPs straight back -- a flap, not a failover.
since=$(cat "$STATE/since" 2>/dev/null || echo 0)
[ $(( $(date +%s) - since )) -lt "$GRACE" ] && exit 0

# Master, past grace, still no WAN: release. This is the 2026-09-02 case.
exit 1
