Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9a3a27fca | ||
|
|
d795bc6f70 | ||
|
|
c139debd8d | ||
|
|
01632034b0 | ||
|
|
071c1dc7f1 | ||
|
|
dff6af2c8e | ||
|
|
071d3695e6 | ||
|
|
5354656bb1 | ||
|
|
5a01a6eed2 | ||
|
|
7eac63343d | ||
|
|
093301eb51 | ||
|
|
1a0460b927 | ||
|
|
95806be3ed | ||
|
|
a686ea967e | ||
|
|
8e491b8f29 | ||
|
|
e215cedbb3 | ||
|
|
eac33b3cf6 | ||
|
|
3831292330 | ||
|
|
13644f6991 | ||
|
|
07fc4657bf | ||
|
|
e97b106adc | ||
|
|
390501ad34 | ||
|
|
e430d1a70c | ||
|
|
fde7b9b897 | ||
|
|
c1a151903a | ||
|
|
38ff3ae3dd | ||
|
|
4c1c82afec | ||
|
|
0f98c074c8 | ||
|
|
286142bd7a | ||
|
|
cf4b5f79b5 | ||
|
|
5f97c6c6d7 | ||
|
|
14bb6ae589 | ||
|
|
51712310f6 | ||
|
|
6ec1782d8b | ||
|
|
e61ec09b18 | ||
|
|
ed3071a971 | ||
|
|
db4cab7a38 | ||
|
|
9f402be886 | ||
|
|
8971565e6e | ||
|
|
b6525bc0ae | ||
|
|
d072c8227f | ||
|
|
7b941dc532 | ||
|
|
8c1654ecaa | ||
|
|
dc83d90a35 | ||
|
|
e0ea54e83a | ||
|
|
628973374e | ||
|
|
041a533457 |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
|||||||
custom:
|
|
||||||
- "https://boosty.to/andrey27/donate"
|
|
||||||
- "https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798"
|
|
||||||
187
README.md
187
README.md
@@ -1,182 +1,11 @@
|
|||||||
# PCA Phobos — VPN, который не отключается
|
# PCA Phobos — канал dev закрыт
|
||||||
|
|
||||||
**Веб-панель и 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">
|
1. Обновись до стабильной 1.4.5 или новее: `phobos-update stable`
|
||||||
</p>
|
2. Вставь токен: панель → Настройки → Версия и обновления → «токен подписчика» → Сохранить токен
|
||||||
|
3. `phobos-update dev` (или кнопка dev в панели)
|
||||||
|
|
||||||
---
|
Токен — по подписке: https://boosty.to/andrey27
|
||||||
|
|
||||||
## Что это простыми словами
|
|
||||||
|
|
||||||
Обычный VPN падает, когда падает сервер или провайдер банит IP. **PCA Phobos — нет.**
|
|
||||||
|
|
||||||
- 🌐 **Маскированный WireGuard** — трафик не отличить от обычного, проходит мимо DPI и блокировок по сигнатуре.
|
|
||||||
- 🔁 **Авто-переключение между серверами** — сервер лёг или порт прикрыли → роутер сам прыгает на резервный за ~10–15 секунд, без твоего участия.
|
|
||||||
- 📡 **Работает за любым NAT** — серый IP, мобильный интернет, домашний роутер за провайдером. Управление едет внутри самого защищённого туннеля, поэтому бан публичного IP ему не страшен.
|
|
||||||
- 🖥️ **Одна команда — новое устройство** — создал клиента в панели → вставил команду на роутер → готово.
|
|
||||||
- ♻️ **Само-восстановление** — роутер перезагрузился, провайдер сменил IP, сервис отвалился → система поднимет всё обратно сама.
|
|
||||||
- 📱 **Телефоны** — Android (PhobosWG, с обфускацией) и iPhone (обычный WireGuard) через QR-код.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Возможности
|
|
||||||
|
|
||||||
- **Активные сессии** — VPN IP, Real IP, метка, handshake, трафик RX/TX, Kick.
|
|
||||||
- **Клиенты VPN** — добавление/удаление, статус online (по handshake на любом сервере), назначение сервера.
|
|
||||||
- **Мульти-сервер + failover** — приоритеты, балансировка по нагрузке (CPU/RAM), резервные серверы; роутеры переключаются автоматически.
|
|
||||||
- **NAT-friendly управление** — роутеры сами тянут конфиг по туннелю (`10.25.0.1`), переживает бан публичного IP.
|
|
||||||
- **Авто-восстановление роутеров** — серверный сторож (watchdog) поднимает роутер после неудачной перезагрузки через KeenDNS.
|
|
||||||
- **Конфиги для телефона** — 📱 Android `phobos://` + QR, 🍎 iPhone обычный WireGuard + QR.
|
|
||||||
- **Метки** — имя объекта по IP (видно в сессиях и Telegram).
|
|
||||||
- **Срок подписки** — дата окончания, обратный отсчёт, автокик + предупреждения.
|
|
||||||
- **Telegram-уведомления** — подключение/отключение, сервер вверх/вниз, истечение подписки, авто-восстановление.
|
|
||||||
- **Двуязычный интерфейс** RU/EN + подсказки `?` у каждого действия.
|
|
||||||
- **Версии и обновления** — обновление и откат прямо из панели.
|
|
||||||
- **Уведомления per-client** — 🔔 у каждого клиента: включить/выключить Telegram-оповещения о подключении/отключении.
|
|
||||||
- **Telegram-бот** — управление клиентами командами прямо из чата:
|
|
||||||
- `/add <имя>` — создать клиента (бот пришлёт команду установки)
|
|
||||||
- `/del <имя>` — удалить клиента
|
|
||||||
- `/list` — список клиентов
|
|
||||||
- **Кнопки** — постоянная клавиатура: ➕ Создать клиента · 📋 Список · 🗑 Удалить (инлайн-выбор) · 📊 Статус · ❓ Помощь.
|
|
||||||
(Работает только для чата из настроек `tg_chat_id`.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Скриншоты
|
|
||||||
|
|
||||||
| Сессии | Клиенты | Серверы |
|
|
||||||
|--------|---------|---------|
|
|
||||||
|  |  |  |
|
|
||||||
|
|
||||||
> Если картинки не отображаются — положи свои PNG в `docs/img/` (`sessions.png`, `clients.png`, `servers.png`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Быстрый старт (с чистого VPS, одной командой)
|
|
||||||
|
|
||||||
Ставит **всё** со всеми зависимостями: wg-obfuscator, WireGuard, обфускатор-сервисы, веб-панель, nginx, скрипты онбординга роутеров, сторож авто-восстановления. Предустановленный Phobos **не требуется**.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/install.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
### С параметрами
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PANEL_PASS=МойПароль TG_TOKEN=123:abc TG_CHAT=123456789 \
|
|
||||||
bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/install.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Переменная | По умолчанию | Описание |
|
|
||||||
|-------------------|-----------------|----------|
|
|
||||||
| `PANEL_PASS` | `OcAdmin2026!` | Пароль панели (логин `admin`) |
|
|
||||||
| `PANEL_PORT` | случайный | Порт веб-панели |
|
|
||||||
| `API_KEY` | случайный | Общий ключ (агенты + pull-токен роутеров) |
|
|
||||||
| `OBF_PORTS` | `2083,5443,993` | Порты обфускатора |
|
|
||||||
| `TG_TOKEN`/`TG_CHAT` | пусто | Telegram-уведомления |
|
|
||||||
| `ALLOW_PLAIN_WG` | пусто | `=1` открыть порт 51820 для iOS WireGuard (без обфускации) |
|
|
||||||
|
|
||||||
После установки панель напечатает адрес, логин, пароль и **API key** — сохрани их.
|
|
||||||
|
|
||||||
### Вторичный сервер (для failover / балансировки)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MAIN_SERVER=<ip_основного> MAIN_API_KEY=<API key из основного> \
|
|
||||||
bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/server/secondary-setup.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
Затем в панели → **Серверы** → добавь его в список.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Клиенты для телефона
|
|
||||||
|
|
||||||
В таблице **Клиенты** у каждого клиента есть кнопки:
|
|
||||||
|
|
||||||
- 📱 **Android** — `phobos://`-ссылка + QR для приложения **PhobosWG** (с обфускацией). Скан QR → импорт.
|
|
||||||
- 🍎 **iPhone / iOS** — обычный WireGuard-конфиг + QR для официального WireGuard (**без** обфускации). Требует открытого порта 51820 (`ALLOW_PLAIN_WG=1` при установке).
|
|
||||||
|
|
||||||
> Для каждого устройства создавай **отдельного клиента** (свой ключ и IP). Один конфиг на двух устройствах = конфликт ключей.
|
|
||||||
|
|
||||||
**Приложения:**
|
|
||||||
- **Android (PhobosWG):** автор пока не публикует APK официально. Положи `.apk` на сервер в `/opt/Phobos/www/app/PhobosWG.apk` — панель сама покажет кнопку «Скачать приложение PhobosWG» на странице Android-клиента (раздаётся по `http://<сервер>/app/PhobosWG.apk`).
|
|
||||||
- **iPhone (iOS):** официальный [WireGuard из App Store](https://apps.apple.com/app/wireguard/id1441195209) — кнопка есть на странице iOS-клиента.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Обновление и откат
|
|
||||||
|
|
||||||
Проект версионируется (см. файл `VERSION` и git-теги `vX.Y.Z`).
|
|
||||||
|
|
||||||
**Из панели:** Настройки → «Версия и обновления» → **Обновить** / **Откатить**.
|
|
||||||
|
|
||||||
**Из консоли:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
phobos-update # обновить до последней версии
|
|
||||||
phobos-update v1.1.0 # обновить до конкретной версии (git tag)
|
|
||||||
phobos-update --rollback # откатить на предыдущую версию
|
|
||||||
phobos-update --version # показать установленную и доступную версию
|
|
||||||
phobos-update --list # список бэкапов
|
|
||||||
```
|
|
||||||
|
|
||||||
Обновляется только слой панели и скриптов. **Ключи WireGuard, `server.env` и клиенты не трогаются.** Перед каждым обновлением делается бэкап → откат восстанавливает его.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Возможные проблемы и решения
|
|
||||||
|
|
||||||
| Симптом | Причина | Решение |
|
|
||||||
|---------|---------|---------|
|
|
||||||
| **Команда установки → 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` |
|
|
||||||
|
|
||||||
Логи: `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:** [@Iot_andrey](https://t.me/Iot_andrey) — вопросы, баги, идеи
|
|
||||||
|
|
||||||
## На основе и благодарности
|
|
||||||
|
|
||||||
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) ❤️
|
|
||||||
|
|
||||||
Спасибо авторам за отличные инструменты.
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Скриншоты
|
|
||||||
|
|
||||||
Положи сюда 3 PNG (панель открыта — сделай скрины своих страниц):
|
|
||||||
|
|
||||||
- `sessions.png` — страница «Сессии»
|
|
||||||
- `clients.png` — страница «Клиенты»
|
|
||||||
- `servers.png` — страница «Серверы»
|
|
||||||
|
|
||||||
README в корне на них ссылается. Рекомендуемая ширина ~1000–1400px.
|
|
||||||
254
install.sh
254
install.sh
@@ -1,254 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# ============================================================
|
|
||||||
# PCA Phobos — TURNKEY installer (primary / panel node)
|
|
||||||
#
|
|
||||||
# One command, all dependencies, from a clean VPS:
|
|
||||||
# bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/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_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!}"
|
|
||||||
TG_TOKEN="${TG_TOKEN:-}"
|
|
||||||
TG_CHAT="${TG_CHAT:-}"
|
|
||||||
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
|
|
||||||
PCA_BRANCH="${PCA_BRANCH:-main}"
|
|
||||||
GH_TOKEN="${GH_TOKEN:-}" # set for private-repo installs; empty for public
|
|
||||||
PHOBOS_DIR="/opt/Phobos"
|
|
||||||
PANEL_DIR="/opt/phobos-panel"
|
|
||||||
RAW="https://raw.githubusercontent.com/andrey271192/PCA_Phobos/${PCA_BRANCH}"
|
|
||||||
|
|
||||||
[ "$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
|
|
||||||
API_KEY="${API_KEY:-$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24)}"
|
|
||||||
|
|
||||||
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 \
|
|
||||||
python3 python3-flask gunicorn nginx cron python3-qrcode >/dev/null
|
|
||||||
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}
|
|
||||||
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-${ARCH}" "$PHOBOS_DIR/bin/" 2>/dev/null || true
|
|
||||||
chmod +x "$PHOBOS_DIR/bin/"wg-obfuscator-* 2>/dev/null || true
|
|
||||||
ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-${ARCH}" /usr/local/bin/wg-obfuscator
|
|
||||||
cd /; rm -rf "$R"
|
|
||||||
fi
|
|
||||||
[ -x /usr/local/bin/wg-obfuscator ] || { echo "ERROR: obfuscator binary for $ARCH missing"; exit 1; }
|
|
||||||
|
|
||||||
# ── 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 = 10.25.0.1/16
|
|
||||||
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
|
|
||||||
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() {
|
|
||||||
if [ -n "$GH_TOKEN" ]; then
|
|
||||||
curl -fsSL -m20 -H "Authorization: token $GH_TOKEN" "$RAW/$1" -o "$2" && return 0
|
|
||||||
else
|
|
||||||
curl -fsSL -m20 "$RAW/$1" -o "$2" && return 0
|
|
||||||
fi
|
|
||||||
echo " WARN: fetch $1 failed"; return 1
|
|
||||||
}
|
|
||||||
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/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/phobos-pull.sh "$PHOBOS_DIR/repo/client/templates/phobos-pull.sh" && chmod +x "$PHOBOS_DIR/repo/client/templates/phobos-pull.sh"
|
|
||||||
fetch server/phobos-health.sh "$PHOBOS_DIR/server/phobos-health.sh" && chmod +x "$PHOBOS_DIR/server/phobos-health.sh"
|
|
||||||
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 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
|
|
||||||
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"
|
|
||||||
|
|
||||||
# ── 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"
|
|
||||||
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 -b 0.0.0.0:$PANEL_PORT app:app
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
Environment=PYTHONUNBUFFERED=1
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
systemctl daemon-reload; systemctl enable phobos-panel -q; systemctl restart phobos-panel
|
|
||||||
|
|
||||||
# ── 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
|
|
||||||
echo ""
|
|
||||||
echo "============================================"
|
|
||||||
echo " Installation complete"
|
|
||||||
echo " Panel : http://$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
|
|
||||||
@@ -1,669 +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"
|
|
||||||
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 "$@"
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/lib-core.sh"
|
|
||||||
|
|
||||||
check_root
|
|
||||||
load_env
|
|
||||||
ensure_dirs
|
|
||||||
|
|
||||||
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 client_ip_v4="$manual_ip"
|
|
||||||
local ipv4_prefix_main=$(echo "${SERVER_WG_IPV4_NETWORK:-10.25.0.0/16}" | cut -d'/' -f1 | cut -d'.' -f1-2)
|
|
||||||
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..."
|
|
||||||
declare -A 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)
|
|
||||||
[[ -n "$ip" ]] && used_ips["$ip"]=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
used_ips["${ipv4_prefix_main}.0.1"]=1
|
|
||||||
|
|
||||||
local found=false
|
|
||||||
for oct3 in {0..255}; do
|
|
||||||
local start_oct4=2
|
|
||||||
for oct4 in $(seq $start_oct4 254); do
|
|
||||||
local candidate="${ipv4_prefix_main}.${oct3}.${oct4}"
|
|
||||||
if [[ -z "${used_ips[$candidate]:-}" ]]; then
|
|
||||||
client_ip_v4="$candidate"
|
|
||||||
found=true
|
|
||||||
break 2
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ "$found" == "false" ]] && 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 = $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 "$pkg_root/bin"
|
|
||||||
|
|
||||||
cp "$dir/${id}.conf" "$pkg_root/${id}.conf"
|
|
||||||
cp "$dir/wg-obfuscator.conf" "$pkg_root/wg-obfuscator.conf"
|
|
||||||
|
|
||||||
for arch in mipsel mips aarch64 armv7 x86_64; do
|
|
||||||
[[ -f "$PHOBOS_DIR/bin/wg-obfuscator-$arch" ]] && cp "$PHOBOS_DIR/bin/wg-obfuscator-$arch" "$pkg_root/bin/"
|
|
||||||
done
|
|
||||||
|
|
||||||
local tpl_dir="$REPO_DIR/client/templates"
|
|
||||||
|
|
||||||
if [[ -d "$tpl_dir" ]]; then
|
|
||||||
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"
|
|
||||||
[[ -f "$tpl_dir/lib-client.sh" ]] && cp "$tpl_dir/lib-client.sh" "$pkg_root/lib-client.sh"
|
|
||||||
[[ -f "$tpl_dir/install-obfuscator.sh" ]] && cp "$tpl_dir/install-obfuscator.sh" "$pkg_root/install-obfuscator.sh"
|
|
||||||
[[ -f "$tpl_dir/install-wireguard.sh" ]] && cp "$tpl_dir/install-wireguard.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"
|
|
||||||
cat > "$pkg_root/failover.conf" <<FAILEOF
|
|
||||||
# Phobos Failover Configuration
|
|
||||||
SERVER_1=${SERVER_PUBLIC_IP_V4}: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 script_url="http://${SERVER_PUBLIC_IP_V4}:${HTTP_PORT:-80}/packages/$token/phobos-$id.tar.gz"
|
|
||||||
|
|
||||||
cat > "$WWW_DIR/init/$token.sh" <<EOF
|
|
||||||
#!/bin/sh
|
|
||||||
url="$script_url"
|
|
||||||
dir="/tmp/phobos_install_\$\$"
|
|
||||||
mkdir -p "\$dir"
|
|
||||||
echo "Downloading..."
|
|
||||||
if command -v curl >/dev/null; then
|
|
||||||
curl -L -s -o "\$dir/package.tar.gz" "\$url"
|
|
||||||
else
|
|
||||||
wget -q -O "\$dir/package.tar.gz" "\$url"
|
|
||||||
fi
|
|
||||||
if [ ! -f "\$dir/package.tar.gz" ]; then echo "Download failed"; exit 1; fi
|
|
||||||
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 cmd="curl -s http://${SERVER_PUBLIC_IP_V4}:${HTTP_PORT:-80}/init/$token.sh | sh"
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,93 +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: 10.25.0.1 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.
|
|
||||||
# - 10.25.0.1:8444 -> secondary server agent (phobos-api)
|
|
||||||
# - 10.25.0.1:10514 -> primary server panel
|
|
||||||
# - public panel IP -> bootstrap / if tunnel is down
|
|
||||||
PANEL="${PANEL:-http://212.118.52.193:10514}"
|
|
||||||
PANEL_URLS="${PANEL_URLS:-http://10.25.0.1:8444 http://10.25.0.1:10514 $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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
curl -s -m 8 -o "$TMP" "${base}/api/router-config/${CLIENT_ID}?token=${TOKEN}" 2>/dev/null || 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
|
|
||||||
@@ -1,429 +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.25.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}"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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 "$@"
|
|
||||||
@@ -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 -b 0.0.0.0:8443 app:app
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
Environment=PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
147
server/api.py
147
server/api.py
@@ -1,147 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Phobos Secondary Server API — peer management + health."""
|
|
||||||
import json, os, subprocess
|
|
||||||
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 key == env.get("MAIN_API_KEY", "")
|
|
||||||
|
|
||||||
@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 = {}
|
|
||||||
try:
|
|
||||||
out = subprocess.check_output("top -bn1 | grep Cpu", shell=True, text=True, timeout=5)
|
|
||||||
idle = float([x for x in out.split(",") if "id" in x][0].split()[0])
|
|
||||||
cpu = f"{round(100 - idle, 1)}%"
|
|
||||||
except Exception:
|
|
||||||
cpu = "?"
|
|
||||||
try:
|
|
||||||
mem = subprocess.check_output("free -m", shell=True, text=True).split("\n")[1].split()
|
|
||||||
mem_str = f"{mem[2]}/{mem[1]}MB"
|
|
||||||
except Exception:
|
|
||||||
mem_str = "?"
|
|
||||||
return jsonify({"status": "ok", "peers": peers, "peer_keys": peer_keys, "handshakes": handshakes, "cpu": cpu, "mem": mem_str})
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
@app.route("/api/peers/add", methods=["POST"])
|
|
||||||
def add_peer():
|
|
||||||
if not check_api_key():
|
|
||||||
return jsonify({"error": "unauthorized"}), 401
|
|
||||||
data = request.json
|
|
||||||
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
|
|
||||||
pub_key = request.json.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://10.25.0.1: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)
|
|
||||||
@@ -1,446 +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_PORT=10514
|
|
||||||
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.
|
|
||||||
code=$(curl -s -m 4 -o /dev/null -w '%{http_code}' "http://${host}:${PRIMARY_PROBE_PORT}${PRIMARY_PROBE_PATH}" 2>/dev/null)
|
|
||||||
if [ "$code" = "200" ]; then
|
|
||||||
log "PRIMARY ($host obf-health http=200) alive, switching back"
|
|
||||||
switch_endpoint 1 0
|
|
||||||
echo "0" > "$STATE/fail_count"
|
|
||||||
else
|
|
||||||
log "PRIMARY still down (obf-health http=${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
|
|
||||||
@@ -1,93 +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: 10.25.0.1 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.
|
|
||||||
# - 10.25.0.1:8444 -> secondary server agent (phobos-api)
|
|
||||||
# - 10.25.0.1:10514 -> primary server panel
|
|
||||||
# - public panel IP -> bootstrap / if tunnel is down
|
|
||||||
PANEL="${PANEL:-http://212.118.52.193:10514}"
|
|
||||||
PANEL_URLS="${PANEL_URLS:-http://10.25.0.1:8444 http://10.25.0.1:10514 $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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
curl -s -m 8 -o "$TMP" "${base}/api/router-config/${CLIENT_ID}?token=${TOKEN}" 2>/dev/null || 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
|
|
||||||
@@ -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()
|
|
||||||
@@ -1,391 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# ============================================================
|
|
||||||
# Phobos Secondary Server Setup
|
|
||||||
# Deploys WG + obfuscator + mini-API (no web panel)
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# MAIN_SERVER=212.118.52.193 MAIN_API_KEY=secret123 \
|
|
||||||
# bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/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}"
|
|
||||||
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
|
|
||||||
PHOBOS_DIR="/opt/Phobos"
|
|
||||||
|
|
||||||
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}"
|
|
||||||
|
|
||||||
# ── 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 "║ 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..."
|
|
||||||
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 jq curl 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
|
|
||||||
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-$(uname -m) "$PHOBOS_DIR/bin/" 2>/dev/null || true
|
|
||||||
chmod +x "$PHOBOS_DIR/bin/"wg-obfuscator-*
|
|
||||||
ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-$(uname -m)" /usr/local/bin/wg-obfuscator
|
|
||||||
rm -rf "$REPO_DIR"
|
|
||||||
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 = 10.25.0.1/16
|
|
||||||
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
|
|
||||||
OBFUSCATOR_KEY=$OBF_KEY
|
|
||||||
OBFUSCATOR_PORTS=$OBF_PORTS
|
|
||||||
MAIN_SERVER=$MAIN_SERVER
|
|
||||||
MAIN_API_KEY=$MAIN_API_KEY
|
|
||||||
ROLE=secondary
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# ── 6. Mini-API ──
|
|
||||||
echo "[6/7] Setting up mini-API..."
|
|
||||||
# Mini-API (fetched from PCA — includes /api/router-config for tunnel pull)
|
|
||||||
PCA_BRANCH="${PCA_BRANCH:-main}"
|
|
||||||
RAW="https://raw.githubusercontent.com/andrey271192/PCA_Phobos/${PCA_BRANCH}"
|
|
||||||
if [ -n "${GH_TOKEN:-}" ]; then
|
|
||||||
curl -fsSL -H "Authorization: token $GH_TOKEN" "$RAW/server/api.py" -o "$PHOBOS_DIR/server/api.py"
|
|
||||||
else
|
|
||||||
curl -fsSL "$RAW/server/api.py" -o "$PHOBOS_DIR/server/api.py"
|
|
||||||
fi
|
|
||||||
[ -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..."
|
|
||||||
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 -s -X POST "http://${MAIN_SERVER}:$(cat /opt/phobos-panel/.port 2>/dev/null || echo 8443)/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 ""
|
|
||||||
94
update.sh
94
update.sh
@@ -1,94 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# ============================================================
|
|
||||||
# PCA Phobos — updater / rollback
|
|
||||||
#
|
|
||||||
# phobos-update # обновить до последней версии (main)
|
|
||||||
# phobos-update v1.1.0 # обновить до конкретной версии (git tag)
|
|
||||||
# phobos-update --rollback # откатить на предыдущую версию (из бэкапа)
|
|
||||||
# phobos-update --version # показать установленную и последнюю версию
|
|
||||||
# phobos-update --list # список бэкапов для отката
|
|
||||||
#
|
|
||||||
# Обновляет ТОЛЬКО PCA-слой (панель + скрипты онбординга/служебные).
|
|
||||||
# НЕ трогает ключи WireGuard, server.env, клиентов — они сохраняются.
|
|
||||||
# Перед каждым обновлением делает бэкап; откат восстанавливает его.
|
|
||||||
# ============================================================
|
|
||||||
set -e
|
|
||||||
PHOBOS_DIR=/opt/Phobos
|
|
||||||
PANEL_DIR=/opt/phobos-panel
|
|
||||||
REPO=andrey271192/PCA_Phobos
|
|
||||||
RAW="https://raw.githubusercontent.com/$REPO"
|
|
||||||
BACKUPS="$PHOBOS_DIR/updates"
|
|
||||||
VERFILE="$PANEL_DIR/.version"
|
|
||||||
CUR=$(cat "$VERFILE" 2>/dev/null || echo "unknown")
|
|
||||||
|
|
||||||
managed_files() {
|
|
||||||
cat <<LIST
|
|
||||||
$PANEL_DIR/app.py|app.py
|
|
||||||
$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/router-configure-wireguard.sh|overlay/router-configure-wireguard.sh
|
|
||||||
$PHOBOS_DIR/repo/client/templates/phobos-pull.sh|overlay/phobos-pull.sh
|
|
||||||
$PHOBOS_DIR/server/phobos-health.sh|server/phobos-health.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/update.sh|update.sh
|
|
||||||
LIST
|
|
||||||
}
|
|
||||||
|
|
||||||
latest_version() { curl -fsSL -m10 "$RAW/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 curl -fsSL -m25 "$RAW/$ref/$rp" -o "$tmp" && [ -s "$tmp" ]; then
|
|
||||||
mkdir -p "$(dirname "$lp")"; mv "$tmp" "$lp"
|
|
||||||
case "$lp" in *.sh|*.py) chmod +x "$lp" 2>/dev/null || true;; esac
|
|
||||||
else
|
|
||||||
rm -f "$tmp"; echo " WARN: не удалось скачать $rp (пропуск)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-}" in
|
|
||||||
--version|-v)
|
|
||||||
echo "Установлено: $CUR"
|
|
||||||
echo "Доступно: $(latest_version)"
|
|
||||||
;;
|
|
||||||
--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"
|
|
||||||
systemctl restart phobos-panel 2>/dev/null || true
|
|
||||||
echo "Откат выполнен -> $fromver"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
ref="${1:-main}"; [ "$ref" = "latest" ] && ref="main"
|
|
||||||
target=$(curl -fsSL -m10 "$RAW/$ref/VERSION" 2>/dev/null | tr -d ' \r\n'); [ -z "$target" ] && target="$ref"
|
|
||||||
echo "Текущая: $CUR -> целевая: $target ($ref)"
|
|
||||||
b=$(do_backup); echo "Бэкап: $b"
|
|
||||||
apply_ref "$ref"
|
|
||||||
echo "$target" > "$VERFILE"
|
|
||||||
systemctl restart phobos-panel 2>/dev/null || true
|
|
||||||
echo "Обновлено до $target. Откат: phobos-update --rollback"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
Reference in New Issue
Block a user