From 809b0ca63df6c74ed91f966c7f672d4d0f4717fc Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Sat, 25 Apr 2026 10:40:21 +0300 Subject: [PATCH] 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. --- README.md | 14 + README_RU.md | 409 ++++++++++ README_ZH.md | 409 ++++++++++ RELEASE_NOTES.md | 224 ++++++ inc/InstallProtocolManager.php | 748 +++++++++++++++--- .../066_add_cloudflare_warp_protocol.sql | 296 +++++++ .../067_warp_auto_redsocks_integration.sql | 402 ++++++++++ migrations/068_fix_warp_heredoc_compat.sql | 246 ++++++ migrations/069_warp_aivpn_subnet_detect.sql | 237 ++++++ public/index.php | 65 ++ templates/servers/view.twig | 241 +++++- 11 files changed, 3178 insertions(+), 113 deletions(-) create mode 100644 README_RU.md create mode 100644 README_ZH.md create mode 100644 RELEASE_NOTES.md create mode 100644 migrations/066_add_cloudflare_warp_protocol.sql create mode 100644 migrations/067_warp_auto_redsocks_integration.sql create mode 100644 migrations/068_fix_warp_heredoc_compat.sql create mode 100644 migrations/069_warp_aivpn_subnet_detect.sql diff --git a/README.md b/README.md index a224ea1..4287f75 100644 --- a/README.md +++ b/README.md @@ -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**: diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 0000000..328de88 --- /dev/null +++ b/README_RU.md @@ -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 " \ + -d '{"expires_at": "2025-12-31 23:59:59"}' + +# Продлить на 30 дней +curl -X POST http://localhost:8082/api/clients/123/extend \ + -H "Authorization: Bearer " \ + -d '{"days": 30}' + +# Получить клиентов, у которых скоро истекает срок (в течение 7 дней) +curl http://localhost:8082/api/clients/expiring?days=7 \ + -H "Authorization: Bearer " +``` + +### Управление лимитами трафика + +Установите и отслеживайте лимиты трафика через UI или API: +```bash +# Установить лимит трафика (10 ГБ = 10737418240 байт) +curl -X POST http://localhost:8082/api/clients/123/set-traffic-limit \ + -H "Authorization: Bearer " \ + -d '{"limit_bytes": 10737418240}' + +# Удалить лимит трафика (установить безлимитный) +curl -X POST http://localhost:8082/api/clients/123/set-traffic-limit \ + -H "Authorization: Bearer " \ + -d '{"limit_bytes": null}' + +# Проверить статус лимита трафика +curl http://localhost:8082/api/clients/123/traffic-limit-status \ + -H "Authorization: Bearer " + +# Получить клиентов, превысивших лимит трафика +curl http://localhost:8082/api/clients/overlimit \ + -H "Authorization: Bearer " +``` + +### Резервное копирование серверов + +Создавайте и восстанавливайте резервные копии через UI или API: +```bash +# Создать резервную копию +curl -X POST http://localhost:8082/api/servers/1/backup \ + -H "Authorization: Bearer " + +# Список резервных копий +curl http://localhost:8082/api/servers/1/backups \ + -H "Authorization: Bearer " + +# Восстановить из резервной копии +curl -X POST http://localhost:8082/api/servers/1/restore \ + -H "Authorization: Bearer " \ + -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 " \ + 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 \ No newline at end of file diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000..d17c955 --- /dev/null +++ b/README_ZH.md @@ -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 " \ + -d '{"expires_at": "2025-12-31 23:59:59"}' + +# 延长 30 天 +curl -X POST http://localhost:8082/api/clients/123/extend \ + -H "Authorization: Bearer " \ + -d '{"days": 30}' + +# 获取即将过期的客户端(7 天内) +curl http://localhost:8082/api/clients/expiring?days=7 \ + -H "Authorization: Bearer " +``` + +### 管理流量限制 + +通过 UI 或 API 设置和监控流量限制: +```bash +# 设置流量限制(10 GB = 10737418240 字节) +curl -X POST http://localhost:8082/api/clients/123/set-traffic-limit \ + -H "Authorization: Bearer " \ + -d '{"limit_bytes": 10737418240}' + +# 移除流量限制(设置为无限制) +curl -X POST http://localhost:8082/api/clients/123/set-traffic-limit \ + -H "Authorization: Bearer " \ + -d '{"limit_bytes": null}' + +# 检查流量限制状态 +curl http://localhost:8082/api/clients/123/traffic-limit-status \ + -H "Authorization: Bearer " + +# 获取超过流量限制的客户端 +curl http://localhost:8082/api/clients/overlimit \ + -H "Authorization: Bearer " +``` + +### 服务器备份 + +通过 UI 或 API 创建和恢复备份: +```bash +# 创建备份 +curl -X POST http://localhost:8082/api/servers/1/backup \ + -H "Authorization: Bearer " + +# 列出备份 +curl http://localhost:8082/api/servers/1/backups \ + -H "Authorization: Bearer " + +# 从备份恢复 +curl -X POST http://localhost:8082/api/servers/1/restore \ + -H "Authorization: Bearer " \ + -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 " \ + 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_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 - 获取服务器导入历史记录 +``` + +## 翻译 + +在设置中添加 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 \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..8c7eaee --- /dev/null +++ b/RELEASE_NOTES.md @@ -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 \ No newline at end of file diff --git a/inc/InstallProtocolManager.php b/inc/InstallProtocolManager.php index e6971f9..9fb68d3 100644 --- a/inc/InstallProtocolManager.php +++ b/inc/InstallProtocolManager.php @@ -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 + } + } } diff --git a/migrations/066_add_cloudflare_warp_protocol.sql b/migrations/066_add_cloudflare_warp_protocol.sql new file mode 100644 index 0000000..6bb2d93 --- /dev/null +++ b/migrations/066_add_cloudflare_warp_protocol.sql @@ -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); diff --git a/migrations/067_warp_auto_redsocks_integration.sql b/migrations/067_warp_auto_redsocks_integration.sql new file mode 100644 index 0000000..f80f7c3 --- /dev/null +++ b/migrations/067_warp_auto_redsocks_integration.sql @@ -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'); diff --git a/migrations/068_fix_warp_heredoc_compat.sql b/migrations/068_fix_warp_heredoc_compat.sql new file mode 100644 index 0000000..e7c3132 --- /dev/null +++ b/migrations/068_fix_warp_heredoc_compat.sql @@ -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'; diff --git a/migrations/069_warp_aivpn_subnet_detect.sql b/migrations/069_warp_aivpn_subnet_detect.sql new file mode 100644 index 0000000..860f44e --- /dev/null +++ b/migrations/069_warp_aivpn_subnet_detect.sql @@ -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'; diff --git a/public/index.php b/public/index.php index 651b356..fbf9893 100644 --- a/public/index.php +++ b/public/index.php @@ -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(); diff --git a/templates/servers/view.twig b/templates/servers/view.twig index 0bc3cc7..8a98905 100644 --- a/templates/servers/view.twig +++ b/templates/servers/view.twig @@ -98,6 +98,59 @@ {% if sp.server_host %}Host: {{ sp.server_host }}{% endif %} {% if sp.server_port %}Port: {{ sp.server_port }}{% endif %} + {% if sp.slug == 'cf-warp' %} + {# ── WARP Status Widget ── #} +
+
+
+ + + + Cloudflare WARP +
+
+ + Загрузка... + +
+
+ +
+
+ Прокси + +
+
+ WARP IP + +
+
+ Режим + +
+
+ Сервис + +
+
+ +
+ + + +
+ +

+ ⚠️ WARP ~50-100 МБ RAM • Цепочка: Клиент → WG → WARP → CF → Интернет +

+
+ {% endif %} {% else %}
Нет установленных протоколов
@@ -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 = 'Удаление всех контейнеров...'; + uninstallAllBtn.innerHTML = 'Удаление...'; + uninstallAllBtn.className = uninstallAllBtn.className.replace('bg-gray-600', 'bg-yellow-600'); + uninstallAllBtn.style.cursor = 'wait'; + msg.innerHTML = '
' + + '' + + 'Удаление всех протоколов... Это может занять минуту
'; + 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 = 'Удалено'; + uninstallAllBtn.className = uninstallAllBtn.className.replace('bg-yellow-600', 'bg-green-600'); + msg.innerHTML = '
' + + '' + + '' + (data.message || 'Все протоколы удалены') + '
'; 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 = '
' + + '' + + '' + (data.error || 'Ошибка') + '
'; } } 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 = '
' + + '' + + '' + (e.message || 'Ошибка связи') + '
'; } 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 = 'Удаление протокола...'; + 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 = 'Удаление...'; + 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 = '
' + + '' + + 'Удаление ' + slug + '... Это может занять до 30 секунд
'; + 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 = 'Удалено'; + btn.className = btn.className.replace('bg-yellow-600', 'bg-green-600'); + if (card) { card.style.opacity = '0.4'; } + m.innerHTML = '
' + + '' + + 'Протокол удалён. Клиенты: ' + (data.clients_removed || 0) + '
'; + setTimeout(() => location.reload(), 1200); } else { - m.textContent = (data && data.error) ? data.error : ('Ошибка удаления (' + resp.status + ')'); + // Error state + btn.innerHTML = 'Ошибка'; + btn.className = origClasses; + if (card) { card.style.opacity = '1'; const ov = document.getElementById('uninstall-overlay-' + slug); if (ov) ov.remove(); } + m.innerHTML = '
' + + '' + + '' + ((data && data.error) ? data.error : ('Ошибка (' + resp.status + ')')) + '
'; + 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 = 'Ошибка'; + btn.className = origClasses; + if (card) { card.style.opacity = '1'; const ov = document.getElementById('uninstall-overlay-' + slug); if (ov) ov.remove(); } + m.innerHTML = '
' + + '' + + '' + (err.message || 'Ошибка связи') + '
'; + 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 = 'Не установлен'; + return; + } + + if (data.connected) { + badge.innerHTML = 'Connected'; + } else { + badge.innerHTML = 'Disconnected'; + } + + 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 = '' + action + '...'; + + 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 = '' + (data.error || 'Ошибка') + ''; + } + } catch (e) { + if (badge) badge.innerHTML = '' + e.message + ''; + } + 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 }});