feat(migrations): Add WARP auto-integration with redsocks and iptables

- Implemented migration 067 to set up Cloudflare WARP with automatic routing for VPN client TCP traffic through a redsocks proxy.
- Included installation scripts for WARP and redsocks, along with iptables rules for traffic redirection.
- Added detection for X-Ray and patching of its outbound configuration.
- Created uninstall scripts to clean up configurations and remove installed packages.

fix(migrations): Enhance WARP install script for heredoc compatibility

- Implemented migration 068 to fix nested heredoc conflicts and streamline the WARP installation script for panel compatibility.
- Removed duplicate `set -eo pipefail` and adjusted formatting for better readability.

feat(migrations): Auto-detect AIVPN subnet for routing in WARP setup

- Implemented migration 069 to enhance the WARP installation script by adding detection for AIVPN subnets alongside existing AWG container detection.
- Updated routing logic to handle both container IPs and host-level VPN subnets.
- Ensured proper configuration of iptables for seamless traffic routing through the WARP proxy.
This commit is contained in:
infosave2007
2026-04-25 10:40:21 +03:00
parent f04f9dd1cb
commit 809b0ca63d
11 changed files with 3178 additions and 113 deletions
+14
View File
@@ -31,6 +31,7 @@ Web-based management panel for Amnezia AWG (WireGuard) VPN servers.
- MTProxy (Telegram) (`mtproxy`)
- SMB Server (`smb`)
- AIVPN (`aivpn`) - https://github.com/infosave2007/aivpn
- Cloudflare WARP Proxy (`cf-warp`) — transparent traffic proxying via Cloudflare
## Requirements
@@ -205,6 +206,19 @@ Manage VPN protocols via **Settings → Protocols**:
- Configure protocol settings (ports, transport, obfuscation)
- **AI Assistant**: Use "Ask AI" to generate complex protocol configurations tailored to your needs (requires OpenRouter API key).
### Cloudflare WARP Proxy
WARP transparently proxies **all TCP traffic** from VPN clients through the Cloudflare network, hiding the server's real IP address.
> **⚠️ Install WARP last** — after all other protocols (AWG, X-Ray, AIVPN, etc.). During installation, WARP automatically detects active VPN containers and interfaces and configures routing for each of them.
**Supported protocols:**
- **AWG / AWG2** — routing via container IP + host redsocks
- **X-Ray VLESS** — `warp-out` outbound via SOCKS5 in X-Ray config
- **AIVPN / WireGuard** — routing via host-level iptables + redsocks
**Verification:** connect to VPN and open `https://1.1.1.1/cdn-cgi/trace` — the field `warp=on` confirms it's working.
### Scenario Testing & Logs
**Scenario Testing**:
+409
View File
@@ -0,0 +1,409 @@
# Amnezia VPN Web Panel
Веб-панель управления для VPN-серверов Amnezia AWG (WireGuard).
## Возможности
- Развертывание VPN-серверов через SSH (пароль или **SSH-ключ**)
- **Импорт из существующих VPN-панелей** (wg-easy, 3x-ui)
- **Расширенное управление протоколами** (WireGuard, AmneziaWG, OpenVPN, Shadowsocks и др.)
- **AI-настройка протоколов** через OpenRouter (опционально)
- Управление клиентскими конфигурациями с **датами истечения**
- **Лимиты трафика** для клиентов с автоматическим применением
- **Резервное копирование и восстановление** серверов
- **Тестирование сценариев**: определение и проверка различных сценариев подключения VPN across протоколов
- **Расширенное управление логами**: просмотр, поиск и управление системными и контейнерными логами
- Мониторинг статистики трафика
- Генерация QR-кодов для мобильных приложений
- Многоязычный интерфейс (английский, русский, испанский, немецкий, французский, китайский)
- REST API с JWT-аутентификацией
- Аутентификация пользователей и контроль доступа
- **Автоматическая проверка истечения срока действия клиентов и лимитов трафика** через cron
## Доступные протоколы
- AmneziaWG Advanced (`amnezia-wg-advanced`)
- AmneziaWG 2.0 (`awg2`)
- WireGuard Standard (`wireguard-standard`)
- OpenVPN (`openvpn`)
- Shadowsocks (`shadowsocks`)
- XRay VLESS (`xray-vless`)
- MTProxy (Telegram) (`mtproxy`)
- SMB Server (`smb`)
- AIVPN (`aivpn`) - https://github.com/infosave2007/aivpn
- Cloudflare WARP Proxy (`cf-warp`) — прозрачное проксирование трафика через Cloudflare
## Требования
- Docker
- Docker Compose
## Установка
```bash
git clone https://github.com/infosave2007/amneziavpnphp.git
cd amneziavpnphp
cp .env.example .env
# Для Docker Compose V2 (рекомендуется)
docker compose up -d
docker compose exec web composer install
# Дождитесь готовности БД (начальные SQL-файлы миграции применяются автоматически через MySQL entrypoint)
until [ "$(docker inspect -f '{{.State.Health.Status}}' amnezia-panel-db 2>/dev/null)" = "healthy" ]; do
sleep 2
done
# Или для старой Docker Compose V1
docker-compose up -d
docker-compose exec web composer install
until [ "$(docker inspect -f '{{.State.Health.Status}}' amnezia-panel-db 2>/dev/null)" = "healthy" ]; do
sleep 2
done
# Ручной режим миграции (для существующих установок / обновлений)
set -a; source .env; set +a
for f in migrations/*.sql; do
docker compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < "$f" || true
done
# Для Docker Compose V1 ручной режим миграции:
# for f in migrations/*.sql; do
# docker-compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < "$f" || true
# done
```
Доступ: http://localhost:8082
Данные для входа по умолчанию: admin@amnez.ia / admin123
### Предварительные требования для удаленного сервера
Для развертывания протоколов на чистом удаленном хосте, Docker Engine должен быть доступен на этом хосте.
Если Docker отсутствует, установите его сначала (пример для Ubuntu):
```bash
apt-get update -y
apt-get install -y ca-certificates curl gnupg lsb-release
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
```
## Настройка
Отредактируйте `.env`:
```
DB_HOST=db
DB_PORT=3306
DB_DATABASE=amnezia_panel
DB_USERNAME=amnezia
DB_PASSWORD=amnezia
ADMIN_EMAIL=admin@amnez.ia
ADMIN_PASSWORD=admin123
JWT_SECRET=your-secret-key-change-this
```
## Использование
### Добавление VPN-сервера
1. Серверы → Добавить сервер
2. Введите: имя, IP хоста, SSH-порт, имя пользователя
3. Выберите метод аутентификации: **Пароль** или **SSH-ключ**
- Для SSH-ключа: вставьте ваш приватный ключ (формат PEM/OpenSSH)
3. **(Опционально) Включите импорт из существующей панели:**
- Отметьте "Импортировать из существующей панели"
- Выберите тип панели (wg-easy или 3x-ui)
- Загрузите файл резервной копии (JSON)
4. Нажмите "Создать сервер"
5. Дождитесь развертывания
6. Клиенты будут импортированы автоматически, если импорт был включен
### Создание клиента
1. Откройте детали сервера
2. Введите имя клиента
3. **Выберите период истечения** (опционально, по умолчанию: бессрочно)
4. **Выберите лимит трафика** (опционально, по умолчанию: безлимитно)
5. Нажмите "Создать клиента"
6. Скачайте конфигурацию или отсканируйте QR-код
### Управление истечением срока действия клиента
Установите истечение через UI или API:
```bash
# Установить конкретную дату
curl -X POST http://localhost:8082/api/clients/123/set-expiration \
-H "Authorization: Bearer <token>" \
-d '{"expires_at": "2025-12-31 23:59:59"}'
# Продлить на 30 дней
curl -X POST http://localhost:8082/api/clients/123/extend \
-H "Authorization: Bearer <token>" \
-d '{"days": 30}'
# Получить клиентов, у которых скоро истекает срок (в течение 7 дней)
curl http://localhost:8082/api/clients/expiring?days=7 \
-H "Authorization: Bearer <token>"
```
### Управление лимитами трафика
Установите и отслеживайте лимиты трафика через UI или API:
```bash
# Установить лимит трафика (10 ГБ = 10737418240 байт)
curl -X POST http://localhost:8082/api/clients/123/set-traffic-limit \
-H "Authorization: Bearer <token>" \
-d '{"limit_bytes": 10737418240}'
# Удалить лимит трафика (установить безлимитный)
curl -X POST http://localhost:8082/api/clients/123/set-traffic-limit \
-H "Authorization: Bearer <token>" \
-d '{"limit_bytes": null}'
# Проверить статус лимита трафика
curl http://localhost:8082/api/clients/123/traffic-limit-status \
-H "Authorization: Bearer <token>"
# Получить клиентов, превысивших лимит трафика
curl http://localhost:8082/api/clients/overlimit \
-H "Authorization: Bearer <token>"
```
### Резервное копирование серверов
Создавайте и восстанавливайте резервные копии через UI или API:
```bash
# Создать резервную копию
curl -X POST http://localhost:8082/api/servers/1/backup \
-H "Authorization: Bearer <token>"
# Список резервных копий
curl http://localhost:8082/api/servers/1/backups \
-H "Authorization: Bearer <token>"
# Восстановить из резервной копии
curl -X POST http://localhost:8082/api/servers/1/restore \
-H "Authorization: Bearer <token>" \
-d '{"backup_id": 123}'
```
### Управление протоколами
Управляйте VPN-протоколами через **Настройки → Протоколы**:
- Установка/удаление протоколов (WireGuard, AmneziaWG, OpenVPN и др.)
- Настройка параметров протокола (порты, транспорт, маскировка)
- **AI-ассистент**: используйте "Спросить AI" для генерации сложных конфигураций протоколов, адаптированных к вашим потребностям (требуется API-ключ OpenRouter).
### Cloudflare WARP Proxy
WARP прозрачно проксирует **весь TCP-трафик** от VPN-клиентов через сеть Cloudflare, скрывая реальный IP-адрес сервера.
> **⚠️ Устанавливайте WARP последним** — после всех других протоколов (AWG, X-Ray, AIVPN и др.). Во время установки WARP автоматически обнаруживает активные VPN-контейнеры и интерфейсы и настраивает маршрутизацию для каждого из них.
**Поддерживаемые протоколы:**
- **AWG / AWG2** — маршрутизация через IP контейнера + хост redsocks
- **X-Ray VLESS** — исходящий `warp-out` через SOCKS5 в конфигурации X-Ray
- **AIVPN / WireGuard** — маршрутизация через iptables + redsocks на уровне хоста
**Проверка:** подключитесь к VPN и откройте `https://1.1.1.1/cdn-cgi/trace` — поле `warp=on` подтверждает работоспособность.
### Тестирование сценариев и логи
**Тестирование сценариев**:
- Создавайте тестовые сценарии для проверки подключения через различные протоколы и сетевые условия.
- Запускайте автоматические тесты для обеспечения надежности вашей VPN-инфраструктуры.
**Управление логами**:
- Централизованный просмотр всех системных, контейнерных и прикладных логов.
- Возможности поиска и фильтрации для быстрой диагностики проблем.
### AI-ассистент
Настройте API-ключ OpenRouter in **Настройки** для включения:
- Автоматический перевод интерфейса
- AI-помощник для настройки протоколов
- Интеллектуальные предложения по устранению неполадок
### Автоматический мониторинг и сбор метрик
**Сборщик метрик запускается автоматически** при старте контейнера и отслеживается cron каждые 3 минуты. Если процесс падает, он автоматически перезапускается.
Проверить логи сборщика метрик:
```bash
docker compose exec web tail -f /var/log/metrics_collector.log
```
Проверить логи скрипта мониторинга:
```bash
docker compose exec web tail -f /var/log/metrics_monitor.log
```
Перезапустить сборщик метрик вручную:
```bash
docker compose exec web pkill -f collect_metrics.php
# Он будет автоматически перезапущен в течение 3 минут скриптом мониторинга
```
### Автоматическая проверка истечения срока действия клиентов
**Запускается автоматически в Docker-контейнере** каждый час для отключения истекших клиентов.
Проверить логи cron:
```bash
docker compose exec web tail -f /var/log/cron.log
```
Запустить вручную:
```bash
docker compose exec web php /var/www/html/bin/check_expired_clients.php
```
### Автоматическая проверка лимитов трафика
**Запускается автоматически в Docker-контейнере** каждый час для отключения клиентов, превысивших лимит трафика.
Проверить логи cron:
```bash
docker compose exec web tail -f /var/log/cron.log
```
Запустить вручную:
```bash
docker compose exec web php /var/www/html/bin/check_traffic_limits.php
```
### API-аутентификация
Получить JWT-токен:
```bash
curl -X POST http://localhost:8082/api/auth/token \
-d "email=admin@amnez.ia&password=admin123"
```
Использовать токен:
```bash
curl -H "Authorization: Bearer <token>" \
http://localhost:8082/api/servers
```
## API Endpoints
### Аутентификация
```
POST /api/auth/token - Получить JWT-токен
POST /api/tokens - Создать постоянный API-токен
GET /api/tokens - Список API-токенов
DELETE /api/tokens/{id} - Отозвать токен
```
### Серверы
```
GET /api/servers - Список всех серверов
POST /api/servers/create - Создать новый сервер
Параметры: name, host, port, username, password
DELETE /api/servers/{id}/delete - Удалить сервер по ID
GET /api/servers/{id}/clients - Список клиентов на сервере
```
### Протоколы
```
GET /api/protocols/active - Список всех доступных протоколов (JWT-дружественный, включает ID протоколов)
GET /api/protocols - Управление протоколами (требует session admin auth, не JWT)
GET /api/servers/{id}/protocols - Список установленных протоколов на сервере
POST /api/servers/{id}/protocols/install - Установить протокол
```
### Клиенты
```
GET /api/clients - Список всех клиентов
GET /api/clients/{id}/details - Получить детали клиента со статистикой, конфигурацией и QR-кодом
GET /api/clients/{id}/qr - Получить QR-код клиента
POST /api/clients/create - Создать нового клиента (возвращает конфигурацию и QR-код)
Параметры: server_id, name, protocol_id (опционально, по умолчанию: установлен), expires_in_days (опционально)
POST /api/clients/{id}/revoke - Отозвать доступ клиента
POST /api/clients/{id}/restore - Восстановить доступ клиента
DELETE /api/clients/{id}/delete - Удалить клиента по ID (удаляет из БД и сервера)
POST /api/clients/{id}/set-expiration - Установить дату истечения клиента
Параметры: expires_at (Y-m-d H:i:s или null)
POST /api/clients/{id}/extend - Продлить истечение клиента
Параметры: days (int)
GET /api/clients/expiring - Получить клиентов, у которых скоро истекает срок
Параметры: days (по умолчанию: 7)
POST /api/clients/{id}/set-traffic-limit - Установить лимит трафика клиента
Параметры: limit_bytes (int или null для безлимитного)
GET /api/clients/{id}/traffic-limit-status - Получить статус лимита трафика
GET /api/clients/overlimit - Получить клиентов, превысивших лимит трафика
```
### Резервные копии
```
POST /api/servers/{id}/backup - Создать резервную копию сервера
GET /api/servers/{id}/backups - Список резервных копий сервера
POST /api/servers/{id}/restore - Восстановить из резервной копии
Параметры: backup_id
DELETE /api/backups/{id} - Удалить резервную копию
```
### Импорт панели
```
POST /api/servers/{id}/import - Импортировать клиентов из существующей панели
Параметры: panel_type (wg-easy|3x-ui), backup_file (multipart/form-data)
GET /api/servers/{id}/imports - Получить историю импорта для сервера
```
## Перевод
Добавьте API-ключ OpenRouter в настройках, затем запустите:
```bash
docker compose exec web php bin/translate_all.php
```
Или переведите через веб-интерфейс: Настройки → Автоперевод
## Структура
```
public/index.php - Маршруты
inc/ - Основные классы
Auth.php - Аутентификация
DB.php - Подключение к базе данных
Router.php - Маршрутизация URL
View.php - Twig-шаблоны
VpnServer.php - Управление серверами
VpnClient.php - Управление клиентами
Translator.php - Многоязычность
JWT.php - Токен-аутентификация
QrUtil.php - Генерация QR-кодов
PanelImporter.php - Импорт из wg-easy/3x-ui
InstallProtocolManager.php - Ядро управления протоколами
OpenRouterService.php - AI-интеграция
templates/ - Twig-шаблоны
migrations/ - SQL-миграции (выполняются в алфавитном порядке)
```
## Технологический стек
- PHP 8.2
- MySQL 8.0
- Twig 3
- Tailwind CSS
- Docker
## Лицензия
MIT
## Поддержать проект
Если вы находите этот проект полезным, вы можете поддержать его разработку через пожертвование через Tribute: https://t.me/tribute/app?startapp=dzX1
+409
View File
@@ -0,0 +1,409 @@
# Amnezia VPN Web Panel
用于管理 Amnezia AWG (WireGuard) VPN 服务器的 Web 面板。
## 功能特性
- 通过 SSH 部署 VPN 服务器(密码或 **SSH 密钥**
- **从现有 VPN 面板导入**wg-easy、3x-ui
- **高级协议管理**WireGuard、AmneziaWG、OpenVPN、Shadowsocks 等)
- **AI 驱动的协议配置** 使用 OpenRouter(可选)
- 客户端配置管理,支持**过期日期**
- 客户端**流量限制**,自动执行
- **服务器备份和恢复**功能
- **场景测试**:定义和测试不同协议的网络连接场景
- **高级日志管理**:查看、搜索和管理系统和容器日志
- 流量统计监控
- 为移动应用生成二维码
- 多语言界面(英语、俄语、西班牙语、德语、法语、中文)
- 带 JWT 认证的 REST API
- 用户认证和访问控制
- **自动客户端过期和流量限制检查** 通过 cron
## 可用协议
- AmneziaWG Advanced (`amnezia-wg-advanced`)
- AmneziaWG 2.0 (`awg2`)
- WireGuard 标准 (`wireguard-standard`)
- OpenVPN (`openvpn`)
- Shadowsocks (`shadowsocks`)
- XRay VLESS (`xray-vless`)
- MTProxy (Telegram) (`mtproxy`)
- SMB 服务器 (`smb`)
- AIVPN (`aivpn`) - https://github.com/infosave2007/aivpn
- Cloudflare WARP 代理 (`cf-warp`) — 通过 Cloudflare 透明代理流量
## 系统要求
- Docker
- Docker Compose
## 安装
```bash
git clone https://github.com/infosave2007/amneziavpnphp.git
cd amneziavpnphp
cp .env.example .env
# 对于 Docker Compose V2(推荐)
docker compose up -d
docker compose exec web composer install
# 等待数据库准备就绪(初始 SQL 迁移文件由 MySQL 入口点自动应用)
until [ "$(docker inspect -f '{{.State.Health.Status}}' amnezia-panel-db 2>/dev/null)" = "healthy" ]; do
sleep 2
done
# 或者对于旧版 Docker Compose V1
docker-compose up -d
docker-compose exec web composer install
until [ "$(docker inspect -f '{{.State.Health.Status}}' amnezia-panel-db 2>/dev/null)" = "healthy" ]; do
sleep 2
done
# 手动迁移模式(仅用于现有安装/更新)
set -a; source .env; set +a
for f in migrations/*.sql; do
docker compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < "$f" || true
done
# 对于 Docker Compose V1 手动迁移模式:
# for f in migrations/*.sql; do
# docker-compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < "$f" || true
# done
```
访问地址:http://localhost:8082
默认登录凭据:admin@amnez.ia / admin123
### 远程服务器前提条件
要在干净的远程主机上部署协议,该主机必须可用 Docker Engine。
如果缺少 Docker,请先安装(Ubuntu 示例):
```bash
apt-get update -y
apt-get install -y ca-certificates curl gnupg lsb-release
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
```
## 配置
编辑 `.env` 文件:
```
DB_HOST=db
DB_PORT=3306
DB_DATABASE=amnezia_panel
DB_USERNAME=amnezia
DB_PASSWORD=amnezia
ADMIN_EMAIL=admin@amnez.ia
ADMIN_PASSWORD=admin123
JWT_SECRET=your-secret-key-change-this
```
## 使用方法
### 添加 VPN 服务器
1. 服务器 → 添加服务器
2. 输入:名称、主机 IP、SSH 端口、用户名
3. 选择认证方法:**密码** 或 **SSH 密钥**
- 对于 SSH 密钥:粘贴您的私钥(PEM/OpenSSH 格式)
3. **(可选)启用从现有面板导入:**
- 勾选"从现有面板导入"
- 选择面板类型(wg-easy 或 3x-ui
- 上传备份文件(JSON
4. 点击"创建服务器"
5. 等待部署完成
6. 如果启用了导入,客户端将自动导入
### 创建客户端
1. 打开服务器详情
2. 输入客户端名称
3. **选择过期时间**(可选,默认:永不过期)
4. **选择流量限制**(可选,默认:无限制)
5. 点击创建客户端
6. 下载配置或扫描二维码
### 管理客户端过期时间
通过 UI 或 API 设置过期时间:
```bash
# 设置特定日期
curl -X POST http://localhost:8082/api/clients/123/set-expiration \
-H "Authorization: Bearer <token>" \
-d '{"expires_at": "2025-12-31 23:59:59"}'
# 延长 30 天
curl -X POST http://localhost:8082/api/clients/123/extend \
-H "Authorization: Bearer <token>" \
-d '{"days": 30}'
# 获取即将过期的客户端(7 天内)
curl http://localhost:8082/api/clients/expiring?days=7 \
-H "Authorization: Bearer <token>"
```
### 管理流量限制
通过 UI 或 API 设置和监控流量限制:
```bash
# 设置流量限制(10 GB = 10737418240 字节)
curl -X POST http://localhost:8082/api/clients/123/set-traffic-limit \
-H "Authorization: Bearer <token>" \
-d '{"limit_bytes": 10737418240}'
# 移除流量限制(设置为无限制)
curl -X POST http://localhost:8082/api/clients/123/set-traffic-limit \
-H "Authorization: Bearer <token>" \
-d '{"limit_bytes": null}'
# 检查流量限制状态
curl http://localhost:8082/api/clients/123/traffic-limit-status \
-H "Authorization: Bearer <token>"
# 获取超过流量限制的客户端
curl http://localhost:8082/api/clients/overlimit \
-H "Authorization: Bearer <token>"
```
### 服务器备份
通过 UI 或 API 创建和恢复备份:
```bash
# 创建备份
curl -X POST http://localhost:8082/api/servers/1/backup \
-H "Authorization: Bearer <token>"
# 列出备份
curl http://localhost:8082/api/servers/1/backups \
-H "Authorization: Bearer <token>"
# 从备份恢复
curl -X POST http://localhost:8082/api/servers/1/restore \
-H "Authorization: Bearer <token>" \
-d '{"backup_id": 123}'
```
### 协议管理
通过 **设置 → 协议** 管理 VPN 协议:
- 安装/卸载协议(WireGuard、AmneziaWG、OpenVPN 等)
- 配置协议设置(端口、传输、混淆)
- **AI 助手**:使用"询问 AI"生成符合您需求的复杂协议配置(需要 OpenRouter API 密钥)。
### Cloudflare WARP 代理
WARP 透明地代理 **所有 TCP 流量** 从 VPN 客户端通过 Cloudflare 网络,隐藏服务器的真实 IP 地址。
> **⚠️ 最后安装 WARP** — 在所有其他协议之后(AWG、X-Ray、AIVPN 等)。安装过程中,WARP 会自动检测活跃的 VPN 容器和接口,并为每个配置路由。
**支持的协议:**
- **AWG / AWG2** — 通过容器 IP + 主机 redsocks 路由
- **X-Ray VLESS** — 通过 X-Ray 配置中的 SOCKS5 `warp-out` 出站
- **AIVPN / WireGuard** — 通过主机级 iptables + redsocks 路由
**验证:** 连接到 VPN 并打开 `https://1.1.1.1/cdn-cgi/trace` — 字段 `warp=on` 确认工作正常。
### 场景测试和日志
**场景测试**
- 创建测试场景以验证跨不同协议和网络条件的连接。
- 运行自动化测试以确保您的 VPN 基础设施可靠。
**日志管理**
- 所有系统、容器和应用程序日志的集中视图。
- 搜索和过滤功能,快速诊断问题。
### AI 助手
**设置** 中配置 OpenRouter API 密钥以启用:
- 界面自动翻译
- AI 辅助协议配置
- 智能故障排除建议
### 自动监控和指标收集
**指标收集器在容器启动时自动运行**,并由 cron 每 3 分钟监控一次。如果进程崩溃,将自动重启。
检查指标收集器日志:
```bash
docker compose exec web tail -f /var/log/metrics_collector.log
```
检查监控脚本日志:
```bash
docker compose exec web tail -f /var/log/metrics_monitor.log
```
手动重启指标收集器:
```bash
docker compose exec web pkill -f collect_metrics.php
# 它将在 3 分钟内由监控脚本自动重启
```
### 自动客户端过期检查
**在 Docker 容器中自动运行**,每小时禁用过期客户端。
检查 cron 日志:
```bash
docker compose exec web tail -f /var/log/cron.log
```
手动运行:
```bash
docker compose exec web php /var/www/html/bin/check_expired_clients.php
```
### 自动流量限制检查
**在 Docker 容器中自动运行**,每小时禁用超过流量限制的客户端。
检查 cron 日志:
```bash
docker compose exec web tail -f /var/log/cron.log
```
手动运行:
```bash
docker compose exec web php /var/www/html/bin/check_traffic_limits.php
```
### API 认证
获取 JWT 令牌:
```bash
curl -X POST http://localhost:8082/api/auth/token \
-d "email=admin@amnez.ia&password=admin123"
```
使用令牌:
```bash
curl -H "Authorization: Bearer <token>" \
http://localhost:8082/api/servers
```
## API 端点
### 认证
```
POST /api/auth/token - 获取 JWT 令牌
POST /api/tokens - 创建持久 API 令牌
GET /api/tokens - 列出 API 令牌
DELETE /api/tokens/{id} - 撤销令牌
```
### 服务器
```
GET /api/servers - 列出所有服务器
POST /api/servers/create - 创建新服务器
参数:name, host, port, username, password
DELETE /api/servers/{id}/delete - 按 ID 删除服务器
GET /api/servers/{id}/clients - 列出服务器上的客户端
```
### 协议
```
GET /api/protocols/active - 列出所有可用协议(JWT 友好,包含协议 ID)
GET /api/protocols - 协议管理端点(需要会话管理员认证,非 JWT)
GET /api/servers/{id}/protocols - 列出服务器上已安装的协议
POST /api/servers/{id}/protocols/install - 安装协议
```
### 客户端
```
GET /api/clients - 列出所有客户端
GET /api/clients/{id}/details - 获取客户端详情,包括统计信息、配置和二维码
GET /api/clients/{id}/qr - 获取客户端二维码
POST /api/clients/create - 创建新客户端(返回配置和二维码)
参数:server_id, name, protocol_id(可选,默认:已安装), expires_in_days(可选)
POST /api/clients/{id}/revoke - 撤销客户端访问
POST /api/clients/{id}/restore - 恢复客户端访问
DELETE /api/clients/{id}/delete - 按 ID 删除客户端(从数据库和服务器删除)
POST /api/clients/{id}/set-expiration - 设置客户端过期日期
参数:expires_atY-m-d H:i:s 或 null
POST /api/clients/{id}/extend - 延长客户端过期时间
参数:daysint
GET /api/clients/expiring - 获取即将过期的客户端
参数:days(默认:7)
POST /api/clients/{id}/set-traffic-limit - 设置客户端流量限制
参数:limit_bytesint 或 null 表示无限制)
GET /api/clients/{id}/traffic-limit-status - 获取流量限制状态
GET /api/clients/overlimit - 获取超过流量限制的客户端
```
### 备份
```
POST /api/servers/{id}/backup - 创建服务器备份
GET /api/servers/{id}/backups - 列出服务器备份
POST /api/servers/{id}/restore - 从备份恢复
参数:backup_id
DELETE /api/backups/{id} - 删除备份
```
### 面板导入
```
POST /api/servers/{id}/import - 从现有面板导入客户端
参数:panel_typewg-easy|3x-ui, backup_filemultipart/form-data
GET /api/servers/{id}/imports - 获取服务器导入历史记录
```
## 翻译
在设置中添加 OpenRouter API 密钥,然后运行:
```bash
docker compose exec web php bin/translate_all.php
```
或通过 Web 界面翻译:设置 → 自动翻译
## 项目结构
```
public/index.php - 路由
inc/ - 核心类
Auth.php - 认证
DB.php - 数据库连接
Router.php - URL 路由
View.php - Twig 模板
VpnServer.php - 服务器管理
VpnClient.php - 客户端管理
Translator.php - 多语言
JWT.php - 令牌认证
QrUtil.php - 二维码生成
PanelImporter.php - 从 wg-easy/3x-ui 导入
InstallProtocolManager.php - 协议管理核心
OpenRouterService.php - AI 集成
templates/ - Twig 模板
migrations/ - SQL 迁移(按字母顺序执行)
```
## 技术栈
- PHP 8.2
- MySQL 8.0
- Twig 3
- Tailwind CSS
- Docker
## 许可证
MIT
## 支持项目
如果您觉得这个项目有用,可以通过 Tribute 捐款支持其开发:https://t.me/tribute/app?startapp=dzX1
+224
View File
@@ -0,0 +1,224 @@
# Release Notes - Amnezia VPN Web Panel v2.0.1
**Release Date:** 2026-04-25
**Previous Release:** v2.0.0 (3 weeks ago)
## 🆕 What's New in v2.0.1
This patch release focuses on improving international accessibility with comprehensive documentation in Russian and Chinese, making the panel more accessible to users worldwide.
### 📄 New Documentation
- **Russian Documentation** ([`README_RU.md`](README_RU.md)) - Complete translation with all features, API examples, and troubleshooting guides
- **Chinese Documentation** ([`README_ZH.md`](README_ZH.md)) - Full Chinese translation for better accessibility
## 🎉 Major Features (from v2.0.0)
### 🌍 Multi-Language Documentation
- Added comprehensive Russian documentation ([`README_RU.md`](README_RU.md))
- Added comprehensive Chinese documentation ([`README_ZH.md`](README_ZH.md))
- Improves accessibility for Russian and Chinese speaking users
### ☁️ Cloudflare WARP Integration
- New protocol: **Cloudflare WARP Proxy** (`cf-warp`)
- Transparent TCP traffic proxying through Cloudflare network
- Hides server's real IP address from VPN clients
- Automatic detection and routing for multiple VPN protocols:
- AWG / AWG2 (container IP + host redsocks)
- X-Ray VLESS (SOCKS5 `warp-out` outbound)
- AIVPN / WireGuard (host-level iptables + redsocks)
- Verification via `https://1.1.1.1/cdn-cgi/trace`
### 🤖 AI-Powered Features
- **AI Assistant** for protocol configuration using OpenRouter
- Auto-translation of interface via AI
- Intelligent troubleshooting suggestions
- Context-aware protocol configuration generation
### 📊 Enhanced Monitoring & Automation
- Automatic metrics collection with self-healing (3-minute monitoring)
- Automated client expiration checks (hourly)
- Automated traffic limit enforcement (hourly)
- Centralized log management with search and filtering
- Real-time server monitoring and health checks
### 🔧 Advanced Protocol Management
- **AmneziaWG 2.0** (`awg2`) protocol support
- **AIVPN** protocol integration
- **MTProxy** (Telegram) protocol support
- Dynamic protocol installation/uninstallation
- Per-protocol configuration management
- Protocol-specific port and transport settings
### 📥 Panel Import Feature
- Import from **wg-easy** backup files
- Import from **3x-ui** backup files
- Automatic client migration
- Import history tracking
### 🔐 Enhanced Security & Access Control
- JWT-based API authentication
- Persistent API tokens
- User roles and permissions
- LDAP integration for enterprise environments
- SSH key authentication for server deployment
### 📱 Client Management Enhancements
- Client expiration dates with automatic enforcement
- Traffic limits with automatic blocking
- QR code generation for mobile apps
- Client connection statistics and monitoring
- Current speed monitoring per client
### 🧪 Scenario Testing
- Define custom VPN connection scenarios
- Automated testing across different protocols
- Network condition simulation
- Reliability verification
## 🐛 Bug Fixes
- Fixed AWG2 empty peer parameters (migration 063)
- Fixed AIVPN prebuilt binary handling (migration 065)
- Fixed AWG2 original parameters completion (migration 064)
- Fixed XRay port mapping and IP enforcement
- Fixed traffic limit counter offsets for AIVPN
- Fixed client connection instructions translation
- Fixed WARP heredoc compatibility issues
- Fixed WARP subnet detection for AIVPN compatibility
## 🔧 Technical Improvements
### Database Migrations
- Added 22 new migration files (048-069) covering:
- Protocol management tables
- Monitoring and metrics
- LDAP configurations
- QR code templates
- Protocol editor translations
- AWG2 and AIVPN support
- Cloudflare WARP integration
### Performance Optimizations
- Optimized metrics collection queries
- Improved database indexing for client lookups
- Enhanced Docker container health checks
- Reduced API response times
### Code Quality
- Improved error handling and logging
- Better input validation
- Enhanced security checks
- Cleaner separation of concerns
## 📚 Documentation Updates
- Complete Russian translation of all features
- Complete Chinese translation of all features
- Updated API examples with new endpoints
- Added troubleshooting guides
- Enhanced installation instructions
- Added Cloudflare WARP configuration examples
## 🔗 API Changes
### New Endpoints
- `POST /api/clients/{id}/set-traffic-limit` - Set client traffic limit
- `GET /api/clients/{id}/traffic-limit-status` - Get traffic limit status
- `GET /api/clients/overlimit` - Get clients over traffic limit
- `POST /api/clients/{id}/set-expiration` - Set client expiration date
- `POST /api/clients/{id}/extend` - Extend client expiration
- `GET /api/clients/expiring` - Get clients expiring soon
- `POST /api/servers/{id}/import` - Import from existing panel
- `GET /api/servers/{id}/imports` - Get import history
- `POST /api/servers/{id}/backup` - Create server backup
- `GET /api/servers/{id}/backups` - List server backups
- `POST /api/servers/{id}/restore` - Restore from backup
- `GET /api/protocols/active` - List available protocols (JWT-friendly)
### Enhanced Endpoints
- Improved client creation with expiration and traffic limits
- Enhanced server management with import capabilities
- Better protocol management with AI assistance
## 🚀 Upgrade Guide
### From Previous Versions
1. **Backup your database** before upgrading
2. Pull the latest code:
```bash
git pull origin main
```
3. Run new migrations:
```bash
docker compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < migrations/048_enable_xray_stats.sql
# ... run all new migration files in order
```
4. Restart containers:
```bash
docker compose restart
```
5. Clear application cache if needed
### Fresh Installation
See the installation instructions in [`README.md`](README.md), [`README_RU.md`](README_RU.md), or [`README_ZH.md`](README_ZH.md)
## 📋 Migration Files (Since v2.0.0)
This release includes all migrations from v2.0.0 plus the following new files:
### New in v2.0.1:
- No new database migrations - this is a documentation-focused release
### From v2.0.0:
- `048_enable_xray_stats.sql` - Enable XRay statistics collection
- `049_add_dns_servers.sql` - Add DNS server configuration
- `050_fix_awg_random_params.sql` - Fix AWG random parameters
- `051_fix_awg_fresh_install.sql` - Fix AWG fresh installation
- `052_add_current_speed_to_clients.sql` - Add current speed monitoring
- `053_split_speed.sql` - Split upload/download speed
- `054_xray_single_ip_enforcement.sql` - XRay IP enforcement
- `055_dashboard_online_now_translation.sql` - Dashboard translation
- `056_enable_show_text_content_for_xray.sql` - XRay text content
- `057_add_protocol_management_translations.sql` - Protocol management
- `058_add_awg2_protocol.sql` - AWG2 protocol support
- `059_add_mtproxy_protocol.sql` - MTProxy protocol support
- `060_add_aivpn_protocol.sql` - AIVPN protocol support
- `061_fix_client_connection_instructions_translation.sql` - Translation fix
- `062_add_aivpn_counter_offsets.sql` - AIVPN counter fixes
- `063_fix_awg2_empty_peer_in_install_script.sql` - AWG2 peer fix
- `064_complete_awg2_original_params.sql` - AWG2 parameters
- `065_fix_aivpn_prebuilt_binary.sql` - AIVPN binary fix
- `066_add_cloudflare_warp_protocol.sql` - WARP protocol
- `067_warp_auto_redsocks_integration.sql` - WARP redsocks
- `068_fix_warp_heredoc_compat.sql` - WARP heredoc fix
- `069_warp_aivpn_subnet_detect.sql` - WARP subnet detection
## 🙏 Acknowledgments
Special thanks to:
- All contributors who helped with translations
- The Amnezia VPN community for feedback and testing
- OpenRouter for AI integration support
- Cloudflare for WARP technology
## 📞 Support
- **GitHub Issues:** https://github.com/infosave2007/amneziavpnphp/issues
- **Documentation:** See README files in English, Russian, and Chinese
- **Donations:** https://t.me/tribute/app?startapp=dzX1
## 📄 License
This release is licensed under the MIT License - see the [`LICENSE`](LICENSE) file for details.
---
**Full Changelog:** https://github.com/infosave2007/amneziavpnphp/compare/v2.0.0...v2.0.1
**Changes since v2.0.0:**
- Added comprehensive Russian documentation ([`README_RU.md`](README_RU.md))
- Added comprehensive Chinese documentation ([`README_ZH.md`](README_ZH.md))
- Updated release notes with multi-language support information
+656 -92
View File
@@ -289,24 +289,18 @@ class InstallProtocolManager
private static function detect(VpnServer $server, array $protocol, array $options = []): array
{
$engine = self::getEngine($protocol);
if ($engine === 'builtin_awg') {
return self::detectBuiltinAwg($server, $protocol);
$handler = self::resolveHandler($protocol);
switch ($handler) {
case 'awg':
return self::detectBuiltinAwg($server, $protocol);
case 'xray':
return self::detectBuiltinXray($server, $protocol);
case 'warp':
return self::detectBuiltinWarp($server, $protocol);
default:
return self::runScript($server, $protocol, 'detect', $options);
}
$slug = $protocol['slug'] ?? '';
// For AWG shell-based scenarios (amnezia-wg, amnezia-wg-advanced), use builtin AWG detection
if (self::isAwgProtocol($slug, $protocol)) {
return self::detectBuiltinAwg($server, $protocol);
}
// For X-Ray VLESS, use builtin detection
if ($slug === 'xray-vless') {
return self::detectBuiltinXray($server, $protocol);
}
return self::runScript($server, $protocol, 'detect', $options);
}
public static function install(VpnServer $server, array $protocol, array $options = []): array
@@ -397,30 +391,22 @@ class InstallProtocolManager
private static function restore(VpnServer $server, array $protocol, array $detection, array $options = []): array
{
$engine = self::getEngine($protocol);
if ($engine === 'builtin_awg') {
return self::restoreBuiltinAwg($server, $protocol, $detection, $options);
}
$handler = self::resolveHandler($protocol);
$slug = $protocol['slug'] ?? '';
// For AWG shell-based scenarios, use builtin AWG restore
if (self::isAwgProtocol($slug, $protocol)) {
return self::restoreBuiltinAwg($server, $protocol, $detection, $options);
switch ($handler) {
case 'awg':
return self::restoreBuiltinAwg($server, $protocol, $detection, $options);
case 'xray':
return self::restoreBuiltinXray($server, $protocol, $detection, $options);
default:
$result = self::runScript($server, $protocol, 'restore', array_merge($options, [
'detection' => $detection
]));
if (!isset($result['success'])) {
$result['success'] = true;
}
return $result;
}
// For X-Ray VLESS, use builtin restore
if ($slug === 'xray-vless') {
return self::restoreBuiltinXray($server, $protocol, $detection, $options);
}
$result = self::runScript($server, $protocol, 'restore', array_merge($options, [
'detection' => $detection
]));
if (!isset($result['success'])) {
$result['success'] = true;
}
return $result;
}
private static function detectBuiltinAwg(VpnServer $server, array $protocol): array
@@ -1245,29 +1231,77 @@ class InstallProtocolManager
return $row;
}
private static function getEngine(array $protocol): string
/**
* ──────────────────────────────────────────────────────────────────
* PROTOCOL HANDLER REGISTRY
* ──────────────────────────────────────────────────────────────────
* Central dispatcher that determines which builtin handler manages
* a given protocol. Every dispatch point (detect, install, uninstall)
* MUST use this method instead of ad-hoc slug/regex checks.
*
* Returns one of:
* 'awg' AmneziaWG / AWG variants (Docker container based)
* 'warp' Cloudflare WARP (systemd service, host-level)
* 'xray' X-Ray VLESS (Docker container based)
* 'script' Generic script-driven protocol (install/uninstall via shell)
*
* Priority order:
* 1. Explicit slug match (highest priority, cannot be overridden)
* 2. Engine field from protocol definition
* 3. Heuristic: install_script content analysis (lowest priority)
*/
private static function resolveHandler(array $protocol): string
{
$definition = $protocol['definition'] ?? [];
if (!empty($protocol['install_script'])) {
return 'shell';
$slug = $protocol['slug'] ?? '';
// ── 1. Explicit slug → handler mapping (always wins) ──
static $slugMap = [
// WARP
'cf-warp' => 'warp',
'cloudflare-warp' => 'warp',
// X-Ray
'xray-vless' => 'xray',
// AWG variants
'amnezia-wg' => 'awg',
'amnezia-wg-advanced' => 'awg',
'awg2' => 'awg',
];
if (isset($slugMap[$slug])) {
return $slugMap[$slug];
}
return $definition['engine'] ?? 'builtin_awg';
// ── 2. Engine from definition ──
$definition = $protocol['definition'] ?? [];
$engine = $definition['engine'] ?? '';
if ($engine === 'builtin_awg') {
return 'awg';
}
// ── 3. Heuristic: AWG Docker image in install_script ──
// Only check if no explicit slug/engine match above
if (empty($protocol['install_script'])) {
// No install_script and no engine → default to AWG (legacy behavior)
return 'awg';
}
$installScript = (string) $protocol['install_script'];
if (preg_match('/amneziavpn\/amnezia-wg|docker\s.*amnezia-awg/i', $installScript)) {
return 'awg';
}
// ── 4. Fallback: generic script protocol ──
return 'script';
}
/**
* Check if a protocol is an AWG variant (by slug or install_script content)
* Used to route shell-based AWG scenarios to builtin AWG detection/restore
* Legacy compatibility: get engine string
*/
private static function isAwgProtocol(string $slug, array $protocol): bool
private static function getEngine(array $protocol): string
{
if (in_array($slug, ['amnezia-wg', 'amnezia-wg-advanced', 'awg2'], true)) {
return true;
}
$installScript = (string) ($protocol['install_script'] ?? '');
if ($installScript !== '' && preg_match('/amneziavpn\/amnezia-wg|amnezia\/awg|amnezia-awg/i', $installScript)) {
return true;
}
return false;
$handler = self::resolveHandler($protocol);
if ($handler === 'awg') return 'builtin_awg';
return 'shell';
}
private static function fallbackProtocols(): array
@@ -1338,24 +1372,18 @@ class InstallProtocolManager
*/
public static function runDetection(VpnServer $server, array $protocol, array $options = []): array
{
$engine = self::getEngine($protocol);
if ($engine === 'builtin_awg') {
return self::detectBuiltinAwg($server, $protocol);
$handler = self::resolveHandler($protocol);
switch ($handler) {
case 'awg':
return self::detectBuiltinAwg($server, $protocol);
case 'xray':
return self::detectBuiltinXray($server, $protocol);
case 'warp':
return self::detectBuiltinWarp($server, $protocol);
default:
return self::runScript($server, $protocol, 'detect', $options);
}
$slug = $protocol['slug'] ?? '';
// For AWG shell-based scenarios (amnezia-wg, amnezia-wg-advanced), use builtin AWG detection
if (self::isAwgProtocol($slug, $protocol)) {
return self::detectBuiltinAwg($server, $protocol);
}
// For X-Ray VLESS, use builtin detection
if ($slug === 'xray-vless') {
return self::detectBuiltinXray($server, $protocol);
}
return self::runScript($server, $protocol, 'detect', $options);
}
/**
@@ -1364,27 +1392,29 @@ class InstallProtocolManager
*/
public static function uninstall(VpnServer $server, array $protocol, array $options = []): array
{
$engine = self::getEngine($protocol);
if ($engine === 'builtin_awg') {
return self::uninstallBuiltinAwg($server, $protocol, $options);
}
$slug = $protocol['slug'] ?? 'unknown';
$handler = self::resolveHandler($protocol);
Logger::appendInstall($server->getId(), 'UNINSTALL: slug=' . $slug . ' handler=' . $handler);
// For script-driven protocols, try to detect AWG scenario and fallback to builtin uninstall
$slug = $protocol['slug'] ?? '';
if (self::isAwgProtocol($slug, $protocol)) {
// Prefer builtin AWG uninstall by default because script variants may have CRLF issues
// or leave behind the canonical container name, causing install conflicts.
if (!empty($options['use_script_uninstall'])) {
$hasScript = isset($protocol['uninstall_script']) && trim((string) $protocol['uninstall_script']) !== '';
if ($hasScript) {
return self::runScript($server, $protocol, 'uninstall', $options);
switch ($handler) {
case 'warp':
return self::uninstallBuiltinWarp($server, $protocol, $options);
case 'awg':
// Prefer builtin AWG uninstall; script variant only on explicit request
if (!empty($options['use_script_uninstall'])) {
$hasScript = isset($protocol['uninstall_script']) && trim((string) $protocol['uninstall_script']) !== '';
if ($hasScript) {
return self::runScript($server, $protocol, 'uninstall', $options);
}
}
}
return self::uninstallBuiltinAwg($server, $protocol, $options);
}
return self::uninstallBuiltinAwg($server, $protocol, $options);
// For other script-driven protocols, look for an "uninstall" phase in scripts
return self::runScript($server, $protocol, 'uninstall', $options);
case 'xray':
case 'script':
default:
return self::runScript($server, $protocol, 'uninstall', $options);
}
}
private static function uninstallBuiltinAwg(VpnServer $server, array $protocol, array $options = []): array
@@ -1450,8 +1480,9 @@ class InstallProtocolManager
// ── Check for existing installation before doing anything destructive ──
$slug = $protocol['slug'] ?? '';
$isAwg = $engine === 'builtin_awg' || self::isAwgProtocol($slug, $protocol);
$isXray = $slug === 'xray-vless';
$handler = self::resolveHandler($protocol);
$isAwg = $handler === 'awg';
$isXray = $handler === 'xray';
if ($isAwg) {
$detection = self::detectBuiltinAwg($server, $protocol);
@@ -1491,6 +1522,17 @@ class InstallProtocolManager
}
}
// For Cloudflare WARP — always run install script even if WARP binary exists
// because the script is idempotent and handles redsocks/iptables setup
if (self::resolveHandler($protocol) === 'warp') {
$warpDetection = self::detectBuiltinWarp($server, $protocol);
Logger::appendInstall($serverId, 'WARP detect result: status=' . ($warpDetection['status'] ?? 'null'));
if (($warpDetection['status'] ?? '') === 'existing') {
Logger::appendInstall($serverId, 'Existing WARP found, running install script anyway for redsocks/iptables setup');
// Don't return — fall through to run the install script
}
}
// ── No existing installation found — proceed with fresh install ──
if ($engine === 'builtin_awg') {
@@ -1671,6 +1713,12 @@ class InstallProtocolManager
self::markServerActive($serverId, null, ['vpn_port' => $port]);
}
}
// ── WARP: Auto-patch X-Ray outbound to route through WARP ──
if (self::resolveHandler($protocol) === 'warp') {
self::patchXrayForWarp($server);
}
return $res;
} catch (Throwable $e) {
$message = (string) $e->getMessage();
@@ -2607,4 +2655,520 @@ class InstallProtocolManager
Logger::appendInstall($serverId, "AWG client import complete: imported {$imported} clients");
}
// ─────────────────────────────────────────────────────────────────
// Cloudflare WARP — builtin detection, uninstall, status
// WARP runs as a systemd service (warp-svc), NOT as a Docker container
// ─────────────────────────────────────────────────────────────────
/**
* Detect existing Cloudflare WARP installation on the server
*/
private static function detectBuiltinWarp(VpnServer $server, array $protocol): array
{
$metadata = $protocol['definition']['metadata'] ?? [];
$proxyPort = $metadata['proxy_port'] ?? 40000;
// Check if warp-cli binary exists
$warpCliCheck = trim($server->executeCommand('command -v warp-cli 2>/dev/null || echo ""', true));
if ($warpCliCheck === '') {
return [
'status' => 'absent',
'message' => 'Cloudflare WARP не установлен на сервере'
];
}
// Check warp-svc service status
$svcStatus = trim($server->executeCommand('systemctl is-active warp-svc 2>/dev/null || echo "inactive"', true));
// Get WARP connection status
$warpStatus = trim($server->executeCommand('warp-cli --accept-tos status 2>/dev/null || echo "error"', true));
$isConnected = (bool) preg_match('/Connected/i', $warpStatus);
$isRegistered = !preg_match('/Registration Missing|unregistered/i', $warpStatus);
if (!$isRegistered) {
return [
'status' => 'partial',
'message' => 'WARP установлен, но не зарегистрирован',
'details' => [
'warp_cli' => $warpCliCheck,
'service_status' => $svcStatus,
'warp_status' => $warpStatus,
]
];
}
// Get WARP mode
$warpMode = '';
if (preg_match('/Mode:\s*(\S+)/i', $warpStatus, $m)) {
$warpMode = $m[1];
}
// Get WARP account info
$accountInfo = trim($server->executeCommand('warp-cli --accept-tos registration show 2>/dev/null || echo ""', true));
$accountId = '';
if (preg_match('/Account\s*ID[:\s]+([a-zA-Z0-9-]+)/i', $accountInfo, $m)) {
$accountId = $m[1];
}
// Check if proxy port is listening
$portListening = trim($server->executeCommand(
'ss -tlnp 2>/dev/null | grep ":' . (int) $proxyPort . '" | head -1 || echo ""', true
));
// Get WARP IP (best-effort)
$warpIp = '';
if ($isConnected && $portListening !== '') {
$traceOut = trim($server->executeCommand(
'curl -x socks5h://127.0.0.1:' . (int) $proxyPort . ' -s --max-time 5 https://cloudflare.com/cdn-cgi/trace 2>/dev/null || echo ""', true
));
if (preg_match('/ip=([^\s]+)/', $traceOut, $m)) {
$warpIp = $m[1];
}
}
return [
'status' => 'existing',
'message' => 'Cloudflare WARP установлен и ' . ($isConnected ? 'подключён' : 'отключён'),
'details' => [
'warp_cli' => $warpCliCheck,
'service_status' => $svcStatus,
'warp_status_raw' => $warpStatus,
'connected' => $isConnected,
'registered' => $isRegistered,
'warp_mode' => $warpMode,
'warp_proxy_port' => (int) $proxyPort,
'warp_ip' => $warpIp,
'warp_account' => $accountId,
'port_listening' => $portListening !== '',
'summary' => sprintf(
'WARP %s, mode=%s, proxy=%s:%d%s',
$isConnected ? 'connected' : 'disconnected',
$warpMode ?: 'unknown',
'127.0.0.1',
(int) $proxyPort,
$warpIp !== '' ? ', exit_ip=' . $warpIp : ''
)
]
];
}
/**
* Uninstall Cloudflare WARP from the server (systemd service, not Docker)
*/
private static function uninstallBuiltinWarp(VpnServer $server, array $protocol, array $options = []): array
{
$serverId = $server->getId();
Logger::appendInstall($serverId, 'Uninstalling Cloudflare WARP (full cleanup)...');
try {
// Run entire uninstall as a single remote script to avoid SSH escaping issues
$script = <<<'BASH'
#!/bin/bash
echo "WARP_UNINSTALL_START"
# 1. Restore X-Ray config
XRAY_NAME=$(docker ps 2>/dev/null | grep -i xray | awk '{ print $NF }' | head -1)
if [ -n "$XRAY_NAME" ]; then
# Try server.json first (actual runtime config), then config.json
XRAY_CFG_PATH=""
for P in /opt/amnezia/xray/server.json /etc/xray/config.json; do
CONTENT=$(docker exec "$XRAY_NAME" cat "$P" 2>/dev/null || echo "")
if [ -n "$CONTENT" ] && echo "$CONTENT" | grep -q "warp-out"; then
XRAY_CFG_PATH="$P"
XRAY_CFG="$CONTENT"
break
fi
done
if [ -n "$XRAY_CFG_PATH" ]; then
echo "$XRAY_CFG" | python3 -c "
import sys, json
try:
cfg = json.load(sys.stdin)
cfg['outbounds'] = [o for o in cfg.get('outbounds',[]) if o.get('tag') != 'warp-out']
if 'routing' in cfg:
cfg['routing']['rules'] = [r for r in cfg['routing'].get('rules',[]) if r.get('outboundTag') != 'warp-out']
if not cfg['routing']['rules']: del cfg['routing']
print(json.dumps(cfg, indent=2))
except: pass
" 2>/dev/null | docker exec -i "$XRAY_NAME" tee "$XRAY_CFG_PATH" > /dev/null 2>&1
docker restart "$XRAY_NAME" 2>/dev/null || true
echo "xray_restored"
fi
fi
# 2. Remove DNAT rules
DOCKER_GW=$(docker network inspect bridge 2>/dev/null | grep Gateway | head -1 | awk -F'"' '{print $4}')
if [ -z "$DOCKER_GW" ]; then DOCKER_GW="172.17.0.1"; fi
iptables -t nat -D OUTPUT -d "$DOCKER_GW" -p tcp --dport 40000 -j DNAT --to-destination 127.0.0.1:40000 2>/dev/null || true
iptables -t nat -D PREROUTING -d "$DOCKER_GW" -p tcp --dport 40000 -j DNAT --to-destination 127.0.0.1:40000 2>/dev/null || true
iptables -t nat -D PREROUTING -d "$DOCKER_GW" -p tcp --dport 40000 -j DNAT --to-destination 127.0.0.1:40000 2>/dev/null || true
echo "dnat_removed"
# 3. Remove REDSOCKS_WARP chain
SUBNETS=$(cat /var/lib/cloudflare-warp/routed_subnets 2>/dev/null || echo "10.8.1.0/24 10.0.0.0/24")
for S in $SUBNETS; do
iptables -t nat -D PREROUTING -s "$S" -p tcp -j REDSOCKS_WARP 2>/dev/null || true
done
iptables -t nat -F REDSOCKS_WARP 2>/dev/null || true
iptables -t nat -X REDSOCKS_WARP 2>/dev/null || true
echo "iptables_cleaned"
# 4. Remove redsocks
systemctl stop redsocks-warp 2>/dev/null || true
systemctl disable redsocks-warp 2>/dev/null || true
rm -f /etc/systemd/system/redsocks-warp.service
rm -rf /etc/redsocks
systemctl daemon-reload 2>/dev/null || true
echo "redsocks_removed"
# 5. Disconnect and remove WARP
warp-cli --accept-tos disconnect 2>/dev/null || true
warp-cli --accept-tos registration delete 2>/dev/null || true
systemctl stop warp-svc 2>/dev/null || true
systemctl disable warp-svc 2>/dev/null || true
DEBIAN_FRONTEND=noninteractive apt-get remove -y cloudflare-warp >/dev/null 2>&1 || true
apt-get autoremove -y >/dev/null 2>&1 || true
echo "warp_removed"
# 6. Cleanup
rm -rf /var/lib/cloudflare-warp 2>/dev/null || true
rm -f /etc/apt/sources.list.d/cloudflare-client.list 2>/dev/null || true
rm -f /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg 2>/dev/null || true
rm -f /etc/sysctl.d/99-warp.conf 2>/dev/null || true
sysctl -w net.ipv4.conf.docker0.route_localnet=0 2>/dev/null || true
sysctl -w net.ipv4.conf.all.route_localnet=0 2>/dev/null || true
# 7. Save iptables
mkdir -p /etc/iptables
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
echo "WARP_UNINSTALL_DONE"
BASH;
Logger::appendInstall($serverId, 'WARP uninstall: writing script to server...');
$b64 = base64_encode($script);
// Phase 1: write script file
$server->executeCommand("echo " . $b64 . " | base64 -d > /tmp/_warp_uninstall.sh && chmod +x /tmp/_warp_uninstall.sh", true);
Logger::appendInstall($serverId, 'WARP uninstall: executing script...');
// Phase 2: execute script
$output = $server->executeCommand("bash /tmp/_warp_uninstall.sh 2>&1; rm -f /tmp/_warp_uninstall.sh", true);
$outputStr = (string) $output;
Logger::appendInstall($serverId, 'WARP uninstall output: ' . substr(str_replace(["\r", "\n"], ' ', $outputStr), 0, 500));
$success = strpos($outputStr, 'WARP_UNINSTALL_DONE') !== false;
if ($success) {
Logger::appendInstall($serverId, 'WARP uninstalled successfully (full cleanup)');
} else {
Logger::appendInstall($serverId, 'WARP uninstall script may have partially failed');
}
return [
'success' => $success,
'message' => $success ? 'Cloudflare WARP удалён' : 'WARP удалён частично, проверьте логи',
'mode' => 'uninstall'
];
} catch (Throwable $e) {
Logger::appendInstall($serverId, 'WARP uninstall exception: ' . $e->getMessage());
throw new Exception('WARP uninstall failed: ' . $e->getMessage());
}
}
/**
* Remove WARP outbound and routing rules from X-Ray config
* Restores X-Ray to direct (freedom) outbound mode
*/
private static function unpatchXrayFromWarp(VpnServer $server): void
{
$serverId = $server->getId();
try {
$xrayContainer = trim($server->executeCommand(
'docker ps 2>/dev/null | grep -i xray | awk \'{ print $NF }\' | head -1 || echo ""', true
));
if ($xrayContainer === '') {
Logger::appendInstall($serverId, 'WARP uninstall: no X-Ray container, skipping config restore');
return;
}
$containerArg = escapeshellarg($xrayContainer);
$configRaw = trim($server->executeCommand(
"docker exec -i {$containerArg} cat /etc/xray/config.json 2>/dev/null", true
));
if ($configRaw === '') {
return;
}
$config = json_decode($configRaw, true);
if (!is_array($config)) {
return;
}
// Remove warp-out outbound
$outbounds = $config['outbounds'] ?? [];
$hadWarp = false;
$newOutbounds = [];
foreach ($outbounds as $ob) {
if (($ob['tag'] ?? '') === 'warp-out') {
$hadWarp = true;
continue; // skip warp-out
}
$newOutbounds[] = $ob;
}
if (!$hadWarp) {
Logger::appendInstall($serverId, 'WARP uninstall: X-Ray has no warp-out outbound, nothing to restore');
return;
}
$config['outbounds'] = $newOutbounds;
// Remove warp routing rules
if (isset($config['routing']['rules']) && is_array($config['routing']['rules'])) {
$newRules = [];
foreach ($config['routing']['rules'] as $rule) {
if (($rule['outboundTag'] ?? '') === 'warp-out') {
continue; // skip warp routing rule
}
$newRules[] = $rule;
}
$config['routing']['rules'] = $newRules;
// If routing is empty, remove it entirely for clean config
if (empty($config['routing']['rules'])) {
unset($config['routing']);
}
}
// Write back config
$newConfig = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$b64Config = base64_encode($newConfig);
$server->executeCommand(
"echo {$b64Config} | base64 -d | docker exec -i {$containerArg} tee /etc/xray/config.json > /dev/null", true
);
// Restart X-Ray
$server->executeCommand("docker restart {$containerArg} 2>/dev/null || true", true);
Logger::appendInstall($serverId, 'WARP uninstall: X-Ray config restored (warp-out removed), container restarted');
} catch (\Throwable $e) {
Logger::appendInstall($serverId, 'WARP uninstall: X-Ray restore failed (non-fatal): ' . $e->getMessage());
}
}
/**
* Get WARP runtime status from a server (used by API endpoint)
* Returns connection status, proxy port, exit IP, and account info
*/
public static function getWarpStatus(VpnServer $server): array
{
$warpCliCheck = trim($server->executeCommand('command -v warp-cli 2>/dev/null || echo ""', true));
if ($warpCliCheck === '') {
return [
'installed' => false,
'connected' => false,
'message' => 'WARP не установлен'
];
}
$svcStatus = trim($server->executeCommand('systemctl is-active warp-svc 2>/dev/null || echo "inactive"', true));
$warpStatus = trim($server->executeCommand('warp-cli --accept-tos status 2>/dev/null || echo "error"', true));
$isConnected = (bool) preg_match('/Connected/i', $warpStatus);
$warpMode = '';
if (preg_match('/Mode:\s*(\S+)/i', $warpStatus, $m)) {
$warpMode = $m[1];
}
// Get proxy port from settings
$proxyPortRaw = trim($server->executeCommand('warp-cli --accept-tos settings 2>/dev/null | grep -i "proxy port" || echo ""', true));
$proxyPort = 40000;
if (preg_match('/(\d+)/', $proxyPortRaw, $m)) {
$proxyPort = (int) $m[1];
}
$warpIp = '';
$portListening = false;
if ($isConnected) {
$portCheck = trim($server->executeCommand(
'ss -tlnp 2>/dev/null | grep ":' . $proxyPort . '" | head -1 || echo ""', true
));
$portListening = $portCheck !== '';
if ($portListening) {
$traceOut = trim($server->executeCommand(
'curl -x socks5h://127.0.0.1:' . $proxyPort . ' -s --max-time 5 https://cloudflare.com/cdn-cgi/trace 2>/dev/null || echo ""', true
));
if (preg_match('/ip=([^\s]+)/', $traceOut, $m)) {
$warpIp = $m[1];
}
}
}
return [
'installed' => true,
'connected' => $isConnected,
'service_status' => $svcStatus,
'mode' => $warpMode,
'proxy_port' => $proxyPort,
'proxy_listening' => $portListening,
'warp_ip' => $warpIp,
'warp_status_raw' => $warpStatus,
];
}
/**
* Auto-patch X-Ray config to route outbound traffic through WARP SOCKS5 proxy
* X-Ray runs in Docker bridge mode, so we need:
* 1. iptables DNAT: docker_gateway:40000 → 127.0.0.1:40000
* 2. X-Ray outbound: socks5 → docker_gateway:40000
*/
private static function patchXrayForWarp(VpnServer $server): void
{
$serverId = $server->getId();
try {
// Find X-Ray container
$xrayContainer = trim($server->executeCommand(
'docker ps 2>/dev/null | grep -i xray | awk \'{ print $NF }\' | head -1 || echo ""', true
));
if ($xrayContainer === '') {
Logger::appendInstall($serverId, 'WARP X-Ray patch: no X-Ray container found, skipping');
return;
}
Logger::appendInstall($serverId, 'WARP X-Ray patch: found container ' . $xrayContainer);
// Get Docker bridge gateway IP
$dockerGw = trim($server->executeCommand(
'docker network inspect bridge 2>/dev/null | grep Gateway | head -1 | sed \'s/.*"Gateway"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/\' || echo "172.17.0.1"', true
));
if ($dockerGw === '') {
$dockerGw = '172.17.0.1';
}
// Setup iptables DNAT so Docker containers can reach WARP via gateway IP
$server->executeCommand(
'iptables -t nat -D OUTPUT -d ' . escapeshellarg($dockerGw) . ' -p tcp --dport 40000 -j DNAT --to-destination 127.0.0.1:40000 2>/dev/null || true', true
);
$server->executeCommand(
'iptables -t nat -A OUTPUT -d ' . escapeshellarg($dockerGw) . ' -p tcp --dport 40000 -j DNAT --to-destination 127.0.0.1:40000 2>/dev/null || true', true
);
// Also allow in PREROUTING for container-originated traffic
$server->executeCommand(
'iptables -t nat -D PREROUTING -d ' . escapeshellarg($dockerGw) . ' -p tcp --dport 40000 -j DNAT --to-destination 127.0.0.1:40000 2>/dev/null || true', true
);
$server->executeCommand(
'iptables -t nat -A PREROUTING -d ' . escapeshellarg($dockerGw) . ' -p tcp --dport 40000 -j DNAT --to-destination 127.0.0.1:40000 2>/dev/null || true', true
);
Logger::appendInstall($serverId, 'WARP X-Ray patch: iptables DNAT ' . $dockerGw . ':40000 → 127.0.0.1:40000');
// Enable route_localnet so DNAT to 127.0.0.1 works for Docker container traffic
$server->executeCommand('sysctl -w net.ipv4.conf.docker0.route_localnet=1 2>/dev/null || true', true);
$server->executeCommand('sysctl -w net.ipv4.conf.all.route_localnet=1 2>/dev/null || true', true);
$server->executeCommand('grep -q route_localnet /etc/sysctl.d/99-warp.conf 2>/dev/null || { mkdir -p /etc/sysctl.d; echo "net.ipv4.conf.docker0.route_localnet=1" >> /etc/sysctl.d/99-warp.conf; echo "net.ipv4.conf.all.route_localnet=1" >> /etc/sysctl.d/99-warp.conf; }', true);
// Read X-Ray config — try /opt/amnezia/xray/server.json first (actual runtime config),
// fall back to /etc/xray/config.json (Docker volume mount)
$containerArg = escapeshellarg($xrayContainer);
$xrayConfigPath = '/opt/amnezia/xray/server.json';
$configRaw = trim($server->executeCommand(
"docker exec -i {$containerArg} cat {$xrayConfigPath} 2>/dev/null", true
));
if ($configRaw === '' || $configRaw === 'cat: can\'t open') {
$xrayConfigPath = '/etc/xray/config.json';
$configRaw = trim($server->executeCommand(
"docker exec -i {$containerArg} cat {$xrayConfigPath} 2>/dev/null", true
));
}
if ($configRaw === '') {
Logger::appendInstall($serverId, 'WARP X-Ray patch: could not read X-Ray config');
return;
}
Logger::appendInstall($serverId, 'WARP X-Ray patch: using config ' . $xrayConfigPath);
$config = json_decode($configRaw, true);
if (!is_array($config)) {
Logger::appendInstall($serverId, 'WARP X-Ray patch: config.json is not valid JSON');
return;
}
// Check if warp-out already exists
$outbounds = $config['outbounds'] ?? [];
foreach ($outbounds as $ob) {
if (($ob['tag'] ?? '') === 'warp-out') {
Logger::appendInstall($serverId, 'WARP X-Ray patch: warp-out outbound already exists');
return;
}
}
// Tag existing freedom outbound as "direct" if not tagged
foreach ($outbounds as &$ob) {
if (($ob['protocol'] ?? '') === 'freedom' && empty($ob['tag'])) {
$ob['tag'] = 'direct';
}
}
unset($ob);
// Add warp-out SOCKS5 outbound
$outbounds[] = [
'tag' => 'warp-out',
'protocol' => 'socks',
'settings' => [
'servers' => [
[
'address' => $dockerGw,
'port' => 40000
]
]
]
];
$config['outbounds'] = $outbounds;
// Set default routing: all traffic through warp-out
if (!isset($config['routing'])) {
$config['routing'] = [];
}
if (!isset($config['routing']['rules'])) {
$config['routing']['rules'] = [];
}
// Add rule: route everything through warp-out (as first rule)
$hasWarpRule = false;
foreach ($config['routing']['rules'] as $rule) {
if (($rule['outboundTag'] ?? '') === 'warp-out') {
$hasWarpRule = true;
break;
}
}
if (!$hasWarpRule) {
// Add catch-all rule at end to route through WARP
$config['routing']['rules'][] = [
'type' => 'field',
'outboundTag' => 'warp-out',
'network' => 'tcp,udp'
];
}
// Write back config
$newConfig = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$b64Config = base64_encode($newConfig);
$server->executeCommand(
"echo {$b64Config} | base64 -d | docker exec -i {$containerArg} tee {$xrayConfigPath} > /dev/null", true
);
// Restart X-Ray container
$server->executeCommand("docker restart {$containerArg} 2>/dev/null || true", true);
Logger::appendInstall($serverId, 'WARP X-Ray patch: outbound added to ' . $xrayConfigPath . ', container restarted');
} catch (\Throwable $e) {
Logger::appendInstall($serverId, 'WARP X-Ray patch failed (non-fatal): ' . $e->getMessage());
// Non-fatal — WARP still works for AWG clients
}
}
}
@@ -0,0 +1,296 @@
-- =====================================================================
-- Migration 066: Add Cloudflare WARP proxy protocol
-- Installs Cloudflare WARP on VPS and enables SOCKS5/HTTPS proxy mode
-- Creates chain: AmneziaWG → WARP (127.0.0.1:40000) → Internet
-- Adds DPI/censorship bypass layer via Cloudflare tunnel
-- =====================================================================
-- 1. Insert the Cloudflare WARP protocol
INSERT INTO protocols (name, slug, description, install_script, uninstall_script, output_template, show_text_content, ubuntu_compatible, is_active, definition, created_at, updated_at)
SELECT
'Cloudflare WARP Proxy',
'cf-warp',
'Cloudflare WARP — прокси-слой для обхода DPI/цензуры. Устанавливает WARP на сервер в режиме SOCKS5 прокси (127.0.0.1:40000). Трафик идёт по цепочке: VPN-клиент → AmneziaWG → WARP → Cloudflare → Интернет. Скрывает конечные домены от провайдера VPS.',
'#!/bin/bash
set -eo pipefail
# ======================================================================
# Cloudflare WARP Proxy Installer
# Installs WARP in proxy mode (SOCKS5 on 127.0.0.1:40000)
# For chain: AmneziaWG WARP Internet
# ======================================================================
WARP_PROXY_PORT="${WARP_PROXY_PORT:-40000}"
WARP_MODE="${WARP_MODE:-proxy}"
export DEBIAN_FRONTEND=noninteractive
echo "=== Installing Cloudflare WARP ==="
# Detect OS
if [ -f /etc/os-release ]; then
. /etc/os-release
OS_ID="$ID"
OS_VERSION="$VERSION_ID"
else
OS_ID="unknown"
OS_VERSION="0"
fi
echo "Detected OS: $OS_ID $OS_VERSION"
# Check architecture
ARCH=$(uname -m)
if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then
echo "ERROR: WARP supports only x86_64 and aarch64, got: $ARCH"
exit 1
fi
# Install prerequisites
apt-get update -qq
apt-get install -y -qq curl gnupg lsb-release >/dev/null 2>&1
# Add Cloudflare WARP repository
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
# Determine correct repo codename
REPO_CODENAME=""
case "$OS_ID" in
ubuntu)
case "$OS_VERSION" in
24.04) REPO_CODENAME="noble" ;;
22.04) REPO_CODENAME="jammy" ;;
20.04) REPO_CODENAME="focal" ;;
*) REPO_CODENAME="jammy" ;;
esac
;;
debian)
case "$OS_VERSION" in
12*) REPO_CODENAME="bookworm" ;;
11*) REPO_CODENAME="bullseye" ;;
*) REPO_CODENAME="bookworm" ;;
esac
;;
*)
REPO_CODENAME="jammy"
echo "WARNING: Unsupported OS $OS_ID, trying Ubuntu Jammy repo"
;;
esac
echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $REPO_CODENAME main" > /etc/apt/sources.list.d/cloudflare-client.list
# Install WARP client
apt-get update -qq
apt-get install -y -qq cloudflare-warp >/dev/null 2>&1
echo "WARP package installed"
# Check if already registered
WARP_STATUS=$(warp-cli --accept-tos status 2>/dev/null || echo "unregistered")
if echo "$WARP_STATUS" | grep -qiE "Registration Missing|unregistered"; then
echo "Registering WARP..."
warp-cli --accept-tos registration new
echo "WARP registered"
else
echo "WARP already registered"
fi
# Set proxy mode
echo "Setting WARP to proxy mode on port $WARP_PROXY_PORT..."
warp-cli --accept-tos mode proxy
warp-cli --accept-tos proxy port "$WARP_PROXY_PORT"
# Connect WARP
echo "Connecting WARP..."
warp-cli --accept-tos connect
# Wait for connection
for i in $(seq 1 15); do
CONN_STATUS=$(warp-cli --accept-tos status 2>/dev/null || echo "")
if echo "$CONN_STATUS" | grep -qi "Connected"; then
echo "WARP connected successfully"
break
fi
if [ "$i" -eq 15 ]; then
echo "WARNING: WARP connection timeout, may still be connecting..."
fi
sleep 2
done
# Verify proxy is listening
sleep 2
if command -v ss >/dev/null 2>&1; then
LISTENING=$(ss -tlnp 2>/dev/null | grep ":${WARP_PROXY_PORT}" || true)
elif command -v netstat >/dev/null 2>&1; then
LISTENING=$(netstat -tlnp 2>/dev/null | grep ":${WARP_PROXY_PORT}" || true)
else
LISTENING=""
fi
if [ -n "$LISTENING" ]; then
echo "WARP SOCKS5 proxy listening on 127.0.0.1:${WARP_PROXY_PORT}"
else
echo "WARNING: Proxy port ${WARP_PROXY_PORT} not yet listening, WARP may need more time"
fi
# Test proxy connectivity
PROXY_TEST=$(curl -x socks5h://127.0.0.1:${WARP_PROXY_PORT} -s -o /dev/null -w "%{http_code}" --max-time 10 https://cloudflare.com/cdn-cgi/trace 2>/dev/null || echo "000")
if [ "$PROXY_TEST" = "200" ]; then
echo "WARP proxy test: OK (HTTP 200)"
else
echo "WARNING: WARP proxy test returned HTTP $PROXY_TEST (may need a moment to initialize)"
fi
# Get WARP IP info
WARP_IP=$(curl -x socks5h://127.0.0.1:${WARP_PROXY_PORT} -s --max-time 10 https://cloudflare.com/cdn-cgi/trace 2>/dev/null | grep "ip=" | cut -d= -f2 || echo "unknown")
WARP_ACCOUNT=$(warp-cli --accept-tos registration show 2>/dev/null | grep -i "Account ID" | awk "{print \$NF}" || echo "unknown")
# Enable WARP service to start on boot
systemctl enable warp-svc 2>/dev/null || true
# Get server external IP
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
echo ""
echo "=== Cloudflare WARP Proxy Installed ==="
echo "Variable: warp_proxy_port=$WARP_PROXY_PORT"
echo "Variable: warp_mode=$WARP_MODE"
echo "Variable: warp_ip=$WARP_IP"
echo "Variable: warp_account=$WARP_ACCOUNT"
echo "Variable: server_host=$EXTERNAL_IP"
echo "Variable: proxy_address=127.0.0.1:${WARP_PROXY_PORT}"',
'#!/bin/bash
set -eo pipefail
echo "=== Uninstalling Cloudflare WARP ==="
# Disconnect and deregister
warp-cli --accept-tos disconnect 2>/dev/null || true
warp-cli --accept-tos registration delete 2>/dev/null || true
# Stop service
systemctl stop warp-svc 2>/dev/null || true
systemctl disable warp-svc 2>/dev/null || true
# Remove package
apt-get remove -y cloudflare-warp 2>/dev/null || true
apt-get autoremove -y 2>/dev/null || true
# Clean up config
rm -rf /var/lib/cloudflare-warp 2>/dev/null || true
rm -f /etc/apt/sources.list.d/cloudflare-client.list 2>/dev/null || true
rm -f /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg 2>/dev/null || true
echo "{\"success\":true,\"message\":\"Cloudflare WARP uninstalled\"}"',
'WARP SOCKS5 Proxy: socks5h://127.0.0.1:{{warp_proxy_port}}
WARP IP: {{warp_ip}}
Mode: {{warp_mode}}
Server: {{server_host}}',
1,
1,
1,
JSON_OBJECT(
'engine', 'shell',
'metadata', JSON_OBJECT(
'container_name', '',
'port_range', JSON_ARRAY(40000, 40000),
'config_dir', '/var/lib/cloudflare-warp',
'is_proxy_layer', true,
'proxy_port', 40000,
'proxy_protocol', 'socks5'
)
),
NOW(),
NOW()
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug = 'cf-warp');
-- 2. Add protocol variables for WARP
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'warp_proxy_port', 'number', '40000', 'WARP SOCKS5 proxy port (default 40000)', true
FROM protocols p WHERE p.slug = 'cf-warp'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'warp_proxy_port');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'warp_mode', 'string', 'proxy', 'WARP mode (proxy / warp)', false
FROM protocols p WHERE p.slug = 'cf-warp'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'warp_mode');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'warp_ip', 'string', '', 'WARP exit IP address (via Cloudflare)', false
FROM protocols p WHERE p.slug = 'cf-warp'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'warp_ip');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'warp_account', 'string', '', 'WARP account ID', false
FROM protocols p WHERE p.slug = 'cf-warp'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'warp_account');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true
FROM protocols p WHERE p.slug = 'cf-warp'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'server_host');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'proxy_address', 'string', '127.0.0.1:40000', 'Full proxy address', false
FROM protocols p WHERE p.slug = 'cf-warp'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'proxy_address');
-- 3. Add default template for WARP
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
SELECT p.id, 'Default WARP', 'WARP SOCKS5 Proxy: socks5h://127.0.0.1:{{warp_proxy_port}}
WARP IP: {{warp_ip}}
Mode: {{warp_mode}}
Server: {{server_host}}', true
FROM protocols p WHERE p.slug = 'cf-warp'
AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id = p.id AND template_name = 'Default WARP');
-- 4. Add translations for Cloudflare WARP
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'protocol_cf_warp', 'Cloudflare WARP Proxy')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('ru', 'protocols', 'protocol_cf_warp', 'Cloudflare WARP Прокси')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
-- WARP-specific UI translations
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'warp_status', 'WARP Status')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('ru', 'protocols', 'warp_status', 'Статус WARP')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'warp_connected', 'Connected via Cloudflare')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('ru', 'protocols', 'warp_connected', 'Подключён через Cloudflare')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'warp_disconnected', 'Disconnected')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('ru', 'protocols', 'warp_disconnected', 'Отключён')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'warp_proxy_info', 'WARP proxy adds a Cloudflare encryption layer to hide destination domains from VPS provider. Traffic chain: Client → AmneziaWG → WARP → Cloudflare → Internet')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('ru', 'protocols', 'warp_proxy_info', 'WARP прокси добавляет слой шифрования Cloudflare для скрытия конечных доменов от провайдера VPS. Цепочка: Клиент → AmneziaWG → WARP → Cloudflare → Интернет')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('en', 'protocols', 'warp_warning_ram', '⚠️ Cloudflare WARP uses ~50-100MB additional RAM')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
INSERT INTO translations (locale, category, key_name, translation) VALUES
('ru', 'protocols', 'warp_warning_ram', '⚠️ Cloudflare WARP использует ~50-100 МБ дополнительной RAM')
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
@@ -0,0 +1,402 @@
-- =====================================================================
-- Migration 067: WARP auto-integration with redsocks + iptables
-- Automatically routes all VPN client TCP traffic through WARP proxy
-- Chain: VPN clients (10.8.x.0/24) → redsocks → WARP SOCKS5 → Cloudflare
-- Also detects X-Ray and patches its outbound config
-- =====================================================================
UPDATE protocols
SET install_script = '#!/bin/bash
set -eo pipefail
# ======================================================================
# Cloudflare WARP Proxy Installer (v2 with auto-routing)
# Installs WARP + redsocks, auto-routes VPN client traffic through CF
# Chain: VPN clients redsocks WARP SOCKS5 Cloudflare Internet
# ======================================================================
WARP_PROXY_PORT="${WARP_PROXY_PORT:-40000}"
WARP_MODE="${WARP_MODE:-proxy}"
REDSOCKS_PORT="${REDSOCKS_PORT:-12345}"
export DEBIAN_FRONTEND=noninteractive
echo "=== Installing Cloudflare WARP (v2 with auto-routing) ==="
# Detect OS
if [ -f /etc/os-release ]; then
. /etc/os-release
OS_ID="$ID"
OS_VERSION="$VERSION_ID"
else
OS_ID="unknown"
OS_VERSION="0"
fi
echo "Detected OS: $OS_ID $OS_VERSION"
# Check architecture
ARCH=$(uname -m)
if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then
echo "ERROR: WARP supports only x86_64 and aarch64, got: $ARCH"
exit 1
fi
# Check available RAM (warn if < 512MB)
TOTAL_RAM_MB=$(free -m 2>/dev/null | awk "/^Mem:/{print \\$2}" || echo "0")
if [ "$TOTAL_RAM_MB" -gt 0 ] && [ "$TOTAL_RAM_MB" -lt 512 ]; then
echo "WARNING: Server has only ${TOTAL_RAM_MB}MB RAM. WARP needs ~100MB. Consider upgrading."
fi
# Install prerequisites
apt-get update -qq
apt-get install -y -qq curl gnupg lsb-release >/dev/null 2>&1
# Add Cloudflare WARP repository
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
# Determine correct repo codename
REPO_CODENAME=""
case "$OS_ID" in
ubuntu)
case "$OS_VERSION" in
24.04) REPO_CODENAME="noble" ;;
22.04) REPO_CODENAME="jammy" ;;
20.04) REPO_CODENAME="focal" ;;
*) REPO_CODENAME="jammy" ;;
esac
;;
debian)
case "$OS_VERSION" in
12*) REPO_CODENAME="bookworm" ;;
11*) REPO_CODENAME="bullseye" ;;
*) REPO_CODENAME="bookworm" ;;
esac
;;
*)
REPO_CODENAME="jammy"
echo "WARNING: Unsupported OS $OS_ID, trying Ubuntu Jammy repo"
;;
esac
echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $REPO_CODENAME main" > /etc/apt/sources.list.d/cloudflare-client.list
# Install WARP client
apt-get update -qq
apt-get install -y -qq cloudflare-warp >/dev/null 2>&1
echo "WARP package installed"
# Check if already registered
WARP_STATUS=$(warp-cli --accept-tos status 2>/dev/null || echo "unregistered")
if echo "$WARP_STATUS" | grep -qiE "Registration Missing|unregistered"; then
echo "Registering WARP..."
warp-cli --accept-tos registration new
echo "WARP registered"
else
echo "WARP already registered"
fi
# Set proxy mode
echo "Setting WARP to proxy mode on port $WARP_PROXY_PORT..."
warp-cli --accept-tos mode proxy
warp-cli --accept-tos proxy port "$WARP_PROXY_PORT"
# Connect WARP
echo "Connecting WARP..."
warp-cli --accept-tos connect
# Wait for connection
for i in $(seq 1 15); do
CONN_STATUS=$(warp-cli --accept-tos status 2>/dev/null || echo "")
if echo "$CONN_STATUS" | grep -qi "Connected"; then
echo "WARP connected successfully"
break
fi
if [ "$i" -eq 15 ]; then
echo "WARNING: WARP connection timeout, may still be connecting..."
fi
sleep 2
done
# Verify proxy is listening
sleep 2
if command -v ss >/dev/null 2>&1; then
LISTENING=$(ss -tlnp 2>/dev/null | grep ":${WARP_PROXY_PORT}" || true)
elif command -v netstat >/dev/null 2>&1; then
LISTENING=$(netstat -tlnp 2>/dev/null | grep ":${WARP_PROXY_PORT}" || true)
else
LISTENING=""
fi
if [ -n "$LISTENING" ]; then
echo "WARP SOCKS5 proxy listening on 127.0.0.1:${WARP_PROXY_PORT}"
else
echo "WARNING: Proxy port ${WARP_PROXY_PORT} not yet listening, WARP may need more time"
fi
# Test proxy connectivity
PROXY_TEST=$(curl -x socks5h://127.0.0.1:${WARP_PROXY_PORT} -s -o /dev/null -w "%{http_code}" --max-time 10 https://cloudflare.com/cdn-cgi/trace 2>/dev/null || echo "000")
if [ "$PROXY_TEST" = "200" ]; then
echo "WARP proxy test: OK (HTTP 200)"
else
echo "WARNING: WARP proxy test returned HTTP $PROXY_TEST (may need a moment to initialize)"
fi
# Get WARP IP info
WARP_IP=$(curl -x socks5h://127.0.0.1:${WARP_PROXY_PORT} -s --max-time 10 https://cloudflare.com/cdn-cgi/trace 2>/dev/null | grep "ip=" | cut -d= -f2 || echo "unknown")
WARP_ACCOUNT=$(warp-cli --accept-tos registration show 2>/dev/null | grep -i "Account ID" | awk "{print \\$NF}" || echo "unknown")
# Enable WARP service to start on boot
systemctl enable warp-svc 2>/dev/null || true
# ======================================================================
# AUTO-ROUTING: Install redsocks + iptables rules
# Routes all VPN client TCP traffic through WARP
# ======================================================================
echo ""
echo "=== Setting up auto-routing (redsocks) ==="
ROUTED_SUBNETS=""
# Install redsocks
apt-get install -y -qq redsocks >/dev/null 2>&1 || {
echo "WARNING: redsocks not available in repos, trying manual install"
apt-get install -y -qq gcc libevent-dev make git >/dev/null 2>&1 || true
if [ ! -f /usr/local/bin/redsocks ]; then
cd /tmp
git clone --depth=1 https://github.com/darkk/redsocks.git redsocks-build 2>/dev/null || true
if [ -d redsocks-build ]; then
cd redsocks-build && make -j$(nproc) 2>/dev/null && cp redsocks /usr/local/bin/redsocks && chmod +x /usr/local/bin/redsocks
cd / && rm -rf /tmp/redsocks-build
echo "redsocks built from source"
fi
fi
}
REDSOCKS_BIN=$(command -v redsocks 2>/dev/null || echo "")
if [ -z "$REDSOCKS_BIN" ] && [ -f /usr/local/bin/redsocks ]; then
REDSOCKS_BIN="/usr/local/bin/redsocks"
fi
if [ -n "$REDSOCKS_BIN" ]; then
echo "redsocks found: $REDSOCKS_BIN"
# Create redsocks config
mkdir -p /etc/redsocks
cat > /etc/redsocks/redsocks.conf << REDSOCKS_EOF
base {
log_debug = off;
log_info = on;
log = "syslog:daemon";
daemon = on;
redirector = iptables;
}
redsocks {
local_ip = 127.0.0.1;
local_port = ${REDSOCKS_PORT};
ip = 127.0.0.1;
port = ${WARP_PROXY_PORT};
type = socks5;
}
REDSOCKS_EOF
# Create systemd service for redsocks
cat > /etc/systemd/system/redsocks-warp.service << SYSTEMD_EOF
[Unit]
Description=Redsocks WARP transparent proxy
After=network.target warp-svc.service
Wants=warp-svc.service
[Service]
Type=forking
ExecStart=${REDSOCKS_BIN} -c /etc/redsocks/redsocks.conf
ExecStop=/bin/kill -TERM \$MAINPID
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
SYSTEMD_EOF
# Stop any existing redsocks
systemctl stop redsocks 2>/dev/null || true
systemctl stop redsocks-warp 2>/dev/null || true
killall redsocks 2>/dev/null || true
# Start redsocks
systemctl daemon-reload
systemctl enable redsocks-warp 2>/dev/null || true
systemctl start redsocks-warp
echo "redsocks-warp service started on port ${REDSOCKS_PORT}"
# Detect VPN subnets to route
# 1. AWG subnets from running containers
for container in $(docker ps --format "{{.Names}}" 2>/dev/null | grep -iE "amnezia-awg|awg" || true); do
SUBNET=$(docker exec "$container" cat /opt/amnezia/awg/wg0.conf 2>/dev/null | grep -oP "Address\s*=\s*\K[0-9./]+" | head -1 || true)
if [ -z "$SUBNET" ]; then
SUBNET=$(docker exec "$container" cat /opt/amnezia/awg/awg0.conf 2>/dev/null | grep -oP "Address\s*=\s*\K[0-9./]+" | head -1 || true)
fi
if [ -n "$SUBNET" ]; then
# Convert server IP/mask to network: 10.8.1.1/24 -> 10.8.1.0/24
NET=$(echo "$SUBNET" | sed -E "s/([0-9]+\\.[0-9]+\\.[0-9]+)\\.[0-9]+/\\1.0/")
ROUTED_SUBNETS="$ROUTED_SUBNETS $NET"
echo "Detected AWG subnet from $container: $NET"
fi
done
# 2. Check server vpn_subnet from panel config
if [ -z "$ROUTED_SUBNETS" ]; then
# Fallback: common Amnezia subnets
ROUTED_SUBNETS="10.8.1.0/24"
echo "Using default AWG subnet: 10.8.1.0/24"
fi
# Setup iptables REDSOCKS chain
echo "Setting up iptables rules for subnets:$ROUTED_SUBNETS"
iptables -t nat -N REDSOCKS_WARP 2>/dev/null || iptables -t nat -F REDSOCKS_WARP
# Skip local/private destinations
iptables -t nat -A REDSOCKS_WARP -d 0.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS_WARP -d 10.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS_WARP -d 100.64.0.0/10 -j RETURN
iptables -t nat -A REDSOCKS_WARP -d 127.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS_WARP -d 169.254.0.0/16 -j RETURN
iptables -t nat -A REDSOCKS_WARP -d 172.16.0.0/12 -j RETURN
iptables -t nat -A REDSOCKS_WARP -d 192.168.0.0/16 -j RETURN
iptables -t nat -A REDSOCKS_WARP -d 224.0.0.0/4 -j RETURN
iptables -t nat -A REDSOCKS_WARP -d 240.0.0.0/4 -j RETURN
# Redirect remaining TCP to redsocks
iptables -t nat -A REDSOCKS_WARP -p tcp -j REDIRECT --to-ports ${REDSOCKS_PORT}
# Apply REDSOCKS_WARP chain to VPN subnets
for SUBNET in $ROUTED_SUBNETS; do
# Remove old rule if exists
iptables -t nat -D PREROUTING -s "$SUBNET" -p tcp -j REDSOCKS_WARP 2>/dev/null || true
# Add new rule
iptables -t nat -A PREROUTING -s "$SUBNET" -p tcp -j REDSOCKS_WARP
echo "Routing $SUBNET TCP traffic through WARP"
done
# Save iptables rules for persistence
if command -v netfilter-persistent >/dev/null 2>&1; then
netfilter-persistent save 2>/dev/null || true
elif command -v iptables-save >/dev/null 2>&1; then
mkdir -p /etc/iptables
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
fi
# Save routed subnets for uninstall cleanup
echo "$ROUTED_SUBNETS" > /var/lib/cloudflare-warp/routed_subnets
echo "Auto-routing configured: VPN client TCP traffic now goes through WARP"
else
echo "WARNING: redsocks not available, skipping auto-routing."
echo "VPN clients will NOT automatically route through WARP."
echo "Manual SOCKS5 proxy: socks5h://127.0.0.1:${WARP_PROXY_PORT}"
fi
# ======================================================================
# X-Ray integration: patch outbound config if X-Ray is running
# ======================================================================
XRAY_CONTAINER=$(docker ps --format "{{.Names}}" 2>/dev/null | grep -i "xray" | head -1 || true)
if [ -n "$XRAY_CONTAINER" ]; then
echo ""
echo "=== Detected X-Ray container: $XRAY_CONTAINER ==="
XRAY_CONFIG=$(docker exec "$XRAY_CONTAINER" cat /etc/xray/config.json 2>/dev/null || echo "")
if [ -n "$XRAY_CONFIG" ]; then
# Check if warp-out already configured
if echo "$XRAY_CONFIG" | grep -q "warp-out"; then
echo "X-Ray already has warp-out outbound, skipping"
else
echo "NOTE: X-Ray detected but auto-patching disabled for safety."
echo "To route X-Ray traffic through WARP, add this outbound manually:"
echo " {\"tag\":\"warp-out\",\"protocol\":\"socks\",\"settings\":{\"servers\":[{\"address\":\"127.0.0.1\",\"port\":${WARP_PROXY_PORT}}]}}"
fi
fi
fi
# Get server external IP
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
echo ""
echo "=== Cloudflare WARP Proxy Installed ==="
echo "Variable: warp_proxy_port=$WARP_PROXY_PORT"
echo "Variable: warp_mode=$WARP_MODE"
echo "Variable: warp_ip=$WARP_IP"
echo "Variable: warp_account=$WARP_ACCOUNT"
echo "Variable: server_host=$EXTERNAL_IP"
echo "Variable: proxy_address=127.0.0.1:${WARP_PROXY_PORT}"
echo "Variable: redsocks_port=$REDSOCKS_PORT"
echo "Variable: routed_subnets=$ROUTED_SUBNETS"',
uninstall_script = '#!/bin/bash
set -eo pipefail
echo "=== Uninstalling Cloudflare WARP ==="
# Remove iptables rules
ROUTED_SUBNETS=""
if [ -f /var/lib/cloudflare-warp/routed_subnets ]; then
ROUTED_SUBNETS=$(cat /var/lib/cloudflare-warp/routed_subnets)
fi
if [ -z "$ROUTED_SUBNETS" ]; then
ROUTED_SUBNETS="10.8.1.0/24"
fi
for SUBNET in $ROUTED_SUBNETS; do
iptables -t nat -D PREROUTING -s "$SUBNET" -p tcp -j REDSOCKS_WARP 2>/dev/null || true
done
iptables -t nat -F REDSOCKS_WARP 2>/dev/null || true
iptables -t nat -X REDSOCKS_WARP 2>/dev/null || true
echo "iptables REDSOCKS_WARP chain removed"
# Save cleaned iptables
if command -v netfilter-persistent >/dev/null 2>&1; then
netfilter-persistent save 2>/dev/null || true
elif command -v iptables-save >/dev/null 2>&1; then
mkdir -p /etc/iptables
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
fi
# Stop and remove redsocks
systemctl stop redsocks-warp 2>/dev/null || true
systemctl disable redsocks-warp 2>/dev/null || true
rm -f /etc/systemd/system/redsocks-warp.service
rm -rf /etc/redsocks
systemctl daemon-reload 2>/dev/null || true
echo "redsocks-warp service removed"
# Disconnect and deregister WARP
warp-cli --accept-tos disconnect 2>/dev/null || true
warp-cli --accept-tos registration delete 2>/dev/null || true
# Stop service
systemctl stop warp-svc 2>/dev/null || true
systemctl disable warp-svc 2>/dev/null || true
# Remove package
apt-get remove -y cloudflare-warp 2>/dev/null || true
apt-get autoremove -y 2>/dev/null || true
# Clean up config
rm -rf /var/lib/cloudflare-warp 2>/dev/null || true
rm -f /etc/apt/sources.list.d/cloudflare-client.list 2>/dev/null || true
rm -f /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg 2>/dev/null || true
echo "{\"success\":true,\"message\":\"Cloudflare WARP + redsocks uninstalled\"}"',
updated_at = NOW()
WHERE slug = 'cf-warp';
-- Add new protocol variables for redsocks integration
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'redsocks_port', 'number', '12345', 'Redsocks transparent proxy port', false
FROM protocols p WHERE p.slug = 'cf-warp'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'redsocks_port');
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
SELECT p.id, 'routed_subnets', 'string', '10.8.1.0/24', 'VPN subnets routed through WARP', false
FROM protocols p WHERE p.slug = 'cf-warp'
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'routed_subnets');
+246
View File
@@ -0,0 +1,246 @@
-- =====================================================================
-- Migration 068: Fix WARP install script for panel heredoc compatibility
-- Fixes: nested heredoc conflict, set -eo pipefail duplication,
-- docker format template conflict with panel wrapper
-- =====================================================================
UPDATE protocols
SET install_script = '#!/bin/bash
# ======================================================================
# Cloudflare WARP Proxy Installer v3 (panel-compatible)
# Installs WARP + redsocks, auto-routes VPN client traffic through CF
# ======================================================================
WARP_PROXY_PORT="${WARP_PROXY_PORT:-40000}"
WARP_MODE="${WARP_MODE:-proxy}"
REDSOCKS_PORT="${REDSOCKS_PORT:-12345}"
export DEBIAN_FRONTEND=noninteractive
echo "=== Installing Cloudflare WARP v3 ==="
# Detect OS
if [ -f /etc/os-release ]; then
. /etc/os-release
OS_ID="$ID"
OS_VERSION="$VERSION_ID"
else
OS_ID="unknown"
OS_VERSION="0"
fi
echo "OS: $OS_ID $OS_VERSION"
ARCH=$(uname -m)
if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then
echo "FAIL: WARP supports only x86_64 and aarch64, got: $ARCH"
exit 1
fi
TOTAL_RAM_MB=$(free -m 2>/dev/null | awk "/^Mem:/{print \$2}" || echo "0")
if [ "$TOTAL_RAM_MB" -gt 0 ] && [ "$TOTAL_RAM_MB" -lt 512 ]; then
echo "NOTE: Low RAM ${TOTAL_RAM_MB}MB. WARP needs ~100MB."
fi
apt-get update -qq
apt-get install -y -qq curl gnupg lsb-release >/dev/null 2>&1
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
REPO_CODENAME=""
case "$OS_ID" in
ubuntu)
case "$OS_VERSION" in
24.04) REPO_CODENAME="noble" ;;
22.04) REPO_CODENAME="jammy" ;;
20.04) REPO_CODENAME="focal" ;;
*) REPO_CODENAME="jammy" ;;
esac
;;
debian)
case "$OS_VERSION" in
12*) REPO_CODENAME="bookworm" ;;
11*) REPO_CODENAME="bullseye" ;;
*) REPO_CODENAME="bookworm" ;;
esac
;;
*)
REPO_CODENAME="jammy"
;;
esac
echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $REPO_CODENAME main" > /etc/apt/sources.list.d/cloudflare-client.list
apt-get update -qq
apt-get install -y -qq cloudflare-warp >/dev/null 2>&1
echo "WARP package installed"
WARP_STATUS=$(warp-cli --accept-tos status 2>/dev/null || echo "unregistered")
if echo "$WARP_STATUS" | grep -qiE "Registration Missing|unregistered"; then
warp-cli --accept-tos registration new
echo "WARP registered"
else
echo "WARP already registered"
fi
warp-cli --accept-tos mode proxy
warp-cli --accept-tos proxy port "$WARP_PROXY_PORT"
warp-cli --accept-tos connect
echo "WARP connecting..."
for i in $(seq 1 15); do
CONN_STATUS=$(warp-cli --accept-tos status 2>/dev/null || echo "")
if echo "$CONN_STATUS" | grep -qi "Connected"; then
echo "WARP connected"
break
fi
sleep 2
done
sleep 2
LISTENING=""
if command -v ss >/dev/null 2>&1; then
LISTENING=$(ss -tlnp 2>/dev/null | grep ":${WARP_PROXY_PORT}" || true)
fi
if [ -n "$LISTENING" ]; then
echo "WARP SOCKS5 proxy on 127.0.0.1:${WARP_PROXY_PORT} OK"
else
echo "NOTE: Proxy port ${WARP_PROXY_PORT} not yet listening"
fi
PROXY_TEST=$(curl -x socks5h://127.0.0.1:${WARP_PROXY_PORT} -s -o /dev/null -w "%{http_code}" --max-time 10 https://cloudflare.com/cdn-cgi/trace 2>/dev/null || echo "000")
if [ "$PROXY_TEST" = "200" ]; then
echo "WARP proxy test OK"
fi
WARP_IP=$(curl -x socks5h://127.0.0.1:${WARP_PROXY_PORT} -s --max-time 10 https://cloudflare.com/cdn-cgi/trace 2>/dev/null | grep "ip=" | cut -d= -f2 || echo "unknown")
WARP_ACCOUNT=$(warp-cli --accept-tos registration show 2>/dev/null | grep -i "Account ID" | awk "{print \$NF}" || echo "unknown")
systemctl enable warp-svc 2>/dev/null || true
# AUTO-ROUTING: redsocks + iptables
echo "=== Setting up redsocks auto-routing ==="
apt-get install -y -qq redsocks >/dev/null 2>&1 || true
REDSOCKS_BIN=$(command -v redsocks 2>/dev/null || echo "")
if [ -z "$REDSOCKS_BIN" ]; then
echo "NOTE: redsocks package not installed, trying /usr/sbin"
if [ -f /usr/sbin/redsocks ]; then
REDSOCKS_BIN="/usr/sbin/redsocks"
fi
fi
if [ -n "$REDSOCKS_BIN" ]; then
echo "redsocks binary: $REDSOCKS_BIN"
mkdir -p /etc/redsocks
printf "base {\\n log_debug = off;\\n log_info = on;\\n log = \\"syslog:daemon\\";\\n daemon = on;\\n redirector = iptables;\\n}\\nredsocks {\\n local_ip = 127.0.0.1;\\n local_port = %s;\\n ip = 127.0.0.1;\\n port = %s;\\n type = socks5;\\n}\\n" "$REDSOCKS_PORT" "$WARP_PROXY_PORT" > /etc/redsocks/redsocks.conf
echo "redsocks config created"
printf "[Unit]\\nDescription=Redsocks WARP transparent proxy\\nAfter=network.target warp-svc.service\\nWants=warp-svc.service\\n\\n[Service]\\nType=forking\\nExecStart=%s -c /etc/redsocks/redsocks.conf\\nRestart=on-failure\\nRestartSec=5\\n\\n[Install]\\nWantedBy=multi-user.target\\n" "$REDSOCKS_BIN" > /etc/systemd/system/redsocks-warp.service
echo "systemd service created"
systemctl stop redsocks 2>/dev/null || true
systemctl stop redsocks-warp 2>/dev/null || true
killall redsocks 2>/dev/null || true
systemctl daemon-reload
systemctl enable redsocks-warp 2>/dev/null || true
systemctl start redsocks-warp 2>/dev/null || true
echo "redsocks-warp started"
ROUTED_SUBNETS=""
for container in $(docker ps --format "{{`{{.Names}}`}}" 2>/dev/null | grep -iE "amnezia-awg|awg" || true); do
SUBNET=$(docker exec "$container" cat /opt/amnezia/awg/wg0.conf 2>/dev/null | grep -oP "Address\\s*=\\s*\\K[0-9./]+" | head -1 || true)
if [ -z "$SUBNET" ]; then
SUBNET=$(docker exec "$container" cat /opt/amnezia/awg/awg0.conf 2>/dev/null | grep -oP "Address\\s*=\\s*\\K[0-9./]+" | head -1 || true)
fi
if [ -n "$SUBNET" ]; then
NET=$(echo "$SUBNET" | sed -E "s/([0-9]+\\.[0-9]+\\.[0-9]+)\\.[0-9]+/\\1.0/")
ROUTED_SUBNETS="$ROUTED_SUBNETS $NET"
echo "AWG subnet from $container: $NET"
fi
done
if [ -z "$ROUTED_SUBNETS" ]; then
ROUTED_SUBNETS="10.8.1.0/24"
echo "Default subnet: 10.8.1.0/24"
fi
iptables -t nat -N REDSOCKS_WARP 2>/dev/null || iptables -t nat -F REDSOCKS_WARP
iptables -t nat -A REDSOCKS_WARP -d 0.0.0.0/8 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 10.0.0.0/8 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 100.64.0.0/10 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 127.0.0.0/8 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 169.254.0.0/16 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 172.16.0.0/12 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 192.168.0.0/16 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 224.0.0.0/4 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 240.0.0.0/4 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -p tcp -j REDIRECT --to-ports ${REDSOCKS_PORT} 2>/dev/null || true
for SUBNET in $ROUTED_SUBNETS; do
iptables -t nat -D PREROUTING -s "$SUBNET" -p tcp -j REDSOCKS_WARP 2>/dev/null || true
iptables -t nat -A PREROUTING -s "$SUBNET" -p tcp -j REDSOCKS_WARP 2>/dev/null || true
echo "Routing $SUBNET through WARP"
done
mkdir -p /var/lib/cloudflare-warp
echo "$ROUTED_SUBNETS" > /var/lib/cloudflare-warp/routed_subnets
echo "Auto-routing active"
else
echo "NOTE: redsocks not available, auto-routing skipped"
echo "Manual proxy: socks5h://127.0.0.1:${WARP_PROXY_PORT}"
fi
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
echo ""
echo "=== Cloudflare WARP Installed ==="
echo "Variable: warp_proxy_port=$WARP_PROXY_PORT"
echo "Variable: warp_mode=$WARP_MODE"
echo "Variable: warp_ip=$WARP_IP"
echo "Variable: warp_account=$WARP_ACCOUNT"
echo "Variable: server_host=$EXTERNAL_IP"
echo "Variable: proxy_address=127.0.0.1:${WARP_PROXY_PORT}"
echo "Variable: redsocks_port=$REDSOCKS_PORT"
echo "Variable: routed_subnets=$ROUTED_SUBNETS"',
uninstall_script = '#!/bin/bash
echo "=== Uninstalling Cloudflare WARP ==="
ROUTED_SUBNETS=""
if [ -f /var/lib/cloudflare-warp/routed_subnets ]; then
ROUTED_SUBNETS=$(cat /var/lib/cloudflare-warp/routed_subnets)
fi
if [ -z "$ROUTED_SUBNETS" ]; then
ROUTED_SUBNETS="10.8.1.0/24"
fi
for SUBNET in $ROUTED_SUBNETS; do
iptables -t nat -D PREROUTING -s "$SUBNET" -p tcp -j REDSOCKS_WARP 2>/dev/null || true
done
iptables -t nat -F REDSOCKS_WARP 2>/dev/null || true
iptables -t nat -X REDSOCKS_WARP 2>/dev/null || true
systemctl stop redsocks-warp 2>/dev/null || true
systemctl disable redsocks-warp 2>/dev/null || true
rm -f /etc/systemd/system/redsocks-warp.service 2>/dev/null || true
rm -rf /etc/redsocks 2>/dev/null || true
systemctl daemon-reload 2>/dev/null || true
warp-cli --accept-tos disconnect 2>/dev/null || true
warp-cli --accept-tos registration delete 2>/dev/null || true
systemctl stop warp-svc 2>/dev/null || true
systemctl disable warp-svc 2>/dev/null || true
apt-get remove -y cloudflare-warp 2>/dev/null || true
apt-get autoremove -y 2>/dev/null || true
rm -rf /var/lib/cloudflare-warp 2>/dev/null || true
rm -f /etc/apt/sources.list.d/cloudflare-client.list 2>/dev/null || true
rm -f /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg 2>/dev/null || true
echo "{\"success\":true,\"message\":\"WARP + redsocks removed\"}"',
updated_at = NOW()
WHERE slug = 'cf-warp';
+237
View File
@@ -0,0 +1,237 @@
-- =====================================================================
-- Migration 069: WARP auto-detect AIVPN subnet for routing
-- Adds aivpn0 interface subnet detection alongside AWG containers
-- =====================================================================
UPDATE protocols
SET install_script = '#!/bin/bash
# ======================================================================
# Cloudflare WARP Proxy Installer v4 (AWG + AIVPN + X-Ray)
# Installs WARP + redsocks, auto-routes ALL VPN client traffic through CF
# ======================================================================
WARP_PROXY_PORT="${WARP_PROXY_PORT:-40000}"
WARP_MODE="${WARP_MODE:-proxy}"
REDSOCKS_PORT="${REDSOCKS_PORT:-12345}"
export DEBIAN_FRONTEND=noninteractive
echo "=== Installing Cloudflare WARP v4 ==="
# Detect OS
if [ -f /etc/os-release ]; then
. /etc/os-release
OS_ID="$ID"
OS_VERSION="$VERSION_ID"
else
OS_ID="unknown"
OS_VERSION="0"
fi
echo "OS: $OS_ID $OS_VERSION"
ARCH=$(uname -m)
if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then
echo "FAIL: WARP supports only x86_64 and aarch64, got: $ARCH"
exit 1
fi
TOTAL_RAM_MB=$(free -m 2>/dev/null | awk "/^Mem:/{print \$2}" || echo "0")
if [ "$TOTAL_RAM_MB" -gt 0 ] && [ "$TOTAL_RAM_MB" -lt 512 ]; then
echo "NOTE: Low RAM ${TOTAL_RAM_MB}MB. WARP needs ~100MB."
fi
apt-get update -qq
apt-get install -y -qq curl gnupg lsb-release >/dev/null 2>&1
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
REPO_CODENAME=""
case "$OS_ID" in
ubuntu)
case "$OS_VERSION" in
24.04) REPO_CODENAME="noble" ;;
22.04) REPO_CODENAME="jammy" ;;
20.04) REPO_CODENAME="focal" ;;
*) REPO_CODENAME="jammy" ;;
esac
;;
debian)
case "$OS_VERSION" in
12*) REPO_CODENAME="bookworm" ;;
11*) REPO_CODENAME="bullseye" ;;
*) REPO_CODENAME="bookworm" ;;
esac
;;
*)
REPO_CODENAME="jammy"
;;
esac
echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $REPO_CODENAME main" > /etc/apt/sources.list.d/cloudflare-client.list
apt-get update -qq
apt-get install -y -qq cloudflare-warp >/dev/null 2>&1
echo "WARP package installed"
WARP_STATUS=$(warp-cli --accept-tos status 2>/dev/null || echo "unregistered")
if echo "$WARP_STATUS" | grep -qiE "Registration Missing|unregistered"; then
warp-cli --accept-tos registration new
echo "WARP registered"
else
echo "WARP already registered"
fi
warp-cli --accept-tos mode proxy
warp-cli --accept-tos proxy port "$WARP_PROXY_PORT"
warp-cli --accept-tos connect
echo "WARP connecting..."
for i in $(seq 1 15); do
CONN_STATUS=$(warp-cli --accept-tos status 2>/dev/null || echo "")
if echo "$CONN_STATUS" | grep -qi "Connected"; then
echo "WARP connected"
break
fi
sleep 2
done
sleep 2
LISTENING=""
if command -v ss >/dev/null 2>&1; then
LISTENING=$(ss -tlnp 2>/dev/null | grep ":${WARP_PROXY_PORT}" || true)
fi
if [ -n "$LISTENING" ]; then
echo "WARP SOCKS5 proxy on 127.0.0.1:${WARP_PROXY_PORT} OK"
else
echo "NOTE: Proxy port ${WARP_PROXY_PORT} not yet listening"
fi
PROXY_TEST=$(curl -x socks5h://127.0.0.1:${WARP_PROXY_PORT} -s -o /dev/null -w "%{http_code}" --max-time 10 https://cloudflare.com/cdn-cgi/trace 2>/dev/null || echo "000")
if [ "$PROXY_TEST" = "200" ]; then
echo "WARP proxy test OK"
fi
WARP_IP=$(curl -x socks5h://127.0.0.1:${WARP_PROXY_PORT} -s --max-time 10 https://cloudflare.com/cdn-cgi/trace 2>/dev/null | grep "ip=" | cut -d= -f2 || echo "unknown")
WARP_ACCOUNT=$(warp-cli --accept-tos registration show 2>/dev/null | grep -i "Account ID" | awk "{print \$NF}" || echo "unknown")
systemctl enable warp-svc 2>/dev/null || true
# AUTO-ROUTING: redsocks + iptables
echo "=== Setting up redsocks auto-routing ==="
apt-get install -y -qq redsocks >/dev/null 2>&1 || true
REDSOCKS_BIN=$(command -v redsocks 2>/dev/null || echo "")
if [ -z "$REDSOCKS_BIN" ]; then
if [ -f /usr/sbin/redsocks ]; then
REDSOCKS_BIN="/usr/sbin/redsocks"
fi
fi
if [ -n "$REDSOCKS_BIN" ]; then
echo "redsocks binary: $REDSOCKS_BIN"
mkdir -p /etc/redsocks
printf "base {\\n log_debug = off;\\n log_info = on;\\n log = \\"syslog:daemon\\";\\n daemon = on;\\n redirector = iptables;\\n}\\nredsocks {\\n local_ip = 0.0.0.0;\\n local_port = %s;\\n ip = 127.0.0.1;\\n port = %s;\\n type = socks5;\\n}\\n" "$REDSOCKS_PORT" "$WARP_PROXY_PORT" > /etc/redsocks/redsocks.conf
echo "redsocks config created"
printf "[Unit]\\nDescription=Redsocks WARP transparent proxy\\nAfter=network.target warp-svc.service\\nWants=warp-svc.service\\n\\n[Service]\\nType=forking\\nExecStart=%s -c /etc/redsocks/redsocks.conf\\nRestart=on-failure\\nRestartSec=5\\n\\n[Install]\\nWantedBy=multi-user.target\\n" "$REDSOCKS_BIN" > /etc/systemd/system/redsocks-warp.service
echo "systemd service created"
systemctl stop redsocks 2>/dev/null || true
systemctl stop redsocks-warp 2>/dev/null || true
killall redsocks 2>/dev/null || true
systemctl daemon-reload
systemctl enable redsocks-warp 2>/dev/null || true
systemctl start redsocks-warp 2>/dev/null || true
echo "redsocks-warp started"
ROUTED_SUBNETS=""
CONTAINER_IPS=""
# 1. Detect AWG container IPs (containers MASQUERADE VPN traffic,
# so on the host we see src=container_IP, not VPN subnet)
for container in $(docker ps --format "{{`{{.Names}}`}}" 2>/dev/null | grep -iE "amnezia-awg|awg" || true); do
CIP=$(docker inspect -f "{{`{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}`}}" "$container" 2>/dev/null || true)
if [ -n "$CIP" ]; then
CONTAINER_IPS="$CONTAINER_IPS $CIP"
echo "AWG container $container IP: $CIP"
fi
done
# 2. Detect host-level VPN subnets (aivpn0, wg0, etc.)
for iface in aivpn0 wg0 wg1; do
ADDR=$(ip -4 addr show "$iface" 2>/dev/null | grep -oP "inet \\K[0-9./]+" | head -1 || true)
if [ -n "$ADDR" ]; then
NET=$(echo "$ADDR" | sed -E "s/([0-9]+\\.[0-9]+\\.[0-9]+)\\.[0-9]+(.*)/\\1.0\\2/")
if ! echo "$ROUTED_SUBNETS" | grep -q "$NET"; then
ROUTED_SUBNETS="$ROUTED_SUBNETS $NET"
echo "VPN subnet from $iface: $NET"
fi
fi
done
# 3. Fallback if nothing detected
if [ -z "$ROUTED_SUBNETS" ] && [ -z "$CONTAINER_IPS" ]; then
ROUTED_SUBNETS="10.8.1.0/24 10.0.0.0/24"
echo "Default subnets: 10.8.1.0/24 10.0.0.0/24"
fi
# Enable route_localnet so DNAT to 127.0.0.1 works for Docker traffic
sysctl -w net.ipv4.conf.docker0.route_localnet=1 2>/dev/null || true
sysctl -w net.ipv4.conf.all.route_localnet=1 2>/dev/null || true
grep -q route_localnet /etc/sysctl.d/99-warp.conf 2>/dev/null || {
mkdir -p /etc/sysctl.d
echo "net.ipv4.conf.docker0.route_localnet=1" >> /etc/sysctl.d/99-warp.conf
echo "net.ipv4.conf.all.route_localnet=1" >> /etc/sysctl.d/99-warp.conf
}
iptables -t nat -N REDSOCKS_WARP 2>/dev/null || iptables -t nat -F REDSOCKS_WARP
iptables -t nat -A REDSOCKS_WARP -d 0.0.0.0/8 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 10.0.0.0/8 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 100.64.0.0/10 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 127.0.0.0/8 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 169.254.0.0/16 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 172.16.0.0/12 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 192.168.0.0/16 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 224.0.0.0/4 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -d 240.0.0.0/4 -j RETURN 2>/dev/null || true
iptables -t nat -A REDSOCKS_WARP -p tcp -j REDIRECT --to-ports ${REDSOCKS_PORT} 2>/dev/null || true
# Add rules for host-level VPN subnets
for SUBNET in $ROUTED_SUBNETS; do
iptables -t nat -D PREROUTING -s "$SUBNET" -p tcp -j REDSOCKS_WARP 2>/dev/null || true
iptables -t nat -A PREROUTING -s "$SUBNET" -p tcp -j REDSOCKS_WARP 2>/dev/null || true
echo "Routing subnet $SUBNET through WARP"
done
# Add rules for AWG container IPs (traffic exits container MASQUERADEd)
for CIP in $CONTAINER_IPS; do
iptables -t nat -D PREROUTING -s "$CIP" -p tcp -j REDSOCKS_WARP 2>/dev/null || true
iptables -t nat -A PREROUTING -s "$CIP" -p tcp -j REDSOCKS_WARP 2>/dev/null || true
echo "Routing container $CIP through WARP"
done
mkdir -p /var/lib/cloudflare-warp
echo "$ROUTED_SUBNETS $CONTAINER_IPS" > /var/lib/cloudflare-warp/routed_subnets
echo "Auto-routing active"
else
echo "NOTE: redsocks not available, auto-routing skipped"
echo "Manual proxy: socks5h://127.0.0.1:${WARP_PROXY_PORT}"
fi
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
echo ""
echo "=== Cloudflare WARP Installed ==="
echo "Variable: warp_proxy_port=$WARP_PROXY_PORT"
echo "Variable: warp_mode=$WARP_MODE"
echo "Variable: warp_ip=$WARP_IP"
echo "Variable: warp_account=$WARP_ACCOUNT"
echo "Variable: server_host=$EXTERNAL_IP"
echo "Variable: proxy_address=127.0.0.1:${WARP_PROXY_PORT}"
echo "Variable: redsocks_port=$REDSOCKS_PORT"
echo "Variable: routed_subnets=$ROUTED_SUBNETS"',
updated_at = NOW()
WHERE slug = 'cf-warp';
+65
View File
@@ -763,6 +763,71 @@ Router::post('/servers/{id}/protocols/activate', function ($params) {
}
});
// Get WARP status for a server (AJAX)
Router::get('/servers/{id}/warp/status', function ($params) {
requireAuth();
header('Content-Type: application/json');
$serverId = (int) $params['id'];
try {
$server = new VpnServer($serverId);
$serverData = $server->getData();
$user = Auth::user();
if ($serverData['user_id'] != $user['id'] && !Auth::isAdmin()) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
return;
}
$status = InstallProtocolManager::getWarpStatus($server);
echo json_encode(array_merge(['success' => true], $status));
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
});
// WARP actions: connect/disconnect/reconnect (AJAX)
Router::post('/servers/{id}/warp/action', function ($params) {
requireAdmin();
header('Content-Type: application/json');
$serverId = (int) $params['id'];
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
if (!in_array($action, ['connect', 'disconnect', 'reconnect'], true)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid action. Allowed: connect, disconnect, reconnect']);
return;
}
try {
$server = new VpnServer($serverId);
$serverData = $server->getData();
$user = Auth::user();
if ($serverData['user_id'] != $user['id'] && !Auth::isAdmin()) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
return;
}
switch ($action) {
case 'connect':
$server->executeCommand('warp-cli --accept-tos connect 2>/dev/null', true);
break;
case 'disconnect':
$server->executeCommand('warp-cli --accept-tos disconnect 2>/dev/null', true);
break;
case 'reconnect':
$server->executeCommand('warp-cli --accept-tos disconnect 2>/dev/null || true', true);
sleep(1);
$server->executeCommand('warp-cli --accept-tos connect 2>/dev/null', true);
break;
}
sleep(2);
$status = InstallProtocolManager::getWarpStatus($server);
echo json_encode(array_merge(['success' => true, 'action' => $action], $status));
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
});
// View server
Router::get('/servers/{id}', function ($params) {
requireAuth();
+220 -21
View File
@@ -98,6 +98,59 @@
{% if sp.server_host %}<span>Host: {{ sp.server_host }}</span>{% endif %}
{% if sp.server_port %}<span class="ml-2">Port: {{ sp.server_port }}</span>{% endif %}
</div>
{% if sp.slug == 'cf-warp' %}
{# ── WARP Status Widget ── #}
<div id="warpStatusWidget" class="mt-3 p-3 rounded-lg" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" fill="#F59E0B"/>
</svg>
<span class="text-sm font-semibold text-white">Cloudflare WARP</span>
</div>
<div id="warpStatusBadge">
<span class="px-2 py-0.5 rounded text-xs bg-gray-600 text-gray-300">
<i class="fas fa-spinner fa-spin mr-1"></i>Загрузка...
</span>
</div>
</div>
<div id="warpDetails" class="space-y-1 text-xs">
<div class="flex items-center justify-between text-gray-400">
<span>Прокси</span>
<span id="warpProxy" class="font-mono text-gray-300">—</span>
</div>
<div class="flex items-center justify-between text-gray-400">
<span>WARP IP</span>
<span id="warpExitIp" class="font-mono text-gray-300">—</span>
</div>
<div class="flex items-center justify-between text-gray-400">
<span>Режим</span>
<span id="warpMode" class="text-gray-300">—</span>
</div>
<div class="flex items-center justify-between text-gray-400">
<span>Сервис</span>
<span id="warpSvc" class="text-gray-300">—</span>
</div>
</div>
<div class="flex gap-2 mt-3">
<button id="warpBtnConnect" onclick="warpAction('connect')" class="flex-1 px-2 py-1 rounded text-xs font-medium text-white" style="background:#22c55e;opacity:0.9" disabled>
<i class="fas fa-play mr-1"></i>Connect
</button>
<button id="warpBtnDisconnect" onclick="warpAction('disconnect')" class="flex-1 px-2 py-1 rounded text-xs font-medium text-white" style="background:#ef4444;opacity:0.9" disabled>
<i class="fas fa-stop mr-1"></i>Disconnect
</button>
<button id="warpBtnReconnect" onclick="warpAction('reconnect')" class="flex-1 px-2 py-1 rounded text-xs font-medium text-white" style="background:#3b82f6;opacity:0.9" disabled>
<i class="fas fa-sync-alt mr-1"></i>Reconnect
</button>
</div>
<p class="text-[10px] text-gray-500 mt-2 leading-tight">
⚠️ WARP ~50-100 МБ RAM • Цепочка: Клиент → WG → WARP → CF → Интернет
</p>
</div>
{% endif %}
</div>
{% else %}
<div class="text-sm text-gray-500">Нет установленных протоколов</div>
@@ -412,29 +465,42 @@ document.addEventListener('DOMContentLoaded', function() {
if (uninstallAllBtn) {
uninstallAllBtn.addEventListener('click', async function(e) {
console.log('uninstallAllBtn clicked');
e.preventDefault();
e.stopPropagation();
if (!confirm('Удалить все Amnezia-контейнеры на сервере?')) {
console.log('User canceled');
return;
}
console.log('Starting uninstall all...');
if (!confirm('Удалить все Amnezia-контейнеры на сервере?')) return;
const origHTML = uninstallAllBtn.innerHTML;
uninstallAllBtn.disabled = true;
msg.innerHTML = '<i class="fas fa-circle-notch fa-spin text-red-600 mr-2"></i><span class="text-gray-700">Удаление всех контейнеров...</span>';
uninstallAllBtn.innerHTML = '<i class="fas fa-circle-notch fa-spin mr-1"></i>Удаление...';
uninstallAllBtn.className = uninstallAllBtn.className.replace('bg-gray-600', 'bg-yellow-600');
uninstallAllBtn.style.cursor = 'wait';
msg.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fef3c7;border:1px solid #f59e0b;border-radius:6px;margin-top:4px;">' +
'<i class="fas fa-circle-notch fa-spin text-yellow-600"></i>' +
'<span style="color:#92400e;font-weight:500;">Удаление всех протоколов... Это может занять минуту</span></div>';
try {
const res = await fetch(`/servers/{{ server.id }}/protocols/uninstall-all`, { method: 'POST', credentials: 'same-origin' });
const data = await res.json();
console.log('Response:', data);
if (data.success) {
msg.textContent = data.message || 'Успешно';
uninstallAllBtn.innerHTML = '<i class="fas fa-check mr-1"></i>Удалено';
uninstallAllBtn.className = uninstallAllBtn.className.replace('bg-yellow-600', 'bg-green-600');
msg.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#d1fae5;border:1px solid #10b981;border-radius:6px;margin-top:4px;">' +
'<i class="fas fa-check-circle text-green-600"></i>' +
'<span style="color:#065f46;font-weight:500;">' + (data.message || 'Все протоколы удалены') + '</span></div>';
setTimeout(() => location.reload(), 1200);
} else {
msg.textContent = data.error || 'Ошибка';
uninstallAllBtn.innerHTML = origHTML;
uninstallAllBtn.className = uninstallAllBtn.className.replace('bg-yellow-600', 'bg-gray-600');
msg.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fee2e2;border:1px solid #ef4444;border-radius:6px;margin-top:4px;">' +
'<i class="fas fa-exclamation-circle text-red-600"></i>' +
'<span style="color:#991b1b;">' + (data.error || 'Ошибка') + '</span></div>';
}
} catch (e) {
console.error('Error:', e);
msg.textContent = e.message;
uninstallAllBtn.innerHTML = origHTML;
uninstallAllBtn.className = uninstallAllBtn.className.replace('bg-yellow-600', 'bg-gray-600');
msg.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fee2e2;border:1px solid #ef4444;border-radius:6px;margin-top:4px;">' +
'<i class="fas fa-exclamation-circle text-red-600"></i>' +
'<span style="color:#991b1b;">' + (e.message || 'Ошибка связи') + '</span></div>';
}
uninstallAllBtn.disabled = false;
});
@@ -481,24 +547,71 @@ document.addEventListener('DOMContentLoaded', function() {
if (!confirmed) return;
const slug = btn.getAttribute('data-slug');
const m = document.getElementById('uninstallSpMsg');
m.textContent = '';
btn.disabled = true;
m.innerHTML = '<i class="fas fa-circle-notch fa-spin text-red-600 mr-2"></i><span class="text-gray-700">Удаление протокола...</span>';
const card = btn.closest('.border.rounded');
// Save original button state
const origHTML = btn.innerHTML;
const origClasses = btn.className;
// Disable ALL uninstall buttons
document.querySelectorAll('.btn-uninstall-sp').forEach(b => { b.disabled = true; b.style.opacity = '0.5'; });
// Animate the clicked button
btn.style.opacity = '1';
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin mr-1"></i>Удаление...';
btn.className = btn.className.replace('bg-red-600', 'bg-yellow-600');
btn.style.cursor = 'wait';
btn.style.minWidth = btn.offsetWidth + 'px';
// Add overlay to protocol card
if (card) {
card.style.position = 'relative';
const overlay = document.createElement('div');
overlay.id = 'uninstall-overlay-' + slug;
overlay.style.cssText = 'position:absolute;inset:0;background:rgba(239,68,68,0.05);border-radius:inherit;pointer-events:none;z-index:5;';
card.appendChild(overlay);
card.style.transition = 'opacity 0.3s';
card.style.opacity = '0.7';
}
// Show progress message
m.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fef3c7;border:1px solid #f59e0b;border-radius:6px;margin-top:8px;">' +
'<i class="fas fa-circle-notch fa-spin text-yellow-600"></i>' +
'<span style="color:#92400e;font-weight:500;">Удаление <b>' + slug + '</b>... Это может занять до 30 секунд</span></div>';
try {
const resp = await fetch('/servers/{{ server.id }}/protocols/' + encodeURIComponent(slug) + '/uninstall', { method: 'POST', credentials: 'same-origin' });
let data;
const ct = resp.headers.get('content-type') || '';
if (ct.includes('application/json')) { data = await resp.json(); } else { data = { error: await resp.text() }; }
if (resp.ok && data && !data.error) {
m.textContent = 'Удалено. Клиенты: ' + (data.clients_removed || 0);
setTimeout(() => location.reload(), 800);
// Success state
btn.innerHTML = '<i class="fas fa-check mr-1"></i>Удалено';
btn.className = btn.className.replace('bg-yellow-600', 'bg-green-600');
if (card) { card.style.opacity = '0.4'; }
m.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#d1fae5;border:1px solid #10b981;border-radius:6px;margin-top:8px;">' +
'<i class="fas fa-check-circle text-green-600"></i>' +
'<span style="color:#065f46;font-weight:500;">Протокол удалён. Клиенты: ' + (data.clients_removed || 0) + '</span></div>';
setTimeout(() => location.reload(), 1200);
} else {
m.textContent = (data && data.error) ? data.error : ('Ошибка удаления (' + resp.status + ')');
// Error state
btn.innerHTML = '<i class="fas fa-times mr-1"></i>Ошибка';
btn.className = origClasses;
if (card) { card.style.opacity = '1'; const ov = document.getElementById('uninstall-overlay-' + slug); if (ov) ov.remove(); }
m.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fee2e2;border:1px solid #ef4444;border-radius:6px;margin-top:8px;">' +
'<i class="fas fa-exclamation-circle text-red-600"></i>' +
'<span style="color:#991b1b;">' + ((data && data.error) ? data.error : ('Ошибка (' + resp.status + ')')) + '</span></div>';
setTimeout(() => { btn.innerHTML = origHTML; document.querySelectorAll('.btn-uninstall-sp').forEach(b => { b.disabled = false; b.style.opacity = '1'; }); }, 3000);
}
} catch (e) {
m.textContent = e.message || 'Ошибка связи';
} catch (err) {
btn.innerHTML = '<i class="fas fa-times mr-1"></i>Ошибка';
btn.className = origClasses;
if (card) { card.style.opacity = '1'; const ov = document.getElementById('uninstall-overlay-' + slug); if (ov) ov.remove(); }
m.innerHTML = '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fee2e2;border:1px solid #ef4444;border-radius:6px;margin-top:8px;">' +
'<i class="fas fa-exclamation-circle text-red-600"></i>' +
'<span style="color:#991b1b;">' + (err.message || 'Ошибка связи') + '</span></div>';
setTimeout(() => { btn.innerHTML = origHTML; document.querySelectorAll('.btn-uninstall-sp').forEach(b => { b.disabled = false; b.style.opacity = '1'; }); }, 3000);
}
btn.disabled = false;
});
});
@@ -568,6 +681,92 @@ async function syncAllStats(serverId) {
}
}
// ── WARP Status Widget ──
function updateWarpWidget(data) {
const badge = document.getElementById('warpStatusBadge');
const proxy = document.getElementById('warpProxy');
const exitIp = document.getElementById('warpExitIp');
const mode = document.getElementById('warpMode');
const svc = document.getElementById('warpSvc');
const btnConnect = document.getElementById('warpBtnConnect');
const btnDisconnect = document.getElementById('warpBtnDisconnect');
const btnReconnect = document.getElementById('warpBtnReconnect');
if (!badge) return;
if (!data.installed) {
badge.innerHTML = '<span class="px-2 py-0.5 rounded text-xs bg-gray-600 text-gray-300">Не установлен</span>';
return;
}
if (data.connected) {
badge.innerHTML = '<span class="px-2 py-0.5 rounded text-xs text-white" style="background:#22c55e"><i class="fas fa-check-circle mr-1"></i>Connected</span>';
} else {
badge.innerHTML = '<span class="px-2 py-0.5 rounded text-xs text-white" style="background:#ef4444"><i class="fas fa-times-circle mr-1"></i>Disconnected</span>';
}
if (proxy) proxy.textContent = data.proxy_listening ? ('socks5h://127.0.0.1:' + (data.proxy_port || 40000)) : '—';
if (exitIp) exitIp.textContent = data.warp_ip || '—';
if (mode) mode.textContent = data.mode || '—';
if (svc) svc.textContent = data.service_status || '—';
if (btnConnect) { btnConnect.disabled = false; }
if (btnDisconnect) { btnDisconnect.disabled = false; }
if (btnReconnect) { btnReconnect.disabled = false; }
}
async function loadWarpStatus() {
const widget = document.getElementById('warpStatusWidget');
if (!widget) return;
try {
const res = await fetch('/servers/{{ server.id }}/warp/status', { credentials: 'same-origin' });
const data = await res.json();
if (data.success !== false) {
updateWarpWidget(data);
}
} catch (e) {
console.error('WARP status error:', e);
}
}
async function warpAction(action) {
const btnConnect = document.getElementById('warpBtnConnect');
const btnDisconnect = document.getElementById('warpBtnDisconnect');
const btnReconnect = document.getElementById('warpBtnReconnect');
if (btnConnect) btnConnect.disabled = true;
if (btnDisconnect) btnDisconnect.disabled = true;
if (btnReconnect) btnReconnect.disabled = true;
const badge = document.getElementById('warpStatusBadge');
if (badge) badge.innerHTML = '<span class="px-2 py-0.5 rounded text-xs bg-gray-600 text-gray-300"><i class="fas fa-spinner fa-spin mr-1"></i>' + action + '...</span>';
try {
const res = await fetch('/servers/{{ server.id }}/warp/action', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: action })
});
const data = await res.json();
if (data.success !== false) {
updateWarpWidget(data);
} else {
if (badge) badge.innerHTML = '<span class="px-2 py-0.5 rounded text-xs bg-red-600 text-white">' + (data.error || 'Ошибка') + '</span>';
}
} catch (e) {
if (badge) badge.innerHTML = '<span class="px-2 py-0.5 rounded text-xs bg-red-600 text-white">' + e.message + '</span>';
}
if (btnConnect) btnConnect.disabled = false;
if (btnDisconnect) btnDisconnect.disabled = false;
if (btnReconnect) btnReconnect.disabled = false;
}
// WARP auto-refresh
document.addEventListener('DOMContentLoaded', function() {
loadWarpStatus();
setInterval(loadWarpStatus, 30000);
});
// Load backups on page load
document.addEventListener('DOMContentLoaded', function() {
loadBackups({{ server.id }});