mirror of
https://github.com/andrey271192/amnezia_web-PRO.git
synced 2026-09-20 14:42:00 +00:00
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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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` | Политика по клиентам |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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): <code class=\"inline\">bash scripts/warp-amnezia.sh install</code> — из каталога клона репозитория на VPS. Если контейнер не угадан автоматически: <code class=\"inline\">bash scripts/warp-amnezia.sh install amnezia-awg2</code>.";
|
||||
"Вручную на хосте (root), из каталога репозитория: <code class=\"inline\">bash scripts/warp-amnezia.sh install</code> или с именем контейнера: <code class=\"inline\">bash scripts/warp-amnezia.sh install amnezia-awg2</code>.";
|
||||
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) {
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<strong>Необязательно.</strong> Если достаточно обычного AmneziaWG без выхода части клиентов через Cloudflare — раздел можно не трогать (статус «Не установлен» — норма).
|
||||
Если WARP нужен: выход в интернет через Cloudflare для <em>выбранных</em> клиентов (правила маршрутизации и NAT в контейнере).
|
||||
Один раз на хосте VPS под root: в каталоге с репозиторием выполните <code class="inline">bash scripts/warp-amnezia.sh install</code>.
|
||||
Затем отметьте клиентов ниже и нажмите «Применить» (контейнер AWG перезапустится). Убрать WARP полностью: <code class="inline">bash scripts/warp-amnezia.sh uninstall</code> — см. README.
|
||||
Затем отметьте клиентов ниже и нажмите «Применить» (контейнер AWG перезапустится). Установить или удалить WARP с хоста можно кнопками ниже (пароль root по SSH — как при синхронизации времени). Убрать вручную: <code class="inline">bash scripts/warp-amnezia.sh uninstall</code> — см. README.
|
||||
</p>
|
||||
<div id="warp-actions" class="warp-actions"></div>
|
||||
<div id="warp-client-list" class="warp-client-list"></div>
|
||||
@@ -220,6 +220,20 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="warp-ssh-dialog" class="dt-dialog">
|
||||
<div class="dt-dialog-inner">
|
||||
<h3 id="warp-ssh-title">WARP на хосте VPS</h3>
|
||||
<p class="muted" id="warp-ssh-lead"></p>
|
||||
<label for="warp-ssh-pw">Пароль root на хосте VPS (не сохраняется)</label>
|
||||
<input id="warp-ssh-pw" type="password" autocomplete="off" class="clock-sync-input" />
|
||||
<div class="dt-dialog-actions">
|
||||
<button type="button" class="btn ghost" id="warp-ssh-cancel">Отмена</button>
|
||||
<button type="button" class="btn primary" id="warp-ssh-ok">Выполнить</button>
|
||||
</div>
|
||||
<p id="warp-ssh-err" class="status err" role="alert"></p>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script src="/app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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
|
||||
|
||||
47
server.js
47
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,
|
||||
|
||||
Reference in New Issue
Block a user