Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c1c82afec | ||
|
|
0f98c074c8 | ||
|
|
286142bd7a | ||
|
|
cf4b5f79b5 | ||
|
|
5f97c6c6d7 | ||
|
|
14bb6ae589 | ||
|
|
51712310f6 | ||
|
|
6ec1782d8b | ||
|
|
e61ec09b18 | ||
|
|
ed3071a971 | ||
|
|
db4cab7a38 | ||
|
|
9f402be886 | ||
|
|
8971565e6e | ||
|
|
b6525bc0ae | ||
|
|
d072c8227f | ||
|
|
7b941dc532 | ||
|
|
8c1654ecaa | ||
|
|
dc83d90a35 | ||
|
|
e0ea54e83a | ||
|
|
628973374e | ||
|
|
041a533457 | ||
|
|
48ae0c2fd8 | ||
|
|
73af670f59 | ||
|
|
c48d727f6c | ||
|
|
b37b4cc1a3 |
70
CHANGELOG.md
Normal file
70
CHANGELOG.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Changelog — PCA Phobos
|
||||
|
||||
Каналы: **stable** (публичный, без токена) · **beta** / **dev** (приватный repo, токен подписчика).
|
||||
Обновление: `phobos-update stable` · `GH_TOKEN=… phobos-update beta|dev` · из панели → «Версия и обновления».
|
||||
|
||||
---
|
||||
|
||||
## Как обновиться после фиксов
|
||||
|
||||
```bash
|
||||
# stable (открытый)
|
||||
phobos-update stable
|
||||
|
||||
# beta/dev (токен уже сохранён в /opt/phobos-panel/.gh_token)
|
||||
phobos-update beta # или: phobos-update dev
|
||||
```
|
||||
Затем **пересоздать клиентов** в панели (старые конфиги могли содержать неверный порт обфускатора).
|
||||
Обновляются: панель, скрипты онбординга, агент, updater. Ключи WireGuard, `server.env`, клиенты — НЕ трогаются.
|
||||
|
||||
---
|
||||
|
||||
## stable
|
||||
|
||||
### v1.2.7
|
||||
- **fix:** импорт `phobos://` в PhobosWG. Конфиги дополняются `= none` для недостающих полей (`PresharedKey` и др.) перед base64 — иначе приложение писало «невозможно импортировать туннель».
|
||||
|
||||
### v1.2.6
|
||||
- **fix:** бинарники wg-obfuscator для роутеров. Keenetic = aarch64, а Ground-Zerro раздаёт только x86_64. Теперь install.sh и сборка пакета клиента авто-качают aarch64/mipsel/armv7 из ClusterM/wg-obfuscator releases.
|
||||
|
||||
### v1.2.5
|
||||
- **feat:** поле токена подписчика + кнопки каналов (stable/beta/dev) прямо в карточке «Версия и обновления». Подписчик вставляет токен в панель и переключается на beta/dev без консоли.
|
||||
|
||||
### v1.2.4
|
||||
- **feat:** установка по умолчанию стабильная; beta/dev — опционально (`CHANNEL=`).
|
||||
|
||||
### v1.2.3
|
||||
- **fix:** дебаунс DOWN-алертов (3 цикла + повтор) — убирает ложный флап серверов.
|
||||
- **fix:** `phobos-update` отменяет обновление на несуществующую версию (текущая не трогается).
|
||||
- **feat:** `--check` / `--versions`, кнопка «Проверить обновления».
|
||||
|
||||
### v1.2.2
|
||||
- контакт во всех местах → группа `@PCAdministration`.
|
||||
|
||||
---
|
||||
|
||||
## Критичные фиксы (общие, во всех каналах)
|
||||
|
||||
| Версия | Проблема | Решение |
|
||||
|--------|----------|---------|
|
||||
| 1.2.7 / 1.3.x | PhobosWG: «невозможно импортировать туннель» | `phobos://` дополняется `= none` для всех обязательных полей (`PresharedKey`) по спеке формата |
|
||||
| 1.2.6 / 1.3.x | Роутер aarch64: «бинарник не найден в архиве» | авто-загрузка aarch64/mipsel/armv7 из ClusterM releases |
|
||||
| (внутр.) | Клиент не подключается, «НЕТ СОЕДИНЕНИЯ», handshake не встаёт | клиентский обфускатор целился в порт 51821 (никто не слушал); теперь берёт первый из `OBFUSCATOR_PORTS` (2083) |
|
||||
| (внутр.) | агент secondary не обновлялся через `phobos-update` | `server/api.py` добавлен в managed_files |
|
||||
|
||||
---
|
||||
|
||||
## beta (приватный)
|
||||
|
||||
### v1.3.0-beta
|
||||
- скачивание `.conf` для телефона (Android `phobos://` + QR, iOS обычный WireGuard + QR);
|
||||
- кнопка скачивания приложения **PhobosWG (APK)** — авто-загрузка из GitHub Release приватного repo;
|
||||
- все критичные фиксы выше.
|
||||
|
||||
---
|
||||
|
||||
## dev (приватный, активная разработка)
|
||||
|
||||
### v1.3.1-dev
|
||||
- **баланс по трафику:** агенты отдают пропускную способность канала (rx/tx NIC), `server_load = 0.45·cpu + 0.30·mem + 0.25·bw`; карточки серверов показывают ↕ Mbps;
|
||||
- все фиксы и фичи из beta.
|
||||
289
README.md
289
README.md
@@ -1,133 +1,197 @@
|
||||
# PCA Phobos — Web Panel
|
||||
# PCA Phobos — VPN, который не отключается
|
||||
|
||||
Веб-панель управления для [Phobos](https://git.zerrolabs.org/Ground-Zerro/Phobos) (обфусцированный WireGuard VPN).
|
||||
**Веб-панель и turnkey-установщик для [Phobos](https://github.com/Ground-Zerro/Phobos)** — обфусцированного WireGuard VPN. Ставится на чистый VPS одной командой, управляет десятками роутеров и устройств с одной страницы, сам переключается между серверами при сбоях и восстанавливается после перезагрузок.
|
||||
|
||||
## Быстрый старт (turnkey — с чистого VPS, одной командой)
|
||||
<p align="center">
|
||||
<img src="docs/img/sessions.png" alt="Панель — активные сессии" width="850">
|
||||
</p>
|
||||
|
||||
Ставит ВСЁ со всеми зависимостями: wg-obfuscator, WireGuard, обфускатор-сервисы,
|
||||
веб-панель, nginx, скрипты онбординга роутеров и сторож авто-восстановления.
|
||||
Предустановленный Phobos НЕ требуется.
|
||||
---
|
||||
|
||||
## Что это простыми словами
|
||||
|
||||
Обычный VPN падает, когда падает сервер или провайдер банит IP. **PCA Phobos — нет.**
|
||||
|
||||
- 🌐 **Маскированный WireGuard** — трафик не отличить от обычного, проходит мимо DPI и блокировок по сигнатуре.
|
||||
- 🔁 **Авто-переключение между серверами** — сервер лёг или порт прикрыли → роутер сам прыгает на резервный за ~10–15 секунд, без твоего участия.
|
||||
- 📡 **Работает за любым NAT** — серый IP, мобильный интернет, домашний роутер за провайдером. Управление едет внутри самого защищённого туннеля, поэтому бан публичного IP ему не страшен.
|
||||
- 🖥️ **Одна команда — новое устройство** — создал клиента в панели → вставил команду на роутер → готово.
|
||||
- ♻️ **Само-восстановление** — роутер перезагрузился, провайдер сменил IP, сервис отвалился → система поднимет всё обратно сама.
|
||||
- 📱 **Телефоны** — Android (PhobosWG, с обфускацией) и iPhone (обычный WireGuard) через QR-код.
|
||||
|
||||
---
|
||||
|
||||
## Возможности
|
||||
|
||||
- **Активные сессии** — VPN IP, Real IP, метка, handshake, трафик RX/TX, Kick.
|
||||
- **Клиенты VPN** — добавление/удаление, статус online (по handshake на любом сервере), назначение сервера.
|
||||
- **Мульти-сервер + failover** — приоритеты, балансировка по нагрузке (CPU/RAM), резервные серверы; роутеры переключаются автоматически.
|
||||
- **NAT-friendly управление** — роутеры сами тянут конфиг по туннелю (`10.25.0.1`), переживает бан публичного IP.
|
||||
- **Авто-восстановление роутеров** — серверный сторож (watchdog) поднимает роутер после неудачной перезагрузки через KeenDNS.
|
||||
- **Конфиги для телефона** — 📱 Android `phobos://` + QR, 🍎 iPhone обычный WireGuard + QR.
|
||||
- **Метки** — имя объекта по IP (видно в сессиях и Telegram).
|
||||
- **Срок подписки** — дата окончания, обратный отсчёт, автокик + предупреждения.
|
||||
- **Telegram-уведомления** — подключение/отключение, сервер вверх/вниз, истечение подписки, авто-восстановление.
|
||||
- **Двуязычный интерфейс** RU/EN + подсказки `?` у каждого действия.
|
||||
- **Версии и обновления** — обновление и откат прямо из панели.
|
||||
- **Уведомления per-client** — 🔔 у каждого клиента: включить/выключить Telegram-оповещения о подключении/отключении.
|
||||
- **Telegram-бот** — управление клиентами командами прямо из чата:
|
||||
- `/add <имя>` — создать клиента (бот пришлёт команду установки)
|
||||
- `/del <имя>` — удалить клиента
|
||||
- `/list` — список клиентов
|
||||
- **Кнопки** — постоянная клавиатура: ➕ Создать клиента · 📋 Список · 🗑 Удалить (инлайн-выбор) · 📊 Статус · ❓ Помощь.
|
||||
(Работает только для чата из настроек `tg_chat_id`.)
|
||||
|
||||
---
|
||||
|
||||
## Скриншоты
|
||||
|
||||
| Сессии | Клиенты | Серверы |
|
||||
|--------|---------|---------|
|
||||
|  |  |  |
|
||||
|
||||
> Если картинки не отображаются — положи свои PNG в `docs/img/` (`sessions.png`, `clients.png`, `servers.png`).
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт (с чистого VPS, одной командой)
|
||||
|
||||
Ставит **всё** со всеми зависимостями: wg-obfuscator, WireGuard, обфускатор-сервисы, веб-панель, nginx, скрипты онбординга роутеров, сторож авто-восстановления. Предустановленный Phobos **не требуется**.
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/install.sh)
|
||||
```
|
||||
|
||||
> Если репозиторий приватный — добавь `GH_TOKEN`:
|
||||
> ```bash
|
||||
> GH_TOKEN=ghp_xxx bash <(curl -fsSL -H "Authorization: token ghp_xxx" \
|
||||
> https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/install.sh)
|
||||
> ```
|
||||
### С параметрами
|
||||
|
||||
### Вторичный сервер (для failover/балансировки)
|
||||
```bash
|
||||
PANEL_PASS=МойПароль TG_TOKEN=123:abc TG_CHAT=123456789 \
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/install.sh)
|
||||
```
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|-------------------|-----------------|----------|
|
||||
| `PANEL_PASS` | `OcAdmin2026!` | Пароль панели (логин `admin`) |
|
||||
| `PANEL_PORT` | случайный | Порт веб-панели |
|
||||
| `API_KEY` | случайный | Общий ключ (агенты + pull-токен роутеров) |
|
||||
| `OBF_PORTS` | `2083,5443,993` | Порты обфускатора |
|
||||
| `TG_TOKEN`/`TG_CHAT` | пусто | Telegram-уведомления |
|
||||
| `ALLOW_PLAIN_WG` | пусто | `=1` открыть порт 51820 для iOS WireGuard (без обфускации) |
|
||||
| `CHANNEL` | `stable` | канал: `stable` (открытый) · `beta`/`dev` (под ключом `PHOBOS_KEY`) |
|
||||
|
||||
После установки панель напечатает адрес, логин, пароль и **API key** — сохрани их.
|
||||
|
||||
> **Каналы:**
|
||||
> - **stable** — стабильная (ветка `main`), ставится **без ключа**, по умолчанию.
|
||||
> - **beta** — кандидат в релиз (тест), **под ключом** подписчика.
|
||||
> - **dev** — активная разработка (нестабильно), **под ключом** подписчика.
|
||||
>
|
||||
> Ключ `PHOBOS_KEY` выдаётся по подписке [Boosty](https://boosty.to/andrey27). Закрытый канал:
|
||||
> ```bash
|
||||
> PHOBOS_KEY=ваш_ключ CHANNEL=dev bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/install.sh)
|
||||
> ```
|
||||
> Переключение после установки: `phobos-update stable` (без ключа) · `PHOBOS_KEY=... phobos-update beta|dev` · статус: `phobos-update --check`.
|
||||
|
||||
### Вторичный сервер (для failover / балансировки)
|
||||
|
||||
```bash
|
||||
MAIN_SERVER=<ip_основного> MAIN_API_KEY=<API key из основного> \
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/server/secondary-setup.sh)
|
||||
```
|
||||
|
||||
### С кастомными параметрами
|
||||
|
||||
```bash
|
||||
PANEL_PASS=AdminPass456 \
|
||||
TG_TOKEN=1234567890:AABBCCDDaabbccdd \
|
||||
TG_CHAT=123456789 \
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/install.sh)
|
||||
```
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|--------------|----------------|---------------------------------|
|
||||
| `PANEL_PASS` | `OcAdmin2026!` | Пароль веб-панели (admin) |
|
||||
| `TG_TOKEN` | пусто | Telegram bot token |
|
||||
| `TG_CHAT` | пусто | Telegram chat ID для уведомлений|
|
||||
| `PANEL_PORT` | `8443` | Порт веб-панели |
|
||||
Затем в панели → **Серверы** → добавь его в список.
|
||||
|
||||
---
|
||||
|
||||
## Возможности
|
||||
## Клиенты для телефона
|
||||
|
||||
- **Активные сессии** — VPN IP, Real IP, handshake, трафик RX/TX, Kick
|
||||
- **Клиенты VPN** — добавить/удалить через Phobos, статус online/offline
|
||||
- **Именование объектов** — привязать имя к Real IP (отображается в сессиях и Telegram)
|
||||
- **Срок подписки** — дата окончания для каждого клиента:
|
||||
- Date picker в таблице клиентов
|
||||
- Обратный отсчёт (18д, 3д⚠️, завтра⚠️, истёк⛔)
|
||||
- При истечении: автокик + Telegram уведомление
|
||||
- Предупреждения за 3 дня и 1 день
|
||||
- **Telegram уведомления** — 🟢 подключение, 🔴 отключение, ⚠️ за 3 дня, ⛔ истёк
|
||||
- **Настройки** — смена пароля панели, Telegram bot token + chat ID, интервал мониторинга
|
||||
- **Инфо о сервере** — порты, пути, команда установки на роутер
|
||||
В таблице **Клиенты** у каждого клиента есть кнопки:
|
||||
|
||||
- 📱 **Android** — `phobos://`-ссылка + QR для приложения **PhobosWG** (с обфускацией). Скан QR → импорт.
|
||||
- 🍎 **iPhone / iOS** — обычный WireGuard-конфиг + QR для официального WireGuard (**без** обфускации). Требует открытого порта 51820 (`ALLOW_PLAIN_WG=1` при установке).
|
||||
|
||||
> Для каждого устройства создавай **отдельного клиента** (свой ключ и IP). Один конфиг на двух устройствах = конфликт ключей.
|
||||
|
||||
**Приложения:**
|
||||
- **Android (PhobosWG):** на странице Android-клиента в панели есть кнопка **«Скачать приложение PhobosWG (APK)»** — приложение скачивается автоматически (доступно в каналах beta/dev). Установи APK, затем импортируй `phobos://`-ссылку или QR.
|
||||
- **iPhone (iOS):** официальный [WireGuard из App Store](https://apps.apple.com/app/wireguard/id1441195209) — кнопка есть на странице iOS-клиента.
|
||||
|
||||
---
|
||||
|
||||
## Обновление и откат
|
||||
|
||||
Проект версионируется (см. файл `VERSION` и git-теги `vX.Y.Z`).
|
||||
|
||||
**Из панели:** Настройки → «Версия и обновления» → **Обновить** / **Откатить**.
|
||||
|
||||
**Из консоли:**
|
||||
|
||||
```bash
|
||||
phobos-update # обновить до последней версии
|
||||
phobos-update v1.1.0 # обновить до конкретной версии (git tag)
|
||||
phobos-update --rollback # откатить на предыдущую версию
|
||||
phobos-update --version # показать установленную и доступную версию
|
||||
phobos-update --list # список бэкапов
|
||||
```
|
||||
|
||||
Обновляется только слой панели и скриптов. **Ключи WireGuard, `server.env` и клиенты не трогаются.** Перед каждым обновлением делается бэкап → откат восстанавливает его.
|
||||
|
||||
---
|
||||
|
||||
## Полное удаление сервера
|
||||
|
||||
Одной командой (спросит подтверждение):
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/uninstall.sh)
|
||||
```
|
||||
|
||||
Без подтверждения: `PURGE=1 bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/uninstall.sh)`
|
||||
|
||||
Удаляет: веб-панель, обфускатор-сервисы, WireGuard `wg0`, агент, nginx-сайт, watchdog-cron, iptables-правила, бинарники (`wg-obfuscator`, `phobos-update`) и каталоги `/opt/Phobos`, `/opt/phobos-panel`. Общие пакеты (wireguard, nginx, cron) НЕ трогает.
|
||||
|
||||
**Удаление с роутера** (на самом роутере): `/opt/etc/Phobos/phobos-uninstall.sh`
|
||||
|
||||
---
|
||||
|
||||
## Возможные проблемы и решения
|
||||
|
||||
| Симптом | Причина | Решение |
|
||||
|---------|---------|---------|
|
||||
| **Команда установки → 403 / не качается** | репозиторий приватный | Сделай репозиторий публичным, либо ставь с токеном: `GH_TOKEN=ghp_xxx bash <(curl -fsSL -H "Authorization: token ghp_xxx" .../install.sh)` |
|
||||
| **Панель не открывается** | панель перезапускалась (обновление) / сервер недоступен | Подожди 5 сек, обнови страницу. Проверь: `systemctl status phobos-panel` |
|
||||
| **Клиент Offline, хотя работает** | роутер на резервном сервере | Норма — статус считается по handshake на **любом** сервере; обнови страницу |
|
||||
| **«SSH timeout» при тесте роутера** | роутер за NAT, входящий SSH закрыт | Это нормально — управление идёт по pull-каналу через туннель, SSH не нужен |
|
||||
| **У роутера нет интернета на LAN-устройствах** | WG-интерфейс `private` / нет привязки устройства к политике | На роутере: `security-level public` + `ip hotspot host <mac> policy <PolicyName>`; health-скрипт чинит это сам в течение ~5 мин |
|
||||
| **Роутер не поднялся после перезагрузки** | Entware/USB не запустил автозагрузку | Сторож (watchdog) поднимет за ~3 мин (нужны KeenDNS + web-логин роутера в `router_access`), либо перезагрузи роутер ещё раз |
|
||||
| **iPhone не подключается по `phobos://`** | на iOS нет приложения-обфускатора | Используй 🍎-конфиг (обычный WireGuard) + открой 51820 (`ALLOW_PLAIN_WG=1`) |
|
||||
| **Смена сервера применяется не сразу** | роутер тянет конфиг по cron | Применяется за ~12–15 сек, не мгновенно |
|
||||
| **Забыл порт панели** | случайный порт | `cat /opt/phobos-panel/.port` |
|
||||
| **Забыл API key** | | `grep server_api_key /opt/phobos-panel/settings.json` |
|
||||
| **PhobosWG: «невозможно импортировать туннель»** | старая панель генерила неполный `phobos://` (без `PresharedKey`) | Обнови панель, заново скопируй `phobos://`-ссылку или пересканируй QR. Исправлено в v1.2.7 |
|
||||
| **Роутер aarch64: «бинарник не найден в архиве»** | Ground-Zerro раздаёт только x86_64 | Обнови (`phobos-update`) — aarch64/mipsel/armv7 авто-качаются из ClusterM, затем пересоздай клиента. Исправлено в v1.2.6 |
|
||||
| **Клиент не подключается, «НЕТ СОЕДИНЕНИЯ»** | клиентский обфускатор целился в неверный порт (51821) | Обнови панель → **пересоздай клиента** → переустанови на роутере. Порт берётся из `OBFUSCATOR_PORTS` |
|
||||
| **Удалить сервер полностью** | | `bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/uninstall.sh)` |
|
||||
|
||||
Логи: `journalctl -u phobos-panel -n 50` · сторож: `/opt/Phobos/server/watchdog.log` · роутер: `/opt/etc/Phobos/health.log`.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
Keenetic Router → wg-obfuscator (client) → :51821 → wg-obfuscator (server) → :51820 WireGuard → Internet
|
||||
│
|
||||
10.25.0.x
|
||||
│
|
||||
Web Panel :8443
|
||||
(Flask + Gunicorn)
|
||||
```
|
||||
|
||||
- Протокол: WireGuard + wg-obfuscator (обфускация от DPI)
|
||||
- Аутентификация: ключевые пары (нет паролей — только WireGuard ключи)
|
||||
- Подсеть VPN: `10.25.0.0/16`
|
||||
- Мониторинг сессий: каждые 30 сек (настраивается)
|
||||
|
||||
---
|
||||
|
||||
## Управление
|
||||
|
||||
```bash
|
||||
# Phobos (VPN)
|
||||
phobos # Интерактивное меню
|
||||
systemctl status wg-quick@wg0 # WireGuard
|
||||
systemctl status wg-obfuscator # Обфускатор
|
||||
wg show wg0 # Активные peers
|
||||
|
||||
# Веб-панель
|
||||
systemctl status phobos-panel
|
||||
systemctl restart phobos-panel
|
||||
journalctl -u phobos-panel -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Структура файлов
|
||||
|
||||
```
|
||||
/opt/Phobos/
|
||||
├── clients/ # Клиенты VPN (ключи, конфиги)
|
||||
│ └── {name}/
|
||||
│ ├── metadata.json
|
||||
│ ├── {name}.conf
|
||||
│ └── wg-obfuscator.conf
|
||||
├── server/
|
||||
│ ├── server.env # Конфигурация сервера
|
||||
│ └── wg-obfuscator.conf
|
||||
└── repo/server/scripts/
|
||||
└── phobos-client.sh # Управление клиентами
|
||||
|
||||
/opt/phobos-panel/
|
||||
├── app.py # Flask веб-панель
|
||||
├── settings.json # Настройки (пароль, Telegram, метки, сроки)
|
||||
└── .secret_key # Ключ сессии
|
||||
|
||||
/etc/wireguard/
|
||||
└── wg0.conf # WireGuard конфигурация
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обновление панели
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/app.py \
|
||||
> /opt/phobos-panel/app.py
|
||||
systemctl restart phobos-panel
|
||||
Телефон/Роутер → wg-obfuscator (клиент) ──обфускация──► сервер :2083/5443/993
|
||||
│ де-обфускация
|
||||
WireGuard :51820
|
||||
│ 10.25.0.x
|
||||
┌──────────┴──────────┐
|
||||
Веб-панель Failover на
|
||||
(Flask+Gunicorn) резервные серверы
|
||||
```
|
||||
|
||||
- Протокол: **WireGuard + wg-obfuscator** (обфускация от DPI).
|
||||
- Управление роутерами — по **туннелю** (`10.25.0.1`), устойчиво к бану публичного IP.
|
||||
- Состояние: JSON-файлы (без БД).
|
||||
|
||||
---
|
||||
|
||||
@@ -137,23 +201,14 @@ systemctl restart phobos-panel
|
||||
|
||||
- 💖 **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) — вопросы, баги, идеи
|
||||
- ✉️ **Telegram:** [@PCAdministration](https://t.me/PCAdministration) — вопросы, баги, идеи
|
||||
|
||||
## На основе и благодарности
|
||||
|
||||
PCA Phobos — это веб-панель и turnkey-инсталлятор поверх:
|
||||
PCA Phobos — это веб-панель и turnkey-инсталлятор поверх отличных открытых проектов:
|
||||
|
||||
- [**Phobos**](https://github.com/Ground-Zerro/Phobos) (Ground-Zerro) — обфусцированный WireGuard. Поддержать автора: [Boosty](https://boosty.to/ground_zerro) ❤️
|
||||
- [**WireGuard Easy**](https://github.com/wg-easy/wg-easy) (Emile Nijssen, AGPL-3.0) — веб-панель WireGuard, на которой основан Phobos. Поддержать автора: [GitHub Sponsors](https://github.com/sponsors/WeeJeWel) ❤️
|
||||
- [**wg-obfuscator**](https://github.com/ClusterM/wg-obfuscator) (ClusterM) — обфускация WireGuard-трафика. Поддержать автора: [Boosty](https://boosty.to/cluster) ❤️
|
||||
- [**Phobos**](https://github.com/Ground-Zerro/Phobos) — обфусцированный WireGuard, автор **Ground_Zerro**. Поддержать: [Boosty](https://boosty.to/ground_zerro) ❤️
|
||||
- [**WireGuard Easy**](https://github.com/wg-easy/wg-easy) — веб-панель WireGuard (AGPL-3.0), автор **Emile Nijssen**. Поддержать: [GitHub Sponsors](https://github.com/sponsors/WeeJeWel) ❤️
|
||||
- [**wg-obfuscator**](https://github.com/ClusterM/wg-obfuscator) — обфускация WireGuard-трафика (GPL-3.0), автор **ClusterM**. Поддержать: [Boosty](https://boosty.to/cluster) ❤️
|
||||
|
||||
Спасибо авторам за отличные инструменты.
|
||||
|
||||
## Клиенты для телефона
|
||||
|
||||
В таблице клиентов у каждого клиента есть кнопки:
|
||||
|
||||
- 📱 **Android** — `phobos://`-ссылка + QR для приложения **PhobosWG** (с обфускацией). Скан QR → импорт.
|
||||
- 🍎 **iPhone / iOS** — обычный WireGuard-конфиг + QR для официального WireGuard (БЕЗ обфускации). Требует открытого порта 51820 на сервере (поставь `ALLOW_PLAIN_WG=1` при установке, либо удали правило `iptables ... 51820 ... DROP`).
|
||||
|
||||
> Для каждого устройства создавай **отдельного клиента** (свой ключ и IP). Один конфиг на двух устройствах = конфликт ключей.
|
||||
|
||||
427
app.py
427
app.py
@@ -72,6 +72,249 @@ def tg_send(text):
|
||||
pass
|
||||
|
||||
|
||||
def notify_on(cid):
|
||||
return load_settings().get("client_notify", {}).get(cid, True)
|
||||
|
||||
|
||||
def create_client_cli(name):
|
||||
"""Create a client (add + assign + fanout + install link). Returns (ok, html_msg). Used by web + Telegram bot."""
|
||||
import re as _re
|
||||
name = (name or "").strip()
|
||||
if not name or not _re.match(r"^[a-zA-Z0-9_-]+$", name):
|
||||
return False, "Имя: латиница, цифры, _ и - (без пробелов)."
|
||||
if os.path.isdir(os.path.join(CLIENTS_DIR, name)):
|
||||
return False, "Клиент %s уже существует." % name
|
||||
script = "%s/repo/server/scripts/phobos-client.sh" % PHOBOS_DIR
|
||||
try:
|
||||
subprocess.check_output([script, "add", name], text=True, timeout=45,
|
||||
stderr=subprocess.STDOUT, env={**os.environ, **_load_server_env()})
|
||||
except subprocess.CalledProcessError as e:
|
||||
return False, "Ошибка создания: %s" % (e.output or "")[-300:]
|
||||
try:
|
||||
auto_assign_server(name); fanout_router_config(name)
|
||||
except Exception:
|
||||
pass
|
||||
cmd = ""
|
||||
try:
|
||||
out = subprocess.check_output([script, "link", name], text=True, timeout=25,
|
||||
stderr=subprocess.STDOUT, env={**os.environ, **_load_server_env()})
|
||||
for ln in out.splitlines():
|
||||
if ln.strip().startswith("curl") or "/init/" in ln:
|
||||
cmd = ln.strip(); break
|
||||
except Exception:
|
||||
pass
|
||||
msg = "\u2705 Клиент <b>%s</b> создан." % name
|
||||
if cmd:
|
||||
msg += "\n\nКоманда установки на роутер:\n<code>%s</code>" % cmd
|
||||
return True, msg
|
||||
|
||||
|
||||
def delete_client_cli(cid):
|
||||
cid = (cid or "").strip()
|
||||
if not cid or not os.path.isdir(os.path.join(CLIENTS_DIR, cid)):
|
||||
return False, "Клиент %s не найден." % cid
|
||||
script = "%s/repo/server/scripts/phobos-client.sh" % PHOBOS_DIR
|
||||
try:
|
||||
subprocess.check_output([script, "remove", cid], text=True, timeout=25,
|
||||
stderr=subprocess.STDOUT, env={**os.environ, **_load_server_env()})
|
||||
except subprocess.CalledProcessError as e:
|
||||
return False, "Ошибка удаления: %s" % (e.output or "")[-300:]
|
||||
try:
|
||||
st = load_settings()
|
||||
st.get("client_assignments", {}).pop(cid, None)
|
||||
st.get("client_notify", {}).pop(cid, None)
|
||||
save_settings(st)
|
||||
except Exception:
|
||||
pass
|
||||
return True, "\U0001F5D1 Клиент <b>%s</b> удалён." % cid
|
||||
|
||||
|
||||
def tg_api(method, payload, token=None):
|
||||
import urllib.request
|
||||
if token is None:
|
||||
token = load_settings().get("tg_bot_token", "")
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request("https://api.telegram.org/bot%s/%s" % (token, method),
|
||||
data=data, headers={"Content-Type": "application/json"})
|
||||
return json.loads(urllib.request.urlopen(req, timeout=15).read())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
MAIN_KB = {
|
||||
"keyboard": [
|
||||
["➕ Создать клиента", "\U0001F4CB Список"],
|
||||
["\U0001F5D1 Удалить", "\U0001F4CA Статус"],
|
||||
["❓ Помощь"],
|
||||
],
|
||||
"resize_keyboard": True,
|
||||
}
|
||||
|
||||
_bot_state = {}
|
||||
|
||||
|
||||
def _bot_status_text():
|
||||
servers = [{"ip": SERVER_IP, "is_primary": True}] + load_servers()
|
||||
lines = ["\U0001F4CA <b>Серверы</b>:"]
|
||||
for sv in servers:
|
||||
ip = sv.get("ip", "")
|
||||
st = "ok" if ip == SERVER_IP else server_stats_cache.get(ip, {}).get("status", "?")
|
||||
emoji = "\U0001F7E2" if st == "ok" else ("\U0001F534" if st == "down" else "⚪")
|
||||
lines.append("%s %s%s" % (emoji, ip, " (primary)" if sv.get("is_primary") else ""))
|
||||
online = set()
|
||||
try:
|
||||
for p, info in get_wg_peers().items():
|
||||
if is_peer_online(info.get("latest handshake", "")):
|
||||
online.add(p)
|
||||
for _ip, hs in server_handshakes_cache.items():
|
||||
for p, tsv in hs.items():
|
||||
if tsv and (time.time() - tsv) < 180:
|
||||
online.add(p)
|
||||
except Exception:
|
||||
pass
|
||||
lines.append("\n\U0001F4F1 <b>Клиенты</b>:")
|
||||
cl = get_clients()
|
||||
if not cl:
|
||||
lines.append("нет")
|
||||
for c in cl:
|
||||
on = c.get("public_key", "") in online
|
||||
lines.append(("\U0001F7E2 " if on else "⚪ ") + c.get("client_id", ""))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def qr_png_bytes(data):
|
||||
import qrcode, io
|
||||
buf = io.BytesIO(); qrcode.make(data).save(buf, format="PNG"); return buf.getvalue()
|
||||
|
||||
|
||||
def tg_send_photo(chat, png, caption=""):
|
||||
import urllib.request, secrets as _sec
|
||||
token = load_settings().get("tg_bot_token", "")
|
||||
if not token:
|
||||
return None
|
||||
boundary = "----phobos" + _sec.token_hex(8)
|
||||
def fld(name, val):
|
||||
return ("--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n" % (boundary, name, val)).encode()
|
||||
body = fld("chat_id", str(chat)) + fld("caption", caption) + fld("parse_mode", "HTML")
|
||||
body += ("--%s\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"qr.png\"\r\nContent-Type: image/png\r\n\r\n" % boundary).encode()
|
||||
body += png + ("\r\n--%s--\r\n" % boundary).encode()
|
||||
try:
|
||||
req = urllib.request.Request("https://api.telegram.org/bot%s/sendPhoto" % token, data=body,
|
||||
headers={"Content-Type": "multipart/form-data; boundary=%s" % boundary})
|
||||
return urllib.request.urlopen(req, timeout=25).read()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def bot_send_new_client(chat, name):
|
||||
ok, rep = create_client_cli(name)
|
||||
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||
if ok:
|
||||
try:
|
||||
conf, link = build_phone_config(name, "android")
|
||||
if link:
|
||||
tg_send_photo(chat, qr_png_bytes(link), "📱 Android (PhobosWG) — отсканируй QR в приложении")
|
||||
tg_api("sendMessage", {"chat_id": chat, "text": "phobos:// ссылка (Android):\n<code>%s</code>" % link, "parse_mode": "HTML"})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def telegram_bot():
|
||||
offset = 0
|
||||
import urllib.request
|
||||
while True:
|
||||
try:
|
||||
s = load_settings()
|
||||
token = s.get("tg_bot_token", "")
|
||||
chat = str(s.get("tg_chat_id", ""))
|
||||
if not token or not chat:
|
||||
time.sleep(15); continue
|
||||
url = "https://api.telegram.org/bot%s/getUpdates?timeout=30&offset=%d" % (token, offset)
|
||||
data = json.loads(urllib.request.urlopen(url, timeout=40).read())
|
||||
for upd in data.get("result", []):
|
||||
offset = upd["update_id"] + 1
|
||||
cq = upd.get("callback_query")
|
||||
if cq:
|
||||
if str(cq.get("message", {}).get("chat", {}).get("id", "")) != chat:
|
||||
continue
|
||||
cdata = cq.get("data", "")
|
||||
if cdata.startswith("del:"):
|
||||
_, rep = delete_client_cli(cdata[4:])
|
||||
tg_api("answerCallbackQuery", {"callback_query_id": cq["id"], "text": "OK"})
|
||||
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||
continue
|
||||
m = upd.get("message") or upd.get("channel_post")
|
||||
if not m:
|
||||
continue
|
||||
if str(m.get("chat", {}).get("id", "")) != chat:
|
||||
continue
|
||||
text = (m.get("text") or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
# detect reply-keyboard button by its emoji prefix (NOT a command/arg)
|
||||
btn = None
|
||||
if not text.startswith("/"):
|
||||
if text.startswith("➕"):
|
||||
btn = "add"
|
||||
elif text.startswith("\U0001F5D1"):
|
||||
btn = "del"
|
||||
elif text.startswith("\U0001F4CB"):
|
||||
btn = "list"
|
||||
elif text.startswith("\U0001F4CA"):
|
||||
btn = "status"
|
||||
elif text.startswith("❓"):
|
||||
btn = "help"
|
||||
# conversation: awaiting a client name to create
|
||||
if _bot_state.get(chat) == "add":
|
||||
if btn is None and not text.startswith("/"):
|
||||
_bot_state.pop(chat, None)
|
||||
bot_send_new_client(chat, text)
|
||||
continue
|
||||
_bot_state.pop(chat, None) # a button/command cancels the prompt
|
||||
# parse slash command + arg (buttons carry no arg)
|
||||
cmd = ""
|
||||
arg = ""
|
||||
if text.startswith("/"):
|
||||
toks = text.split()
|
||||
cmd = toks[0].split("@")[0].lower()
|
||||
arg = toks[1] if len(toks) > 1 else ""
|
||||
if btn == "add" or cmd in ("/add", "/new", "/create"):
|
||||
if arg:
|
||||
bot_send_new_client(chat, arg)
|
||||
else:
|
||||
_bot_state[chat] = "add"
|
||||
tg_api("sendMessage", {"chat_id": chat, "text": "Введите имя нового клиента (латиница, цифры, _ и -):"})
|
||||
elif btn == "del" or cmd in ("/del", "/delete", "/remove", "/rm"):
|
||||
if arg:
|
||||
_, rep = delete_client_cli(arg)
|
||||
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||
else:
|
||||
cl = get_clients()
|
||||
if not cl:
|
||||
tg_api("sendMessage", {"chat_id": chat, "text": "Клиентов нет.", "reply_markup": MAIN_KB})
|
||||
else:
|
||||
kb = {"inline_keyboard": [[{"text": "\U0001F5D1 " + c.get("client_id", ""), "callback_data": "del:" + c.get("client_id", "")}] for c in cl]}
|
||||
tg_api("sendMessage", {"chat_id": chat, "text": "Кого удалить? Нажми на клиента:", "reply_markup": kb})
|
||||
elif btn == "list" or cmd in ("/list", "/ls"):
|
||||
cl = [c.get("client_id", "") for c in get_clients()]
|
||||
rep = ("Клиенты (%d):\n" % len(cl)) + "\n".join("• " + c for c in cl) if cl else "Клиентов нет."
|
||||
tg_api("sendMessage", {"chat_id": chat, "text": rep, "reply_markup": MAIN_KB})
|
||||
elif btn == "status" or cmd in ("/status", "/stat"):
|
||||
tg_api("sendMessage", {"chat_id": chat, "text": _bot_status_text(), "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||
elif btn == "help" or cmd in ("/help", "/start"):
|
||||
rep = ("<b>PCA Phobos — бот</b>\n\n"
|
||||
"Кнопки внизу или команды:\n"
|
||||
"➕ /add <имя> — создать клиента\n"
|
||||
"\U0001F5D1 /del <имя> — удалить\n"
|
||||
"\U0001F4CB /list — список\n"
|
||||
"\U0001F4CA /status — статус")
|
||||
tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB})
|
||||
except Exception:
|
||||
time.sleep(10)
|
||||
|
||||
def get_wg_peers():
|
||||
"""Parse `wg show wg0` to get active peers with transfer/handshake info."""
|
||||
try:
|
||||
@@ -236,6 +479,7 @@ def kick_client_by_id(client_id):
|
||||
|
||||
prev_session_keys = None
|
||||
prev_server_status = {}
|
||||
_down_streak = {}
|
||||
_last_fanout = 0
|
||||
server_stats_cache = {}
|
||||
server_handshakes_cache = {}
|
||||
@@ -260,12 +504,19 @@ def get_local_stats():
|
||||
|
||||
|
||||
def get_remote_stats(server):
|
||||
import urllib.request
|
||||
url = f"http://{server['ip']}:8444/api/health"
|
||||
req = urllib.request.Request(url, headers={"X-API-Key": server.get("api_key", "")})
|
||||
data = None
|
||||
for _attempt in range(2): # retry once: smooths transient timeouts (1-worker agent)
|
||||
try:
|
||||
data = json.loads(urllib.request.urlopen(req, timeout=6).read())
|
||||
break
|
||||
except Exception:
|
||||
data = None
|
||||
try:
|
||||
import urllib.request
|
||||
url = f"http://{server['ip']}:8444/api/health"
|
||||
req = urllib.request.Request(url, headers={"X-API-Key": server.get("api_key", "")})
|
||||
resp = urllib.request.urlopen(req, timeout=3)
|
||||
data = json.loads(resp.read())
|
||||
if data is None:
|
||||
raise Exception("no response")
|
||||
# cache handshakes so page renders never block on remote HTTP
|
||||
server_handshakes_cache[server.get("ip", "")] = data.get("handshakes", {}) or {}
|
||||
# Filter peers: only count known client public keys
|
||||
@@ -410,6 +661,28 @@ def panel_version():
|
||||
return "unknown"
|
||||
|
||||
|
||||
GH_TOKEN_FILE = "/opt/phobos-panel/.gh_token"
|
||||
|
||||
|
||||
def gh_token_present():
|
||||
try:
|
||||
return os.path.exists(GH_TOKEN_FILE) and os.path.getsize(GH_TOKEN_FILE) > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def save_gh_token(tok):
|
||||
tok = (tok or "").strip()
|
||||
if not tok:
|
||||
return False
|
||||
fd = os.open(GH_TOKEN_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
try:
|
||||
os.write(fd, tok.encode())
|
||||
finally:
|
||||
os.close(fd)
|
||||
return True
|
||||
|
||||
|
||||
def run_update(arg):
|
||||
import subprocess
|
||||
try:
|
||||
@@ -448,16 +721,56 @@ def build_phone_config(cid, mode="android"):
|
||||
if os.path.exists(instpath):
|
||||
inst = open(instpath).read().strip()
|
||||
conf = wg + "\n\n" + inst + "\n"
|
||||
b64 = base64.urlsafe_b64encode(conf.encode()).decode().rstrip("=")
|
||||
link = "phobos://" + b64 + "#" + urllib.parse.quote(cid)
|
||||
# PhobosWG requires ALL fields present; missing -> "= none" before base64
|
||||
# (spec 2.3). Ground-Zerro configs lack PresharedKey -> import fails otherwise.
|
||||
padded = _pad_conf_with_none(conf)
|
||||
b64 = base64.urlsafe_b64encode(padded.encode()).decode().rstrip("=")
|
||||
link = "phobos://" + b64 + "#" + urllib.parse.quote(cid or "none")
|
||||
return conf, link
|
||||
|
||||
|
||||
def _pad_conf_with_none(conf):
|
||||
"""Insert '<key> = none' for every missing mandatory PhobosWG field
|
||||
(phobos:// spec 2.3). Only for phobos:// payload, NOT raw .conf."""
|
||||
mandatory = {
|
||||
"[interface]": ["PrivateKey", "Address", "MTU", "DNS"],
|
||||
"[peer]": ["PublicKey", "PresharedKey", "AllowedIPs", "PersistentKeepalive", "Endpoint"],
|
||||
"[instance]": ["source-if", "source-lport", "target", "key", "masking",
|
||||
"verbose", "idle-timeout", "max-dummy"],
|
||||
}
|
||||
sections = []
|
||||
cur = None
|
||||
for ln in conf.split("\n"):
|
||||
s = ln.strip()
|
||||
if s.startswith("[") and s.endswith("]"):
|
||||
cur = [s, []]
|
||||
sections.append(cur)
|
||||
elif cur is not None:
|
||||
cur[1].append(ln)
|
||||
out = []
|
||||
for header, lines in sections:
|
||||
keys = {ln.split("=", 1)[0].strip().lower() for ln in lines if "=" in ln}
|
||||
while lines and lines[-1].strip() == "":
|
||||
lines.pop()
|
||||
for req in mandatory.get(header.lower(), []):
|
||||
if req.lower() not in keys:
|
||||
lines.append("%s = none" % req)
|
||||
out.append(header)
|
||||
out.extend(lines)
|
||||
out.append("")
|
||||
return "\n".join(out).rstrip("\n") + "\n"
|
||||
|
||||
|
||||
def qr_datauri(data):
|
||||
try:
|
||||
import qrcode, io, base64
|
||||
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=4, border=3)
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
buf = io.BytesIO()
|
||||
qrcode.make(data).save(buf, format="PNG")
|
||||
img.save(buf, format="PNG")
|
||||
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
|
||||
except Exception:
|
||||
return ""
|
||||
@@ -534,7 +847,7 @@ def push_config_to_router(client_id):
|
||||
|
||||
|
||||
def session_monitor():
|
||||
global prev_session_keys, prev_server_status, server_stats_cache, _last_fanout
|
||||
global prev_session_keys, prev_server_status, server_stats_cache, _last_fanout, _down_streak
|
||||
while True:
|
||||
try:
|
||||
s = load_settings()
|
||||
@@ -554,13 +867,15 @@ def session_monitor():
|
||||
client_id, real_ip = key
|
||||
label = labels.get(real_ip, "")
|
||||
name = f"{label} ({real_ip})" if label else real_ip
|
||||
tg_send(f"🟢 <b>{client_id}</b> connected — {name}")
|
||||
if notify_on(client_id):
|
||||
tg_send(f"🟢 <b>{client_id}</b> connected — {name}")
|
||||
|
||||
for key in prev_session_keys - current_keys:
|
||||
client_id, real_ip = key
|
||||
label = labels.get(real_ip, "")
|
||||
name = f"{label} ({real_ip})" if label else real_ip
|
||||
tg_send(f"🔴 <b>{client_id}</b> disconnected — {name}")
|
||||
if notify_on(client_id):
|
||||
tg_send(f"🔴 <b>{client_id}</b> disconnected — {name}")
|
||||
|
||||
prev_session_keys = current_keys
|
||||
|
||||
@@ -569,19 +884,24 @@ def session_monitor():
|
||||
local_stats = get_local_stats()
|
||||
server_stats_cache[SERVER_IP] = local_stats
|
||||
|
||||
DOWN_CONFIRM = 3 # consecutive failed cycles before declaring a server DOWN
|
||||
for srv in servers:
|
||||
ip = srv["ip"]
|
||||
stats = get_remote_stats(srv)
|
||||
server_stats_cache[ip] = stats
|
||||
raw = stats.get("status", "down")
|
||||
streak = _down_streak.get(ip, 0)
|
||||
streak = streak + 1 if raw == "down" else 0
|
||||
_down_streak[ip] = streak
|
||||
# update cache on OK, or only once a DOWN is confirmed (keep last-good on transient)
|
||||
if raw == "ok" or streak >= DOWN_CONFIRM:
|
||||
server_stats_cache[ip] = stats
|
||||
eff = "down" if streak >= DOWN_CONFIRM else "ok"
|
||||
was_up = prev_server_status.get(ip, "ok")
|
||||
now_status = stats["status"]
|
||||
|
||||
if was_up == "ok" and now_status == "down":
|
||||
if was_up == "ok" and eff == "down":
|
||||
tg_send(f"🔴 <b>Server {ip}</b> DOWN!")
|
||||
elif was_up == "down" and now_status == "ok":
|
||||
elif was_up == "down" and eff == "ok":
|
||||
tg_send(f"🟢 <b>Server {ip}</b> back ONLINE")
|
||||
|
||||
prev_server_status[ip] = now_status
|
||||
prev_server_status[ip] = eff
|
||||
|
||||
# periodic config fan-out to secondaries (covers CLI-created clients
|
||||
# and config drift; gated ~5 min, skips down servers)
|
||||
@@ -599,6 +919,8 @@ def session_monitor():
|
||||
|
||||
monitor_thread = threading.Thread(target=session_monitor, daemon=True)
|
||||
monitor_thread.start()
|
||||
bot_thread = threading.Thread(target=telegram_bot, daemon=True)
|
||||
bot_thread.start()
|
||||
|
||||
|
||||
PAGE = """<!DOCTYPE html>
|
||||
@@ -661,7 +983,7 @@ input:focus{outline:none;border-color:#4f46e5}
|
||||
CONTENT
|
||||
<div class="footer">
|
||||
<b style="color:#a5b4fc">PCA Phobos Panel</b> — панель управления © <a href="https://github.com/andrey271192/PCA_Phobos" target="_blank" rel="noopener">andrey271192</a><br>
|
||||
Поддержать проект: <a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener">💖 Boosty</a> · <a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener">💳 Ozon Bank</a> · <a href="https://t.me/c/2474115507/78722" target="_blank" rel="noopener">✉️ Telegram</a>
|
||||
Поддержать проект: <a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener">💖 Boosty</a> · <a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener">💳 Ozon Bank</a> · <a href="https://t.me/PCAdministration" target="_blank" rel="noopener">✉️ Telegram</a>
|
||||
<div style="margin-top:8px;font-size:.88em;color:#475569;line-height:1.7;text-align:left;max-width:660px;margin:8px auto 0">
|
||||
<b>Создано на основе открытых проектов:</b><br>
|
||||
• <a href="https://github.com/Ground-Zerro/Phobos" target="_blank" rel="noopener">Phobos</a> — обфусцированный WireGuard VPN, автор <b>Ground_Zerro</b> · <a href="https://boosty.to/ground_zerro" target="_blank" rel="noopener">поддержать</a><br>
|
||||
@@ -874,6 +1196,15 @@ def clients_page():
|
||||
except subprocess.CalledProcessError as e:
|
||||
msg = f'<div class="alert alert-error">{e.output}</div>'
|
||||
|
||||
elif action == "toggle_notify":
|
||||
client_id = request.form.get("client_id", "").strip()
|
||||
if client_id:
|
||||
cn = s.setdefault("client_notify", {})
|
||||
cn[client_id] = not cn.get(client_id, True)
|
||||
save_settings(s)
|
||||
st = "вкл" if cn[client_id] else "выкл"
|
||||
msg = f'<div class="alert alert-ok">Уведомления {client_id}: {st}</div>'
|
||||
|
||||
elif action == "set_expiry":
|
||||
client_id = request.form.get("client_id", "").strip()
|
||||
expiry = request.form.get("expiry", "").strip()
|
||||
@@ -938,6 +1269,7 @@ def clients_page():
|
||||
|
||||
all_srv = get_all_servers_ordered()
|
||||
assignments = s.get("client_assignments", {})
|
||||
assignments_notify = s.get("client_notify", {})
|
||||
# Online = fresh handshake on ANY server (truthful during failover/transition)
|
||||
online_all = set(online_pubs)
|
||||
for _sv in all_srv:
|
||||
@@ -991,6 +1323,10 @@ def clients_page():
|
||||
h_del = hlp("Безвозвратно удалить клиента, его ключи и конфигурацию. Отменить нельзя.",
|
||||
"Permanently delete the client, its keys and config. Cannot be undone.")
|
||||
del_confirm = tr("Удалить " + cid + "?", "Delete " + cid + "?")
|
||||
_notify = assignments_notify.get(cid, True)
|
||||
notify_icon = "🔔" if _notify else "🔕"
|
||||
notify_bg = "#4f46e5" if _notify else "#64748b"
|
||||
notify_ttl = tr("Уведомления в Telegram включены — нажми, чтобы выключить", "Telegram notifications ON — click to turn off") if _notify else tr("Уведомления выключены — нажми, чтобы включить", "Notifications OFF — click to turn on")
|
||||
rows += f"""<tr>
|
||||
<td>{cid}</td>
|
||||
<td>{ip}</td>
|
||||
@@ -1015,6 +1351,11 @@ def clients_page():
|
||||
</form>
|
||||
</td>
|
||||
<td style="display:flex;gap:4px;align-items:center">
|
||||
<form method="post" title="{notify_ttl}">
|
||||
<input type="hidden" name="action" value="toggle_notify">
|
||||
<input type="hidden" name="client_id" value="{cid}">
|
||||
<button class="btn btn-sm" style="background:{notify_bg}" type="submit">{notify_icon}</button>
|
||||
</form>
|
||||
<form method="post" onsubmit="return confirm('{del_confirm}')">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="client_id" value="{cid}">
|
||||
@@ -1105,9 +1446,19 @@ def labels_page():
|
||||
def settings_page():
|
||||
s = load_settings()
|
||||
msg = ""
|
||||
if request.method == "POST" and request.form.get("action") in ("update", "rollback"):
|
||||
if request.form.get("action") == "update":
|
||||
if request.method == "POST" and request.form.get("action") in ("update", "rollback", "check", "save_token"):
|
||||
_act = request.form.get("action")
|
||||
_tok = request.form.get("gh_token", "").strip()
|
||||
if _tok:
|
||||
save_gh_token(_tok)
|
||||
if _act == "save_token":
|
||||
_out = tr("Токен подписчика сохранён. Теперь можно переключаться на beta/dev.",
|
||||
"Subscriber token saved. You can now switch to beta/dev.") if _tok else \
|
||||
tr("Поле токена пустое.", "Token field is empty.")
|
||||
elif _act == "update":
|
||||
_out = run_update(request.form.get("target", "").strip() or "main")
|
||||
elif _act == "check":
|
||||
_out = run_update("--check")
|
||||
else:
|
||||
_out = run_update("--rollback")
|
||||
s = load_settings()
|
||||
@@ -1146,11 +1497,31 @@ def settings_page():
|
||||
<div class="card">
|
||||
<h2>{tr("Версия и обновления","Version & updates")} {hlp("Скачивает свежие файлы PCA-слоя (панель + скрипты) с GitHub и перезапускает панель. Ключи WireGuard, server.env и клиенты НЕ трогаются. Пусто или main = последняя версия; можно указать тег (например v1.1.0). Откат восстанавливает предыдущую версию из автоматического бэкапа.", "Fetches the latest PCA files (panel + scripts) from GitHub and restarts the panel. WireGuard keys, server.env and clients are NOT touched. Empty or main = latest; you can pass a tag (e.g. v1.1.0). Rollback restores the previous version from an automatic backup.")}</h2>
|
||||
<p>{tr("Установленная версия","Installed version")}: <b style="color:#a5b4fc">{panel_version()}</b></p>
|
||||
<p style="font-size:.85em;color:#94a3b8;margin:4px 0 8px">{tr("Каналы","Channels")}:
|
||||
<b style="color:#22c55e">stable</b> — {tr("стабильная","stable")} ·
|
||||
<b style="color:#eab308">beta</b> — {tr("тест","testing")} ·
|
||||
<b style="color:#f97316">dev</b> — {tr("разработка","development")}.
|
||||
{tr("stable — без токена. Для beta/dev вставь токен подписчика (по подписке Boosty), сохрани — и жми канал.","stable needs no token. For beta/dev paste your subscriber token (Boosty), save it, then click the channel.")}</p>
|
||||
<form method="post" class="form-row" style="margin-bottom:8px">
|
||||
<input type="hidden" name="action" value="save_token">
|
||||
<input type="password" name="gh_token" placeholder="{tr('токен подписчика (для beta/dev)','subscriber token (for beta/dev)')}" style="width:240px" autocomplete="off">
|
||||
<button class="btn btn-sm" style="background:#475569" type="submit">{tr("Сохранить токен","Save token")}</button>
|
||||
<span style="font-size:.82em;color:{'#22c55e' if gh_token_present() else '#f59e0b'}">{(tr("✓ токен сохранён","✓ token saved") if gh_token_present() else tr("токен не задан — beta/dev недоступны","no token — beta/dev locked"))}</span>
|
||||
</form>
|
||||
<div class="form-row" style="gap:6px;margin-bottom:8px">
|
||||
<form method="post" style="display:inline"><input type="hidden" name="action" value="update"><input type="hidden" name="target" value="stable"><button class="btn btn-sm" style="background:#16a34a" type="submit" onclick="return confirm('{tr("Перейти на стабильную?","Switch to stable?")}')">stable</button></form>
|
||||
<form method="post" style="display:inline"><input type="hidden" name="action" value="update"><input type="hidden" name="target" value="beta"><button class="btn btn-sm" style="background:#ca8a04" type="submit" onclick="return confirm('{tr("Перейти на beta?","Switch to beta?")}')">beta</button></form>
|
||||
<form method="post" style="display:inline"><input type="hidden" name="action" value="update"><input type="hidden" name="target" value="dev"><button class="btn btn-sm" style="background:#ea580c" type="submit" onclick="return confirm('{tr("Перейти на dev (нестабильно)?","Switch to dev (unstable)?")}')">dev</button></form>
|
||||
</div>
|
||||
<form method="post" class="form-row">
|
||||
<input type="hidden" name="action" value="update">
|
||||
<input type="text" name="target" placeholder="main / v1.1.0" style="width:150px">
|
||||
<input type="text" name="target" placeholder="stable / beta / dev / v1.2.4" style="width:170px">
|
||||
<button class="btn btn-primary" type="submit" onclick="return confirm('{tr("Обновить панель и скрипты?","Update panel and scripts?")}')">{tr("Обновить","Update")}</button>
|
||||
</form>
|
||||
<form method="post" style="display:inline">
|
||||
<input type="hidden" name="action" value="check">
|
||||
<button class="btn btn-sm" style="background:#0ea5e9" type="submit">{tr("Проверить обновления","Check for updates")}</button>
|
||||
</form>
|
||||
<form method="post" style="margin-top:8px">
|
||||
<input type="hidden" name="action" value="rollback">
|
||||
<button class="btn btn-danger btn-sm" type="submit" onclick="return confirm('{tr("Откатить на предыдущую версию?","Roll back to the previous version?")}')">{tr("Откатить на предыдущую","Rollback")}</button>
|
||||
@@ -1307,17 +1678,23 @@ def client_phone(cid, mode):
|
||||
qr = qr_datauri(link if (mode == "android" and link) else conf)
|
||||
if mode == "android":
|
||||
title = tr("Android — PhobosWG (с обфускацией)", "Android — PhobosWG (obfuscated)")
|
||||
extra = (f'<p>{tr("phobos:// ссылка — импорт в PhobosWG:", "phobos:// link — import into PhobosWG:")}</p>'
|
||||
apk_btn = ""
|
||||
if os.path.exists("/opt/Phobos/www/app/PhobosWG.apk"):
|
||||
apk_btn = (f'<p style="margin-top:6px"><a href="http://{SERVER_IP}/app/PhobosWG.apk" '
|
||||
f'class="btn btn-primary" style="background:#16a34a">📥 {tr("Скачать приложение PhobosWG (APK)","Download PhobosWG app (APK)")}</a></p>')
|
||||
extra = (apk_btn +
|
||||
f'<p>{tr("phobos:// ссылка — импорт в PhobosWG:", "phobos:// link — import into PhobosWG:")}</p>'
|
||||
f'<textarea readonly onclick="this.select()" style="width:100%;height:90px;font-size:.75em">{link}</textarea>')
|
||||
else:
|
||||
title = tr("iPhone / iOS WireGuard (без обфускации)", "iPhone / iOS WireGuard (plain, no obfuscation)")
|
||||
extra = (f'<p style="color:#fcd34d">{tr("⚠ Обычный WireGuard без маскировки (Endpoint → :51820). На сервере должен быть открыт порт 51820.", "⚠ Plain WireGuard, no obfuscation (Endpoint → :51820). Port 51820 must be open on the server.")}</p>')
|
||||
extra = (f'<p><a href="https://apps.apple.com/app/wireguard/id1441195209" target="_blank" rel="noopener" class="btn btn-primary" style="background:#0ea5e9">📥 {tr("WireGuard в App Store","WireGuard on the App Store")}</a></p>'
|
||||
f'<p style="color:#fcd34d">{tr("⚠ Обычный WireGuard без маскировки (Endpoint → :51820). На сервере должен быть открыт порт 51820 (ALLOW_PLAIN_WG=1).", "⚠ Plain WireGuard, no obfuscation (Endpoint → :51820). Port 51820 must be open on the server (ALLOW_PLAIN_WG=1).")}</p>')
|
||||
html = f"""
|
||||
<div class="container">
|
||||
{nav('clients')}
|
||||
<div class="card" style="text-align:center">
|
||||
<h2>{cid} — {title}</h2>
|
||||
<img src="{qr}" alt="QR" style="width:300px;height:300px;background:#fff;padding:10px;border-radius:10px"><br>
|
||||
<img src="{qr}" alt="QR" style="max-width:340px;width:100%;background:#fff;padding:10px;border-radius:10px;image-rendering:pixelated"><br>
|
||||
<p style="margin-top:10px">{tr("Отсканируй QR в приложении или импортируй конфиг:", "Scan the QR in the app or import the config:")}</p>
|
||||
{extra}
|
||||
<textarea readonly onclick="this.select()" style="width:100%;height:250px;font-family:monospace;font-size:.78em">{conf}</textarea>
|
||||
|
||||
9
docs/img/README.md
Normal file
9
docs/img/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Скриншоты
|
||||
|
||||
Положи сюда 3 PNG (панель открыта — сделай скрины своих страниц):
|
||||
|
||||
- `sessions.png` — страница «Сессии»
|
||||
- `clients.png` — страница «Клиенты»
|
||||
- `servers.png` — страница «Серверы»
|
||||
|
||||
README в корне на них ссылается. Рекомендуемая ширина ~1000–1400px.
|
||||
55
install.sh
55
install.sh
@@ -25,11 +25,31 @@ PANEL_PASS="${PANEL_PASS:-OcAdmin2026!}"
|
||||
TG_TOKEN="${TG_TOKEN:-}"
|
||||
TG_CHAT="${TG_CHAT:-}"
|
||||
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
|
||||
PCA_BRANCH="${PCA_BRANCH:-main}"
|
||||
GH_TOKEN="${GH_TOKEN:-}" # set for private-repo installs; empty for public
|
||||
# Каналы: stable (открытый, публичный repo) · beta (тест) · dev (разработка).
|
||||
# beta и dev живут в ПРИВАТНОМ repo PCA_Phobos-dev — доступ по токену подписчика
|
||||
# (GH_TOKEN, read-only, выдаётся по подписке Boosty). stable — без токена.
|
||||
GH_TOKEN="${GH_TOKEN:-}"
|
||||
PUB_REPO="andrey271192/PCA_Phobos"
|
||||
DEV_REPO="andrey271192/PCA_Phobos-dev"
|
||||
CHANNEL="${CHANNEL:-stable}"
|
||||
case "$CHANNEL" in
|
||||
beta) REPO="$DEV_REPO"; PCA_BRANCH="${PCA_BRANCH:-beta}";;
|
||||
dev) REPO="$DEV_REPO"; PCA_BRANCH="${PCA_BRANCH:-dev}";;
|
||||
stable|main|"") REPO="$PUB_REPO"; PCA_BRANCH="${PCA_BRANCH:-main}";;
|
||||
*) REPO="$PUB_REPO"; PCA_BRANCH="${PCA_BRANCH:-$CHANNEL}";;
|
||||
esac
|
||||
if [ "$REPO" = "$DEV_REPO" ] && [ -z "$GH_TOKEN" ]; then
|
||||
echo "Канал '$CHANNEL' закрыт — это $([ "$CHANNEL" = dev ] && echo 'разработка (нестабильно)' || echo 'бета (тест)'), приватный репозиторий."
|
||||
echo "Нужен read-only токен подписчика:"
|
||||
echo " GH_TOKEN=ваш_токен CHANNEL=$CHANNEL bash <(curl -fsSL https://raw.githubusercontent.com/$PUB_REPO/main/install.sh)"
|
||||
echo "Токен выдаётся по подписке: https://boosty.to/andrey27"
|
||||
echo "Стабильная версия ставится без токена (CHANNEL=stable, по умолчанию)."
|
||||
exit 1
|
||||
fi
|
||||
[ "$REPO" = "$DEV_REPO" ] && echo "Токен принят — закрытый канал '$CHANNEL' (приватный repo)."
|
||||
PHOBOS_DIR="/opt/Phobos"
|
||||
PANEL_DIR="/opt/phobos-panel"
|
||||
RAW="https://raw.githubusercontent.com/andrey271192/PCA_Phobos/${PCA_BRANCH}"
|
||||
RAW="https://raw.githubusercontent.com/${REPO}/${PCA_BRANCH}"
|
||||
|
||||
[ "$EUID" -eq 0 ] || { echo "Run as root"; exit 1; }
|
||||
|
||||
@@ -65,12 +85,27 @@ if [ ! -x /usr/local/bin/wg-obfuscator ]; then
|
||||
git init -q; git remote add origin https://github.com/Ground-Zerro/Phobos.git
|
||||
git config core.sparseCheckout true; echo "wg-obfuscator" > .git/info/sparse-checkout
|
||||
git pull origin main -q
|
||||
cp -f "wg-obfuscator/bin/wg-obfuscator-${ARCH}" "$PHOBOS_DIR/bin/" 2>/dev/null || true
|
||||
cp -f "wg-obfuscator/bin/"wg-obfuscator-* "$PHOBOS_DIR/bin/" 2>/dev/null || true
|
||||
chmod +x "$PHOBOS_DIR/bin/"wg-obfuscator-* 2>/dev/null || true
|
||||
ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-${ARCH}" /usr/local/bin/wg-obfuscator
|
||||
ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-${ARCH}" /usr/local/bin/wg-obfuscator 2>/dev/null || true
|
||||
cd /; rm -rf "$R"
|
||||
fi
|
||||
_OBF_TAG=$(curl -fsSL -m10 https://api.github.com/repos/ClusterM/wg-obfuscator/releases/latest 2>/dev/null | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*/\1/')
|
||||
_CB="https://github.com/ClusterM/wg-obfuscator/releases/download/${_OBF_TAG}"
|
||||
for _am in "aarch64:linux-arm64" "mipsel:linux-mipsel-mips32" "mips:linux-mips-mips32" "armv7:linux-armv7-hf"; do
|
||||
_da="${_am%%:*}"; _ss="${_am##*:}"
|
||||
if [ ! -f "$PHOBOS_DIR/bin/wg-obfuscator-${_da}" ] && [ -n "$_OBF_TAG" ]; then
|
||||
_t=$(mktemp -d)
|
||||
curl -fsSL -m30 "${_CB}/wg-obfuscator-${_OBF_TAG}-${_ss}.tar.gz" -o "$_t/o.tgz" 2>/dev/null &&
|
||||
tar -xzf "$_t/o.tgz" -C "$_t" 2>/dev/null &&
|
||||
_b=$(find "$_t" -name "wg-obfuscator" -type f | head -1) &&
|
||||
[ -n "$_b" ] && cp "$_b" "$PHOBOS_DIR/bin/wg-obfuscator-${_da}" && chmod +x "$PHOBOS_DIR/bin/wg-obfuscator-${_da}" && echo " +wg-obfuscator-${_da} (ClusterM ${_OBF_TAG})"
|
||||
rm -rf "$_t"
|
||||
fi
|
||||
done
|
||||
[ -x /usr/local/bin/wg-obfuscator ] || ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-${ARCH}" /usr/local/bin/wg-obfuscator 2>/dev/null || true
|
||||
[ -x /usr/local/bin/wg-obfuscator ] || { echo "ERROR: obfuscator binary for $ARCH missing"; exit 1; }
|
||||
echo " bins: $(ls $PHOBOS_DIR/bin/ | tr '\n' ' ')"
|
||||
|
||||
# ── 3. Phobos repo (onboarding scripts) ──
|
||||
echo "[3/9] Phobos repo (onboarding scripts)..."
|
||||
@@ -221,11 +256,12 @@ server {
|
||||
listen [::]:80 default_server;
|
||||
location /init/ { alias /opt/Phobos/www/init/; default_type application/x-sh; }
|
||||
location /packages/ { alias /opt/Phobos/www/packages/; default_type application/octet-stream; }
|
||||
location /app/ { alias /opt/Phobos/www/app/; default_type application/octet-stream; }
|
||||
location / { return 404; }
|
||||
}
|
||||
NGINX
|
||||
ln -sf /etc/nginx/sites-available/phobos /etc/nginx/sites-enabled/phobos
|
||||
chmod 755 /opt/Phobos/www /opt/Phobos/www/init /opt/Phobos/www/packages
|
||||
mkdir -p /opt/Phobos/www/app; chmod 755 /opt/Phobos/www /opt/Phobos/www/init /opt/Phobos/www/packages /opt/Phobos/www/app
|
||||
nginx -t >/dev/null 2>&1 && systemctl enable nginx -q 2>/dev/null && systemctl restart nginx || echo " WARN: nginx config test failed"
|
||||
|
||||
# ── 9. router watchdog (auto reboot-recovery) ──
|
||||
@@ -239,6 +275,13 @@ if [ -f "$PHOBOS_DIR/server/phobos-router-watchdog.py" ]; then
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
# persist channel + subscriber token so phobos-update can reach private channels
|
||||
mkdir -p "$PANEL_DIR"
|
||||
echo "$CHANNEL" > "$PANEL_DIR/.channel" 2>/dev/null || true
|
||||
if [ "$REPO" = "$DEV_REPO" ] && [ -n "$GH_TOKEN" ]; then
|
||||
( umask 077; printf %s "$GH_TOKEN" > "$PANEL_DIR/.gh_token" )
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Installation complete"
|
||||
|
||||
@@ -6,6 +6,12 @@ check_root
|
||||
load_env
|
||||
ensure_dirs
|
||||
|
||||
# server.env stores OBFUSCATOR_PORTS (plural, comma list). Client obfuscator
|
||||
# must target an ACTUAL listening port. lib-core load_env defaults OBFUSCATOR_PORT
|
||||
# to 51821 (NOT listened on) -> handshake failed. Override unconditionally from
|
||||
# the real port list when present.
|
||||
[ -n "${OBFUSCATOR_PORTS:-}" ] && export OBFUSCATOR_PORT="$(echo "$OBFUSCATOR_PORTS" | cut -d',' -f1)"
|
||||
|
||||
CMD="${1:-help}"
|
||||
CLIENT_ARG="${2:-}"
|
||||
EXTRA_ARG="${3:-}"
|
||||
@@ -209,8 +215,21 @@ action_package() {
|
||||
cp "$dir/${id}.conf" "$pkg_root/${id}.conf"
|
||||
cp "$dir/wg-obfuscator.conf" "$pkg_root/wg-obfuscator.conf"
|
||||
|
||||
for arch in mipsel mips aarch64 armv7 x86_64; do
|
||||
[[ -f "$PHOBOS_DIR/bin/wg-obfuscator-$arch" ]] && cp "$PHOBOS_DIR/bin/wg-obfuscator-$arch" "$pkg_root/bin/"
|
||||
# Fetch missing router-arch binaries from ClusterM on-demand
|
||||
local _tag; _tag=$(curl -fsSL -m10 https://api.github.com/repos/ClusterM/wg-obfuscator/releases/latest 2>/dev/null | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*//')
|
||||
local _cb="https://github.com/ClusterM/wg-obfuscator/releases/download/${_tag}"
|
||||
declare -A _am=([aarch64]="linux-arm64" [mipsel]="linux-mipsel-mips32" [mips]="linux-mips-mips32" [armv7]="linux-armv7-hf" [x86_64]="linux-x64")
|
||||
for _arch in aarch64 mipsel mips armv7 x86_64; do
|
||||
if [[ ! -f "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" ]] && [[ -n "$_tag" ]]; then
|
||||
local _t; _t=$(mktemp -d)
|
||||
if curl -fsSL -m30 "${_cb}/wg-obfuscator-${_tag}-${_am[$_arch]}.tar.gz" -o "$_t/o.tgz" 2>/dev/null; then
|
||||
tar -xzf "$_t/o.tgz" -C "$_t" 2>/dev/null || true
|
||||
local _b; _b=$(find "$_t" -name "wg-obfuscator" -type f | head -1)
|
||||
[[ -n "$_b" ]] && cp "$_b" "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" && chmod +x "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" && log_info "+wg-obfuscator-$_arch (ClusterM ${_tag})"
|
||||
fi
|
||||
rm -rf "$_t"
|
||||
fi
|
||||
[[ -f "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" ]] && cp "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" "$pkg_root/bin/"
|
||||
done
|
||||
|
||||
local tpl_dir="$REPO_DIR/client/templates"
|
||||
@@ -249,7 +268,7 @@ action_package() {
|
||||
source "$PHOBOS_DIR/server/server.env"
|
||||
cat > "$pkg_root/failover.conf" <<FAILEOF
|
||||
# Phobos Failover Configuration
|
||||
SERVER_1=${SERVER_PUBLIC_IP_V4}:51821,51822,51823
|
||||
SERVER_1=${SERVER_PUBLIC_IP_V4}:${OBFUSCATOR_PORTS:-51821,51822,51823}
|
||||
KEY_1=${OBFUSCATOR_KEY}
|
||||
FAILEOF
|
||||
|
||||
|
||||
70
uninstall.sh
Executable file
70
uninstall.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# PCA Phobos — полное удаление сервера (primary / panel node)
|
||||
#
|
||||
# bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/uninstall.sh)
|
||||
#
|
||||
# Останавливает и удаляет: панель, обфускатор-сервисы, WireGuard wg0,
|
||||
# nginx-сайт, watchdog-cron, бинарники, iptables-правила, каталоги Phobos.
|
||||
# По умолчанию спрашивает подтверждение. PURGE=1 — без вопросов.
|
||||
# ============================================================
|
||||
set -u
|
||||
PHOBOS_DIR="/opt/Phobos"
|
||||
PANEL_DIR="/opt/phobos-panel"
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || { echo "Запусти от root"; exit 1; }
|
||||
|
||||
echo "=================================================="
|
||||
echo " PCA Phobos — УДАЛЕНИЕ сервера"
|
||||
echo " Будут удалены: панель, obfuscator, wg0, nginx-сайт,"
|
||||
echo " watchdog, бинарники, $PHOBOS_DIR, $PANEL_DIR"
|
||||
echo "=================================================="
|
||||
if [ "${PURGE:-0}" != "1" ]; then
|
||||
printf "Точно удалить ВСЁ? [yes/NO]: "
|
||||
read ans
|
||||
[ "$ans" = "yes" ] || { echo "Отменено."; exit 0; }
|
||||
fi
|
||||
|
||||
IFACE=$(ip route get 8.8.8.8 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="dev") print $(i+1)}' | head -1)
|
||||
IFACE="${IFACE:-eth0}"
|
||||
|
||||
echo "[1/8] stop + disable services..."
|
||||
# obfuscator services (any port)
|
||||
for svc in $(systemctl list-units --all --plain --no-legend 'wg-obfuscator-*' 2>/dev/null | awk '{print $1}'); do
|
||||
systemctl stop "$svc" 2>/dev/null; systemctl disable "$svc" 2>/dev/null
|
||||
done
|
||||
for svc in phobos-panel phobos-api wg-quick@wg0; do
|
||||
systemctl stop "$svc" 2>/dev/null; systemctl disable "$svc" 2>/dev/null
|
||||
done
|
||||
|
||||
echo "[2/8] remove systemd units..."
|
||||
rm -f /etc/systemd/system/wg-obfuscator-*.service 2>/dev/null
|
||||
rm -f /etc/systemd/system/phobos-panel.service /etc/systemd/system/phobos-api.service 2>/dev/null
|
||||
systemctl daemon-reload 2>/dev/null
|
||||
|
||||
echo "[3/8] remove WireGuard wg0..."
|
||||
rm -f /etc/wireguard/wg0.conf 2>/dev/null
|
||||
|
||||
echo "[4/8] remove iptables rules..."
|
||||
iptables -t nat -D POSTROUTING -o "$IFACE" -j MASQUERADE 2>/dev/null || true
|
||||
iptables -D FORWARD -i wg0 -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
|
||||
iptables -D INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null || true
|
||||
|
||||
echo "[5/8] remove nginx site..."
|
||||
rm -f /etc/nginx/sites-enabled/phobos /etc/nginx/sites-available/phobos 2>/dev/null
|
||||
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
|
||||
|
||||
echo "[6/8] remove watchdog cron..."
|
||||
( crontab -l 2>/dev/null | grep -v 'phobos-router-watchdog' | grep -v '/opt/Phobos' || true ) | crontab - 2>/dev/null || true
|
||||
|
||||
echo "[7/8] remove binaries / symlinks..."
|
||||
rm -f /usr/local/bin/wg-obfuscator /usr/local/bin/phobos-update 2>/dev/null
|
||||
|
||||
echo "[8/8] remove directories..."
|
||||
rm -rf "$PHOBOS_DIR" "$PANEL_DIR" 2>/dev/null
|
||||
|
||||
echo "=================================================="
|
||||
echo " Phobos удалён."
|
||||
echo " (WireGuard/nginx/cron-пакеты НЕ удалены — общие; убрать вручную при желании)"
|
||||
echo "=================================================="
|
||||
105
update.sh
105
update.sh
@@ -2,24 +2,47 @@
|
||||
# ============================================================
|
||||
# PCA Phobos — updater / rollback
|
||||
#
|
||||
# phobos-update # обновить до последней версии (main)
|
||||
# phobos-update v1.1.0 # обновить до конкретной версии (git tag)
|
||||
# phobos-update # обновить до стабильной (канал stable / main)
|
||||
# phobos-update stable # стабильный канал (публичный, без токена)
|
||||
# GH_TOKEN=... phobos-update beta # бета-канал (приватный, по токену подписчика)
|
||||
# GH_TOKEN=... phobos-update dev # канал разработки (приватный, по токену)
|
||||
# phobos-update v1.2.4 # конкретная версия (git tag)
|
||||
# phobos-update --rollback # откатить на предыдущую версию (из бэкапа)
|
||||
# phobos-update --version # показать установленную и последнюю версию
|
||||
# phobos-update --check # установленная + версии всех каналов
|
||||
# phobos-update --list # список бэкапов для отката
|
||||
#
|
||||
# Обновляет ТОЛЬКО PCA-слой (панель + скрипты онбординга/служебные).
|
||||
# НЕ трогает ключи WireGuard, server.env, клиентов — они сохраняются.
|
||||
# Перед каждым обновлением делает бэкап; откат восстанавливает его.
|
||||
# Каналы:
|
||||
# stable -> публичный репозиторий andrey271192/PCA_Phobos (открыт)
|
||||
# beta / dev -> приватный andrey271192/PCA_Phobos-dev (только подписчики)
|
||||
# Для beta/dev нужен read-only токен подписчика: env GH_TOKEN или файл
|
||||
# $PANEL_DIR/.gh_token (сохраняется при установке закрытого канала).
|
||||
#
|
||||
# Обновляет ТОЛЬКО PCA-слой (панель + скрипты). НЕ трогает ключи WireGuard,
|
||||
# server.env, клиентов. Перед каждым обновлением — бэкап; откат восстанавливает.
|
||||
# ============================================================
|
||||
set -e
|
||||
PHOBOS_DIR=/opt/Phobos
|
||||
PANEL_DIR=/opt/phobos-panel
|
||||
REPO=andrey271192/PCA_Phobos
|
||||
RAW="https://raw.githubusercontent.com/$REPO"
|
||||
PUB_REPO=andrey271192/PCA_Phobos
|
||||
DEV_REPO=andrey271192/PCA_Phobos-dev
|
||||
BACKUPS="$PHOBOS_DIR/updates"
|
||||
VERFILE="$PANEL_DIR/.version"
|
||||
CUR=$(cat "$VERFILE" 2>/dev/null || echo "unknown")
|
||||
# Токен подписчика для приватных каналов: env > сохранённый файл
|
||||
GH_TOKEN="${GH_TOKEN:-$(cat "$PANEL_DIR/.gh_token" 2>/dev/null || true)}"
|
||||
|
||||
# repo for a ref/channel: beta/dev/*-beta/*-dev -> private, остальное -> public
|
||||
repo_for() { case "$1" in beta|dev|*-beta|*-dev) echo "$DEV_REPO";; *) echo "$PUB_REPO";; esac; }
|
||||
is_private() { case "$1" in beta|dev|*-beta|*-dev) return 0;; *) return 1;; esac; }
|
||||
# curl with token header only when ref is a private channel
|
||||
gh_curl() { # $1=ref, rest=curl args
|
||||
local ref="$1"; shift
|
||||
if is_private "$ref" && [ -n "$GH_TOKEN" ]; then
|
||||
curl -H "Authorization: token $GH_TOKEN" "$@"
|
||||
else
|
||||
curl "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
managed_files() {
|
||||
cat <<LIST
|
||||
@@ -35,7 +58,13 @@ $PHOBOS_DIR/server/update.sh|update.sh
|
||||
LIST
|
||||
}
|
||||
|
||||
latest_version() { curl -fsSL -m10 "$RAW/main/VERSION" 2>/dev/null | tr -d ' \r\n'; }
|
||||
# fetch VERSION of a channel (public repos / or private if token present)
|
||||
chan_version() { # $1 = ref(channel)
|
||||
local repo; repo=$(repo_for "$1"); local repo_ref="$1"
|
||||
if is_private "$1" && [ -z "$GH_TOKEN" ]; then echo ""; return; fi
|
||||
gh_curl "$1" -fsSL -m8 "https://raw.githubusercontent.com/$repo/$1/VERSION" 2>/dev/null | tr -d ' \r\n'
|
||||
}
|
||||
latest_version() { curl -fsSL -m10 "https://raw.githubusercontent.com/$PUB_REPO/main/VERSION" 2>/dev/null | tr -d ' \r\n'; }
|
||||
|
||||
do_backup() {
|
||||
local dir="$BACKUPS/${CUR}-$(date +%Y%m%d-%H%M%S)"
|
||||
@@ -48,10 +77,12 @@ do_backup() {
|
||||
}
|
||||
|
||||
apply_ref() {
|
||||
local ref="$1"
|
||||
local ref="$1"; local repo; repo=$(repo_for "$ref")
|
||||
local repo_ref="$ref"
|
||||
local RAW="https://raw.githubusercontent.com/$repo/$ref"
|
||||
managed_files | while IFS='|' read -r lp rp; do
|
||||
local tmp; tmp=$(mktemp)
|
||||
if curl -fsSL -m25 "$RAW/$ref/$rp" -o "$tmp" && [ -s "$tmp" ]; then
|
||||
if gh_curl "$ref" -fsSL -m25 "$RAW/$rp" -o "$tmp" && [ -s "$tmp" ]; then
|
||||
mkdir -p "$(dirname "$lp")"; mv "$tmp" "$lp"
|
||||
case "$lp" in *.sh|*.py) chmod +x "$lp" 2>/dev/null || true;; esac
|
||||
else
|
||||
@@ -60,10 +91,33 @@ apply_ref() {
|
||||
done
|
||||
}
|
||||
|
||||
ref_exists() {
|
||||
local repo; repo=$(repo_for "$1"); local repo_ref="$1"
|
||||
gh_curl "$1" -fsSL -m12 "https://raw.githubusercontent.com/$repo/$1/app.py" -o /dev/null 2>/dev/null
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--version|-v)
|
||||
echo "Установлено: $CUR"
|
||||
echo "Доступно: $(latest_version)"
|
||||
--version|-v|--check)
|
||||
echo "Установлено: $CUR"
|
||||
echo "Стабильная: $(latest_version) [открытый]"
|
||||
b=$(chan_version beta); d=$(chan_version dev)
|
||||
if [ -n "$GH_TOKEN" ]; then
|
||||
[ -n "$b" ] && echo "Бета (beta): $b [приватный, токен ок]"
|
||||
[ -n "$d" ] && echo "Разработка(dev):$d [приватный, токен ок]"
|
||||
else
|
||||
echo "Бета (beta): — [приватный: нужен GH_TOKEN подписчика]"
|
||||
echo "Разработка(dev):— [приватный: нужен GH_TOKEN подписчика]"
|
||||
fi
|
||||
;;
|
||||
--versions|--tags)
|
||||
echo "Стабильные версии (публичные теги):"
|
||||
curl -fsSL -m10 "https://api.github.com/repos/$PUB_REPO/tags?per_page=100" 2>/dev/null \
|
||||
| grep -oE '"name": *"[^"]+"' | sed 's/.*"name": *"\(.*\)"/ \1/' || echo " (нет данных)"
|
||||
if [ -n "$GH_TOKEN" ]; then
|
||||
echo "Закрытые версии (приватные теги, beta/dev):"
|
||||
curl -fsSL -m10 -H "Authorization: token $GH_TOKEN" "https://api.github.com/repos/$DEV_REPO/tags?per_page=100" 2>/dev/null \
|
||||
| grep -oE '"name": *"[^"]+"' | sed 's/.*"name": *"\(.*\)"/ \1/' || echo " (нет данных)"
|
||||
fi
|
||||
;;
|
||||
--list)
|
||||
echo "Бэкапы (для отката):"
|
||||
@@ -82,8 +136,27 @@ case "${1:-}" in
|
||||
echo "Откат выполнен -> $fromver"
|
||||
;;
|
||||
*)
|
||||
ref="${1:-main}"; [ "$ref" = "latest" ] && ref="main"
|
||||
target=$(curl -fsSL -m10 "$RAW/$ref/VERSION" 2>/dev/null | tr -d ' \r\n'); [ -z "$target" ] && target="$ref"
|
||||
ref="${1:-main}"
|
||||
case "$ref" in latest|stable) ref="main";; esac
|
||||
# Каналы beta/dev — приватный репозиторий, нужен токен подписчика
|
||||
if is_private "$ref" && [ -z "$GH_TOKEN" ]; then
|
||||
echo "Канал '$ref' закрыт (бета/разработка) — приватный репозиторий."
|
||||
echo "Нужен read-only токен подписчика:"
|
||||
echo " GH_TOKEN=ваш_токен phobos-update $ref"
|
||||
echo "Токен — по подписке: https://boosty.to/andrey27 . Стабильная (без токена): phobos-update stable"
|
||||
exit 1
|
||||
fi
|
||||
if ! ref_exists "$ref"; then
|
||||
echo "Версия/ветка '$ref' не найдена (или токен неверный). Обновление отменено (текущая $CUR не тронута)."
|
||||
echo "Доступные: phobos-update --versions · стабильная: $(latest_version)"
|
||||
exit 1
|
||||
fi
|
||||
# сохранить токен для будущих обновлений приватного канала
|
||||
if is_private "$ref" && [ -n "$GH_TOKEN" ]; then
|
||||
( umask 077; printf %s "$GH_TOKEN" > "$PANEL_DIR/.gh_token" )
|
||||
fi
|
||||
repo_ref="$ref"
|
||||
target=$(gh_curl "$ref" -fsSL -m10 "https://raw.githubusercontent.com/$(repo_for "$ref")/$ref/VERSION" 2>/dev/null | tr -d ' \r\n'); [ -z "$target" ] && target="$ref"
|
||||
echo "Текущая: $CUR -> целевая: $target ($ref)"
|
||||
b=$(do_backup); echo "Бэкап: $b"
|
||||
apply_ref "$ref"
|
||||
|
||||
Reference in New Issue
Block a user