mirror of
https://github.com/andrey271192/kaskad.git
synced 2026-09-20 13:49:56 +00:00
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:
214
README.md
214
README.md
@@ -1,104 +1,200 @@
|
|||||||
# Kaskad
|
# Kaskad
|
||||||
|
|
||||||
Каскадная маршрутизация русских сайтов через свой набор RU-серверов (WireGuard) с автоматическим failover, Telegram-ботом и веб-интерфейсом.
|
**Свой VPN для россиян за границей.** Российские сайты (госуслуги, банки, ВК) открываются через ваш сервер в РФ. Всё остальное — напрямую через зарубежный. Если RU-сервер падает — за минуту переключается на запасной. Управляется из браузера или Telegram.
|
||||||
|
|
||||||
## Что это
|
  
|
||||||
|
|
||||||
Несколько зарубежных серверов с X-ray («ам.») держат WireGuard-туннели к одному из RU-серверов. Российские IP/домены идут через RU, всё остальное — через зарубежный. Если RU упал — за минуту переключение на следующий по приоритету. Когда восстановился — пробный failback с автоматическим откатом если WG не отвечает.
|
---
|
||||||
|
|
||||||
|
## Зачем это нужно
|
||||||
|
|
||||||
|
Живёте за границей и сталкивались с тем, что:
|
||||||
|
|
||||||
|
- 🏦 Тинькофф не пускает с заграничного IP
|
||||||
|
- 🧾 Госуслуги «вы не из России»
|
||||||
|
- 📺 Кинопоиск, ivi, ВК — недоступны
|
||||||
|
- 🛍 Озон, Wildberries — корзина не работает
|
||||||
|
|
||||||
|
При этом обычный VPN в РФ **тормозит всё подряд**, включая Netflix, рабочие звонки и игры.
|
||||||
|
|
||||||
|
**Каскад** делит трафик по умному:
|
||||||
|
|
||||||
```
|
```
|
||||||
телефон/клиент ──> ам. сервер (X-ray) ──┬──> RU primary ──> RU-сайты
|
┌──────────────────────────────────────────┐
|
||||||
├──> RU backup (failover)
|
вы ───▶ │ зарубежный сервер (Амстердам, X-ray) │
|
||||||
└──> RU N (по приоритету)
|
│ │
|
||||||
└──> остальное идёт напрямую через ам.
|
│ если адрес РФ → 🇷🇺 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
|
|
||||||
|
|
||||||
## Структура репозитория
|
## Что умеет
|
||||||
|
|
||||||
```
|
- ⚡ **Авто-failover** — если основной RU-сервер упал, за минуту переключение на запасной. Когда вернулся — автоматически обратно.
|
||||||
kaskad/
|
- 🎛 **WebUI** — добавлять серверы, домены, IP в один клик. Никакого SSH.
|
||||||
├── bin/ # скрипты для ам. серверов (failover, switch, routes, domains)
|
- 🤖 **Telegram-бот** — то же самое из телефона.
|
||||||
├── bot/ # Telegram-бот + systemd unit
|
- 🌐 **N RU-серверов** в каскаде с приоритетами.
|
||||||
├── webui/ # Flask-приложение
|
- 🌍 **N зарубежных нод** под одним каскадом — все автоматически переключаются вместе.
|
||||||
├── examples/ # шаблоны конфигов
|
- 🔔 **Уведомления в Telegram** при каждом переключении.
|
||||||
├── docs/ # подробная документация
|
- 📋 **Маршруты по доменам** — ввёл `vk.com`, бот сам резолвит IP и держит их в маршрутах.
|
||||||
└── README.md
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Установка в одну команду
|
||||||
|
|
||||||
|
На зарубежном сервере (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-сервер»)
|
- 🌐 WebUI на `http://ВАШ_СЕРВЕР:8088`
|
||||||
2. **Настроить первый ам. сервер** (см. § «Ам. сервер»)
|
- 🤖 Активного Telegram-бота — `/status` покажет состояние
|
||||||
3. **Создать `/etc/wireguard/ru-servers.json`** по шаблону `examples/ru-servers.example.json`
|
- 🔁 Авто-failover каждую минуту через cron
|
||||||
4. **Запустить TG-бот** на одном из ам. серверов (`bot/ru-tg-bot.service`)
|
|
||||||
5. **(опц.) Запустить WebUI** на том же ам. сервере (`webui/ru-webui.service`)
|
|
||||||
6. Дальше — добавлять RU/ам./домены/IP через бот или WebUI
|
|
||||||
|
|
||||||
## Команды бота
|
Готово. Никаких ручных правок конфигов.
|
||||||
|
|
||||||
|
> 💡 **Если 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` |
|
| Туннели | `/status`, `/use <id>`, `/primary`, `/backup` |
|
||||||
| RU-серверы | `/server-list`, `/server-add`, `/server-remove`, `/bot-key` |
|
| RU-серверы | `/server-list`, `/server-add`, `/server-remove`, `/bot-key` |
|
||||||
| Ам. серверы | `/ams-list`, `/ams-add`, `/ams-remove` |
|
| Ам. серверы | `/ams-list`, `/ams-add`, `/ams-remove` |
|
||||||
| Маршруты | `/ips`, `/list`, `/add <IP/CIDR>`, `/remove <IP/CIDR>`, `/clear` |
|
| Маршруты | `/ips`, `/list`, `/add <IP>`, `/remove <IP>`, `/clear` |
|
||||||
| Домены | `/list-domains`, `/add-domain <domain>`, `/remove-domain <domain>`, `/show-domain <domain>`, `/refresh-domains` |
|
| Домены | `/list-domains`, `/add-domain <d>`, `/remove-domain <d>`, `/show-domain <d>`, `/refresh-domains` |
|
||||||
| Прочее | `/help` |
|
| Прочее | `/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`, **не коммитятся** в репо
|
- Все конфиги (`ru-servers.json`, `notify.env`, `webui.env`) с правами `600`, **не коммитятся**.
|
||||||
- SSH между серверами — по ключам; пароли используются только при первичной онбординге нового сервера через `password=` в боте/WebUI и **сразу затираются** после установки ключа
|
- Между серверами — только SSH-ключи. Пароли используются один раз, при добавлении нового сервера, и **сразу затираются**.
|
||||||
- Веб-интерфейс через basic auth, рекомендуется ставить за HTTPS reverse-proxy (nginx/caddy)
|
- WebUI защищён HTTP basic auth. Рекомендация: поставьте за HTTPS reverse-proxy (Caddy, Nginx, Traefik).
|
||||||
- Telegram-бот принимает команды только от заранее заданного `TG_CHAT_ID`
|
- 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-unified](https://github.com/andrey271192/keenetic-unified) — мониторинг роутеров Keenetic
|
||||||
- 🌐 **[keenetic_ssh-web](https://github.com/andrey271192/keenetic_ssh-web)** — веб-панель shell-команд на самом роутере
|
- 🌐 [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-dns-routes](https://github.com/andrey271192/keenetic-dns-routes) — DNS-маршрутизация без HydraRoute Neo
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Поддержка проекта
|
## Поддержать проект
|
||||||
|
|
||||||
Если **Kaskad** оказался полезен — поддержи разработку:
|
- ⭐ Поставьте звезду на [andrey271192/kaskad](https://github.com/andrey271192/kaskad)
|
||||||
|
- 💖 [Boosty](https://boosty.to/andrey27/donate)
|
||||||
- ⭐ **GitHub:** [andrey271192/kaskad](https://github.com/andrey271192/kaskad) — поставь звезду
|
- 💳 [Ozon Bank СБП](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798)
|
||||||
- 💖 **Boosty:** [boosty.to/andrey27/donate](https://boosty.to/andrey27/donate)
|
- ✉️ Telegram: [@Iot_andrey](https://t.me/Iot_andrey) — вопросы, баги, идеи
|
||||||
- 💳 **Ozon Bank (СБП):** [перевод](https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798)
|
|
||||||
- ✉️ **Telegram:** [@Iot_andrey](https://t.me/Iot_andrey) — вопросы, предложения, баги
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
||||||
|
|||||||
221
docs/install.md
221
docs/install.md
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт интерактивный, спросит 5–6 вопросов:
|
||||||
|
|
||||||
|
| Вопрос | Что вводить |
|
||||||
|
|---|---|
|
||||||
|
| 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` |
|
||||||
|
|
||||||
|
Через 2–3 минуты:
|
||||||
|
|
||||||
|
```
|
||||||
|
✓ Готово!
|
||||||
|
|
||||||
|
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:
|
На RU-сервере под root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
apt update && apt install -y wireguard iptables-persistent
|
apt update && apt install -y wireguard iptables-persistent
|
||||||
|
|
||||||
# 1. Генерация ключа
|
|
||||||
cd /etc/wireguard
|
cd /etc/wireguard
|
||||||
umask 077
|
umask 077
|
||||||
wg genkey | tee ru_private.key | wg pubkey > ru_public.key
|
wg genkey | tee ru_private.key | wg pubkey > ru_public.key
|
||||||
PRIVKEY=$(cat ru_private.key)
|
PRIVKEY=$(cat ru_private.key)
|
||||||
|
|
||||||
# 2. ip_forward
|
|
||||||
sysctl -w net.ipv4.ip_forward=1
|
sysctl -w net.ipv4.ip_forward=1
|
||||||
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
|
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
|
||||||
|
|
||||||
# 3. Конфиг — пока без peer'ов, добавятся когда подключим первый ам.
|
|
||||||
IFACE=$(ip route | awk '/^default/{print $5; exit}')
|
IFACE=$(ip route | awk '/^default/{print $5; exit}')
|
||||||
cat > /etc/wireguard/wg_ru.conf <<EOF
|
cat > /etc/wireguard/wg_ru.conf <<EOF
|
||||||
[Interface]
|
[Interface]
|
||||||
@@ -36,181 +104,144 @@ PostDown = iptables -t nat -D POSTROUTING -o $IFACE -j MASQUERADE; iptables -D F
|
|||||||
EOF
|
EOF
|
||||||
chmod 600 /etc/wireguard/wg_ru.conf
|
chmod 600 /etc/wireguard/wg_ru.conf
|
||||||
|
|
||||||
# 4. Открыть порт 1939/UDP
|
|
||||||
iptables -A INPUT -p udp --dport 1939 -j ACCEPT
|
iptables -A INPUT -p udp --dport 1939 -j ACCEPT
|
||||||
netfilter-persistent save
|
netfilter-persistent save
|
||||||
|
|
||||||
# 5. Запуск + автостарт
|
|
||||||
wg-quick up wg_ru
|
wg-quick up wg_ru
|
||||||
systemctl enable wg-quick@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:
|
На ам. сервере под root:
|
||||||
|
|
||||||
```bash
|
```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
|
cd /etc/wireguard
|
||||||
umask 077
|
umask 077
|
||||||
wg genkey | tee ru_private.key | wg pubkey > ru_public.key
|
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 '@'
|
ip a | grep -E 'amn|tun' | grep -v '@'
|
||||||
# обычно amn0 — используем дальше
|
# обычно amn0
|
||||||
|
|
||||||
# 3. Конфиг ru.conf — IP в туннеле = 10.0.0.2 (первый ам.)
|
|
||||||
PRIVKEY=$(cat ru_private.key)
|
PRIVKEY=$(cat ru_private.key)
|
||||||
cat > /etc/wireguard/ru.conf <<EOF
|
cat > /etc/wireguard/ru.conf <<EOF
|
||||||
[Interface]
|
[Interface]
|
||||||
Address = 10.0.0.2/32
|
Address = 10.0.0.2/32
|
||||||
PrivateKey = $PRIVKEY
|
PrivateKey = $PRIVKEY
|
||||||
Table = off
|
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
|
PostDown = ip rule del fwmark 100 table 200; ip route flush table 200; iptables -t nat -D POSTROUTING -o ru -j MASQUERADE
|
||||||
[Peer]
|
[Peer]
|
||||||
PublicKey = ПУБЛИЧНЫЙ_КЛЮЧ_RU_СЕРВЕРА
|
PublicKey = ПУБЛИЧНЫЙ_КЛЮЧ_RU
|
||||||
Endpoint = ВНЕШНИЙ_IP_RU:1939
|
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
|
PersistentKeepalive = 25
|
||||||
EOF
|
EOF
|
||||||
chmod 600 /etc/wireguard/ru.conf
|
chmod 600 /etc/wireguard/ru.conf
|
||||||
|
|
||||||
# 4. Mangle для X-ray трафика — за каждой подсетью
|
# 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 -j MARK --set-mark 100
|
||||||
iptables -t mangle -A PREROUTING -i amn0 -d $net -j MARK --set-mark 100
|
|
||||||
done
|
|
||||||
netfilter-persistent save
|
netfilter-persistent save
|
||||||
|
|
||||||
# 5. Запуск
|
|
||||||
wg-quick up ru
|
wg-quick up ru
|
||||||
systemctl enable wg-quick@ru
|
systemctl enable wg-quick@ru
|
||||||
wg show ru # должен быть handshake
|
wg show ru # должен быть handshake
|
||||||
```
|
```
|
||||||
|
|
||||||
На RU-сервере добавить этот ам. как peer:
|
На RU-сервере добавьте этот ам. как peer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wg set wg_ru peer ПУБЛИЧНЫЙ_КЛЮЧ_АМ allowed-ips 10.0.0.2/32
|
|
||||||
cat >> /etc/wireguard/wg_ru.conf <<EOF
|
cat >> /etc/wireguard/wg_ru.conf <<EOF
|
||||||
|
|
||||||
[Peer]
|
[Peer]
|
||||||
PublicKey = ПУБЛИЧНЫЙ_КЛЮЧ_АМ
|
PublicKey = ПУБЛИЧНЫЙ_КЛЮЧ_АМ
|
||||||
AllowedIPs = 10.0.0.2/32
|
AllowedIPs = 10.0.0.2/32
|
||||||
EOF
|
EOF
|
||||||
|
wg set wg_ru peer ПУБЛИЧНЫЙ_КЛЮЧ_АМ allowed-ips 10.0.0.2/32
|
||||||
```
|
```
|
||||||
|
|
||||||
Проверить что трафик идёт:
|
Проверьте на ам.:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# на ам. сервере
|
|
||||||
curl --interface ru https://gosuslugi.ru -I
|
curl --interface ru https://gosuslugi.ru -I
|
||||||
# должен ответить HTTP/... 200
|
# должен ответить HTTP/... 200
|
||||||
```
|
```
|
||||||
|
|
||||||
## Шаг 3. Установка скриптов и бота
|
### Шаг 3. Скрипты, бот, WebUI
|
||||||
|
|
||||||
На ам. сервере где будет жить бот (в Амстердаме, Telegram должен быть доступен!):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/andrey271192/kaskad.git /opt/kaskad
|
git clone https://github.com/andrey271192/kaskad.git /opt/kaskad
|
||||||
cd /opt/kaskad
|
cd /opt/kaskad
|
||||||
|
|
||||||
# 1. Скопировать скрипты
|
|
||||||
install -m 755 bin/ru-failover.py /usr/local/bin/
|
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-set.sh /usr/local/bin/
|
||||||
install -m 755 bin/ru-routes.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/ru-domains.py /usr/local/bin/
|
||||||
install -m 755 bin/add-ru-helper.sh /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 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
|
cp examples/notify.env.example /etc/wireguard/notify.env
|
||||||
chmod 600 /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
|
cp examples/ru-servers.example.json /etc/wireguard/ru-servers.json
|
||||||
chmod 600 /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)
|
mkdir -p /etc/kaskad
|
||||||
# создаст ru-base.aips автоматически
|
cp webui/webui.env.example /etc/kaskad/webui.env
|
||||||
/usr/local/bin/ru-routes.sh apply
|
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; \
|
( crontab -l 2>/dev/null; \
|
||||||
echo '* * * * * /usr/local/bin/ru-failover.py'; \
|
echo '* * * * * /usr/local/bin/ru-failover.py'; \
|
||||||
echo '17 */6 * * * /usr/local/bin/ru-domains.py refresh >> /var/log/ru-domains.log 2>&1' \
|
echo '17 */6 * * * /usr/local/bin/ru-domains.py refresh >> /var/log/ru-domains.log 2>&1' \
|
||||||
) | crontab -
|
) | crontab -
|
||||||
|
|
||||||
# 6. SSH-ключ бота (нужен для управления остальными серверами через ssh-key auth)
|
# SSH-ключ для бота
|
||||||
[ -f /root/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
|
[ -f /root/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
|
||||||
cat /root/.ssh/id_ed25519.pub
|
cat /root/.ssh/id_ed25519.pub
|
||||||
# скопировать в /root/.ssh/authorized_keys на всех остальных ам. серверах
|
# добавьте в /root/.ssh/authorized_keys на остальных ам. и RU серверах
|
||||||
# и на всех 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 daemon-reload
|
||||||
systemctl enable --now ru-tg-bot.service
|
systemctl enable --now ru-tg-bot.service ru-webui.service
|
||||||
journalctl -u ru-tg-bot.service -f
|
|
||||||
```
|
|
||||||
|
|
||||||
Послать боту в Telegram `/status` — должно прийти текущее состояние.
|
# открываем 8088
|
||||||
|
iptables -A INPUT -p tcp --dport 8088 -j ACCEPT
|
||||||
## Шаг 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
|
|
||||||
netfilter-persistent save
|
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
295
install.sh
Executable 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
112
uninstall.sh
Executable 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
|
||||||
73
webui/app.py
73
webui/app.py
@@ -395,37 +395,58 @@ systemctl enable wg-quick@ru 2>&1 | tail -1
|
|||||||
@app.route("/api/ams/<sid>", methods=["DELETE"])
|
@app.route("/api/ams/<sid>", methods=["DELETE"])
|
||||||
@require_auth
|
@require_auth
|
||||||
def api_ams_remove(sid):
|
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()
|
rus = ru_list_data()
|
||||||
peer_results = []
|
peer_results, hard_fail = [], []
|
||||||
|
|
||||||
for ru in rus:
|
for ru in rus:
|
||||||
cmd = f"""
|
cmd = (
|
||||||
python3 - <<'PY'
|
f"set -e; cd /etc/wireguard; "
|
||||||
import pathlib, re
|
f"cp -a wg_ru.conf wg_ru.conf.bak.$(date +%s); "
|
||||||
p = pathlib.Path('/etc/wireguard/wg_ru.conf')
|
f"python3 -c \"import pathlib,re,sys; "
|
||||||
t = p.read_text()
|
f"p=pathlib.Path('wg_ru.conf'); t=p.read_text(); "
|
||||||
parts = re.split(r'(\\[Peer\\])', t)
|
f"blocks=re.split(r'(?m)(?=^\\[Peer\\])', t); "
|
||||||
result = parts[0]
|
f"kept=[b for b in blocks if {pubkey!r} not in b]; "
|
||||||
i = 1
|
f"p.write_text(''.join(kept))\"; "
|
||||||
while i < len(parts):
|
f"wg set wg_ru peer {shlex.quote(pubkey)} remove 2>/dev/null || true; "
|
||||||
block = parts[i] + (parts[i+1] if i+1 < len(parts) else '')
|
f"echo OK"
|
||||||
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
|
|
||||||
"""
|
|
||||||
out_pr, rc_pr = ssh_ru(ru, cmd, timeout=20)
|
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"]]
|
data["ams_servers"] = [a for a in data["ams_servers"] if a["id"] != target["id"]]
|
||||||
save_and_distribute(data)
|
ok, err = save_and_distribute(data)
|
||||||
return jsonify(removed=target["id"], peers=peer_results)
|
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"])
|
@app.route("/api/domains", methods=["POST"])
|
||||||
|
|||||||
@@ -51,6 +51,17 @@ button:hover:not(:disabled) { background: #30363d; border-color: #58a6ff; color:
|
|||||||
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
button.danger { color: #f85149; border-color: #4a1f23; }
|
button.danger { color: #f85149; border-color: #4a1f23; }
|
||||||
button.danger:hover:not(:disabled) { background: #4a1f23; color: #fff; border-color: #f85149; }
|
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; }
|
button.sm { padding: 2px 8px; font-size: 11px; }
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
|||||||
@@ -79,18 +79,25 @@
|
|||||||
|
|
||||||
<!-- Ам. серверы -->
|
<!-- Ам. серверы -->
|
||||||
<section>
|
<section>
|
||||||
<h2>Ам. серверы</h2>
|
<h2>Ам. серверы (зарубежные)</h2>
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>ID</th><th>Host</th><th>SSH</th><th>Tunnel</th><th>Pubkey</th><th></th></tr></thead>
|
<thead><tr><th>ID</th><th>Host</th><th>SSH</th><th>Tunnel</th><th>Pubkey</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template x-for="a in state.ams_servers" :key="a.id">
|
<template x-for="a in state.ams_servers" :key="a.id">
|
||||||
<tr>
|
<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.host"></td>
|
||||||
<td x-text="a.ssh_port"></td>
|
<td x-text="a.ssh_port"></td>
|
||||||
<td x-text="a.tunnel_ip"></td>
|
<td x-text="a.tunnel_ip"></td>
|
||||||
<td class="muted mono" x-text="a.pubkey.slice(0, 20) + '...'"></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>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -258,8 +265,18 @@
|
|||||||
this.load();
|
this.load();
|
||||||
},
|
},
|
||||||
async removeAms(id) {
|
async removeAms(id) {
|
||||||
if (!confirm(`Удалить ам. '${id}'? Snimet peer на всех RU.`)) return;
|
if (!confirm(`Удалить зарубежный сервер '${id}'?\n\nЧто произойдёт:\n • peer уберётся со всех RU-серверов\n • запись удалится из конфига\n • сам сервер ${id} останется жив, но больше не будет частью каскада\n\nПродолжить?`)) return;
|
||||||
await this.req('DELETE',`/api/ams/${id}`); this.flash('удалено'); this.load();
|
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() {
|
async addDomains() {
|
||||||
const doms = this.forms.domains.split(/[\s,]+/).map(s=>s.trim()).filter(Boolean);
|
const doms = this.forms.domains.split(/[\s,]+/).map(s=>s.trim()).filter(Boolean);
|
||||||
|
|||||||
Reference in New Issue
Block a user