Бенелюкс: сторож восстановления обхода (Этап 1) — мониторинг+автолечение на LXC137, боевой тест пройден
This commit is contained in:
156
snippets/benelux/benelux-podkop-watchdog.sh
Normal file
156
snippets/benelux/benelux-podkop-watchdog.sh
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/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")"
|
||||
Reference in New Issue
Block a user