Files
knowledge-base/snippets/benelux/benelux-podkop-watchdog.sh

157 lines
9.7 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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")"