5 Commits
v1.4.1 ... beta

Author SHA1 Message Date
PCA
5768968863 channel beta is token-only now: private repo PCA_Phobos-dev (stub)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 03:49:13 +03:00
Бобырев Андрей
d795bc6f70 release: 1.4.2 — server obfuscator settings, Android 1.4.3 fix for configs with unknown obfuscator keys
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:57:13 +03:00
Бобырев Андрей
c139debd8d Keep desktop (macOS/Windows) clients private
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 04:15:32 +03:00
Бобырев Андрей
01632034b0 macos: PCA Phobos for Mac
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:54:34 +03:00
Бобырев Андрей
071c1dc7f1 ios: PCA Phobos VPN for iPhone (source)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:37:06 +03:00
87 changed files with 9 additions and 22581 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
custom:
- "https://boosty.to/andrey27/donate"
- "https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798"

2
.gitignore vendored
View File

@@ -1,2 +0,0 @@
__pycache__/
*.pyc

10
BETA.md
View File

@@ -1,10 +0,0 @@
# Каналы PCA Phobos
С версии **1.4.0** все три канала (stable / beta / dev) — одна и та же версия.
Новые фичи по-прежнему сначала выкатываются в `dev`, затем в `beta`, затем в `main`
(`promote.sh`), но база одна — без расхождений.
```bash
phobos-update # stable (открытый)
GH_TOKEN=… phobos-update beta|dev # для подписчиков Boosty
```

View File

@@ -1,132 +0,0 @@
# 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`, клиенты — НЕ трогаются.
---
## 1.4.1 — резервный TCP-канал, «РФ напрямую»
- **feat:** резервный канал TCP: если провайдер режет UDP, клиент сам переходит на TCP (порт 443, иначе 8443/2096/4433). Сервер: служба `phobos-tcp` (`udptcp`), ставится и включается автоматически при установке и обновлении. Проверено реальной блокировкой UDP: переключение за ~15 с, без блокировки остаётся UDP.
- **feat (Android 1.4.2):** «Российские сайты — напрямую, без VPN» (по умолчанию), выбор канала Авто/UDP/TCP, журнал TCP-канала.
- **fix:** установка и обновления с git.bobyrev.com (GitHub — запасной), каскад раздаёт панель.
- Ограничения: TCP-резерв пока только на основном сервере (не на вторых) и только в приложении PCA Phobos VPN (роутеры и PhobosWG — по-прежнему UDP с перебором портов).
## 1.4.0 — единая версия (stable = beta = dev)
Три канала сведены в одну версию: всё из dev (WARP, ttyd, бэкап, маршрут Keenetic, параметры обфускатора)
+ фиксы beta (обновлятор через API, порт/схема панели) + новое:
- **feat:** светлая/тёмная тема, компактные карточки клиентов, мобильная версия без горизонтальной прокрутки.
- **feat:** лимит скорости на клиента (tc на wg0, применяется и на вторых серверах).
- **feat:** замер скорости сервера (реальный) и клиента (живой + `/speedtest` на устройстве через туннель).
- **feat:** домен панели (nginx + Let's Encrypt), адрес для клиентов по домену, каскад (`phobos-cascade.sh`).
- **feat:** сторож — блоклист сканеров РКН (`phobos-guard.sh`).
- **fix:** secondary-setup на чистой Ubuntu 24.04 (нет iptables → wg0 не поднимался), без tty, `SERVER_IP` для NAT; клиенты досылаются на новый сервер сразу.
- **fix:** очистка orphan-peer (не было WG_CONFIG), дубль карточки WARP и кнопки WARP, имена клиентов кириллицей.
- **fix:** после обновления старым обновлятором 1.2.x/1.3.x панель сама докачивает новые скрипты.
- **fix:** APK без токена подписчика — из публичного релиза (как в 1.2.8).
Обновление: `phobos-update` (stable) — клиенты, ключи и `server.env` не трогаются. Проверено на школе: 1.2.8 → 1.4.0, старый клиент работает без переустановки.
## 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.8 / 1.3.x | Android PhobosWG: «Неправильная длина ключа в PublicKey» | Android/iOS payload восстанавливает потерянный `=` в WireGuard ключах и не добавляет `PresharedKey = none`; QR/link нужно открыть заново |
| 1.2.8 / 1.3.x | Установка/обновление могли пройти, но потом панель генерила кривую ссылку | `phobos-self-check --fix` встроен в install/update и доступен как отдельная команда |
| 1.2.7 / 1.3.x | PhobosWG: «невозможно импортировать туннель» | `phobos://` дополняется `= none` для всех обязательных полей (`PresharedKey`) по спеке формата |
| 1.2.8 / 1.3.x | Install `[6/9] PCA patches`: `curl: (23) Failure writing output to destination` | `fetch()` создаёт папку назначения перед `curl -o` |
| 1.2.8 / 1.3.x | VPS x86_64: `ERROR: obfuscator binary for x86_64 missing` | авто-загрузка `linux-x64` из ClusterM releases + нормализация `amd64 -> x86_64` |
| 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
- **fix:** install/update теперь запускают `phobos-self-check --fix`: автоматически чинят потерянный `=` в `SERVER_WG_PUBLIC_KEY`, сверяют ключ с `wg0`, проверяют router helper-файлы и shell syntax до выдачи клиентских ссылок.
- **fix:** Android/iOS phone configs теперь восстанавливают потерянный `=` в WireGuard `PrivateKey/PublicKey/PresharedKey`. Это закрывает ошибку PhobosWG «Неправильная длина ключа в PublicKey».
- **fix:** `lib-core.sh` больше не теряет финальный `=` при чтении `server.env`, поэтому новые клиентские `.conf` получают полный `PublicKey`.
- **fix:** клиентский router package теперь содержит обязательные `lib-client.sh`, `install-obfuscator.sh`, `install-wireguard.sh` и дополнительные helper-скрипты из PCA repo, поэтому `wget .../init/TOKEN.sh | sh` больше не падает с `can't open './lib-client.sh'`.
- **fix:** шаг `[6/9] PCA patches` больше не падает с `curl: (23) Failure writing output to destination`, если папки `repo/server/scripts` или `repo/client/templates` не были созданы upstream sparse clone.
- **fix:** установка `wg-obfuscator` на x86_64 больше не падает с `ERROR: obfuscator binary for x86_64 missing`. Installer и secondary installer берут `linux-x64` из ClusterM releases.
- **fix:** свежая beta-установка снова получает `lib-core.sh`, без которого client helper мог ломаться на чистой машине.
- **fix:** старые ссылки `/tags` теперь ведут в `/labels`, без 404.
- **fix:** поле имени клиента больше не даёт ошибку в консоли Chrome из-за HTML `pattern`. UI bench-test прошёл без console/page errors.
- **fix:** WG tunnel subnet перестала быть жёстко зашита в `10.25.0.0/16`. Installer, secondary deploy, client packaging и router pull теперь используют `SERVER_WG_IPV4_NETWORK`, так что `10.26`/`10.27` и другие отдельные WG-сети на разных VPS работают без ручных правок.
- **fix:** в клиентский пакет добавляется `panel_tunnel_ip`, а `phobos-pull` использует его как tunnel endpoint панели вместо жёсткого `10.25.0.1`.
- скачивание `.conf` для телефона (Android `phobos://` + QR, iOS обычный WireGuard + QR);
- кнопка скачивания приложения **PhobosWG (APK)** — авто-загрузка из GitHub Release приватного repo;
- все критичные фиксы выше.
---
## dev (приватный, активная разработка)
### v1.3.4-dev
- **fix:** install/update теперь запускают `phobos-self-check --fix`: автоматически чинят потерянный `=` в `SERVER_WG_PUBLIC_KEY`, сверяют ключ с `wg0`, проверяют router helper-файлы и shell syntax до выдачи клиентских ссылок.
- **fix:** Android/iOS phone configs теперь восстанавливают потерянный `=` в WireGuard `PrivateKey/PublicKey/PresharedKey`. Это закрывает ошибку PhobosWG «Неправильная длина ключа в PublicKey».
- **fix:** `lib-core.sh` и WARP helper больше не теряют финальный `=` при чтении `server.env`, поэтому новые клиентские `.conf` получают полный `PublicKey`.
- **fix:** клиентский router package теперь содержит обязательные `lib-client.sh`, `install-obfuscator.sh`, `install-wireguard.sh` и дополнительные helper-скрипты из PCA repo, поэтому `wget .../init/TOKEN.sh | sh` больше не падает с `can't open './lib-client.sh'`.
- **fix:** шаг `[6/9] PCA patches` больше не падает с `curl: (23) Failure writing output to destination`, если папки `repo/server/scripts` или `repo/client/templates` не были созданы upstream sparse clone.
- **fix:** установка `wg-obfuscator` на x86_64 больше не падает с `ERROR: obfuscator binary for x86_64 missing`. Installer и secondary installer берут `linux-x64` из ClusterM releases.
- **fix:** поле имени клиента больше не даёт ошибку в консоли Chrome из-за HTML `pattern`. UI bench-test прошёл без console/page errors.
- **fix:** WG tunnel subnet больше не зашита в `10.25.0.0/16`. `install.sh`, `secondary-setup.sh`, client packaging, router pull и secondary deploy теперь берут `SERVER_WG_IPV4_NETWORK`, поэтому один сервер можно держать на `10.25`, другой на `10.26`, `10.27` и дальше без ручного `nano`.
- **fix:** в клиентский пакет добавляется `panel_tunnel_ip`, а `phobos-pull`/`phobos-ttyd` используют его вместо жёсткого `10.25.0.1`. Это убирает поломку управления роутером после смены WG-подсети на сервере.
- **fix:** per-client WARP стал действительно точечным. Удалено старое правило на всю подсеть WG, правила теперь ставятся и снимаются только по `client tunnel IP /32`.
- **fix:** клиентские пакеты и install-команды снова собираются стабильно: создаётся `/opt/Phobos/bin`, шаблон `install-router.sh.template` обрабатывается мягко, пустой `OBFUSCATOR_KEY` больше не валит сборку.
- **fix:** `/clients` больше не падает при неполной выкладке dev-стенда. Добавлены обязательные `lib-core.sh` и client templates в install/update flow.
- **fix:** “живой peer есть, клиента нет” больше не выглядит как пропавший клиент. Сиротские peer теперь отображаются в `Сессии` и `Клиенты` как `orphan-*`, с кнопками `WARP`, `Kick`, `Purge`.
- **fix:** старые ссылки `/tags` теперь ведут в `/labels`.
- **security:** login hardening — `HttpOnly` + `SameSite=Lax` cookie, ограничение попыток входа, constant-time compare для пароля и API keys, upload limit `64 MB`.
- **ops:** `phobos-update` и `phobos-backup restore` теперь сами определяют активный panel service (`phobos-panel` или `phobos-dev-test`) и перезапускают правильный unit.
- **security/cleanup:** из secondary API убран `shell=True` там, где он был не нужен; чтение CPU/mem/network переведено на более безопасный путь.
### v1.3.1-dev
- **баланс по трафику:** агенты отдают пропускную способность канала (rx/tx NIC), `server_load = 0.45·cpu + 0.30·mem + 0.25·bw`; карточки серверов показывают ↕ Mbps;
- **WARP / WARP+:** в `/settings` добавлен отдельный блок с режимами `egress`, `relay`, `custom profile` и полем `WARP+ key`;
- **egress mode:** исходящий трафик клиентов можно завернуть через Cloudflare WARP на стороне сервера;
- **relay mode:** добавлен UDP relay по мотивам `warp-relay` для отдельных WARP/WireGuard профилей;
- **custom profile:** полный `warp.conf` можно вставить и сохранить прямо из веб-панели;
- **help UI:** у блока WARP появился `?` с человеческим объяснением режимов, ограничений и сценариев использования;
- все фиксы и фичи из beta.

240
README.md
View File

@@ -1,235 +1,11 @@
# PCA Phobos — VPN, который не отключается
# PCA Phobos — канал beta закрыт
**Веб-панель и turnkey-установщик для [Phobos](https://github.com/Ground-Zerro/Phobos)** — обфусцированного WireGuard VPN. Ставится на чистый VPS одной командой, управляет десятками роутеров и устройств с одной страницы, сам переключается между серверами при сбоях и восстанавливается после перезагрузок.
Каналы **beta** и **dev** выдаются по токену подписчика (read-only токен Gitea) и лежат в приватном репозитории.
Здесь их нет — публичная только стабильная версия (`main`, теги `v*`).
<p align="center">
<img src="docs/img/sessions.png" alt="Панель — активные сессии" width="850">
</p>
Как подключить:
1. Обновись до стабильной 1.4.5 или новее: `phobos-update stable`
2. Вставь токен: панель → Настройки → Версия и обновления → «токен подписчика» → Сохранить токен
3. `phobos-update beta` (или кнопка beta в панели)
---
## Что это простыми словами
Обычный VPN падает, когда падает сервер или провайдер банит IP. **PCA Phobos — нет.**
- 🌐 **Маскированный WireGuard** — трафик не отличить от обычного, проходит мимо DPI и блокировок по сигнатуре.
- 🔁 **Авто-переключение между серверами** — сервер лёг или порт прикрыли → роутер сам прыгает на резервный за ~10–15 секунд, без твоего участия.
- 📡 **Работает за любым NAT** — серый IP, мобильный интернет, домашний роутер за провайдером. Управление едет внутри самого защищённого туннеля, поэтому бан публичного IP ему не страшен.
- 🖥️ **Одна команда — новое устройство** — создал клиента в панели → вставил команду на роутер → готово.
- ♻️ **Само-восстановление** — роутер перезагрузился, провайдер сменил IP, сервис отвалился → система поднимет всё обратно сама.
- 📱 **Телефоны** — своё приложение **PCA Phobos VPN** для Android (обфускация + раздельный трафик по приложениям), PhobosWG, iPhone (обычный WireGuard) — через QR/ссылку.
- 🛠 **Админ-приложение** PCA Phobos для Android — все ваши панели в одном месте.
---
## Возможности
- **Активные сессии** — 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 + подсказки `?` у каждого действия. **Светлая и тёмная тема** (кнопка ◐), компактная вёрстка — удобно с телефона.
- **Лимит скорости на клиента** — Мбит/с в карточке клиента; работает на всех серверах, сам восстанавливается после перезапуска WireGuard.
- **Замер скорости** — сервера (реальная скачка/отдача, кнопка ⚡ на «Серверах»), клиента (живая скорость в карточке + страница `http://10.25.0.1:<порт>/speedtest` на устройстве через VPN).
- **Домен панели** — `https://ваш-домен/` с бесплатным сертификатом Let's Encrypt; старый адрес `http://IP:порт` продолжает работать.
- **Адрес для клиентов и каскад** — клиенты подключаются по домену или через входной сервер (например, в РФ) → ваш сервер; роутеры получают адрес сами.
- **Резервный TCP-канал** — если провайдер режет UDP, клиент сам переходит на TCP (порт 443), обфускация сохраняется.
- **Сторож** — блокировка зондов и сканеров РКН (nftables, список обновляется раз в сутки).
- **WARP / WARP+** — выпуск клиентов в интернет через Cloudflare WARP (всем или выборочно), relay-режим.
- **Бэкап и восстановление** сервера из панели; **терминал роутера в браузере** (ttyd/RCI) за NAT.
- **Версии и обновления** — обновление и откат прямо из панели.
- **Уведомления per-client** — 🔔 у каждого клиента: включить/выключить Telegram-оповещения о подключении/отключении.
- **Telegram-бот** — управление клиентами командами прямо из чата:
- `/add <имя>` — создать клиента (бот пришлёт команду установки)
- `/del <имя>` — удалить клиента
- `/list` — список клиентов
- **Кнопки** — постоянная клавиатура: ➕ Создать клиента · 📋 Список · 🗑 Удалить (инлайн-выбор) · 📊 Статус · ❓ Помощь.
(Работает только для чата из настроек `tg_chat_id`.) Бот **необязателен**: хочешь — впиши токен от @BotFather и свой chat id в «Настройки», не хочешь — оставь пустым.
---
## Скриншоты
| Клиенты (светлая) | Клиенты (тёмная) |
|---|---|
| ![Клиенты](docs/img/clients.png) | ![Клиенты тёмная](docs/img/clients-dark.png) |
| **Сессии** | **Серверы: замер скорости, второй сервер** |
| ![Сессии](docs/img/sessions.png) | ![Серверы](docs/img/servers.png) |
| Телефон: панель | Телефон: тёмная | Android: VPN-клиент | Android: раздельный трафик | Android: админ-приложение |
|---|---|---|---|---|
| ![](docs/img/mobile-clients.png) | ![](docs/img/mobile-clients-dark.png) | ![](docs/img/android-vpn.png) | ![](docs/img/android-vpn-apps.png) | ![](docs/img/android-admin.png) |
Настройки (домен, каскад, сторож, WARP, бэкап): [светлая](docs/img/settings.png) · [тёмная](docs/img/settings-dark.png)
--------|---------|---------|
| ![Сессии](docs/img/sessions.png) | ![Клиенты](docs/img/clients.png) | ![Серверы](docs/img/servers.png) |
> Если картинки не отображаются — положи свои PNG в `docs/img/` (`sessions.png`, `clients.png`, `servers.png`).
---
> **Источник с 23.10.2026:** `git.bobyrev.com/PCA/PCA_Phobos` (GitHub-аккаунт временно недоступен). Серверы на 1.2.x обновляются одной командой:
> ```bash
> bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/update.sh)
> ```
## Быстрый старт (с чистого VPS, одной командой)
Ставит **всё** со всеми зависимостями: wg-obfuscator, WireGuard, обфускатор-сервисы, веб-панель, nginx, скрипты онбординга роутеров, сторож авто-восстановления. Предустановленный Phobos **не требуется**.
```bash
bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/install.sh)
```
### С параметрами
```bash
PANEL_PASS=МойПароль TG_TOKEN=123:abc TG_CHAT=123456789 \
bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/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 (без обфускации) |
| `PANEL_TLS_CERT` / `PANEL_TLS_KEY` | пусто | Включить HTTPS для панели через свой сертификат |
После установки панель напечатает адрес, логин, пароль и **API key** — сохрани их.
Полный список портов и что можно закрывать: [docs/PORTS.md](docs/PORTS.md).
### Вторичный сервер (для failover / балансировки)
```bash
MAIN_PROTO=http MAIN_SERVER=<ip_основного> MAIN_PORT=<порт_панели> MAIN_API_KEY=<API key из основного> \
bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/server/secondary-setup.sh)
```
Сервер сам зарегистрируется в панели, клиенты досылаются на него сразу. Если сервер за NAT — добавь `SERVER_IP=<внешний_ip>`.
### Каскад (вход в РФ → выход за рубежом)
На входном VPS (чистый Debian/Ubuntu, Phobos не нужен):
```bash
bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/server/phobos-cascade.sh) enable <IP_выходного_сервера> 2083,5443,993
```
Затем в панели выходного сервера → **Настройки → Адрес для клиентов и каскад** → впиши IP/домен входа. Роутеры переключатся сами (~15 с), телефонам — заново открыть ссылку/QR. Отключить: `phobos-cascade disable`.
### Сторонние «сторожа» по просьбам участников
- Защита от зондов РКН ([Loorrr293/Auto_IPtables](https://github.com/Loorrr293/Auto_IPtables)) — **встроена**: «Настройки → Защита сервера», включается кнопкой.
- «Прикрывашка» ([miha75vu-bit/Flashkeen](https://github.com/miha75vu-bit/Flashkeen)) — WARP на выход + Psiphon; интерактивная, ставится по SSH: `bash <(curl -fsSL https://raw.githubusercontent.com/miha75vu-bit/Flashkeen/main/wp)`. Перестраивает выход wg0 — не включай одновременно со встроенным WARP egress.
- ipidea-kimwolf-ban — репозиторий автора удалён (404), встроить нечего.
---
## Приложения для Android
- **PCA Phobos VPN** — клиент: включает VPN одной кнопкой, импорт `phobos://` из панели, встроенный обфускатор, **раздельный трафик** (весь / только выбранные приложения / все кроме выбранных), **российские сайты напрямую** (по умолчанию), **канал Авто/UDP/TCP** (при блокировке UDP — сам на TCP), журнал, проверка скорости. [Скачать APK](https://git.bobyrev.com/PCA/PCA_Phobos/releases/download/v1.4.1/PCA-Phobos-VPN.apk) · исходники `android-client/`.
- **PCA Phobos** (админ) — список ваших панелей, открываются в приложении. [Скачать APK](https://git.bobyrev.com/PCA/PCA_Phobos/releases/download/v1.4.1/PCA-Phobos.apk) · исходники `android/`.
## Клиенты для телефона
В таблице **Клиенты** у каждого клиента есть кнопки:
- 📱 **Android** — `phobos://`-ссылка + QR для приложения **PhobosWG** (с обфускацией). Скан QR → импорт.
- 🍎 **iPhone / iOS** — обычный WireGuard-конфиг + QR для официального WireGuard (**без** обфускации). Требует открытого порта 51820 (`ALLOW_PLAIN_WG=1` при установке).
> Для каждого устройства создавай **отдельного клиента** (свой ключ и IP). Один конфиг на двух устройствах = конфликт ключей.
**Приложения:**
- **Android (PhobosWG):** кнопка «Скачать приложение PhobosWG (APK)» на странице Android-клиента. Можно положить свой `.apk` в `/opt/Phobos/www/app/PhobosWG.apk` — тогда панель раздаёт его.
- **iPhone (iOS):** официальный [WireGuard из App Store](https://apps.apple.com/app/wireguard/id1441195209) — кнопка есть на странице iOS-клиента.
---
## Обновление и откат
Проект версионируется (см. файл `VERSION` и git-теги `vX.Y.Z`).
**Из панели:** Настройки → «Версия и обновления» → **Обновить** / **Откатить**.
**Из консоли:**
```bash
phobos-update # обновить до последней версии (stable)
phobos-update beta|dev # каналы для подписчиков (GH_TOKEN), сейчас = та же 1.4.0
phobos-update v1.1.0 # обновить до конкретной версии (git tag)
phobos-update --rollback # откатить на предыдущую версию
phobos-update --version # показать установленную и доступную версию
phobos-update --list # список бэкапов
```
Обновляется только слой панели и скриптов. **Ключи WireGuard, `server.env` и клиенты не трогаются.** Перед каждым обновлением делается бэкап → откат восстанавливает его.
---
## Возможные проблемы и решения
| Симптом | Причина | Решение |
|---------|---------|---------|
| **Команда установки → 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` |
| **Домен: certbot failed** | A-запись не на этот сервер / закрыт 80 | `dig +short домен` = IP сервера; открой 80/tcp; повтори в «Настройках» |
| **Лимит скорости не действует** | wg0 перезапущен | панель сама восстановит за ≤1 мин; вручную: `phobos-limit.sh apply` |
| **Кнопки клиента «уезжают»** | старая версия | обнови до 1.4.0 — карточки клиентов компактные, телефоны тоже |
Логи: `journalctl -u phobos-panel -n 50` · сторож: `/opt/Phobos/server/watchdog.log` · роутер: `/opt/etc/Phobos/health.log`.
---
## Архитектура
```
Телефон/Роутер → wg-obfuscator (клиент) ──обфускация──► сервер :2083/5443/993
│ де-обфускация
WireGuard :51820
│ 10.25.0.x
┌──────────┴──────────┐
Веб-панель Failover на
(Flask+Gunicorn) резервные серверы
```
- Протокол: **WireGuard + wg-obfuscator** (обфускация от DPI).
- Управление роутерами — по **туннелю** (`10.25.0.1`), устойчиво к бану публичного IP.
- Состояние: JSON-файлы (без БД).
---
## Поддержка проекта
Если PCA Phobos оказался полезен — буду благодарен за поддержку:
- 💖 **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:** [@PCAdministration](https://t.me/PCAdministration) — вопросы, баги, идеи
## На основе и благодарности
PCA Phobos — это веб-панель и turnkey-инсталлятор поверх отличных открытых проектов:
- [**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) ❤️
Спасибо авторам за отличные инструменты.
Токен — по подписке: https://boosty.to/andrey27

View File

@@ -1 +1 @@
1.4.1
token-required

View File

@@ -1,2 +0,0 @@
build/
libs/x/

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pca.phobos.vpn"
android:versionCode="142"
android:versionName="1.4.2">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="35" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application
android:label="PCA Phobos VPN"
android:icon="@drawable/ic_launcher"
android:theme="@style/AppTheme"
android:extractNativeLibs="true"
android:allowBackup="false">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- panel "phobos://" links open straight into the app -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="phobos" />
</intent-filter>
</activity>
<!-- from com.wireguard.android:tunnel (AAR manifest, merged by hand: no Gradle) -->
<service
android:name="com.wireguard.android.backend.GoBackend$VpnService"
android:exported="false"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -1,20 +0,0 @@
# PCA Phobos VPN — Android-клиент
Своё приложение-клиент: импорт `phobos://` из панели, встроенный wg-obfuscator + WireGuard,
**раздельный трафик** по приложениям (весь / только выбранные / все кроме выбранных).
- WireGuard: `com.wireguard.android:tunnel` (Apache-2.0), `libs/`.
- Обфускатор: [ClusterM/wg-obfuscator](https://github.com/ClusterM/wg-obfuscator) v1.6 (GPL-3.0),
собран NDK под bionic: `jni/<abi>/libwgobfuscator.so`. Статический glibc-бинарник с роутеров
внутри приложения падает с SIGSYS (seccomp), поэтому только bionic-сборка.
- Само приложение всегда вне туннеля — иначе обфускатор не достучится до сервера.
Сборка без Gradle: `KS_PASS=… ./build.sh` → `build/PCA-Phobos-VPN.apk`
(ключ подписи — `~/.android/pca-phobos.jks`, пароль в ДОСТУПЫ.md; ключ не менять, иначе обновления не встанут).
Пересборка обфускатора:
```bash
NDK=~/Library/Android/sdk/ndk/29.0.14206865/toolchains/llvm/prebuilt/darwin-x86_64/bin
$NDK/aarch64-linux-android24-clang -O2 -std=gnu11 -o jni/arm64-v8a/libwgobfuscator.so wg-obfuscator/*.c
$NDK/armv7a-linux-androideabi24-clang -O2 -std=gnu11 -o jni/armeabi-v7a/libwgobfuscator.so wg-obfuscator/*.c
```

File diff suppressed because one or more lines are too long

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
# Build PCA Phobos VPN (client) without Gradle. Needs libs/: tunnel-*.aar (com.wireguard.android:tunnel),
# collection-*.jar, annotation-*.jar; jni/<abi>/libwgobfuscator.so (static ClusterM wg-obfuscator).
# KS_PASS=… ./build.sh -> build/PCA-Phobos-VPN.apk
set -euo pipefail
cd "$(dirname "$0")"
SDK="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
BT="$SDK/build-tools/$(ls "$SDK/build-tools" | sort -V | tail -1)"
JAR="$SDK/platforms/$(ls "$SDK/platforms" | sort -V | tail -1)/android.jar"
KS="${KS:-$HOME/.android/pca-phobos.jks}"
KS_PASS="${KS_PASS:?set KS_PASS}"
AAR=$(ls libs/tunnel-*.aar | tail -1)
rm -rf build && mkdir -p build/res build/classes build/dex build/aar build/apk/lib
unzip -q "$AAR" -d build/aar
LIBS="build/aar/classes.jar:$(ls libs/*.jar | tr '\n' ':')"
"$BT/aapt2" compile --dir res -o build/res/
"$BT/aapt2" link -o build/base.apk -I "$JAR" --manifest AndroidManifest.xml -A assets \
--java build/gen build/res/*.flat --min-sdk-version 24 --target-sdk-version 35
javac -source 11 -target 11 -nowarn -classpath "$JAR:$LIBS" -d build/classes \
$(find src build/gen -name '*.java') 2>&1 | grep -v "bootstrap classpath\|^Note:\|^warning: \[options\]\|^1 warning" || true
[ -f build/classes/com/pca/phobos/vpn/MainActivity.class ] || { echo "javac failed"; exit 1; }
"$BT/d8" --release --lib "$JAR" --min-api 24 --output build/dex \
$(find build/classes -name '*.class') build/aar/classes.jar libs/*.jar
cp build/base.apk build/unsigned.apk
(cd build/dex && zip -q ../unsigned.apk classes*.dex)
for abi in arm64-v8a armeabi-v7a; do
mkdir -p "build/apk/lib/$abi"
cp build/aar/jni/$abi/*.so "build/apk/lib/$abi/"
cp "jni/$abi/libwgobfuscator.so" "jni/$abi/libudptcp.so" "build/apk/lib/$abi/"
done
(cd build/apk && zip -qr ../unsigned.apk lib)
"$BT/zipalign" -f 4 build/unsigned.apk build/aligned.apk
"$BT/apksigner" sign --ks "$KS" --ks-pass "pass:$KS_PASS" --out build/PCA-Phobos-VPN.apk build/aligned.apk
"$BT/apksigner" verify build/PCA-Phobos-VPN.apk && ls -la build/PCA-Phobos-VPN.apk

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportHeight="108">
<path android:fillColor="#1e1b4b" android:pathData="M0,0h108v108h-108z" />
<path android:fillColor="#4f46e5"
android:pathData="M54,18 L82,28 L82,52 C82,70 70,84 54,90 C38,84 26,70 26,52 L26,28 Z" />
<path android:fillColor="#c7d2fe"
android:pathData="M54,30 L72,36.5 L72,52 C72,64 64,73.5 54,78 C44,73.5 36,64 36,52 L36,36.5 Z" />
<path android:fillColor="#1e1b4b"
android:pathData="M47,46 h14 v5 h-4.5 v16 h-5 v-16 h-4.5 z" />
</vector>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:Theme.Material.NoActionBar">
<item name="android:colorPrimary">#a5b4fc</item>
<item name="android:colorPrimaryDark">#1e1b4b</item>
<item name="android:colorAccent">#818cf8</item>
<item name="android:windowBackground">#0f172a</item>
<item name="android:statusBarColor">#1e1b4b</item>
</style>
</resources>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:colorPrimary">#4338ca</item>
<item name="android:colorPrimaryDark">#1e1b4b</item>
<item name="android:colorAccent">#4f46e5</item>
<item name="android:windowBackground">#f1f5f9</item>
<item name="android:statusBarColor">#1e1b4b</item>
</style>
</resources>

View File

@@ -1,537 +0,0 @@
package com.pca.phobos.vpn;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.net.VpnService;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Base64;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.backend.Statistics;
import com.wireguard.android.backend.Tunnel;
import com.wireguard.config.Config;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* PCA Phobos VPN — client: imports a phobos:// link from the panel, runs the
* bundled wg-obfuscator + userspace WireGuard, split tunnelling by apps.
* The app itself is always outside the tunnel so the obfuscator reaches the server directly.
*/
public class MainActivity extends Activity {
static final int LPORT = 13255;
static final String MODE_ALL = "all", MODE_ONLY = "only", MODE_EXCEPT = "except";
static GoBackend backend;
static Process obfuscator, relay;
static final int TCP_LPORT = 13256;
static volatile String transport = ""; // "UDP" | "TCP" while up
static final Tunnel TUNNEL = new Tunnel() {
public String getName() { return "phobos"; }
public void onStateChange(Tunnel.State s) { }
};
private final ExecutorService bg = Executors.newSingleThreadExecutor();
private final Handler ui = new Handler(Looper.getMainLooper());
private SharedPreferences prefs;
private boolean dark;
private TextView status, traffic;
private Button power;
private long lastRx, lastTx, lastT;
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
prefs = getSharedPreferences("vpn", MODE_PRIVATE);
dark = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
if (backend == null) backend = new GoBackend(getApplicationContext());
handleIntent(getIntent());
render();
}
@Override
protected void onNewIntent(Intent i) {
super.onNewIntent(i);
handleIntent(i);
render();
}
private void handleIntent(Intent i) {
if (i != null && i.getData() != null && "phobos".equals(i.getData().getScheme())) {
importLink(i.getDataString());
}
}
// ── colors / widgets ──────────────────────────────────────────────────
private int bg() { return dark ? 0xFF0F172A : 0xFFF1F5F9; }
private int card() { return dark ? 0xFF1E293B : 0xFFFFFFFF; }
private int text() { return dark ? 0xFFE2E8F0 : 0xFF0F172A; }
private int muted() { return dark ? 0xFF94A3B8 : 0xFF64748B; }
private int dp(int v) { return Math.round(v * getResources().getDisplayMetrics().density); }
private GradientDrawable round(int color, int r) {
GradientDrawable g = new GradientDrawable();
g.setColor(color);
g.setCornerRadius(dp(r));
return g;
}
private Button button(String label, int color, View.OnClickListener l) {
Button b = new Button(this);
b.setText(label);
b.setAllCaps(false);
b.setTextColor(Color.WHITE);
b.setBackground(round(color, 10));
b.setOnClickListener(l);
return b;
}
private TextView tv(String s, int color, int size, boolean bold) {
TextView t = new TextView(this);
t.setText(s);
t.setTextColor(color);
t.setTextSize(size);
if (bold) t.setTypeface(Typeface.DEFAULT_BOLD);
return t;
}
private LinearLayout.LayoutParams mw(int top) {
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.topMargin = dp(top);
return lp;
}
// ── profile ───────────────────────────────────────────────────────────
/** phobos://<urlsafe-base64 of conf>#name (format produced by the PCA Phobos panel). */
private void importLink(String link) {
try {
String s = link.trim();
String name = "Phobos";
int hash = s.indexOf('#');
if (hash > 0) { name = Uri.decode(s.substring(hash + 1)); s = s.substring(0, hash); }
s = s.replaceFirst("^phobos://", "").trim();
String conf = new String(Base64.decode(s, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING), StandardCharsets.UTF_8);
if (!conf.contains("[Interface]") || !conf.contains("[instance]")) throw new IllegalArgumentException("not a Phobos config");
Config.parse(new BufferedReader(new StringReader(wgPart(conf, MODE_ALL, "")))); // validate
prefs.edit().putString("conf", conf).putString("name", name).apply();
toast("Профиль «" + name + "» импортирован");
} catch (Exception e) {
toast("Не удалось импортировать: " + e.getMessage());
}
}
/** Split payload into sections: header -> lines (drop "= none" placeholders). */
private static List<String> section(String conf, String header) {
List<String> out = new ArrayList<>();
boolean in = false;
for (String raw : conf.split("\n")) {
String ln = raw.trim();
if (ln.startsWith("[") && ln.endsWith("]")) { in = ln.equalsIgnoreCase(header); continue; }
if (!in || ln.isEmpty() || !ln.contains("=")) continue;
if (ln.substring(ln.indexOf('=') + 1).trim().equalsIgnoreCase("none")) continue;
out.add(ln);
}
return out;
}
private static String key(String line) { return line.substring(0, line.indexOf('=')).trim().toLowerCase(); }
private static String val(String line) { return line.substring(line.indexOf('=') + 1).trim(); }
String wgPart(String conf, String mode, String apps) {
StringBuilder sb = new StringBuilder("[Interface]\n");
for (String ln : section(conf, "[Interface]")) {
String k = key(ln);
if (k.equals("excludedapplications") || k.equals("includedapplications")) continue;
sb.append(ln).append('\n');
}
List<String> sel = new ArrayList<>();
for (String a : apps.split(",")) if (!a.trim().isEmpty()) sel.add(a.trim());
if (MODE_ONLY.equals(mode) && !sel.isEmpty()) {
sel.remove(getPackageNameSafe());
sb.append("IncludedApplications = ").append(String.join(", ", sel)).append('\n');
} else {
Set<String> ex = new LinkedHashSet<>();
ex.add(getPackageNameSafe()); // obfuscator must bypass the tunnel
if (MODE_EXCEPT.equals(mode)) ex.addAll(sel);
sb.append("ExcludedApplications = ").append(String.join(", ", ex)).append('\n');
}
sb.append("\n[Peer]\n");
String ruBypass = prefs != null && prefs.getBoolean("ru_direct", true) ? ruBypassAllowedIps() : null;
for (String ln : section(conf, "[Peer]")) {
if (key(ln).equals("endpoint")) ln = "Endpoint = 127.0.0.1:" + LPORT;
if (key(ln).equals("allowedips") && ruBypass != null) ln = "AllowedIPs = " + ruBypass;
sb.append(ln).append('\n');
}
return sb.toString();
}
private String ruCache;
/** Internet minus Russian networks (assets/ru_bypass.txt, generated from ipdeny). */
private String ruBypassAllowedIps() {
if (ruCache != null) return ruCache;
try (BufferedReader r = new BufferedReader(new java.io.InputStreamReader(
getAssets().open("ru_bypass.txt"), StandardCharsets.UTF_8))) {
for (String ln; (ln = r.readLine()) != null; ) {
if (!ln.startsWith("#") && ln.contains("/")) { ruCache = ln.trim(); break; }
}
} catch (Exception e) { log("ru_bypass: " + e); }
return ruCache;
}
private static String getPackageNameSafe() { return "com.pca.phobos.vpn"; }
private String obfPart(String conf, boolean viaTcp) {
StringBuilder sb = new StringBuilder("[instance]\nsource-if = 127.0.0.1\nsource-lport = " + LPORT + "\n");
for (String ln : section(conf, "[instance]")) {
String k = key(ln);
if (k.equals("source-if") || k.equals("source-lport") || k.equals("verbose")) continue;
if (viaTcp && k.equals("target")) ln = "target = 127.0.0.1:" + TCP_LPORT;
sb.append(ln).append('\n');
}
sb.append("verbose = INFO\n");
return sb.toString();
}
/** "# pca-tcp = 443" written by the panel when the server has the TCP fallback. */
private static String tcpPort(String conf) {
for (String ln : conf.split("\n")) {
String t = ln.trim();
if (t.startsWith("#") && t.contains("pca-tcp") && t.contains("=")) return val(t);
}
return "";
}
private String target(String conf) {
for (String ln : section(conf, "[instance]")) if (key(ln).equals("target")) return val(ln);
return "?";
}
// ── connect / disconnect ──────────────────────────────────────────────
private boolean isUp() {
try { return backend.getState(TUNNEL) == Tunnel.State.UP; } catch (Exception e) { return false; }
}
private void toggle() {
if (isUp()) { bg.execute(this::down); return; }
if (prefs.getString("conf", "").isEmpty()) { toast("Сначала импортируйте ссылку phobos:// из панели"); return; }
Intent prep = VpnService.prepare(this);
if (prep != null) startActivityForResult(prep, 7);
else bg.execute(this::up);
}
@Override
protected void onActivityResult(int req, int res, Intent data) {
if (req == 7 && res == RESULT_OK) bg.execute(this::up);
else if (req == 7) toast("Без разрешения на VPN подключиться нельзя");
}
/** Start (or restart) obfuscator; via TCP it dials the local udptcp relay. */
private void startTransport(String conf, boolean viaTcp) throws Exception {
stopObfuscator();
String lib = getApplicationInfo().nativeLibraryDir;
if (viaTcp) {
String host = target(conf).replaceAll(":\\d+$", "");
relay = new ProcessBuilder(lib + "/libudptcp.so", "-c", "127.0.0.1:" + TCP_LPORT, host + ":" + tcpPort(conf))
.redirectErrorStream(true).redirectOutput(new File(getFilesDir(), "tcp.log")).start();
}
File cfg = new File(getFilesDir(), "obf.conf");
try (FileOutputStream o = new FileOutputStream(cfg)) { o.write(obfPart(conf, viaTcp).getBytes(StandardCharsets.UTF_8)); }
obfuscator = new ProcessBuilder(lib + "/libwgobfuscator.so", "-c", cfg.getAbsolutePath())
.redirectErrorStream(true).redirectOutput(new File(getFilesDir(), "obf.log")).start();
Thread.sleep(600);
if (!obfuscator.isAlive()) throw new IllegalStateException("обфускатор завершился, код " + obfuscator.exitValue() + " — см. «Журнал»");
if (relay != null && !relay.isAlive()) throw new IllegalStateException("TCP-канал не запустился — см. «Журнал»");
transport = viaTcp ? "TCP" : "UDP";
log("transport " + transport + " -> " + target(conf) + (viaTcp ? " tcp:" + tcpPort(conf) : ""));
}
private void up() {
try {
String conf = prefs.getString("conf", "");
String mode = prefs.getString("transport", "auto");
boolean tcpAvail = !tcpPort(conf).isEmpty();
if ("tcp".equals(mode) && !tcpAvail) throw new IllegalStateException("на сервере нет TCP-канала — обновите панель и заново импортируйте ссылку");
startTransport(conf, "tcp".equals(mode));
Config c = Config.parse(new BufferedReader(new StringReader(
wgPart(conf, prefs.getString("mode", MODE_ALL), prefs.getString("apps", "")))));
backend.setState(TUNNEL, Tunnel.State.UP, c);
log("up ok, mode " + prefs.getString("mode", MODE_ALL) + ", transport " + mode);
post("Подключено (" + transport + ")");
if ("auto".equals(mode) && tcpAvail) watchUdp(conf);
} catch (Throwable e) {
stopObfuscator();
log("up failed: " + e);
post("Ошибка: " + e.getMessage());
}
}
/** Auto: no bytes back over UDP within 15 s -> UDP is cut, move the obfuscator to TCP. */
private void watchUdp(String conf) {
new Thread(() -> {
try {
Thread.sleep(15000);
if (!isUp() || !"UDP".equals(transport)) return;
if (backend.getStatistics(TUNNEL).totalRx() > 0) return;
log("UDP: no answer in 15 s -> switching to TCP");
startTransport(conf, true);
post("UDP не отвечает — переключился на TCP");
} catch (Throwable e) { log("watchUdp: " + e); }
}).start();
}
private void down() {
try { backend.setState(TUNNEL, Tunnel.State.DOWN, null); } catch (Throwable ignored) { }
stopObfuscator();
transport = "";
post("Отключено");
}
private static void stopObfuscator() {
if (obfuscator != null) { obfuscator.destroy(); obfuscator = null; }
if (relay != null) { relay.destroy(); relay = null; }
}
private void post(String msg) { ui.post(() -> { toast(msg); refresh(); }); }
// ── UI ────────────────────────────────────────────────────────────────
private void render() {
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(bg());
LinearLayout h = new LinearLayout(this);
h.setBackgroundColor(0xFF1E1B4B);
h.setPadding(dp(16), dp(14), dp(16), dp(14));
h.addView(tv("🛡 PCA Phobos VPN", 0xFFC7D2FE, 18, true));
root.addView(h);
ScrollView sv = new ScrollView(this);
LinearLayout box = new LinearLayout(this);
box.setOrientation(LinearLayout.VERTICAL);
box.setPadding(dp(16), dp(16), dp(16), dp(24));
sv.addView(box);
String conf = prefs.getString("conf", "");
LinearLayout prof = new LinearLayout(this);
prof.setOrientation(LinearLayout.VERTICAL);
prof.setBackground(round(card(), 14));
prof.setPadding(dp(16), dp(14), dp(16), dp(14));
if (conf.isEmpty()) {
prof.addView(tv("Профиль не импортирован", text(), 16, true));
prof.addView(tv("В панели PCA Phobos откройте клиента → 📱 Android → нажмите ссылку phobos:// "
+ "на этом устройстве, или скопируйте её и нажмите «Импорт».", muted(), 14, false));
} else {
prof.addView(tv(prefs.getString("name", "Phobos"), text(), 18, true));
prof.addView(tv("Сервер: " + target(conf), muted(), 14, false));
}
box.addView(prof, mw(0));
power = new Button(this);
power.setAllCaps(false);
power.setTextColor(Color.WHITE);
power.setTextSize(20);
power.setTypeface(Typeface.DEFAULT_BOLD);
power.setOnClickListener(v -> toggle());
LinearLayout.LayoutParams plp = new LinearLayout.LayoutParams(dp(180), dp(180));
plp.gravity = Gravity.CENTER_HORIZONTAL;
plp.topMargin = dp(24);
box.addView(power, plp);
status = tv("", text(), 16, true);
status.setGravity(Gravity.CENTER);
box.addView(status, mw(12));
traffic = tv("", muted(), 14, false);
traffic.setGravity(Gravity.CENTER);
box.addView(traffic, mw(2));
// split tunnelling
LinearLayout split = new LinearLayout(this);
split.setOrientation(LinearLayout.VERTICAL);
split.setBackground(round(card(), 14));
split.setPadding(dp(16), dp(14), dp(16), dp(14));
split.addView(tv("Раздельный трафик", text(), 16, true));
RadioGroup rg = new RadioGroup(this);
String mode = prefs.getString("mode", MODE_ALL);
String[][] modes = {{MODE_ALL, "Весь трафик через VPN"}, {MODE_ONLY, "Только выбранные приложения"},
{MODE_EXCEPT, "Все, кроме выбранных приложений"}};
for (String[] m : modes) {
RadioButton r = new RadioButton(this);
r.setText(m[1]);
r.setTextColor(text());
r.setId(View.generateViewId());
r.setChecked(m[0].equals(mode));
r.setOnClickListener(v -> { prefs.edit().putString("mode", m[0]).apply(); applyIfUp(); refresh(); });
rg.addView(r);
}
split.addView(rg);
android.widget.CheckBox ru = new android.widget.CheckBox(this);
ru.setText("Российские сайты — напрямую, без VPN");
ru.setTextColor(text());
ru.setChecked(prefs.getBoolean("ru_direct", true));
ru.setOnCheckedChangeListener((b, on) -> { prefs.edit().putBoolean("ru_direct", on).apply(); applyIfUp(); });
split.addView(ru, mw(4));
split.addView(tv("Канал до сервера", text(), 14, true), mw(8));
RadioGroup tg = new RadioGroup(this);
tg.setOrientation(RadioGroup.HORIZONTAL);
String tmode = prefs.getString("transport", "auto");
for (String[] m : new String[][]{{"auto", "Авто"}, {"udp", "UDP"}, {"tcp", "TCP"}}) {
RadioButton r = new RadioButton(this);
r.setText(m[1]);
r.setTextColor(text());
r.setId(View.generateViewId());
r.setChecked(m[0].equals(tmode));
r.setOnClickListener(v -> { prefs.edit().putString("transport", m[0]).apply(); applyIfUp(); });
tg.addView(r);
}
split.addView(tg);
int n = prefs.getString("apps", "").isEmpty() ? 0 : prefs.getString("apps", "").split(",").length;
split.addView(button("Выбрать приложения (" + n + ")", 0xFF475569, v -> pickApps()), mw(8));
split.addView(tv("Изменения применяются сразу. Само приложение всегда идёт напрямую — так обфускатор держит связь с сервером.",
muted(), 12, false), mw(6));
box.addView(split, mw(20));
box.addView(button("Импорт ссылки phobos://", 0xFF4F46E5, v -> importDialog()), mw(16));
box.addView(button("Проверить скорость", 0xFF0EA5E9, v -> speedtest()), mw(8));
box.addView(button("Журнал", 0xFF475569, v -> showLog()), mw(8));
root.addView(sv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
setContentView(root);
refresh();
ui.removeCallbacksAndMessages(null);
ui.postDelayed(new Runnable() { public void run() { refresh(); ui.postDelayed(this, 2000); } }, 2000);
}
private void refresh() {
if (power == null) return;
boolean up = isUp();
power.setText(up ? "Отключить" : "Подключить");
power.setBackground(round(up ? 0xFF16A34A : 0xFF4F46E5, 90));
status.setText(up ? "🟢 VPN включён" + (transport.isEmpty() ? "" : " · " + transport) : "⚪ VPN выключен");
if (up) {
try {
Statistics st = backend.getStatistics(TUNNEL);
long rx = st.totalRx(), tx = st.totalTx(), now = System.currentTimeMillis();
String speed = "";
if (lastT > 0 && now > lastT) {
double s = (now - lastT) / 1000.0;
speed = String.format(" · ↓ %.1f / ↑ %.1f Мбит/с", (rx - lastRx) * 8 / s / 1e6, (tx - lastTx) * 8 / s / 1e6);
}
lastRx = rx; lastTx = tx; lastT = now;
traffic.setText(String.format("Получено %.1f МБ · Отправлено %.1f МБ", rx / 1048576.0, tx / 1048576.0) + speed);
} catch (Exception e) { traffic.setText(""); }
} else { traffic.setText(""); lastT = 0; }
}
private void applyIfUp() { if (isUp()) bg.execute(() -> { down(); up(); }); }
private void importDialog() {
final EditText e = new EditText(this);
e.setHint("phobos://…");
ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
if (cm != null && cm.getPrimaryClip() != null && cm.getPrimaryClip().getItemCount() > 0) {
CharSequence c = cm.getPrimaryClip().getItemAt(0).getText();
if (c != null && c.toString().trim().startsWith("phobos://")) e.setText(c);
}
new AlertDialog.Builder(this).setTitle("Ссылка из панели").setView(e)
.setPositiveButton("Импорт", (d, w) -> { importLink(e.getText().toString()); applyIfUp(); render(); })
.setNegativeButton("Отмена", null).show();
}
private void pickApps() {
PackageManager pm = getPackageManager();
Intent main = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> ris = pm.queryIntentActivities(main, 0);
List<String[]> apps = new ArrayList<>();
Set<String> seen = new LinkedHashSet<>();
for (ResolveInfo ri : ris) {
String p = ri.activityInfo.packageName;
if (p.equals(getPackageName()) || !seen.add(p)) continue;
apps.add(new String[]{String.valueOf(ri.loadLabel(pm)), p});
}
Collections.sort(apps, (a, b) -> a[0].compareToIgnoreCase(b[0]));
final Set<String> chosen = new LinkedHashSet<>(Arrays.asList(prefs.getString("apps", "").split(",")));
chosen.remove("");
String[] labels = new String[apps.size()];
boolean[] checked = new boolean[apps.size()];
for (int i = 0; i < apps.size(); i++) { labels[i] = apps.get(i)[0]; checked[i] = chosen.contains(apps.get(i)[1]); }
new AlertDialog.Builder(this).setTitle("Приложения")
.setMultiChoiceItems(labels, checked, (d, i, on) -> { if (on) chosen.add(apps.get(i)[1]); else chosen.remove(apps.get(i)[1]); })
.setPositiveButton("Готово", (d, w) -> { prefs.edit().putString("apps", String.join(",", chosen)).apply(); applyIfUp(); render(); })
.setNegativeButton("Отмена", null).show();
}
/** Real speed through the tunnel: the browser is inside the VPN (unless excluded by the split). */
private void speedtest() {
if (!isUp()) { toast("Сначала подключитесь"); return; }
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://speed.cloudflare.com/")));
}
private void log(String s) {
try (FileOutputStream o = new FileOutputStream(new File(getFilesDir(), "app.log"), true)) {
o.write((new java.util.Date() + " " + s + "\n").getBytes(StandardCharsets.UTF_8));
} catch (Exception ignored) { }
}
private static String tail(File f, int max) {
try {
java.io.ByteArrayOutputStream o = new java.io.ByteArrayOutputStream();
try (java.io.FileInputStream in = new java.io.FileInputStream(f)) {
byte[] buf = new byte[8192];
for (int n; (n = in.read(buf)) > 0; ) o.write(buf, 0, n);
}
String t = new String(o.toByteArray(), StandardCharsets.UTF_8);
return t.length() > max ? t.substring(t.length() - max) : t;
} catch (Exception e) { return ""; }
}
private void showLog() {
String t = "— приложение —\n" + tail(new File(getFilesDir(), "app.log"), 3000)
+ "\n— обфускатор —\n" + tail(new File(getFilesDir(), "obf.log"), 3000)
+ "\n— TCP-канал —\n" + tail(new File(getFilesDir(), "tcp.log"), 1500)
+ "\nобфускатор запущен: " + (obfuscator != null && obfuscator.isAlive());
TextView v = tv(t, text(), 12, false);
v.setTextIsSelectable(true);
v.setPadding(dp(16), dp(8), dp(16), dp(8));
ScrollView sv = new ScrollView(this);
sv.addView(v);
new AlertDialog.Builder(this).setTitle("Журнал").setView(sv).setPositiveButton("OK", null).show();
}
private void toast(String s) { Toast.makeText(this, s, Toast.LENGTH_LONG).show(); }
}

1
android/.gitignore vendored
View File

@@ -1 +0,0 @@
build/

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pca.phobos"
android:versionCode="140"
android:versionName="1.4.0">
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="35" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="PCA Phobos"
android:icon="@drawable/ic_launcher"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:allowBackup="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -1,4 +0,0 @@
# PCA Phobos — админ-приложение (Android)
Список ваших панелей Phobos, каждая открывается в приложении (WebView), тема по системе,
ссылки `phobos://` передаются в VPN-клиент. Сборка: `KS_PASS=… ./build.sh` → `build/PCA-Phobos.apk`.

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env bash
# Build PCA Phobos Android app without Gradle: aapt2 -> javac -> d8 -> zipalign -> apksigner.
# ANDROID_HOME=~/Library/Android/sdk KS=~/.android/pca-phobos.jks KS_PASS=… ./build.sh
# Output: build/PCA-Phobos.apk. The keystore is NOT in git (keep the same one, or updates won't install).
set -euo pipefail
cd "$(dirname "$0")"
SDK="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
BT="$SDK/build-tools/$(ls "$SDK/build-tools" | sort -V | tail -1)"
JAR="$SDK/platforms/$(ls "$SDK/platforms" | sort -V | tail -1)/android.jar"
KS="${KS:-$HOME/.android/pca-phobos.jks}"
KS_PASS="${KS_PASS:?set KS_PASS (keystore password)}"
rm -rf build && mkdir -p build/res build/classes build/dex
"$BT/aapt2" compile --dir res -o build/res/
"$BT/aapt2" link -o build/base.apk -I "$JAR" --manifest AndroidManifest.xml \
--java build/gen build/res/*.flat --min-sdk-version 23 --target-sdk-version 35
javac -source 11 -target 11 -nowarn -classpath "$JAR" -d build/classes \
$(find src build/gen -name '*.java') 2>&1 | grep -v "bootstrap classpath" || true
"$BT/d8" --release --lib "$JAR" --min-api 23 --output build/dex $(find build/classes -name '*.class')
cp build/base.apk build/unsigned.apk
(cd build/dex && zip -q ../unsigned.apk classes.dex)
"$BT/zipalign" -f 4 build/unsigned.apk build/aligned.apk
[ -f "$KS" ] || keytool -genkeypair -keystore "$KS" -storepass "$KS_PASS" -keypass "$KS_PASS" \
-alias pca -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=PCA Phobos, O=PCAdministration" >/dev/null
"$BT/apksigner" sign --ks "$KS" --ks-pass "pass:$KS_PASS" --out build/PCA-Phobos.apk build/aligned.apk
"$BT/apksigner" verify build/PCA-Phobos.apk && ls -la build/PCA-Phobos.apk

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportHeight="108">
<path android:fillColor="#1e1b4b" android:pathData="M0,0h108v108h-108z" />
<path android:fillColor="#4f46e5"
android:pathData="M54,18 L82,28 L82,52 C82,70 70,84 54,90 C38,84 26,70 26,52 L26,28 Z" />
<path android:fillColor="#c7d2fe"
android:pathData="M54,30 L72,36.5 L72,52 C72,64 64,73.5 54,78 C44,73.5 36,64 36,52 L36,36.5 Z" />
<path android:fillColor="#1e1b4b"
android:pathData="M47,46 h14 v5 h-4.5 v16 h-5 v-16 h-4.5 z" />
</vector>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:Theme.Material.NoActionBar">
<item name="android:colorPrimary">#a5b4fc</item>
<item name="android:colorPrimaryDark">#1e1b4b</item>
<item name="android:colorAccent">#818cf8</item>
<item name="android:windowBackground">#0f172a</item>
<item name="android:statusBarColor">#1e1b4b</item>
</style>
</resources>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:colorPrimary">#4338ca</item>
<item name="android:colorPrimaryDark">#1e1b4b</item>
<item name="android:colorAccent">#4f46e5</item>
<item name="android:windowBackground">#f1f5f9</item>
<item name="android:statusBarColor">#1e1b4b</item>
</style>
</resources>

View File

@@ -1,367 +0,0 @@
package com.pca.phobos;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.InputType;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* PCA Phobos — admin app: a list of your Phobos panels, each opens in a WebView.
* phobos:// links are handed to PhobosWG, downloads go to the system browser.
*/
public class MainActivity extends Activity {
private SharedPreferences prefs;
private WebView web;
private ProgressBar progress;
private TextView title;
private boolean dark;
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
prefs = getSharedPreferences("phobos", MODE_PRIVATE);
dark = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
== Configuration.UI_MODE_NIGHT_YES;
JSONArray list = panels();
String last = prefs.getString("last", "");
if (!last.isEmpty() && b == null) openPanel(last, nameFor(last));
else if (list.length() == 0) showEditor(null, -1);
else showList();
}
// ── colors ────────────────────────────────────────────────────────────
private int bg() { return dark ? 0xFF0F172A : 0xFFF1F5F9; }
private int card() { return dark ? 0xFF1E293B : 0xFFFFFFFF; }
private int text() { return dark ? 0xFFE2E8F0 : 0xFF0F172A; }
private int muted() { return dark ? 0xFF94A3B8 : 0xFF64748B; }
private static final int ACCENT = 0xFF4F46E5;
private static final int HEADER = 0xFF1E1B4B;
private int dp(int v) { return Math.round(v * getResources().getDisplayMetrics().density); }
private GradientDrawable round(int color, int radius) {
GradientDrawable g = new GradientDrawable();
g.setColor(color);
g.setCornerRadius(dp(radius));
return g;
}
private Button button(String label, int color, View.OnClickListener l) {
Button btn = new Button(this);
btn.setText(label);
btn.setAllCaps(false);
btn.setTextColor(Color.WHITE);
btn.setBackground(round(color, 10));
btn.setOnClickListener(l);
return btn;
}
private LinearLayout header(String text, View right) {
LinearLayout h = new LinearLayout(this);
h.setBackgroundColor(HEADER);
h.setGravity(Gravity.CENTER_VERTICAL);
h.setPadding(dp(16), dp(12), dp(8), dp(12));
TextView t = new TextView(this);
t.setText("🛡 " + text);
t.setTextColor(0xFFC7D2FE);
t.setTextSize(18);
t.setTypeface(Typeface.DEFAULT_BOLD);
t.setSingleLine(true);
h.addView(t, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
if (right != null) h.addView(right);
title = t;
return h;
}
// ── storage ───────────────────────────────────────────────────────────
private JSONArray panels() {
try { return new JSONArray(prefs.getString("panels", "[]")); }
catch (Exception e) { return new JSONArray(); }
}
private void savePanels(JSONArray a) { prefs.edit().putString("panels", a.toString()).apply(); }
private String nameFor(String url) {
JSONArray a = panels();
for (int i = 0; i < a.length(); i++) {
JSONObject o = a.optJSONObject(i);
if (o != null && url.equals(o.optString("url"))) return o.optString("name", url);
}
return url;
}
static String normalize(String raw) {
String u = raw.trim();
if (u.isEmpty()) return "";
if (!u.startsWith("http://") && !u.startsWith("https://")) u = "http://" + u;
while (u.endsWith("/")) u = u.substring(0, u.length() - 1);
return u;
}
// ── screens ───────────────────────────────────────────────────────────
private void showList() {
web = null;
prefs.edit().remove("last").apply();
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(bg());
root.addView(header("PCA Phobos", null));
ScrollView sv = new ScrollView(this);
LinearLayout box = new LinearLayout(this);
box.setOrientation(LinearLayout.VERTICAL);
box.setPadding(dp(16), dp(16), dp(16), dp(16));
sv.addView(box);
TextView hint = new TextView(this);
hint.setText("Ваши серверы Phobos. Нажмите — откроется панель. Долгое нажатие — изменить.");
hint.setTextColor(muted());
hint.setPadding(0, 0, 0, dp(12));
box.addView(hint);
final JSONArray a = panels();
for (int i = 0; i < a.length(); i++) {
final int idx = i;
final JSONObject o = a.optJSONObject(i);
if (o == null) continue;
LinearLayout c = new LinearLayout(this);
c.setOrientation(LinearLayout.VERTICAL);
c.setBackground(round(card(), 12));
c.setPadding(dp(16), dp(14), dp(16), dp(14));
TextView n = new TextView(this);
n.setText(o.optString("name"));
n.setTextColor(text());
n.setTextSize(17);
n.setTypeface(Typeface.DEFAULT_BOLD);
TextView u = new TextView(this);
u.setText(o.optString("url"));
u.setTextColor(muted());
c.addView(n);
c.addView(u);
c.setOnClickListener(v -> openPanel(o.optString("url"), o.optString("name")));
c.setOnLongClickListener(v -> { showEditor(o, idx); return true; });
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.bottomMargin = dp(10);
box.addView(c, lp);
}
box.addView(button("+ Добавить сервер", ACCENT, v -> showEditor(null, -1)));
root.addView(sv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
setContentView(root);
}
private void showEditor(final JSONObject existing, final int idx) {
web = null;
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(bg());
root.addView(header(existing == null ? "Новый сервер" : "Изменить сервер", null));
LinearLayout box = new LinearLayout(this);
box.setOrientation(LinearLayout.VERTICAL);
box.setPadding(dp(16), dp(16), dp(16), dp(16));
final EditText name = field("Название (например: Амстердам)", existing == null ? "" : existing.optString("name"));
final EditText url = field("Адрес панели: http://IP:порт или https://домен", existing == null ? "" : existing.optString("url"));
url.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
box.addView(label("Название"));
box.addView(name);
box.addView(label("Адрес панели"));
box.addView(url);
TextView tip = new TextView(this);
tip.setText("Адрес и порт панели печатает установщик в конце установки (или: cat /opt/phobos-panel/.port). "
+ "Если задан домен в «Настройках» панели — используйте https://домен.");
tip.setTextColor(muted());
tip.setPadding(0, dp(4), 0, dp(16));
box.addView(tip);
box.addView(button("Сохранить и открыть", ACCENT, v -> {
String u = normalize(url.getText().toString());
if (u.isEmpty()) { toast("Введите адрес панели"); return; }
String n = name.getText().toString().trim();
if (n.isEmpty()) n = Uri.parse(u).getHost();
try {
JSONArray a = panels();
JSONObject o = new JSONObject().put("name", n).put("url", u);
if (idx >= 0) a.put(idx, o); else a.put(o);
savePanels(a);
} catch (Exception ignored) { }
openPanel(u, n);
}));
if (existing != null) {
Button del = button("Удалить из списка", 0xFFDC2626, v -> {
JSONArray a = panels(), out = new JSONArray();
for (int i = 0; i < a.length(); i++) if (i != idx) out.put(a.opt(i));
savePanels(out);
showList();
});
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.topMargin = dp(10);
box.addView(del, lp);
}
if (panels().length() > 0) {
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.topMargin = dp(10);
box.addView(button("Назад", 0xFF475569, v -> showList()), lp);
}
root.addView(box);
setContentView(root);
}
private TextView label(String s) {
TextView t = new TextView(this);
t.setText(s);
t.setTextColor(muted());
t.setPadding(0, dp(8), 0, dp(4));
return t;
}
private EditText field(String hint, String value) {
EditText e = new EditText(this);
e.setHint(hint);
e.setText(value);
e.setSingleLine(true);
e.setTextColor(text());
e.setHintTextColor(muted());
e.setBackground(round(card(), 10));
e.setPadding(dp(12), dp(12), dp(12), dp(12));
return e;
}
private void openPanel(final String url, String name) {
prefs.edit().putString("last", url).apply();
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(bg());
LinearLayout actions = new LinearLayout(this);
TextView reload = headerAction("⟳", v -> { if (web != null) web.reload(); });
TextView servers = headerAction("☰", v -> showList());
actions.addView(reload);
actions.addView(servers);
root.addView(header(name, actions));
progress = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal);
progress.setMax(100);
root.addView(progress, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(3)));
web = new WebView(this);
WebSettings s = web.getSettings();
s.setJavaScriptEnabled(true);
s.setDomStorageEnabled(true);
s.setLoadWithOverviewMode(true);
s.setUseWideViewPort(true);
s.setBuiltInZoomControls(true);
s.setDisplayZoomControls(false);
s.setUserAgentString(s.getUserAgentString() + " PCAPhobosApp/1.4.0");
CookieManager.getInstance().setAcceptCookie(true);
web.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest req) {
Uri u = req.getUrl();
String scheme = u.getScheme() == null ? "" : u.getScheme();
if (scheme.equals("http") || scheme.equals("https")) {
Uri base = Uri.parse(url);
// other sites (GitHub, Boosty, Telegram…) -> system browser / apps
if (u.getHost() != null && !u.getHost().equals(base.getHost())) { external(u); return true; }
return false;
}
external(u); // phobos:// -> PhobosWG, tg:// -> Telegram
return true;
}
@Override
public void onPageFinished(WebView view, String u) {
progress.setVisibility(View.GONE);
CookieManager.getInstance().flush();
}
@Override
public void onReceivedError(WebView view, int code, String desc, String failing) {
if (failing != null && failing.startsWith(url)) {
view.loadDataWithBaseURL(null, errorPage(url, desc), "text/html", "utf-8", null);
}
}
});
web.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int p) {
progress.setVisibility(p < 100 ? View.VISIBLE : View.GONE);
progress.setProgress(p);
}
});
web.setDownloadListener((dlUrl, ua, cd, mime, len) -> external(Uri.parse(dlUrl)));
root.addView(web, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1));
setContentView(root);
web.loadUrl(url + "/");
}
private TextView headerAction(String s, View.OnClickListener l) {
TextView t = new TextView(this);
t.setText(s);
t.setTextColor(0xFFC7D2FE);
t.setTextSize(22);
t.setPadding(dp(14), 0, dp(14), 0);
t.setOnClickListener(l);
return t;
}
private String errorPage(String url, String desc) {
String bgc = dark ? "#0f172a" : "#f1f5f9", fg = dark ? "#e2e8f0" : "#0f172a";
return "<html><body style='font-family:sans-serif;background:" + bgc + ";color:" + fg
+ ";padding:24px;text-align:center'><h2>Панель недоступна</h2><p>" + url + "</p><p style='color:#64748b'>"
+ desc + "</p><p>Проверьте адрес и порт, интернет и что сервер включён. Кнопка ⟳ — повторить.</p></body></html>";
}
private void external(Uri u) {
try {
startActivity(new Intent(Intent.ACTION_VIEW, u));
} catch (ActivityNotFoundException e) {
if ("phobos".equals(u.getScheme())) {
new AlertDialog.Builder(this)
.setTitle("Нужно приложение PhobosWG")
.setMessage("Ссылки phobos:// открывает приложение PhobosWG. Скачайте его на странице Android-клиента в панели (кнопка «Скачать приложение PhobosWG»).")
.setPositiveButton("OK", null).show();
} else {
toast("Нет приложения для " + u.getScheme());
}
}
}
private void toast(String s) { Toast.makeText(this, s, Toast.LENGTH_SHORT).show(); }
@Override
public void onBackPressed() {
if (web != null && web.canGoBack()) { web.goBack(); return; }
if (web != null && panels().length() > 0) { showList(); return; }
super.onBackPressed();
}
}

4505
app.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
# Phobos Failover Configuration
# Format: SERVER_N=host:port1,port2,port3
# KEY_N=obfuscation_key_for_server_N
# Primary server (main, with web panel)
SERVER_1=144.124.252.104:2083,5443,993
KEY_1=Fc3X8GbVQD2FuHPbOjKiaP83nWSwNojJ
# Secondary server (backup, VPN only)
# SERVER_2=backup.example.com:51821,51822
# KEY_2=backup_obfuscation_key_here
# Emergency server (last resort)
# SERVER_3=emergency.example.com:43210
# KEY_3=emergency_key_here

View File

@@ -1,247 +0,0 @@
#!/bin/sh
# ============================================================
# Phobos Health Monitor — Auto-heal + Failover for routers
# Runs via cron every 60 seconds on Keenetic/Entware
#
# Install: crontab -e → */1 * * * * /opt/etc/Phobos/phobos-health.sh
# ============================================================
PHOBOS_DIR="/opt/etc/Phobos"
CONF="$PHOBOS_DIR/failover.conf"
STATE="$PHOBOS_DIR/state"
LOG="$PHOBOS_DIR/health.log"
WG_CONF="$PHOBOS_DIR/wg0.conf"
OBF_CONF="$PHOBOS_DIR/wg-obfuscator.conf"
LOCKFILE="/tmp/phobos-health.lock"
MAX_LOG_LINES=200
HANDSHAKE_WARN=180
HANDSHAKE_RESTART_OBF=300
HANDSHAKE_RESTART_WG=600
HANDSHAKE_FAILOVER=900
log() {
echo "$(date '+%H:%M:%S') $1" >> "$LOG"
# Trim log
if [ "$(wc -l < "$LOG" 2>/dev/null)" -gt "$MAX_LOG_LINES" ]; then
tail -n 100 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
fi
}
# Prevent concurrent runs
if [ -f "$LOCKFILE" ]; then
pid=$(cat "$LOCKFILE" 2>/dev/null)
if kill -0 "$pid" 2>/dev/null; then
exit 0
fi
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
# Create state dir
mkdir -p "$STATE"
# ── Read failover config ──
if [ ! -f "$CONF" ]; then
log "ERROR: no failover.conf"
exit 1
fi
# Parse servers: SERVER_1=ip:port1,port2,port3
# Parse current state
CURRENT_SERVER=$(cat "$STATE/current_server" 2>/dev/null || echo "1")
CURRENT_PORT_IDX=$(cat "$STATE/current_port_idx" 2>/dev/null || echo "0")
RESTART_COUNT=$(cat "$STATE/restart_count" 2>/dev/null || echo "0")
PRIMARY_CHECK_TS=$(cat "$STATE/primary_check_ts" 2>/dev/null || echo "0")
# Load server list
SERVER_COUNT=0
idx=1
while true; do
val=$(grep "^SERVER_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)
if [ -z "$val" ]; then
break
fi
eval "SERVER_${idx}_HOST=$(echo "$val" | cut -d: -f1)"
eval "SERVER_${idx}_PORTS=$(echo "$val" | cut -d: -f2-)"
eval "SERVER_${idx}_KEY=$(grep "^KEY_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)"
SERVER_COUNT=$idx
idx=$((idx + 1))
done
if [ "$SERVER_COUNT" -eq 0 ]; then
log "ERROR: no servers in failover.conf"
exit 1
fi
# ── Get current handshake age ──
get_handshake_age() {
hs=$(wg show wg0 latest-handshakes 2>/dev/null | awk '{print $2}')
if [ -z "$hs" ] || [ "$hs" = "0" ]; then
echo "9999"
return
fi
now=$(date +%s)
age=$((now - hs))
echo "$age"
}
# ── Get port by index from comma-separated list ──
get_port() {
server_idx=$1
port_idx=$2
eval "ports=\$SERVER_${server_idx}_PORTS"
echo "$ports" | tr ',' '\n' | sed -n "$((port_idx + 1))p"
}
get_port_count() {
server_idx=$1
eval "ports=\$SERVER_${server_idx}_PORTS"
echo "$ports" | tr ',' '\n' | wc -l
}
# ── Switch to specific server:port ──
switch_endpoint() {
server_idx=$1
port_idx=$2
eval "host=\$SERVER_${server_idx}_HOST"
eval "obf_key=\$SERVER_${server_idx}_KEY"
port=$(get_port "$server_idx" "$port_idx")
if [ -z "$host" ] || [ -z "$port" ]; then
log "ERROR: invalid server $server_idx port_idx $port_idx"
return 1
fi
log "SWITCH → server $server_idx ($host:$port)"
# Update obfuscator config target
if [ -f "$OBF_CONF" ]; then
sed -i "s|^target = .*|target = ${host}:${port}|" "$OBF_CONF"
if [ -n "$obf_key" ]; then
sed -i "s|^key = .*|key = ${obf_key}|" "$OBF_CONF"
fi
fi
# Restart obfuscator
killall wg-obfuscator 2>/dev/null
sleep 1
wg-obfuscator --config "$OBF_CONF" &
# Save state
echo "$server_idx" > "$STATE/current_server"
echo "$port_idx" > "$STATE/current_port_idx"
echo "0" > "$STATE/restart_count"
}
# ── Try next port on current server ──
try_next_port() {
port_count=$(get_port_count "$CURRENT_SERVER")
next_idx=$(( (CURRENT_PORT_IDX + 1) % port_count ))
if [ "$next_idx" -eq 0 ]; then
return 1
fi
log "PORT HOP → port idx $next_idx on server $CURRENT_SERVER"
switch_endpoint "$CURRENT_SERVER" "$next_idx"
return 0
}
# ── Try next server ──
try_next_server() {
next=$((CURRENT_SERVER + 1))
if [ "$next" -gt "$SERVER_COUNT" ]; then
next=1
fi
if [ "$next" -eq "$CURRENT_SERVER" ]; then
log "WARN: only one server, can't failover"
return 1
fi
log "FAILOVER → server $next"
switch_endpoint "$next" "0"
return 0
}
# ── Check if primary is back (every 5 min when on secondary) ──
check_primary() {
if [ "$CURRENT_SERVER" -eq 1 ]; then
return
fi
now=$(date +%s)
elapsed=$((now - PRIMARY_CHECK_TS))
if [ "$elapsed" -lt 300 ]; then
return
fi
echo "$now" > "$STATE/primary_check_ts"
eval "host=\$SERVER_1_HOST"
port=$(get_port 1 0)
# Quick UDP probe — send a byte, see if port responds
if ping -c 1 -W 2 "$host" >/dev/null 2>&1; then
log "PRIMARY alive, switching back"
switch_endpoint 1 0
fi
}
# ── Main logic ──
AGE=$(get_handshake_age)
if [ "$AGE" -lt "$HANDSHAKE_WARN" ]; then
# All good, reset counters
if [ "$RESTART_COUNT" -gt 0 ]; then
log "OK: connection restored (age=${AGE}s)"
echo "0" > "$STATE/restart_count"
fi
check_primary
exit 0
fi
log "STALE handshake: ${AGE}s (server=$CURRENT_SERVER port_idx=$CURRENT_PORT_IDX restarts=$RESTART_COUNT)"
if [ "$AGE" -lt "$HANDSHAKE_RESTART_OBF" ]; then
# Restart obfuscator only
RESTART_COUNT=$((RESTART_COUNT + 1))
echo "$RESTART_COUNT" > "$STATE/restart_count"
log "ACTION: restart obfuscator (attempt $RESTART_COUNT)"
killall wg-obfuscator 2>/dev/null
sleep 1
wg-obfuscator --config "$OBF_CONF" &
exit 0
fi
if [ "$AGE" -lt "$HANDSHAKE_RESTART_WG" ]; then
# Restart WireGuard + obfuscator
RESTART_COUNT=$((RESTART_COUNT + 1))
echo "$RESTART_COUNT" > "$STATE/restart_count"
log "ACTION: restart WG + obfuscator (attempt $RESTART_COUNT)"
wg-quick down wg0 2>/dev/null
killall wg-obfuscator 2>/dev/null
sleep 2
wg-quick up wg0 2>/dev/null
sleep 1
wg-obfuscator --config "$OBF_CONF" &
exit 0
fi
if [ "$AGE" -lt "$HANDSHAKE_FAILOVER" ]; then
# Try next port
log "ACTION: try port hop"
if ! try_next_port; then
log "ACTION: all ports exhausted, try next server"
try_next_server
fi
exit 0
fi
# Full failover
log "ACTION: failover (handshake ${AGE}s)"
if ! try_next_server; then
# Only one server, cycle ports
try_next_port
fi

View File

@@ -1,107 +0,0 @@
# PCA Phobos ports and firewall checklist
This page is the short truth table for normal Phobos work. Use it before
opening a ticket about "router does not switch", "secondary is online but sync
fails", or "panel is too exposed".
## Primary server
| Port | Protocol | Who connects | Required | Why |
| --- | --- | --- | --- | --- |
| `PANEL_PORT` | TCP | admin browser, secondary setup/register, router pull fallback | yes | Web panel and `/api/*`. Real value: `cat /opt/phobos-panel/.port`. |
| `80` | TCP | routers during install, Android APK download | recommended | nginx serves `/init/`, `/packages/`, `/app/`. |
| `2083,5443,993` | UDP | routers and Android PhobosWG | yes | wg-obfuscator public entry ports. Real value: `grep OBFUSCATOR_PORTS /opt/Phobos/server/server.env`. |
| `51820` | UDP | plain WireGuard clients only | no by default | Usually blocked to force obfuscation. Open only if installed with `ALLOW_PLAIN_WG=1` for iOS/plain WG. |
| `22` | TCP | admin only | optional | SSH maintenance. Do not expose broadly. |
`PANEL_PORT` is random on clean install unless passed as env:
```bash
cat /opt/phobos-panel/.port
```
## Secondary server
| Port | Protocol | Who connects | Required | Why |
| --- | --- | --- | --- | --- |
| `8444` | TCP | primary panel, routers via tunnel/public path | yes | Mini-API: health, peer sync, router config cache. |
| `2083,5443,993` | UDP | routers and Android PhobosWG | yes | wg-obfuscator public entry ports. |
| `51820` | UDP | nobody public | no | WireGuard listens locally behind obfuscator; public access is blocked. |
| `22` | TCP | admin during install | optional | SSH maintenance. |
If secondary is "Online" but `Info`, `Sync`, or client assignment fails, check
`8444/tcp` first and confirm the API key matches the main panel key.
## Router outbound access
Routers need outbound access to:
- every server UDP obfuscator port, default `2083,5443,993`;
- primary `PANEL_PORT/tcp` for initial install and public pull fallback;
- primary `80/tcp` if using the nginx install fallback;
- tunnel management paths after VPN is up:
- `http://10.25.0.1:8444` for secondary mini-API;
- `http(s)://10.25.0.1:<PANEL_PORT>` for primary panel API.
Current router packages write these files on the router:
```bash
/opt/etc/Phobos/panel_url
/opt/etc/Phobos/panel_urls
/opt/etc/Phobos/panel_port
/opt/etc/Phobos/panel_scheme
```
If a router does not switch after changing its server in the panel, check:
```bash
cat /opt/etc/Phobos/panel_urls
cat /opt/etc/Phobos/failover.conf
tail -n 80 /opt/etc/Phobos/health.log
/opt/etc/Phobos/phobos-pull.sh
```
If `panel_urls` contains an old IP or old panel port, create a fresh install
command in the panel and reinstall/update that router package.
## HTTPS panel
Native HTTPS is supported by the primary installer:
```bash
PANEL_TLS_CERT=/etc/letsencrypt/live/example.com/fullchain.pem \
PANEL_TLS_KEY=/etc/letsencrypt/live/example.com/privkey.pem \
bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/install.sh)
```
Then the panel runs as:
```text
https://<server-ip>:<PANEL_PORT>
```
The generated router and secondary commands use `curl -k`/`wget
--no-check-certificate` fallback so a self-signed certificate can work for
bootstrap. For humans in a browser, use a real certificate to avoid warnings.
## Can I IP-whitelist the panel port?
Yes, but with care. `PANEL_PORT` is not only the admin UI. It also carries
server registration and router pull API. If you whitelist it too tightly, these
features break:
- secondary auto-register;
- `Info` / `Sync` / client assignment to secondary;
- router pull fallback before tunnel is up;
- router config update when public path is the only available path.
Minimum allowlist for `PANEL_PORT`:
- your admin IPs;
- all secondary server public IPs;
- router public IPs, if they are static;
- or no router IP restriction until first install is complete and tunnel pull is proven.
For CGNAT/dynamic-router fleets, do not rely on a strict public-IP allowlist for
router management. Use strong panel password, API key secrecy, optional HTTPS,
and keep only the required ports open.

View File

@@ -1,160 +0,0 @@
# WARP in `dev` — modes, checks, current limits
Date: `2026-06-06`
Channel: `dev`
Version: `v1.3.1-dev`
## What was added
The `dev` panel now has a dedicated `WARP / WARP+` block in `/settings`.
Supported modes:
- `egress` — route client outbound traffic through WARP on the server side;
- `relay` — expose a UDP relay for a separate WARP/WireGuard client profile;
- `custom profile` — save a full custom `warp.conf` from the web UI;
- `WARP+ key` — save a license key and rebuild the profile through `wgcf`.
The UI also has a `?` help block with plain-language explanations:
- what each mode does;
- when to use `free` vs `custom`;
- what `WARP+ key` changes;
- what `Enable` and `Disable` do;
- how per-client WARP works from the client list.
## How the modes work
### `egress`
Use this when you want Phobos clients to keep connecting the usual way, but send
their outgoing traffic through Cloudflare WARP after they land on the server.
Implementation:
- WireGuard clients still connect to the Phobos server;
- server-side policy routing sends only selected client tunnel IPs through `warp`;
- NAT is done with `POSTROUTING MASQUERADE`;
- client rows in `/clients` have `WARP ON/OFF`, which adds/removes `ip rule from <client-ip>/32`.
### `relay`
Use this when you need a separate relay endpoint for another WARP/WireGuard
client profile.
Important:
- this is **not** a drop-in replacement for the normal Phobos router config;
- the client must use a separate WARP/WireGuard profile aimed at the relay port;
- this mode is for relay-style scenarios, not for replacing the regular
`home.conf` path one-to-one.
Implementation:
- UDP relay rules are added with tagged iptables entries;
- incoming UDP on the selected port is DNAT'ed to the target WARP endpoint;
- outgoing packets are MASQUERADE'd back through the relay host.
### `custom profile` and `WARP+`
Two ways to use a non-default profile:
- paste a full custom `warp.conf`;
- save a `WARP+ key` and let `wgcf` rebuild the profile from it.
This keeps the UI simple:
- `free WARP` still works out of the box;
- `WARP+` is possible when the owner has a real key;
- no hidden magic is required outside the panel.
## Live checks completed
### Server-side checks
Passed:
- `python3 -m py_compile app.py server/api.py`
- `bash -n install.sh update.sh server/phobos-warp.sh`
- live `status` checks on dev server `64.188.81.115`
Verified on the dev server:
- `egress` mode enables cleanly;
- current egress IPv4 is reported in status JSON;
- `custom profile` and `WARP+ key` save from the web UI;
- relay rules appear with separate tagged iptables entries.
### Real router check
Router used for testing:
- public SSH: `95.72.5.253`
- web: `https://homesmart.netcraze.pro/dashboard`
Confirmed:
- real router traffic was sent through the dev server;
- with `egress` enabled, a live `curl https://api.ipify.org` from the router
returned WARP egress IP `104.28.193.219`;
- this proves the real data path for `router -> Phobos -> WARP -> internet`.
### Relay check
Confirmed:
- relay mode enables from the server side;
- relay iptables rules are created;
- counters increase on live traffic;
- a separate WARP profile pointed at the relay port completes handshake.
Not fully closed yet:
- relay handshake and packet flow were confirmed;
- a full end-to-end "internet works through relay profile" check on the router
was not fully finished in a clean state before router access became limited.
## Router recovery note
During live tests, WAN SSH access to the router stopped responding, so full file
restore over shell was blocked.
What was still restored safely:
- temporary KeenDNS helper proxies created for testing were removed;
- temporary extra WireGuard peer entries were cleaned up;
- the router is currently left on a **working direct dev WireGuard peer** so it
stays reachable through the test tunnel rather than returning to a broken
down state.
What still needs one final shell pass later:
- restore `/opt/etc/Phobos/home.conf` from backup;
- restore `/opt/etc/Phobos/wg-obfuscator.conf` from backup;
- restore `/opt/etc/Phobos/failover.conf` from backup;
- restore minute cron jobs for `phobos-health.sh` and `phobos-pull.sh`.
Known backup path on the router:
- `/opt/etc/Phobos/test-backup-20260606-044834`
## Current honest status
Ready now:
- `egress` mode
- per-client `WARP ON/OFF`
- `custom profile`
- `WARP+ key`
- UI help text
Partially verified:
- `relay` mode
Extra audit fixes after live testing:
- orphan WireGuard peers now show up in both `Sessions` and `Clients` as `orphan-*` instead of disappearing into `unknown`;
- orphan rows can be `Kick`ed, `Purge`d, and also toggled through per-peer WARP;
- client package build no longer fails when `/opt/Phobos/bin` is missing or `OBFUSCATOR_KEY` is empty;
- update / restore scripts now restart the active panel service automatically (`phobos-panel` or `phobos-dev-test`).

View File

@@ -1,9 +0,0 @@
# Скриншоты
Положи сюда 3 PNG (панель открыта — сделай скрины своих страниц):
- `sessions.png` — страница «Сессии»
- `clients.png` — страница «Клиенты»
- `servers.png` — страница «Серверы»
README в корне на них ссылается. Рекомендуемая ширина ~1000–1400px.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

View File

@@ -1,350 +0,0 @@
#!/bin/bash
# ============================================================
# PCA Phobos — TURNKEY installer (primary / panel node)
#
# One command, all dependencies, from a clean VPS:
# bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/install.sh)
#
# Installs, in order:
# deps -> wg-obfuscator (Ground-Zerro) -> WireGuard wg0 ->
# obfuscator services -> Phobos repo (onboarding scripts) +
# PCA patches -> web panel -> nginx (/init,/packages) ->
# server-side router watchdog.
#
# Env (all optional):
# PANEL_PORT random 10000-59999 web panel port
# PANEL_TLS_CERT / PANEL_TLS_KEY enable native HTTPS for panel
# PANEL_PASS OcAdmin2026! panel admin password
# API_KEY random shared key (agents + router pull token)
# OBF_PORTS 2083,5443,993 obfuscator listen ports
# TG_TOKEN / TG_CHAT Telegram alerts
# PCA_BRANCH main branch to pull PCA files from
# ============================================================
set -e
PANEL_PASS="${PANEL_PASS:-OcAdmin2026!}"
PANEL_TLS_CERT="${PANEL_TLS_CERT:-}"
PANEL_TLS_KEY="${PANEL_TLS_KEY:-}"
PANEL_SCHEME="${PANEL_SCHEME:-http}"
TG_TOKEN="${TG_TOKEN:-}"
TG_CHAT="${TG_CHAT:-}"
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
SERVER_WG_IPV4_NETWORK="${SERVER_WG_IPV4_NETWORK:-10.25.0.0/16}"
# Источник: Gitea git.bobyrev.com (основной), GitHub — запасной.
# Каналы stable/beta/dev — ветки main/beta/dev одного публичного репозитория.
PCA_GIT="${PCA_GIT:-https://git.bobyrev.com/PCA/PCA_Phobos}"
GH_RAW="${GH_RAW:-https://raw.githubusercontent.com/andrey271192/PCA_Phobos}"
CHANNEL="${CHANNEL:-stable}"
case "$CHANNEL" in
stable|main|"") PCA_BRANCH="${PCA_BRANCH:-main}";;
*) PCA_BRANCH="${PCA_BRANCH:-$CHANNEL}";;
esac
case "$PCA_BRANCH" in v[0-9]*) _kind=tag;; *) _kind=branch;; esac
PHOBOS_DIR="/opt/Phobos"
PANEL_DIR="/opt/phobos-panel"
RAW="$PCA_GIT/raw/$_kind/$PCA_BRANCH"
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[ "$EUID" -eq 0 ] || { echo "Run as root"; exit 1; }
if [ -z "$PANEL_PORT" ]; then
PANEL_PORT=$(shuf -i 10000-59999 -n 1 2>/dev/null || awk 'BEGIN{srand(); print int(10000+rand()*50000)}')
fi
if [ -n "$PANEL_TLS_CERT" ] || [ -n "$PANEL_TLS_KEY" ]; then
[ -f "$PANEL_TLS_CERT" ] || { echo "ERROR: PANEL_TLS_CERT not found: $PANEL_TLS_CERT"; exit 1; }
[ -f "$PANEL_TLS_KEY" ] || { echo "ERROR: PANEL_TLS_KEY not found: $PANEL_TLS_KEY"; exit 1; }
PANEL_SCHEME="https"
fi
case "$PANEL_SCHEME" in http|https) ;; *) echo "ERROR: PANEL_SCHEME must be http or https"; exit 1;; esac
API_KEY="${API_KEY:-$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24)}"
WG_SERVER_IPV4="$(python3 - "$SERVER_WG_IPV4_NETWORK" <<'PY'
import ipaddress, sys
net = ipaddress.ip_network(sys.argv[1], strict=False)
print(next(net.hosts()))
PY
)"
WG_PREFIXLEN="$(python3 - "$SERVER_WG_IPV4_NETWORK" <<'PY'
import ipaddress, sys
print(ipaddress.ip_network(sys.argv[1], strict=False).prefixlen)
PY
)"
SERVER_IP=$(curl -s -m8 https://api.ipify.org || hostname -I | awk '{print $1}')
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}"
ARCH=$(uname -m)
echo "============================================"
echo " PCA Phobos - turnkey primary install"
echo " IP=$SERVER_IP iface=$IFACE arch=$ARCH"
echo " panel port=$PANEL_PORT obf ports=$OBF_PORTS"
echo "============================================"
# ── 1. dependencies ──
echo "[1/9] dependencies..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq wireguard wireguard-tools iptables jq curl git ca-certificates \
python3 python3-flask python3-pip gunicorn nginx cron python3-qrcode sshpass >/dev/null
apt-get install -y -qq python3-flask-sock >/dev/null 2>&1 || true
apt-get install -y -qq python3-asyncssh >/dev/null 2>&1 || true
apt-get install -y -qq python3-websocket >/dev/null 2>&1 || true
python3 - <<'PY' >/dev/null 2>&1 || python3 -m pip install flask-sock asyncssh websocket-client >/dev/null 2>&1 || python3 -m pip install --break-system-packages flask-sock asyncssh websocket-client >/dev/null
import flask_sock, asyncssh, websocket
PY
mkdir -p "$PANEL_DIR/static"
curl -fsSL -m20 https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css -o "$PANEL_DIR/static/xterm.min.css" 2>/dev/null || true
curl -fsSL -m20 https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js -o "$PANEL_DIR/static/xterm.min.js" 2>/dev/null || true
curl -fsSL -m20 https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js -o "$PANEL_DIR/static/xterm-addon-fit.min.js" 2>/dev/null || true
systemctl enable cron -q 2>/dev/null || true; systemctl start cron 2>/dev/null || true
# ── 2. wg-obfuscator binary (Ground-Zerro) ──
echo "[2/9] wg-obfuscator..."
mkdir -p "$PHOBOS_DIR"/{server,clients,bin,tokens,www/init,www/packages,packages}
OBF_ARCH="$ARCH"
[ "$OBF_ARCH" = "amd64" ] && OBF_ARCH="x86_64"
[ "$OBF_ARCH" = "arm64" ] && OBF_ARCH="aarch64"
if [ ! -x /usr/local/bin/wg-obfuscator ]; then
R=/tmp/phobos-obf; rm -rf "$R"; mkdir -p "$R"; cd "$R"
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-* "$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-${OBF_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 "x86_64:linux-x64" "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-${OBF_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)..."
R="$PHOBOS_DIR/repo"; rm -rf "$R"; mkdir -p "$R"; cd "$R"
git init -q; git remote add origin https://github.com/Ground-Zerro/Phobos.git
git config core.sparseCheckout true
printf 'server\nclient\n' > .git/info/sparse-checkout
git pull origin main -q; rm -rf .git
find "$R" -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true
cd /
# ── 4. WireGuard wg0 (primary) ──
echo "[4/9] WireGuard wg0..."
if [ ! -f /etc/wireguard/wg0.conf ]; then
WG_PRIV=$(wg genkey); WG_PUB=$(echo "$WG_PRIV" | wg pubkey)
cat > /etc/wireguard/wg0.conf <<WG
[Interface]
Address = ${WG_SERVER_IPV4}/${WG_PREFIXLEN}
ListenPort = 51820
PrivateKey = $WG_PRIV
PostUp = iptables -I FORWARD 1 -i wg0 -j ACCEPT; iptables -I FORWARD 1 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -o $IFACE -j MASQUERADE
WG
chmod 600 /etc/wireguard/wg0.conf
else
WG_PRIV=$(grep '^PrivateKey' /etc/wireguard/wg0.conf | cut -d= -f2- | tr -d ' ')
WG_PUB=$(echo "$WG_PRIV" | wg pubkey)
fi
sysctl -w net.ipv4.ip_forward=1 -q
grep -q '^net.ipv4.ip_forward = 1' /etc/sysctl.conf || echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
systemctl enable wg-quick@wg0 -q 2>/dev/null || true
systemctl restart wg-quick@wg0
# ── 5. obfuscator services (multi-port) ──
echo "[5/9] obfuscator services..."
OBF_KEY=$(head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 32)
# Block direct WG (force obfuscation) unless ALLOW_PLAIN_WG=1 (e.g. for iOS WireGuard)
if [ -z "${ALLOW_PLAIN_WG:-}" ]; then
iptables -C INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null \
|| iptables -A INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP
else
iptables -D INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null || true
fi
IFS=',' read -ra PORTS <<< "$OBF_PORTS"
for PORT in "${PORTS[@]}"; do
cat > "$PHOBOS_DIR/server/wg-obfuscator-${PORT}.conf" <<EOF
[instance]
source-if = 0.0.0.0
source-lport = ${PORT}
target = 127.0.0.1:51820
key = ${OBF_KEY}
masking = AUTO
verbose = INFO
idle-timeout = 300
max-dummy = 50
EOF
cat > /etc/systemd/system/wg-obfuscator-${PORT}.service <<EOF
[Unit]
Description=WireGuard Obfuscator (port ${PORT})
After=network.target wg-quick@wg0.service
[Service]
Type=simple
ExecStart=/usr/local/bin/wg-obfuscator --config ${PHOBOS_DIR}/server/wg-obfuscator-${PORT}.conf
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
done
systemctl daemon-reload
for PORT in "${PORTS[@]}"; do systemctl enable wg-obfuscator-${PORT} -q; systemctl restart wg-obfuscator-${PORT}; done
# server.env (primary)
cat > "$PHOBOS_DIR/server/server.env" <<EOF
SERVER_WG_PRIVATE_KEY=$WG_PRIV
SERVER_WG_PUBLIC_KEY=$WG_PUB
SERVER_PUBLIC_IP_V4=$SERVER_IP
SERVER_WG_IPV4_NETWORK=$SERVER_WG_IPV4_NETWORK
SERVER_WG_IPV4_ADDRESS=${WG_SERVER_IPV4}/${WG_PREFIXLEN}
OBFUSCATOR_KEY=$OBF_KEY
OBFUSCATOR_PORTS=$OBF_PORTS
CLIENT_WG_PORT=51820
ROLE=primary
EOF
# ── 6. PCA patches over onboarding scripts + server-side helpers ──
echo "[6/9] PCA patches (tunnel-pull, self-heal, watchdog, 403 fix)..."
fetch() {
mkdir -p "$(dirname "$2")"
# ponytail: файл из архива рядом с install.sh — не ходим в сеть.
if [ -f "$SRC/$1" ]; then cp -f "$SRC/$1" "$2" && return 0; fi
curl -fsSL -m20 "$RAW/$1?cb=$(date +%s)" -o "$2" && return 0
curl -fsSL -m20 "$GH_RAW/$PCA_BRANCH/$1?cb=$(date +%s)" -o "$2" && return 0
echo " WARN: fetch $1 failed"; return 1
}
fetch overlay/lib-core.sh "$PHOBOS_DIR/repo/server/scripts/lib-core.sh" && chmod +x "$PHOBOS_DIR/repo/server/scripts/lib-core.sh"
fetch overlay/phobos-client.sh "$PHOBOS_DIR/repo/server/scripts/phobos-client.sh" && chmod +x "$PHOBOS_DIR/repo/server/scripts/phobos-client.sh"
fetch overlay/install-router.sh.template "$PHOBOS_DIR/repo/client/templates/install-router.sh.template"
fetch overlay/lib-client.sh "$PHOBOS_DIR/repo/client/templates/lib-client.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/lib-client.sh"
fetch overlay/install-obfuscator.sh "$PHOBOS_DIR/repo/client/templates/install-obfuscator.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/install-obfuscator.sh"
fetch overlay/install-wireguard.sh "$PHOBOS_DIR/repo/client/templates/install-wireguard.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/install-wireguard.sh"
fetch overlay/router-configure-wireguard.sh "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard.sh"
fetch overlay/router-configure-wireguard-openwrt.sh "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard-openwrt.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/router-configure-wireguard-openwrt.sh"
fetch overlay/phobos-pull.sh "$PHOBOS_DIR/repo/client/templates/phobos-pull.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/phobos-pull.sh"
fetch overlay/phobos-ttyd.sh "$PHOBOS_DIR/repo/client/templates/phobos-ttyd.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/phobos-ttyd.sh" && cp "$PHOBOS_DIR/repo/client/templates/phobos-ttyd.sh" "$PHOBOS_DIR/www/phobos-ttyd.sh" 2>/dev/null || true
fetch overlay/phobos-uninstall.sh "$PHOBOS_DIR/repo/client/templates/phobos-uninstall.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/phobos-uninstall.sh"
fetch overlay/3xui.sh "$PHOBOS_DIR/repo/client/templates/3xui.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/3xui.sh"
fetch server/phobos-warp.sh "$PHOBOS_DIR/server/phobos-warp.sh" && chmod +x "$PHOBOS_DIR/server/phobos-warp.sh" && ln -sf "$PHOBOS_DIR/server/phobos-warp.sh" /usr/local/bin/phobos-warp
fetch server/phobos-guard.sh "$PHOBOS_DIR/server/phobos-guard.sh" && chmod +x "$PHOBOS_DIR/server/phobos-guard.sh" && ln -sf "$PHOBOS_DIR/server/phobos-guard.sh" /usr/local/bin/phobos-guard
fetch server/phobos-cascade.sh "$PHOBOS_DIR/server/phobos-cascade.sh" && chmod +x "$PHOBOS_DIR/server/phobos-cascade.sh" && ln -sf "$PHOBOS_DIR/server/phobos-cascade.sh" /usr/local/bin/phobos-cascade
fetch server/phobos-domain.sh "$PHOBOS_DIR/server/phobos-domain.sh" && chmod +x "$PHOBOS_DIR/server/phobos-domain.sh" && ln -sf "$PHOBOS_DIR/server/phobos-domain.sh" /usr/local/bin/phobos-domain
fetch server/phobos-limit.sh "$PHOBOS_DIR/server/phobos-limit.sh" && chmod +x "$PHOBOS_DIR/server/phobos-limit.sh"
fetch server/phobos-speedtest.sh "$PHOBOS_DIR/server/phobos-speedtest.sh" && chmod +x "$PHOBOS_DIR/server/phobos-speedtest.sh"
fetch server/phobos-tcp.sh "$PHOBOS_DIR/server/phobos-tcp.sh" && chmod +x "$PHOBOS_DIR/server/phobos-tcp.sh" && ln -sf "$PHOBOS_DIR/server/phobos-tcp.sh" /usr/local/bin/phobos-tcp
fetch "udptcp/bin/udptcp-$OBF_ARCH" "$PHOBOS_DIR/bin/udptcp" && chmod +x "$PHOBOS_DIR/bin/udptcp"
fetch server/phobos-health.sh "$PHOBOS_DIR/server/phobos-health.sh" && chmod +x "$PHOBOS_DIR/server/phobos-health.sh"
fetch server/phobos-self-check.sh "$PHOBOS_DIR/server/phobos-self-check.sh" && chmod +x "$PHOBOS_DIR/server/phobos-self-check.sh" && ln -sf "$PHOBOS_DIR/server/phobos-self-check.sh" /usr/local/bin/phobos-self-check
fetch server/phobos-pull.sh "$PHOBOS_DIR/server/phobos-pull.sh" && chmod +x "$PHOBOS_DIR/server/phobos-pull.sh"
fetch server/phobos-router-watchdog.py "$PHOBOS_DIR/server/phobos-router-watchdog.py"
fetch server/api.py "$PHOBOS_DIR/server/api.py"
fetch server/secondary-setup.sh "$PHOBOS_DIR/server/secondary-setup.sh" && chmod +x "$PHOBOS_DIR/server/secondary-setup.sh"
fetch update.sh "$PHOBOS_DIR/server/update.sh" && chmod +x "$PHOBOS_DIR/server/update.sh" && ln -sf "$PHOBOS_DIR/server/update.sh" /usr/local/bin/phobos-update
fetch server/phobos-backup.sh "$PHOBOS_DIR/server/phobos-backup.sh" && chmod +x "$PHOBOS_DIR/server/phobos-backup.sh" && ln -sf "$PHOBOS_DIR/server/phobos-backup.sh" /usr/local/bin/phobos-backup
mkdir -p "$PANEL_DIR"; fetch VERSION "$PANEL_DIR/.version" 2>/dev/null || true
[ -f "$PHOBOS_DIR/tokens/tokens.json" ] || echo '[]' > "$PHOBOS_DIR/tokens/tokens.json"
"$PHOBOS_DIR/server/phobos-self-check.sh" --fix
# ── 7. web panel ──
echo "[7/9] web panel..."
mkdir -p "$PANEL_DIR"
fetch app.py "$PANEL_DIR/app.py" || { echo "ERROR: panel app.py fetch failed"; exit 1; }
if [ ! -f "$PANEL_DIR/settings.json" ]; then
cat > "$PANEL_DIR/settings.json" <<EOF
{
"admin_pass": "$PANEL_PASS",
"tg_bot_token": "$TG_TOKEN",
"tg_chat_id": "$TG_CHAT",
"monitor_interval": 30,
"server_api_key": "$API_KEY",
"labels": {},
"subscriptions": {},
"router_access": {},
"client_assignments": {}
}
EOF
fi
echo "$PANEL_PORT" > "$PANEL_DIR/.port"
echo "$PANEL_SCHEME" > "$PANEL_DIR/.scheme"
TLS_ARGS=""
if [ "$PANEL_SCHEME" = "https" ]; then
TLS_ARGS="--certfile $PANEL_TLS_CERT --keyfile $PANEL_TLS_KEY"
fi
cat > /etc/systemd/system/phobos-panel.service <<EOF
[Unit]
Description=Phobos VPN Web Panel
After=network.target wg-quick@wg0.service
Wants=wg-quick@wg0.service
[Service]
Type=simple
WorkingDirectory=$PANEL_DIR
ExecStart=/usr/bin/gunicorn -w 1 --threads 20 $TLS_ARGS -b 0.0.0.0:$PANEL_PORT app:app
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
Environment=PANEL_SCHEME=$PANEL_SCHEME
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload; systemctl enable phobos-panel -q; systemctl restart phobos-panel
# TCP fallback for clients whose UDP is cut (port 443/8443/... picked automatically)
"$PHOBOS_DIR/server/phobos-tcp.sh" enable || echo " WARN: TCP fallback not enabled"
# ── 8. nginx (serve /init + /packages over plain HTTP for routers) ──
echo "[8/9] nginx..."
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
cat > /etc/nginx/sites-available/phobos <<'NGINX'
server {
listen 80 default_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
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) ──
echo "[9/9] router watchdog..."
if [ -f "$PHOBOS_DIR/server/phobos-router-watchdog.py" ]; then
# set -e safe: grep -v on an empty crontab returns 1, so guard with || true
CRON_CUR=$(crontab -l 2>/dev/null | grep -v phobos-router-watchdog || true)
printf '%s\n%s\n' "$CRON_CUR" \
"*/3 * * * * /usr/bin/python3 $PHOBOS_DIR/server/phobos-router-watchdog.py >/dev/null 2>&1" \
| grep -v '^[[:space:]]*$' | crontab -
fi
sleep 2
# persist channel so phobos-update follows it
mkdir -p "$PANEL_DIR"
echo "$CHANNEL" > "$PANEL_DIR/.channel" 2>/dev/null || true
echo ""
echo "============================================"
echo " Installation complete"
echo " Panel : $PANEL_SCHEME://$SERVER_IP:$PANEL_PORT"
echo " Login : admin"
echo " Pass : $PANEL_PASS"
echo " API key (agents+pull): $API_KEY"
echo " WG pub: $WG_PUB"
echo "============================================"
echo "Status:"
for s in wg-quick@wg0 phobos-panel nginx; do printf " %-18s %s\n" "$s" "$(systemctl is-active $s 2>/dev/null)"; done
for PORT in "${PORTS[@]}"; do printf " %-18s %s\n" "wg-obfuscator-$PORT" "$(systemctl is-active wg-obfuscator-$PORT 2>/dev/null)"; done

View File

@@ -1,456 +0,0 @@
#!/bin/bash
GREEN='\033[92m'
YELLOW='\033[93m'
RED='\033[91m'
BLUE='\033[94m'
CYAN='\033[96m'
RESET='\033[0m'
BOLD='\033[1m'
print_status() {
local message="$1"
local status="${2:-info}"
local prefix=""
case "$status" in
success) prefix="${GREEN}[OK]" ;;
error) prefix="${RED}[ERR]" ;;
warning) prefix="${YELLOW}[WARN]" ;;
info) prefix="${BLUE}[INFO]" ;;
debug) prefix="${CYAN}[DBG]" ;;
esac
echo -e "${prefix} ${message}${RESET}"
}
check_dependencies() {
local missing=()
for cmd in jq sqlite3; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
print_status "Installing missing dependencies: ${missing[*]}" "info"
if command -v apt-get >/dev/null 2>&1; then
apt-get update -qq && apt-get install -y -qq "${missing[@]}" >/dev/null 2>&1
elif command -v yum >/dev/null 2>&1; then
yum install -y -q "${missing[@]/sqlite3/sqlite}" >/dev/null 2>&1
elif command -v dnf >/dev/null 2>&1; then
dnf install -y -q "${missing[@]/sqlite3/sqlite}" >/dev/null 2>&1
elif command -v apk >/dev/null 2>&1; then
apk add --quiet "${missing[@]/sqlite3/sqlite}" >/dev/null 2>&1
fi
for cmd in "${missing[@]}"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
print_status "Failed to install $cmd" "error"
exit 1
fi
done
print_status "Dependencies installed successfully" "success"
fi
}
parse_wireguard_conf() {
local config_path="$1"
local section=""
if [[ ! -f "$config_path" ]]; then
print_status "File $config_path not found" "error"
exit 1
fi
INTERFACE_PRIVATEKEY=""
INTERFACE_ADDRESS=""
INTERFACE_MTU="1420"
PEER_PUBLICKEY=""
PEER_ENDPOINT=""
PEER_ALLOWEDIPS=""
PEER_PRESHAREDKEY=""
PEER_KEEPALIVE="0"
while IFS= read -r line || [[ -n "$line" ]]; do
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$line" || "$line" == \#* ]] && continue
if [[ "$line" == "["*"]" ]]; then
section=$(echo "$line" | tr -d '[]' | tr '[:upper:]' '[:lower:]')
continue
fi
if [[ "$line" == *"="* ]]; then
local key=$(echo "$line" | cut -d'=' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
local value=$(echo "$line" | cut -d'=' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [[ "$section" == "interface" ]]; then
case "$key" in
PrivateKey) INTERFACE_PRIVATEKEY="$value" ;;
Address) INTERFACE_ADDRESS="$value" ;;
MTU) INTERFACE_MTU="$value" ;;
esac
elif [[ "$section" == "peer" ]]; then
case "$key" in
PublicKey) PEER_PUBLICKEY="$value" ;;
Endpoint) PEER_ENDPOINT="$value" ;;
AllowedIPs) PEER_ALLOWEDIPS="$value" ;;
PresharedKey|PreSharedKey) PEER_PRESHAREDKEY="$value" ;;
PersistentKeepalive) PEER_KEEPALIVE="$value" ;;
esac
fi
fi
done < "$config_path"
}
extract_endpoint() {
local endpoint="$1"
if [[ "$endpoint" == "["*"]:"* ]]; then
SERVER_ADDRESS=$(echo "$endpoint" | sed 's/^\[\([^]]*\)\]:.*/\1/')
SERVER_PORT=$(echo "$endpoint" | sed 's/.*\]:\([0-9]*\).*/\1/')
else
SERVER_ADDRESS=$(echo "$endpoint" | rev | cut -d':' -f2- | rev)
SERVER_PORT=$(echo "$endpoint" | rev | cut -d':' -f1 | rev)
fi
[[ -z "$SERVER_PORT" ]] && SERVER_PORT="51820"
}
build_local_addresses() {
local addresses="$1"
local result="[]"
IFS=',' read -ra ADDR_ARRAY <<< "$addresses"
for addr in "${ADDR_ARRAY[@]}"; do
addr=$(echo "$addr" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$addr" ]] && continue
local ip_part="$addr"
if [[ "$addr" == *"/"* ]]; then
ip_part=$(echo "$addr" | cut -d'/' -f1)
fi
if [[ "$ip_part" == *":"* ]]; then
result=$(echo "$result" | jq --arg ip "${ip_part}/128" '. + [$ip]')
else
result=$(echo "$result" | jq --arg ip "${ip_part}/32" '. + [$ip]')
fi
done
echo "$result"
}
build_allowed_ips() {
local allowed="$1"
local result="[]"
if [[ -z "$allowed" ]]; then
echo '["0.0.0.0/0", "::/0"]'
return
fi
IFS=',' read -ra IP_ARRAY <<< "$allowed"
for ip in "${IP_ARRAY[@]}"; do
ip=$(echo "$ip" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$ip" ]] && continue
result=$(echo "$result" | jq --arg ip "$ip" '. + [$ip]')
done
if [[ "$result" == "[]" ]]; then
echo '["0.0.0.0/0", "::/0"]'
else
echo "$result"
fi
}
convert_to_xray_outbound() {
local tag="${1:-Phobos}"
extract_endpoint "$PEER_ENDPOINT"
local local_addresses=$(build_local_addresses "$INTERFACE_ADDRESS")
local allowed_ips=$(build_allowed_ips "$PEER_ALLOWEDIPS")
local peer_config
peer_config=$(jq -n \
--arg pubkey "$PEER_PUBLICKEY" \
--argjson allowed "$allowed_ips" \
--arg endpoint "${SERVER_ADDRESS}:${SERVER_PORT}" \
'{
"publicKey": $pubkey,
"allowedIPs": $allowed,
"endpoint": $endpoint
}')
if [[ -n "$PEER_KEEPALIVE" && "$PEER_KEEPALIVE" != "0" ]]; then
peer_config=$(echo "$peer_config" | jq --argjson ka "$PEER_KEEPALIVE" '. + {"keepAlive": $ka}')
fi
if [[ -n "$PEER_PRESHAREDKEY" ]]; then
peer_config=$(echo "$peer_config" | jq --arg psk "$PEER_PRESHAREDKEY" '. + {"preSharedKey": $psk}')
fi
local mtu="${INTERFACE_MTU:-1420}"
OUTBOUND_JSON=$(jq -n \
--arg tag "$tag" \
--argjson mtu "$mtu" \
--arg secret "$INTERFACE_PRIVATEKEY" \
--argjson addresses "$local_addresses" \
--argjson peer "$peer_config" \
'{
"protocol": "wireguard",
"settings": {
"mtu": $mtu,
"secretKey": $secret,
"address": $addresses,
"workers": 2,
"peers": [$peer],
"noKernelTun": true
},
"tag": $tag
}')
}
get_default_xray_config() {
cat <<'XRAY_CONFIG'
{
"api": {
"services": ["HandlerService", "StatsService", "LoggerService"],
"tag": "api"
},
"inbounds": [
{
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {"address": "127.0.0.1"},
"tag": "api"
}
],
"outbounds": [
{"protocol": "freedom", "settings": {}, "tag": "direct"},
{"protocol": "blackhole", "settings": {}, "tag": "block"}
],
"policy": {
"levels": {"0": {"statsUserDownlink": true, "statsUserUplink": true}},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true,
"statsOutboundDownlink": true,
"statsOutboundUplink": true
}
},
"routing": {
"domainStrategy": "AsIs",
"rules": [
{"inboundTag": ["api"], "outboundTag": "api", "type": "field"},
{"ip": ["geoip:private"], "outboundTag": "block", "type": "field"}
]
},
"stats": {}
}
XRAY_CONFIG
}
find_database() {
local paths=(
"/etc/x-ui/x-ui.db"
"/usr/local/x-ui/x-ui.db"
"/opt/x-ui/x-ui.db"
"./x-ui.db"
)
for path in "${paths[@]}"; do
if [[ -f "$path" ]]; then
print_status "Database found: $path" "success" >&2
echo "$path"
return 0
fi
done
return 1
}
diagnose_database() {
local db_path="$1"
local tables=$(sqlite3 "$db_path" "SELECT name FROM sqlite_master WHERE type='table';" 2>/dev/null)
print_status "Found tables: $tables" "debug" >&2
if echo "$tables" | grep -q "settings"; then
local keys=$(sqlite3 "$db_path" "SELECT key FROM settings;" 2>/dev/null)
print_status "Keys in settings: $keys" "debug" >&2
for key in xrayTemplateConfig xrayConfig xray_config config; do
local result=$(sqlite3 "$db_path" "SELECT value FROM settings WHERE key = '$key';" 2>/dev/null)
if [[ -n "$result" ]]; then
print_status "Found config key: $key" "success" >&2
echo "$key"
return 0
fi
done
fi
return 1
}
write_setting_to_db() {
local db_path="$1"
local config_key="$2"
local config_value="$3"
local tmp
tmp=$(mktemp)
printf '%s' "$config_value" > "$tmp"
sqlite3 "$db_path" "INSERT OR REPLACE INTO settings (key, value) VALUES ('$config_key', readfile('$tmp'));"
local ret=$?
rm -f "$tmp"
return $ret
}
import_via_database() {
local db_path="$1"
print_status "Importing to database..." "info"
if [[ ! -f "$db_path" ]]; then
print_status "Database not found: $db_path" "error"
return 1
fi
local config_key=$(diagnose_database "$db_path")
if [[ -z "$config_key" ]]; then
print_status "Xray config not found in database, creating new..." "warning"
local base_config=$(get_default_xray_config)
local new_config=$(echo "$base_config" | jq --argjson ob "$OUTBOUND_JSON" '.outbounds = [$ob] + .outbounds')
if write_setting_to_db "$db_path" "xrayTemplateConfig" "$new_config"; then
print_status "Base Xray config created with outbound" "success"
return 0
else
print_status "Failed to create config" "error"
return 1
fi
fi
local current_config=$(sqlite3 "$db_path" "SELECT value FROM settings WHERE key = '$config_key';" 2>/dev/null)
if [[ -z "$current_config" ]]; then
print_status "Xray config not found" "error"
return 1
fi
if ! echo "$current_config" | jq . >/dev/null 2>&1; then
print_status "Invalid JSON in config" "error"
return 1
fi
local tag=$(echo "$OUTBOUND_JSON" | jq -r '.tag')
local existing_tag=$(echo "$current_config" | jq -r --arg t "$tag" '.outbounds[]? | select(.tag == $t) | .tag' 2>/dev/null)
if [[ -n "$existing_tag" ]]; then
print_status "Outbound '$tag' exists, replacing..." "warning"
current_config=$(echo "$current_config" | jq --arg t "$tag" '.outbounds = [.outbounds[] | select(.tag != $t)]')
fi
local new_config=$(echo "$current_config" | jq --argjson ob "$OUTBOUND_JSON" '.outbounds = [$ob] + .outbounds')
if write_setting_to_db "$db_path" "$config_key" "$new_config"; then
print_status "Outbound added to database" "success"
return 0
else
print_status "Failed to update database" "error"
return 1
fi
}
restart_xui_service() {
print_status "Restarting x-ui service..." "info"
if systemctl restart x-ui 2>/dev/null; then
sleep 1
if systemctl is-active --quiet x-ui; then
print_status "x-ui service restarted and active" "success"
return 0
fi
fi
for cmd in "service x-ui restart" "/etc/init.d/x-ui restart" "x-ui restart"; do
if $cmd 2>/dev/null; then
print_status "Service restarted via: $cmd" "success"
return 0
fi
done
print_status "Failed to restart service automatically" "error"
print_status "Restart manually: systemctl restart x-ui" "info"
return 1
}
main() {
echo -e "\n${BOLD}=== WireGuard to 3x-ui Auto-Import Tool ===${RESET}\n"
check_dependencies
local config_path="$1"
if [[ -z "$config_path" ]]; then
echo -n "Path to wireguard.conf: "
read config_path
fi
print_status "Parsing $config_path..." "info"
parse_wireguard_conf "$config_path"
print_status "Converting to Xray format..." "info"
convert_to_xray_outbound "Phobos"
echo -e "\n${BOLD}Outbound configuration:${RESET}"
echo "$OUTBOUND_JSON" | jq .
echo
local db_path="/etc/x-ui/x-ui.db"
if [[ ! -f "$db_path" ]]; then
print_status "Database not found at: $db_path" "warning"
db_path=$(find_database)
if [[ -z "$db_path" ]]; then
print_status "Database not found in standard locations" "error"
print_status "Make sure 3x-ui is installed" "info"
exit 1
fi
fi
systemctl stop x-ui 2>/dev/null
if ! import_via_database "$db_path"; then
systemctl start x-ui 2>/dev/null
print_status "Database import failed" "error"
exit 1
fi
local output_path="${config_path%.*}_3xui_outbound.json"
echo "$OUTBOUND_JSON" | jq . > "$output_path" 2>/dev/null
if [[ $? -eq 0 ]]; then
print_status "JSON saved to: $output_path" "success"
fi
echo
local restart_ok=0
restart_xui_service && restart_ok=1
echo
print_status "Import completed successfully!" "success"
echo
print_status "${BOLD}Info:${RESET}" "info"
print_status " Outbound 'Phobos' added to config" "info"
if [[ $restart_ok -eq 1 ]]; then
print_status " x-ui service restarted" "info"
else
print_status " x-ui service needs manual restart" "warning"
fi
print_status " JSON config saved to $output_path" "info"
echo
print_status "${BOLD}Verification:${RESET}" "info"
print_status " Open 3x-ui web panel" "info"
print_status " Go to Outbounds section" "info"
print_status " Verify 'Phobos' outbound is present" "info"
echo
}
main "$@"

View File

@@ -1,248 +0,0 @@
OBF_BINARY_NAME="wg-obfuscator"
OBF_CONF_NAME="wg-obfuscator.conf"
OBF_INIT_NAME="S49wg-obfuscator"
OBF_SERVICE_NAME="phobos-obfuscator"
OBF_WG_IFACE="phobos"
resolve_install_names() {
local binary_dir
local binary_path
if [ "$ROUTER_PLATFORM" = "openwrt" ]; then
binary_dir="/usr/bin"
elif [ "$ROUTER_PLATFORM" = "linux" ]; then
binary_dir="/usr/local/bin"
else
binary_dir="/opt/bin"
fi
binary_path="${binary_dir}/wg-obfuscator"
if [ ! -f "$binary_path" ] && [ ! -f "$PHOBOS_DIR/wg-obfuscator.conf" ]; then
return
fi
if [ -f "$binary_path" ] && [ -f "$PHOBOS_DIR/${CLIENT_NAME}.conf" ]; then
return
fi
local idx=1
while [ -f "${binary_dir}/wg-obfuscator${idx}" ]; do
idx=$((idx + 1))
done
local init_num=50
while [ -f "/opt/etc/init.d/S${init_num}wg-obfuscator" ]; do
init_num=$((init_num + 1))
done
OBF_BINARY_NAME="wg-obfuscator${idx}"
OBF_CONF_NAME="wg-obfuscator${idx}.conf"
OBF_INIT_NAME="S${init_num}wg-obfuscator"
OBF_SERVICE_NAME="phobos-obfuscator${idx}"
OBF_WG_IFACE="phobos${idx}"
}
install_obfuscator() {
local arch=$1
log "Установка wg-obfuscator для архитектуры $arch..."
if [ "$OBF_BINARY_NAME" = "wg-obfuscator" ]; then
if [ -f /opt/etc/init.d/S49wg-obfuscator ]; then
log "Остановка текущего процесса wg-obfuscator..."
/opt/etc/init.d/S49wg-obfuscator stop >/dev/null 2>&1 || true
fi
if [ -f /etc/init.d/phobos-obfuscator ]; then
log "Остановка текущего процесса wg-obfuscator..."
/etc/init.d/phobos-obfuscator stop >/dev/null 2>&1 || true
fi
fi
local binary_name="wg-obfuscator-$arch"
if [ ! -f "bin/$binary_name" ]; then
log "Ошибка: бинарник $binary_name не найден в архиве"
log "Доступные бинарники:"
ls -1 bin/
exit 1
fi
local target_path="/opt/bin/${OBF_BINARY_NAME}"
if [ "$ROUTER_PLATFORM" = "openwrt" ]; then
target_path="/usr/bin/${OBF_BINARY_NAME}"
fi
if [ -f "$target_path" ]; then
rm "$target_path"
fi
if [ "$ROUTER_PLATFORM" = "openwrt" ]; then
mkdir -p /usr/bin
else
mkdir -p /opt/bin
fi
cp "bin/$binary_name" "$target_path"
chmod +x "$target_path"
log "Бинарник wg-obfuscator установлен в $target_path"
}
create_init_script() {
log "Создание init-скрипта для obfuscator..."
cat > /opt/etc/init.d/${OBF_INIT_NAME} <<EOF
#!/bin/sh
ENABLED=yes
PROCS=${OBF_BINARY_NAME}
ARGS="--config $PHOBOS_DIR/${OBF_CONF_NAME}"
PREARGS=""
DESC=\$PROCS
PATH=/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
. /opt/etc/init.d/rc.func
EOF
chmod +x /opt/etc/init.d/${OBF_INIT_NAME}
log "Init-скрипт создан: /opt/etc/init.d/${OBF_INIT_NAME}"
}
start_obfuscator() {
log "Запуск wg-obfuscator..."
if [ "$ROUTER_PLATFORM" = "keenetic" ] || [ -f /opt/etc/init.d/${OBF_INIT_NAME} ]; then
/opt/etc/init.d/${OBF_INIT_NAME} start
elif [ "$ROUTER_PLATFORM" = "openwrt" ] && [ -f /etc/init.d/${OBF_SERVICE_NAME} ]; then
/etc/init.d/${OBF_SERVICE_NAME} start
/etc/init.d/${OBF_SERVICE_NAME} enable
fi
sleep 2
local process_found=0
if command -v pidof >/dev/null 2>&1 && pidof ${OBF_BINARY_NAME} >/dev/null 2>&1; then
process_found=1
elif command -v pgrep >/dev/null 2>&1 && pgrep -f wg-obfuscator >/dev/null 2>&1; then
process_found=1
elif ps w 2>/dev/null | grep -v grep | grep -q "wg-obfuscator\|/opt/bin/wg-obfuscator\|/usr/bin/wg-obfuscator"; then
process_found=1
elif ps 2>/dev/null | grep -v grep | grep -q "wg-obfuscator"; then
process_found=1
fi
if [ $process_found -eq 0 ]; then
if [ "$ROUTER_PLATFORM" = "keenetic" ] && [ -f /opt/etc/init.d/${OBF_INIT_NAME} ]; then
local status_output=$(/opt/etc/init.d/${OBF_INIT_NAME} status 2>&1 || echo "")
if echo "$status_output" | grep -q "alive"; then
process_found=1
fi
elif [ "$ROUTER_PLATFORM" = "openwrt" ] && [ -f /etc/init.d/${OBF_SERVICE_NAME} ]; then
if /etc/init.d/${OBF_SERVICE_NAME} status >/dev/null 2>&1; then
process_found=1
fi
fi
fi
if [ $process_found -eq 1 ]; then
log "✓ wg-obfuscator успешно запущен"
else
log "✗ wg-obfuscator не запущен. Проверьте логи."
fi
}
create_procd_init_script() {
log "Создание procd init-скрипта для obfuscator..."
cat > /etc/init.d/${OBF_SERVICE_NAME} <<EOF
#!/bin/sh /etc/rc.common
START=49
STOP=51
USE_PROCD=1
PROG=/usr/bin/${OBF_BINARY_NAME}
CONFIG_FILE=/etc/Phobos/${OBF_CONF_NAME}
start_service() {
if [ ! -f "\$PROG" ]; then
echo "Error: wg-obfuscator not found at \$PROG"
return 1
fi
if [ ! -f "\$CONFIG_FILE" ]; then
echo "Error: config not found at \$CONFIG_FILE"
return 1
fi
procd_open_instance
procd_set_param command \$PROG --config \$CONFIG_FILE
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
EOF
chmod +x /etc/init.d/${OBF_SERVICE_NAME}
log "Procd init-скрипт создан: /etc/init.d/${OBF_SERVICE_NAME}"
}
create_systemd_obfuscator_service() {
log "Создание systemd service для obfuscator..."
cat > /etc/systemd/system/${OBF_SERVICE_NAME}.service <<EOFS
[Unit]
Description=Phobos WireGuard Obfuscator
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/${OBF_BINARY_NAME} --config $PHOBOS_DIR/${OBF_CONF_NAME}
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOFS
if [ ! -f /usr/local/bin/${OBF_BINARY_NAME} ]; then
local target_path="/usr/local/bin/${OBF_BINARY_NAME}"
if [ -f /opt/bin/${OBF_BINARY_NAME} ]; then
cp /opt/bin/${OBF_BINARY_NAME} "$target_path"
chmod +x "$target_path"
elif [ -f /usr/bin/${OBF_BINARY_NAME} ]; then
cp /usr/bin/${OBF_BINARY_NAME} "$target_path"
chmod +x "$target_path"
fi
fi
systemctl daemon-reload
systemctl enable ${OBF_SERVICE_NAME}
systemctl start ${OBF_SERVICE_NAME}
log "Ожидание запуска obfuscator (до 10 секунд)..."
local wait_count=0
while [ $wait_count -lt 10 ]; do
sleep 1
if systemctl is-active --quiet ${OBF_SERVICE_NAME}; then
log "✓ Obfuscator успешно запущен"
break
fi
wait_count=$((wait_count + 1))
done
if [ $wait_count -ge 10 ]; then
log "ПРЕДУПРЕЖДЕНИЕ: Obfuscator может быть не готов (таймаут 10 сек)"
fi
log "Systemd service создан: /etc/systemd/system/${OBF_SERVICE_NAME}.service"
}

View File

@@ -1,674 +0,0 @@
#!/bin/sh
set -e
CLIENT_NAME="{{CLIENT_NAME}}"
PHOBOS_DIR=""
ROUTER_PLATFORM=""
. "$(dirname "$0")/lib-client.sh"
. "$(dirname "$0")/install-obfuscator.sh"
. "$(dirname "$0")/install-wireguard.sh"
detect_3xui_mode() {
local platform="$1"
if [ "$platform" = "linux" ] && [ -f /etc/x-ui/x-ui.db ]; then
echo "true"
else
echo "false"
fi
}
check_dependencies() {
local platform="$1"
local missing=""
local base_deps="grep cut date tee tar curl jq"
local platform_deps=""
if [ "$platform" = "openwrt" ]; then
platform_deps="uci"
fi
for cmd in $base_deps $platform_deps; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing="$missing $cmd"
fi
done
if [ -n "$missing" ]; then
log "ВНИМАНИЕ: Отсутствуют необходимые утилиты:$missing"
if [ "$platform" = "linux" ]; then
log "Устанавливаю недостающие пакеты через apt-get..."
if command -v apt-get >/dev/null 2>&1; then
if ! apt-get update; then
log "ОШИБКА: Не удалось обновить список пакетов apt-get"
return 1
fi
for cmd in $missing; do
log "Установка $cmd..."
if ! apt-get install -y "$cmd" 2>/dev/null; then
log "ПРЕДУПРЕЖДЕНИЕ: Не удалось установить $cmd через apt-get"
fi
done
else
log "ОШИБКА: apt-get не найден. Невозможно установить зависимости."
return 1
fi
else
log "Устанавливаю недостающие пакеты через opkg..."
if command -v opkg >/dev/null 2>&1; then
if ! opkg update; then
log "ОШИБКА: Не удалось обновить список пакетов opkg"
return 1
fi
for cmd in $missing; do
log "Установка $cmd..."
if ! opkg install "$cmd" 2>/dev/null; then
log "ПРЕДУПРЕЖДЕНИЕ: Не удалось установить $cmd через opkg"
fi
done
else
log "ОШИБКА: opkg не найден. Невозможно установить зависимости."
return 1
fi
fi
fi
return 0
}
setup_configs() {
log "Настройка конфигураций..."
mkdir -p "$PHOBOS_DIR"
cp wg-obfuscator.conf "$PHOBOS_DIR/${OBF_CONF_NAME}"
chmod 600 "$PHOBOS_DIR/${OBF_CONF_NAME}"
cp "${CLIENT_NAME}.conf" "$PHOBOS_DIR/${CLIENT_NAME}.conf"
chmod 600 "$PHOBOS_DIR/${CLIENT_NAME}.conf"
printf '%s' "${CLIENT_NAME}" > "$PHOBOS_DIR/${OBF_CONF_NAME%.conf}.link"
if [ "$OBF_BINARY_NAME" != "wg-obfuscator" ]; then
local used_ports=""
for existing_conf in "$PHOBOS_DIR"/wg-obfuscator*.conf; do
[ -f "$existing_conf" ] || continue
[ "$(basename "$existing_conf")" = "${OBF_CONF_NAME}" ] && continue
local port
port=$(grep 'source-lport' "$existing_conf" 2>/dev/null | tr -d ' ' | cut -d'=' -f2)
[ -n "$port" ] && used_ports="${used_ports} ${port}"
done
local new_port=13255
local port_taken=1
while [ "$port_taken" -eq 1 ]; do
port_taken=0
for p in $used_ports; do
if [ "$p" = "$new_port" ]; then
port_taken=1
new_port=$((new_port + 1))
break
fi
done
done
sed -i "s/^source-lport = [0-9]*/source-lport = ${new_port}/" "$PHOBOS_DIR/${OBF_CONF_NAME}"
sed -i "s/^Endpoint = 127\.0\.0\.1:[0-9]*/Endpoint = 127.0.0.1:${new_port}/" "$PHOBOS_DIR/${CLIENT_NAME}.conf"
log " Назначен локальный порт obfuscator: $new_port"
fi
log "Конфигурации установлены:"
log " - Obfuscator: $PHOBOS_DIR/${OBF_CONF_NAME}"
log " - WireGuard: $PHOBOS_DIR/${CLIENT_NAME}.conf"
}
deploy_lib_client() {
if [ -f "lib-client.sh" ]; then
cp "lib-client.sh" "$PHOBOS_DIR/lib-client.sh"
fi
}
deploy_uninstall_script() {
log "Развертывание скрипта удаления Phobos..."
if [ -f "phobos-uninstall.sh" ]; then
cp "phobos-uninstall.sh" "$PHOBOS_DIR/phobos-uninstall.sh"
chmod +x "$PHOBOS_DIR/phobos-uninstall.sh"
log "Скрипт удаления установлен: $PHOBOS_DIR/phobos-uninstall.sh"
else
log "ПРЕДУПРЕЖДЕНИЕ: phobos-uninstall.sh не найден в архиве"
fi
}
deploy_3xui_script() {
log "Развертывание скрипта 3xui.sh..."
if [ -f "3xui.sh" ]; then
cp "3xui.sh" "$PHOBOS_DIR/3xui.sh"
chmod +x "$PHOBOS_DIR/3xui.sh"
log "Скрипт 3xui.sh установлен: $PHOBOS_DIR/3xui.sh"
else
log "ПРЕДУПРЕЖДЕНИЕ: 3xui.sh не найден в архиве"
fi
}
run_3xui_integration() {
log ""
log "==> Интеграция WireGuard конфигурации в 3x-ui..."
if [ ! -f "$PHOBOS_DIR/3xui.sh" ]; then
log "ОШИБКА: Скрипт $PHOBOS_DIR/3xui.sh не найден"
return 1
fi
for cmd in jq sqlite3; do
if ! command -v "$cmd" >/dev/null 2>&1; then
log "Устанавливаю $cmd..."
if command -v apt-get >/dev/null 2>&1; then
apt-get update -qq && apt-get install -y -qq "$cmd" >/dev/null 2>&1
elif command -v yum >/dev/null 2>&1; then
yum install -y -q "${cmd/sqlite3/sqlite}" >/dev/null 2>&1
elif command -v dnf >/dev/null 2>&1; then
dnf install -y -q "${cmd/sqlite3/sqlite}" >/dev/null 2>&1
elif command -v apk >/dev/null 2>&1; then
apk add --quiet "${cmd/sqlite3/sqlite}" >/dev/null 2>&1
fi
if ! command -v "$cmd" >/dev/null 2>&1; then
log "ОШИБКА: не удалось установить $cmd"
return 1
fi
fi
done
local wg_config="$PHOBOS_DIR/${CLIENT_NAME}.conf"
if [ ! -f "$wg_config" ]; then
log "ОШИБКА: Конфигурация WireGuard не найдена: $wg_config"
return 1
fi
log "Запуск интеграции: $PHOBOS_DIR/3xui.sh $wg_config"
if "$PHOBOS_DIR/3xui.sh" "$wg_config"; then
log "[OK] WireGuard конфигурация успешно интегрирована в 3x-ui"
log "[OK] Outbound 'Phobos' добавлен в конфигурацию 3x-ui"
return 0
else
log "ОШИБКА: Не удалось интегрировать конфигурацию в 3x-ui"
return 1
fi
}
cleanup_3xui_script() {
local keep_script="$1"
if [ "$keep_script" = "true" ]; then
log "Скрипт 3xui.sh сохранен для использования в 3x-ui режиме"
else
if [ -f "$PHOBOS_DIR/3xui.sh" ]; then
rm -f "$PHOBOS_DIR/3xui.sh"
log "Скрипт 3xui.sh удален (не требуется в обычном режиме)"
fi
fi
}
install_wireguard_openwrt() {
log "Установка пакетов WireGuard для OpenWRT..."
if ! command -v wg >/dev/null 2>&1; then
log "Установка kmod-wireguard и wireguard-tools..."
opkg update
opkg install kmod-wireguard wireguard-tools luci-proto-wireguard
else
log "WireGuard уже установлен"
fi
log "Установка luci-app-wireguard для веб-интерфейса..."
opkg install luci-app-wireguard >/dev/null 2>&1 || log "ПРЕДУПРЕЖДЕНИЕ: luci-app-wireguard не удалось установить"
log "✓ Пакеты WireGuard установлены"
}
configure_wireguard_openwrt() {
log ""
log "==> Автоматическая настройка WireGuard через UCI..."
extract_wireguard_params
if [ ! -f "./router-configure-wireguard-openwrt.sh" ]; then
log "⚠ Скрипт router-configure-wireguard-openwrt.sh не найден"
log " Используйте ручную настройку через LuCI или UCI"
return 1
fi
chmod +x ./router-configure-wireguard-openwrt.sh
if ./router-configure-wireguard-openwrt.sh \
--client-name "$CLIENT_NAME" \
--client-private-key "$WG_PRIVATE_KEY" \
--client-ip "$CLIENT_IP" \
--client-ipv6 "$CLIENT_IPV6" \
--server-public-key "$WG_SERVER_PUBKEY" \
--endpoint-port "$WG_ENDPOINT_PORT" \
--keepalive 25 \
--mtu 1420 \
--fallback-config "$PHOBOS_DIR/${CLIENT_NAME}.conf"; then
return 0
else
return 1
fi
}
extract_wireguard_params() {
log "Извлечение параметров WireGuard из конфигурации..."
local config_file="$PHOBOS_DIR/${CLIENT_NAME}.conf"
WG_PRIVATE_KEY=$(grep '^PrivateKey' "$config_file" | cut -d'=' -f2- | tr -d ' \t\n\r')
WG_ADDRESS=$(grep '^Address' "$config_file" | cut -d'=' -f2- | tr -d ' \t\n\r')
WG_SERVER_PUBKEY=$(grep '^PublicKey' "$config_file" | cut -d'=' -f2- | tr -d ' \t\n\r')
WG_ENDPOINT_PORT=$(grep '^Endpoint' "$config_file" | cut -d':' -f2 | tr -d ' \t\n\r')
CLIENT_IP=$(echo "$WG_ADDRESS" | cut -d',' -f1 | tr -d ' ')
CLIENT_IPV6=$(echo "$WG_ADDRESS" | cut -d',' -f2 | tr -d ' ')
if [ -z "$CLIENT_IPV6" ] || [ "$CLIENT_IPV6" = "$CLIENT_IP" ]; then
CLIENT_IPV6=$(grep -A 10 '\[Interface\]' "$config_file" | grep '^Address' | grep -o 'fd[0-9a-f:\/]*' | head -1)
fi
if [ -z "$CLIENT_IPV6" ]; then
CLIENT_IPV6="none"
fi
if [ -z "$WG_ENDPOINT_PORT" ]; then
WG_ENDPOINT_PORT=13255
fi
log " Private Key: ${WG_PRIVATE_KEY:0:20}..."
log " IPv4: $CLIENT_IP"
log " IPv6: $CLIENT_IPV6"
log " Server PubKey: ${WG_SERVER_PUBKEY:0:20}..."
log " Endpoint port: $WG_ENDPOINT_PORT"
}
configure_wireguard_rci() {
log ""
log "==> Автоматическая настройка WireGuard через RCI API..."
extract_wireguard_params
if [ ! -f "./router-configure-wireguard.sh" ]; then
log "⚠ Скрипт router-configure-wireguard.sh не найден"
log " Используйте ручной импорт (см. инструкции ниже)"
return 1
fi
chmod +x ./router-configure-wireguard.sh
if ./router-configure-wireguard.sh \
--client-name "$CLIENT_NAME" \
--client-private-key "$WG_PRIVATE_KEY" \
--client-ip "$CLIENT_IP" \
--client-ipv6 "$CLIENT_IPV6" \
--server-public-key "$WG_SERVER_PUBKEY" \
--endpoint-port "$WG_ENDPOINT_PORT" \
--keepalive 25 \
--mtu 1420 \
--fallback-config "$PHOBOS_DIR/${CLIENT_NAME}.conf"; then
return 0
else
return 1
fi
}
show_manual_instructions() {
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ Требуется ручной импорт WireGuard конфигурации ║"
log "╚════════════════════════════════════════════════════════════╝"
log ""
log "Выполните следующие шаги:"
log ""
if [ "$ROUTER_PLATFORM" = "keenetic" ]; then
log "1. Откройте веб-панель администрирования роутера Keenetic"
log " (http://192.168.1.1 или http://my.keenetic.net)"
log ""
log "2. Перейдите в раздел: Интернет → Другие подключения"
log ""
log "3. Выберите 'Загрузить конфигурацию из файла' в разделе 'WireGuard'"
log ""
log "4. Укажите путь к файлу:"
log " $PHOBOS_DIR/${CLIENT_NAME}.conf"
log ""
log "5. Активируйте подключение"
elif [ "$ROUTER_PLATFORM" = "openwrt" ]; then
log "1. Откройте веб-интерфейс LuCI роутера OpenWRT"
log " (обычно http://192.168.1.1)"
log ""
log "2. Перейдите в: Network → Interfaces"
log ""
log "3. Создайте новый интерфейс с протоколом WireGuard"
log ""
log "4. Используйте параметры из файла:"
log " $PHOBOS_DIR/${CLIENT_NAME}.conf"
log ""
log "5. Настройте файрволл зону для интерфейса"
fi
log ""
}
show_final_info() {
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ Информация об установке ║"
log "╚════════════════════════════════════════════════════════════╝"
log ""
log "Файлы установки:"
if [ "$ROUTER_PLATFORM" = "keenetic" ]; then
log " Бинарник: /opt/bin/${OBF_BINARY_NAME}"
elif [ "$ROUTER_PLATFORM" = "openwrt" ]; then
log " Бинарник: /usr/bin/${OBF_BINARY_NAME}"
elif [ "$ROUTER_PLATFORM" = "linux" ]; then
log " Бинарник: /usr/local/bin/${OBF_BINARY_NAME}"
fi
log " Конфиг obfuscator: $PHOBOS_DIR/${OBF_CONF_NAME}"
log " Конфиг WireGuard: $PHOBOS_DIR/${CLIENT_NAME}.conf"
if [ "$ROUTER_PLATFORM" = "keenetic" ] || [ -f /opt/etc/init.d/${OBF_INIT_NAME} ]; then
log " Init-скрипт: /opt/etc/init.d/${OBF_INIT_NAME}"
elif [ "$ROUTER_PLATFORM" = "openwrt" ] && [ -f /etc/init.d/${OBF_SERVICE_NAME} ]; then
log " Init-скрипт: /etc/init.d/${OBF_SERVICE_NAME}"
elif [ "$ROUTER_PLATFORM" = "linux" ]; then
log " Systemd service: /etc/systemd/system/${OBF_SERVICE_NAME}.service"
log " WireGuard конфиг: /etc/wireguard/${OBF_WG_IFACE}.conf"
fi
log " Uninstall: $PHOBOS_DIR/phobos-uninstall.sh"
log " Health monitor: $PHOBOS_DIR/phobos-health.sh"
log " Failover config: $PHOBOS_DIR/failover.conf"
log ""
log "Управление:"
if [ "$ROUTER_PLATFORM" = "keenetic" ] || [ -f /opt/etc/init.d/${OBF_INIT_NAME} ]; then
log " /opt/etc/init.d/${OBF_INIT_NAME} status # Проверить что obfuscator запущен"
elif [ "$ROUTER_PLATFORM" = "openwrt" ] && [ -f /etc/init.d/${OBF_SERVICE_NAME} ]; then
log " /etc/init.d/${OBF_SERVICE_NAME} status # Проверить что obfuscator запущен"
elif [ "$ROUTER_PLATFORM" = "linux" ]; then
log " systemctl status ${OBF_SERVICE_NAME} # Проверить что obfuscator запущен"
log " systemctl status wg-quick@${OBF_WG_IFACE} # Проверить что WireGuard запущен"
fi
log " $PHOBOS_DIR/phobos-uninstall.sh # Удалить Phobos"
log ""
}
deploy_health_monitor() {
log "Развертывание монитора здоровья Phobos..."
if [ ! -f "phobos-health.sh" ]; then
log "ПРЕДУПРЕЖДЕНИЕ: phobos-health.sh не найден в архиве"
return 0
fi
cp "phobos-health.sh" "$PHOBOS_DIR/phobos-health.sh"
chmod +x "$PHOBOS_DIR/phobos-health.sh"
if [ -f "failover.conf" ]; then
cp "failover.conf" "$PHOBOS_DIR/failover.conf"
chmod 600 "$PHOBOS_DIR/failover.conf"
fi
mkdir -p "$PHOBOS_DIR/state"
# --- Phobos pull agent: panel -> router config sync (NAT-friendly) ---
# Routers behind NAT cannot be reached by the panel over SSH, so they PULL
# their failover.conf over HTTP. Ships client_id + pull_token from the package.
if [ -f "phobos-pull.sh" ]; then
cp "phobos-pull.sh" "$PHOBOS_DIR/phobos-pull.sh"
chmod +x "$PHOBOS_DIR/phobos-pull.sh"
echo "{{CLIENT_NAME}}" > "$PHOBOS_DIR/client_id"
[ -f "pull_token" ] && cp "pull_token" "$PHOBOS_DIR/pull_token"
[ -f "panel_tunnel_ip" ] && cp "panel_tunnel_ip" "$PHOBOS_DIR/panel_tunnel_ip"
[ -f "panel_port" ] && cp "panel_port" "$PHOBOS_DIR/panel_port"
[ -f "panel_scheme" ] && cp "panel_scheme" "$PHOBOS_DIR/panel_scheme"
[ -f "panel_url" ] && cp "panel_url" "$PHOBOS_DIR/panel_url"
[ -f "panel_urls" ] && cp "panel_urls" "$PHOBOS_DIR/panel_urls"
log " Pull-агент установлен (client_id={{CLIENT_NAME}})"
fi
local cron_line="*/1 * * * * $PHOBOS_DIR/phobos-health.sh"
local pull_line="*/1 * * * * $PHOBOS_DIR/phobos-pull.sh"
[ -f "$PHOBOS_DIR/phobos-pull.sh" ] || pull_line=""
if [ "$ROUTER_PLATFORM" = "keenetic" ]; then
# Install cron if not present (Entware)
if ! command -v crontab >/dev/null 2>&1 && command -v opkg >/dev/null 2>&1; then
log " Установка cron через opkg..."
opkg update >/dev/null 2>&1
opkg install cron >/dev/null 2>&1
/opt/etc/init.d/S10cron start >/dev/null 2>&1 || true
fi
local cron_file="/opt/etc/crontab"
# Create crontab file if missing
if [ ! -f "$cron_file" ]; then
mkdir -p /opt/etc
touch "$cron_file"
fi
if ! grep -q "phobos-health" "$cron_file" 2>/dev/null; then
echo "$cron_line" >> "$cron_file"
log " Cron (health) добавлен в $cron_file"
fi
if [ -n "$pull_line" ] && ! grep -q "phobos-pull" "$cron_file" 2>/dev/null; then
echo "$pull_line" >> "$cron_file"
log " Cron (pull) добавлен в $cron_file"
fi
/opt/etc/init.d/S10cron restart >/dev/null 2>&1 || true
elif [ "$ROUTER_PLATFORM" = "openwrt" ]; then
(crontab -l 2>/dev/null | grep -v "phobos-health" | grep -v "phobos-pull"; echo "$cron_line"; [ -n "$pull_line" ] && echo "$pull_line") | crontab -
log " Cron добавлен через crontab (health + pull)"
elif [ "$ROUTER_PLATFORM" = "linux" ]; then
{ echo "$cron_line"; [ -n "$pull_line" ] && echo "$pull_line"; } > /etc/cron.d/phobos-health
chmod 644 /etc/cron.d/phobos-health
log " Cron добавлен в /etc/cron.d/phobos-health (health + pull)"
fi
log " Монитор здоровья: $PHOBOS_DIR/phobos-health.sh"
log " Failover конфиг: $PHOBOS_DIR/failover.conf"
log " Проверка каждую минуту через cron"
}
main() {
ROUTER_PLATFORM=$(detect_router_platform)
PHOBOS_DIR=$(detect_phobos_dir "$ROUTER_PLATFORM")
IS_3XUI_MODE=$(detect_3xui_mode "$ROUTER_PLATFORM")
log "==> Определена платформа: $ROUTER_PLATFORM"
log "==> Директория Phobos: $PHOBOS_DIR"
resolve_install_names
log "==> Режим установки obfuscator: binary=${OBF_BINARY_NAME}, conf=${OBF_CONF_NAME}, init=${OBF_INIT_NAME}, service=${OBF_SERVICE_NAME}"
if [ "$IS_3XUI_MODE" = "true" ]; then
log "==> Обнаружен режим установки: 3x-ui"
log "==> В этом режиме WireGuard не устанавливается"
log "==> Будет развернут только wg-obfuscator и интеграция с 3x-ui"
fi
if [ "$ROUTER_PLATFORM" = "unknown" ]; then
log "ОШИБКА: Неподдерживаемая платформа"
log "Вывод uname -a: $(uname -a)"
log ""
log "Поддерживаемые платформы:"
log " - Keenetic/Netcraze (определяется по 'Keenetic' или 'Netcraze' в uname)"
log " - OpenWRT (определяется по 'OpenWrt', 'LEDE' или 'ImmortalWrt' в uname)"
log " - Linux (Ubuntu/Debian)"
exit 1
fi
mkdir -p "$PHOBOS_DIR"
log "==> Начало установки Phobos на роутер $ROUTER_PLATFORM"
log "==> Клиент: $CLIENT_NAME"
check_root
if ! check_dependencies "$ROUTER_PLATFORM"; then
log "ОШИБКА: Не удалось установить зависимости"
exit 1
fi
ARCH=$(detect_arch)
log "Определена архитектура: $ARCH"
if [ "$ARCH" = "unknown" ]; then
log "Ошибка: неподдерживаемая архитектура"
log "Вывод uname -m: $(uname -m)"
exit 1
fi
install_obfuscator "$ARCH"
setup_configs
deploy_lib_client
deploy_uninstall_script
deploy_health_monitor
if [ "$IS_3XUI_MODE" = "true" ]; then
deploy_3xui_script
fi
if [ "$ROUTER_PLATFORM" = "keenetic" ]; then
create_init_script
start_obfuscator
if configure_wireguard_rci; then
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ ✓ Установка завершена успешно! ║"
log "║ ✓ WireGuard настроен автоматически через RCI API ║"
log "╚════════════════════════════════════════════════════════════╝"
show_final_info
else
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ ✓ Obfuscator установлен успешно ║"
log "║ ⚠ WireGuard требует ручной настройки ║"
log "╚════════════════════════════════════════════════════════════╝"
show_manual_instructions
show_final_info
fi
elif [ "$ROUTER_PLATFORM" = "openwrt" ]; then
install_wireguard_openwrt
if [ -d "/opt/etc" ]; then
create_init_script
else
create_procd_init_script
fi
start_obfuscator
if configure_wireguard_openwrt; then
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ ✓ Установка завершена успешно! ║"
log "║ ✓ WireGuard настроен автоматически через UCI ║"
log "╚════════════════════════════════════════════════════════════╝"
show_final_info
else
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ ✓ Obfuscator установлен успешно ║"
log "║ ⚠ WireGuard требует ручной настройки ║"
log "╚════════════════════════════════════════════════════════════╝"
show_manual_instructions
show_final_info
fi
elif [ "$ROUTER_PLATFORM" = "linux" ]; then
if [ "$IS_3XUI_MODE" = "true" ]; then
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ Режим установки: 3x-ui ║"
log "╚════════════════════════════════════════════════════════════╝"
log ""
create_systemd_obfuscator_service
if run_3xui_integration; then
cleanup_3xui_script "true"
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ ✓ Установка в режиме 3x-ui завершена успешно! ║"
log "║ ✓ Obfuscator настроен через systemd ║"
log "║ ✓ Outbound 'Phobos' добавлен в конфигурацию 3x-ui ║"
log "╚════════════════════════════════════════════════════════════╝"
log ""
log "Файлы установки:"
log " Бинарник: /usr/local/bin/wg-obfuscator"
log " Конфиг obfuscator: $PHOBOS_DIR/wg-obfuscator.conf"
log " Конфиг WireGuard: $PHOBOS_DIR/${CLIENT_NAME}.conf"
log " Скрипт 3xui.sh: $PHOBOS_DIR/3xui.sh"
log " Systemd service: /etc/systemd/system/phobos-obfuscator.service"
log ""
log "Управление:"
log " systemctl status phobos-obfuscator # Проверить что obfuscator запущен"
log ""
log "Примечание:"
log " WireGuard управляется через 3x-ui панель"
log " Скрипт 3xui.sh сохранен для повторной интеграции при необходимости"
log ""
else
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ ✗ Установка в режиме 3x-ui завершена с ошибками ║"
log "╚════════════════════════════════════════════════════════════╝"
exit 1
fi
else
install_wireguard_linux || {
log "ОШИБКА: Не удалось установить WireGuard"
exit 1
}
create_systemd_obfuscator_service
if configure_wireguard_linux; then
configure_ufw_linux
cleanup_3xui_script "false"
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ ✓ Установка завершена успешно! ║"
log "║ ✓ WireGuard и Obfuscator настроены через systemd ║"
log "╚════════════════════════════════════════════════════════════╝"
show_final_info
else
cleanup_3xui_script "false"
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ ⚠ Установка завершена с предупреждениями ║"
log "║ ⚠ Проверьте статус служб вручную ║"
log "╚════════════════════════════════════════════════════════════╝"
show_final_info
fi
fi
fi
log "==> Установка завершена."
}
main "$@"

View File

@@ -1,94 +0,0 @@
install_wireguard_linux() {
log "Установка WireGuard для Linux..."
if command -v apt-get >/dev/null 2>&1; then
apt-get update || log "ПРЕДУПРЕЖДЕНИЕ: Не удалось обновить список пакетов"
apt-get install -y wireguard wireguard-tools resolvconf net-tools || {
log "ОШИБКА: Не удалось установить WireGuard"
return 1
}
else
log "ОШИБКА: apt-get не найден. Поддерживаются только Ubuntu/Debian"
return 1
fi
log "✓ WireGuard установлен"
return 0
}
configure_wireguard_linux() {
log ""
log "==> Настройка WireGuard через systemd..."
local wg_interface="${OBF_WG_IFACE}"
local wg_config_dir="/etc/wireguard"
mkdir -p "$wg_config_dir"
cp "$PHOBOS_DIR/${CLIENT_NAME}.conf" "$wg_config_dir/${wg_interface}.conf"
chmod 600 "$wg_config_dir/${wg_interface}.conf"
log "Настройка routing: VPN как запасной интерфейс..."
local client_ip=$(grep '^Address' "$wg_config_dir/${wg_interface}.conf" | cut -d'=' -f2 | tr -d ' ' | cut -d',' -f1 | cut -d'/' -f1)
local route_target=$(echo "$client_ip" | cut -d'.' -f1-2).0.0/16
sed -i '/^MTU/a Table = off' "$wg_config_dir/${wg_interface}.conf"
sed -i "/^Table = off/a PostUp = ip route add $route_target dev %i" "$wg_config_dir/${wg_interface}.conf"
sed -i "/^PostUp/a PostDown = ip route del $route_target dev %i || true" "$wg_config_dir/${wg_interface}.conf"
log "Настройка зависимости WireGuard от obfuscator..."
mkdir -p "/etc/systemd/system/wg-quick@${wg_interface}.service.d"
cat > "/etc/systemd/system/wg-quick@${wg_interface}.service.d/override.conf" <<EOF
[Unit]
After=${OBF_SERVICE_NAME}.service
Requires=${OBF_SERVICE_NAME}.service
EOF
systemctl daemon-reload
log "Включение и запуск WireGuard интерфейса ${wg_interface}..."
systemctl enable wg-quick@${wg_interface} || log "ПРЕДУПРЕЖДЕНИЕ: Не удалось включить автозапуск"
systemctl start wg-quick@${wg_interface} || {
log "ОШИБКА: Не удалось запустить WireGuard"
return 1
}
sleep 3
if systemctl is-active --quiet wg-quick@${wg_interface}; then
log "✓ WireGuard успешно запущен (интерфейс: ${wg_interface})"
log " VPN настроен как запасной интерфейс (не перехватывает системный трафик)"
if wg show ${wg_interface} 2>/dev/null | grep -q "latest handshake"; then
log "✓ Туннель установлен и работает"
else
log " Ожидание установки туннеля..."
fi
return 0
else
log "ПРЕДУПРЕЖДЕНИЕ: WireGuard может быть не запущен, проверьте статус"
return 1
fi
}
configure_ufw_linux() {
log ""
log "==> Проверка UFW firewall..."
if ! command -v ufw >/dev/null 2>&1; then
log "UFW не установлен, пропуск настройки"
return 0
fi
log "✓ UFW обнаружен"
log " Клиентская конфигурация не требует открытия портов:"
log " - obfuscator слушает на 127.0.0.1:13255 (локальный интерфейс)"
log " - obfuscator делает исходящие соединения (разрешены по умолчанию)"
if ufw status 2>/dev/null | grep -q "Status: active"; then
log " UFW активен, клиент будет работать корректно"
else
log " UFW неактивен"
fi
}

View File

@@ -1,93 +0,0 @@
detect_router_platform() {
if [ -f /etc/openwrt_release ] || [ -f /etc/openwrt_version ]; then
echo "openwrt"
elif [ -f /opt/etc/.entware_install_log ] || [ -d /opt/etc/ndm ]; then
echo "keenetic"
elif [ -f /etc/debian_version ]; then
echo "linux"
elif [ -d /etc/config ] && command -v uci >/dev/null 2>&1; then
echo "openwrt"
elif [ -f /opt/etc/init.d/rc.func ]; then
echo "keenetic"
elif [ -d /run/systemd/system ] || pidof systemd >/dev/null 2>&1; then
echo "linux"
else
local uname_output=$(uname -a)
if echo "$uname_output" | grep -qi "Keenetic\|Netcraze"; then
echo "keenetic"
elif echo "$uname_output" | grep -qi "OpenWrt\|LEDE\|ImmortalWrt"; then
echo "openwrt"
elif echo "$uname_output" | grep -qi "Linux"; then
echo "linux"
else
echo "unknown"
fi
fi
}
detect_phobos_dir() {
local platform="$1"
if [ "$platform" = "keenetic" ]; then
echo "/opt/etc/Phobos"
elif [ "$platform" = "openwrt" ]; then
echo "/etc/Phobos"
elif [ "$platform" = "linux" ]; then
echo "/opt/Phobos"
else
if [ -d "/opt" ] && [ -d "/opt/etc" ]; then
echo "/opt/etc/Phobos"
else
echo "/etc/Phobos"
fi
fi
}
detect_arch() {
local arch=$(uname -m)
case "$arch" in
mips)
if [ "$(echo -n I | hexdump -o 2>/dev/null | awk 'NR==1{print $2}')" = "000111" ]; then
echo "mipsel"
else
echo "mips"
fi
;;
aarch64|arm64)
echo "aarch64"
;;
armv7l|armv6l)
echo "armv7"
;;
x86_64)
echo "x86_64"
;;
*)
echo "unknown"
;;
esac
}
check_root() {
if [ "$(id -u)" -ne 0 ]; then
echo "Этот скрипт требует root привилегии. Запустите: su -c '$0'"
exit 1
fi
}
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
print_status() {
local status="$1"
local message="$2"
if [ "${status}" = "OK" ]; then
printf "\\033[0;32m✓\\033[0m %s\n" "${message}"
elif [ "${status}" = "WARN" ]; then
printf "\\033[1;33m⚠\\033[0m %s\n" "${message}"
else
printf "\\033[0;31m✗\\033[0m %s\n" "${message}"
fi
}

View File

@@ -1,68 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
PHOBOS_DIR="${PHOBOS_DIR:-/opt/Phobos}"
SERVER_ENV="${SERVER_ENV:-$PHOBOS_DIR/server/server.env}"
CLIENTS_DIR="${CLIENTS_DIR:-$PHOBOS_DIR/clients}"
PACKAGES_DIR="${PACKAGES_DIR:-$PHOBOS_DIR/packages}"
WWW_DIR="${WWW_DIR:-$PHOBOS_DIR/www}"
TOKENS_DIR="${TOKENS_DIR:-$PHOBOS_DIR/tokens}"
TOKENS_FILE="${TOKENS_FILE:-$TOKENS_DIR/tokens.json}"
REPO_DIR="${REPO_DIR:-$PHOBOS_DIR/repo}"
WG_CONFIG="${WG_CONFIG:-/etc/wireguard/wg0.conf}"
TOKEN_TTL="${TOKEN_TTL:-86400}"
CLIENT_WG_PORT="${CLIENT_WG_PORT:-13255}"
log_info() { echo "[INFO] $*"; }
log_warn() { echo "[WARN] $*" >&2; }
log_success() { echo "[OK] $*"; }
die() { echo "[ERROR] $*" >&2; exit 1; }
check_root() {
[ "$(id -u)" = "0" ] || die "run as root"
}
load_env() {
if [ -f "$SERVER_ENV" ]; then
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|\#*) continue ;;
esac
k="${line%%=*}"
v="${line#*=}"
case "$k" in
''|\#*) continue ;;
esac
export "$k=$v"
done < "$SERVER_ENV"
fi
if [ -z "${SERVER_PUBLIC_IP_V4:-}" ]; then
SERVER_PUBLIC_IP_V4="$(curl -4 -fsS --max-time 6 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}')"
export SERVER_PUBLIC_IP_V4
fi
if [ -z "${SERVER_WG_PUBLIC_KEY:-}" ]; then
SERVER_WG_PUBLIC_KEY="$(wg show wg0 public-key 2>/dev/null || true)"
export SERVER_WG_PUBLIC_KEY
fi
if [ -n "${OBFUSCATOR_PORTS:-}" ]; then
OBFUSCATOR_PORT="$(echo "$OBFUSCATOR_PORTS" | cut -d',' -f1 | tr -d ' ')"
export OBFUSCATOR_PORT
fi
}
ensure_dirs() {
mkdir -p \
"$CLIENTS_DIR" \
"$PACKAGES_DIR" \
"$WWW_DIR/init" \
"$WWW_DIR/packages" \
"$TOKENS_DIR" \
"$REPO_DIR/server/scripts" \
"$REPO_DIR/client/templates"
[ -f "$TOKENS_FILE" ] || echo "[]" > "$TOKENS_FILE"
}

View File

@@ -1,485 +0,0 @@
#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/lib-core.sh"
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:-}"
resolve_client() {
local name="$1"
local id=$(echo "$name" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
if [[ -d "$CLIENTS_DIR/$id" ]]; then
echo "$id"
return 0
fi
return 1
}
action_add() {
local name="$CLIENT_ARG"
local manual_ip="$EXTRA_ARG"
if [[ -z "$name" ]]; then die "Использование: $0 add <client_name> [ip]"; fi
local id=$(echo "$name" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
local dir="$CLIENTS_DIR/$id"
if [[ -d "$dir" ]]; then die "Клиент $id уже существует."; fi
if [[ -z "$SERVER_WG_PUBLIC_KEY" ]]; then
die "Публичный ключ сервера не найден в server.env. Запустите установку."
fi
local server_pub="$SERVER_WG_PUBLIC_KEY"
local server_ip_v4="${SERVER_PUBLIC_IP_V4:-}"
local server_ip_v6="${SERVER_PUBLIC_IP_V6:-}"
local server_wg_network="${SERVER_WG_IPV4_NETWORK:-10.25.0.0/16}"
local client_ip_v4="$manual_ip"
local ipv6_prefix_main=$(echo "${SERVER_WG_IPV6_NETWORK:-fd00:10:25::/48}" | cut -d'/' -f1 | sed 's/::.*//')
if [[ -z "$client_ip_v4" ]]; then
log_info "Поиск свободного IP..."
local used_ips=""
for d in "$CLIENTS_DIR"/*; do
if [[ -d "$d" ]] && [[ -f "$d/metadata.json" ]]; then
local ip=$(jq -r '.tunnel_ip_v4 // empty' "$d/metadata.json" 2>/dev/null | cut -d/ -f1)
[[ -n "$ip" ]] && used_ips="${used_ips}${ip}\n"
fi
done
client_ip_v4="$(USED_IPS="$(printf "%b" "$used_ips")" python3 - "$server_wg_network" <<'PY'
import ipaddress
import os
import sys
net = ipaddress.ip_network(sys.argv[1], strict=False)
used = {line.strip() for line in os.environ.get("USED_IPS", "").splitlines() if line.strip()}
hosts = net.hosts()
reserved_first = True
for candidate in hosts:
ip = str(candidate)
if reserved_first:
reserved_first = False
continue
if ip not in used:
print(ip)
raise SystemExit(0)
raise SystemExit(1)
PY
)" || die "Нет свободных IP в подсети."
fi
local oct3=$(echo "$client_ip_v4" | cut -d. -f3)
local oct4=$(echo "$client_ip_v4" | cut -d. -f4)
local hex_part=$(printf "%x:%x" "$oct3" "$oct4")
local client_ip_v6=""
[[ -n "$server_ip_v6" ]] && client_ip_v6="${ipv6_prefix_main}::${hex_part}"
log_info "Назначен IP: $client_ip_v4 $([[ -n $client_ip_v6 ]] && echo "/ $client_ip_v6")"
mkdir -p "$dir"
umask 077
wg genkey > "$dir/client_private.key"
wg pubkey < "$dir/client_private.key" > "$dir/client_public.key"
local priv_key=$(cat "$dir/client_private.key")
local pub_key=$(cat "$dir/client_public.key")
local allowed_ips="0.0.0.0/0"
local addr_str="$client_ip_v4/32"
if [[ -n "$client_ip_v6" ]]; then
allowed_ips="0.0.0.0/0, ::/0"
addr_str="$client_ip_v4/32, $client_ip_v6/128"
fi
cat > "$dir/${id}.conf" <<EOF
[Interface]
PrivateKey = $priv_key
Address = $addr_str
DNS = 1.1.1.1, 8.8.8.8
MTU = 1420
[Peer]
PublicKey = $server_pub
Endpoint = 127.0.0.1:${CLIENT_WG_PORT:-13255}
AllowedIPs = $allowed_ips
PersistentKeepalive = 25
EOF
chmod 600 "$dir/${id}.conf"
cat > "$dir/wg-obfuscator.conf" <<EOF
[instance]
source-if = 127.0.0.1
source-lport = ${CLIENT_WG_PORT:-13255}
target = ${CLIENT_ENDPOINT:-$SERVER_PUBLIC_IP_V4}:${OBFUSCATOR_PORT:-51821}
key = ${OBFUSCATOR_KEY:-KEY}
masking = ${OBFUSCATOR_MASKING:-AUTO}
verbose = INFO
idle-timeout = ${OBFUSCATOR_IDLE:-300}
max-dummy = ${OBFUSCATOR_DUMMY:-4}
EOF
chmod 600 "$dir/wg-obfuscator.conf"
cat > "$dir/metadata.json" <<EOF
{
"client_id": "$id",
"client_name": "$name",
"tunnel_ip_v4": "$client_ip_v4",
"tunnel_ip_v6": "$client_ip_v6",
"public_key": "$pub_key",
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"obfuscator_key": "${OBFUSCATOR_KEY:-}",
"obfuscator_dummy": "${OBFUSCATOR_DUMMY:-4}",
"obfuscator_idle": "${OBFUSCATOR_IDLE:-300}",
"server_ip_v4": "$SERVER_PUBLIC_IP_V4",
"server_port": "${OBFUSCATOR_PORT:-}"
}
EOF
chmod 600 "$dir/metadata.json"
local peer_ips="$client_ip_v4/32"
[[ -n "$client_ip_v6" ]] && peer_ips="$peer_ips, $client_ip_v6/128"
cat >> "$WG_CONFIG" <<EOF
[Peer]
PublicKey = $pub_key
AllowedIPs = $peer_ips
EOF
wg syncconf wg0 <(wg-quick strip wg0 2>/dev/null) 2>/dev/null
log_success "Клиент $name создан."
CLIENT_ARG="$id"
action_package
action_link
}
action_remove() {
local id=$(resolve_client "$CLIENT_ARG")
if [[ -z "$id" ]]; then die "Клиент не найден."; fi
local dir="$CLIENTS_DIR/$id"
log_info "Удаление клиента $id..."
if [[ -f "$dir/client_public.key" ]]; then
local pub=$(cat "$dir/client_public.key")
if grep -qF "$pub" "$WG_CONFIG"; then
awk -v key="$pub" '
BEGIN {RS=""; ORS="\n\n"}
index($0, key) == 0 {print $0}
' "$WG_CONFIG" > "$WG_CONFIG.tmp" && mv "$WG_CONFIG.tmp" "$WG_CONFIG"
sed -i '/^$/N;/^\n$/D' "$WG_CONFIG"
wg syncconf wg0 <(wg-quick strip wg0 2>/dev/null) 2>/dev/null
log_success "Peer удален из конфигурации."
fi
fi
rm -rf "$dir"
rm -f "$PACKAGES_DIR/phobos-$id.tar.gz"
if [[ -f "$TOKENS_FILE" ]] && command -v jq >/dev/null; then
local tokens=$(jq -r ".[] | select(.client == \"$id\") | .token" "$TOKENS_FILE")
for t in $tokens; do
rm -f "$WWW_DIR/init/$t.sh"
rm -rf "$WWW_DIR/packages/$t"
done
jq "map(select(.client != \"$id\"))" "$TOKENS_FILE" > "$TOKENS_FILE.tmp" && mv "$TOKENS_FILE.tmp" "$TOKENS_FILE"
fi
log_success "Клиент $id полностью удален."
}
action_package() {
local id=$(resolve_client "$CLIENT_ARG")
if [[ -z "$id" ]]; then die "Клиент не найден."; fi
log_info "Сборка пакета для $id..."
local dir="$CLIENTS_DIR/$id"
local tmp=$(mktemp -d)
local pkg_root="$tmp/phobos-$id"
mkdir -p "$PHOBOS_DIR/bin"
mkdir -p "$pkg_root/bin"
cp "$dir/${id}.conf" "$pkg_root/${id}.conf"
cp "$dir/wg-obfuscator.conf" "$pkg_root/wg-obfuscator.conf"
# 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": *"\(.*\)".*/\1/')
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"
if [[ -d "$tpl_dir" ]]; then
for required in install-router.sh.template lib-client.sh install-obfuscator.sh install-wireguard.sh; do
[[ -f "$tpl_dir/$required" ]] || die "Обязательный шаблон $required не найден в $tpl_dir. Выполни phobos-update и пересоздай пакет."
done
cp "$tpl_dir/install-router.sh.template" "$pkg_root/install-router.sh"
sed -i "s|{{CLIENT_NAME}}|${id}|g" "$pkg_root/install-router.sh"
chmod +x "$pkg_root/install-router.sh"
cp "$tpl_dir/lib-client.sh" "$pkg_root/lib-client.sh"
cp "$tpl_dir/install-obfuscator.sh" "$pkg_root/install-obfuscator.sh"
cp "$tpl_dir/install-wireguard.sh" "$pkg_root/install-wireguard.sh"
chmod +x "$pkg_root/lib-client.sh" "$pkg_root/install-obfuscator.sh" "$pkg_root/install-wireguard.sh"
for f in router-configure-wireguard router-configure-wireguard-openwrt phobos-uninstall 3xui; do
[[ -f "$tpl_dir/$f.sh" ]] && cp "$tpl_dir/$f.sh" "$pkg_root/$f.sh" && chmod +x "$pkg_root/$f.sh"
done
else
log_warn "Шаблоны не найдены в $tpl_dir"
fi
echo "Phobos Client Package for $id" > "$pkg_root/README.txt"
echo "Date: $(date)" >> "$pkg_root/README.txt"
# Health monitor + failover
if [[ -f "$PHOBOS_DIR/server/phobos-health.sh" ]]; then
cp "$PHOBOS_DIR/server/phobos-health.sh" "$pkg_root/phobos-health.sh"
chmod +x "$pkg_root/phobos-health.sh"
fi
# Pull agent: panel -> router config sync (NAT-friendly). Token = panel
# server_api_key (the /api/router-config endpoint accepts it as a fallback).
for src in "$tpl_dir/phobos-pull.sh" "$PHOBOS_DIR/server/phobos-pull.sh"; do
if [[ -f "$src" ]]; then cp "$src" "$pkg_root/phobos-pull.sh"; chmod +x "$pkg_root/phobos-pull.sh"; break; fi
done
local PULL_TOKEN=""
command -v jq >/dev/null && PULL_TOKEN=$(jq -r '.server_api_key // empty' /opt/phobos-panel/settings.json 2>/dev/null)
[[ -n "$PULL_TOKEN" ]] && echo "$PULL_TOKEN" > "$pkg_root/pull_token"
# Generate failover.conf with current server data
source "$PHOBOS_DIR/server/server.env"
local panel_port="${HTTP_PORT:-}"
if [[ -z "$panel_port" && -f /opt/phobos-panel/.port ]]; then
panel_port=$(cat /opt/phobos-panel/.port 2>/dev/null || true)
fi
panel_port="${panel_port:-8443}"
local panel_scheme="${PANEL_SCHEME:-}"
if [[ -z "$panel_scheme" && -f /opt/phobos-panel/.scheme ]]; then
panel_scheme=$(cat /opt/phobos-panel/.scheme 2>/dev/null || true)
fi
case "$panel_scheme" in http|https) ;; *) panel_scheme="http";; esac
local panel_tunnel_ip=""
panel_tunnel_ip="$(python3 - "${SERVER_WG_IPV4_ADDRESS:-}" "${SERVER_WG_IPV4_NETWORK:-10.25.0.0/16}" <<'PY'
import ipaddress, sys
iface = (sys.argv[1] or "").strip()
network = (sys.argv[2] or "10.25.0.0/16").strip()
if iface:
try:
print(ipaddress.ip_interface(iface).ip)
raise SystemExit(0)
except Exception:
pass
net = ipaddress.ip_network(network, strict=False)
print(next(net.hosts()))
PY
)"
[[ -n "$panel_tunnel_ip" ]] && echo "$panel_tunnel_ip" > "$pkg_root/panel_tunnel_ip"
echo "$panel_port" > "$pkg_root/panel_port"
echo "$panel_scheme" > "$pkg_root/panel_scheme"
echo "${panel_scheme}://${SERVER_PUBLIC_IP_V4}:${panel_port}" > "$pkg_root/panel_url"
printf 'http://%s:8444 %s://%s:%s %s://%s:%s\n' \
"$panel_tunnel_ip" "$panel_scheme" "$panel_tunnel_ip" "$panel_port" \
"$panel_scheme" "$SERVER_PUBLIC_IP_V4" "$panel_port" > "$pkg_root/panel_urls"
cat > "$pkg_root/failover.conf" <<FAILEOF
# Phobos Failover Configuration
SERVER_1=${CLIENT_ENDPOINT:-$SERVER_PUBLIC_IP_V4}:${OBFUSCATOR_PORTS:-51821,51822,51823}
KEY_1=${OBFUSCATOR_KEY:-}
FAILEOF
find "$pkg_root" -type f ! -path "*/bin/*" -exec sed -i 's/\r$//' {} \;
tar -C "$tmp" -czf "$PACKAGES_DIR/phobos-$id.tar.gz" "phobos-$id"
rm -rf "$tmp"
log_success "Пакет создан: $PACKAGES_DIR/phobos-$id.tar.gz"
}
action_check() {
local id=$(resolve_client "$CLIENT_ARG")
if [[ -z "$id" ]]; then die "Клиент не найден."; fi
local dir="$CLIENTS_DIR/$id"
local changes=()
if [[ ! -f "$dir/metadata.json" ]]; then
echo "metadata_missing"
return 1
fi
local client_server_ip=$(jq -r '.server_ip_v4 // ""' "$dir/metadata.json")
local client_obf_key=$(jq -r '.obfuscator_key // ""' "$dir/metadata.json")
local client_obf_port=$(jq -r '.server_port // ""' "$dir/metadata.json")
local client_obf_dummy=$(jq -r '.obfuscator_dummy // "4"' "$dir/metadata.json")
local client_obf_idle=$(jq -r '.obfuscator_idle // "300"' "$dir/metadata.json")
local client_wg_pubkey=""
if [[ -f "$dir/${id}.conf" ]]; then
client_wg_pubkey=$(grep "^PublicKey" "$dir/${id}.conf" | cut -d'=' -f2- | tr -d ' ')
fi
[[ "$client_server_ip" != "$SERVER_PUBLIC_IP_V4" ]] && changes+=("IP сервера: $client_server_ip -> $SERVER_PUBLIC_IP_V4")
[[ "$client_obf_key" != "$OBFUSCATOR_KEY" ]] && changes+=("Ключ обфускатора: изменен")
[[ "$client_obf_port" != "$OBFUSCATOR_PORT" ]] && changes+=("Порт обфускатора: $client_obf_port -> $OBFUSCATOR_PORT")
[[ "$client_obf_dummy" != "$OBFUSCATOR_DUMMY" ]] && changes+=("Max dummy: изменен")
[[ "$client_obf_idle" != "$OBFUSCATOR_IDLE" ]] && changes+=("Idle таймаут: изменен")
[[ -n "$client_wg_pubkey" && "$client_wg_pubkey" != "$SERVER_WG_PUBLIC_KEY" ]] && changes+=("Публичный ключ WG: изменен")
if [[ ${#changes[@]} -gt 0 ]]; then
echo "ИЗМЕНЕНИЯ КОНФИГУРАЦИИ:"
for c in "${changes[@]}"; do
echo " - $c"
done
return 1
fi
return 0
}
action_link() {
local id=$(resolve_client "$CLIENT_ARG")
if [[ -z "$id" ]]; then die "Клиент не найден."; fi
local ttl="${EXTRA_ARG:-$TOKEN_TTL}"
if ! command -v jq >/dev/null; then die "jq не установлен. Установите: apt-get install jq"; fi
local token=$(head -c 16 /dev/urandom | md5sum | cut -d' ' -f1)
local exp=$(($(date +%s) + ttl))
if [[ ! -f "$TOKENS_FILE" ]]; then
echo "[]" > "$TOKENS_FILE"
fi
local clean_json=$(jq "map(select(.client != \"$id\"))" "$TOKENS_FILE")
echo "$clean_json" | jq ". + [{\"client\": \"$id\", \"token\": \"$token\", \"expires\": $exp}]" > "$TOKENS_FILE.tmp" && mv "$TOKENS_FILE.tmp" "$TOKENS_FILE"
local link_dir="$WWW_DIR/packages/$token"
rm -rf "$link_dir"
mkdir -p "$link_dir"
ln -s "$PACKAGES_DIR/phobos-$id.tar.gz" "$link_dir/phobos-$id.tar.gz"
mkdir -p "$WWW_DIR/init"
local panel_port="${HTTP_PORT:-}"
if [[ -z "$panel_port" && -f /opt/phobos-panel/.port ]]; then
panel_port=$(cat /opt/phobos-panel/.port 2>/dev/null || true)
fi
panel_port="${panel_port:-8443}"
local panel_scheme="${PANEL_SCHEME:-}"
if [[ -z "$panel_scheme" && -f /opt/phobos-panel/.scheme ]]; then
panel_scheme=$(cat /opt/phobos-panel/.scheme 2>/dev/null || true)
fi
case "$panel_scheme" in http|https) ;; *) panel_scheme="http";; esac
local primary_url="${panel_scheme}://${SERVER_PUBLIC_IP_V4}:${panel_port}/packages/$token/phobos-$id.tar.gz"
local fallback_url="http://${SERVER_PUBLIC_IP_V4}:80/packages/$token/phobos-$id.tar.gz"
cat > "$WWW_DIR/init/$token.sh" <<EOF
#!/bin/sh
urls="$primary_url $fallback_url"
dir="/tmp/phobos_install_\$\$"
mkdir -p "\$dir"
echo "Downloading..."
ok=0
for url in \$urls; do
rm -f "\$dir/package.tar.gz"
if command -v curl >/dev/null; then
curl -k -L -s -o "\$dir/package.tar.gz" "\$url" 2>/dev/null || continue
else
wget --no-check-certificate -q -O "\$dir/package.tar.gz" "\$url" 2>/dev/null || wget -q -O "\$dir/package.tar.gz" "\$url" 2>/dev/null || continue
fi
if grep -qi '<html' "\$dir/package.tar.gz" 2>/dev/null; then
echo "Bad response from \$url (HTML/proxy page), trying next..."
continue
fi
tar tzf "\$dir/package.tar.gz" >/dev/null 2>&1 || {
echo "Bad package from \$url, trying next..."
continue
}
ok=1
break
done
[ "\$ok" = "1" ] || { echo "Download failed: no valid package"; exit 1; }
cd "\$dir"
tar xzf package.tar.gz
cd "phobos-$id"
chmod +x install-router.sh
./install-router.sh
EOF
# nginx (www-data) must read these; the builder runs with a strict root umask
# (files 600 / dirs 700) which makes the install URL return 403. Fix perms.
chmod 755 "$WWW_DIR" "$WWW_DIR/init" "$WWW_DIR/packages" "$link_dir" 2>/dev/null
chmod 644 "$WWW_DIR/init/$token.sh" 2>/dev/null
chmod 755 "$PACKAGES_DIR" 2>/dev/null
chmod 644 "$PACKAGES_DIR/phobos-$id.tar.gz" 2>/dev/null
local init_primary="${panel_scheme}://${SERVER_PUBLIC_IP_V4}:${panel_port}/init/$token.sh"
local init_fallback="http://${SERVER_PUBLIC_IP_V4}:80/init/$token.sh"
local cmd="p=/tmp/phobos-init-\$\$.sh; ok=0; for u in $init_primary $init_fallback; do rm -f \$p; (wget -q -O \$p \$u || wget --no-check-certificate -q -O \$p \$u || curl -kfsSL -o \$p \$u || curl -fsSL -o \$p \$u) 2>/dev/null || continue; head -c 2 \$p | grep -q \"#!\" || continue; sh \$p && ok=1 && break; done; rm -f \$p; [ \$ok -eq 1 ] || echo ERROR_install_script_download_failed"
echo ""
echo "=================================================="
echo "КОМАНДА ДЛЯ УСТАНОВКИ (Действительна $(($ttl / 3600))ч)"
echo "=================================================="
echo "$cmd"
echo "=================================================="
echo ""
}
action_list() {
printf "% -20s % -20s % -20s\n" "CLIENT ID" "IPv4" "CREATED"
echo "------------------------------------------------------------"
for d in "$CLIENTS_DIR"/*; do
if [[ -d "$d" ]]; then
local id=$(basename "$d")
local ip="N/A"
local date="N/A"
if [[ -f "$d/metadata.json" ]]; then
ip=$(jq -r '.tunnel_ip_v4 // "N/A"' "$d/metadata.json")
date=$(jq -r '.created_at // "N/A"' "$d/metadata.json" | cut -d'T' -f1)
fi
printf "% -20s % -20s % -20s\n" "$id" "$ip" "$date"
fi
done
}
case "$CMD" in
add) action_add ;;
remove) action_remove ;;
package) action_package ;;
link) action_link ;;
check) action_check ;;
list) action_list ;;
rebuild)
action_remove
action_add
;;
*)
echo "Usage: $0 {add|remove|list|package|link|check|rebuild}"
exit 1
;;
esac

View File

@@ -1,152 +0,0 @@
#!/bin/sh
# ============================================================
# Phobos Config Pull — NAT-friendly management channel.
# Runs ON the router via cron (every 2 min). OUTBOUND ONLY:
# fetches this client's failover.conf from the panel over
# HTTPS/HTTP and applies it. No inbound SSH needed, so it works
# behind any NAT and regardless of which Phobos server is active.
#
# Why pull (not panel->router SSH):
# - routers sit behind NAT with no public IP (KeenDNS is HTTP-
# only, no port 22),
# - WG3 must be security-level PUBLIC for LAN client routing,
# which blocks inbound SSH on the tunnel,
# - on failover the router's 10.25.0.2 moves to another server's
# wg0, so a fixed panel host cannot reach it.
# Pulling sidesteps all three.
#
# Files (written by installer / bootstrap):
# /opt/etc/Phobos/client_id -> this router's client id (e.g. home)
# /opt/etc/Phobos/pull_token -> shared secret for the panel endpoint
# PANEL env or default below -> panel base URL
# ============================================================
PHOBOS_DIR="/opt/etc/Phobos"
CONF="$PHOBOS_DIR/failover.conf"
HEALTH="$PHOBOS_DIR/phobos-health.sh"
LOG="$PHOBOS_DIR/health.log"
# Config sources, tried in order. TUNNEL FIRST: panel_tunnel_ip is the wg0 of
# whatever server the tunnel currently terminates on, so management rides the
# same obfuscated channel as data — survives a public-IP ban and follows
# failover.
# - panel_tunnel_ip:8444 -> secondary server agent (phobos-api)
# - panel_tunnel_ip:<panel_port> -> primary server panel
# - public panel IP -> bootstrap / if tunnel is down
PANEL_TUNNEL_IP="${PANEL_TUNNEL_IP:-$(cat "$PHOBOS_DIR/panel_tunnel_ip" 2>/dev/null)}"
[ -n "$PANEL_TUNNEL_IP" ] || PANEL_TUNNEL_IP="10.25.0.1"
PANEL="${PANEL:-$(cat "$PHOBOS_DIR/panel_url" 2>/dev/null)}"
PANEL_URLS="${PANEL_URLS:-$(cat "$PHOBOS_DIR/panel_urls" 2>/dev/null)}"
[ -n "$PANEL_URLS" ] || PANEL_URLS="http://${PANEL_TUNNEL_IP}:8444 $PANEL"
CLIENT_ID=$(cat "$PHOBOS_DIR/client_id" 2>/dev/null || echo "home")
TOKEN=$(cat "$PHOBOS_DIR/pull_token" 2>/dev/null)
TMP="/tmp/failover.conf.pull"
LOCK="/tmp/phobos-pull.lock"
log() { echo "$(date '+%H:%M:%S') $1" >> "$LOG"; }
# single instance
if [ -f "$LOCK" ]; then
pid=$(cat "$LOCK" 2>/dev/null)
kill -0 "$pid" 2>/dev/null && exit 0
fi
echo $$ > "$LOCK"
trap 'rm -f "$LOCK"' EXIT
[ -z "$TOKEN" ] && exit 0
detect_tunnel_iface() {
ip route get "$PANEL_TUNNEL_IP" 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="dev") print $(i+1)}' | head -1
}
fetch_config() {
base="$1"
url="${base}/api/router-config/${CLIENT_ID}?token=${TOKEN}"
case "$base" in
"http://${PANEL_TUNNEL_IP}:"*|"https://${PANEL_TUNNEL_IP}:"*)
iface=$(detect_tunnel_iface)
if [ -n "$iface" ] && command -v curl >/dev/null; then
curl -k --interface "$iface" -s -m 8 -o "$TMP" "$url" 2>/dev/null && return 0
fi
;;
esac
if command -v curl >/dev/null; then
curl -k -s -m 8 -o "$TMP" "$url" 2>/dev/null && return 0
fi
wget --no-check-certificate -q -O "$TMP" "$url" 2>/dev/null || wget -q -O "$TMP" "$url" 2>/dev/null
}
# One fetch + compare + apply pass. Returns 0 always (best-effort).
do_pull() {
# Try each source (tunnel first); accept first that returns a valid conf.
got=0
for base in $PANEL_URLS; do
fetch_config "$base" || continue
if grep -q "^SERVER_1=" "$TMP" 2>/dev/null; then got=1; break; fi
done
[ "$got" = 1 ] || { rm -f "$TMP"; return 0; }
new=$(md5sum "$TMP" 2>/dev/null | cut -d' ' -f1)
old=$(md5sum "$CONF" 2>/dev/null | cut -d' ' -f1)
if [ "$new" = "$old" ]; then
rm -f "$TMP"
return 0
fi
old_s1=$(grep "^SERVER_1=" "$CONF" 2>/dev/null | cut -d= -f2- | cut -d: -f1)
new_s1=$(grep "^SERVER_1=" "$TMP" 2>/dev/null | cut -d= -f2- | cut -d: -f1)
cp "$CONF" "$CONF.prev" 2>/dev/null
mv "$TMP" "$CONF"
log "PULL: failover.conf updated (SERVER_1 ${old_s1:-?} -> ${new_s1:-?})"
# apply the new primary now (only if it actually changed)
if [ "$old_s1" != "$new_s1" ] && [ -f "$HEALTH" ]; then
sh "$HEALTH" apply-server 1
fi
apply_obf_params
}
# Apply obfuscator params (masking/max-dummy/idle) delivered in failover.conf.
apply_obf_params() {
M=$(grep "^OBF_MASKING=" "$CONF" 2>/dev/null | cut -d= -f2-)
[ -z "$M" ] && return 0
D=$(grep "^OBF_MAX_DUMMY=" "$CONF" 2>/dev/null | cut -d= -f2-)
I=$(grep "^OBF_IDLE=" "$CONF" 2>/dev/null | cut -d= -f2-)
changed=0
for OBF in "$PHOBOS_DIR"/wg-obfuscator*.conf; do
[ -f "$OBF" ] || continue
cur=$(grep "^masking = " "$OBF" 2>/dev/null | sed 's/masking = //')
[ "$cur" != "$M" ] && { sed -i "s/^masking = .*/masking = $M/" "$OBF"; changed=1; }
if [ -n "$D" ]; then
cur=$(grep "^max-dummy = " "$OBF" 2>/dev/null | sed 's/max-dummy = //')
[ "$cur" != "$D" ] && { sed -i "s/^max-dummy = .*/max-dummy = $D/" "$OBF"; changed=1; }
fi
if [ -n "$I" ]; then
cur=$(grep "^idle-timeout = " "$OBF" 2>/dev/null | sed 's/idle-timeout = //')
[ "$cur" != "$I" ] && { sed -i "s/^idle-timeout = .*/idle-timeout = $I/" "$OBF"; changed=1; }
fi
done
[ "$changed" = 0 ] && return 0
log "PULL: obf params applied (masking=$M dummy=${D:-} idle=${I:-}), restart obfuscator"
if ls /opt/etc/init.d/S*obfuscator* >/dev/null 2>&1; then
for sc in /opt/etc/init.d/S*obfuscator*; do [ -x "$sc" ] && "$sc" restart >/dev/null 2>&1; done
elif command -v systemctl >/dev/null 2>&1; then
systemctl restart 'wg-obfuscator*' >/dev/null 2>&1 || true
else
killall wg-obfuscator 2>/dev/null; sleep 1
for OBF in "$PHOBOS_DIR"/wg-obfuscator*.conf; do [ -f "$OBF" ] && wg-obfuscator --config "$OBF" >/dev/null 2>&1 & done
fi
}
# Inner loop: cron fires this every 60s, but we poll ~4x per minute so a
# panel "Set" applies within ~12-15s instead of up to a full minute. The
# loop stays UNDER 60s and exits so the next cron tick takes over cleanly
# (the lockfile blocks any overlap). POLL_INTERVAL/POLL_PASSES overridable.
POLL_INTERVAL="${POLL_INTERVAL:-12}"
POLL_PASSES="${POLL_PASSES:-4}"
i=1
while [ "$i" -le "$POLL_PASSES" ]; do
do_pull
[ "$i" -lt "$POLL_PASSES" ] && sleep "$POLL_INTERVAL"
i=$((i + 1))
done

View File

@@ -1,67 +0,0 @@
#!/bin/sh
# ============================================================
# PCA Phobos — ttyd web-terminal на роутере Keenetic
#
# phobos-ttyd.sh <client_id> <token> [server_tunnel_ip]
#
# Ставит ttyd (web pty), открывает порт на WG-интерфейсе ТОЛЬКО для панели
# (server tunnel IP), создаёт init-сервис (переживает перезагрузку).
# Панель проксирует терминал к роутеру через WG-туннель (за NAT, без SSH).
# ============================================================
CID="${1:?usage: phobos-ttyd.sh <client_id> <token> [server_ip]}"
TOKEN="${2:?token required}"
SRV_IP="${3:-$(cat /opt/etc/Phobos/panel_tunnel_ip 2>/dev/null)}"
[ -n "$SRV_IP" ] || SRV_IP="10.25.0.1"
PORT="${TTYD_PORT:-7681}"
SHELL_BIN="$( [ -x /opt/bin/bash ] && echo /opt/bin/bash || echo sh )"
# WG tunnel netdev (best effort: interface holding RFC1918 or explicit panel route)
WGDEV=$(ip route get "$SRV_IP" 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="dev") print $(i+1)}' | head -1)
[ -z "$WGDEV" ] && WGDEV=nwg3
opkg update >/dev/null 2>&1
opkg install ttyd >/dev/null 2>&1
opkg install iptables >/dev/null 2>&1
TTYD_BIN="$(command -v ttyd || echo /opt/bin/ttyd)"
[ -x "$TTYD_BIN" ] || { echo "ERROR: ttyd not installed"; exit 1; }
mkdir -p /opt/etc/Phobos
echo "$TOKEN" > /opt/etc/Phobos/ttyd_token
chmod 600 /opt/etc/Phobos/ttyd_token
# init.d service: opens firewall for panel + runs ttyd (base-path + basic auth)
cat > /opt/etc/init.d/S51phobos-ttyd <<EOF
#!/bin/sh
PORT=$PORT
WGDEV=$WGDEV
SRV_IP=$SRV_IP
CID=$CID
TTYD=$TTYD_BIN
SH=$SHELL_BIN
TOKEN=\$(cat /opt/etc/Phobos/ttyd_token 2>/dev/null)
start() {
# firewall: allow ONLY the panel (server tunnel IP) to reach ttyd over WG
iptables -D INPUT -i "\$WGDEV" -s "\$SRV_IP" -p tcp --dport "\$PORT" -j ACCEPT 2>/dev/null
iptables -I INPUT -i "\$WGDEV" -s "\$SRV_IP" -p tcp --dport "\$PORT" -j ACCEPT
pkill -f "ttyd -p \$PORT" 2>/dev/null; sleep 1
# busybox has no setsid/nohup; plain background detaches fine on Entware.
# no -i flag => ttyd binds all interfaces (0.0.0.0).
"\$TTYD" -p "\$PORT" -b "/ttyd/\$CID" -c "phobos:\$TOKEN" --writable "\$SH" >/tmp/phobos-ttyd.log 2>&1 &
sleep 1
echo "ttyd started on \$PORT (base /ttyd/\$CID, panel-only via \$SRV_IP)"
}
stop() {
pkill -f "ttyd -p \$PORT" 2>/dev/null
iptables -D INPUT -i "\$WGDEV" -s "\$SRV_IP" -p tcp --dport "\$PORT" -j ACCEPT 2>/dev/null
}
case "\$1" in
start) start ;;
stop) stop ;;
restart) stop; sleep 1; start ;;
*) echo "usage: \$0 {start|stop|restart}" ;;
esac
EOF
chmod +x /opt/etc/init.d/S51phobos-ttyd
/opt/etc/init.d/S51phobos-ttyd restart
echo "OK: ttyd на роутере, base-path /ttyd/$CID, порт $PORT (только для $SRV_IP по $WGDEV)."

View File

@@ -1,543 +0,0 @@
#!/bin/sh
set -e
PHOBOS_DIR=""
ROUTER_PLATFORM=""
SELECTED_CONF=""
IS_FULL_REMOVAL="1"
OBFUSCATOR_CONFIGS=""
OBFUSCATOR_CONFIG_COUNT=0
. "$(dirname "$0")/lib-client.sh"
collect_obf_configs() {
OBFUSCATOR_CONFIGS=""
OBFUSCATOR_CONFIG_COUNT=0
[ -d "$PHOBOS_DIR" ] || return
for f in "$PHOBOS_DIR"/wg-obfuscator*.conf; do
[ -f "$f" ] || continue
OBFUSCATOR_CONFIGS="${OBFUSCATOR_CONFIGS}$(basename "$f" .conf) "
OBFUSCATOR_CONFIG_COUNT=$((OBFUSCATOR_CONFIG_COUNT + 1))
done
}
get_config_label() {
local obf_conf_base="$1"
local idx_suffix="${obf_conf_base#wg-obfuscator}"
local wg_iface="phobos${idx_suffix}"
local addr=""
if [ -f "/etc/wireguard/${wg_iface}.conf" ]; then
addr=$(grep '^Address' "/etc/wireguard/${wg_iface}.conf" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' | cut -d'/' -f1 | cut -d',' -f1) || addr=""
fi
if [ -n "$addr" ]; then
printf "%s [интерфейс: %s, адрес: %s]" "$obf_conf_base" "$wg_iface" "$addr"
else
printf "%s" "$obf_conf_base"
fi
}
select_config_to_remove() {
collect_obf_configs
if [ "$OBFUSCATOR_CONFIG_COUNT" -eq 0 ]; then
IS_FULL_REMOVAL="1"
SELECTED_CONF="all"
return
fi
if [ "$OBFUSCATOR_CONFIG_COUNT" -eq 1 ]; then
SELECTED_CONF=$(echo "$OBFUSCATOR_CONFIGS" | tr -s ' ' '\n' | grep -v '^$' | head -1)
IS_FULL_REMOVAL="1"
log "Обнаружена конфигурация: ${SELECTED_CONF}"
return
fi
log ""
log "Обнаружено несколько конфигураций:"
log ""
local i=1
for conf_base in $OBFUSCATOR_CONFIGS; do
local label
label=$(get_config_label "$conf_base")
log " ${i}) ${label}"
i=$((i + 1))
done
local all_num=$i
log " ${all_num}) Удалить все"
log ""
printf "Введите номер конфигурации для удаления: "
read -r answer || answer=""
case "$answer" in
''|*[!0-9]*)
log "ОШИБКА: Неверный ввод"
exit 1
;;
esac
if [ "$answer" -eq "$all_num" ]; then
IS_FULL_REMOVAL="1"
SELECTED_CONF="all"
return
fi
if [ "$answer" -lt 1 ] || [ "$answer" -ge "$all_num" ]; then
log "ОШИБКА: Номер вне диапазона"
exit 1
fi
local selected_i=1
for conf_base in $OBFUSCATOR_CONFIGS; do
if [ "$selected_i" -eq "$answer" ]; then
SELECTED_CONF="$conf_base"
IS_FULL_REMOVAL="0"
return
fi
selected_i=$((selected_i + 1))
done
}
remove_single_obfuscator_instance() {
local obf_conf_base="$1"
local idx_suffix="${obf_conf_base#wg-obfuscator}"
local binary_name="wg-obfuscator${idx_suffix}"
local service_name="phobos-obfuscator${idx_suffix}"
local wg_iface="phobos${idx_suffix}"
log "Удаление конфигурации ${obf_conf_base}..."
local link_file="$PHOBOS_DIR/${obf_conf_base}.link"
local linked_client=""
if [ -f "$link_file" ]; then
linked_client=$(cat "$link_file" 2>/dev/null) || linked_client=""
fi
local privkey_fallback=""
if [ -z "$linked_client" ] && [ "$ROUTER_PLATFORM" = "linux" ] && [ -f "/etc/wireguard/${wg_iface}.conf" ]; then
privkey_fallback=$(grep '^PrivateKey' "/etc/wireguard/${wg_iface}.conf" 2>/dev/null | cut -d'=' -f2- | tr -d ' \t') || privkey_fallback=""
fi
for f in /opt/etc/init.d/S[0-9]*wg-obfuscator*; do
[ -f "$f" ] || continue
if grep -q "^PROCS=${binary_name}$" "$f" 2>/dev/null; then
"$f" stop >/dev/null 2>&1 || true
rm -f "$f"
log " ✓ Удален init-скрипт: $f"
break
fi
done
if [ -f "/etc/init.d/${service_name}" ]; then
/etc/init.d/${service_name} stop >/dev/null 2>&1 || true
/etc/init.d/${service_name} disable >/dev/null 2>&1 || true
rm -f "/etc/init.d/${service_name}"
log " ✓ Удален procd init-скрипт: /etc/init.d/${service_name}"
fi
if command -v systemctl >/dev/null 2>&1 && [ -f "/etc/systemd/system/${service_name}.service" ]; then
systemctl stop "${service_name}" >/dev/null 2>&1 || true
systemctl disable "${service_name}" >/dev/null 2>&1 || true
rm -f "/etc/systemd/system/${service_name}.service"
log " ✓ Удален systemd сервис: /etc/systemd/system/${service_name}.service"
fi
for dir in /opt/bin /usr/bin /usr/local/bin; do
if [ -f "${dir}/${binary_name}" ]; then
rm -f "${dir}/${binary_name}"
log " ✓ Удален бинарник: ${dir}/${binary_name}"
fi
done
if [ "$ROUTER_PLATFORM" = "linux" ] && command -v systemctl >/dev/null 2>&1; then
systemctl stop "wg-quick@${wg_iface}" >/dev/null 2>&1 || true
systemctl disable "wg-quick@${wg_iface}" >/dev/null 2>&1 || true
if [ -d "/etc/systemd/system/wg-quick@${wg_iface}.service.d" ]; then
rm -rf "/etc/systemd/system/wg-quick@${wg_iface}.service.d"
log " ✓ Удален systemd override: wg-quick@${wg_iface}"
fi
if [ -f "/etc/wireguard/${wg_iface}.conf" ]; then
rm -f "/etc/wireguard/${wg_iface}.conf"
log " ✓ Удален WireGuard конфиг: /etc/wireguard/${wg_iface}.conf"
fi
systemctl daemon-reload >/dev/null 2>&1 || true
elif [ "$ROUTER_PLATFORM" = "keenetic" ]; then
if command -v curl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1 && [ -n "$linked_client" ]; then
local target_desc="Phobos-${linked_client}"
log " Удаление WireGuard интерфейса Keenetic: ${target_desc}..."
local iface_list=$(curl -s "http://127.0.0.1:79/rci/show/interface" 2>/dev/null || echo "")
if [ -n "$iface_list" ] && echo "$iface_list" | jq -e . >/dev/null 2>&1; then
local iface_id=$(echo "$iface_list" | jq -r --arg desc "$target_desc" 'to_entries[] | select((.value.description? // "") == $desc) | .key' 2>/dev/null)
if [ -n "$iface_id" ]; then
local del_json=$(cat <<EOF
{
"interface": {
"$iface_id": {
"no": true
}
}
}
EOF
)
local del_result=$(echo "$del_json" | curl -s -X POST \
-H "Content-Type: application/json" \
-d @- \
"http://127.0.0.1:79/rci/" 2>/dev/null)
if echo "$del_result" | jq -e '.status == "error"' >/dev/null 2>&1; then
local err_msg=$(echo "$del_result" | jq -r '.message // "Unknown error"' 2>/dev/null)
log " ОШИБКА при удалении ${iface_id}: $err_msg"
else
log " ✓ Интерфейс ${iface_id} (${target_desc}) удален"
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"system":{"configuration":{"save":{}}}}' \
"http://127.0.0.1:79/rci/" >/dev/null 2>&1
log " ✓ Конфигурация сохранена"
fi
else
log " Интерфейс с описанием '${target_desc}' не найден"
fi
else
log " RCI API недоступен, пропускаем удаление интерфейса Keenetic"
fi
else
log " curl/jq не доступны или имя клиента не определено, пропускаем удаление интерфейса Keenetic"
fi
elif [ "$ROUTER_PLATFORM" = "openwrt" ]; then
remove_wireguard_interfaces_openwrt
remove_firewall_zone_openwrt
fi
if [ -f "$PHOBOS_DIR/${obf_conf_base}.conf" ]; then
rm -f "$PHOBOS_DIR/${obf_conf_base}.conf"
log " ✓ Удален конфиг obfuscator: $PHOBOS_DIR/${obf_conf_base}.conf"
fi
[ -f "$link_file" ] && rm -f "$link_file"
if [ -n "$linked_client" ] && [ -f "$PHOBOS_DIR/${linked_client}.conf" ]; then
rm -f "$PHOBOS_DIR/${linked_client}.conf"
log " ✓ Удален конфиг клиента: ${linked_client}.conf"
elif [ -n "$privkey_fallback" ]; then
for client_conf in "$PHOBOS_DIR"/*.conf; do
[ -f "$client_conf" ] || continue
case "$(basename "$client_conf")" in
wg-obfuscator*.conf) continue ;;
esac
if grep -qF "PrivateKey = ${privkey_fallback}" "$client_conf" 2>/dev/null; then
rm -f "$client_conf"
log " ✓ Удален конфиг клиента: $(basename "$client_conf")"
break
fi
done
fi
local remaining=0
for f in "$PHOBOS_DIR"/*.conf; do
[ -f "$f" ] && remaining=$((remaining + 1))
done
if [ "$remaining" -eq 0 ] && [ -d "$PHOBOS_DIR" ]; then
rm -rf "$PHOBOS_DIR"
log " ✓ Удалена директория: $PHOBOS_DIR"
fi
}
stop_obfuscator() {
log "Остановка wg-obfuscator..."
for f in /opt/etc/init.d/S[0-9]*wg-obfuscator*; do
[ -f "$f" ] && "$f" stop >/dev/null 2>&1 || true
done
for f in /etc/init.d/phobos-obfuscator*; do
if [ -f "$f" ]; then
"$f" stop >/dev/null 2>&1 || true
"$f" disable >/dev/null 2>&1 || true
fi
done
if command -v systemctl >/dev/null 2>&1; then
systemctl list-units --no-legend --plain 'phobos-obfuscator*.service' | awk '{print $1}' | while read -r unit; do
systemctl stop "$unit" >/dev/null 2>&1 || true
systemctl disable "$unit" >/dev/null 2>&1 || true
done
fi
if ps | grep -v grep | grep -q wg-obfuscator; then
log " Принудительное завершение процесса..."
pkill -f wg-obfuscator >/dev/null 2>&1 || true
fi
}
remove_wireguard_interfaces_keenetic() {
log "Удаление WireGuard интерфейсов Phobos (Keenetic)..."
if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
log " curl или jq не установлен, пропускаем удаление интерфейсов"
return 0
fi
local interfaces=$(curl -s "http://127.0.0.1:79/rci/show/interface" 2>/dev/null || echo "")
if [ -z "$interfaces" ] || ! echo "$interfaces" | jq -e . >/dev/null 2>&1; then
log " RCI API недоступен или вернул некорректный JSON"
return 0
fi
local removed_count=0
local phobos_interfaces=$(echo "$interfaces" | jq -r 'to_entries[] | select(.value.description? // "" | startswith("Phobos-")) | .key' 2>/dev/null)
if [ -z "$phobos_interfaces" ]; then
log " Интерфейсы Phobos не найдены в системе"
return 0
fi
echo "$phobos_interfaces" | while read -r interface_id; do
if [ -z "$interface_id" ]; then
continue
fi
local interface_desc=$(echo "$interfaces" | jq -r --arg id "$interface_id" '.[$id].description // "unknown"' 2>/dev/null)
log " Удаление интерфейса: $interface_id ($interface_desc)"
local delete_json=$(cat <<EOF
{
"interface": {
"$interface_id": {
"no": true
}
}
}
EOF
)
local result=$(echo "$delete_json" | curl -s -X POST \
-H "Content-Type: application/json" \
-d @- \
"http://127.0.0.1:79/rci/" 2>/dev/null)
if echo "$result" | jq -e '.status == "error"' >/dev/null 2>&1; then
local error_msg=$(echo "$result" | jq -r '.message // "Unknown error"' 2>/dev/null)
log " ОШИБКА при удалении $interface_id: $error_msg"
else
log " ✓ Интерфейс $interface_id удален"
removed_count=$((removed_count + 1))
fi
done
local save_result=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"system":{"configuration":{"save":{}}}}' \
"http://127.0.0.1:79/rci/" 2>/dev/null)
if [ $removed_count -gt 0 ]; then
log "✓ Удалено интерфейсов: $removed_count"
log "✓ Конфигурация сохранена"
fi
}
remove_wireguard_interfaces_openwrt() {
log "Удаление WireGuard интерфейсов Phobos (OpenWRT)..."
if ! command -v uci >/dev/null 2>&1; then
log " uci не установлен, пропускаем удаление интерфейсов"
return 0
fi
local interface_name="phobos_wg"
if uci -q get network.${interface_name} >/dev/null 2>&1; then
log " Удаление интерфейса: ${interface_name}"
ifdown ${interface_name} >/dev/null 2>&1 || true
uci -q delete network.${interface_name} || true
local peers=$(uci show network 2>/dev/null | grep "wireguard_${interface_name}" | cut -d'.' -f2 | cut -d'=' -f1 | sort -u)
for peer in $peers; do
uci -q delete network.$peer || true
done
uci commit network
log " ✓ Интерфейс ${interface_name} удален"
else
log " Интерфейс ${interface_name} не найден"
fi
/etc/init.d/network reload >/dev/null 2>&1 || true
}
remove_firewall_zone_openwrt() {
log "Удаление файрволл зоны Phobos (OpenWRT)..."
if ! command -v uci >/dev/null 2>&1; then
log " uci не установлен, пропускаем удаление зоны"
return 0
fi
local zone_name="phobos"
if uci -q get firewall.${zone_name} >/dev/null 2>&1; then
log " Удаление зоны: ${zone_name}"
uci -q delete firewall.${zone_name} || true
uci commit firewall
log " ✓ Зона ${zone_name} удалена"
/etc/init.d/firewall reload >/dev/null 2>&1 || true
else
log " Зона ${zone_name} не найдена"
fi
}
remove_wireguard_interfaces_linux() {
log "Удаление WireGuard интерфейсов Phobos (Linux)..."
if ! command -v systemctl >/dev/null 2>&1; then
log " systemctl не найден, пропускаем удаление интерфейсов"
return 0
fi
for wg_conf in /etc/wireguard/phobos*.conf; do
[ -f "$wg_conf" ] || continue
local wg_interface
wg_interface=$(basename "$wg_conf" .conf)
if systemctl is-enabled --quiet wg-quick@${wg_interface} 2>/dev/null || systemctl is-active --quiet wg-quick@${wg_interface} 2>/dev/null; then
log " Остановка и удаление WireGuard сервиса: wg-quick@${wg_interface}"
systemctl stop wg-quick@${wg_interface} >/dev/null 2>&1 || true
systemctl disable wg-quick@${wg_interface} >/dev/null 2>&1 || true
log " ✓ Сервис wg-quick@${wg_interface} остановлен и отключен"
fi
if [ -d "/etc/systemd/system/wg-quick@${wg_interface}.service.d" ]; then
log " Удаление systemd override: /etc/systemd/system/wg-quick@${wg_interface}.service.d"
rm -rf "/etc/systemd/system/wg-quick@${wg_interface}.service.d"
log " ✓ Systemd override удален"
fi
log " Удаление конфигурации: $wg_conf"
rm -f "$wg_conf"
log " ✓ Конфигурационный файл WireGuard удален"
done
systemctl daemon-reload >/dev/null 2>&1 || true
}
remove_files() {
log "Удаление файлов Phobos..."
for f in /opt/etc/init.d/S[0-9]*wg-obfuscator*; do
if [ -f "$f" ]; then
rm -f "$f"
log " ✓ Удален init-скрипт: $f"
fi
done
for f in /etc/init.d/phobos-obfuscator*; do
if [ -f "$f" ]; then
rm -f "$f"
log " ✓ Удален procd init-скрипт: $f"
fi
done
for f in /opt/bin/wg-obfuscator*; do
if [ -f "$f" ]; then
rm -f "$f"
log " ✓ Удален бинарник: $f"
fi
done
for f in /usr/bin/wg-obfuscator*; do
if [ -f "$f" ]; then
rm -f "$f"
log " ✓ Удален бинарник: $f"
fi
done
for f in /usr/local/bin/wg-obfuscator*; do
if [ -f "$f" ]; then
rm -f "$f"
log " ✓ Удален бинарник: $f"
fi
done
local daemon_reload_needed=0
for f in /etc/systemd/system/phobos-obfuscator*.service; do
if [ -f "$f" ]; then
rm -f "$f"
daemon_reload_needed=1
log " ✓ Удален systemd сервис: $f"
fi
done
if [ $daemon_reload_needed -eq 1 ]; then
systemctl daemon-reload >/dev/null 2>&1 || true
fi
if [ -d "$PHOBOS_DIR" ]; then
rm -rf "$PHOBOS_DIR"
log " ✓ Удалена директория: $PHOBOS_DIR"
fi
}
show_final_info() {
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ Phobos успешно удален ║"
log "╚════════════════════════════════════════════════════════════╝"
log ""
log "Удалены компоненты:"
log " - wg-obfuscator (процесс, init-скрипт/сервис, бинарник)"
log " - WireGuard интерфейсы Phobos"
log " - Конфигурационные файлы"
log ""
}
main() {
ROUTER_PLATFORM=$(detect_router_platform)
PHOBOS_DIR=$(detect_phobos_dir "$ROUTER_PLATFORM")
log "==> Начало удаления Phobos (платформа: $ROUTER_PLATFORM)"
log "==> Директория: $PHOBOS_DIR"
check_root
select_config_to_remove
if [ "$IS_FULL_REMOVAL" = "1" ]; then
stop_obfuscator
if [ "$ROUTER_PLATFORM" = "keenetic" ]; then
remove_wireguard_interfaces_keenetic
elif [ "$ROUTER_PLATFORM" = "openwrt" ]; then
remove_wireguard_interfaces_openwrt
remove_firewall_zone_openwrt
elif [ "$ROUTER_PLATFORM" = "linux" ]; then
remove_wireguard_interfaces_linux
else
log "ПРЕДУПРЕЖДЕНИЕ: Неизвестная платформа, пропускаем удаление WireGuard интерфейсов"
fi
remove_files
show_final_info
else
log "==> Удаление конфигурации: ${SELECTED_CONF}"
remove_single_obfuscator_instance "$SELECTED_CONF"
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ Конфигурация успешно удалена ║"
log "╚════════════════════════════════════════════════════════════╝"
log ""
fi
log "==> Удаление завершено."
}
main "$@"

View File

@@ -1,349 +0,0 @@
#!/bin/sh
set -e
CLIENT_NAME=""
CLIENT_PRIVATE_KEY=""
CLIENT_IP=""
CLIENT_IPV6=""
SERVER_PUBLIC_KEY=""
ENDPOINT_PORT=13255
KEEPALIVE=25
MTU=1420
FALLBACK_CONFIG=""
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
error() {
echo "[ERROR] $*" >&2
log "ERROR: $*"
}
usage() {
cat <<EOF
Usage: $0 [OPTIONS]
Options:
--client-name NAME Client name (required)
--client-private-key KEY WireGuard private key (required)
--client-ip IP Client tunnel IPv4 address (required)
--client-ipv6 IP Client tunnel IPv6 address (required)
--server-public-key KEY Server WireGuard public key (required)
--endpoint-port PORT Local obfuscator port (default: 13255)
--keepalive SECONDS Keepalive interval (default: 25)
--mtu MTU Interface MTU (default: 1420)
--fallback-config PATH Path to fallback .conf file
--help Show this help
Example:
$0 --client-name home \\
--client-private-key "ABCD..." \\
--client-ip 10.25.0.4/32 \\
--client-ipv6 fd00:10:25::4/128 \\
--server-public-key "EFGH..." \\
--endpoint-port 13255 \\
--fallback-config /opt/etc/Phobos/home.conf
EOF
exit 1
}
check_dependencies() {
local missing=""
for cmd in uci wg; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing="$missing $cmd"
fi
done
if [ -n "$missing" ]; then
error "Missing required utilities:$missing"
return 1
fi
return 0
}
parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
--client-name)
CLIENT_NAME="$2"
shift 2
;;
--client-private-key)
CLIENT_PRIVATE_KEY="$2"
shift 2
;;
--client-ip)
CLIENT_IP="$2"
shift 2
;;
--client-ipv6)
CLIENT_IPV6="$2"
shift 2
;;
--server-public-key)
SERVER_PUBLIC_KEY="$2"
shift 2
;;
--endpoint-port)
ENDPOINT_PORT="$2"
shift 2
;;
--keepalive)
KEEPALIVE="$2"
shift 2
;;
--mtu)
MTU="$2"
shift 2
;;
--fallback-config)
FALLBACK_CONFIG="$2"
shift 2
;;
--help)
usage
;;
*)
error "Unknown option: $1"
usage
;;
esac
done
if [ -z "${CLIENT_NAME}" ] || [ -z "${CLIENT_PRIVATE_KEY}" ] || \
[ -z "${CLIENT_IP}" ] || [ -z "${CLIENT_IPV6}" ] || \
[ -z "${SERVER_PUBLIC_KEY}" ]; then
error "Missing required parameters"
usage
fi
}
check_existing_interface() {
local interface_name="phobos_wg"
if uci -q get network.${interface_name} >/dev/null 2>&1; then
log "Найден существующий интерфейс: ${interface_name}"
return 0
else
log "Существующий интерфейс Phobos не найден"
return 1
fi
}
remove_existing_interface() {
local interface_name="phobos_wg"
log "Удаление существующего интерфейса ${interface_name}..."
uci -q delete network.${interface_name} || true
local peers=$(uci show network | grep "wireguard_${interface_name}" | cut -d'.' -f2 | cut -d'=' -f1 | sort -u)
for peer in $peers; do
uci -q delete network.$peer || true
done
uci commit network
log "Существующий интерфейс удален"
}
configure_wireguard_interface() {
local interface_name="phobos_wg"
log "Настройка интерфейса ${interface_name} через UCI..."
uci set network.${interface_name}=interface
uci set network.${interface_name}.proto='wireguard'
uci set network.${interface_name}.private_key="${CLIENT_PRIVATE_KEY}"
uci set network.${interface_name}.mtu="${MTU}"
uci add_list network.${interface_name}.addresses="${CLIENT_IP}"
if [ "${CLIENT_IPV6}" != "none" ] && [ -n "${CLIENT_IPV6}" ]; then
uci add_list network.${interface_name}.addresses="${CLIENT_IPV6}"
fi
local peer_name="wgpeer_${interface_name}"
uci add network wireguard_${interface_name}
uci rename network.@wireguard_${interface_name}[-1]="${peer_name}"
uci set network.${peer_name}.public_key="${SERVER_PUBLIC_KEY}"
uci set network.${peer_name}.description="Phobos VPS Server"
uci set network.${peer_name}.endpoint_host='127.0.0.1'
uci set network.${peer_name}.endpoint_port="${ENDPOINT_PORT}"
uci set network.${peer_name}.persistent_keepalive="${KEEPALIVE}"
uci set network.${peer_name}.route_allowed_ips='0'
uci add_list network.${peer_name}.allowed_ips='0.0.0.0/0'
uci add_list network.${peer_name}.allowed_ips='::/0'
uci commit network
log "Интерфейс ${interface_name} настроен ✓"
return 0
}
configure_firewall_zone() {
local zone_name="phobos"
local interface_name="phobos_wg"
log "Настройка файрволла для зоны ${zone_name}..."
if uci -q get firewall.${zone_name} >/dev/null 2>&1; then
log "Зона ${zone_name} уже существует, обновляем..."
uci delete firewall.${zone_name}
fi
uci add firewall zone
uci rename firewall.@zone[-1]="${zone_name}"
uci set firewall.${zone_name}.name="${zone_name}"
uci add_list firewall.${zone_name}.network="${interface_name}"
uci set firewall.${zone_name}.input='REJECT'
uci set firewall.${zone_name}.output='ACCEPT'
uci set firewall.${zone_name}.forward='REJECT'
uci set firewall.${zone_name}.masq='0'
uci set firewall.${zone_name}.mtu_fix='1'
uci commit firewall
log "Файрволл зона ${zone_name} настроена ✓"
return 0
}
restart_network_services() {
log "Перезапуск сетевых сервисов..."
/etc/init.d/network reload >/dev/null 2>&1 || true
sleep 3
ifup phobos_wg >/dev/null 2>&1 || true
sleep 2
/etc/init.d/firewall reload >/dev/null 2>&1 || true
sleep 1
log "Сетевые сервисы перезапущены ✓"
}
verify_interface_created() {
log "Проверка создания интерфейса WireGuard..."
local max_attempts=5
local attempt=1
while [ $attempt -le $max_attempts ]; do
if ip link show phobos_wg >/dev/null 2>&1; then
log "Интерфейс phobos_wg найден (попытка $attempt/$max_attempts)"
if wg show phobos_wg >/dev/null 2>&1; then
log "WireGuard интерфейс phobos_wg активен"
if uci -q get network.phobos_wg >/dev/null 2>&1; then
log "UCI конфигурация phobos_wg найдена"
return 0
else
log "UCI конфигурация phobos_wg не найдена, но интерфейс работает"
return 0
fi
else
log "Интерфейс найден, но WireGuard не активен (попытка $attempt/$max_attempts)"
fi
else
log "Интерфейс phobos_wg еще не создан (попытка $attempt/$max_attempts)..."
fi
if [ $attempt -lt $max_attempts ]; then
sleep 2
fi
attempt=$((attempt + 1))
done
error "Интерфейс phobos_wg не найден после $max_attempts попыток"
return 1
}
show_fallback_instructions() {
cat <<EOF
╔═══════════════════════════════════════════════════════════╗
║ Требуется ручная настройка WireGuard ║
╚═══════════════════════════════════════════════════════════╝
Конфигурация сохранена в: ${FALLBACK_CONFIG}
Инструкция по ручной настройке:
1. Установите пакеты: opkg install kmod-wireguard wireguard-tools wireguard-tools
2. Импортируйте конфигурацию из ${FALLBACK_CONFIG}
3. Настройте интерфейс через UCI или LuCI веб-интерфейс
EOF
}
main() {
parse_args "$@"
if ! check_dependencies; then
show_fallback_instructions
exit 1
fi
log "=== Phobos WireGuard OpenWRT Configuration ==="
log "Клиент: ${CLIENT_NAME}"
if check_existing_interface; then
remove_existing_interface
fi
if ! configure_wireguard_interface; then
error "Не удалось настроить WireGuard через UCI"
show_fallback_instructions
exit 1
fi
if ! configure_firewall_zone; then
error "Не удалось настроить файрволл"
exit 1
fi
restart_network_services
log ""
log "Ожидание применения конфигурации..."
sleep 5
if verify_interface_created; then
log ""
log "WireGuard успешно настроен на OpenWRT"
log ""
log "Интерфейс: phobos_wg"
log "Файрволл зона: phobos (без форвардинга)"
log ""
log "Для маршрутизации трафика через туннель настройте правила"
log "файрволла и маршрутизацию вручную через LuCI или UCI."
log ""
log "Текущий статус интерфейса:"
ip addr show phobos_wg 2>/dev/null | sed 's/^/ /' || true
log ""
exit 0
else
log ""
log "Не удалось подтвердить создание интерфейса WireGuard"
log ""
log "Проверьте вручную:"
log " ip link show phobos_wg"
log " wg show phobos_wg"
log " uci show network.phobos_wg"
log ""
log "Если интерфейс существует (ip link show phobos_wg работает),"
log "то настройка прошла успешно, несмотря на ошибку проверки."
log ""
exit 1
fi
}
main "$@"

View File

@@ -1,455 +0,0 @@
#!/bin/sh
set -e
RCI_URL="http://localhost:79/rci/"
MAX_INTERFACE_NUM=9
check_dependencies() {
local missing=""
for cmd in curl jq date; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing="$missing $cmd"
fi
done
if [ -n "$missing" ]; then
echo "ERROR: Missing required utilities:$missing" >&2
echo "Please install them using: opkg update && opkg install$missing" >&2
return 1
fi
return 0
}
CLIENT_NAME=""
CLIENT_PRIVATE_KEY=""
CLIENT_IP=""
CLIENT_IPV6=""
SERVER_PUBLIC_KEY=""
ENDPOINT_PORT=13255
KEEPALIVE=25
MTU=1420
FALLBACK_CONFIG=""
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
error() {
echo "[ERROR] $*" >&2
log "ERROR: $*"
}
usage() {
cat <<EOF
Usage: $0 [OPTIONS]
Options:
--client-name NAME Client name (required)
--client-private-key KEY WireGuard private key (required)
--client-ip IP Client tunnel IPv4 address (required)
--client-ipv6 IP Client tunnel IPv6 address (required)
--server-public-key KEY Server WireGuard public key (required)
--endpoint-port PORT Local obfuscator port (default: 13255)
--keepalive SECONDS Keepalive interval (default: 25)
--mtu MTU Interface MTU (default: 1420)
--fallback-config PATH Path to fallback .conf file
--help Show this help
Example:
$0 --client-name Pegacomp \\
--client-private-key "ABCD..." \\
--client-ip 10.26.0.4 \\
--client-ipv6 fd00:10:25::4 \\
--server-public-key "EFGH..." \\
--endpoint-port 13255 \\
--fallback-config /opt/etc/Phobos/Pegacomp.conf
EOF
exit 1
}
parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
--client-name)
CLIENT_NAME="$2"
shift 2
;;
--client-private-key)
CLIENT_PRIVATE_KEY="$2"
shift 2
;;
--client-ip)
CLIENT_IP="$2"
shift 2
;;
--client-ipv6)
CLIENT_IPV6="$2"
shift 2
;;
--server-public-key)
SERVER_PUBLIC_KEY="$2"
shift 2
;;
--endpoint-port)
ENDPOINT_PORT="$2"
shift 2
;;
--keepalive)
KEEPALIVE="$2"
shift 2
;;
--mtu)
MTU="$2"
shift 2
;;
--fallback-config)
FALLBACK_CONFIG="$2"
shift 2
;;
--help)
usage
;;
*)
error "Unknown option: $1"
usage
;;
esac
done
if [ -z "${CLIENT_NAME}" ] || [ -z "${CLIENT_PRIVATE_KEY}" ] || \
[ -z "${CLIENT_IP}" ] || [ -z "${CLIENT_IPV6}" ] || \
[ -z "${SERVER_PUBLIC_KEY}" ]; then
error "Missing required parameters"
usage
fi
}
find_phobos_interface() {
local client_name="$1"
local target_desc="Phobos-${client_name}"
log "Поиск существующего интерфейса Phobos для клиента: ${client_name}..." >&2
local i
for i in 0 1 2 3 4 5 6 7 8 9; do
local interface_json=$(curl -s "${RCI_URL}show/rc/interface/Wireguard${i}" 2>/dev/null)
if [ -n "${interface_json}" ] && echo "${interface_json}" | jq -e . >/dev/null 2>&1; then
local desc=$(echo "${interface_json}" | jq -r '.description // empty' 2>/dev/null)
if [ "${desc}" = "${target_desc}" ]; then
log "Найден существующий интерфейс: Wireguard${i}" >&2
echo "Wireguard${i}"
return 0
fi
fi
done
log "Существующий интерфейс Phobos не найден" >&2
echo ""
return 0
}
find_free_wireguard_interface() {
log "Поиск свободного интерфейса WireGuard..." >&2
local i
for i in 0 1 2 3 4 5 6 7 8 9; do
local interface_json=$(curl -s "${RCI_URL}show/interface/Wireguard${i}" 2>/dev/null)
if [ -z "${interface_json}" ] || ! echo "${interface_json}" | jq -e '.id' >/dev/null 2>&1; then
log "Найден свободный интерфейс: Wireguard${i}" >&2
echo "Wireguard${i}"
return 0
fi
done
error "Нет свободных интерфейсов WireGuard (0-${MAX_INTERFACE_NUM})"
return 1
}
remove_wireguard_interface() {
local interface_name="$1"
log "Удаление существующего интерфейса: ${interface_name}..."
if command -v ndmc >/dev/null 2>&1; then
if ndmc -c "no interface ${interface_name}" >/dev/null 2>&1; then
log "Интерфейс ${interface_name} успешно удален ✓"
return 0
else
log "Предупреждение: не удалось удалить интерфейс через ndmc"
return 1
fi
else
log "Предупреждение: команда ndmc не найдена"
return 1
fi
}
configure_wireguard_interface() {
local interface_name="$1"
local description="Phobos-${CLIENT_NAME}"
local client_ip_addr=$(echo "${CLIENT_IP}" | cut -d'/' -f1)
local client_ipv6_block="${CLIENT_IPV6}"
local route_target=$(echo "$client_ip_addr" | cut -d'.' -f1-2).0.0/16
log "Настройка интерфейса ${interface_name}..."
local config_json=$(cat <<EOF
{
"interface": {
"${interface_name}": {
"description": "${description}",
"security-level": {
"public": true
},
"ip": {
"address": {
"address": "${client_ip_addr}",
"mask": "255.255.255.255"
},
"mtu": ${MTU},
"global": true,
"defaultgw": false,
"priority": 26622,
"tcp": {
"adjust-mss": {
"pmtu": true
}
}
},
"ipv6": {
"address": [
{"auto": false},
{"block": "${client_ipv6_block}"}
],
"prefix": [
{"auto": false}
]
},
"wireguard": {
"private-key": "${CLIENT_PRIVATE_KEY}",
"peer": [
{
"key": "${SERVER_PUBLIC_KEY}",
"comment": "Phobos VPS Server",
"endpoint": {
"address": "127.0.0.1:${ENDPOINT_PORT}"
},
"keepalive-interval": {
"interval": ${KEEPALIVE}
},
"allow-ips": [
{
"address": "0.0.0.0",
"mask": "0.0.0.0"
},
{
"address": "::",
"mask": "0"
}
]
}
]
},
"up": true
}
}
}
EOF
)
local result=$(echo "${config_json}" | curl -s -X POST \
-H "Content-Type: application/json" \
-d @- \
"${RCI_URL}" 2>/dev/null)
if echo "${result}" | jq -e '.status == "error"' >/dev/null 2>&1; then
local error_msg=$(echo "${result}" | jq -r '.message // "Unknown error"' 2>/dev/null)
error "RCI API отклонил конфигурацию: ${error_msg}"
log "JSON запрос:"
log "${config_json}"
return 1
fi
# Настраиваем статический маршрут в созданный интерфейс отдельным вызовом RCI
local route_target_net=$(echo "$client_ip_addr" | cut -d'.' -f1-2).0.0
log "Настройка маршрута для подсети туннеля (${route_target_net}/16) -> ${interface_name}..."
local route_json=$(cat <<EOF
{
"network": "${route_target_net}",
"mask": "255.255.0.0",
"interface": "${interface_name}",
"auto": true
}
EOF
)
local route_result=$(echo "${route_json}" | curl -s -X POST \
-H "Content-Type: application/json" \
-d @- \
"http://localhost:79/rci/ip/route/" 2>/dev/null)
if echo "${route_result}" | jq -e '.status[] | select(.status == "error")' >/dev/null 2>&1; then
local error_msg=$(echo "${route_result}" | jq -r '.status[] | select(.status == "error") | .message // "Unknown error"' 2>/dev/null)
error "RCI API отклонил настройку маршрута: ${error_msg}"
log "JSON запрос:"
log "${route_json}"
return 1
fi
log "Интерфейс ${interface_name} создан ✓"
return 0
}
save_configuration() {
log "Сохранение конфигурации..."
local result=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"system":{"configuration":{"save":{}}}}' \
"${RCI_URL}" 2>/dev/null)
if echo "${result}" | grep -q '"status"[[:space:]]*:[[:space:]]*"message"'; then
log "Конфигурация сохранена ✓"
return 0
else
error "Ошибка сохранения конфигурации"
return 1
fi
}
verify_interface_created() {
local client_name="$1"
local interface_description="Phobos-${client_name}"
log "Проверка создания интерфейса WireGuard..."
local interfaces=$(curl -s "http://127.0.0.1:79/rci/show/interface" 2>/dev/null || echo "")
if [ -z "$interfaces" ]; then
error "Не удалось получить список интерфейсов через RCI API"
return 1
fi
if ! echo "$interfaces" | jq -e . >/dev/null 2>&1; then
error "Некорректный JSON ответ от RCI API"
return 1
fi
local found=$(echo "$interfaces" | jq -r "to_entries[] | select(.value.description == \"$interface_description\") | .key" 2>/dev/null)
if [ -n "$found" ]; then
log "✓ Интерфейс $found (Phobos-${client_name}) успешно создан"
return 0
else
error "Интерфейс с description '$interface_description' не найден"
return 1
fi
}
show_fallback_instructions() {
cat <<EOF
╔════════════════════════════════════════════════════════════╗
║ RCI API недоступен - требуется ручная настройка ║
╚════════════════════════════════════════════════════════════╝
Конфигурация сохранена в: ${FALLBACK_CONFIG}
Инструкция по ручному импорту:
1. Откройте веб-панель Keenetic (http://192.168.1.1 или http://my.keenetic.net)
2. Перейдите: Интернет → WireGuard
3. Нажмите: 'Добавить подключение'
4. Выберите: 'Загрузить конфигурацию из файла'
5. Укажите путь: ${FALLBACK_CONFIG}
6. Активируйте подключение
EOF
}
main() {
parse_args "$@"
if ! check_dependencies; then
exit 1
fi
mkdir -p /opt/etc/Phobos
log "=== Phobos WireGuard RCI Configuration ==="
log "Клиент: ${CLIENT_NAME}"
EXISTING_INTERFACE=$(find_phobos_interface "${CLIENT_NAME}") || true
if [ -n "${EXISTING_INTERFACE}" ]; then
log "Обнаружен существующий интерфейс: ${EXISTING_INTERFACE}"
if remove_wireguard_interface "${EXISTING_INTERFACE}"; then
log "Интерфейс ${EXISTING_INTERFACE} удален, будет создан заново"
else
log "Не удалось удалить интерфейс ${EXISTING_INTERFACE}, попытка пересоздать"
fi
fi
INTERFACE_NAME=$(find_free_wireguard_interface) || true
if [ -z "${INTERFACE_NAME}" ]; then
show_fallback_instructions
exit 1
fi
log "Создание нового интерфейса: ${INTERFACE_NAME}"
if ! configure_wireguard_interface "${INTERFACE_NAME}"; then
error "Не удалось настроить WireGuard через RCI API"
show_fallback_instructions
exit 1
fi
if ! save_configuration; then
error "Не удалось сохранить конфигурацию"
exit 1
fi
log "Настройка WireGuard завершена успешно! ✓"
log "Интерфейс: ${INTERFACE_NAME}"
log "Description: Phobos-${CLIENT_NAME}"
log ""
log "Проверка статуса wg-obfuscator..."
if [ -f /opt/etc/init.d/S49wg-obfuscator ]; then
local obf_status=$(/opt/etc/init.d/S49wg-obfuscator status 2>&1)
if echo "${obf_status}" | grep -q "dead"; then
log "⚠ wg-obfuscator остановлен, перезапускаем..."
/opt/etc/init.d/S49wg-obfuscator start
sleep 2
log "✓ wg-obfuscator перезапущен"
else
log "✓ wg-obfuscator работает"
fi
fi
log ""
log "Ожидание применения конфигурации..."
sleep 5
if verify_interface_created "${CLIENT_NAME}"; then
log ""
log "╔════════════════════════════════════════════════════════════╗"
log "║ WireGuard успешно настроен! ║"
log "╚════════════════════════════════════════════════════════════╝"
log ""
exit 0
else
log ""
log "⚠ Не удалось подтвердить создание интерфейса WireGuard"
log ""
log "Проверьте вручную в веб-панели Keenetic:"
log " Интернет → WireGuard"
log ""
exit 1
fi
}
main "$@"

View File

@@ -1,15 +0,0 @@
[Unit]
Description=Phobos VPN Web Panel
After=network.target wg-quick@wg0.service
Wants=wg-quick@wg0.service
[Service]
Type=simple
WorkingDirectory=/opt/phobos-panel
ExecStart=/usr/bin/gunicorn -w 1 --threads 20 -b 0.0.0.0:8443 app:app
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target

View File

@@ -1,126 +0,0 @@
#!/bin/bash
# ============================================================
# PCA Phobos — promote / sync каналов (maintainer-инструмент)
#
# Каналы и репозитории:
# stable -> публичный andrey271192/PCA_Phobos ветка main
# beta -> приватный andrey271192/PCA_Phobos-dev ветка beta
# dev -> приватный andrey271192/PCA_Phobos-dev ветка dev
#
# Поток фич — строго в одну сторону: dev -> beta -> stable
# Хотфикс в stable -> forward-port в beta и dev (чтоб не потерялся).
#
# Использование (GH_TOKEN в env):
# GH_TOKEN=ghp_xxx ./promote.sh status
# GH_TOKEN=ghp_xxx ./promote.sh dev2beta
# GH_TOKEN=ghp_xxx ./promote.sh beta2stable 1.3.0
# GH_TOKEN=ghp_xxx ./promote.sh forward # main->beta->dev (после хотфикса)
#
# Мёрж обычный (без -X). При конфликте — НЕ пушит, оставляет рабочую копию
# в $WORK для ручного разбора. Версии (VERSION) выставляются по каналу.
# ============================================================
set -e
GH_TOKEN="${GH_TOKEN:-}"
[ -n "$GH_TOKEN" ] || { echo "Нужен GH_TOKEN (env)."; exit 1; }
PUB="https://andrey271192:${GH_TOKEN}@github.com/andrey271192/PCA_Phobos.git"
PRIV="https://andrey271192:${GH_TOKEN}@github.com/andrey271192/PCA_Phobos-dev.git"
WORK="${PCA_WORK:-/tmp/pca-promote-work}"
repo_for() { case "$1" in main) echo "$PUB";; beta|dev) echo "$PRIV";; esac; }
init_work() {
rm -rf "$WORK"; mkdir -p "$WORK"
git -C "$WORK" init -q
git -C "$WORK" config user.email "maintainer@pca"
git -C "$WORK" config user.name "PCA promote"
}
co_branch() { # $1=branch -> checkout local copy of remote branch
local br="$1" repo; repo=$(repo_for "$br")
git -C "$WORK" fetch -q "$repo" "$br"
git -C "$WORK" checkout -q -B "$br" FETCH_HEAD
}
ver_now() { tr -d ' \r\n' < "$WORK/VERSION" 2>/dev/null; }
do_merge() { # $1=incoming-ref-msg label ; merges FETCH_HEAD into current branch
if ! git -C "$WORK" merge --no-edit -m "$1" FETCH_HEAD; then
echo ""
echo "!!! КОНФЛИКТ. Не запушено. Разрули вручную:"
echo " cd $WORK && git status # правь файлы, git add, git commit"
echo " затем git push <repo> <branch>"
exit 1
fi
}
case "${1:-status}" in
status)
init_work
echo "== Версии каналов =="
for br in main beta dev; do
co_branch "$br" >/dev/null 2>&1 || { printf " %-7s (нет)\n" "$br"; continue; }
printf " %-7s %s (%s)\n" "$br" "$(ver_now)" "$(git -C "$WORK" rev-parse --short HEAD)"
done
echo "Поток: dev -> beta -> stable. После хотфикса в stable: ./promote.sh forward"
;;
dev2beta)
init_work
echo "== Промоут dev -> beta =="
co_branch beta
git -C "$WORK" fetch -q "$PRIV" dev
DEVVER=$(git -C "$WORK" show FETCH_HEAD:VERSION | tr -d ' \r\n')
do_merge "promote: merge dev into beta"
BETAVER="${DEVVER%-dev}-beta"
echo "$BETAVER" > "$WORK/VERSION"
git -C "$WORK" commit -q -am "promote: beta VERSION -> $BETAVER" || true
git -C "$WORK" push -q "$PRIV" beta:beta
git -C "$WORK" tag -f "v$BETAVER" >/dev/null; git -C "$WORK" push -fq "$PRIV" "v$BETAVER"
echo "OK -> beta = $BETAVER (тег v$BETAVER)"
;;
beta2stable)
STABLEVER="${2:-}"
[ -n "$STABLEVER" ] || { echo "Укажи версию: ./promote.sh beta2stable 1.3.0"; exit 1; }
init_work
echo "== Релиз beta -> stable (v$STABLEVER) =="
co_branch main
git -C "$WORK" fetch -q "$PRIV" beta
do_merge "release: merge beta into stable v$STABLEVER"
echo "$STABLEVER" > "$WORK/VERSION"
git -C "$WORK" commit -q -am "release: stable VERSION -> $STABLEVER" || true
git -C "$WORK" push -q "$PUB" main:main
git -C "$WORK" tag -f "v$STABLEVER" >/dev/null; git -C "$WORK" push -fq "$PUB" "v$STABLEVER"
echo "OK -> stable = $STABLEVER (тег v$STABLEVER). Дальше: ./promote.sh forward"
;;
forward)
init_work
echo "== Forward-port stable -> beta -> dev =="
# main -> beta
co_branch beta
BETAVER=$(ver_now)
git -C "$WORK" fetch -q "$PUB" main
do_merge "forward: merge stable into beta"
echo "$BETAVER" > "$WORK/VERSION"; git -C "$WORK" commit -q -am "forward: keep beta VERSION $BETAVER" || true
git -C "$WORK" push -q "$PRIV" beta:beta
echo " beta <- stable OK"
# beta -> dev
co_branch dev
DEVVER=$(ver_now)
git -C "$WORK" fetch -q "$PRIV" beta
do_merge "forward: merge beta into dev"
echo "$DEVVER" > "$WORK/VERSION"; git -C "$WORK" commit -q -am "forward: keep dev VERSION $DEVVER" || true
git -C "$WORK" push -q "$PRIV" dev:dev
echo " dev <- beta OK. Forward-port завершён."
;;
*)
echo "promote.sh — продвижение каналов PCA Phobos"
echo " status версии всех каналов"
echo " dev2beta dev -> beta (кандидат)"
echo " beta2stable <version> beta -> stable (релиз + тег)"
echo " forward stable -> beta -> dev (после хотфикса)"
;;
esac

View File

@@ -1,262 +0,0 @@
#!/usr/bin/env python3
"""Phobos Secondary Server API — peer management + health."""
import json, os, subprocess, time, hmac
from pathlib import Path
from flask import Flask, request, jsonify
app = Flask(__name__)
def load_env():
env = {}
with open("/opt/Phobos/server/server.env") as f:
for line in f:
if "=" in line and not line.startswith("#"):
k, v = line.strip().split("=", 1)
env[k] = v
return env
def check_api_key():
env = load_env()
key = request.headers.get("X-API-Key", "")
return hmac.compare_digest(key, env.get("MAIN_API_KEY", ""))
def default_iface():
try:
out = subprocess.check_output(["ip", "route", "get", "8.8.8.8"], text=True, timeout=4)
parts = out.split()
for i, part in enumerate(parts[:-1]):
if part == "dev":
return parts[i + 1].strip()
except Exception:
pass
return ""
def cpu_percent():
try:
with open("/proc/stat") as fh:
a = fh.readline().split()[1:]
time.sleep(0.1)
with open("/proc/stat") as fh:
b = fh.readline().split()[1:]
a = [int(x) for x in a]
b = [int(x) for x in b]
idle = b[3] - a[3]
total = sum(b) - sum(a)
if total <= 0:
return "?"
return f"{round(100 - (idle * 100.0 / total), 1)}%"
except Exception:
return "?"
def net_bytes():
try:
iface = default_iface()
with open("/proc/net/dev") as fh:
for line in fh:
if line.split(":")[0].strip() == iface:
p = line.split(":")[1].split()
return {"iface": iface, "rx": int(p[0]), "tx": int(p[8]), "ts": int(time.time())}
except Exception:
pass
return {}
@app.route("/api/health")
def health():
try:
out = subprocess.check_output(["wg", "show", "wg0", "dump"], text=True, timeout=5)
peer_keys = []
handshakes = {}
for line in out.strip().split("\n")[1:]:
parts = line.split("\t")
if len(parts) >= 4:
pub = parts[0]
peer_keys.append(pub)
try:
handshakes[pub] = int(parts[4])
except Exception:
handshakes[pub] = 0
peers = len(peer_keys)
except Exception:
peers = 0
peer_keys = []
handshakes = {}
cpu = cpu_percent()
try:
parts = Path("/proc/meminfo").read_text().splitlines()
vals = {}
for line in parts:
k, v = line.split(":", 1)
vals[k] = int(v.strip().split()[0])
total = vals.get("MemTotal", 0) // 1024
avail = vals.get("MemAvailable", 0) // 1024
used = max(total - avail, 0)
mem_str = f"{used}/{total}MB"
except Exception:
mem_str = "?"
return jsonify({"status": "ok", "peers": peers, "peer_keys": peer_keys, "handshakes": handshakes, "cpu": cpu, "mem": mem_str, "net": net_bytes()})
@app.route("/api/obf-health")
def obf_health():
"""Return 200 only when all local wg-obfuscator-* services are active."""
try:
listing = subprocess.check_output(
["systemctl", "list-units", "--type=service", "--no-legend",
"wg-obfuscator-*"], text=True, timeout=5)
names = [ln.split()[0] for ln in listing.splitlines() if ln.strip()]
except Exception:
names = []
if not names:
env = load_env()
ports = [p.strip() for p in env.get("OBFUSCATOR_PORTS", "2083,5443,993").split(",") if p.strip()]
names = [f"wg-obfuscator-{p}.service" for p in ports]
bad = []
for name in names:
status = subprocess.getoutput("systemctl is-active " + name).strip()
if status != "active":
bad.append(name + "=" + status)
if bad:
return ("obf-down: " + ",".join(bad), 503, {"Content-Type": "text/plain"})
return ("obf-ok " + str(len(names)), 200, {"Content-Type": "text/plain"})
@app.route("/api/peers", methods=["GET"])
def list_peers():
if not check_api_key():
return jsonify({"error": "unauthorized"}), 401
try:
out = subprocess.check_output(["wg", "show", "wg0", "allowed-ips"], text=True, timeout=5)
peers = {}
for line in out.strip().split("\n"):
if "\t" in line:
pub, ips = line.split("\t", 1)
peers[pub.strip()] = ips.strip()
return jsonify({"peers": peers})
except Exception as e:
return jsonify({"error": str(e)}), 500
LIMIT_SCRIPT = "/opt/Phobos/server/phobos-limit.sh"
def ensure_script(path):
"""Fetch a helper script from the main panel if this agent predates it."""
if os.path.exists(path):
return
env = load_env()
url = "%s://%s:%s/server/%s" % (env.get("MAIN_PROTO", "http"), env.get("MAIN_SERVER", ""),
env.get("MAIN_PORT", ""), os.path.basename(path))
subprocess.run(["curl", "-kfsSL", "-m", "20", url, "-o", path], timeout=30)
if os.path.exists(path):
os.chmod(path, 0o755)
@app.route("/api/speedtest", methods=["POST"])
def speedtest():
if not check_api_key():
return jsonify({"error": "unauthorized"}), 401
path = "/opt/Phobos/server/phobos-speedtest.sh"
ensure_script(path)
try:
r = subprocess.run([path], capture_output=True, text=True, timeout=80)
return jsonify(json.loads(r.stdout.strip().splitlines()[-1]))
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/limits", methods=["POST"])
def set_limits():
"""Per-client speed limits pushed by the main panel ("<tunnel_ip> <mbit>" lines)."""
if not check_api_key():
return jsonify({"error": "unauthorized"}), 401
text = (request.get_json(force=True, silent=True) or {}).get("limits", "")
Path("/opt/Phobos/server/limits.conf").write_text(text)
ensure_script(LIMIT_SCRIPT)
try:
r = subprocess.run([LIMIT_SCRIPT, "apply"], capture_output=True, text=True, timeout=30)
return jsonify({"status": "ok" if r.returncode == 0 else "error", "out": (r.stdout + r.stderr)[-500:]})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/peers/add", methods=["POST"])
def add_peer():
if not check_api_key():
return jsonify({"error": "unauthorized"}), 401
data = request.get_json(force=True, silent=True) or {}
pub_key = data.get("public_key", "")
allowed_ips = data.get("allowed_ips", "")
if not pub_key or not allowed_ips:
return jsonify({"error": "missing public_key or allowed_ips"}), 400
try:
subprocess.run(["wg", "set", "wg0", "peer", pub_key, "allowed-ips", allowed_ips], check=True, timeout=5)
subprocess.run(["wg-quick", "save", "wg0"], timeout=5)
return jsonify({"status": "ok"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/peers/remove", methods=["POST"])
def remove_peer():
if not check_api_key():
return jsonify({"error": "unauthorized"}), 401
data = request.get_json(force=True, silent=True) or {}
pub_key = data.get("public_key", "")
if not pub_key:
return jsonify({"error": "missing public_key"}), 400
try:
subprocess.run(["wg", "set", "wg0", "peer", pub_key, "remove"], check=True, timeout=5)
subprocess.run(["wg-quick", "save", "wg0"], timeout=5)
return jsonify({"status": "ok"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/info")
def info():
if not check_api_key():
return jsonify({"error": "unauthorized"}), 401
env = load_env()
return jsonify({
"ip": env.get("SERVER_PUBLIC_IP_V4"),
"wg_public_key": env.get("SERVER_WG_PUBLIC_KEY"),
"obfuscator_key": env.get("OBFUSCATOR_KEY"),
"ports": env.get("OBFUSCATOR_PORTS", "2083").split(","),
"role": "secondary"
})
ROUTER_CONFIGS_DIR = "/opt/Phobos/server/router-configs"
@app.route("/api/router-config/<client_id>")
def router_config(client_id):
"""Serve a client's failover.conf so routers can PULL via the tunnel
(http://<server-tunnel-ip>:8444/...). Token via ?token= or X-API-Key."""
token = request.args.get("token", "") or request.headers.get("X-API-Key", "")
if token != load_env().get("MAIN_API_KEY", ""):
return ("forbidden", 403)
path = os.path.join(ROUTER_CONFIGS_DIR, client_id + ".conf")
if not os.path.exists(path):
return ("not found", 404)
with open(path) as fh:
return (fh.read(), 200, {"Content-Type": "text/plain; charset=utf-8"})
@app.route("/api/router-config-set", methods=["POST"])
def router_config_set():
"""Panel fan-out: store a client's failover.conf on this server."""
if not check_api_key():
return jsonify({"status": "error", "msg": "unauthorized"}), 403
data = request.get_json(force=True, silent=True) or {}
cid = data.get("client_id", "")
conf = data.get("conf", "")
if not cid or "SERVER_1=" not in conf:
return jsonify({"status": "error", "msg": "bad payload"}), 400
os.makedirs(ROUTER_CONFIGS_DIR, exist_ok=True)
with open(os.path.join(ROUTER_CONFIGS_DIR, cid + ".conf"), "w") as fh:
fh.write(conf)
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8444)

File diff suppressed because it is too large Load Diff

View File

@@ -1,93 +0,0 @@
#!/bin/bash
# ============================================================
# PCA Phobos — полный бэкап / восстановление сервера
#
# phobos-backup create # создать бэкап -> /opt/Phobos/backups/*.tar.gz
# phobos-backup restore <file> # восстановить из бэкапа
# phobos-backup list # список бэкапов
#
# В бэкап входит ВСЁ состояние (НЕ бинарники/пакеты):
# WireGuard ключ сервера (wg0.conf), server.env (OBF/WG ключи),
# обфускатор-конфиги, все клиенты+ключи, токены, settings.json панели
# (router_access, метки, назначения, токен подписчика, версия/канал/порт).
#
# ⚠ Бэкап содержит ПРИВАТНЫЕ КЛЮЧИ — храни в защищённом месте.
#
# Disaster recovery на чистом VPS:
# 1) поставить turnkey: bash <(curl -fsSL .../install.sh) (ставит бинарники/сервисы)
# 2) залить бэкап и: phobos-backup restore phobos-backup-*.tar.gz
# Ключ сервера восстановится — клиенты переподключатся без пересоздания.
# ============================================================
set -u
PHOBOS_DIR=/opt/Phobos
PANEL_DIR=/opt/phobos-panel
BACKUP_DIR="$PHOBOS_DIR/backups"
panel_service_name() {
for name in phobos-dev-test phobos-panel; do
systemctl status "$name" >/dev/null 2>&1 && { echo "$name"; return; }
done
echo phobos-panel
}
PATHS=(
/etc/wireguard/wg0.conf
"$PHOBOS_DIR/server/server.env"
"$PHOBOS_DIR/clients"
"$PHOBOS_DIR/tokens"
"$PHOBOS_DIR/server/warp"
"$PANEL_DIR/settings.json"
"$PANEL_DIR/servers.json"
"$PANEL_DIR/.version"
"$PANEL_DIR/.channel"
"$PANEL_DIR/.port"
"$PANEL_DIR/.scheme"
"$PANEL_DIR/.gh_token"
)
case "${1:-}" in
create)
mkdir -p "$BACKUP_DIR"
OUT="$BACKUP_DIR/phobos-backup-$(hostname -s 2>/dev/null || echo srv)-$(date +%Y%m%d-%H%M%S).tar.gz"
# -P: absolute paths (restore lands in original locations). obfuscator confs by glob.
EXIST=()
for p in "${PATHS[@]}"; do [ -e "$p" ] && EXIST+=("$p"); done
for f in "$PHOBOS_DIR"/server/wg-obfuscator-*.conf; do [ -e "$f" ] && EXIST+=("$f"); done
tar -czf "$OUT" -P --ignore-failed-read "${EXIST[@]}" 2>/dev/null
chmod 600 "$OUT"
echo "$OUT"
;;
restore)
FILE="${2:-}"
[ -f "$FILE" ] || { echo "Файл не найден: $FILE"; exit 1; }
# sanity: must be a phobos backup (contains server.env or wg0.conf)
if ! tar -tzf "$FILE" 2>/dev/null | grep -qE 'server\.env|wg0\.conf'; then
echo "Не похоже на бэкап PCA Phobos (нет server.env/wg0.conf). Отмена."; exit 1
fi
# safety snapshot before overwrite
PRE="$BACKUP_DIR/pre-restore-$(date +%Y%m%d-%H%M%S).tar.gz"
mkdir -p "$BACKUP_DIR"
PEX=()
for p in "${PATHS[@]}"; do [ -e "$p" ] && PEX+=("$p"); done
for f in "$PHOBOS_DIR"/server/wg-obfuscator-*.conf; do [ -e "$f" ] && PEX+=("$f"); done
[ ${#PEX[@]} -gt 0 ] && tar -czf "$PRE" -P --ignore-failed-read "${PEX[@]}" 2>/dev/null && echo "Снимок до восстановления: $PRE"
tar -xzf "$FILE" -P
chmod 600 /etc/wireguard/wg0.conf 2>/dev/null || true
systemctl restart wg-quick@wg0 2>/dev/null || true
for s in $(systemctl list-units --all --plain --no-legend 'wg-obfuscator-*' 2>/dev/null | awk '{print $1}'); do
systemctl restart "$s" 2>/dev/null || true
done
systemctl restart "$(panel_service_name)" 2>/dev/null || true
echo "Восстановлено из $FILE. Сервисы перезапущены."
;;
list)
echo "Бэкапы в $BACKUP_DIR:"
ls -lht "$BACKUP_DIR"/phobos-backup-*.tar.gz 2>/dev/null | awk '{print " "$9" ("$5")"}' || echo " нет"
;;
*)
echo "phobos-backup — бэкап/восстановление сервера PCA Phobos"
echo " create создать бэкап"
echo " restore <file> восстановить"
echo " list список бэкапов"
;;
esac

View File

@@ -1,93 +0,0 @@
#!/usr/bin/env bash
# PCA Phobos — cascade relay. Run on an ENTRY VPS (e.g. in Russia, "white" IP):
# clients connect to this server, UDP obfuscator ports are forwarded to the EXIT
# Phobos server abroad. The entry box needs no Phobos, no keys, no panel.
# bash <(curl -fsSL .../server/phobos-cascade.sh) enable <exit_host> [ports]
# phobos-cascade.sh disable | status | apply
# Then in the exit server panel: Settings -> "Адрес для клиентов" -> entry IP/domain.
set -uo pipefail
STATE=/etc/phobos-cascade.env
BIN=/usr/local/sbin/phobos-cascade
SELF_URL="${SELF_URL:-https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/server/phobos-cascade.sh}"
TAG="phobos-cascade"
clear_rules() {
local t
for t in nat filter; do
iptables -t $t -S 2>/dev/null | grep -- "--comment $TAG" | sed 's/^-A /-D /' | \
while read -r rule; do eval iptables -t $t $rule 2>/dev/null; done
done
}
apply() {
[ -f "$STATE" ] || { echo "cascade: not configured"; return 0; }
. "$STATE"
local ip p
ip=$(getent ahostsv4 "$EXIT_HOST" | awk 'NR==1{print $1}')
[ -n "$ip" ] || { echo "cascade: cannot resolve $EXIT_HOST" >&2; return 1; }
sysctl -qw net.ipv4.ip_forward=1
clear_rules
for p in ${PORTS//,/ }; do
iptables -t nat -A PREROUTING -p udp --dport "$p" -m comment --comment $TAG -j DNAT --to-destination "$ip:$p"
iptables -t nat -A POSTROUTING -p udp -d "$ip" --dport "$p" -m comment --comment $TAG -j MASQUERADE
iptables -I FORWARD -p udp -d "$ip" --dport "$p" -m comment --comment $TAG -j ACCEPT
iptables -I FORWARD -p udp -s "$ip" --sport "$p" -m comment --comment $TAG -j ACCEPT
done
echo "cascade: udp ${PORTS} -> ${EXIT_HOST} (${ip})"
}
enable() {
local host="${1:-}" ports="${2:-2083,5443,993}"
[ -n "$host" ] || { echo "usage: $0 enable <exit_host> [ports]" >&2; exit 2; }
[[ "$ports" =~ ^[0-9]+(,[0-9]+)*$ ]] || { echo "bad ports: $ports" >&2; exit 2; }
command -v iptables >/dev/null || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq iptables >/dev/null
printf 'EXIT_HOST=%s\nPORTS=%s\n' "$host" "$ports" > "$STATE"
# bash <(curl ...) gives a pipe as $0 -> fetch a real copy for the systemd unit
if [ "$(readlink -f "$0")" != "$BIN" ]; then
if [ -f "$0" ]; then cp "$0" "$BIN"; else curl -kfsSL -m30 "$SELF_URL" -o "$BIN" || { echo "cannot save $BIN" >&2; exit 1; }; fi
chmod +x "$BIN"
fi
cat > /etc/systemd/system/phobos-cascade.service <<EOF
[Unit]
Description=PCA Phobos cascade relay
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=$BIN apply
ExecStop=$BIN stop
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable phobos-cascade.service >/dev/null 2>&1
systemctl restart phobos-cascade.service
apply >/dev/null
status
}
disable() {
systemctl disable --now phobos-cascade.service >/dev/null 2>&1 || true
clear_rules
rm -f /etc/systemd/system/phobos-cascade.service "$STATE"
systemctl daemon-reload
echo "cascade: disabled"
}
status() {
local host="" ports="" rules
[ -f "$STATE" ] && . "$STATE" && host="$EXIT_HOST" && ports="$PORTS"
rules=$(iptables -t nat -S PREROUTING 2>/dev/null | grep -c -- "--comment $TAG")
printf '{"enabled": %s, "exit_host": "%s", "ports": "%s", "rules": %s}\n' \
"$([ "$rules" -gt 0 ] && echo true || echo false)" "$host" "$ports" "$rules"
}
case "${1:-status}" in
enable) enable "${2:-}" "${3:-}" ;;
apply) apply ;;
stop) clear_rules ;;
disable) disable ;;
status) status ;;
*) echo "usage: $0 enable <exit_host> [ports]|disable|status"; exit 2 ;;
esac

View File

@@ -1,105 +0,0 @@
#!/usr/bin/env bash
# PCA Phobos — panel domain: https://<domain>/ via nginx + Let's Encrypt.
# The panel keeps listening on its old http port, so routers/secondaries that
# already know http://IP:PORT keep working; the domain is an extra entrance.
# phobos-domain.sh set <domain> [email] (SELF_SIGNED=1 for LAN tests)
# phobos-domain.sh remove | status
set -uo pipefail
PANEL_DIR=/opt/phobos-panel
SITE=/etc/nginx/sites-available/phobos-domain
WEBROOT=/var/www/phobos-acme
SS_DIR=/etc/phobos-domain
panel_port() { cat "$PANEL_DIR/.port" 2>/dev/null || echo 8443; }
panel_scheme() { cat "$PANEL_DIR/.scheme" 2>/dev/null || echo http; }
write_site() { # $1=domain $2=cert $3=key ($2 empty -> http only, for ACME)
local d="$1" up
up="$(panel_scheme)://127.0.0.1:$(panel_port)"
{
echo "server {"
echo " listen 80;"
echo " listen [::]:80;"
echo " server_name $d;"
echo " location /.well-known/acme-challenge/ { root $WEBROOT; }"
[ -n "$2" ] && echo " location / { return 301 https://\$host\$request_uri; }"
echo "}"
if [ -n "$2" ]; then
cat <<EOF
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name $d;
ssl_certificate $2;
ssl_certificate_key $3;
client_max_body_size 64m;
location / {
proxy_pass $up;
proxy_ssl_verify off;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-For \$remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
}
EOF
fi
} > "$SITE"
ln -sf "$SITE" /etc/nginx/sites-enabled/phobos-domain
# nginx < 1.25 has no "http2 on;" -> fall back to the old listen flag
nginx -t >/dev/null 2>&1 || sed -i -e '/http2 on;/d' -e 's/listen 443 ssl;/listen 443 ssl http2;/' -e 's/listen \[::\]:443 ssl;/listen [::]:443 ssl http2;/' "$SITE"
nginx -t >/dev/null 2>&1 || { nginx -t; return 1; }
systemctl reload nginx
}
set_domain() {
local d="${1:-}" email="${2:-}" cert key
[[ "$d" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?\.[A-Za-z]{2,}$ ]] || { echo "bad domain: $d" >&2; exit 2; }
command -v nginx >/dev/null || { echo "nginx not installed" >&2; exit 1; }
mkdir -p "$WEBROOT"
write_site "$d" "" "" || exit 1
if [ "${SELF_SIGNED:-0}" = 1 ]; then
mkdir -p "$SS_DIR"
openssl req -x509 -nodes -newkey rsa:2048 -days 825 -subj "/CN=$d" \
-keyout "$SS_DIR/$d.key" -out "$SS_DIR/$d.crt" >/dev/null 2>&1
cert="$SS_DIR/$d.crt"; key="$SS_DIR/$d.key"
else
command -v certbot >/dev/null || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq certbot >/dev/null
local mail_args=(--register-unsafely-without-email)
[ -n "$email" ] && mail_args=(-m "$email")
certbot certonly --webroot -w "$WEBROOT" -d "$d" --agree-tos -n "${mail_args[@]}" \
--deploy-hook "systemctl reload nginx" || {
echo "certbot failed: check that $d points to this server and port 80 is open" >&2
rm -f /etc/nginx/sites-enabled/phobos-domain; systemctl reload nginx; exit 1; }
cert="/etc/letsencrypt/live/$d/fullchain.pem"; key="/etc/letsencrypt/live/$d/privkey.pem"
fi
write_site "$d" "$cert" "$key" || exit 1
echo "$d" > "$PANEL_DIR/.domain"
echo "domain: https://$d/ -> panel $(panel_scheme)://127.0.0.1:$(panel_port)"
}
remove() {
rm -f /etc/nginx/sites-enabled/phobos-domain "$SITE" "$PANEL_DIR/.domain"
nginx -t >/dev/null 2>&1 && systemctl reload nginx
echo "domain: removed (certificate kept in /etc/letsencrypt)"
}
status() {
local d exp=""
d=$(cat "$PANEL_DIR/.domain" 2>/dev/null)
if [ -n "$d" ]; then
exp=$(echo | timeout 5 openssl s_client -connect 127.0.0.1:443 -servername "$d" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
fi
printf '{"domain": "%s", "cert_expires": "%s"}\n' "$d" "$exp"
}
case "${1:-status}" in
set) set_domain "${2:-}" "${3:-}" ;;
remove) remove ;;
status) status ;;
*) echo "usage: $0 set <domain> [email]|remove|status"; exit 2 ;;
esac

View File

@@ -1,113 +0,0 @@
#!/usr/bin/env bash
# PCA Phobos — guard: drop RKN probes / scanner subnets before they reach the server.
# Idea and IP list: github.com/Loorrr293/Auto_IPtables (list: Loorrr293/blocklist).
# We only download the IP list (data), never third-party code.
# phobos-guard.sh enable [list_url] install nft table + daily timer
# phobos-guard.sh update refresh the list now
# phobos-guard.sh disable remove table + timer
# phobos-guard.sh status JSON
set -uo pipefail
STATE=/etc/phobos-guard.env
DEFAULT_URL="https://raw.githubusercontent.com/Loorrr293/blocklist/main/blocklist.txt"
TABLE="inet phobos_guard"
SELF="$(readlink -f "$0")"
list_url() { [ -f "$STATE" ] && . "$STATE"; echo "${GUARD_URL:-$DEFAULT_URL}"; }
ensure_nft() {
command -v nft >/dev/null 2>&1 && return 0
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nftables >/dev/null 2>&1
}
update_list() {
local url tmp nf
url="$(list_url)"
tmp="$(mktemp)"; nf="$(mktemp)"
curl -fsSL -m60 "$url" -o "$tmp" || { rm -f "$tmp" "$nf"; echo "download failed: $url" >&2; return 1; }
nft add table $TABLE 2>/dev/null || true
nft add set $TABLE v4 '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null || true
nft add set $TABLE v6 '{ type ipv6_addr; flags interval; auto-merge; }' 2>/dev/null || true
nft add chain $TABLE input '{ type filter hook input priority raw; policy accept; }' 2>/dev/null || true
nft list chain $TABLE input | grep -q '@v4' || nft add rule $TABLE input ip saddr @v4 counter drop
nft list chain $TABLE input | grep -q '@v6' || nft add rule $TABLE input ip6 saddr @v6 counter drop
# collapse + skip private/loopback so a bad list can never cut LAN/tunnel/admin
python3 - "$tmp" > "$nf" <<'PY'
import ipaddress, sys
v4, v6 = [], []
for line in open(sys.argv[1], encoding="utf-8", errors="ignore"):
for tok in line.split("#", 1)[0].split():
try:
n = ipaddress.ip_network(tok, strict=False)
except ValueError:
continue
if n.is_private or n.is_loopback or n.is_link_local or n.prefixlen == 0:
continue
(v6 if n.version == 6 else v4).append(n)
print("flush set inet phobos_guard v4")
print("flush set inet phobos_guard v6")
for name, nets in (("v4", v4), ("v6", v6)):
nets = list(ipaddress.collapse_addresses(nets))
for i in range(0, len(nets), 2000):
print("add element inet phobos_guard %s { %s }" % (name, ", ".join(map(str, nets[i:i + 2000]))))
PY
local rc=0
nft -f "$nf" || rc=1
rm -f "$tmp" "$nf"
[ "$rc" = 0 ] && echo "guard: list loaded from $url"
return "$rc"
}
enable() {
ensure_nft || { echo "nftables install failed" >&2; exit 1; }
[ -n "${1:-}" ] && echo "GUARD_URL=$1" > "$STATE" || touch "$STATE"
cat > /etc/systemd/system/phobos-guard.service <<EOF
[Unit]
Description=PCA Phobos guard (RKN scanner blocklist)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=$SELF update
EOF
cat > /etc/systemd/system/phobos-guard.timer <<'EOF'
[Unit]
Description=Refresh PCA Phobos guard list daily
[Timer]
OnBootSec=1min
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now phobos-guard.timer >/dev/null 2>&1
update_list
}
disable() {
systemctl disable --now phobos-guard.timer >/dev/null 2>&1 || true
rm -f /etc/systemd/system/phobos-guard.service /etc/systemd/system/phobos-guard.timer "$STATE"
systemctl daemon-reload
nft delete table $TABLE 2>/dev/null || true
echo "guard: disabled"
}
status() {
local on=false n4=0 drops=0
if nft list table $TABLE >/dev/null 2>&1; then
on=true
n4=$(nft list set $TABLE v4 2>/dev/null | grep -o '[0-9.]*/[0-9]*\|[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*' | wc -l)
drops=$(nft list chain $TABLE input 2>/dev/null | grep -o 'packets [0-9]*' | awk '{s+=$2} END {print s+0}')
fi
printf '{"enabled": %s, "networks": %s, "dropped_packets": %s, "timer": "%s", "url": "%s"}\n' \
"$on" "$n4" "$drops" "$(systemctl is-active phobos-guard.timer 2>/dev/null)" "$(list_url)"
}
case "${1:-status}" in
enable) enable "${2:-}" ;;
update) update_list ;;
disable) disable ;;
status) status ;;
*) echo "usage: $0 enable [url]|update|disable|status"; exit 2 ;;
esac

View File

@@ -1,458 +0,0 @@
#!/bin/sh
# ============================================================
# Phobos Health Monitor v2 — Keenetic Edition
# Uses ndmc/RCI instead of wg-tools. Connectivity-based failover.
# Runs via cron every 60 seconds
# ============================================================
PHOBOS_DIR="/opt/etc/Phobos"
CONF="$PHOBOS_DIR/failover.conf"
STATE="$PHOBOS_DIR/state"
LOG="$PHOBOS_DIR/health.log"
OBF_CONF="$PHOBOS_DIR/wg-obfuscator.conf"
LOCKFILE="/tmp/phobos-health.lock"
WG_IF="Wireguard3"
MAX_LOG_LINES=200
# Thresholds (seconds)
HANDSHAKE_WARN=150
HANDSHAKE_PORT_HOP=300
HANDSHAKE_SERVER_SWITCH=600
PRIMARY_CHECK_INTERVAL=300
# Primary-alive probe: servers block ICMP and busybox `nc -z` is unreliable,
# so reachability is tested with curl against the panel's obfuscator-health
# endpoint on the primary VPS. It returns http 200 ONLY when all
# wg-obfuscator-* services are active — the bare panel port stays up even
# when the tunnel path is dead, so we must NOT switch back on panel liveness
# alone (that caused premature switchback to a dead primary).
PRIMARY_PROBE_SCHEME="${PRIMARY_PROBE_SCHEME:-$(cat "$PHOBOS_DIR/panel_scheme" 2>/dev/null)}"
PRIMARY_PROBE_SCHEME="${PRIMARY_PROBE_SCHEME:-http}"
PRIMARY_PROBE_PORT="${PRIMARY_PROBE_PORT:-$(cat "$PHOBOS_DIR/panel_port" 2>/dev/null)}"
PRIMARY_PROBE_PORT="${PRIMARY_PROBE_PORT:-8443}"
PRIMARY_PROBE_PATH="/api/obf-health"
# Connectivity check targets (logged for context only)
CHECK_HOST_1="8.8.8.8"
CHECK_HOST_2="1.1.1.1"
log() {
echo "$(date '+%H:%M:%S') $1" >> "$LOG"
if [ "$(wc -l < "$LOG" 2>/dev/null)" -gt "$MAX_LOG_LINES" ]; then
tail -n 100 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
fi
}
# Prevent concurrent runs
if [ -f "$LOCKFILE" ]; then
pid=$(cat "$LOCKFILE" 2>/dev/null)
if kill -0 "$pid" 2>/dev/null; then
exit 0
fi
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
mkdir -p "$STATE"
# ── Detect WG interface ──
detect_wg_interface() {
for i in 0 1 2 3 4 5 6 7 8 9; do
desc=$(ndmc -c "show interface Wireguard${i}" 2>/dev/null | grep description | head -1)
if echo "$desc" | grep -qi phobos; then
WG_IF="Wireguard${i}"
return 0
fi
done
return 1
}
# ── Read failover config ──
if [ ! -f "$CONF" ]; then
log "ERROR: no failover.conf"
exit 1
fi
SERVER_COUNT=0
idx=1
while true; do
val=$(grep "^SERVER_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)
[ -z "$val" ] && break
eval "SERVER_${idx}_HOST=$(echo "$val" | cut -d: -f1)"
eval "SERVER_${idx}_PORTS=$(echo "$val" | cut -d: -f2-)"
eval "SERVER_${idx}_KEY=$(grep "^KEY_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)"
eval "SERVER_${idx}_WGKEY=$(grep "^WGKEY_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)"
SERVER_COUNT=$idx
idx=$((idx + 1))
done
if [ "$SERVER_COUNT" -eq 0 ]; then
log "ERROR: no servers in failover.conf"
exit 1
fi
# Read state
CURRENT_SERVER=$(cat "$STATE/current_server" 2>/dev/null || echo "1")
CURRENT_PORT_IDX=$(cat "$STATE/current_port_idx" 2>/dev/null || echo "0")
FAIL_COUNT=$(cat "$STATE/fail_count" 2>/dev/null || echo "0")
PRIMARY_CHECK_TS=$(cat "$STATE/primary_check_ts" 2>/dev/null || echo "0")
# ── Detect Phobos WG interface (non-fatal, fallback to Wireguard3) ──
detect_wg_interface || {
log "WARN: detect failed, using default $WG_IF"
}
# ── Get handshake age via ndmc ──
# A peer switch can leave the removed peer's stale handshake listed in ndmc
# output (often a huge sentinel like 2147483647). Take the FRESHEST (minimum
# positive, sane) handshake across all peers — that is the live tunnel.
get_handshake_age() {
hs=$(ndmc -c "show interface $WG_IF" 2>/dev/null \
| grep "last-handshake" \
| awk '{v=$2} v>0 && v<86400 {print v}' \
| sort -n | head -1)
if [ -z "$hs" ]; then
echo "9999"
else
echo "$hs"
fi
}
# ── Check real connectivity ──
check_connectivity() {
ping -c 1 -W 3 "$CHECK_HOST_1" >/dev/null 2>&1 && return 0
ping -c 1 -W 3 "$CHECK_HOST_2" >/dev/null 2>&1 && return 0
return 1
}
# ── Get port by index ──
get_port() {
server_idx=$1; port_idx=$2
eval "ports=\$SERVER_${server_idx}_PORTS"
echo "$ports" | tr ',' '\n' | sed -n "$((port_idx + 1))p"
}
get_port_count() {
server_idx=$1
eval "ports=\$SERVER_${server_idx}_PORTS"
echo "$ports" | tr ',' '\n' | wc -l
}
# ── Keys currently on the interface (includes the interface's OWN key) ──
# WG public keys are 43 base64 chars + '='. The interface's own public-key is
# also matched here — callers must only act on KNOWN server WGKEYs so the
# local key is never touched.
list_iface_keys() {
ndmc -c "show interface $WG_IF" 2>/dev/null \
| grep -oE '[A-Za-z0-9+/]{43}=' | sort -u
}
# Count how many KNOWN server WGKEYs are currently attached as peers.
count_server_peers() {
present=$(list_iface_keys)
n=0
i=1
while [ "$i" -le "$SERVER_COUNT" ]; do
eval "wk=\$SERVER_${i}_WGKEY"
if [ -n "$wk" ] && echo "$present" | grep -q "^${wk}$"; then
n=$((n + 1))
fi
i=$((i + 1))
done
echo "$n"
}
# ── Switch WG peer so EXACTLY ONE server peer (new_key) remains ──
# Failover adds a new peer; the RCI "remove" is unreliable on Keenetic and
# leaves dead peers behind. Multiple peers each with allow-ips 0.0.0.0/0 make
# egress routing ambiguous. We hard-purge every OTHER known server key via the
# ndmc CLI (reliable) — never the interface's own key — then ensure new_key.
switch_wg_peer() {
new_key=$1
if [ -z "$new_key" ]; then
log "WARN: no WGKEY for target server, skip peer switch"
return 1
fi
present=$(list_iface_keys)
# Purge any OTHER known server key that is attached
i=1
while [ "$i" -le "$SERVER_COUNT" ]; do
eval "wk=\$SERVER_${i}_WGKEY"
if [ -n "$wk" ] && [ "$wk" != "$new_key" ] && echo "$present" | grep -q "^${wk}$"; then
log "WG PEER purge: $wk"
ndmc -c "interface $WG_IF no wireguard peer $wk" >/dev/null 2>&1
fi
i=$((i + 1))
done
# Add the target peer if it is not already present
if ! echo "$present" | grep -q "^${new_key}$"; then
log "WG PEER add: $new_key"
curl -s -X POST "http://localhost:79/rci/" \
-H "Content-Type: application/json" \
-d "{\"interface\":{\"${WG_IF}\":{\"wireguard\":{\"peer\":{\"key\":\"${new_key}\",\"comment\":\"Phobos VPS Server\",\"endpoint\":{\"address\":\"127.0.0.1:13255\"},\"keepalive-interval\":{\"interval\":25},\"allow-ips\":[{\"address\":\"0.0.0.0\",\"mask\":\"0.0.0.0\"},{\"address\":\"::\",\"mask\":\"0\"}]}}}}}" >/dev/null 2>&1
fi
# Persist config
ndmc -c "system configuration save" >/dev/null 2>&1
return 0
}
# ── Switch to specific server:port ──
switch_endpoint() {
server_idx=$1
port_idx=$2
eval "host=\$SERVER_${server_idx}_HOST"
eval "obf_key=\$SERVER_${server_idx}_KEY"
eval "wg_key=\$SERVER_${server_idx}_WGKEY"
port=$(get_port "$server_idx" "$port_idx")
if [ -z "$host" ] || [ -z "$port" ]; then
log "ERROR: invalid server $server_idx port_idx $port_idx"
return 1
fi
log "SWITCH → server $server_idx ($host:$port)"
# 1. Switch WG peer key if different server
switch_wg_peer "$wg_key"
# 2. Update obfuscator config
if [ -f "$OBF_CONF" ]; then
sed -i "s|^target = .*|target = ${host}:${port}|" "$OBF_CONF"
if [ -n "$obf_key" ]; then
sed -i "s|^key = .*|key = ${obf_key}|" "$OBF_CONF"
fi
fi
# 3. Restart obfuscator
if [ -f /opt/etc/init.d/S49wg-obfuscator ]; then
/opt/etc/init.d/S49wg-obfuscator restart >/dev/null 2>&1
else
killall wg-obfuscator 2>/dev/null
sleep 1
wg-obfuscator --config "$OBF_CONF" &
fi
# 4. Save state (fail_count is managed by the caller, NOT reset here —
# resetting on a port-hop would prevent escalation to server failover)
echo "$server_idx" > "$STATE/current_server"
echo "$port_idx" > "$STATE/current_port_idx"
}
# ── Try next port on current server ──
try_next_port() {
port_count=$(get_port_count "$CURRENT_SERVER")
next_idx=$(( (CURRENT_PORT_IDX + 1) % port_count ))
[ "$next_idx" -eq 0 ] && return 1
log "PORT HOP → port idx $next_idx on server $CURRENT_SERVER"
switch_endpoint "$CURRENT_SERVER" "$next_idx"
return 0
}
# ── Try next server ──
try_next_server() {
next=$((CURRENT_SERVER + 1))
[ "$next" -gt "$SERVER_COUNT" ] && next=1
[ "$next" -eq "$CURRENT_SERVER" ] && return 1
log "FAILOVER → server $next"
switch_endpoint "$next" "0"
return 0
}
# ── Check if primary is back ──
check_primary() {
[ "$CURRENT_SERVER" -eq 1 ] && return
now=$(date +%s)
elapsed=$((now - PRIMARY_CHECK_TS))
[ "$elapsed" -lt "$PRIMARY_CHECK_INTERVAL" ] && return
echo "$now" > "$STATE/primary_check_ts"
eval "host=\$SERVER_1_HOST"
# Probe the obfuscator-health endpoint: http 200 ONLY when the primary's
# obfuscator path is actually up. Anything else (000 no-response, 503
# obf-down, redirects) means the tunnel path is NOT viable → stay put.
ok_url=""
last_code=""
for url in "${PRIMARY_PROBE_SCHEME}://${host}:${PRIMARY_PROBE_PORT}${PRIMARY_PROBE_PATH}" "http://${host}:8444${PRIMARY_PROBE_PATH}"; do
code=$(curl -k -s -m 4 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null)
last_code="$code"
if [ "$code" = "200" ]; then
ok_url="$url"
break
fi
done
if [ -n "$ok_url" ]; then
log "PRIMARY ($ok_url http=200) alive, switching back"
switch_endpoint 1 0
echo "0" > "$STATE/fail_count"
else
log "PRIMARY still down (obf-health http=${last_code:-none}), staying on server $CURRENT_SERVER"
fi
}
# ── LAN routing self-heal ──────────────────────
# A router REBOOT silently drops the per-device `ip hotspot host <mac> policy`
# binding and can reset the WG interface security-level, so LAN clients leak to
# WAN (no VPN). This:
# 1) ensures WG_IF security-level = public (needed for masquerade),
# 2) LEARNS current host->policy bindings into state/lan-hosts (append-only),
# 3) RE-APPLIES any learned binding that is currently missing.
# Append-only learning means a reboot (which clears live bindings) never erases
# the record, so the next run restores them. ndmc show running-config is heavy,
# so the caller gates this to run only every few minutes.
heal_lan_routing() {
LANHOSTS="$STATE/lan-hosts"
rc=$(ndmc -c "show running-config" 2>/dev/null)
[ -z "$rc" ] && return 0
# 1) WG interface must be public
sl=$(echo "$rc" | awk -v ifc="interface $WG_IF" '$0~ifc{f=1} f&&/security-level/{print $2; exit}')
if [ -n "$sl" ] && [ "$sl" != "public" ]; then
ndmc -c "interface $WG_IF security-level public" >/dev/null 2>&1
ndmc -c "system configuration save" >/dev/null 2>&1
log "HEAL: $WG_IF security-level -> public"
fi
# 2) learn live bindings (append-only) into lan-hosts
touch "$LANHOSTS"
echo "$rc" | grep -oE "host [0-9a-f:]+ policy [A-Za-z0-9_]+" | while read -r _h mac _p pol; do
grep -q "^$mac " "$LANHOSTS" 2>/dev/null || echo "$mac $pol" >> "$LANHOSTS"
done
# 3) re-apply any learned binding that is missing live
changed=0
while read -r mac pol; do
[ -z "$mac" ] && continue
case "$mac" in \#*) continue;; esac
if ! echo "$rc" | grep -q "host $mac policy $pol"; then
ndmc -c "ip hotspot host $mac policy $pol" >/dev/null 2>&1
log "HEAL: re-bound host $mac -> $pol"
changed=1
fi
done < "$LANHOSTS"
[ "$changed" = 1 ] && ndmc -c "system configuration save" >/dev/null 2>&1
}
# ══════════════════════════════════════════════
# Explicit apply hook (used by phobos-pull.sh after a config change).
# `phobos-health.sh apply-server N` re-points the tunnel to SERVER_N
# immediately, skipping the failure-escalation logic. Reuses
# switch_endpoint so the WG-peer/obfuscator/state changes stay identical
# to a normal failover. Resets fail_count so the new server starts clean.
# ══════════════════════════════════════════════
if [ "$1" = "apply-server" ]; then
idx="${2:-1}"
eval "ahost=\$SERVER_${idx}_HOST"
if [ -z "$ahost" ]; then
log "APPLY: server $idx not in conf, ignore"
exit 1
fi
log "APPLY: pull requested server $idx ($ahost)"
switch_endpoint "$idx" "0"
echo "0" > "$STATE/fail_count"
echo "$idx" > "$STATE/current_server"
exit 0
fi
# ══════════════════════════════════════════════
# Pre-check: fix desync (obfuscator targeting wrong server)
# ══════════════════════════════════════════════
if [ -f "$OBF_CONF" ]; then
cur_target=$(grep "^target = " "$OBF_CONF" 2>/dev/null | sed 's/target = //')
eval "expected_host=\$SERVER_${CURRENT_SERVER}_HOST"
if [ -n "$cur_target" ] && [ -n "$expected_host" ]; then
echo "$cur_target" | grep -q "$expected_host" || {
log "DESYNC: obf=$cur_target state=server${CURRENT_SERVER}($expected_host). Resync."
switch_endpoint "$CURRENT_SERVER" "$CURRENT_PORT_IDX"
sleep 5
}
fi
fi
# ── LAN routing self-heal (gated ~5 min; a reboot drops host->policy bindings) ──
HEAL_TS=$(cat "$STATE/heal_ts" 2>/dev/null || echo 0)
now_heal=$(date +%s)
if [ $((now_heal - HEAL_TS)) -ge 300 ]; then
echo "$now_heal" > "$STATE/heal_ts"
heal_lan_routing
fi
# ── Peer hygiene: keep exactly ONE WG peer = current server's key ──
# Failovers can leave dead/duplicate peers; multiple 0.0.0.0/0 peers cause
# ambiguous egress routing. Self-heal here every run (cheap when already clean).
eval "cur_wgkey=\$SERVER_${CURRENT_SERVER}_WGKEY"
if [ -n "$cur_wgkey" ]; then
peer_n=$(count_server_peers)
if [ "$peer_n" -gt 1 ]; then
log "HYGIENE: $peer_n server peers present, purging to server${CURRENT_SERVER}"
switch_wg_peer "$cur_wgkey"
fi
fi
# ══════════════════════════════════════════════
# Main logic
# Tunnel health = WireGuard handshake age (authoritative: a fresh
# handshake only happens through the full obfuscator→server→WG path).
# WAN ping is logged for context ONLY — it does NOT gate decisions,
# because the router's default route is not the tunnel, so WAN can be
# up while the tunnel is dead (and a WAN blip must not cause failover).
# ══════════════════════════════════════════════
AGE=$(get_handshake_age)
WAN=$(check_connectivity && echo "yes" || echo "no")
# Tunnel healthy = fresh handshake
if [ "$AGE" -lt "$HANDSHAKE_WARN" ]; then
if [ "$FAIL_COUNT" -gt 0 ]; then
log "OK: tunnel up (handshake=${AGE}s, server=$CURRENT_SERVER, wan=$WAN)"
echo "0" > "$STATE/fail_count"
fi
check_primary
exit 0
fi
# Stale handshake = tunnel down → escalate.
# fail_count is MONOTONIC: it climbs across stages and is only reset by the
# OK branch (real recovery) or after a successful failover to a NEW server
# (so the new server gets a fresh restart→port-hop→failover cycle).
FAIL_COUNT=$((FAIL_COUNT + 1))
echo "$FAIL_COUNT" > "$STATE/fail_count"
log "STALE: handshake=${AGE}s wan=${WAN} server=$CURRENT_SERVER port=$CURRENT_PORT_IDX fails=$FAIL_COUNT"
# Stage 1 (fail 1): Restart obfuscator
if [ "$FAIL_COUNT" -eq 1 ]; then
log "ACTION: restart obfuscator"
if [ -f /opt/etc/init.d/S49wg-obfuscator ]; then
/opt/etc/init.d/S49wg-obfuscator restart >/dev/null 2>&1
else
killall wg-obfuscator 2>/dev/null
sleep 1
wg-obfuscator --config "$OBF_CONF" &
fi
exit 0
fi
# Stage 2 (fail 2): Port hop to an alternate port on the SAME server
if [ "$FAIL_COUNT" -eq 2 ]; then
if try_next_port; then
exit 0
fi
# only one port → fall through to server failover
log "single port, escalate to failover"
fi
# Stage 3 (fail 3+): Server failover. Reset fail_count so the new server
# gets its own restart→port-hop→failover cycle next ticks.
log "ACTION: server failover (fail $FAIL_COUNT)"
if try_next_server; then
echo "0" > "$STATE/fail_count"
else
# Only one server — cycle back to port 0 and restart escalation
echo "0" > "$STATE/fail_count"
switch_endpoint "$CURRENT_SERVER" "0"
fi

View File

@@ -1,50 +0,0 @@
#!/usr/bin/env bash
# PCA Phobos — per-client speed limit on wg0 (tc).
# limits file: one line per client "<tunnel_ip> <mbit>" (download = upload = mbit)
# phobos-limit.sh apply [file] rebuild tc from file (default below)
# phobos-limit.sh clear | status
set -uo pipefail
DEV="${WG_DEV:-wg0}"
FILE="${2:-/opt/Phobos/server/limits.conf}"
clear_tc() {
tc qdisc del dev "$DEV" root 2>/dev/null
tc qdisc del dev "$DEV" ingress 2>/dev/null
return 0
}
apply() {
ip link show "$DEV" >/dev/null 2>&1 || { echo "limit: $DEV is down"; return 0; }
clear_tc
[ -s "$FILE" ] || { echo "limit: no limits"; return 0; }
tc qdisc add dev "$DEV" root handle 1: htb default 9999 r2q 1000
tc class add dev "$DEV" parent 1: classid 1:9999 htb rate 10gbit 2>/dev/null
tc qdisc add dev "$DEV" handle ffff: ingress
local n=10 ip mbit count=0
while read -r ip mbit _; do
[[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ && "$mbit" =~ ^[0-9]+$ && "$mbit" -gt 0 ]] || continue
# download (server -> client): shaped, packets queue instead of dropping
tc class add dev "$DEV" parent 1: classid 1:$n htb rate "${mbit}mbit" ceil "${mbit}mbit" burst 64k
tc qdisc add dev "$DEV" parent 1:$n fq_codel 2>/dev/null
tc filter add dev "$DEV" parent 1: protocol ip prio 1 u32 match ip dst "$ip/32" flowid 1:$n
# upload (client -> server): policed on ingress
tc filter add dev "$DEV" parent ffff: protocol ip prio 1 u32 match ip src "$ip/32" \
police rate "${mbit}mbit" burst $(( mbit * 64 + 256 ))k drop flowid :1
n=$((n + 1)); count=$((count + 1))
done < "$FILE"
echo "limit: $count client(s) limited on $DEV"
}
status() {
local cnt
cnt=$(tc class show dev "$DEV" 2>/dev/null | grep -c 'htb 1:[1-9][0-9]* ' )
cnt=$((cnt > 0 ? cnt - 1 : 0))
printf '{"device": "%s", "limited": %s, "file": "%s"}\n' "$DEV" "$cnt" "$FILE"
}
case "${1:-status}" in
apply) apply ;;
clear) clear_tc; echo "limit: cleared" ;;
status) status ;;
*) echo "usage: $0 apply [file]|clear|status"; exit 2 ;;
esac

View File

@@ -1,118 +0,0 @@
#!/bin/sh
# ============================================================
# Phobos Config Pull — NAT-friendly management channel.
# Runs ON the router via cron (every 2 min). OUTBOUND ONLY:
# fetches this client's failover.conf from the panel over
# HTTPS/HTTP and applies it. No inbound SSH needed, so it works
# behind any NAT and regardless of which Phobos server is active.
#
# Why pull (not panel->router SSH):
# - routers sit behind NAT with no public IP (KeenDNS is HTTP-
# only, no port 22),
# - WG3 must be security-level PUBLIC for LAN client routing,
# which blocks inbound SSH on the tunnel,
# - on failover the router's 10.25.0.2 moves to another server's
# wg0, so a fixed panel host cannot reach it.
# Pulling sidesteps all three.
#
# Files (written by installer / bootstrap):
# /opt/etc/Phobos/client_id -> this router's client id (e.g. home)
# /opt/etc/Phobos/pull_token -> shared secret for the panel endpoint
# PANEL env or default below -> panel base URL
# ============================================================
PHOBOS_DIR="/opt/etc/Phobos"
CONF="$PHOBOS_DIR/failover.conf"
HEALTH="$PHOBOS_DIR/phobos-health.sh"
LOG="$PHOBOS_DIR/health.log"
# Config sources, tried in order. TUNNEL FIRST: panel_tunnel_ip is the wg0 of
# whatever server the tunnel currently terminates on, so management rides the
# same obfuscated channel as data — survives a public-IP ban and follows
# failover.
# - panel_tunnel_ip:8444 -> secondary server agent (phobos-api)
# - panel_tunnel_ip:<panel_port> -> primary server panel
# - public panel IP -> bootstrap / if tunnel is down
PANEL_TUNNEL_IP="${PANEL_TUNNEL_IP:-$(cat "$PHOBOS_DIR/panel_tunnel_ip" 2>/dev/null)}"
[ -n "$PANEL_TUNNEL_IP" ] || PANEL_TUNNEL_IP="10.25.0.1"
PANEL="${PANEL:-$(cat "$PHOBOS_DIR/panel_url" 2>/dev/null)}"
PANEL_URLS="${PANEL_URLS:-$(cat "$PHOBOS_DIR/panel_urls" 2>/dev/null)}"
[ -n "$PANEL_URLS" ] || PANEL_URLS="http://${PANEL_TUNNEL_IP}:8444 $PANEL"
CLIENT_ID=$(cat "$PHOBOS_DIR/client_id" 2>/dev/null || echo "home")
TOKEN=$(cat "$PHOBOS_DIR/pull_token" 2>/dev/null)
TMP="/tmp/failover.conf.pull"
LOCK="/tmp/phobos-pull.lock"
log() { echo "$(date '+%H:%M:%S') $1" >> "$LOG"; }
# single instance
if [ -f "$LOCK" ]; then
pid=$(cat "$LOCK" 2>/dev/null)
kill -0 "$pid" 2>/dev/null && exit 0
fi
echo $$ > "$LOCK"
trap 'rm -f "$LOCK"' EXIT
[ -z "$TOKEN" ] && exit 0
detect_tunnel_iface() {
ip route get "$PANEL_TUNNEL_IP" 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="dev") print $(i+1)}' | head -1
}
fetch_config() {
base="$1"
url="${base}/api/router-config/${CLIENT_ID}?token=${TOKEN}"
case "$base" in
"http://${PANEL_TUNNEL_IP}:"*|"https://${PANEL_TUNNEL_IP}:"*)
iface=$(detect_tunnel_iface)
if [ -n "$iface" ] && command -v curl >/dev/null; then
curl -k --interface "$iface" -s -m 8 -o "$TMP" "$url" 2>/dev/null && return 0
fi
;;
esac
if command -v curl >/dev/null; then
curl -k -s -m 8 -o "$TMP" "$url" 2>/dev/null && return 0
fi
wget --no-check-certificate -q -O "$TMP" "$url" 2>/dev/null || wget -q -O "$TMP" "$url" 2>/dev/null
}
# One fetch + compare + apply pass. Returns 0 always (best-effort).
do_pull() {
# Try each source (tunnel first); accept first that returns a valid conf.
got=0
for base in $PANEL_URLS; do
fetch_config "$base" || continue
if grep -q "^SERVER_1=" "$TMP" 2>/dev/null; then got=1; break; fi
done
[ "$got" = 1 ] || { rm -f "$TMP"; return 0; }
new=$(md5sum "$TMP" 2>/dev/null | cut -d' ' -f1)
old=$(md5sum "$CONF" 2>/dev/null | cut -d' ' -f1)
if [ "$new" = "$old" ]; then
rm -f "$TMP"
return 0
fi
old_s1=$(grep "^SERVER_1=" "$CONF" 2>/dev/null | cut -d= -f2- | cut -d: -f1)
new_s1=$(grep "^SERVER_1=" "$TMP" 2>/dev/null | cut -d= -f2- | cut -d: -f1)
cp "$CONF" "$CONF.prev" 2>/dev/null
mv "$TMP" "$CONF"
log "PULL: failover.conf updated (SERVER_1 ${old_s1:-?} -> ${new_s1:-?})"
# apply the new primary now (only if it actually changed)
if [ "$old_s1" != "$new_s1" ] && [ -f "$HEALTH" ]; then
sh "$HEALTH" apply-server 1
fi
}
# Inner loop: cron fires this every 60s, but we poll ~4x per minute so a
# panel "Set" applies within ~12-15s instead of up to a full minute. The
# loop stays UNDER 60s and exits so the next cron tick takes over cleanly
# (the lockfile blocks any overlap). POLL_INTERVAL/POLL_PASSES overridable.
POLL_INTERVAL="${POLL_INTERVAL:-12}"
POLL_PASSES="${POLL_PASSES:-4}"
i=1
while [ "$i" -le "$POLL_PASSES" ]; do
do_pull
[ "$i" -lt "$POLL_PASSES" ] && sleep "$POLL_INTERVAL"
i=$((i + 1))
done

View File

@@ -1,203 +0,0 @@
#!/usr/bin/env python3
"""
Phobos Router Watchdog (server-side).
Problem it solves: on some Keenetic firmware (seen on 5.1 Beta), after a reboot
the Entware /opt disk mounts but the init hook (rc.unslung) does NOT run, so the
obfuscator + cron + dropbear never start and the router's Phobos tunnel stays
down. Because cron itself didn't start, the on-router self-heal can't help.
This watchdog runs on the primary server (cron, every few minutes). For each
router that has KeenDNS web access configured, it checks whether the client has
a fresh WG handshake on ANY server. If a router has been offline past a grace
period, it logs into the router's web UI over KeenDNS (ndm challenge auth) and
re-triggers the opkg init (which runs rc.unslung -> starts everything). Sends a
Telegram note on down / recovery / action.
Per-router config lives in /opt/phobos-panel/settings.json under
router_access[<client_id>]:
keendns_host : e.g. "homesmart.netcraze.pro"
web_login : Keenetic web user
web_pass : Keenetic web password
opkg_disk : opkg disk id, e.g. "EXT4-XXXX:/" (default below)
Routers without these fields are skipped (watchdog is opt-in per router).
"""
import json, os, time, ssl, hashlib, http.cookiejar, urllib.request, urllib.error, subprocess
SETTINGS = "/opt/phobos-panel/settings.json"
SERVERS_FILE = "/opt/phobos-panel/servers.json"
CLIENTS_DIR = "/opt/Phobos/clients"
STATE_FILE = "/opt/Phobos/server/watchdog-state.json"
LOG = "/opt/Phobos/server/watchdog.log"
OFFLINE_SECS = int(os.environ.get("WD_OFFLINE_SECS", "300")) # offline if newest handshake older than this
COOLDOWN = int(os.environ.get("WD_COOLDOWN", "600")) # min seconds between recovery attempts per router
DEFAULT_DISK = "EXT4-V88axM0d:/"
def log(msg):
try:
with open(LOG, "a") as f:
f.write(time.strftime("%Y-%m-%d %H:%M:%S ") + msg + "\n")
except Exception:
pass
def load(path, default):
try:
with open(path) as f:
return json.load(f)
except Exception:
return default
def tg(token, chat, text):
if not token or not chat:
return
try:
url = f"https://api.telegram.org/bot{token}/sendMessage"
data = json.dumps({"chat_id": chat, "text": text}).encode()
urllib.request.urlopen(urllib.request.Request(
url, data=data, headers={"Content-Type": "application/json"}), timeout=8)
except Exception:
pass
def client_pub(cid):
try:
return json.load(open(f"{CLIENTS_DIR}/{cid}/metadata.json")).get("public_key", "")
except Exception:
return ""
def newest_handshake_age(pub, servers):
"""Smallest handshake age (s) for pub across local wg0 + secondary agents."""
best = 99999
try:
out = subprocess.check_output(["wg", "show", "wg0", "dump"], text=True, timeout=5)
for ln in out.strip().split("\n")[1:]:
f = ln.split("\t")
if f and f[0] == pub and len(f) >= 5 and f[4].isdigit() and int(f[4]) > 0:
best = min(best, int(time.time()) - int(f[4]))
except Exception:
pass
for srv in servers:
try:
req = urllib.request.Request(f"http://{srv['ip']}:8444/api/health",
headers={"X-API-Key": srv.get("api_key", "")})
d = json.loads(urllib.request.urlopen(req, timeout=5).read())
ts = d.get("handshakes", {}).get(pub, 0)
if ts:
best = min(best, int(time.time()) - int(ts))
except Exception:
pass
return best
def rci_session(host, login, pw):
"""Keenetic ndm challenge auth over KeenDNS. Returns (opener, base) or (None, None)."""
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
cj = http.cookiejar.CookieJar()
op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj),
urllib.request.HTTPSHandler(context=ctx))
base = f"https://{host}"
realm = chal = None
try:
op.open(base + "/auth", timeout=10)
except urllib.error.HTTPError as e:
realm = e.headers.get("X-NDM-Realm")
chal = e.headers.get("X-NDM-Challenge")
except Exception:
return None, None
if not realm or not chal:
return None, None
md5 = hashlib.md5(f"{login}:{realm}:{pw}".encode()).hexdigest()
sha = hashlib.sha256((chal + md5).encode()).hexdigest()
body = json.dumps({"login": login, "password": sha}).encode()
try:
op.open(urllib.request.Request(base + "/auth", data=body,
headers={"Content-Type": "application/json"}, method="POST"), timeout=10)
except Exception:
return None, None
return op, base
def retrigger_opkg(op, base, disk):
"""Force opkg 'disk changed' so Keenetic re-runs initrc (rc.unslung)."""
cur = ""
try:
cur = json.loads(op.open(base + "/rci/show/rc/opkg", timeout=8).read()).get("disk", {}).get("disk", "")
except Exception:
pass
newdisk = disk
if cur.strip() == disk.strip():
newdisk = disk.rstrip("/") if disk.endswith("/") else disk + "/"
body = json.dumps([{"opkg": {"disk": newdisk}},
{"system": {"configuration": {"save": {}}}}]).encode()
try:
op.open(urllib.request.Request(base + "/rci/", data=body,
headers={"Content-Type": "application/json"}, method="POST"), timeout=20)
return True
except Exception:
return False
def main():
s = load(SETTINGS, {})
st = load(STATE_FILE, {})
token = s.get("tg_bot_token")
chat = s.get("tg_chat_id")
servers = load(SERVERS_FILE, [])
ra = s.get("router_access", {})
now = int(time.time())
changed = False
for cid, acc in ra.items():
host = acc.get("keendns_host")
login = acc.get("web_login")
pw = acc.get("web_pass")
disk = acc.get("opkg_disk", DEFAULT_DISK)
if not (host and login and pw):
continue
pub = client_pub(cid)
if not pub:
continue
age = newest_handshake_age(pub, servers)
rec = st.get(cid, {})
if age <= OFFLINE_SECS:
if rec.get("offline"):
log(f"{cid}: recovered (handshake {age}s)")
tg(token, chat, f"✅ Router {cid} recovered (handshake {age}s).")
st[cid] = {"offline": False, "last_recover": rec.get("last_recover", 0)}
changed = True
continue
# offline
if now - rec.get("last_recover", 0) < COOLDOWN:
continue
op, base = rci_session(host, login, pw)
if not op:
if not rec.get("offline"):
log(f"{cid}: offline, web unreachable")
tg(token, chat, f"\U0001F534 Router {cid} OFFLINE, web unreachable (powered off / no internet?).")
st[cid] = {"offline": True, "last_recover": rec.get("last_recover", 0)}
changed = True
continue
ok = retrigger_opkg(op, base, disk)
log(f"{cid}: offline ({age}s), re-triggered opkg via RCI -> {'ok' if ok else 'FAIL'}")
tg(token, chat, f"\U0001F6E0 Router {cid} Entware down (reboot didn't autostart) — re-triggered via RCI ({'ok' if ok else 'FAILED'}).")
st[cid] = {"offline": True, "last_recover": now}
changed = True
if changed:
try:
json.dump(st, open(STATE_FILE, "w"))
except Exception:
pass
if __name__ == "__main__":
main()

View File

@@ -1,117 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
PHOBOS_DIR="${PHOBOS_DIR:-/opt/Phobos}"
SERVER_ENV="${SERVER_ENV:-$PHOBOS_DIR/server/server.env}"
REPO_DIR="${REPO_DIR:-$PHOBOS_DIR/repo}"
WG_IFACE="${WG_IFACE:-wg0}"
FIX=0
for arg in "$@"; do
[ "$arg" = "--fix" ] && FIX=1
done
log() { echo "[self-check] $*"; }
fail() { echo "[self-check][ERROR] $*" >&2; exit 1; }
warn() { echo "[self-check][WARN] $*" >&2; }
env_get() {
local key="$1"
[ -f "$SERVER_ENV" ] || return 0
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
"$key="*) printf '%s\n' "${line#*=}"; return 0 ;;
esac
done < "$SERVER_ENV"
}
env_set() {
local key="$1" value="$2" tmp
[ -f "$SERVER_ENV" ] || fail "server.env не найден: $SERVER_ENV"
tmp="$(mktemp)"
awk -v k="$key" -v v="$value" '
BEGIN { done=0 }
$0 ~ "^" k "=" { print k "=" v; done=1; next }
{ print }
END { if (!done) print k "=" v }
' "$SERVER_ENV" > "$tmp"
cat "$tmp" > "$SERVER_ENV"
rm -f "$tmp"
chmod 600 "$SERVER_ENV" 2>/dev/null || true
}
normalize_wg_key() {
python3 - "$1" <<'PY'
import sys
value = (sys.argv[1] or "").strip()
alphabet = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=")
if value and value.lower() != "none" and all(ch in alphabet for ch in value):
padded = value + ("=" * ((4 - len(value) % 4) % 4))
if len(padded) == 44:
value = padded
print(value)
PY
}
check_env_key() {
local key="$1" value normalized
value="$(env_get "$key" || true)"
[ -n "$value" ] || fail "$key пустой или отсутствует в server.env"
normalized="$(normalize_wg_key "$value")"
if [ "$normalized" != "$value" ]; then
if [ "$FIX" = 1 ]; then
env_set "$key" "$normalized"
log "$key: восстановлен base64 padding"
value="$normalized"
else
fail "$key имеет длину ${#value}, нужен padding. Запусти phobos-self-check --fix"
fi
fi
[ "${#value}" = 44 ] || fail "$key имеет длину ${#value}, ожидается 44"
}
check_public_key_matches_wg() {
local env_pub wg_pub
env_pub="$(env_get SERVER_WG_PUBLIC_KEY || true)"
wg_pub="$(wg show "$WG_IFACE" public-key 2>/dev/null || true)"
[ -n "$wg_pub" ] || return 0
if [ "$env_pub" != "$wg_pub" ]; then
if [ "$FIX" = 1 ]; then
env_set SERVER_WG_PUBLIC_KEY "$wg_pub"
log "SERVER_WG_PUBLIC_KEY: обновлен из wg show $WG_IFACE"
else
fail "SERVER_WG_PUBLIC_KEY не совпадает с wg show $WG_IFACE"
fi
fi
}
check_router_templates() {
local tpl="$REPO_DIR/client/templates" missing=0 file
for file in install-router.sh.template lib-client.sh install-obfuscator.sh install-wireguard.sh; do
if [ ! -s "$tpl/$file" ]; then
warn "нет обязательного router helper: $tpl/$file"
missing=1
fi
done
[ "$missing" = 0 ] || fail "router package будет битый. Выполни phobos-update stable|beta|dev"
}
check_scripts_syntax() {
local script
for script in \
"$REPO_DIR/server/scripts/lib-core.sh" \
"$REPO_DIR/server/scripts/phobos-client.sh" \
"$REPO_DIR/client/templates/install-router.sh.template" \
"$REPO_DIR/client/templates/lib-client.sh" \
"$REPO_DIR/client/templates/install-obfuscator.sh" \
"$REPO_DIR/client/templates/install-wireguard.sh"; do
[ -f "$script" ] && bash -n "$script"
done
}
[ -f "$SERVER_ENV" ] || fail "server.env не найден: $SERVER_ENV"
check_env_key SERVER_WG_PUBLIC_KEY
check_public_key_matches_wg
check_router_templates
check_scripts_syntax
log "OK"

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# PCA Phobos — real server internet speed: download/upload actual bytes, print JSON.
# Cloudflare speed endpoints first, Hetzner/OVH files as download fallback.
# phobos-speedtest.sh [seconds]
set -uo pipefail
T="${1:-8}"
CF="https://speed.cloudflare.com"
down_mbit() { # $1=url -> Mbit/s
curl -s -o /dev/null -m "$T" -w '%{speed_download}' "$1" 2>/dev/null | awk '{printf "%.1f", $1*8/1e6}'
}
ping_ms() {
curl -s -o /dev/null -m 5 -w '%{time_connect}' "$CF/__down?bytes=0" 2>/dev/null | awk '{printf "%.0f", $1*1000}'
}
dl=$(down_mbit "$CF/__down?bytes=500000000")
src="cloudflare"
if [ -z "$dl" ] || awk "BEGIN{exit !($dl < 1)}"; then
for u in https://speed.hetzner.de/1GB.bin https://proof.ovh.net/files/1Gb.dat; do
dl=$(down_mbit "$u"); src="$u"
awk "BEGIN{exit !($dl >= 1)}" && break
done
fi
ul=$(head -c 200000000 /dev/zero | curl -s -o /dev/null -m "$T" -w '%{speed_upload}' \
-X POST --data-binary @- "$CF/__up" 2>/dev/null | awk '{printf "%.1f", $1*8/1e6}')
printf '{"download_mbit": %s, "upload_mbit": %s, "ping_ms": %s, "source": "%s", "ts": %s}\n' \
"${dl:-0}" "${ul:-0}" "$(ping_ms || echo 0)" "$src" "$(date +%s)"

View File

@@ -1,65 +0,0 @@
#!/usr/bin/env bash
# PCA Phobos — TCP fallback: clients whose UDP is cut reach the obfuscator over TCP.
# udptcp -s 0.0.0.0:<TCP_PORT> -> 127.0.0.1:<first obfuscator port> (UDP).
# phobos-tcp.sh enable [port] pick a free port (443, 8443, 2096) unless given; idempotent
# phobos-tcp.sh disable | status
set -uo pipefail
PHOBOS_DIR=/opt/Phobos
ENV="$PHOBOS_DIR/server/server.env"
BIN="$PHOBOS_DIR/bin/udptcp"
env_get() { grep -E "^$1=" "$ENV" 2>/dev/null | tail -1 | cut -d= -f2-; }
env_set() {
grep -v -E "^$1=" "$ENV" > "$ENV.tmp" 2>/dev/null || true
echo "$1=$2" >> "$ENV.tmp" && mv "$ENV.tmp" "$ENV"
}
port_busy() { ss -lntH "sport = :$1" 2>/dev/null | grep -q .; }
enable() {
[ -x "$BIN" ] || { echo "udptcp binary missing: $BIN (phobos-update)" >&2; exit 1; }
local port="${1:-$(env_get TCP_PORT)}" obf
obf=$(env_get OBFUSCATOR_PORTS | cut -d, -f1); obf="${obf:-2083}"
# keep our own port if the service already holds it
if [ -z "$port" ] || { port_busy "$port" && ! systemctl is-active -q phobos-tcp; }; then
port=""
for p in 443 8443 2096 4433; do port_busy "$p" || { port=$p; break; }; done
fi
[ -n "$port" ] || { echo "no free TCP port (443/8443/2096/4433)" >&2; exit 1; }
env_set TCP_PORT "$port"
cat > /etc/systemd/system/phobos-tcp.service <<EOF
[Unit]
Description=PCA Phobos TCP fallback (udptcp -> obfuscator :$obf)
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=$BIN -s 0.0.0.0:$port 127.0.0.1:$obf
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable -q phobos-tcp
systemctl restart phobos-tcp
sleep 1
status
}
disable() {
systemctl disable --now phobos-tcp >/dev/null 2>&1 || true
rm -f /etc/systemd/system/phobos-tcp.service
systemctl daemon-reload
echo "tcp fallback: disabled"
}
status() {
printf '{"enabled": %s, "port": "%s"}\n' \
"$(systemctl is-active -q phobos-tcp && echo true || echo false)" "$(env_get TCP_PORT)"
}
case "${1:-status}" in
enable) enable "${2:-}" ;;
disable) disable ;;
status) status ;;
*) echo "usage: $0 enable [port]|disable|status"; exit 2 ;;
esac

View File

@@ -1,519 +0,0 @@
#!/bin/bash
set -euo pipefail
SERVER_ENV="/opt/Phobos/server/server.env"
WARP_DIR="/opt/Phobos/server/warp"
WGCF_BIN="/usr/local/bin/wgcf"
WARP_IF="warp"
WARP_CONF="/etc/wireguard/${WARP_IF}.conf"
WARP_TABLE="51820"
WARP_RULE_PRIO="11930"
STATE_FILE="${WARP_DIR}/state.env"
FREE_PROFILE="${WARP_DIR}/wgcf-profile.conf"
CUSTOM_PROFILE="${WARP_DIR}/custom-profile.conf"
LICENSE_FILE="${WARP_DIR}/license.key"
RELAY_TAG_PREFIX="PHOBOS_WARP_RELAY"
load_env() {
if [ -f "$SERVER_ENV" ]; then
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|\#*) continue ;;
esac
k="${line%%=*}"
v="${line#*=}"
case "$k" in
''|\#*) continue ;;
esac
export "$k=$v"
done < "$SERVER_ENV"
fi
}
load_state() {
STATE_MODE=""
STATE_PROFILE_SOURCE=""
STATE_RELAY_PORTS=""
STATE_RELAY_TARGET=""
STATE_RELAY_TARGET_IP=""
STATE_CLIENTS=""
if [ -f "$STATE_FILE" ]; then
# shellcheck disable=SC1090
. "$STATE_FILE"
STATE_MODE="${MODE:-}"
STATE_PROFILE_SOURCE="${PROFILE_SOURCE:-}"
STATE_RELAY_PORTS="${RELAY_PORTS:-}"
STATE_RELAY_TARGET="${RELAY_TARGET:-}"
STATE_RELAY_TARGET_IP="${RELAY_TARGET_IP:-}"
STATE_CLIENTS="${CLIENTS:-}"
fi
}
save_state() {
mkdir -p "$WARP_DIR"
cat > "$STATE_FILE" <<EOF
MODE=${1:-}
PROFILE_SOURCE=${2:-}
RELAY_PORTS=${3:-}
RELAY_TARGET=${4:-}
RELAY_TARGET_IP=${5:-}
CLIENTS=${6:-}
EOF
}
server_public_ip() {
if [ -n "${SERVER_PUBLIC_IP_V4:-}" ]; then
printf '%s\n' "$SERVER_PUBLIC_IP_V4"
else
curl -4 -fsS --max-time 6 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}'
fi
}
wg_subnet() {
if [ -n "${SERVER_WG_IPV4_NETWORK:-}" ]; then
printf '%s\n' "$SERVER_WG_IPV4_NETWORK"
return
fi
if [ -f /etc/wireguard/wg0.conf ]; then
python3 - <<'PY' /etc/wireguard/wg0.conf
import ipaddress, re, sys
for raw in open(sys.argv[1], encoding="utf-8"):
m = re.match(r"^Address\s*=\s*(.+)$", raw.strip())
if not m:
continue
value = m.group(1).split(",", 1)[0].strip()
print(ipaddress.ip_interface(value).network)
break
PY
return
fi
printf '10.25.0.0/16\n'
}
arch_name() {
case "$(uname -m)" in
x86_64|amd64) echo "amd64" ;;
aarch64|arm64) echo "arm64" ;;
armv7l) echo "armv7" ;;
*) echo "" ;;
esac
}
ensure_wgcf() {
if [ -x "$WGCF_BIN" ]; then
return
fi
local arch version url
arch="$(arch_name)"
[ -n "$arch" ] || { echo "unsupported architecture: $(uname -m)"; exit 1; }
version="2.2.26"
url="https://github.com/ViRb3/wgcf/releases/download/v${version}/wgcf_${version}_linux_${arch}"
curl -fsSL "$url" -o "$WGCF_BIN"
chmod +x "$WGCF_BIN"
}
ensure_free_profile() {
local license_key=""
mkdir -p "$WARP_DIR"
cd "$WARP_DIR"
ensure_wgcf
if [ ! -f "${WARP_DIR}/wgcf-account.toml" ]; then
"$WGCF_BIN" register --accept-tos >/tmp/phobos-warp-register.log 2>&1
fi
if [ -s "$LICENSE_FILE" ]; then
license_key="$(tr -d '\r\n' < "$LICENSE_FILE")"
if [ -n "$license_key" ]; then
"$WGCF_BIN" update --license "$license_key" >/tmp/phobos-warp-license.log 2>&1 || {
echo "failed to apply WARP+ license key" >&2
cat /tmp/phobos-warp-license.log >&2 || true
exit 1
}
rm -f "$FREE_PROFILE"
fi
fi
if [ ! -f "$FREE_PROFILE" ] || [ ! -s "$FREE_PROFILE" ]; then
"$WGCF_BIN" generate >/tmp/phobos-warp-generate.log 2>&1 || {
echo "failed to generate wgcf profile" >&2
cat /tmp/phobos-warp-generate.log >&2 || true
exit 1
}
fi
}
ensure_custom_profile() {
[ -s "$CUSTOM_PROFILE" ] || { echo "custom profile not found: $CUSTOM_PROFILE" >&2; exit 1; }
}
profile_path_for() {
case "${1:-free}" in
custom) ensure_custom_profile; printf '%s\n' "$CUSTOM_PROFILE" ;;
*) ensure_free_profile; printf '%s\n' "$FREE_PROFILE" ;;
esac
}
profile_endpoint() {
awk -F'= *' '/^Endpoint *=/{print $2; exit}' "$1" | tr -d ' '
}
resolve_host_ipv4() {
getent ahostsv4 "$1" | awk 'NR==1{print $1}'
}
build_warp_conf() {
local profile_path subnet
profile_path="$1"
subnet="$(wg_subnet)"
python3 - "$profile_path" "$WARP_CONF" "$subnet" "$WARP_TABLE" "$WARP_RULE_PRIO" <<'PY'
import sys
from pathlib import Path
profile_path = Path(sys.argv[1])
out_path = Path(sys.argv[2])
subnet = sys.argv[3]
table = sys.argv[4]
prio = sys.argv[5]
section = None
data = {"Interface": {}, "Peer": {}}
for raw in profile_path.read_text().splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
if line.startswith("[") and line.endswith("]"):
section = line.strip("[]")
continue
if "=" not in line or section not in data:
continue
k, v = [x.strip() for x in line.split("=", 1)]
data[section][k] = v
iface = data["Interface"]
peer = data["Peer"]
post_up = (
"sysctl -w net.ipv4.ip_forward=1 >/dev/null; "
f"ip route replace {subnet} dev wg0 table {table}; "
f"ip route replace default dev %i table {table}; "
"iptables -C FORWARD -i wg0 -o %i -j ACCEPT 2>/dev/null || iptables -I FORWARD 1 -i wg0 -o %i -j ACCEPT; "
"iptables -C FORWARD -i %i -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || "
"iptables -I FORWARD 1 -i %i -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; "
f"iptables -t nat -C POSTROUTING -s {subnet} -o %i -j MASQUERADE 2>/dev/null || "
f"iptables -t nat -A POSTROUTING -s {subnet} -o %i -j MASQUERADE"
)
post_down = (
f"ip route del {subnet} dev wg0 table {table} 2>/dev/null || true; "
f"ip route del default dev %i table {table} 2>/dev/null || true; "
"iptables -D FORWARD -i wg0 -o %i -j ACCEPT 2>/dev/null || true; "
"iptables -D FORWARD -i %i -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true; "
f"iptables -t nat -D POSTROUTING -s {subnet} -o %i -j MASQUERADE 2>/dev/null || true"
)
lines = [
"[Interface]",
f"PrivateKey = {iface.get('PrivateKey', '')}",
f"Address = {iface.get('Address', '')}",
"MTU = 1280",
"Table = off",
f"PostUp = {post_up}",
f"PostDown = {post_down}",
"",
"[Peer]",
f"PublicKey = {peer.get('PublicKey', '')}",
f"AllowedIPs = {peer.get('AllowedIPs', '0.0.0.0/0, ::/0')}",
f"Endpoint = {peer.get('Endpoint', '')}",
f"PersistentKeepalive = {peer.get('PersistentKeepalive', '25')}",
"",
]
out_path.write_text("\n".join(lines))
PY
chmod 600 "$WARP_CONF"
}
clear_egress() {
systemctl disable --now "wg-quick@${WARP_IF}" >/dev/null 2>&1 || true
ip route del "$(wg_subnet)" dev wg0 table "$WARP_TABLE" 2>/dev/null || true
ip route del default dev "$WARP_IF" table "$WARP_TABLE" 2>/dev/null || true
iptables -D FORWARD -i wg0 -o "$WARP_IF" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -i "$WARP_IF" -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
iptables -t nat -D POSTROUTING -s "$(wg_subnet)" -o "$WARP_IF" -j MASQUERADE 2>/dev/null || true
}
clear_legacy_subnet_rule() {
local subnet
subnet="$(wg_subnet)"
ip rule del from "$subnet" table "$WARP_TABLE" priority "$WARP_RULE_PRIO" 2>/dev/null || true
ip rule del from "$subnet" lookup "$WARP_TABLE" 2>/dev/null || true
}
client_rule_prio() {
python3 - "$1" "$WARP_RULE_PRIO" <<'PY'
import sys
ip = sys.argv[1].split('/')[0].strip()
base = int(sys.argv[2])
parts = [int(x) for x in ip.split('.')]
value = ((parts[2] << 8) + parts[3]) % 5000
print(base + 1 + value)
PY
}
normalize_client_ip() {
printf '%s\n' "${1%%/*}" | tr -d ' '
}
state_clients_csv_add() {
local ip csv
ip="$(normalize_client_ip "$1")"
csv="${2:-}"
python3 - "$ip" "$csv" <<'PY'
import sys
ip = sys.argv[1]
items = [x for x in sys.argv[2].split(',') if x]
if ip not in items:
items.append(ip)
print(",".join(items))
PY
}
state_clients_csv_del() {
local ip csv
ip="$(normalize_client_ip "$1")"
csv="${2:-}"
python3 - "$ip" "$csv" <<'PY'
import sys
ip = sys.argv[1]
items = [x for x in sys.argv[2].split(',') if x and x != ip]
print(",".join(items))
PY
}
clear_client_rules() {
load_state
local ip prio
IFS=',' read -r -a __clients <<< "${STATE_CLIENTS:-}"
for ip in "${__clients[@]}"; do
[ -n "$ip" ] || continue
prio="$(client_rule_prio "$ip")"
ip rule del from "${ip}/32" table "$WARP_TABLE" priority "$prio" 2>/dev/null || true
done
}
reapply_client_rules() {
local csv="${1:-}" ip prio
IFS=',' read -r -a __clients <<< "$csv"
for ip in "${__clients[@]}"; do
[ -n "$ip" ] || continue
prio="$(client_rule_prio "$ip")"
ip rule add from "${ip}/32" table "$WARP_TABLE" priority "$prio" 2>/dev/null || true
done
}
relay_ports_array() {
local raw="${1:-4500}"
raw="${raw// /}"
IFS=',' read -r -a RELAY_PORTS_ARRAY <<< "$raw"
}
relay_rule_tag() {
printf '%s:%s:%s:%s\n' "$RELAY_TAG_PREFIX" "$1" "$2" "$3"
}
clear_relay() {
load_state
local lines line cmd
lines="$(iptables-save -t nat 2>/dev/null | grep -F "$RELAY_TAG_PREFIX" || true)"
if [ -n "$lines" ]; then
while IFS= read -r line; do
[ -n "$line" ] || continue
case "$line" in
-A\ *) cmd="iptables -t nat ${line/-A /-D }"; eval "$cmd" >/dev/null 2>&1 || true ;;
esac
done <<EOF
$lines
EOF
fi
lines="$(iptables-save 2>/dev/null | grep -F "$RELAY_TAG_PREFIX" || true)"
if [ -n "$lines" ]; then
while IFS= read -r line; do
[ -n "$line" ] || continue
case "$line" in
-A\ FORWARD\ *) cmd="iptables ${line/-A /-D }"; eval "$cmd" >/dev/null 2>&1 || true ;;
esac
done <<EOF
$lines
EOF
fi
}
disable_all() {
clear_legacy_subnet_rule
clear_client_rules
clear_egress
clear_relay
}
relay_enabled() {
iptables-save 2>/dev/null | grep -q "$RELAY_TAG_PREFIX"
}
enable_egress() {
local profile_source profile_path existing_clients
load_env
load_state
existing_clients="${STATE_CLIENTS:-}"
profile_source="${1:-free}"
disable_all
profile_path="$(profile_path_for "$profile_source")"
build_warp_conf "$profile_path"
sysctl -w net.ipv4.ip_forward=1 >/dev/null
systemctl enable "wg-quick@${WARP_IF}" >/dev/null 2>&1 || true
systemctl restart "wg-quick@${WARP_IF}"
clear_legacy_subnet_rule
reapply_client_rules "$existing_clients"
save_state "egress" "$profile_source" "" "" "" "$existing_clients"
status_json
}
enable_relay() {
local profile_source ports target profile_path endpoint host port_num target_ip server_ip port tag
load_env
profile_source="${1:-free}"
ports="${2:-4500}"
target="${3:-}"
disable_all
profile_path="$(profile_path_for "$profile_source")"
endpoint="${target:-$(profile_endpoint "$profile_path")}"
[ -n "$endpoint" ] || { echo "relay target endpoint is empty" >&2; exit 1; }
host="${endpoint%:*}"
port_num="${endpoint##*:}"
target_ip="$(resolve_host_ipv4 "$host")"
[ -n "$target_ip" ] || { echo "failed to resolve endpoint host: $host" >&2; exit 1; }
server_ip="$(server_public_ip)"
sysctl -w net.ipv4.ip_forward=1 >/dev/null
relay_ports_array "$ports"
for port in "${RELAY_PORTS_ARRAY[@]}"; do
[ -n "$port" ] || continue
tag="$(relay_rule_tag "$port" "$target_ip" "$port_num")"
iptables -t nat -C PREROUTING -d "$server_ip" -p udp --dport "$port" -m comment --comment "$tag" -j DNAT --to-destination "$target_ip:$port_num" 2>/dev/null \
|| iptables -t nat -A PREROUTING -d "$server_ip" -p udp --dport "$port" -m comment --comment "$tag" -j DNAT --to-destination "$target_ip:$port_num"
iptables -t nat -C POSTROUTING -p udp -d "$target_ip" --dport "$port_num" -m comment --comment "$tag" -j MASQUERADE 2>/dev/null \
|| iptables -t nat -A POSTROUTING -p udp -d "$target_ip" --dport "$port_num" -m comment --comment "$tag" -j MASQUERADE
iptables -C FORWARD -p udp -d "$target_ip" --dport "$port_num" -m comment --comment "$tag" -j ACCEPT 2>/dev/null \
|| iptables -A FORWARD -p udp -d "$target_ip" --dport "$port_num" -m comment --comment "$tag" -j ACCEPT
iptables -C FORWARD -p udp -s "$target_ip" --sport "$port_num" -m comment --comment "$tag" -j ACCEPT 2>/dev/null \
|| iptables -A FORWARD -p udp -s "$target_ip" --sport "$port_num" -m comment --comment "$tag" -j ACCEPT
done
save_state "relay" "$profile_source" "$ports" "$endpoint" "$target_ip" ""
status_json
}
client_enable() {
local ip prio updated
ip="$(normalize_client_ip "${1:-}")"
[ -n "$ip" ] || { echo "client ip required" >&2; exit 1; }
load_state
[ "${STATE_MODE:-}" = "egress" ] || { echo "WARP egress mode is not active" >&2; exit 1; }
systemctl is-active --quiet "wg-quick@${WARP_IF}" || { echo "WARP interface is not active" >&2; exit 1; }
prio="$(client_rule_prio "$ip")"
ip rule add from "${ip}/32" table "$WARP_TABLE" priority "$prio" 2>/dev/null || true
updated="$(state_clients_csv_add "$ip" "${STATE_CLIENTS:-}")"
save_state "${STATE_MODE:-egress}" "${STATE_PROFILE_SOURCE:-free}" "${STATE_RELAY_PORTS:-}" "${STATE_RELAY_TARGET:-}" "${STATE_RELAY_TARGET_IP:-}" "$updated"
status_json
}
client_disable() {
local ip prio updated
ip="$(normalize_client_ip "${1:-}")"
[ -n "$ip" ] || { echo "client ip required" >&2; exit 1; }
load_state
prio="$(client_rule_prio "$ip")"
ip rule del from "${ip}/32" table "$WARP_TABLE" priority "$prio" 2>/dev/null || true
updated="$(state_clients_csv_del "$ip" "${STATE_CLIENTS:-}")"
save_state "${STATE_MODE:-}" "${STATE_PROFILE_SOURCE:-}" "${STATE_RELAY_PORTS:-}" "${STATE_RELAY_TARGET:-}" "${STATE_RELAY_TARGET_IP:-}" "$updated"
status_json
}
status_json() {
load_env
load_state
local subnet service configured endpoint pubkey egress enabled mode profile_source relay_on
subnet="$(wg_subnet)"
service="$(systemctl is-active "wg-quick@${WARP_IF}" 2>/dev/null || true)"
mode="${STATE_MODE:-}"
profile_source="${STATE_PROFILE_SOURCE:-}"
configured=false
endpoint=""
pubkey=""
egress=""
relay_on=false
if [ "$service" = "active" ]; then
mode="${mode:-egress}"
configured=true
endpoint="$(awk -F'= *' '/^Endpoint *=/{print $2; exit}' "$WARP_CONF" 2>/dev/null || true)"
pubkey="$(wg show "${WARP_IF}" public-key 2>/dev/null || true)"
egress="$(curl -4 --interface "${WARP_IF}" -fsS --max-time 6 https://api.ipify.org 2>/dev/null || true)"
[ -n "$profile_source" ] || profile_source="$( [ -s "$CUSTOM_PROFILE" ] && echo custom || echo free )"
fi
if relay_enabled; then
relay_on=true
[ -n "$mode" ] || mode="relay"
configured=true
[ -n "$endpoint" ] || endpoint="${STATE_RELAY_TARGET:-}"
[ -n "$profile_source" ] || profile_source="$( [ -s "$CUSTOM_PROFILE" ] && echo custom || echo free )"
fi
enabled=false
if [ "$service" = "active" ] || [ "$relay_on" = true ]; then
enabled=true
fi
python3 - "$enabled" "$configured" "$service" "$subnet" "$endpoint" "$pubkey" "$egress" "$mode" "$profile_source" "${STATE_RELAY_PORTS:-}" "${STATE_RELAY_TARGET:-}" "${STATE_RELAY_TARGET_IP:-}" "$( [ -s "$CUSTOM_PROFILE" ] && echo true || echo false )" "$relay_on" "$( [ -s "$LICENSE_FILE" ] && echo true || echo false )" "${STATE_CLIENTS:-}" <<'PY'
import json, sys
enabled, configured, service, subnet, endpoint, pubkey, egress, mode, profile_source, relay_ports, relay_target, relay_target_ip, custom_saved, relay_on, license_saved, clients_csv = sys.argv[1:]
if relay_on == "true" and service != "active":
service = "relay"
print(json.dumps({
"enabled": enabled == "true",
"configured": configured == "true",
"service": service or "inactive",
"mode": mode or "off",
"profile_source": profile_source or "free",
"subnet": subnet,
"endpoint": endpoint,
"public_key": pubkey,
"egress_ipv4": egress,
"relay_enabled": relay_on == "true",
"relay_ports": relay_ports,
"relay_target": relay_target,
"relay_target_ip": relay_target_ip,
"clients": [x for x in clients_csv.split(",") if x],
"custom_profile_saved": custom_saved == "true",
"license_key_saved": license_saved == "true",
}, ensure_ascii=False))
PY
}
disable_warp() {
disable_all
save_state "" "" "" "" "" ""
status_json
}
case "${1:-status}" in
enable|up)
case "${2:-egress}" in
relay) enable_relay "${3:-free}" "${4:-4500}" "${5:-}" ;;
*) enable_egress "${3:-free}" ;;
esac
;;
client-enable) client_enable "${2:-}" ;;
client-disable) client_disable "${2:-}" ;;
disable|down) disable_warp ;;
status|check|--json) status_json ;;
*)
echo "usage: $0 {status|enable [egress|relay] [free|custom] [ports] [endpoint]|client-enable <ip>|client-disable <ip>|disable}" >&2
exit 1
;;
esac

View File

@@ -1,439 +0,0 @@
#!/bin/bash
# ============================================================
# Phobos Secondary Server Setup
# Deploys WG + obfuscator + mini-API (no web panel)
#
# Usage:
# MAIN_PROTO=http MAIN_SERVER=203.0.113.10 MAIN_PORT=10514 MAIN_API_KEY=secret123 \
# bash <(curl -fsSL http://203.0.113.10:10514/server/secondary-setup.sh)
#
# Uninstall:
# bash /opt/Phobos/server/phobos-secondary-uninstall.sh
# ============================================================
set -e
MAIN_SERVER="${MAIN_SERVER:?Set MAIN_SERVER=ip_of_main_server}"
MAIN_API_KEY="${MAIN_API_KEY:?Set MAIN_API_KEY=your_api_key}"
MAIN_PROTO="${MAIN_PROTO:-http}"
MAIN_PORT="${MAIN_PORT:-8443}"
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
SERVER_WG_IPV4_NETWORK="${SERVER_WG_IPV4_NETWORK:-10.25.0.0/16}"
PHOBOS_DIR="/opt/Phobos"
WG_SERVER_IPV4="$(python3 - "$SERVER_WG_IPV4_NETWORK" <<'PY'
import ipaddress, sys
net = ipaddress.ip_network(sys.argv[1], strict=False)
print(next(net.hosts()))
PY
)"
WG_PREFIXLEN="$(python3 - "$SERVER_WG_IPV4_NETWORK" <<'PY'
import ipaddress, sys
print(ipaddress.ip_network(sys.argv[1], strict=False).prefixlen)
PY
)"
SERVER_IP="${SERVER_IP:-$(curl -s https://api.ipify.org || hostname -I | awk '{print $1}')}"
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}"
case "$MAIN_PROTO" in http|https) ;; *) echo "ERROR: MAIN_PROTO must be http or https"; exit 1;; esac
# ── Pre-flight safety checks ──
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Phobos Secondary Server — Pre-flight Check ║"
echo "╠══════════════════════════════════════════════════════╣"
echo "║ Server IP : $SERVER_IP"
echo "║ Interface : $IFACE"
echo "║ Main server : $MAIN_SERVER"
echo "║ Main proto : $MAIN_PROTO"
echo "║ Main port : $MAIN_PORT"
echo "║ OBF ports : $OBF_PORTS"
echo "╚══════════════════════════════════════════════════════╝"
echo ""
WARNINGS=0
# Check existing WireGuard
if [ -f /etc/wireguard/wg0.conf ]; then
echo "⚠ EXISTING wg0.conf found at /etc/wireguard/wg0.conf"
echo " Current peers: $(grep -c '^\[Peer\]' /etc/wireguard/wg0.conf 2>/dev/null || echo 0)"
WARNINGS=$((WARNINGS + 1))
fi
if systemctl is-active --quiet wg-quick@wg0 2>/dev/null; then
echo "⚠ WireGuard wg0 is RUNNING"
WARNINGS=$((WARNINGS + 1))
fi
# Check existing Phobos installation
if [ -d "$PHOBOS_DIR/clients" ] && [ "$(ls -A $PHOBOS_DIR/clients 2>/dev/null)" ]; then
CLIENT_COUNT=$(ls -d $PHOBOS_DIR/clients/*/ 2>/dev/null | wc -l)
echo "⚠ EXISTING Phobos client data: $CLIENT_COUNT client(s) in $PHOBOS_DIR/clients/"
WARNINGS=$((WARNINGS + 1))
fi
if [ -f "$PHOBOS_DIR/server/server.env" ]; then
echo "⚠ EXISTING server.env found (previous Phobos installation)"
WARNINGS=$((WARNINGS + 1))
fi
if [ -f "$PHOBOS_DIR/tokens/tokens.json" ] && [ "$(cat $PHOBOS_DIR/tokens/tokens.json 2>/dev/null)" != "[]" ]; then
echo "⚠ EXISTING tokens.json with active tokens"
WARNINGS=$((WARNINGS + 1))
fi
# Check port conflicts
IFS=',' read -ra PORTS <<< "$OBF_PORTS"
for PORT in "${PORTS[@]}"; do
if ss -tlnp 2>/dev/null | grep -q ":${PORT} "; then
PROC=$(ss -tlnp 2>/dev/null | grep ":${PORT} " | awk '{print $NF}')
echo "⚠ PORT $PORT already in use by: $PROC"
WARNINGS=$((WARNINGS + 1))
fi
done
if ss -tlnp 2>/dev/null | grep -q ":51820 "; then
echo "⚠ PORT 51820 (WireGuard) already in use"
WARNINGS=$((WARNINGS + 1))
fi
if ss -tlnp 2>/dev/null | grep -q ":8444 "; then
echo "⚠ PORT 8444 (mini-API) already in use"
WARNINGS=$((WARNINGS + 1))
fi
# Check conflicting services
for SVC in wg-obfuscator phobos-api; do
if systemctl is-active --quiet "$SVC" 2>/dev/null; then
echo "⚠ Service $SVC is already running"
WARNINGS=$((WARNINGS + 1))
fi
done
for PORT in "${PORTS[@]}"; do
if systemctl is-active --quiet "wg-obfuscator-${PORT}" 2>/dev/null; then
echo "⚠ Service wg-obfuscator-${PORT} is already running"
WARNINGS=$((WARNINGS + 1))
fi
done
if [ "$WARNINGS" -gt 0 ]; then
echo ""
echo "Found $WARNINGS warning(s). Existing configs will be preserved where possible."
echo "Press Enter to continue or Ctrl+C to abort..."
[ -t 0 ] && read -r < /dev/tty 2>/dev/null || true
fi
echo ""
echo "Starting installation..."
# ── 1. Backup existing configs ──
BACKUP_DIR="$PHOBOS_DIR/backup/$(date +%Y%m%d_%H%M%S)"
BACKED_UP=0
if [ -f /etc/wireguard/wg0.conf ] || [ -f "$PHOBOS_DIR/server/server.env" ] || [ -f "$PHOBOS_DIR/tokens/tokens.json" ]; then
echo "[0/7] Backing up existing configs..."
mkdir -p "$BACKUP_DIR"
[ -f /etc/wireguard/wg0.conf ] && cp /etc/wireguard/wg0.conf "$BACKUP_DIR/" && BACKED_UP=$((BACKED_UP + 1))
[ -f "$PHOBOS_DIR/server/server.env" ] && cp "$PHOBOS_DIR/server/server.env" "$BACKUP_DIR/" && BACKED_UP=$((BACKED_UP + 1))
[ -f "$PHOBOS_DIR/tokens/tokens.json" ] && cp "$PHOBOS_DIR/tokens/tokens.json" "$BACKUP_DIR/" && BACKED_UP=$((BACKED_UP + 1))
[ -d "$PHOBOS_DIR/clients" ] && cp -r "$PHOBOS_DIR/clients" "$BACKUP_DIR/" 2>/dev/null && BACKED_UP=$((BACKED_UP + 1))
echo " Backed up $BACKED_UP item(s) to $BACKUP_DIR"
fi
# ── 1. Dependencies ──
echo "[1/7] Installing dependencies..."
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq wireguard wireguard-tools iptables jq curl git ca-certificates python3 python3-flask gunicorn 2>/dev/null
# ── 2. Phobos binaries ──
echo "[2/7] Getting Phobos binaries..."
mkdir -p "$PHOBOS_DIR"/{server,clients,bin}
if [ ! -f /usr/local/bin/wg-obfuscator ]; then
ARCH="$(uname -m)"
OBF_ARCH="$ARCH"
[ "$OBF_ARCH" = "amd64" ] && OBF_ARCH="x86_64"
[ "$OBF_ARCH" = "arm64" ] && OBF_ARCH="aarch64"
REPO_DIR="/tmp/phobos-repo"
rm -rf "$REPO_DIR"
mkdir -p "$REPO_DIR"
cd "$REPO_DIR"
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 2>/dev/null
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-${OBF_ARCH}" /usr/local/bin/wg-obfuscator 2>/dev/null || true
rm -rf "$REPO_DIR"
if [ ! -x /usr/local/bin/wg-obfuscator ]; then
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 MAP in "x86_64:linux-x64" "aarch64:linux-arm64" "mipsel:linux-mipsel-mips32" "mips:linux-mips-mips32" "armv7:linux-armv7-hf"; do
DA="${MAP%%:*}"
SS="${MAP##*:}"
[ "$DA" = "$OBF_ARCH" ] || continue
[ -n "$OBF_TAG" ] || continue
TMP_DIR=$(mktemp -d)
if curl -fsSL -m30 "${CB}/wg-obfuscator-${OBF_TAG}-${SS}.tar.gz" -o "$TMP_DIR/o.tgz" 2>/dev/null &&
tar -xzf "$TMP_DIR/o.tgz" -C "$TMP_DIR" 2>/dev/null; then
BIN_PATH=$(find "$TMP_DIR" -name "wg-obfuscator" -type f | head -1)
if [ -n "$BIN_PATH" ]; then
cp "$BIN_PATH" "$PHOBOS_DIR/bin/wg-obfuscator-${DA}"
chmod +x "$PHOBOS_DIR/bin/wg-obfuscator-${DA}"
ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-${DA}" /usr/local/bin/wg-obfuscator
fi
fi
rm -rf "$TMP_DIR"
done
fi
[ -x /usr/local/bin/wg-obfuscator ] || { echo "ERROR: obfuscator binary for $ARCH missing"; exit 1; }
echo " Obfuscator installed."
else
echo " Obfuscator already installed, skipping."
fi
# ── 3. WireGuard ──
echo "[3/7] Configuring WireGuard..."
if [ -f /etc/wireguard/wg0.conf ]; then
echo " Existing wg0.conf preserved (backed up to $BACKUP_DIR)"
WG_PRIV=$(grep '^PrivateKey' /etc/wireguard/wg0.conf | cut -d= -f2- | tr -d ' ')
WG_PUB=$(echo "$WG_PRIV" | wg pubkey)
else
WG_PRIV=$(wg genkey)
WG_PUB=$(echo "$WG_PRIV" | wg pubkey)
cat > /etc/wireguard/wg0.conf << WGEOF
[Interface]
Address = ${WG_SERVER_IPV4}/${WG_PREFIXLEN}
ListenPort = 51820
PrivateKey = $WG_PRIV
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o $IFACE -j MASQUERADE
WGEOF
fi
sysctl -w net.ipv4.ip_forward=1 -q
grep -q "net.ipv4.ip_forward = 1" /etc/sysctl.conf || echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
systemctl enable wg-quick@wg0 -q 2>/dev/null || true
systemctl restart wg-quick@wg0
echo " WireGuard running."
# ── 4. Obfuscator (multi-port) ──
echo "[4/7] Setting up obfuscator..."
OBF_KEY=$(head -c 32 /dev/urandom | base64 | tr -d "/+=" | head -c 32)
# Block direct WG access
iptables -C INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null \
|| iptables -A INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP
IFS=',' read -ra PORTS <<< "$OBF_PORTS"
for PORT in "${PORTS[@]}"; do
cat > "$PHOBOS_DIR/server/wg-obfuscator-${PORT}.conf" << EOF
[instance]
source-if = 0.0.0.0
source-lport = ${PORT}
target = 127.0.0.1:51820
key = ${OBF_KEY}
masking = AUTO
verbose = INFO
idle-timeout = 300
max-dummy = 50
EOF
cat > /etc/systemd/system/wg-obfuscator-${PORT}.service << EOF
[Unit]
Description=WireGuard Obfuscator (port ${PORT})
After=network.target wg-quick@wg0.service
[Service]
Type=simple
ExecStart=/usr/local/bin/wg-obfuscator --config ${PHOBOS_DIR}/server/wg-obfuscator-${PORT}.conf
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
done
systemctl daemon-reload
for PORT in "${PORTS[@]}"; do
systemctl enable "wg-obfuscator-${PORT}" -q
systemctl start "wg-obfuscator-${PORT}"
done
echo " Obfuscator running on ports: $OBF_PORTS"
# ── 5. Save config ──
echo "[5/7] Saving config..."
cat > "$PHOBOS_DIR/server/server.env" << EOF
SERVER_WG_PRIVATE_KEY=$WG_PRIV
SERVER_WG_PUBLIC_KEY=$WG_PUB
SERVER_PUBLIC_IP_V4=$SERVER_IP
SERVER_WG_IPV4_NETWORK=$SERVER_WG_IPV4_NETWORK
SERVER_WG_IPV4_ADDRESS=${WG_SERVER_IPV4}/${WG_PREFIXLEN}
OBFUSCATOR_KEY=$OBF_KEY
OBFUSCATOR_PORTS=$OBF_PORTS
MAIN_SERVER=$MAIN_SERVER
MAIN_PROTO=$MAIN_PROTO
MAIN_API_KEY=$MAIN_API_KEY
MAIN_PORT=$MAIN_PORT
ROLE=secondary
EOF
# ── 6. Mini-API ──
echo "[6/7] Setting up mini-API..."
# Mini-API (fetched from the main panel first, then PCA GitHub fallback)
PCA_BRANCH="${PCA_BRANCH:-main}"
RAW="${PCA_RAW_BASE:-https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/${PCA_BRANCH}}"
PANEL_BOOTSTRAP="${MAIN_PROTO}://${MAIN_SERVER}:${MAIN_PORT}/server/api.py"
curl -kfsSL "$PANEL_BOOTSTRAP" -o "$PHOBOS_DIR/server/api.py" \
|| curl -fsSL "$RAW/server/api.py" -o "$PHOBOS_DIR/server/api.py" \
|| curl -fsSL "https://raw.githubusercontent.com/andrey271192/PCA_Phobos/${PCA_BRANCH}/server/api.py" -o "$PHOBOS_DIR/server/api.py"
[ -s "$PHOBOS_DIR/server/api.py" ] || { echo "ERROR: api.py fetch failed"; exit 1; }
cat > /etc/systemd/system/phobos-api.service << EOF
[Unit]
Description=Phobos Secondary API
After=network.target wg-quick@wg0.service
[Service]
Type=simple
WorkingDirectory=/opt/Phobos/server
ExecStart=/usr/bin/gunicorn -w 1 -b 0.0.0.0:8444 api:app
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable phobos-api -q
systemctl start phobos-api
# ── 7. Uninstall script ──
echo "[7/7] Creating uninstall script..."
cat > "$PHOBOS_DIR/server/phobos-secondary-uninstall.sh" << 'UNINSTEOF'
#!/bin/bash
# Phobos Secondary Server — Clean Uninstall
set -e
PHOBOS_DIR="/opt/Phobos"
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Phobos Secondary Server — Uninstall ║"
echo "╚══════════════════════════════════════════════════════╝"
echo ""
echo "This will remove:"
echo " - WireGuard interface wg0"
echo " - All wg-obfuscator instances"
echo " - Phobos mini-API"
echo " - Phobos config files"
echo ""
echo "Backups (if any) in $PHOBOS_DIR/backup/ will be KEPT."
echo ""
echo "Press Enter to continue or Ctrl+C to abort..."
[ -t 0 ] && read -r < /dev/tty 2>/dev/null || true
echo "Stopping services..."
# Stop obfuscator instances
for svc in /etc/systemd/system/wg-obfuscator-*.service; do
[ -f "$svc" ] || continue
name=$(basename "$svc" .service)
systemctl stop "$name" 2>/dev/null || true
systemctl disable "$name" 2>/dev/null || true
rm -f "$svc"
echo " Removed $name"
done
# Stop mini-API
systemctl stop phobos-api 2>/dev/null || true
systemctl disable phobos-api 2>/dev/null || true
rm -f /etc/systemd/system/phobos-api.service
echo " Removed phobos-api"
# Stop WireGuard
systemctl stop wg-quick@wg0 2>/dev/null || true
systemctl disable wg-quick@wg0 2>/dev/null || true
echo " Stopped WireGuard wg0"
systemctl daemon-reload
# Remove iptables rule
iptables -D INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null || true
echo ""
echo "Removing files..."
# Remove WG config
rm -f /etc/wireguard/wg0.conf
echo " Removed /etc/wireguard/wg0.conf"
# Remove obfuscator binary (only the symlink)
rm -f /usr/local/bin/wg-obfuscator
echo " Removed /usr/local/bin/wg-obfuscator"
# Keep backups, remove the rest
if [ -d "$PHOBOS_DIR/backup" ]; then
echo " Preserving $PHOBOS_DIR/backup/"
# Remove everything except backup dir
find "$PHOBOS_DIR" -mindepth 1 -maxdepth 1 ! -name 'backup' -exec rm -rf {} \;
else
rm -rf "$PHOBOS_DIR"
fi
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Uninstall Complete ║"
echo "╠══════════════════════════════════════════════════════╣"
echo "║ WireGuard : removed ║"
echo "║ Obfuscator : removed ║"
echo "║ Mini-API : removed ║"
if [ -d "$PHOBOS_DIR/backup" ]; then
echo "║ Backups : preserved in $PHOBOS_DIR/backup/ ║"
fi
echo "╚══════════════════════════════════════════════════════╝"
echo ""
UNINSTEOF
chmod +x "$PHOBOS_DIR/server/phobos-secondary-uninstall.sh"
# ── Register with main server ──
echo ""
echo "Registering with main server..."
REG_DATA=$(cat << EOF
{
"ip": "$SERVER_IP",
"wg_public_key": "$WG_PUB",
"obfuscator_key": "$OBF_KEY",
"ports": "$(echo $OBF_PORTS)"
}
EOF
)
curl -ks -X POST "${MAIN_PROTO}://${MAIN_SERVER}:${MAIN_PORT}/api/servers/register" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${MAIN_API_KEY}" \
-d "$REG_DATA" || echo " (Registration will be done manually via panel)"
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Secondary Server Ready! ║"
echo "╠══════════════════════════════════════════════════════╣"
echo "║ Server IP : $SERVER_IP"
echo "║ WG Public Key : $WG_PUB"
echo "║ OBF Key : $OBF_KEY"
echo "║ OBF Ports : $OBF_PORTS"
echo "║ API : http://$SERVER_IP:8444"
echo "║ Main server : $MAIN_SERVER"
echo "║ ║"
echo "║ Uninstall: bash $PHOBOS_DIR/server/phobos-secondary-uninstall.sh"
echo "╚══════════════════════════════════════════════════════╝"
if [ -d "$PHOBOS_DIR/backup" ]; then
echo " Backups: $BACKUP_DIR"
fi
echo ""

View File

@@ -1,6 +0,0 @@
# udptcp — резервный TCP-канал PCA Phobos
UDP-пакеты (уже обфусцированные) внутри TCP: кадр = 2 байта длины + пакет.
- сервер: `udptcp -s 0.0.0.0:443 127.0.0.1:2083` (служба `phobos-tcp`, порт в server.env `TCP_PORT`);
- клиент: `udptcp -c 127.0.0.1:13256 <сервер>:443`, обфускатор клиента шлёт на 127.0.0.1:13256.
Сборка: `gcc -O2 -static -pthread -o bin/udptcp-x86_64 udptcp.c` (+ aarch64-linux-gnu-gcc), Android — NDK (см. android-client/README.md).

Binary file not shown.

Binary file not shown.

View File

@@ -1,175 +0,0 @@
/*
* udptcp — PCA Phobos fallback transport: UDP datagrams carried over TCP.
* Frames on the TCP stream: 2-byte big-endian length + payload.
*
* server: udptcp -s <listen_ip:port> <udp_target_ip:port>
* each TCP connection gets its own UDP socket to the target
* (the local wg-obfuscator), so the obfuscator sees one client per conn.
* client: udptcp -c <local_udp_ip:port> <tcp_server_host:port>
* local UDP peer (client wg-obfuscator) <-> one TCP connection, reconnects.
*
* Plain C, no deps: builds for Linux servers, Android (NDK) and iOS.
*/
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#define MAXPKT 65535
static int parse_addr(const char *s, struct sockaddr_storage *ss, socklen_t *len, int socktype) {
char host[256];
const char *colon = strrchr(s, ':');
if (!colon || colon == s || (size_t)(colon - s) >= sizeof host) return -1;
memcpy(host, s, colon - s);
host[colon - s] = 0;
struct addrinfo hints = {0}, *res;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = socktype;
if (getaddrinfo(host, colon + 1, &hints, &res) != 0) return -1;
memcpy(ss, res->ai_addr, res->ai_addrlen);
*len = res->ai_addrlen;
freeaddrinfo(res);
return 0;
}
static int read_full(int fd, unsigned char *buf, size_t n) {
size_t got = 0;
while (got < n) {
ssize_t r = read(fd, buf + got, n - got);
if (r <= 0) return -1;
got += (size_t)r;
}
return 0;
}
static int write_frame(int fd, const unsigned char *p, size_t n) {
unsigned char hdr[2] = {(unsigned char)(n >> 8), (unsigned char)(n & 0xff)};
if (write(fd, hdr, 2) != 2) return -1;
size_t sent = 0;
while (sent < n) {
ssize_t w = write(fd, p + sent, n - sent);
if (w <= 0) return -1;
sent += (size_t)w;
}
return 0;
}
static void tune_tcp(int fd) {
int one = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof one);
}
/* ── server ─────────────────────────────────────────────────────────────── */
static struct sockaddr_storage g_target;
static socklen_t g_target_len;
static void *serve_conn(void *arg) {
int tcp = (int)(long)arg;
unsigned char buf[MAXPKT + 2];
int udp = socket(g_target.ss_family, SOCK_DGRAM, 0);
if (udp < 0 || connect(udp, (struct sockaddr *)&g_target, g_target_len) < 0) goto out;
tune_tcp(tcp);
struct pollfd p[2] = {{tcp, POLLIN, 0}, {udp, POLLIN, 0}};
for (;;) {
if (poll(p, 2, 300000) <= 0) break; /* 5 min idle -> drop */
if (p[0].revents & (POLLIN | POLLHUP | POLLERR)) {
unsigned char h[2];
if (read_full(tcp, h, 2) < 0) break;
size_t n = ((size_t)h[0] << 8) | h[1];
if (n == 0 || read_full(tcp, buf, n) < 0) break;
send(udp, buf, n, 0);
}
if (p[1].revents & POLLIN) {
ssize_t n = recv(udp, buf, MAXPKT, 0);
if (n > 0 && write_frame(tcp, buf, (size_t)n) < 0) break;
}
}
out:
if (udp >= 0) close(udp);
close(tcp);
return NULL;
}
static int run_server(const char *listen_s, const char *target_s) {
struct sockaddr_storage la; socklen_t ll;
if (parse_addr(listen_s, &la, &ll, SOCK_STREAM) || parse_addr(target_s, &g_target, &g_target_len, SOCK_DGRAM)) {
fprintf(stderr, "bad address\n"); return 2;
}
int ls = socket(la.ss_family, SOCK_STREAM, 0), one = 1;
setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
if (bind(ls, (struct sockaddr *)&la, ll) < 0 || listen(ls, 128) < 0) { perror("listen"); return 1; }
fprintf(stderr, "udptcp server %s -> udp %s\n", listen_s, target_s);
for (;;) {
int c = accept(ls, NULL, NULL);
if (c < 0) { if (errno == EINTR) continue; perror("accept"); sleep(1); continue; }
pthread_t t;
if (pthread_create(&t, NULL, serve_conn, (void *)(long)c) == 0) pthread_detach(t);
else close(c);
}
}
/* ── client ─────────────────────────────────────────────────────────────── */
static int connect_tcp(const char *server_s) {
struct sockaddr_storage sa; socklen_t sl;
if (parse_addr(server_s, &sa, &sl, SOCK_STREAM)) return -1; /* re-resolve each time */
int s = socket(sa.ss_family, SOCK_STREAM, 0);
if (s < 0) return -1;
if (connect(s, (struct sockaddr *)&sa, sl) < 0) { close(s); return -1; }
tune_tcp(s);
return s;
}
static int run_client(const char *local_s, const char *server_s) {
struct sockaddr_storage la, peer; socklen_t ll, pl = 0;
if (parse_addr(local_s, &la, &ll, SOCK_DGRAM)) { fprintf(stderr, "bad address\n"); return 2; }
int us = socket(la.ss_family, SOCK_DGRAM, 0);
if (bind(us, (struct sockaddr *)&la, ll) < 0) { perror("bind"); return 1; }
fprintf(stderr, "udptcp client udp %s -> tcp %s\n", local_s, server_s);
unsigned char buf[MAXPKT + 2];
int tcp = -1;
time_t last_try = 0;
for (;;) {
struct pollfd p[2] = {{us, POLLIN, 0}, {tcp, POLLIN, 0}};
poll(p, tcp >= 0 ? 2 : 1, 1000);
if (p[0].revents & POLLIN) {
pl = sizeof peer;
ssize_t n = recvfrom(us, buf, MAXPKT, 0, (struct sockaddr *)&peer, &pl);
if (n > 0) {
if (tcp < 0 && time(NULL) != last_try) {
last_try = time(NULL);
tcp = connect_tcp(server_s);
fprintf(stderr, tcp >= 0 ? "connected\n" : "connect failed\n");
}
if (tcp >= 0 && write_frame(tcp, buf, (size_t)n) < 0) { close(tcp); tcp = -1; }
}
}
if (tcp >= 0 && (p[1].revents & (POLLIN | POLLHUP | POLLERR))) {
unsigned char h[2];
if (read_full(tcp, h, 2) < 0) { close(tcp); tcp = -1; continue; }
size_t n = ((size_t)h[0] << 8) | h[1];
if (n == 0 || read_full(tcp, buf, n) < 0) { close(tcp); tcp = -1; continue; }
if (pl) sendto(us, buf, n, 0, (struct sockaddr *)&peer, pl);
}
}
}
int main(int argc, char **argv) {
signal(SIGPIPE, SIG_IGN);
if (argc == 4 && !strcmp(argv[1], "-s")) return run_server(argv[2], argv[3]);
if (argc == 4 && !strcmp(argv[1], "-c")) return run_client(argv[2], argv[3]);
fprintf(stderr, "usage: udptcp -s <listen ip:port> <udp target ip:port>\n"
" udptcp -c <local udp ip:port> <tcp server host:port>\n");
return 2;
}

View File

@@ -1,70 +0,0 @@
#!/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 "=================================================="

275
update.sh
View File

@@ -1,275 +0,0 @@
#!/bin/bash
# ============================================================
# PCA Phobos — updater / rollback
#
# phobos-update # обновить до стабильной (канал stable / main)
# phobos-update stable|beta|dev # канал (ветка main/beta/dev)
# phobos-update v1.2.4 # конкретная версия (git tag)
# phobos-update --rollback # откатить на предыдущую версию (из бэкапа)
# phobos-update --check # установленная + версии всех каналов
# phobos-update --list # список бэкапов для отката
#
# Источник: Gitea git.bobyrev.com/PCA/PCA_Phobos (основной), GitHub — запасной.
# Свой адрес: PCA_GIT=https://… phobos-update
#
# Обновляет ТОЛЬКО PCA-слой (панель + скрипты). НЕ трогает ключи WireGuard,
# server.env, клиентов. Перед каждым обновлением — бэкап; откат восстанавливает.
# ============================================================
set -e
PHOBOS_DIR=/opt/Phobos
PANEL_DIR=/opt/phobos-panel
PCA_GIT="${PCA_GIT:-https://git.bobyrev.com/PCA/PCA_Phobos}"
GH_RAW="${GH_RAW:-https://raw.githubusercontent.com/andrey271192/PCA_Phobos}"
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)}"
panel_service_name() {
for name in phobos-dev-test phobos-panel; do
systemctl status "$name" >/dev/null 2>&1 && { echo "$name"; return; }
done
echo phobos-panel
}
restart_panel_service() {
local svc
svc="$(panel_service_name)"
systemctl restart "$svc" 2>/dev/null || true
}
# since 1.4.0 all channels are branches of one public repo (no subscriber token)
is_private() { return 1; }
fetch_repo_file() { # $1=ref $2=repo_path $3=output ; Gitea first, GitHub fallback
local ref="$1" rp="$2" out="$3" kind=branch
case "$ref" in v[0-9]*) kind=tag;; esac
curl -fsSL -m25 "$PCA_GIT/raw/$kind/$ref/$rp?cb=$(date +%s)" -o "$out" 2>/dev/null && return 0
curl -fsSL -m25 "$GH_RAW/$ref/$rp?cb=$(date +%s)" -o "$out"
}
fetch_repo_text() { # $1=ref $2=repo_path
local tmp rc
tmp=$(mktemp)
fetch_repo_file "$1" "$2" "$tmp"; rc=$?
if [ "$rc" = 0 ]; then cat "$tmp"; fi
rm -f "$tmp"
return "$rc"
}
managed_files() {
cat <<LIST
$PANEL_DIR/app.py|app.py
$PHOBOS_DIR/repo/server/scripts/lib-core.sh|overlay/lib-core.sh
$PHOBOS_DIR/repo/server/scripts/phobos-client.sh|overlay/phobos-client.sh
$PHOBOS_DIR/repo/client/templates/install-router.sh.template|overlay/install-router.sh.template
$PHOBOS_DIR/repo/client/templates/lib-client.sh|overlay/lib-client.sh
$PHOBOS_DIR/repo/client/templates/install-obfuscator.sh|overlay/install-obfuscator.sh
$PHOBOS_DIR/repo/client/templates/install-wireguard.sh|overlay/install-wireguard.sh
$PHOBOS_DIR/repo/client/templates/router-configure-wireguard.sh|overlay/router-configure-wireguard.sh
$PHOBOS_DIR/repo/client/templates/router-configure-wireguard-openwrt.sh|overlay/router-configure-wireguard-openwrt.sh
$PHOBOS_DIR/repo/client/templates/phobos-pull.sh|overlay/phobos-pull.sh
$PHOBOS_DIR/repo/client/templates/phobos-ttyd.sh|overlay/phobos-ttyd.sh
$PHOBOS_DIR/repo/client/templates/phobos-uninstall.sh|overlay/phobos-uninstall.sh
$PHOBOS_DIR/repo/client/templates/3xui.sh|overlay/3xui.sh
$PHOBOS_DIR/server/phobos-warp.sh|server/phobos-warp.sh
$PHOBOS_DIR/server/phobos-health.sh|server/phobos-health.sh
$PHOBOS_DIR/server/phobos-self-check.sh|server/phobos-self-check.sh
$PHOBOS_DIR/server/phobos-pull.sh|server/phobos-pull.sh
$PHOBOS_DIR/server/phobos-router-watchdog.py|server/phobos-router-watchdog.py
$PHOBOS_DIR/server/api.py|server/api.py
$PHOBOS_DIR/server/secondary-setup.sh|server/secondary-setup.sh
$PHOBOS_DIR/server/phobos-backup.sh|server/phobos-backup.sh
$PHOBOS_DIR/server/phobos-guard.sh|server/phobos-guard.sh
$PHOBOS_DIR/server/phobos-cascade.sh|server/phobos-cascade.sh
$PHOBOS_DIR/server/phobos-domain.sh|server/phobos-domain.sh
$PHOBOS_DIR/server/phobos-limit.sh|server/phobos-limit.sh
$PHOBOS_DIR/server/phobos-speedtest.sh|server/phobos-speedtest.sh
$PHOBOS_DIR/server/phobos-tcp.sh|server/phobos-tcp.sh
$PHOBOS_DIR/server/update.sh|update.sh
LIST
}
ensure_tcp_fallback() { # $1=ref ; primary only (secondaries have no panel)
[ -f "$PANEL_DIR/app.py" ] || return 0
local arch tmp
arch=$(uname -m); case "$arch" in amd64) arch=x86_64;; arm64) arch=aarch64;; esac
tmp=$(mktemp)
if fetch_repo_file "$1" "udptcp/bin/udptcp-$arch" "$tmp" && [ -s "$tmp" ]; then
install -m755 "$tmp" "$PHOBOS_DIR/bin/udptcp"
fi
rm -f "$tmp"
[ -x "$PHOBOS_DIR/server/phobos-tcp.sh" ] && ln -sf "$PHOBOS_DIR/server/phobos-tcp.sh" /usr/local/bin/phobos-tcp
[ -x "$PHOBOS_DIR/bin/udptcp" ] && "$PHOBOS_DIR/server/phobos-tcp.sh" enable >/dev/null 2>&1 \
&& echo "TCP fallback: port $(grep '^TCP_PORT=' "$PHOBOS_DIR/server/server.env" | cut -d= -f2)" || true
}
run_self_check() {
if [ -x "$PHOBOS_DIR/server/phobos-self-check.sh" ]; then
ln -sf "$PHOBOS_DIR/server/phobos-self-check.sh" /usr/local/bin/phobos-self-check 2>/dev/null || true
"$PHOBOS_DIR/server/phobos-self-check.sh" --fix
else
echo " WARN: phobos-self-check.sh не найден"
fi
[ -x "$PHOBOS_DIR/server/phobos-backup.sh" ] && ln -sf "$PHOBOS_DIR/server/phobos-backup.sh" /usr/local/bin/phobos-backup 2>/dev/null || true
[ -x "$PHOBOS_DIR/server/phobos-warp.sh" ] && ln -sf "$PHOBOS_DIR/server/phobos-warp.sh" /usr/local/bin/phobos-warp 2>/dev/null || true
for _n in phobos-guard phobos-cascade phobos-domain; do [ -x "$PHOBOS_DIR/server/$_n.sh" ] && ln -sf "$PHOBOS_DIR/server/$_n.sh" /usr/local/bin/$_n 2>/dev/null || true; done
}
refresh_router_packages() {
local script="$PHOBOS_DIR/repo/server/scripts/phobos-client.sh"
[ -x "$script" ] || return 0
[ -d "$PHOBOS_DIR/clients" ] || return 0
local rebuilt=0 failed=0 id
for dir in "$PHOBOS_DIR/clients"/*; do
[ -d "$dir" ] || continue
id=$(basename "$dir")
if "$script" package "$id" >/dev/null 2>&1; then
rebuilt=$((rebuilt + 1))
else
failed=$((failed + 1))
echo " WARN: router package rebuild failed for $id"
fi
done
[ "$rebuilt" = 0 ] && [ "$failed" = 0 ] || echo "Router packages rebuilt: $rebuilt, failed: $failed"
}
# fetch VERSION of a channel (public repos / or private if token present)
chan_version() { # $1 = ref(channel)
if is_private "$1" && [ -z "$GH_TOKEN" ]; then echo ""; return; fi
fetch_repo_text "$1" VERSION 2>/dev/null | tr -d ' \r\n'
}
latest_version() { fetch_repo_text main VERSION 2>/dev/null | tr -d ' \r\n'; }
do_backup() {
local dir="$BACKUPS/${CUR}-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$dir"
managed_files | while IFS='|' read -r lp rp; do
[ -f "$lp" ] && install -D "$lp" "$dir/$rp" 2>/dev/null || true
done
echo "$CUR" > "$dir/.fromversion"
echo "$dir"
}
apply_ref() {
local ref="$1"
managed_files | while IFS='|' read -r lp rp; do
local tmp; tmp=$(mktemp)
if fetch_repo_file "$ref" "$rp" "$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
rm -f "$tmp"; echo " WARN: не удалось скачать $rp (пропуск)"
fi
done
}
ensure_runtime_deps() {
if command -v apt-get >/dev/null 2>&1; then
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sshpass python3-pip >/dev/null 2>&1 || true
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-flask-sock >/dev/null 2>&1 || true
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-asyncssh >/dev/null 2>&1 || true
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-websocket >/dev/null 2>&1 || true
fi
python3 - <<'PY' >/dev/null 2>&1 || python3 -m pip install flask-sock asyncssh websocket-client >/dev/null 2>&1 || python3 -m pip install --break-system-packages flask-sock asyncssh websocket-client >/dev/null 2>&1 || true
import flask_sock, asyncssh, websocket
PY
mkdir -p "$PANEL_DIR/static"
curl -fsSL -m20 https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css -o "$PANEL_DIR/static/xterm.min.css" 2>/dev/null || true
curl -fsSL -m20 https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js -o "$PANEL_DIR/static/xterm.min.js" 2>/dev/null || true
curl -fsSL -m20 https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js -o "$PANEL_DIR/static/xterm-addon-fit.min.js" 2>/dev/null || true
if [ -f /etc/systemd/system/phobos-panel.service ]; then
if grep -q 'ExecStart=/usr/bin/gunicorn -w 1 -b ' /etc/systemd/system/phobos-panel.service; then
sed -i 's#ExecStart=/usr/bin/gunicorn -w 1 -b #ExecStart=/usr/bin/gunicorn -w 1 --threads 20 -b #' /etc/systemd/system/phobos-panel.service
systemctl daemon-reload 2>/dev/null || true
fi
if grep -q '0.0.0.0:8443 app:app' /etc/systemd/system/phobos-panel.service && [ -s "$PANEL_DIR/.port" ]; then
port=$(cat "$PANEL_DIR/.port" 2>/dev/null || echo 8443)
sed -i "s#0.0.0.0:8443 app:app#0.0.0.0:${port} app:app#" /etc/systemd/system/phobos-panel.service
systemctl daemon-reload 2>/dev/null || true
fi
fi
}
ref_exists() {
local tmp rc
tmp=$(mktemp)
fetch_repo_file "$1" app.py "$tmp" >/dev/null 2>&1; rc=$?
rm -f "$tmp"
return "$rc"
}
case "${1:-}" in
--version|-v|--check)
echo "Установлено: $CUR"
echo "Стабильная: $(latest_version) [открытый]"
echo "Бета (beta): $(chan_version beta)"
echo "Разработка(dev):$(chan_version dev)"
;;
--versions|--tags)
echo "Версии (теги):"
curl -fsSL -m10 "${PCA_GIT%/*/*}/api/v1/repos/${PCA_GIT#*://*/}/tags?limit=100" 2>/dev/null \
| grep -oE '"name": *"[^"]+"' | sed 's/.*"name": *"\(.*\)"/ \1/' || echo " (нет данных)"
;;
--list)
echo "Бэкапы (для отката):"
ls -dt "$BACKUPS"/*/ 2>/dev/null | sed 's#.*/updates/##' || echo " нет"
;;
--rollback)
last=$(ls -dt "$BACKUPS"/*/ 2>/dev/null | head -1)
[ -z "$last" ] && { echo "Нет бэкапа для отката."; exit 1; }
fromver=$(cat "$last/.fromversion" 2>/dev/null || echo "?")
echo "Откат на $fromver ($last)"
managed_files | while IFS='|' read -r lp rp; do
[ -f "$last/$rp" ] && { mkdir -p "$(dirname "$lp")"; cp "$last/$rp" "$lp"; chmod +x "$lp" 2>/dev/null || true; }
done
echo "$fromver" > "$VERFILE"
run_self_check
restart_panel_service
refresh_router_packages
echo "Откат выполнен -> $fromver"
;;
*)
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"
# run the TARGET version's updater: its file list knows the new files
if [ -z "${PHOBOS_UPDATE_REEXEC:-}" ]; then
_new=$(mktemp)
if fetch_repo_file "$ref" update.sh "$_new" && head -1 "$_new" | grep -q '^#!' && ! cmp -s "$_new" "$0"; then
chmod +x "$_new"
PHOBOS_UPDATE_REEXEC=1 GH_TOKEN="$GH_TOKEN" exec bash "$_new" "$@"
fi
rm -f "$_new"
fi
target=$(fetch_repo_text "$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"
ensure_runtime_deps
run_self_check
ensure_tcp_fallback "$ref"
refresh_router_packages
echo "$target" > "$VERFILE"
restart_panel_service
echo "Обновлено до $target. Откат: phobos-update --rollback"
;;
esac