#!/bin/sh # Tunnel-Umschaltung fuer den Router in Arcen (angelegt 10.10.2026). # # Stufe A: Tunnel "fritzbox" (FRITZ!Box Raderbroich) = Normalbetrieb # Stufe B: Tunnel "pve" (WireGuard-Server in Proxmox) = wenn A nicht geht # Stufe C: direkt ins Internet ueber Huawei/Landal, ohne VPN = wenn weder A noch B gehen # Zurueck zu A bzw. B, sobald der Tunnel wieder stabil geht (6 Pruefungen in Folge, ca. 60 s). # # Geprueft wird je Tunnel per Ping durch genau diesen Tunnel (Policy-Routing nach Quelladresse), # unabhaengig davon, welche Stufe gerade aktiv ist: # fritzbox: 192.168.192.1 (FRITZ!Box) und 1.0.0.1 (Internet) mit Quelle 10.10.11.1 # pve: 10.10.20.1 (Server) und 1.0.0.1 (Internet) mit Quelle 10.10.20.2 # Aufruf mit DRY=1 schreibt nur ins Log und aendert keine Routen. WG1=fritzbox WG2=pve SRC1=10.10.11.1 SRC2=10.10.20.2 LAN_PROBE=192.168.192.1 NET_PROBE=1.0.0.1 SRV_PROBE=10.10.20.1 INTERVAL=10 FAIL_N=3 OK_N=6 PVE_PORT=51820 STATE=/tmp/tunnel-failover.state SAVED=/tmp/tunnel-failover.routes UPSTATE=/tmp/uplink-watchdog.state DRY=${DRY:-0} log() { logger -t tunnel-failover "$*"; echo "$(date +%H:%M:%S) $*"; } ping_via() { ping -c1 -W2 -I "$1" "$2" >/dev/null 2>&1; } ensure_rules() { ip rule | grep -q "from $SRC1 to $LAN_PROBE lookup 100" || ip rule add from $SRC1 to $LAN_PROBE lookup 100 priority 100 ip rule | grep -q "from $SRC1 to $NET_PROBE lookup 100" || ip rule add from $SRC1 to $NET_PROBE lookup 100 priority 101 ip rule | grep -q "from $SRC2 lookup 101" || ip rule add from $SRC2 lookup 101 priority 102 ip route replace default dev $WG1 table 100 2>/dev/null ip route replace default dev $WG2 table 101 2>/dev/null } save_routes() { # Routen des FRITZ!Box-Tunnels merken, solange Stufe A aktiv ist r=$(ip route show dev $WG1 | grep -v "proto kernel") [ -n "$r" ] && echo "$r" > $SAVED } restore_routes() { [ -f $SAVED ] || return while read -r line; do [ -n "$line" ] && ip route replace $line dev $WG1 2>/dev/null done < $SAVED } drop_default() { ip route del default dev $1 2>/dev/null; ip route del default via "$2" dev "$3" metric 5 2>/dev/null; } direct_gw() { # Uplink, den uplink-watchdog gerade bevorzugt (Landal oder Huawei) u=$(grep '^uplink=' $UPSTATE 2>/dev/null | cut -d= -f2) case "$u" in Landal) DDEV=phy1-sta0;; *) DDEV=phy0-sta0;; esac DGW=$(ip route show default dev $DDEV 2>/dev/null | awk '/via/{print $3; exit}') if [ -z "$DGW" ]; then # anderen Uplink versuchen [ "$DDEV" = phy1-sta0 ] && DDEV=phy0-sta0 || DDEV=phy1-sta0 DGW=$(ip route show default dev $DDEV 2>/dev/null | awk '/via/{print $3; exit}') fi } apply_mode() { # $1 = A|B|C, $2 = LAN-Sonde ueber fritzbox ok (1/0) [ "$DRY" = 1 ] && return case "$1" in A) ip route del default dev $WG2 2>/dev/null ip route del 192.168.192.0/24 dev $WG2 2>/dev/null direct_gw; [ -n "$DGW" ] && ip route del default via "$DGW" dev "$DDEV" metric 5 2>/dev/null ip route show default dev $WG1 | grep -q . || restore_routes save_routes ;; B) # FRITZ!Box-Tunnel entlasten, Standard und Raderbroich-LAN ueber pve ip route del default dev $WG1 2>/dev/null ip route del 192.168.192.0/24 dev $WG1 2>/dev/null direct_gw; [ -n "$DGW" ] && ip route del default via "$DGW" dev "$DDEV" metric 5 2>/dev/null ip route replace default dev $WG2 2>/dev/null ip route replace 192.168.192.0/24 dev $WG2 2>/dev/null ;; C) ip route del default dev $WG1 2>/dev/null ip route del default dev $WG2 2>/dev/null ip route del 192.168.192.0/24 dev $WG2 2>/dev/null # Raderbroich-LAN ueber den FRITZ!Box-Tunnel nur behalten, wenn er das LAN noch erreicht if [ "$2" = 1 ]; then ip route replace 192.168.192.0/24 dev $WG1 proto static scope link 2>/dev/null; else ip route del 192.168.192.0/24 dev $WG1 2>/dev/null; fi direct_gw [ -n "$DGW" ] && ip route replace default via "$DGW" dev "$DDEV" metric 5 ;; esac } HOOKFILE=/etc/tunnel-hook report() { [ "$DRY" = 1 ] && return [ -f $HOOKFILE ] || return H=$(cat $HOOKFILE) wget -q -T 6 -O /dev/null --post-data="stufe=$mode&info=fritzbox:$f_state,+pve:$p_state" "$H" 2>/dev/null } sync_pve_endpoint() { [ "$DRY" = 1 ] && return ep=$(wg show $WG1 endpoints 2>/dev/null | head -n 1 | cut -f2 | cut -d: -f1) pk=$(wg show $WG2 peers 2>/dev/null | head -n 1) cur=$(wg show $WG2 endpoints 2>/dev/null | head -n 1 | cut -f2 | cut -d: -f1) [ -n "$ep" ] && [ -n "$pk" ] && [ "$ep" != "$cur" ] && wg set $WG2 peer "$pk" endpoint "$ep:$PVE_PORT" && log "Endpunkt $WG2 auf $ep gesetzt" } cleanup() { log "beendet, stelle Stufe A her"; [ "$DRY" = 1 ] || apply_mode A 1; exit 0; } trap cleanup TERM INT f_state=up; p_state=down f_bad=0; f_good=0; p_bad=0; p_good=0 mode=A last_reported="" rcount=0 since=$(date +%s) log "gestartet (DRY=$DRY)" while true; do ensure_rules sync_pve_endpoint # Pruefung fritzbox f_lan=0 if ping_via $SRC1 $LAN_PROBE; then f_lan=1; fi if [ $f_lan = 1 ] && ping_via $SRC1 $NET_PROBE; then f_probe=1; else f_probe=0; fi # Pruefung pve if ping_via $SRC2 $SRV_PROBE && ping_via $SRC2 $NET_PROBE; then p_probe=1; else p_probe=0; fi # Hysterese fritzbox if [ $f_probe = 1 ]; then f_good=$((f_good+1)); f_bad=0; else f_bad=$((f_bad+1)); f_good=0; fi if [ $f_state = up ] && [ $f_bad -ge $FAIL_N ]; then f_state=down; log "$WG1 gestoert (LAN=$f_lan)"; fi if [ $f_state = down ] && [ $f_good -ge $OK_N ]; then f_state=up; log "$WG1 wieder stabil"; fi # Hysterese pve if [ $p_probe = 1 ]; then p_good=$((p_good+1)); p_bad=0; else p_bad=$((p_bad+1)); p_good=0; fi if [ $p_state = up ] && [ $p_bad -ge $FAIL_N ]; then p_state=down; log "$WG2 gestoert"; fi if [ $p_state = down ] && [ $p_good -ge $OK_N ]; then p_state=up; log "$WG2 wieder stabil"; fi if [ $f_state = up ]; then want=A elif [ $p_state = up ]; then want=B else want=C; fi if [ "$want" != "$mode" ]; then case $want in A) log "Stufe A: Tunnel $WG1 (FRITZ!Box)";; B) log "Stufe B: Tunnel $WG2 (Server in Proxmox), $WG1 ausgefallen";; C) log "Stufe C: direkt ins Internet ohne VPN, beide Tunnel ausgefallen";; esac mode=$want; since=$(date +%s) fi apply_mode $mode $f_lan rcount=$((rcount+1)) if [ "$mode" != "$last_reported" ] || [ $rcount -ge 30 ]; then report; last_reported=$mode; rcount=0; fi { echo "stufe=$mode" echo "fritzbox=$f_state" echo "pve=$p_state" echo "fritzbox_lan=$f_lan" echo "seit=$(date -d @$since '+%d.%m.%Y %H:%M:%S' 2>/dev/null || echo $since)" echo "geprueft=$(date '+%d.%m.%Y %H:%M:%S')" echo "dry=$DRY" } > $STATE.tmp && mv $STATE.tmp $STATE sleep $INTERVAL done