#!/bin/bash
# benelux-podkop-watchdog.sh — внешний сторож обхода РКН на Бенелюксе (Cudy TR3000).
# Живёт на LXC 137 (Антошка), cron */5. Проверки делает УДАЛЁННО по SSH через NetBird,
# лечит лестницей с гистерезисом, алертит Олегу через токен бота (как antoshka-watch-self.sh).
#
# Принцип против flapping (грабли OpenWrt_4/Оливье): лечим только после 2 подряд провалов,
# cooldown между шагами, лимит ребутов в сутки. Тупой рестарт по таймеру — хуже чем ничего.
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# ── конфиг ───────────────────────────────────────────────────────────────────
ROUTER=100.70.207.97
SSH="ssh -i /root/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=8 -o BatchMode=yes root@$ROUTER"
CFG=/root/.openclaw/openclaw.json          # источник токена бота
CHAT=1292155421                            # Олег @it5870
HS_MAX=200                                 # макс. возраст awg-handshake, сек
PRIMARY=awg0                               # основной выход (Finland-Helsinki)
SECONDARY=awg1                             # резервный выход (Finland) — если поднят

STATE=/root/.benelux-wd.state              # счётчик подряд-провалов + шаг лестницы
ALERT=/root/.benelux-wd.alert              # md5 последнего алерта (антиспам)
REBOOTS=/root/.benelux-wd.reboots          # "YYYY-MM-DD count" — лимит ребутов/сутки
COOLDOWN=/run/benelux-wd.cooldown          # ts последнего лечащего действия
COOLDOWN_SEC=300                           # не лечить чаще раза в 5 мин
REBOOT_CAP=2                               # максимум ребутов в сутки

TOKEN=$(grep -oE '[0-9]{8,}:[A-Za-z0-9_-]{30,}' "$CFG" 2>/dev/null | head -1)
send(){ [ -n "$TOKEN" ] && curl -s --max-time 20 "https://api.telegram.org/bot${TOKEN}/sendMessage" \
        --data-urlencode "chat_id=$CHAT" --data-urlencode "text=$1" >/dev/null 2>&1; }
NOW=$(date +%s); TODAY=$(date +%F)

issues=""; heals=""

# ── 0. достижимость роутера ───────────────────────────────────────────────────
PROBE=$($SSH '
  echo "SINGBOX=$(pgrep sing-box >/dev/null && echo 1 || echo 0)"
  echo "IFACE=$(uci -q get podkop.main.interface)"
  echo "HS0=$(awg show '"$PRIMARY"' latest-handshakes 2>/dev/null | awk "{print \$2}" | sort -rn | head -1)"
  echo "HS1=$(awg show '"$SECONDARY"' latest-handshakes 2>/dev/null | awk "{print \$2}" | sort -rn | head -1)"
  echo "AWG1=$(uci -q get network.'"$SECONDARY"' >/dev/null 2>&1 && echo 1 || echo 0)"
  echo "PING0=$(ping -I '"$PRIMARY"' -c1 -W2 1.1.1.1 >/dev/null 2>&1 && echo 1 || echo 0)"
  echo "PING1=$(ping -I '"$SECONDARY"' -c1 -W2 1.1.1.1 >/dev/null 2>&1 && echo 1 || echo 0)"
  echo "CLASH=$(wget -qO- http://192.168.1.1:9090/version 2>/dev/null | grep -q sing-box && echo 1 || echo 0)"
  echo "YT=$(nslookup youtube.com 127.0.0.42 2>/dev/null | grep -c "198.1[89].")"
  echo "OZ=$(nslookup ozon.ru 127.0.0.42 2>/dev/null | grep -c "198.1[89].")"
' 2>/dev/null)

if [ -z "$PROBE" ]; then
  # SSH не прошёл — отличаем «роутер лёг» от «SSH/NetBird моргнул»
  if ping -c2 -W2 "$ROUTER" >/dev/null 2>&1; then
    issues="• Роутер пингуется, но SSH не отвечает (NetBird/dropbear моргнул).\n"
  else
    issues="• Бенелюкс НЕДОСТУПЕН — не пингуется по NetBird (роутер/WAN/туннель лёг).\n"
  fi
  # удалённо чинить нечем → только алерт (антиспам ниже)
  h=$(printf '%s' "$issues" | md5sum | cut -d' ' -f1)
  echo "UNREACHABLE"; printf '%b' "$issues"
  [ "$(cat "$ALERT" 2>/dev/null)" = "$h" ] && exit 0
  echo "$h" > "$ALERT"; send "$(printf '⚠️ Бенелюкс-обход:\n%b' "$issues")"
  exit 0
fi

eval "$PROBE"   # SINGBOX IFACE HS0 HS1 AWG1 PING0 PING1 CLASH YT OZ
AGE0=999999; [ -n "$HS0" ] && AGE0=$((NOW - HS0))
AGE1=999999; [ -n "$HS1" ] && AGE1=$((NOW - HS1))

# текущий выход здоров? (handshake свежий + транзит идёт)
cur_ok(){
  case "$IFACE" in
    "$PRIMARY")   [ "$AGE0" -lt "$HS_MAX" ] && [ "$PING0" = 1 ] ;;
    "$SECONDARY") [ "$AGE1" -lt "$HS_MAX" ] && [ "$PING1" = 1 ] ;;
    *) return 1 ;;
  esac
}

HEALTHY=1
[ "$SINGBOX" = 1 ] || { HEALTHY=0; issues="${issues}• sing-box не запущен.\n"; }
[ "$CLASH"   = 1 ] || { HEALTHY=0; issues="${issues}• Clash API ($IFACE) не отвечает.\n"; }
cur_ok            || { HEALTHY=0; issues="${issues}• Выход $IFACE мёртв (handshake ${AGE0}/${AGE1}s, ping0=$PING0 ping1=$PING1).\n"; }
[ "${YT:-0}" -ge 1 ] || { HEALTHY=0; issues="${issues}• FakeIP не работает (youtube не заворачивается).\n"; }

# анти-утечка: РФ-сайт НЕ должен фейкапиться (рецидив russia_outside) — алерт, но НЕ автофикс
LEAK=""
[ "${OZ:-0}" -ge 1 ] && LEAK="• ⚠️ Утечка: ozon.ru уходит в туннель (вернулся russia_outside?). Проверь списки podkop.\n"

# ── здоров ────────────────────────────────────────────────────────────────────
if [ "$HEALTHY" = 1 ] && [ -z "$LEAK" ]; then
  echo "OK: Бенелюкс-обход здоров (выход $IFACE, hs ${AGE0}s)."
  rm -f "$STATE"
  if [ -f "$ALERT" ]; then send "✅ Бенелюкс-обход снова в норме (выход $IFACE)."; rm -f "$ALERT"; fi
  exit 0
fi

# ── гистерезис: считаем подряд-провалы ────────────────────────────────────────
FAILS=0; STEP=0; [ -f "$STATE" ] && read -r FAILS STEP < "$STATE"; FAILS=${FAILS:-0}; STEP=${STEP:-0}
FAILS=$((FAILS + 1))

# утечка списков — только сигнал, не повод лечить сервис
if [ -n "$LEAK" ] && [ "$HEALTHY" = 1 ]; then
  echo "$FAILS $STEP" > "$STATE"
  h=$(printf '%s' "$LEAK" | md5sum | cut -d' ' -f1)
  [ "$(cat "$ALERT" 2>/dev/null)" = "$h" ] && exit 0
  echo "$h" > "$ALERT"; send "$(printf '⚠️ Бенелюкс-обход:\n%b' "$LEAK")"
  exit 0
fi

if [ "$FAILS" -lt 2 ]; then
  echo "$FAILS $STEP" > "$STATE"
  echo "DEGRADED (провал $FAILS/2, жду подтверждения перед лечением):"; printf '%b' "$issues"
  exit 0
fi

# cooldown между лечащими действиями
if [ -f "$COOLDOWN" ] && [ $((NOW - $(cat "$COOLDOWN"))) -lt "$COOLDOWN_SEC" ]; then
  echo "cooldown активен — жду"; echo "$FAILS $STEP" > "$STATE"; exit 0
fi

# ── лестница лечения ──────────────────────────────────────────────────────────
do_heal(){ echo "$NOW" > "$COOLDOWN"; }

case "$STEP" in
  0)  # мягкий рестарт podkop
      $SSH '/etc/init.d/podkop restart' >/dev/null 2>&1
      heals="🔧 Шаг1: перезапустил podkop.\n"; STEP=1; do_heal ;;
  1)  # переподнять интерфейс / переключить на резерв
      if [ "$AWG1" = 1 ] && [ "$IFACE" = "$PRIMARY" ] && [ "$AGE1" -lt "$HS_MAX" ] && [ "$PING1" = 1 ]; then
        $SSH "uci set podkop.main.interface=$SECONDARY; uci commit podkop; /etc/init.d/podkop restart" >/dev/null 2>&1
        heals="🔧 Шаг2: основной выход ($PRIMARY/Finland-Helsinki) мёртв → переключил на резерв $SECONDARY (Finland).\n"
      else
        $SSH "ifdown $IFACE; sleep 2; ifup $IFACE; sleep 3; /etc/init.d/podkop restart" >/dev/null 2>&1
        heals="🔧 Шаг2: переподнял интерфейс $IFACE + рестарт podkop.\n"
      fi
      STEP=2; do_heal ;;
  2)  # ребут (с лимитом в сутки)
      RDAY=""; RCNT=0; [ -f "$REBOOTS" ] && read -r RDAY RCNT < "$REBOOTS"
      [ "$RDAY" != "$TODAY" ] && { RDAY=$TODAY; RCNT=0; }
      if [ "${RCNT:-0}" -lt "$REBOOT_CAP" ]; then
        $SSH 'reboot' >/dev/null 2>&1
        echo "$TODAY $((RCNT + 1))" > "$REBOOTS"
        heals="🔧 Шаг3: рестарты не помогли — перезагружаю роутер (ребут $((RCNT+1))/$REBOOT_CAP сегодня).\n"
        STEP=3; do_heal
      else
        issues="${issues}• Лимит ребутов на сегодня исчерпан ($REBOOT_CAP).\n"
        STEP=3
      fi ;;
  *)  # сдаёмся — нужно руками
      issues="${issues}• Автолечение не помогло (пройдены все шаги). Нужно вмешательство.\n" ;;
esac

echo "$FAILS $STEP" > "$STATE"

# ── алерт (антиспам по содержимому) ───────────────────────────────────────────
echo "ISSUES:"; printf '%b%b' "$issues" "$heals"
h=$(printf '%s%s' "$issues" "$heals" | md5sum | cut -d' ' -f1)
[ "$(cat "$ALERT" 2>/dev/null)" = "$h" ] && exit 0
echo "$h" > "$ALERT"
send "$(printf '⚠️ Бенелюкс-обход — сбой:\n%b\n%b' "$issues" "$heals")"
