#!/bin/bash # Auto-revert the VLAN 2 IPv6 change if IPv4 stops working. # # The change it guards is additive and IPv6-only, so in theory it cannot affect # IPv4 at all. This exists because "in theory" is exactly what has been wrong # repeatedly, and because it is applied in an unattended window: the person who # would notice is away, and a router that has lost IPv4 takes the house and the # cluster with it. # # Modelled on wan-drill-watchdog: arm before the risky thing, disarm after, and # in between let the ROUTER decide for itself rather than depending on anything # off-box. A watchdog that needs the network it is protecting is not a watchdog. # # vlan2-v6-watchdog arm [seconds] start watching (default 2400 = 40 min) # vlan2-v6-watchdog disarm stop # vlan2-v6-watchdog status is it armed, and what has it seen # # Runs ON the router, out of /config, detached via setsid so it outlives the ssh # session that started it. set -uo pipefail STATE=/run/vlan2-v6-watchdog PIDF=$STATE/pid LOGF=$STATE/log V6_PREFIX="${V6_PREFIX:-2001:470:187e:2}" # How long IPv4 must be continuously bad before reverting. Long enough that a # commit's own brief disruption, or one lost probe, does not trigger it; short # enough to matter. The reconciler ticks at 30s, so 90s is three chances. GRACE="${GRACE:-90}" PROBE="${PROBE:-9.9.9.9}" log() { mkdir -p "$STATE"; printf '%s %s\n' "$(date -Is)" "$*" >> "$LOGF"; } # IPv4 health, asked of the box itself -- and it MUST be role-aware. # # The first version required a default route plus internet on both routers, and # would have reverted on vyos002 within 90s of being armed. That box is the # gated BACKUP: by design it holds no VIP, has bond0.53 disabled and pppoe0 # down, so it has no default route and no internet, and that is the correct # resting state rather than a fault. Caught at 20s of the 90s grace. # # So: the master must be able to reach the internet. The backup only has to # still be on the network and able to see its peer -- which is what would # actually be at risk if a VLAN 2 change went wrong. VIP="${VIP:-192.168.1.1}" holds_vip() { ip -4 -o addr show 2>/dev/null | grep -q " ${VIP}/"; } v4_ok() { if holds_vip; then ip -4 route show default 2>/dev/null | grep -q . || return 1 ping -c1 -W2 "$PROBE" >/dev/null 2>&1 else # Backup: management address present and the VIP answers. If the VIP has # gone too then the pair has a bigger problem than this change, and # reverting an IPv6 addition would not help -- so this deliberately does # not fire on peer loss alone. ip -4 -o addr show bond0.1 2>/dev/null | grep -q 'inet ' || return 1 ping -c1 -W2 "$VIP" >/dev/null 2>&1 fi } revert() { log "REVERTING: IPv4 has been down for ${GRACE}s" # A script file, not vbash -c: the latter never starts a config session and # the commit fails to stderr where nobody sees it. cat > /tmp/vlan2-v6-revert.sh <<'REOF' #!/bin/vbash source /opt/vyatta/etc/functions/script-template configure delete service dhcpv6-server delete service router-advert interface bond0.2 commit save exit REOF chmod +x /tmp/vlan2-v6-revert.sh /tmp/vlan2-v6-revert.sh >> "$LOGF" 2>&1 # The interface address is deleted separately: it is the one node whose # removal could plausibly disturb something else, so it goes last and its # failure does not block the rest. for n in 1 2; do ip -6 addr del "${V6_PREFIX}::${n}/64" dev bond0.2 2>/dev/null done log "revert done; IPv4 now $(v4_ok && echo OK || echo STILL BAD)" } watch_loop() { local deadline=$(( $(date +%s) + $1 )) bad=0 log "armed for $1s (grace ${GRACE}s, probe ${PROBE})" while [ "$(date +%s)" -lt "$deadline" ]; do if v4_ok; then [ "$bad" -ne 0 ] && log "IPv4 recovered after ${bad}s" bad=0 else bad=$((bad + 10)) log "IPv4 bad (${bad}s/${GRACE}s)" if [ "$bad" -ge "$GRACE" ]; then revert log "disarming after revert" rm -f "$PIDF" return 0 fi fi sleep 10 done log "expired without incident" rm -f "$PIDF" } case "${1:-status}" in arm) mkdir -p "$STATE" [ -f "$PIDF" ] && kill -0 "$(cat "$PIDF")" 2>/dev/null && { echo "already armed"; exit 0; } setsid "$0" _run "${2:-2400}" >/dev/null 2>&1 < /dev/null & sleep 1 [ -f "$PIDF" ] && echo " armed (pid $(cat "$PIDF"), ${2:-2400}s)" || echo " FAILED to arm" ;; _run) echo $$ > "$PIDF" watch_loop "${2:-2400}" ;; disarm) if [ -f "$PIDF" ]; then kill "$(cat "$PIDF")" 2>/dev/null; rm -f "$PIDF"; echo " disarmed" else echo " not armed"; fi ;; status) if [ -f "$PIDF" ] && kill -0 "$(cat "$PIDF")" 2>/dev/null; then echo " armed (pid $(cat "$PIDF"))" else echo " not armed"; fi echo " --- log ---"; tail -12 "$LOGF" 2>/dev/null | sed 's/^/ /' || echo " (none)" ;; *) echo "usage: $0 {arm [seconds]|disarm|status}" >&2; exit 2 ;; esac