From 62f40f92fb08afdab92d0eea0d697e5af7ca8712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Thu, 14 May 2026 20:18:23 +0300 Subject: [PATCH] feat(warp): host install/uninstall via panel SSH + clarify status POST /api/warp/host-setup runs warp-amnezia.sh on VPS root SSH. UI buttons when sshpass available; explain missing warp.conf status. Co-authored-by: Cursor --- README.md | 9 +++-- docs/panel-guide.md | 1 + package.json | 2 +- public/app.js | 99 +++++++++++++++++++++++++++++++++++++++++++-- public/index.html | 16 +++++++- scripts/install.sh | 6 +-- server.js | 47 +++++++++++++++++++++ 7 files changed, 169 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 289a75a..12e59b0 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas | `WARP_CONF_PATH` | `{WARP_DIR}/warp.conf` | Нестандартный путь к конфигу WARP | | `WARP_CLIENTS_LIST` | `{WARP_DIR}/clients.list` | Список строк `10.8.x.x/32` — кому маршрутизировать трафик через интерфейс `warp` | | `AMNEZIA_START_SCRIPT` | `/opt/amnezia/start.sh` | Куда встраивается блок автоподъёма WARP после перезапуска (маркеры совместимы с прежним `warp-manager`) | +| `WARP_SSH_INSTALL_DIR` | `/opt/amnezia-admin` | Каталог на **хосте VPS**, откуда по SSH выполняется `scripts/warp-amnezia.sh` при установке/удалении WARP из панели (`POST /api/warp/host-setup`) | | `SCHEDULE_DISCONNECT_MS` | `60000` | Как часто планировщик проверяет отложенное отключение из туннеля (мс) | | `TZ` | _(часто UTC в Docker)_ | Пояс строки «Сервер» в панели (IANA, например `Europe/Berlin`). Без `TZ` берётся из образа (часто UTC) — тогда от браузера будет видна разница часов | | `TIME_SYNC_SSH_HOST` | `172.17.0.1` | Хост для SSH root при синхронизации времени из панели (часто шлюз Docker к хосту) | @@ -108,10 +109,12 @@ curl -fsSL https://raw.githubusercontent.com/andrey271192/Amnezia_web/main/scrip ### Cloudflare WARP (необязательно) -**Устанавливать не обязательно.** Панель и обычный AmneziaWG работают без WARP. Раздел **Cloudflare WARP** в веб-интерфейсе нужен только если вы хотите, чтобы **выбранные** клиенты (с IPv4 `10.8.x.x/32` в AllowedIPs) выходили в интернет через туннель Cloudflare внутри контейнера AWG. +**Устанавливать не обязательно.** Панель и обычный AmneziaWG работают без WARP. Статус **«Не установлен»** значит: в контейнере AWG ещё **нет** `warp.conf` (скрипт на VPS не запускали) — это не ошибка. + +Раздел **Cloudflare WARP** в веб-интерфейсе нужен только если вы хотите, чтобы **выбранные** клиенты (с IPv4 `10.8.x.x/32` в AllowedIPs) выходили в интернет через туннель Cloudflare внутри контейнера AWG. - **Не нужен WARP** — ничего на хосте не запускайте. В панели будет статус **«Не установлен»** — это нормально, блок можно не трогать. -- **Нужен WARP** — один раз на хосте VPS (root), из каталога с репозиторием (часто `/opt/amnezia-admin`): +- **Нужен WARP** — в панели кнопка **«Установить WARP на VPS»** (пароль root по SSH, как у синхронизации времени хоста; нужны `sshpass` в образе панели и не отключённый `TIME_SYNC_DISABLED`) **или** один раз на хосте VPS (root), из каталога репозитория (часто `/opt/amnezia-admin`): ```bash cd /opt/amnezia-admin @@ -128,7 +131,7 @@ chmod +x scripts/warp-amnezia.sh Подкоманды: `install`, `start`, `stop`, `status`, `rekey`, **`uninstall`**. Учёт wgcf на хосте: `/root/wgcf-account.toml` (и бинарник `/root/wgcf`, если скачан). -**Полностью убрать WARP** после установки (интерфейс `warp`, правила маршрутизации/NAT, блок `# --- WARP-MANAGER ---` в `start.sh`, `warp.conf` и список клиентов в каталоге WARP в контейнере; затем перезапуск контейнера AWG): +**Полностью убрать WARP** — кнопка **«Удалить WARP с VPS»** в веб-панели (SSH с паролем root, как при установке) **или** на хосте вручную (интерфейс `warp`, правила маршрутизации/NAT, блок `# --- WARP-MANAGER ---` в `start.sh`, `warp.conf` и список клиентов в каталоге WARP в контейнере; затем перезапуск контейнера AWG): ```bash cd /opt/amnezia-admin diff --git a/docs/panel-guide.md b/docs/panel-guide.md index de67c91..aa898ef 100644 --- a/docs/panel-guide.md +++ b/docs/panel-guide.md @@ -82,6 +82,7 @@ | POST | `/api/clients/disconnect-date` | Даты отключения / расписание | | GET/POST | `/api/clients/export-config` | Скачать `.conf`; GET — прямая ссылка (сессия); опционально `?token=…` если задан `EXPORT_CONFIG_SECRET` | | POST | `/api/clients/create-cascade` | `{ "endpointHost", "endpointPort?", "tunnelIp?", "clientName?", "profileId?" }` — новый peer и файл `.conf` | +| POST | `/api/warp/host-setup` | Установка/удаление WARP на хосте по SSH: `{ "rootPassword", "cmd": "install" \| "uninstall" }` (как синхронизация времени; каталог скрипта — `WARP_SSH_INSTALL_DIR`) | | POST | `/api/warp/start` | Поднять WARP | | POST | `/api/warp/stop` | Остановить WARP | | POST | `/api/warp/routing` | Политика по клиентам | diff --git a/package.json b/package.json index 6b443d0..eb82fbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amnezia-admin", - "version": "1.1.6", + "version": "1.1.7", "private": true, "description": "Web-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)", "license": "MIT", diff --git a/public/app.js b/public/app.js index ccb9975..85f7770 100644 --- a/public/app.js +++ b/public/app.js @@ -47,6 +47,16 @@ const dtExtra = document.querySelector("#dt-dialog-extra"); const dtHint = document.querySelector("#dt-dialog-hint"); const dtScheduleTunnel = document.querySelector("#dt-dialog-schedule-tunnel"); +const warpSshDialog = document.querySelector("#warp-ssh-dialog"); +const warpSshTitle = document.querySelector("#warp-ssh-title"); +const warpSshLead = document.querySelector("#warp-ssh-lead"); +const warpSshPw = document.querySelector("#warp-ssh-pw"); +const warpSshCancel = document.querySelector("#warp-ssh-cancel"); +const warpSshOk = document.querySelector("#warp-ssh-ok"); +const warpSshErr = document.querySelector("#warp-ssh-err"); +/** @type {"install" | "uninstall" | null} */ +let warpSshPendingCmd = null; + /** Какие панели скрыты настройкой сервера (`UI_HIDE_SECTIONS`). */ let uiHidden = { users: false, warp: false, cascade: false }; @@ -165,6 +175,61 @@ dtOk.addEventListener("click", async () => { } }); +function openWarpHostSetup(cmd) { + if (!warpSshDialog || !warpSshTitle || !warpSshLead || !warpSshPw || !warpSshOk || !warpSshErr) return; + warpSshPendingCmd = cmd; + warpSshErr.textContent = ""; + warpSshPw.value = ""; + if (cmd === "install") { + warpSshTitle.textContent = "Установить Cloudflare WARP"; + warpSshLead.textContent = + "На хосте VPS выполнится scripts/warp-amnezia.sh install для контейнера текущего инстанса. SSH так же, как у блока синхронизации времени (TIME_SYNC_SSH_HOST, часто 172.17.0.1). Пароль root не сохраняется."; + warpSshOk.textContent = "Установить"; + } else { + warpSshTitle.textContent = "Удалить Cloudflare WARP"; + warpSshLead.textContent = + "На хосте выполнится scripts/warp-amnezia.sh uninstall (интерфейс warp, правила, автозапуск в start.sh; контейнер AWG перезапустится)."; + warpSshOk.textContent = "Удалить"; + } + warpSshDialog.showModal(); +} + +if (warpSshCancel && warpSshDialog) { + warpSshCancel.addEventListener("click", () => { + warpSshDialog.close(); + warpSshPendingCmd = null; + }); +} + +if (warpSshOk && warpSshDialog && warpSshPw) { + warpSshOk.addEventListener("click", async () => { + const cmd = warpSshPendingCmd; + if (!cmd) return; + const pw = warpSshPw.value; + if (!String(pw).trim()) { + warpSshErr.textContent = "Введите пароль root."; + return; + } + warpSshErr.textContent = ""; + try { + setStatus(cmd === "install" ? "Устанавливаю WARP на хосте VPS…" : "Удаляю WARP на хосте VPS…", false); + await api("/api/warp/host-setup", { + method: "POST", + body: JSON.stringify({ rootPassword: pw, cmd }), + }); + warpSshDialog.close(); + warpSshPendingCmd = null; + warpSshPw.value = ""; + setStatus("Готово.", false); + await loadClients(); + } catch (e) { + const msg = String(e.message || e); + warpSshErr.textContent = msg; + setStatus(msg, true); + } + }); +} + function showLogin() { stopClocks(); loginGate.classList.remove("hidden"); @@ -661,15 +726,40 @@ function renderWarpPanel(data) { if (!w.installed) { warpStatusLine.textContent = "Не установлен"; + const explain = document.createElement("p"); + explain.className = "muted warp-muted"; + explain.textContent = + "Так и должно быть, пока на VPS не создан файл warp.conf внутри контейнера AWG. После установки статус сменится; без WARP обычный AmneziaWG уже работает."; + warpActionsEl.appendChild(explain); + + if (w.hostSshInstall) { + warpActionsEl.appendChild( + btn("Установить WARP на VPS", "btn small primary", () => openWarpHostSetup("install")), + ); + const sshHint = document.createElement("p"); + sshHint.className = "muted warp-muted"; + sshHint.textContent = + "Кнопка запускает на хосте тот же скрипт, что в README; понадобится пароль root по SSH (не сохраняется). Альтернатива — команда вручную по SSH."; + warpActionsEl.appendChild(sshHint); + } + const hint = document.createElement("p"); hint.className = "muted warp-muted"; hint.innerHTML = - "Один раз на хосте (root): bash scripts/warp-amnezia.sh install — из каталога клона репозитория на VPS. Если контейнер не угадан автоматически: bash scripts/warp-amnezia.sh install amnezia-awg2."; + "Вручную на хосте (root), из каталога репозитория: bash scripts/warp-amnezia.sh install или с именем контейнера: bash scripts/warp-amnezia.sh install amnezia-awg2."; warpActionsEl.appendChild(hint); + + if (!w.hostSshInstall) { + const noBtn = document.createElement("p"); + noBtn.className = "muted warp-muted"; + noBtn.textContent = + "Кнопка установки с панели недоступна: нет sshpass в образе панели или включено TIME_SYNC_DISABLED=1 — используйте SSH вручную."; + warpActionsEl.appendChild(noBtn); + } + const skip = document.createElement("p"); skip.className = "muted warp-muted"; - skip.textContent = - "Если Cloudflare WARP не нужен, ничего не выполняйте — AmneziaWG и панель работают без него. Полное удаление: bash scripts/warp-amnezia.sh uninstall (см. README)."; + skip.textContent = "Если выход через Cloudflare не нужен, ничего не нажимайте — VPN уже работает без этого блока."; warpActionsEl.appendChild(skip); return; } @@ -771,6 +861,9 @@ function renderWarpPanel(data) { } }), ); + warpActionsEl.appendChild( + btn("Удалить WARP с VPS", "btn small warn", () => openWarpHostSetup("uninstall")), + ); } function escapeHtml(s) { diff --git a/public/index.html b/public/index.html index 50a2e42..002fa7b 100644 --- a/public/index.html +++ b/public/index.html @@ -103,7 +103,7 @@ Необязательно. Если достаточно обычного AmneziaWG без выхода части клиентов через Cloudflare — раздел можно не трогать (статус «Не установлен» — норма). Если WARP нужен: выход в интернет через Cloudflare для выбранных клиентов (правила маршрутизации и NAT в контейнере). Один раз на хосте VPS под root: в каталоге с репозиторием выполните bash scripts/warp-amnezia.sh install. - Затем отметьте клиентов ниже и нажмите «Применить» (контейнер AWG перезапустится). Убрать WARP полностью: bash scripts/warp-amnezia.sh uninstall — см. README. + Затем отметьте клиентов ниже и нажмите «Применить» (контейнер AWG перезапустится). Установить или удалить WARP с хоста можно кнопками ниже (пароль root по SSH — как при синхронизации времени). Убрать вручную: bash scripts/warp-amnezia.sh uninstall — см. README.

@@ -220,6 +220,20 @@ + +
+

WARP на хосте VPS

+

+ + +
+ + +
+ +
+
+ diff --git a/scripts/install.sh b/scripts/install.sh index 7ff337d..23f0c09 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -113,7 +113,7 @@ PREV_CONTAINER_ENV="" if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then PREV_CONTAINER_ENV="$(docker inspect "${CONTAINER_NAME}" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null || true)" fi -for __ui_var in UI_HIDE_SECTIONS UI_HIDE_USERS UI_HIDE_WARP UI_HIDE_CASCADE; do +for __ui_var in UI_HIDE_SECTIONS UI_HIDE_USERS UI_HIDE_WARP UI_HIDE_CASCADE WARP_SSH_INSTALL_DIR; do if [[ -z "${!__ui_var:-}" ]] && [[ -n "${PREV_CONTAINER_ENV}" ]]; then PREV_VAL="" while IFS= read -r __line; do @@ -160,7 +160,7 @@ if [[ -n "${TZ:-}" ]]; then RUN_ENV+=( -e "TZ=${TZ}" ) fi -for __warp_var in WARP_DIR WARP_CONF_PATH WARP_CLIENTS_LIST AMNEZIA_START_SCRIPT; do +for __warp_var in WARP_DIR WARP_CONF_PATH WARP_CLIENTS_LIST AMNEZIA_START_SCRIPT WARP_SSH_INSTALL_DIR; do if [[ -n "${!__warp_var:-}" ]]; then RUN_ENV+=( -e "${__warp_var}=${!__warp_var}" ) fi @@ -172,7 +172,7 @@ for __export_var in CLIENT_CONFIG_ENDPOINT CLIENT_EXPORT_DNS1 CLIENT_EXPORT_DNS2 fi done -for __ui_var in UI_HIDE_SECTIONS UI_HIDE_USERS UI_HIDE_WARP UI_HIDE_CASCADE; do +for __ui_var in UI_HIDE_SECTIONS UI_HIDE_USERS UI_HIDE_WARP UI_HIDE_CASCADE WARP_SSH_INSTALL_DIR; do if [[ -n "${!__ui_var:-}" ]]; then RUN_ENV+=( -e "${__ui_var}=${!__ui_var}" ) fi diff --git a/server.js b/server.js index 52931bd..836f288 100644 --- a/server.js +++ b/server.js @@ -1400,6 +1400,50 @@ app.post("/api/sync-host-time", requireAuth, async (req, res) => { } }); +app.post("/api/warp/host-setup", requireAuth, async (req, res) => { + if (UI_HIDDEN.warp) { + return res.status(403).json({ error: MSG_UI_WARP_OFF }); + } + if (!hostTimeSyncConfigured()) { + return res.status(503).json({ + error: + "С панели недоступно: в образе панели нет sshpass или задано TIME_SYNC_DISABLED=1. Запустите на хосте VPS вручную: bash /opt/amnezia-admin/scripts/warp-amnezia.sh install", + }); + } + const pw = req.body?.rootPassword; + const cmd = req.body?.cmd; + if (typeof pw !== "string" || !pw.trim()) { + return res.status(400).json({ error: "Укажите пароль root VPS" }); + } + if (cmd !== "install" && cmd !== "uninstall") { + return res.status(400).json({ error: "Ожидается cmd: install или uninstall" }); + } + const rt = runtimeForRequest(req); + const container = String(rt.profile.container || "").trim(); + if (!/^[a-zA-Z0-9_.-]+$/.test(container)) { + return res.status(400).json({ error: "Некорректное имя контейнера в профиле AWG" }); + } + let installDir = "/opt/amnezia-admin"; + try { + installDir = assertSafeUnixPath(process.env.WARP_SSH_INSTALL_DIR?.trim() || "/opt/amnezia-admin"); + } catch { + return res.status(500).json({ error: "Некорректная переменная WARP_SSH_INSTALL_DIR на сервере панели" }); + } + const host = process.env.TIME_SYNC_SSH_HOST?.trim() || "172.17.0.1"; + const remoteCmd = `bash -lc 'cd ${installDir} && chmod +x scripts/warp-amnezia.sh 2>/dev/null || true && AWG_CONTAINER=${container} ./scripts/warp-amnezia.sh ${cmd}'`; + try { + const out = await sshRootRun(pw.trim(), host, remoteCmd); + res.json({ ok: true, output: out.slice(0, 8000) }); + } catch (e) { + console.warn("warp host-setup:", e); + res.status(400).json({ + error: + String(e.message || e) || + "Не удалось выполнить по SSH. Проверьте пароль root, что вход root по паролю разрешён, переменную TIME_SYNC_SSH_HOST и наличие каталога со скриптом на хосте.", + }); + } +}); + app.post("/api/login", (req, res) => { const pw = req.body?.password; if (typeof pw !== "string" || !pw) { @@ -1520,6 +1564,9 @@ app.get("/api/clients", requireAuth, async (req, res) => { wgShowWarp: warpMeta.wgShowWarp || "", selectedAllowedIps: warpMeta.selectedAllowedIps, paths: warpMeta.paths, + hostSshInstall: hostTimeSyncConfigured(), + sshHost: process.env.TIME_SYNC_SSH_HOST?.trim() || "172.17.0.1", + installDir: process.env.WARP_SSH_INSTALL_DIR?.trim() || "/opt/amnezia-admin", }; res.json({ profileId: rt.profile.id,