Бенелюкс: сторож восстановления обхода (Этап 1) — мониторинг+автолечение на LXC137, боевой тест пройден
This commit is contained in:
58
decisions/2026-06-26-benelux-podkop-recovery-watchdog.md
Normal file
58
decisions/2026-06-26-benelux-podkop-recovery-watchdog.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
date: 2026-06-26
|
||||
type: decision
|
||||
tags: [benelux, podkop, watchdog, monitoring, self-heal, openwrt, amneziawg, failover]
|
||||
status: stage1-done
|
||||
---
|
||||
|
||||
# Бенелюкс — инструмент восстановления обхода (мониторинг + автолечение)
|
||||
|
||||
## Задача
|
||||
Олег: «инструмент восстановления обхода блокировок, мониторинг + автоисправление, должно работать на 100%, наверное на внешних ресурсах от Бенелюкса».
|
||||
|
||||
## Калибровка «100%»
|
||||
Буквальные 100% одним туннелём недостижимы (ISP/нода/NetBird могут лечь — удалённо не починить). Реальная цель: **обход никогда не остаётся сломанным незаметно, чинится сам за 1–3 мин, иначе алерт раньше клиента.** Достигается тремя слоями.
|
||||
|
||||
## Решения Олега (AskUserQuestion)
|
||||
- Наблюдатель — **дома, LXC** (клон antoshka-watch-self, алерт через бота).
|
||||
- Автофикс — **до ребута включительно**, с гистерезисом.
|
||||
- Резерв выхода — **второй AWG-сервер** (не VLESS) → Finland-хаб.
|
||||
|
||||
## Архитектура (3 слоя)
|
||||
1. **Резерв выхода:** awg0 Singapore (primary) + awg1 Finland (secondary). Этап 2.
|
||||
2. **Лёгкое самолечение на роутере:** podkop `enable_badwan_interface_monitoring=1` (следит за wan, перезагружает sing-box) — уже было.
|
||||
3. **Внешний сторож (главный):** `benelux-podkop-watchdog.sh` на LXC 137, cron `*/5`, проверки УДАЛЁННО по SSH через NetBird.
|
||||
|
||||
## Ключевой принцип — анти-flapping
|
||||
Грабли OpenWrt_4/Оливье: собственный watchdog.sh агрессивно рестартовал sing-box и сам создавал обрывы. Поэтому: лечим только после **2 подряд провалов**, cooldown 5 мин между шагами, лимит **2 ребута/сутки**.
|
||||
|
||||
## Что проверяет сторож (изнутри роутера)
|
||||
- sing-box жив + Clash API (`192.168.1.1:9090`) отвечает
|
||||
- handshake текущего выхода < 200с + `ping -I awgN 1.1.1.1` (транзит, ловит rp_filter-ловушку)
|
||||
- FakeIP: youtube → `198.18.x` (заворачивается)
|
||||
- **анти-утечка:** ozon.ru НЕ `198.18.x` (страж рецидива `russia_outside`) — только алерт, не автофикс
|
||||
- достижимость: SSH нет → различает «роутер пингуется, SSH моргнул» vs «Бенелюкс лёг»
|
||||
|
||||
## Лестница лечения (с гистерезисом)
|
||||
0. `podkop restart`
|
||||
1. флип `podkop.main.interface` awg0→awg1 (если awg1 поднят и здоров), иначе `ifdown/ifup` + restart
|
||||
2. `reboot` (лимит 2/сутки)
|
||||
3. сдаёмся → алерт «нужно руками»
|
||||
Каждое срабатывание + восстановление (✅) → Telegram Олегу (1292155421) через токен бота из `/root/.openclaw/openclaw.json` (тот же тракт, что antoshka-watch-self).
|
||||
|
||||
## Деплой Этапа 1 (2026-06-26) — СДЕЛАНО
|
||||
- Ключ LXC137 `root@openclaw` добавлен в `/etc/dropbear/authorized_keys` Бенелюкса.
|
||||
- `/root/benelux-podkop-watchdog.sh` на LXC 137, cron `*/5`. Исходник в vault `snippets/benelux/benelux-podkop-watchdog.sh`.
|
||||
- **Боевой тест пройден:** `podkop stop` → прогон1 DEGRADED 1/2 (без действий) → прогон2 2/2 → Шаг1 restart → sing-box поднят, FakeIP вернулся → прогон3 OK + ✅-отбой + сброс счётчика. ⚠️/✅ алерты дошли.
|
||||
- Грабли при написании: busybox `pgrep -x` не работает (→ `pgrep`); `read < нет_файла` течёт ошибкой (→ guard `[ -f ]`).
|
||||
|
||||
## Этап 2 (TODO) — резервный выход Finland + failover
|
||||
- Через Amnezia Web Panel (LXC 143 `10.0.0.143:5000`, admin/AmnPanel!2026-fi) нарезать пир Бенелюкса на Finland-хаб `151.241.234.241:41624` (AmneziaWG 2.0, subnet `10.8.1.0/24`).
|
||||
- Поднять `awg1` на роутере (UCI-референс — HomeLab awg2, [[../projects/dttb/openwrt-router]]); awg1 в firewall WAN-зону; per-iface `rp_filter=2`.
|
||||
- Failover уже заложен в watchdog (Шаг1 флипает на awg1) — активируется автоматически, как только `network.awg1` появится.
|
||||
- Закрывает TODO из [[2026-06-23-amnezia-web-panel-lxc143]] «failover (AWG сам не переключается)».
|
||||
|
||||
## Опционально (Этап 3)
|
||||
- Второй независимый сторож на Finland VPS (на случай падения дома) — взаимный догляд ближе к «100%».
|
||||
|
||||
См. [[../snippets/podkop-reference]], [[../claude-memory/benelux]].
|
||||
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