diff --git a/README.md b/README.md index 5c7d5be..9b8d9d8 100644 --- a/README.md +++ b/README.md @@ -94,12 +94,12 @@ PCATELEGRAM_WEB_ADMIN_PASSWORD='strong-password' bash bootstrap.sh - `Off` — WARP выключен. - `WARP` — обычный Cloudflare WARP. - `WARP+` — WARP+ с license key. -- `All clients` — применяет WARP на весь proxy-трафик через `warp-cli`, если Cloudflare WARP установлен на сервере. -- `One client` — сохраняет WARP/WARP+ профиль и key для выбранного клиента в `/opt/pcatelegram_web/warp.json`. +- `All clients` — если `warp-cli` не установлен, web-admin ставит `cloudflare-warp`, затем применяет WARP на весь proxy-трафик через `warp-cli`. +- `One client` — если `warp-cli` не установлен, web-admin ставит `cloudflare-warp`, затем сохраняет WARP/WARP+ профиль и key для выбранного клиента в `/opt/pcatelegram_web/warp.json`. WARP+ key не отдается в API целиком: web показывает только маску. Файл `warp.json` хранится с правами `0600` и входит в backup. -Для реального global WARP на сервере нужен установленный `cloudflare-warp` и доступная команда `warp-cli`. По документации Cloudflare: регистрация `warp-cli registration new`, WARP+ key `warp-cli registration license `, подключение `warp-cli connect`. +Для real global WARP web-admin использует официальный Cloudflare Linux repo: добавляет GPG key, repo `pkg.cloudflareclient.com`, ставит пакет `cloudflare-warp`, затем выполняет регистрацию `warp-cli registration new`, WARP+ key `warp-cli registration license `, подключение `warp-cli connect`. Per-client runtime routing в текущем telemt не включается автоматически: публичные параметры telemt дают users/limits/quotas/ad tags, но не документируют привязку upstream к конкретному user. Для настоящего WARP только одному клиенту нужен отдельный telemt route/service или upstream-схема. diff --git a/admin-web/server.py b/admin-web/server.py index ad7a566..8f4066f 100644 --- a/admin-web/server.py +++ b/admin-web/server.py @@ -604,15 +604,74 @@ def user_warp_payload(name: str) -> dict[str, Any]: } +def install_warp_cli() -> dict[str, Any]: + result: dict[str, Any] = {"attempted": False, "installed": bool(shutil.which("warp-cli")), "commands": [], "warnings": []} + if result["installed"]: + return result + if os.geteuid() != 0: + result["warnings"].append("root required to install cloudflare-warp") + return result + + def call(label: str, cmd: list[str], timeout: int = 120) -> tuple[int, str, str]: + result["attempted"] = True + code, out, err = run(cmd, timeout=timeout) + result["commands"].append({"cmd": label, "exit_code": code}) + return code, out, err + + apt_get = shutil.which("apt-get") + dnf = shutil.which("dnf") + yum = shutil.which("yum") + + if apt_get: + script = r""" +set -e +export DEBIAN_FRONTEND=noninteractive +apt-get update +apt-get install -y curl gpg lsb-release ca-certificates +install -d -m 0755 /usr/share/keyrings +curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg +echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/cloudflare-client.list +apt-get update +apt-get install -y cloudflare-warp +""" + code, _, err = call("install cloudflare-warp via apt", ["bash", "-lc", script], timeout=420) + if code != 0: + result["warnings"].append(err.strip() or "apt install failed") + elif dnf or yum: + manager = dnf or yum + repo_url = "https://pkg.cloudflareclient.com/cloudflare-warp-ascii.repo" + script = f""" +set -e +rpm --import https://pkg.cloudflareclient.com/pubkey.gpg || true +curl -fsSL {shlex.quote(repo_url)} > /etc/yum.repos.d/cloudflare-warp.repo +{shlex.quote(manager)} install -y cloudflare-warp +""" + code, _, err = call("install cloudflare-warp via rpm repo", ["bash", "-lc", script], timeout=420) + if code != 0: + result["warnings"].append(err.strip() or "rpm install failed") + else: + result["warnings"].append("supported package manager not found") + + result["installed"] = bool(shutil.which("warp-cli")) + if not result["installed"] and not result["warnings"]: + result["warnings"].append("warp-cli still not available after install") + return result + + def apply_warp_runtime(cfg: dict[str, Any]) -> dict[str, Any]: warp_cli = shutil.which("warp-cli") - result: dict[str, Any] = {"applied": False, "commands": [], "warnings": []} + result: dict[str, Any] = {"applied": False, "install": {"attempted": False, "installed": bool(warp_cli)}, "commands": [], "warnings": []} + if cfg["enabled"] and cfg["mode"] != "off" and not warp_cli: + install_result = install_warp_cli() + result["install"] = install_result + result["warnings"].extend(install_result.get("warnings", [])) + warp_cli = shutil.which("warp-cli") + if (not cfg["enabled"] or cfg["mode"] == "off") and not warp_cli: + result["applied"] = True + return result if not warp_cli: result["warnings"].append("warp-cli not installed") return result - if cfg["scope"] == "user": - result["warnings"].append("per-user WARP route saved only; telemt per-user upstream routing is not documented") - return result def call(args: list[str], timeout: int = 20) -> tuple[int, str, str]: code, out, err = run([warp_cli, *args], timeout=timeout) @@ -627,6 +686,10 @@ def apply_warp_runtime(cfg: dict[str, Any]) -> dict[str, Any]: result["applied"] = True return result + if cfg["scope"] == "user": + result["warnings"].append("per-user WARP route saved only; telemt per-user upstream routing is not documented") + return result + run(["systemctl", "enable", "--now", "warp-svc"], timeout=20) code, _, _ = call(["registration", "show"], timeout=10) if code != 0: diff --git a/admin-web/static/app.js b/admin-web/static/app.js index 6edef13..5e15015 100644 --- a/admin-web/static/app.js +++ b/admin-web/static/app.js @@ -118,9 +118,9 @@ const i18n = { warpSave: "Save WARP", warpSaved: "WARP settings saved", warpInstalled: "warp-cli installed", - warpNotInstalled: "warp-cli not installed", - warpPerUserNote: "One-client WARP profile is stored here. Runtime per-client routing needs a dedicated telemt route; global WARP applies to all clients.", - warpAllNote: "Global WARP applies to all proxy traffic when warp-cli is installed and connected.", + warpNotInstalled: "will install on save", + warpPerUserNote: "One-client WARP profile is stored here. If warp-cli is missing, it will be installed on save. Runtime per-client routing needs a dedicated telemt route; global WARP applies to all clients.", + warpAllNote: "Global WARP applies to all proxy traffic. If warp-cli is missing, it will be installed on save, then connected.", savedKey: "saved key", dashboard: "Dashboard", noKeys: "No keys yet", @@ -362,9 +362,9 @@ const i18n = { warpSave: "Сохранить WARP", warpSaved: "Настройки WARP сохранены", warpInstalled: "warp-cli установлен", - warpNotInstalled: "warp-cli не установлен", - warpPerUserNote: "Профиль WARP для одного клиента сохраняется здесь. Runtime-маршрут на одного клиента требует отдельный маршрут telemt; global WARP действует на всех клиентов.", - warpAllNote: "Global WARP применится ко всему proxy-трафику, если warp-cli установлен и подключён.", + warpNotInstalled: "установится при сохранении", + warpPerUserNote: "Профиль WARP для одного клиента сохраняется здесь. Если warp-cli нет, он установится при сохранении. Runtime-маршрут на одного клиента требует отдельный маршрут telemt; global WARP действует на всех клиентов.", + warpAllNote: "Global WARP применится ко всему proxy-трафику. Если warp-cli нет, он установится при сохранении и затем подключится.", savedKey: "ключ сохранён", dashboard: "Обзор", noKeys: "Ключей пока нет", diff --git a/admin-web/static/index.html b/admin-web/static/index.html index cf34138..1c2e710 100644 --- a/admin-web/static/index.html +++ b/admin-web/static/index.html @@ -440,6 +440,6 @@ - +