feat: one-command install/uninstall + fix delete-ams in WebUI

- install.sh: интерактивный one-liner — ставит WG, ключи, скрипты,
  бот, WebUI, cron; сам поднимает RU-сервер если он чистый
- uninstall.sh: полная очистка (сервисы, конфиги, cron, /opt/kaskad)
  с подтверждением; опц. KASKAD_PURGE_KEYS=1 для затирания ключей
- webui: кнопка «удалить» для зарубежных серверов теперь явно красная
  (class danger.solid), для локального — « нельзя» вместо disabled
- webui: DELETE /api/ams/<id> переписан — портативный python-based
  peer-removal, явные ошибки, поддержка ?force=1
- README + docs/install.md: переписаны под обычного пользователя,
  one-command flow на первом плане, ручной — как fallback

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
andrey271192
2026-05-12 06:28:56 +03:00
parent b856909437
commit f6514e9bba
7 changed files with 768 additions and 185 deletions

214
README.md
View File

@@ -1,104 +1,200 @@
# Kaskad
Каскадная маршрутизация русских сайтов через свой набор RU-серверов (WireGuard) с автоматическим failover, Telegram-ботом и веб-интерфейсом.
**Свой VPN для россиян за границей.** Российские сайты (госуслуги, банки, ВК) открываются через ваш сервер в РФ. Всё остальное — напрямую через зарубежный. Если RU-сервер падает — за минуту переключается на запасной. Управляется из браузера или Telegram.
## Что это
![Каскад работает](https://img.shields.io/badge/status-в%20продакшне-success) ![License](https://img.shields.io/github/license/andrey271192/kaskad) ![Stars](https://img.shields.io/github/stars/andrey271192/kaskad?style=social)
Несколько зарубежных серверов с X-ray («ам.») держат WireGuard-туннели к одному из RU-серверов. Российские IP/домены идут через RU, всё остальное — через зарубежный. Если RU упал — за минуту переключение на следующий по приоритету. Когда восстановился — пробный failback с автоматическим откатом если WG не отвечает.
---
## Зачем это нужно
Живёте за границей и сталкивались с тем, что:
- 🏦 Тинькофф не пускает с заграничного IP
- 🧾 Госуслуги «вы не из России»
- 📺 Кинопоиск, ivi, ВК — недоступны
- 🛍 Озон, Wildberries — корзина не работает
При этом обычный VPN в РФ **тормозит всё подряд**, включая Netflix, рабочие звонки и игры.
**Каскад** делит трафик по умному:
```
телефон/клиент ──> ам. сервер (X-ray) ──┬──> RU primary ──> RU-сайты
├──> RU backup (failover)
└──> RU N (по приоритету)
└──> остальное идёт напрямую через ам.
┌──────────────────────────────────────────┐
вы ───▶ │ зарубежный сервер (Амстердам, X-ray) │
если адрес РФ → 🇷🇺 RU-сервер → инет │
│ всё остальное → 🌍 напрямую │
└──────────────────────────────────────────┘
```
## Возможности
Vk.com видит ваш московский IP. Netflix видит амстердамский. Скорость не страдает.
- **N RU-серверов** в конфиге, упорядочены по приоритету
- **Авто-failover** по handshake age + TCP-проба; cooldown между переключениями; автоматический failback с проверкой
- **Telegram-бот** для всех операций: смотреть статус, переключать вручную, добавлять/удалять серверы, домены, IP
- **Веб-интерфейс** с тем же функционалом
- **Маршруты по доменам**: бот резолвит `vk.com → IP`, добавляет в маршруты на всех ам. серверах. Cron каждые 6ч переподнимает резолв
- **Доп. подсети/IP** добавляются на лету через бот или WebUI; переживают рестарт туннеля и сервера
- **Уведомления в TG** при каждом failover/failback
---
## Структура репозитория
## Что умеет
```
kaskad/
├── bin/ # скрипты для ам. серверов (failover, switch, routes, domains)
├── bot/ # Telegram-бот + systemd unit
├── webui/ # Flask-приложение
├── examples/ # шаблоны конфигов
├── docs/ # подробная документация
└── README.md
-**Авто-failover** — если основной RU-сервер упал, за минуту переключение на запасной. Когда вернулся — автоматически обратно.
- 🎛 **WebUI** — добавлять серверы, домены, IP в один клик. Никакого SSH.
- 🤖 **Telegram-бот** — то же самое из телефона.
- 🌐 **N RU-серверов** в каскаде с приоритетами.
- 🌍 **N зарубежных нод** под одним каскадом — все автоматически переключаются вместе.
- 🔔 **Уведомления в Telegram** при каждом переключении.
- 📋 **Маршруты по доменам** — ввёл `vk.com`, бот сам резолвит IP и держит их в маршрутах.
---
## Установка в одну команду
На зарубежном сервере (Ubuntu 20.04+) с уже работающим X-ray:
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad/main/install.sh | sudo bash
```
## Быстрый старт
Скрипт спросит:
См. [docs/install.md](docs/install.md) для полной установки.
1. Telegram bot token (от [@BotFather](https://t.me/BotFather))
2. Telegram chat ID (узнать у [@userinfobot](https://t.me/userinfobot))
3. Хост вашего RU-сервера + пароль root (пароль нужен один раз)
4. Логин/пароль для WebUI
Краткая последовательность:
Через 2-3 минуты получите:
1. **Настроить первый RU-сервер** вручную (см. `docs/install.md` § «RU-сервер»)
2. **Настроить первый ам. сервер** (см. § «Ам. сервер»)
3. **Создать `/etc/wireguard/ru-servers.json`** по шаблону `examples/ru-servers.example.json`
4. **Запустить TG-бот** на одном из ам. серверов (`bot/ru-tg-bot.service`)
5. **(опц.) Запустить WebUI** на том же ам. сервере (`webui/ru-webui.service`)
6. Дальше — добавлять RU/ам./домены/IP через бот или WebUI
- 🌐 WebUI на `http://ВАШ_СЕРВЕР:8088`
- 🤖 Активного Telegram-бота — `/status` покажет состояние
- 🔁 Авто-failover каждую минуту через cron
## Команды бота
Готово. Никаких ручных правок конфигов.
> 💡 **Если RU-сервер ещё чистый** — скрипт сам поставит на него WireGuard, ip_forward, iptables и поднимет туннель. Достаточно SSH-пароля.
---
## Удаление в одну команду
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad/main/uninstall.sh | sudo bash
```
или, если уже склонировано:
```bash
sudo bash /opt/kaskad/uninstall.sh
```
Спросит подтверждение (`YES`), затем уберёт:
- сервисы, скрипты, cron, WG-туннели, конфиги, `/opt/kaskad/`
X-ray, ваши SSH-ключи и системные пакеты **не трогает**. Если хотите снести и WG-ключи — `KASKAD_PURGE_KEYS=1 sudo bash uninstall.sh`.
---
## WebUI
После установки откройте `http://ВАШ_СЕРВЕР:8088`.
| Что | Как |
|---|---|
| Переключиться на запасной RU | кнопка `backup` в верхней таблице |
| Добавить ещё один RU-сервер | секция «RU-серверы» → «+ добавить RU-сервер», ввести IP/домен и пароль root |
| Добавить ещё одну зарубежную ноду | секция «Ам. серверы» → «+ добавить ам. сервер» |
| **Удалить зарубежный сервер** | красная кнопка `удалить` в строке — peer уберётся со всех RU и из конфига |
| Добавить русский сайт | секция «Домены» → ввести `vk.com ozon.ru` |
| Принудительный refresh DNS для доменов | кнопка `🔄 Refresh DNS` |
Все действия идут с подтверждением, ошибки видны сразу.
---
## Telegram-бот
| Категория | Команды |
|---|---|
| Туннели | `/status`, `/use <id>`, `/primary`, `/backup` |
| RU-серверы | `/server-list`, `/server-add`, `/server-remove`, `/bot-key` |
| Ам. серверы | `/ams-list`, `/ams-add`, `/ams-remove` |
| Маршруты | `/ips`, `/list`, `/add <IP/CIDR>`, `/remove <IP/CIDR>`, `/clear` |
| Домены | `/list-domains`, `/add-domain <domain>`, `/remove-domain <domain>`, `/show-domain <domain>`, `/refresh-domains` |
| Маршруты | `/ips`, `/list`, `/add <IP>`, `/remove <IP>`, `/clear` |
| Домены | `/list-domains`, `/add-domain <d>`, `/remove-domain <d>`, `/show-domain <d>`, `/refresh-domains` |
| Прочее | `/help` |
Полный синтаксис: [docs/bot.md](docs/bot.md).
Полный синтаксис [docs/bot.md](docs/bot.md).
## Веб-интерфейс
---
Запускается на любом ам. сервере (там же где бот), HTTP basic auth.
## Как это работает (короткая версия)
После запуска: `http://<ам-сервер>:8088`
1. На каждом зарубежном сервере поднят WireGuard-туннель `ru` → к выбранному RU-серверу.
2. iptables `mangle` помечает трафик от X-ray меткой `100`, `ip rule` отправляет его в таблицу маршрутизации `200`, где default → через туннель.
3. Маршруты на конкретные RU-подсети/IP добавляются в `ru-base.aips` и `ru-extra.list`.
4. Cron каждую минуту запускает `ru-failover.py`: проверяет handshake + TCP-пробу на RU. Если пусто > N секунд — переключает endpoint на следующий по приоритету.
Подробнее: [docs/webui.md](docs/webui.md).
Деталки — [docs/architecture.md](docs/architecture.md).
## Архитектура
---
См. [docs/architecture.md](docs/architecture.md) — как устроены failover, маршрутизация, синхронизация конфига между серверами.
## Структура репозитория
```
kaskad/
├── install.sh ← одна команда — всё ставится
├── uninstall.sh ← одна команда — всё убирается
├── bin/ ← скрипты для ам. серверов
├── bot/ ← Telegram-бот
├── webui/ ← Flask-приложение
├── examples/ ← шаблоны конфигов (если ставите вручную)
└── docs/ ← подробная документация
```
---
## Безопасность
- Конфиги (`ru-servers.json`, `notify.env`, `webui.env`) с правами `600`, **не коммитятся** в репо
- SSH между серверами — по ключам; пароли используются только при первичной онбординге нового сервера через `password=` в боте/WebUI и **сразу затираются** после установки ключа
- Веб-интерфейс через basic auth, рекомендуется ставить за HTTPS reverse-proxy (nginx/caddy)
- Telegram-бот принимает команды только от заранее заданного `TG_CHAT_ID`
- Все конфиги (`ru-servers.json`, `notify.env`, `webui.env`) с правами `600`, **не коммитятся**.
- Между серверами — только SSH-ключи. Пароли используются один раз, при добавлении нового сервера, и **сразу затираются**.
- WebUI защищён HTTP basic auth. Рекомендация: поставьте за HTTPS reverse-proxy (Caddy, Nginx, Traefik).
- Telegram-бот принимает команды только от заранее заданного `TG_CHAT_ID`.
---
## Требования
- Ubuntu / Debian 20.04+ на ам. сервере (где будет WebUI и бот) — root доступ
- Ubuntu / Debian 20.04+ на RU-сервере — root + SSH-пароль (нужен один раз)
- На ам. сервере: уже настроенный X-ray (3x-ui подойдёт) с интерфейсом `amn0`
- На RU-сервере: открыт UDP/1939 наружу (или проброс на роутере)
---
## FAQ
**Не работает «удалить» в WebUI?** — кнопки `удалить` рядом с зарубежными серверами теперь явно красные. Если показывает «⛔ нельзя» — это сервер, на котором сам WebUI запущен; такой нельзя убрать (надо сначала перенести WebUI на другой узел).
**Сервер удалён, но peer остался на RU?** — при удалении WebUI сам делает `wg set wg_ru peer ... remove` на всех RU. Если RU был недоступен — попросит подтверждение и удалит запись принудительно (`force`).
**Скорость упала?** — каскад не должен влиять на не-RU трафик. Если падает — проверьте `iptables -t mangle -L PREROUTING -v` (помечается ли трафик) и `ip rule show`.
**Бот молчит**`journalctl -u ru-tg-bot -f`. Чаще всего: неверный `TG_BOT_TOKEN` или `TG_CHAT_ID`, или сервер не может выйти к `api.telegram.org`.
Подробнее: [docs/troubleshooting.md](docs/troubleshooting.md).
---
## Связанные проекты
- 🔧 **[keenetic-unified](https://github.com/andrey271192/keenetic-unified)** — мониторинг роутеров Keenetic с TG-ботом и веб-дашбордом
- 🌐 **[keenetic_ssh-web](https://github.com/andrey271192/keenetic_ssh-web)** — веб-панель shell-команд на самом роутере
- 🛣 **[keenetic-dns-routes](https://github.com/andrey271192/keenetic-dns-routes)** — DNS-маршрутизация без HydraRoute Neo
- 🔧 [keenetic-unified](https://github.com/andrey271192/keenetic-unified) — мониторинг роутеров Keenetic
- 🌐 [keenetic_ssh-web](https://github.com/andrey271192/keenetic_ssh-web) — веб-панель shell-команд на роутере
- 🛣 [keenetic-dns-routes](https://github.com/andrey271192/keenetic-dns-routes) — DNS-маршрутизация без HydraRoute Neo
---
## Поддержка проекта
## Поддержать проект
Если **Kaskad** оказался полезен — поддержи разработку:
- **GitHub:** [andrey271192/kaskad](https://github.com/andrey271192/kaskad) — поставь звезду
- 💖 **Boosty:** [boosty.to/andrey27/donate](https://boosty.to/andrey27/donate)
- 💳 **Ozon Bank (СБП):** [перевод](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798)
- ✉️ **Telegram:** [@Iot_andrey](https://t.me/Iot_andrey) — вопросы, предложения, баги
---
- ⭐ Поставьте звезду на [andrey271192/kaskad](https://github.com/andrey271192/kaskad)
- 💖 [Boosty](https://boosty.to/andrey27/donate)
- 💳 [Ozon Bank СБП](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798)
- ✉️ Telegram: [@Iot_andrey](https://t.me/Iot_andrey) — вопросы, баги, идеи
## Лицензия

View File

@@ -1,30 +1,98 @@
# Установка
## Требования
Есть два пути: **простой** (одна команда, всё автоматом) и **ручной** (если хочется контроля над каждым шагом).
- Минимум 1 RU-сервер (Ubuntu 20.04+) с публичным IP или пробросом UDP/1939 на роутере
- Минимум 1 ам. сервер (Ubuntu 20.04+) с установленной X-ray панелью (3x-ui)
- Telegram-бот (создать в @BotFather) и chat ID для уведомлений (узнать у @userinfobot)
- Root доступ на оба сервера
---
## Шаг 1. Первый RU-сервер
## 🚀 Простой путь — одна команда
### Что нужно заранее
- **Один зарубежный сервер** (Ubuntu/Debian 20.04+, root) с настроенным X-ray (3x-ui подойдёт), интерфейс обычно `amn0`. Здесь будут жить WebUI и Telegram-бот.
- **Один RU-сервер** (Ubuntu/Debian 20.04+, root, открыт UDP/1939 наружу или проброшен с роутера). Если он чистый — скрипт сам поставит на него WireGuard.
- **Telegram-бот**: создайте через [@BotFather](https://t.me/BotFather), запишите token.
- **Свой Telegram chat ID**: узнайте через [@userinfobot](https://t.me/userinfobot).
### Запуск
На зарубежном сервере, под root:
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad/main/install.sh | sudo bash
```
Скрипт интерактивный, спросит 56 вопросов:
| Вопрос | Что вводить |
|---|---|
| Telegram bot token | строка от @BotFather, типа `123456:AAH...` |
| Telegram chat ID | число, например `123456789` |
| Короткое имя ам. сервера | любое, `ams1` подойдёт |
| Внешний IP | сам подтянет с `api.ipify.org` |
| Хост RU-сервера | IP или домен RU-машины |
| SSH-порт RU | обычно `22` |
| Пароль root@RU | используется ОДИН раз, потом затирается |
| Логин WebUI | например `admin` |
| Пароль WebUI | минимум 8 символов |
| Имя X-ray-интерфейса | обычно `amn0` |
Через 23 минуты:
```
✓ Готово!
WebUI: http://YOUR_AMS_IP:8088
Логин: admin
Пароль: (тот, что ты ввёл)
Telegram-бот активен — отправь ему /status.
```
Дальше всё через WebUI или бот.
### Обновление
```bash
cd /opt/kaskad && git pull && sudo bash install.sh
```
Скрипт идемпотентный — повторный запуск ничего не сломает, просто обновит скрипты.
### Удаление
```bash
sudo bash /opt/kaskad/uninstall.sh
```
Или одной командой:
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad/main/uninstall.sh | sudo bash
```
Подтверждение через ввод `YES`. Если хотите снести и WG-ключи — `KASKAD_PURGE_KEYS=1 sudo bash uninstall.sh`.
---
## 🛠 Ручной путь — для тех, кто хочет понять каждый шаг
Все шаги ниже делает за вас `install.sh` — приведены для отладки и образовательных целей.
### Шаг 1. Первый RU-сервер
На RU-сервере под root:
```bash
apt update && apt install -y wireguard iptables-persistent
# 1. Генерация ключа
cd /etc/wireguard
umask 077
wg genkey | tee ru_private.key | wg pubkey > ru_public.key
PRIVKEY=$(cat ru_private.key)
# 2. ip_forward
sysctl -w net.ipv4.ip_forward=1
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
# 3. Конфиг — пока без peer'ов, добавятся когда подключим первый ам.
IFACE=$(ip route | awk '/^default/{print $5; exit}')
cat > /etc/wireguard/wg_ru.conf <<EOF
[Interface]
@@ -36,181 +104,144 @@ PostDown = iptables -t nat -D POSTROUTING -o $IFACE -j MASQUERADE; iptables -D F
EOF
chmod 600 /etc/wireguard/wg_ru.conf
# 4. Открыть порт 1939/UDP
iptables -A INPUT -p udp --dport 1939 -j ACCEPT
netfilter-persistent save
# 5. Запуск + автостарт
wg-quick up wg_ru
systemctl enable wg-quick@wg_ru
# 6. Сохранить публичный ключ — пригодится
cat ru_public.key
cat ru_public.key # запомните — понадобится
```
Если RU за NAT (Keenetic и т.п.) — пробрось UDP/1939 на роутере на этот сервер.
Если RU за NAT (Keenetic и т.п.) — пробросьте UDP/1939 на роутере на этот сервер.
## Шаг 2. Первый ам. сервер
### Шаг 2. Первый ам. сервер
На ам. сервере под root:
```bash
apt update && apt install -y wireguard iptables-persistent dnsutils
apt update && apt install -y wireguard iptables-persistent dnsutils python3 python3-flask sshpass
# 1. Ключ
cd /etc/wireguard
umask 077
wg genkey | tee ru_private.key | wg pubkey > ru_public.key
cat ru_public.key # записать — добавим в peer'ы RU
cat ru_public.key # запомните — добавим в peer'ы RU
# 2. Узнать имя интерфейса X-ray
# Узнать имя интерфейса X-ray
ip a | grep -E 'amn|tun' | grep -v '@'
# обычно amn0 — используем дальше
# обычно amn0
# 3. Конфиг ru.conf — IP в туннеле = 10.0.0.2 (первый ам.)
PRIVKEY=$(cat ru_private.key)
cat > /etc/wireguard/ru.conf <<EOF
[Interface]
Address = 10.0.0.2/32
PrivateKey = $PRIVKEY
Table = off
PostUp = ip route add 95.163.0.0/16 dev ru; ip route add 185.73.192.0/22 dev ru; ip route add 213.59.0.0/16 dev ru; ip route add 77.88.0.0/18 dev ru; ip route add 93.158.128.0/18 dev ru; ip route add 188.40.167.0/24 dev ru; ip route add 176.114.120.0/22 dev ru; ip route add 178.248.232.0/22 dev ru; ip route add 213.180.192.0/20 dev ru; ip route add 87.240.128.0/18 dev ru; ip rule add fwmark 100 table 200; ip route add default dev ru table 200; iptables -t nat -A POSTROUTING -o ru -j MASQUERADE
PostUp = ip rule add fwmark 100 table 200; ip route add default dev ru table 200; iptables -t nat -A POSTROUTING -o ru -j MASQUERADE
PostDown = ip rule del fwmark 100 table 200; ip route flush table 200; iptables -t nat -D POSTROUTING -o ru -j MASQUERADE
[Peer]
PublicKey = ПУБЛИЧНЫЙ_КЛЮЧ_RU_СЕРВЕРА
PublicKey = ПУБЛИЧНЫЙ_КЛЮЧ_RU
Endpoint = ВНЕШНИЙ_IP_RU:1939
AllowedIPs = 10.0.0.0/24, 95.163.0.0/16, 185.73.192.0/22, 213.59.0.0/16, 77.88.0.0/18, 93.158.128.0/18, 188.40.167.0/24, 176.114.120.0/22, 178.248.232.0/22, 213.180.192.0/20, 87.240.128.0/18
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
EOF
chmod 600 /etc/wireguard/ru.conf
# 4. Mangle для X-ray трафика — за каждой подсетью
for net in 95.163.0.0/16 185.73.192.0/22 213.59.0.0/16 77.88.0.0/18 93.158.128.0/18 188.40.167.0/24 176.114.120.0/22 178.248.232.0/22 213.180.192.0/20 87.240.128.0/18; do
iptables -t mangle -A PREROUTING -i amn0 -d $net -j MARK --set-mark 100
done
# mangle для X-ray трафика
iptables -t mangle -A PREROUTING -i amn0 -j MARK --set-mark 100
netfilter-persistent save
# 5. Запуск
wg-quick up ru
systemctl enable wg-quick@ru
wg show ru # должен быть handshake
```
На RU-сервере добавить этот ам. как peer:
На RU-сервере добавьте этот ам. как peer:
```bash
wg set wg_ru peer ПУБЛИЧНЫЙ_КЛЮЧ_АМ allowed-ips 10.0.0.2/32
cat >> /etc/wireguard/wg_ru.conf <<EOF
[Peer]
PublicKey = ПУБЛИЧНЫЙ_КЛЮЧ_АМ
AllowedIPs = 10.0.0.2/32
EOF
wg set wg_ru peer ПУБЛИЧНЫЙ_КЛЮЧ_АМ allowed-ips 10.0.0.2/32
```
Проверить что трафик идёт:
Проверьте на ам.:
```bash
# на ам. сервере
curl --interface ru https://gosuslugi.ru -I
# должен ответить HTTP/... 200
```
## Шаг 3. Установка скриптов и бота
На ам. сервере где будет жить бот (в Амстердаме, Telegram должен быть доступен!):
### Шаг 3. Скрипты, бот, WebUI
```bash
git clone https://github.com/andrey271192/kaskad.git /opt/kaskad
cd /opt/kaskad
# 1. Скопировать скрипты
install -m 755 bin/ru-failover.py /usr/local/bin/
install -m 755 bin/ru-set.sh /usr/local/bin/
install -m 755 bin/ru-routes.sh /usr/local/bin/
install -m 755 bin/ru-domains.py /usr/local/bin/
install -m 755 bin/add-ru-helper.sh /usr/local/bin/
install -m 755 bin/add-ams-helper.sh /usr/local/bin/
install -m 755 bot/ru-tg-bot.py /usr/local/bin/
# 2. Создать notify.env
cp examples/notify.env.example /etc/wireguard/notify.env
chmod 600 /etc/wireguard/notify.env
# ВПИСАТЬ TG_BOT_TOKEN и TG_CHAT_ID
# впишите TG_BOT_TOKEN и TG_CHAT_ID
# 3. Создать ru-servers.json
cp examples/ru-servers.example.json /etc/wireguard/ru-servers.json
chmod 600 /etc/wireguard/ru-servers.json
# ОТРЕДАКТИРОВАТЬ — вписать host, pubkey, ssh_user/port для каждого RU; для каждого ам. — host, pubkey, tunnel_ip
# отредактируйте — host, pubkey, ssh_port для RU; host, pubkey, tunnel_ip для ам.
# 4. Базовый список allowed-ips (берётся из ru.conf при первом apply)
# создаст ru-base.aips автоматически
/usr/local/bin/ru-routes.sh apply
mkdir -p /etc/kaskad
cp webui/webui.env.example /etc/kaskad/webui.env
chmod 600 /etc/kaskad/webui.env
# впишите KASKAD_WEB_USER и KASKAD_WEB_PASS
# 5. Cron на failover и refresh доменов
install -m 644 bot/ru-tg-bot.service /etc/systemd/system/
install -m 644 webui/ru-webui.service /etc/systemd/system/
# отредактируйте WorkingDirectory и ExecStart в ru-webui.service → /opt/kaskad/webui
# cron
( crontab -l 2>/dev/null; \
echo '* * * * * /usr/local/bin/ru-failover.py'; \
echo '17 */6 * * * /usr/local/bin/ru-domains.py refresh >> /var/log/ru-domains.log 2>&1' \
) | crontab -
# 6. SSH-ключ бота (нужен для управления остальными серверами через ssh-key auth)
# SSH-ключ для бота
[ -f /root/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
cat /root/.ssh/id_ed25519.pub
# скопировать в /root/.ssh/authorized_keys на всех остальных ам. серверах
# и на всех RU-серверах
# добавьте в /root/.ssh/authorized_keys на остальных ам. и RU серверах
# 7. Установить бот
install -m 755 bot/ru-tg-bot.py /usr/local/bin/
install -m 644 bot/ru-tg-bot.service /etc/systemd/system/
apt install -y python3 sshpass
systemctl daemon-reload
systemctl enable --now ru-tg-bot.service
journalctl -u ru-tg-bot.service -f
```
systemctl enable --now ru-tg-bot.service ru-webui.service
Послать боту в Telegram `/status` — должно прийти текущее состояние.
## Шаг 4. Веб-интерфейс (опционально)
На том же ам. сервере, где бот:
```bash
cd /opt/kaskad
apt install -y python3-flask
install -m 755 webui/app.py /usr/local/bin/ru-webui.py
mkdir -p /usr/local/share/kaskad
cp -r webui/templates webui/static /usr/local/share/kaskad/
# в app.py указать template_folder и static_folder если нужно;
# по умолчанию работает из текущей директории, поэтому проще:
ln -s /usr/local/share/kaskad/templates /usr/local/bin/templates
ln -s /usr/local/share/kaskad/static /usr/local/bin/static
mkdir -p /etc/kaskad
cp webui/webui.env.example /etc/kaskad/webui.env
chmod 600 /etc/kaskad/webui.env
# ВПИСАТЬ KASKAD_WEB_USER и KASKAD_WEB_PASS
install -m 644 webui/ru-webui.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now ru-webui.service
# Открыть порт 8088 (по желанию)
iptables -I INPUT -p tcp --dport 8088 -j ACCEPT
# открываем 8088
iptables -A INPUT -p tcp --dport 8088 -j ACCEPT
netfilter-persistent save
# Открыть в браузере: http://AMS-IP:8088
```
Рекомендуется поставить за HTTPS reverse-proxy (nginx, caddy, traefik) с Let's Encrypt.
Проверьте: `journalctl -u ru-tg-bot -u ru-webui -f`. Отправьте боту `/status` — должен ответить. Откройте `http://AMS_IP:8088`.
## Шаг 5. Дальше
---
Через бот или WebUI:
- `/server-add` — добавить новый RU-сервер
- `/ams-add` — добавить новый ам. сервер
- `/add-domain vk.com ozon.ru` — добавить русские сайты по доменам
- `/add 5.45.192.1/32` — добавить конкретные IP/CIDR
## Дальнейшее расширение
Failover-скрипт каждую минуту проверяет здоровье текущего peer'а и переключает при необходимости.
После того как первая пара RU + ам. работает — добавлять новые **через WebUI или бот**, ничего больше не надо настраивать вручную.
- **+RU-сервер** → в WebUI «+ добавить RU-сервер» (нужен root-пароль для первичной онбординги) или `/server-add` в боте
- **+ам. сервер** → «+ добавить ам. сервер» или `/ams-add`
- **+домен** (vk.com, ozon.ru) → секция «Домены» или `/add-domain vk.com ozon.ru`
- **+IP/CIDR** → секция «Доп. IP/CIDR» или `/add 5.45.192.1/32`
Cron каждую минуту проверяет здоровье текущего RU peer'а через handshake age + TCP-пробу и переключает на следующий по приоритету при необходимости.
---
## Если что-то не работает
См. [docs/troubleshooting.md](troubleshooting.md).
См. [troubleshooting.md](troubleshooting.md).

295
install.sh Executable file
View File

@@ -0,0 +1,295 @@
#!/usr/bin/env bash
# Kaskad — one-command installer.
# Запускается на ПЕРВОМ ам. (зарубежном) сервере, который станет хостом WebUI и TG-бота.
# Сервер уже должен быть настроен как X-ray-нода (3x-ui или ручной xray) с интерфейсом amn0.
#
# curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad/main/install.sh | bash
#
# Или клонируй репо и запусти: sudo bash install.sh
#
# Скрипт интерактивный — задаст 5-6 вопросов и сам всё развернёт.
set -euo pipefail
IFS=$'\n\t'
# --------- цвета и хелперы ---------
RED=$(printf '\033[31m'); GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m')
BLU=$(printf '\033[34m'); BLD=$(printf '\033[1m'); RST=$(printf '\033[0m')
say() { printf "%s==>%s %s\n" "$BLU" "$RST" "$*"; }
ok() { printf "%s✓%s %s\n" "$GRN" "$RST" "$*"; }
warn() { printf "%s!%s %s\n" "$YEL" "$RST" "$*"; }
die() { printf "%s✗%s %s\n" "$RED" "$RST" "$*" >&2; exit 1; }
ask() { local p="$1" def="${2:-}" var; read -r -p "$p${def:+ [$def]}: " var; echo "${var:-$def}"; }
ask_secret() { local p="$1" var; read -r -s -p "$p: " var; echo >&2; echo "$var"; }
[ "$(id -u)" -eq 0 ] || die "запускай под root (или через sudo)"
REPO_URL="${KASKAD_REPO:-https://github.com/andrey271192/kaskad.git}"
KASKAD_DIR="${KASKAD_DIR:-/opt/kaskad}"
cat <<EOF
${BLD}╔══════════════════════════════════════════════════════════════╗
║ Kaskad — установка ║
║ каскадный VPN с авто-failover + WebUI + бот ║
╚══════════════════════════════════════════════════════════════╝${RST}
Запускай этот скрипт на ${BLD}зарубежном (ам.) сервере${RST} — он станет
хостом WebUI и Telegram-бота. Остальные ам. серверы добавишь потом
через WebUI или бот в один клик.
EOF
# --------- 1. собираем входные данные ---------
say "Шаг 1/6: данные для настройки"
TG_BOT_TOKEN=$(ask "Telegram bot token (от @BotFather)")
[ -n "$TG_BOT_TOKEN" ] || die "TG bot token нужен"
TG_CHAT_ID=$(ask "Telegram chat ID для уведомлений (узнай через @userinfobot)")
[ -n "$TG_CHAT_ID" ] || die "TG chat ID нужен"
PUBLIC_IP=$(curl -fsS https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}')
LOCAL_HOST_ID=$(ask "Короткое имя ЭТОГО ам. сервера" "ams1")
LOCAL_IP=$(ask "Внешний IP ЭТОГО ам. сервера" "$PUBLIC_IP")
RU_HOST=$(ask "Хост ПЕРВОГО RU-сервера (IP или домен)")
[ -n "$RU_HOST" ] || die "RU host нужен"
RU_SSH_PORT=$(ask "SSH-порт RU-сервера" "22")
RU_PASS=$(ask_secret "Пароль root@${RU_HOST} (нужен ОДИН раз — для установки SSH-ключа)")
[ -n "$RU_PASS" ] || die "пароль RU нужен (используется только сейчас)"
WEB_USER=$(ask "Логин для WebUI" "admin")
WEB_PASS=$(ask_secret "Пароль для WebUI (минимум 8 символов)")
[ "${#WEB_PASS}" -ge 8 ] || die "пароль WebUI должен быть ≥ 8 символов"
XRAY_IFACE=$(ask "Имя интерфейса X-ray на этом сервере" "amn0")
# --------- 2. ставим пакеты ---------
say "Шаг 2/6: установка пакетов"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq wireguard iptables iptables-persistent python3 python3-flask \
sshpass dnsutils curl git cron >/dev/null
ok "пакеты"
# --------- 3. клонируем репу ---------
say "Шаг 3/6: получаем код Kaskad"
if [ -d "$KASKAD_DIR/.git" ]; then
git -C "$KASKAD_DIR" pull --ff-only >/dev/null
else
git clone --depth 1 "$REPO_URL" "$KASKAD_DIR" >/dev/null 2>&1
fi
ok "$KASKAD_DIR"
# --------- 4. локальные ключи и конфиги ---------
say "Шаг 4/6: WireGuard, ключи, конфиги"
mkdir -p /etc/wireguard /etc/kaskad
chmod 700 /etc/wireguard /etc/kaskad
# WG-ключ для этого ам.
if [ ! -f /etc/wireguard/ru_private.key ]; then
umask 077
wg genkey | tee /etc/wireguard/ru_private.key | wg pubkey > /etc/wireguard/ru_public.key
fi
AMS_PUB=$(cat /etc/wireguard/ru_public.key)
AMS_PRIV=$(cat /etc/wireguard/ru_private.key)
# SSH-ключ для управления остальными серверами
if [ ! -f /root/.ssh/id_ed25519 ]; then
mkdir -p /root/.ssh; chmod 700 /root/.ssh
ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519 -q
fi
BOT_KEY_PUB=$(cat /root/.ssh/id_ed25519.pub)
# Заливаем SSH-ключ на RU через пароль
say " • устанавливаю SSH-ключ на $RU_HOST"
sshpass -p "$RU_PASS" ssh -o StrictHostKeyChecking=accept-new -p "$RU_SSH_PORT" \
"root@$RU_HOST" "mkdir -p /root/.ssh && chmod 700 /root/.ssh && \
grep -qF '$BOT_KEY_PUB' /root/.ssh/authorized_keys 2>/dev/null || \
echo '$BOT_KEY_PUB' >> /root/.ssh/authorized_keys && \
chmod 600 /root/.ssh/authorized_keys" >/dev/null
ok "ssh-ключ установлен"
# --------- настройка RU-сервера, если ещё пуст ---------
say " • настраиваю RU-сервер $RU_HOST"
ssh -p "$RU_SSH_PORT" -o StrictHostKeyChecking=no -o BatchMode=yes "root@$RU_HOST" bash <<'RUSETUP' >/dev/null
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq wireguard iptables iptables-persistent python3 curl >/dev/null
sysctl -w net.ipv4.ip_forward=1 >/dev/null
grep -q '^net.ipv4.ip_forward=1' /etc/sysctl.conf || echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
cd /etc/wireguard
if [ ! -f wg_ru.conf ]; then
umask 077
[ -f ru_private.key ] || wg genkey | tee ru_private.key | wg pubkey > ru_public.key
IFACE=$(ip route | awk '/^default/{print $5; exit}')
PRIVKEY=$(cat ru_private.key)
cat > wg_ru.conf <<EOF
[Interface]
Address = 10.0.0.1/24
PrivateKey = $PRIVKEY
ListenPort = 1939
PostUp = iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE; iptables -I FORWARD 1 -i wg_ru -j ACCEPT; iptables -I FORWARD 1 -o wg_ru -j ACCEPT; iptables -I INPUT -i wg_ru -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o $IFACE -j MASQUERADE; iptables -D FORWARD -i wg_ru -j ACCEPT; iptables -D FORWARD -o wg_ru -j ACCEPT; iptables -D INPUT -i wg_ru -j ACCEPT
EOF
chmod 600 wg_ru.conf
iptables -C INPUT -p udp --dport 1939 -j ACCEPT 2>/dev/null || iptables -A INPUT -p udp --dport 1939 -j ACCEPT
netfilter-persistent save >/dev/null 2>&1 || true
systemctl enable --now wg-quick@wg_ru >/dev/null 2>&1 || wg-quick up wg_ru
fi
RUSETUP
RU_PUB=$(ssh -p "$RU_SSH_PORT" "root@$RU_HOST" cat /etc/wireguard/ru_public.key)
RU_IFACE=$(ssh -p "$RU_SSH_PORT" "root@$RU_HOST" "ip route | awk '/^default/{print \$5; exit}'")
ok "RU настроен, pubkey: ${RU_PUB:0:20}"
# --------- локальный ru.conf на этом ам. ---------
cat > /etc/wireguard/ru.conf <<EOF
[Interface]
Address = 10.0.0.2/32
PrivateKey = $AMS_PRIV
Table = off
PostUp = ip rule add fwmark 100 table 200; ip route add default dev ru table 200; iptables -t nat -A POSTROUTING -o ru -j MASQUERADE
PostDown = ip rule del fwmark 100 table 200 2>/dev/null; ip route flush table 200 2>/dev/null; iptables -t nat -D POSTROUTING -o ru -j MASQUERADE 2>/dev/null
[Peer]
PublicKey = $RU_PUB
Endpoint = $RU_HOST:1939
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
EOF
chmod 600 /etc/wireguard/ru.conf
# Базовая mangle для X-ray → wg
iptables -t mangle -C PREROUTING -i "$XRAY_IFACE" -j MARK --set-mark 100 2>/dev/null || \
iptables -t mangle -A PREROUTING -i "$XRAY_IFACE" -j MARK --set-mark 100
netfilter-persistent save >/dev/null 2>&1 || true
# notify.env
cat > /etc/wireguard/notify.env <<EOF
TG_BOT_TOKEN=$TG_BOT_TOKEN
TG_CHAT_ID=$TG_CHAT_ID
EOF
chmod 600 /etc/wireguard/notify.env
# webui.env
cat > /etc/kaskad/webui.env <<EOF
KASKAD_WEB_USER=$WEB_USER
KASKAD_WEB_PASS=$WEB_PASS
LOCAL_HOST=$LOCAL_HOST_ID
LOCAL_IP=$LOCAL_IP
KASKAD_HOST=0.0.0.0
KASKAD_PORT=8088
KASKAD_SSH_KEY=/root/.ssh/id_ed25519
KASKAD_SERVERS_JSON=/etc/wireguard/ru-servers.json
EOF
chmod 600 /etc/kaskad/webui.env
# ru-servers.json
cat > /etc/wireguard/ru-servers.json <<EOF
{
"servers": [
{"id":"primary","host":"$RU_HOST","endpoint":"$RU_HOST:1939","pubkey":"$RU_PUB",
"probe_port":$RU_SSH_PORT,"priority":1,"label":"$RU_HOST",
"ssh_user":"root","ssh_port":$RU_SSH_PORT,"wg_iface":"$RU_IFACE"}
],
"ams_servers": [
{"id":"$LOCAL_HOST_ID","host":"$LOCAL_IP","ssh_port":22,"tunnel_ip":"10.0.0.2",
"pubkey":"$AMS_PUB","xray_iface":"$XRAY_IFACE","is_local":true}
]
}
EOF
chmod 600 /etc/wireguard/ru-servers.json
# базовые ip-списки
touch /etc/wireguard/ru-extra.list /etc/wireguard/ru-base.aips
[ -s /etc/wireguard/ru-domains.json ] || echo '{}' > /etc/wireguard/ru-domains.json
chmod 600 /etc/wireguard/ru-extra.list /etc/wireguard/ru-base.aips /etc/wireguard/ru-domains.json
ok "локальные конфиги созданы"
# --------- 5. ставим скрипты, юниты, cron ---------
say "Шаг 5/6: скрипты, сервисы, cron"
install -m 755 "$KASKAD_DIR/bin/ru-failover.py" /usr/local/bin/
install -m 755 "$KASKAD_DIR/bin/ru-set.sh" /usr/local/bin/
install -m 755 "$KASKAD_DIR/bin/ru-routes.sh" /usr/local/bin/
install -m 755 "$KASKAD_DIR/bin/ru-domains.py" /usr/local/bin/
install -m 755 "$KASKAD_DIR/bin/add-ru-helper.sh" /usr/local/bin/
install -m 755 "$KASKAD_DIR/bin/add-ams-helper.sh" /usr/local/bin/
install -m 755 "$KASKAD_DIR/bot/ru-tg-bot.py" /usr/local/bin/
# webui — поднимаем из /opt/kaskad/webui
install -m 644 "$KASKAD_DIR/bot/ru-tg-bot.service" /etc/systemd/system/
install -m 644 "$KASKAD_DIR/webui/ru-webui.service" /etc/systemd/system/
# Прокидываем LOCAL_HOST/LOCAL_IP в бот-юнит (там Environment=, мы должны их перезаписать)
sed -i "s|^Environment=LOCAL_HOST=.*$|Environment=LOCAL_HOST=$LOCAL_HOST_ID|" /etc/systemd/system/ru-tg-bot.service || true
sed -i "s|^Environment=LOCAL_IP=.*$|Environment=LOCAL_IP=$LOCAL_IP|" /etc/systemd/system/ru-tg-bot.service || true
grep -q "^Environment=LOCAL_HOST=" /etc/systemd/system/ru-tg-bot.service || \
sed -i "/^EnvironmentFile=/a Environment=LOCAL_HOST=$LOCAL_HOST_ID\nEnvironment=LOCAL_IP=$LOCAL_IP" /etc/systemd/system/ru-tg-bot.service
# cron
( crontab -l 2>/dev/null | grep -v 'ru-failover\|ru-domains' ; \
echo '* * * * * /usr/local/bin/ru-failover.py' ; \
echo '17 */6 * * * /usr/local/bin/ru-domains.py refresh >> /var/log/ru-domains.log 2>&1' \
) | crontab -
# WG up
systemctl enable --now wg-quick@ru >/dev/null 2>&1 || wg-quick up ru
ok "WG-туннель ru поднят"
# Добавляем этот ам. как peer на RU
ssh -p "$RU_SSH_PORT" -o StrictHostKeyChecking=no "root@$RU_HOST" bash <<RUPEER >/dev/null
grep -qF "$AMS_PUB" /etc/wireguard/wg_ru.conf || cat >> /etc/wireguard/wg_ru.conf <<EOF
[Peer]
# $LOCAL_HOST_ID
PublicKey = $AMS_PUB
AllowedIPs = 10.0.0.2/32
EOF
wg set wg_ru peer "$AMS_PUB" allowed-ips 10.0.0.2/32 2>/dev/null || \
(wg-quick down wg_ru; wg-quick up wg_ru)
RUPEER
ok "ам. добавлен как peer на RU"
# --------- 6. поднимаем сервисы ---------
say "Шаг 6/6: запуск бота и WebUI"
# WebUI юнит ссылается на /opt/kaskad-webui — пере-указываем на /opt/kaskad/webui
sed -i "s|WorkingDirectory=.*$|WorkingDirectory=$KASKAD_DIR/webui|" /etc/systemd/system/ru-webui.service
sed -i "s|ExecStart=.*$|ExecStart=/usr/bin/python3 $KASKAD_DIR/webui/app.py|" /etc/systemd/system/ru-webui.service
systemctl daemon-reload
systemctl enable --now ru-tg-bot.service
systemctl enable --now ru-webui.service
sleep 2
# открываем 8088 на firewall (опционально)
iptables -C INPUT -p tcp --dport 8088 -j ACCEPT 2>/dev/null || \
iptables -A INPUT -p tcp --dport 8088 -j ACCEPT
netfilter-persistent save >/dev/null 2>&1 || true
# затираем пароль RU из памяти
unset RU_PASS
cat <<EOF
${GRN}${BLD}✓ Готово!${RST}
WebUI: ${BLD}http://$LOCAL_IP:8088${RST}
Логин: $WEB_USER
Пароль: (тот, что ты ввёл)
Telegram-бот активен — отправь ему ${BLD}/status${RST}.
Файлы:
/opt/kaskad/ — код (git pull чтобы обновить)
/etc/wireguard/ru-servers.json — список серверов
/etc/kaskad/webui.env — настройки WebUI
/etc/wireguard/notify.env — TG token/chat
Дальше:
• открой WebUI и добавь больше ам. серверов / RU-серверов / доменов
• или используй TG-бот: /help
Удалить всё: ${BLD}sudo bash $KASKAD_DIR/uninstall.sh${RST}
EOF

112
uninstall.sh Executable file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# Kaskad — полное удаление.
#
# curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad/main/uninstall.sh | bash
#
# Что удаляется:
# • systemd-юниты ru-tg-bot.service, ru-webui.service
# • скрипты в /usr/local/bin (ru-failover.py, ru-set.sh, ru-routes.sh, ru-domains.py,
# add-ru-helper.sh, add-ams-helper.sh, ru-tg-bot.py)
# • cron-задачи ru-failover и ru-domains
# • WG-туннель ru (на ам.) или wg_ru (на RU)
# • конфиги: /etc/kaskad/, /etc/wireguard/ru-servers.json, ru-extra.list, ru-base.aips,
# ru-domains.json, ru.conf, notify.env
# • директория /opt/kaskad
#
# По умолчанию НЕ трогает: SSH-ключ /root/.ssh/id_ed25519, X-ray, общесистемный iptables-persistent.
# Запросит подтверждение перед удалением.
set -euo pipefail
RED=$(printf '\033[31m'); GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m')
BLU=$(printf '\033[34m'); BLD=$(printf '\033[1m'); RST=$(printf '\033[0m')
say() { printf "%s==>%s %s\n" "$BLU" "$RST" "$*"; }
ok() { printf "%s✓%s %s\n" "$GRN" "$RST" "$*"; }
warn() { printf "%s!%s %s\n" "$YEL" "$RST" "$*"; }
die() { printf "%s✗%s %s\n" "$RED" "$RST" "$*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || die "запускай под root"
FORCE=${KASKAD_FORCE:-0}
PURGE_KEYS=${KASKAD_PURGE_KEYS:-0}
if [ "$FORCE" != "1" ]; then
cat <<EOF
${BLD}Удаление Kaskad${RST}
Будут удалены:
• сервисы: ru-tg-bot.service, ru-webui.service
• скрипты в /usr/local/bin/
• cron-задачи (failover, refresh доменов)
• WG-туннели (ru / wg_ru)
• конфиги: /etc/kaskad/, /etc/wireguard/ru-servers.json, ru.conf, notify.env, ru-base.aips, ru-extra.list, ru-domains.json
• директория /opt/kaskad/
НЕ удаляются (по умолчанию):
• /root/.ssh/id_ed25519 (SSH-ключ — может использоваться чем-то ещё)
• X-ray / 3x-ui
• системные пакеты (wireguard, python3-flask, ...)
EOF
read -r -p "Точно удалить? (введи ${BLD}YES${RST}): " ans
[ "$ans" = "YES" ] || { echo "отмена"; exit 0; }
fi
say "стопаю сервисы"
systemctl disable --now ru-tg-bot.service 2>/dev/null || true
systemctl disable --now ru-webui.service 2>/dev/null || true
rm -f /etc/systemd/system/ru-tg-bot.service /etc/systemd/system/ru-webui.service
systemctl daemon-reload || true
ok "сервисы остановлены"
say "опускаю WG-туннели"
wg-quick down ru 2>/dev/null || true
wg-quick down wg_ru 2>/dev/null || true
systemctl disable wg-quick@ru 2>/dev/null || true
systemctl disable wg-quick@wg_ru 2>/dev/null || true
ok "WG"
say "удаляю скрипты"
rm -f /usr/local/bin/ru-failover.py /usr/local/bin/ru-failover.py.bak.* \
/usr/local/bin/ru-set.sh /usr/local/bin/ru-routes.sh /usr/local/bin/ru-domains.py \
/usr/local/bin/add-ru-helper.sh /usr/local/bin/add-ams-helper.sh \
/usr/local/bin/ru-tg-bot.py
ok "скрипты"
say "удаляю cron-задачи"
( crontab -l 2>/dev/null | grep -v 'ru-failover\|ru-domains' || true ) | crontab - || true
ok "cron"
say "удаляю конфиги"
rm -rf /etc/kaskad
rm -f /etc/wireguard/ru.conf /etc/wireguard/wg_ru.conf \
/etc/wireguard/ru-servers.json /etc/wireguard/ru-extra.list \
/etc/wireguard/ru-base.aips /etc/wireguard/ru-domains.json \
/etc/wireguard/notify.env \
/etc/wireguard/ru.conf.bak.* /etc/wireguard/wg_ru.conf.bak.*
ok "конфиги"
# Опционально — затираем WG-ключи и SSH-ключ бота
if [ "$PURGE_KEYS" = "1" ]; then
say "затираю ключи (KASKAD_PURGE_KEYS=1)"
rm -f /etc/wireguard/ru_private.key /etc/wireguard/ru_public.key
rm -f /root/.ssh/id_ed25519 /root/.ssh/id_ed25519.pub
ok "ключи затёрты"
else
warn "ключи /etc/wireguard/ru_*.key и /root/.ssh/id_ed25519 НЕ удалены"
warn "хочешь грохнуть и их: KASKAD_PURGE_KEYS=1 bash uninstall.sh"
fi
say "удаляю /opt/kaskad"
rm -rf /opt/kaskad /opt/kaskad-webui
ok "/opt/kaskad"
cat <<EOF
${GRN}${BLD}✓ Kaskad удалён.${RST}
Что осталось (можно убрать вручную, если надо):
• системные пакеты: apt purge wireguard sshpass python3-flask
• iptables-persistent: netfilter-persistent save (после очистки правил)
EOF

View File

@@ -395,37 +395,58 @@ systemctl enable wg-quick@ru 2>&1 | tail -1
@app.route("/api/ams/<sid>", methods=["DELETE"])
@require_auth
def api_ams_remove(sid):
data = load_data()
target = next((a for a in data.get("ams_servers", []) if a["id"] == sid or a["host"] == sid), None)
if not target: return jsonify(error="не найден"), 404
if target.get("is_local"): return jsonify(error="нельзя удалить local"), 400
"""Удалить зарубежный (ам.) сервер из каскада.
Шаги:
1) на каждом RU-сервере убрать [Peer] с pubkey удаляемого ам.
2) обновить /etc/wireguard/ru-servers.json и разлить по остальным ам.
3) синхронизировать живой wg-интерфейс на RU через `wg set ... peer ... remove`
?force=1 — продолжить, даже если какой-то RU недоступен.
"""
force = request.args.get("force") in ("1", "true", "yes")
data = load_data()
target = next((a for a in data.get("ams_servers", [])
if a["id"] == sid or a["host"] == sid), None)
if not target:
return jsonify(error=f"ам. сервер '{sid}' не найден"), 404
if target.get("is_local"):
return jsonify(error="нельзя удалить сервер, на котором запущен WebUI"), 400
pubkey = target["pubkey"]
rus = ru_list_data()
peer_results = []
peer_results, hard_fail = [], []
for ru in rus:
cmd = f"""
python3 - <<'PY'
import pathlib, re
p = pathlib.Path('/etc/wireguard/wg_ru.conf')
t = p.read_text()
parts = re.split(r'(\\[Peer\\])', t)
result = parts[0]
i = 1
while i < len(parts):
block = parts[i] + (parts[i+1] if i+1 < len(parts) else '')
if {target['pubkey']!r} in block:
i += 2; continue
result += block
i += 2
p.write_text(result)
PY
wg syncconf wg_ru <(wg-quick strip wg_ru) 2>&1
"""
cmd = (
f"set -e; cd /etc/wireguard; "
f"cp -a wg_ru.conf wg_ru.conf.bak.$(date +%s); "
f"python3 -c \"import pathlib,re,sys; "
f"p=pathlib.Path('wg_ru.conf'); t=p.read_text(); "
f"blocks=re.split(r'(?m)(?=^\\[Peer\\])', t); "
f"kept=[b for b in blocks if {pubkey!r} not in b]; "
f"p.write_text(''.join(kept))\"; "
f"wg set wg_ru peer {shlex.quote(pubkey)} remove 2>/dev/null || true; "
f"echo OK"
)
out_pr, rc_pr = ssh_ru(ru, cmd, timeout=20)
peer_results.append({"ru": ru["id"], "ok": rc_pr == 0})
ok = (rc_pr == 0 and "OK" in (out_pr or ""))
peer_results.append({"ru": ru["id"], "ok": ok, "msg": (out_pr or "")[-200:]})
if not ok:
hard_fail.append(f"{ru['id']}: rc={rc_pr} {(out_pr or '')[-120:]}")
if hard_fail and not force:
return jsonify(
error=("не удалось убрать peer на: " + "; ".join(hard_fail) +
". Повторите с ?force=1 чтобы удалить запись принудительно."),
peers=peer_results,
), 502
data["ams_servers"] = [a for a in data["ams_servers"] if a["id"] != target["id"]]
save_and_distribute(data)
return jsonify(removed=target["id"], peers=peer_results)
ok, err = save_and_distribute(data)
if not ok and not force:
return jsonify(error=f"sync конфига: {err}", peers=peer_results), 500
return jsonify(removed=target["id"], peers=peer_results, force=force)
@app.route("/api/domains", methods=["POST"])

View File

@@ -51,6 +51,17 @@ button:hover:not(:disabled) { background: #30363d; border-color: #58a6ff; color:
button:disabled { opacity: 0.4; cursor: not-allowed; }
button.danger { color: #f85149; border-color: #4a1f23; }
button.danger:hover:not(:disabled) { background: #4a1f23; color: #fff; border-color: #f85149; }
button.danger.solid {
background: #b1361a;
color: #fff;
border-color: #f85149;
font-weight: 600;
}
button.danger.solid:hover:not(:disabled) {
background: #f85149;
color: #fff;
border-color: #f85149;
}
button.sm { padding: 2px 8px; font-size: 11px; }
.actions {

View File

@@ -79,18 +79,25 @@
<!-- Ам. серверы -->
<section>
<h2>Ам. серверы</h2>
<h2>Ам. серверы (зарубежные)</h2>
<table>
<thead><tr><th>ID</th><th>Host</th><th>SSH</th><th>Tunnel</th><th>Pubkey</th><th></th></tr></thead>
<tbody>
<template x-for="a in state.ams_servers" :key="a.id">
<tr>
<td><strong x-text="a.id"></strong> <span class="muted" x-show="a.is_local">(local)</span></td>
<td><strong x-text="a.id"></strong> <span class="muted" x-show="a.is_local">(этот сервер)</span></td>
<td x-text="a.host"></td>
<td x-text="a.ssh_port"></td>
<td x-text="a.tunnel_ip"></td>
<td class="muted mono" x-text="a.pubkey.slice(0, 20) + '...'"></td>
<td><button class="danger" @click="removeAms(a.id)" :disabled="a.is_local">удалить</button></td>
<td>
<template x-if="!a.is_local">
<button class="danger solid" @click="removeAms(a.id)">удалить</button>
</template>
<template x-if="a.is_local">
<span class="muted" title="нельзя удалить сервер, на котором запущен WebUI/бот">⛔ нельзя</span>
</template>
</td>
</tr>
</template>
</tbody>
@@ -258,8 +265,18 @@
this.load();
},
async removeAms(id) {
if (!confirm(`Удалить ам. '${id}'? Snimet peer на всех RU.`)) return;
await this.req('DELETE',`/api/ams/${id}`); this.flash('удалено'); this.load();
if (!confirm(`Удалить зарубежный сервер '${id}'?\n\nЧто произойдёт:\n • peer уберётся со всех RU-серверов\n • запись удалится из конфига\n • сам сервер ${id} останется жив, но больше не будет частью каскада\n\nПродолжить?`)) return;
try {
const r = await this.req('DELETE',`/api/ams/${id}`);
const peers = (r.peers||[]).map(p => `${p.ru}: ${p.ok?'✓':'✗'}`).join(', ');
this.flash(`удалён ${id}${peers ? ' ('+peers+')' : ''}`);
} catch (e) {
if (confirm(`Ошибка: ${e.message}\n\nУдалить запись принудительно (force, даже если RU-серверы недоступны)?`)) {
await this.req('DELETE',`/api/ams/${id}?force=1`);
this.flash(`удалён ${id} (force)`);
}
}
this.load();
},
async addDomains() {
const doms = this.forms.domains.split(/[\s,]+/).map(s=>s.trim()).filter(Boolean);