#!/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 # основной выход (Singapore) 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/Singapore) мёртв → переключил на резерв $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")"