#!/bin/bash
# vrrp_gap_watch.sh — misst Luecken im VRRP-Advertisement-Strom.
#
# Warum nicht vm_timegap_watch.sh reicht: der misst Zeitkontinuitaet und sieht damit genau
# das, was bei einer Live-Migration NICHT unterbrochen wird — die Rechenzeit. Am 2026-09-02
# haben zwei Migrationen das VRRP kippen lassen (10:02:45 und 15:28:30 im keepalived-Log
# von rt-1), ohne dass der Freeze-Detektor ausschlug. Verloren gingen Pakete, nicht Sekunden.
#
# Gemessen wird der Abstand aufeinanderfolgender Advertisements anhand der Zeitstempel, die
# der KERNEL beim Empfang setzt (tcpdump -tt). Das ist der entscheidende Unterschied zu einer
# Messschleife: wird dieser Prozess selbst kurz nicht eingeplant, liest er die gepufferten
# Pakete verspaetet, die Abstaende bleiben aber korrekt. Ein angehaltener oder ausgehungerter
# Prozess kann hier keinen Ausfall vortaeuschen — genau der Fehlalarm, der am 2026-09-02
# beim Freeze-Detektor entstand.
#
# Entscheidungsschwelle, aus der keepalived-Konfiguration hergeleitet:
#   master_down = 3 x advert_int + (256 - prio)/256 = 3 x 1 s + 156/256 = 3,61 s
# Alles darunter ist Reserve, alles darueber ein Failover.
#
# Nur der MASTER sendet Advertisements. Auf dem Master ist deshalb der TX-Strom belegt und
# RX leer, auf dem Backup umgekehrt. Beide Richtungen werden gemessen; welche vorhanden ist,
# ergibt sich aus der Rolle. Der Vergleich beider Hosts trennt die Ursache:
#   rt-2 RX-Luecke + rt-1 TX-Luecke gleichzeitig -> rt-1 stand
#   rt-2 RX-Luecke, rt-1 TX lueckenlos           -> Strecke oder rt-2
set -u

STATE=${STATE:-/var/lib/vrrp-gap}   # ueberschreibbar, damit die Logik testbar bleibt
CONF=${CONF:-/etc/default/vrrp-gap}
# shellcheck source=/dev/null
[ -r "$CONF" ] && . "$CONF"

IFACE=${IFACE:-eth1}
SELF=${SELF:-}
PEER=${PEER:-}
THRESH_MS=${THRESH_MS:-2000}      # ab hier fehlt mindestens ein Advertisement
MAX_GAP_MS=${MAX_GAP_MS:-60000}   # darueber: Strom endete (Rollenwechsel), kein Stoerungsfall
MAX_EVENTS=500

[ -n "$SELF" ] && [ -n "$PEER" ] || {
    echo "vrrp_gap_watch: SELF/PEER nicht gesetzt (erwartet in $CONF)" >&2; exit 1; }

mkdir -p "$STATE"
for f in rx_count tx_count; do [ -f "$STATE/$f" ] || echo 0 > "$STATE/$f"; done

# Laeuft schon eine Instanz? PID-Datei statt "pgrep -f": eine Kommandozeilensuche traefe
# auch fremde Prozesse, die den Skriptnamen nur erwaehnen (etwa die aufrufende Shell oder
# ein ssh-Kommando). Genau daran ist der Freeze-Detektor beim Bau zweimal gescheitert.
#
# Die PID-Datei wird aber nur respektiert, wenn dahinter WIRKLICH DIESES Binary steckt --
# Pfadvergleich, nicht Namensvergleich. Grund (2026-09-22): auf nfs-1 und nfs-2 lief seit
# dem 4. September ein von Hand gestarteter Waechter aus /usr/local/bin weiter, dessen
# Dateien es laengst nicht mehr gab. Er hielt /var/lib/vrrp-gap/pid, und weil der alte
# Vergleich nur nach dem Namen "vrrp_gap_watch" suchte, hat sich die Paket-Unit seit 0.1.9
# bei JEDEM Start sofort wieder beendet: der Dienst galt als laufend, gemessen hat nie
# jemand. Ein fremder Prozess darf diese Unit nicht blockieren -- also uebernehmen.
_self=$(readlink -f "$0" 2>/dev/null || echo "$0")
if [ -r "$STATE/pid" ]; then
    p=$(cat "$STATE/pid" 2>/dev/null)
    if [ -n "$p" ] && [ "$p" != "$$" ] && [ -r "/proc/$p/cmdline" ]; then
        _same=0
        while IFS= read -r _f; do
            if [ "$_f" = "$_self" ]; then _same=1; break; fi
        done <<EOF
$(tr '\0' '\n' < "/proc/$p/cmdline" 2>/dev/null)
EOF
        if [ "$_same" = 1 ]; then
            echo "vrrp_gap_watch: laeuft bereits als PID $p" >&2
            exit 0
        fi
        echo "vrrp_gap_watch: PID-Datei zeigt auf PID $p, und das ist nicht ${_self} -- uebernehme" >&2
    fi
fi
echo $$ > "$STATE/pid"
# Beim Aufraeumen nur DIE EIGENE Datei loeschen.
#
# Ein blindes "rm -f $STATE/pid" reisst die Datei des NACHFOLGERS mit: beim
# Rollout am 22.09. hat der beendete Hand-Waechter genau das getan, nachdem die
# Paket-Unit ihre PID schon geschrieben hatte. Auf nfs-1 und nfs-2 lief der
# Waechter danach acht Stunden ohne pid-Datei (Zustandsdateien aktuell,
# s6-svstat "up 29549 seconds"), und vrrp.gap[watcher] meldete 0 -- ein
# Falschalarm, der Tage steht und Trigger unglaubwuerdig macht.
trap 'if [ "$(cat "$STATE/pid" 2>/dev/null)" = "$$" ]; then rm -f "$STATE/pid"; fi' EXIT

record() {   # $1=richtung(rx|tx)  $2=luecke_ms  $3=zeitpunkt_s
    local dir=$1 gap=$2 ts=$3
    echo "$ts $dir $gap" >> "$STATE/events"
    echo "$(( $(cat "$STATE/${dir}_count" 2>/dev/null || echo 0) + 1 ))" > "$STATE/${dir}_count"
    echo "$ts"  > "$STATE/${dir}_last_ts"
    echo "$gap" > "$STATE/${dir}_last_ms"
    if [ "$(wc -l < "$STATE/events")" -gt "$MAX_EVENTS" ]; then
        tail -n $(( MAX_EVENTS / 2 )) "$STATE/events" > "$STATE/events.tmp" \
            && mv "$STATE/events.tmp" "$STATE/events"
    fi
}

declare -A last=()

# Ohne "-p" laesst tcpdump das Interface im Normalmodus; --immediate-mode verhindert, dass
# Pakete im Kernelpuffer liegenbleiben. Der BPF-Filter haelt fremde VRRP-Instanzen drausssen
# (kube-router faehrt vrid 254 auf eth3, der Provider vrid 1 auf eth0).
while read -r ts _ src _; do
    case "$ts" in ''|*[!0-9.]*) continue ;; esac
    case "$ts" in *.*) : ;; *) continue ;; esac

    # Sekunden.Mikrosekunden -> Millisekunden, ganzzahlig (kein bc noetig)
    ms=$(( ${ts%.*} * 1000 + 10#${ts#*.} / 1000 ))
    src=${src%:}

    case "$src" in
        "$SELF") dir=tx ;;
        "$PEER") dir=rx ;;
        *)       continue ;;
    esac

    prev=${last[$dir]:-0}
    last[$dir]=$ms
    [ "$prev" = 0 ] && continue

    gap=$(( ms - prev ))
    [ "$gap" -le "$THRESH_MS" ] && continue
    [ "$gap" -ge "$MAX_GAP_MS" ] && continue

    record "$dir" "$gap" "$(( ms / 1000 ))"
done < <(exec tcpdump -i "$IFACE" -nn -tt -l --immediate-mode \
              "proto 112 and (src host $SELF or src host $PEER)" 2>/dev/null)
