From 5451ca806a80abcf7f9d9279a9a1f432cc9d4f9e 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: Fri, 15 May 2026 04:25:11 +0300 Subject: [PATCH] fix(panel): harden WARP and MTProto routing - Re-apply WARP rules after container restart; wait for warp up - Drop stale WARP IPs from UI when no matching peer AllowedIPs - MTProto: status?withLogs=1, GET /api/mtproto/tail, richer /api/* 404 JSON - WARP intro + MTProto copy; bump patch to 1.2.24 Co-authored-by: Cursor --- package.json | 2 +- public/app.js | 52 +++++++++++++++++++++++++++-------- public/index.html | 3 +- server.js | 70 +++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 105 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index b089598..d170f65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amnezia-admin", - "version": "1.2.23", + "version": "1.2.24", "private": true, "description": "amnezia_web — базовая панель AmneziaWG (FREE: просмотр и удаление клиентов; полная версия — PRO).", "license": "MIT", diff --git a/public/app.js b/public/app.js index 80bb970..057d4d5 100644 --- a/public/app.js +++ b/public/app.js @@ -467,16 +467,23 @@ async function refreshMtprotoPanel() { } try { if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = "Загрузка логов…"; - const snap = await api("/api/mtproto/status"); - void (async () => { - try { - const lg = await api("/api/mtproto/logs"); - const tail = typeof lg?.logsTail === "string" ? lg.logsTail : ""; - if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = tail; - } catch { - if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = "(лог не загрузился — обновите раздел позже)"; - } - })(); + const snap = await api("/api/mtproto/status?withLogs=1"); + if (snap.logsFetched !== true && snap.exists === true && snap.running === true) { + void (async () => { + try { + const lg = (await api("/api/mtproto/tail").catch(() => + api("/api/mtproto/logs"), + )) || { logsTail: "" }; + const tail = typeof lg?.logsTail === "string" ? lg.logsTail : ""; + if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = tail; + } catch { + if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = "(лог не загрузился — обновите раздел позже)"; + } + })(); + } else { + const tail = typeof snap.logsTail === "string" ? snap.logsTail : ""; + if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = tail; + } if (mtprotoCalloutEl) { if (snap.exists) { @@ -805,6 +812,8 @@ function setPwMsg(text, isErr) { } async function api(path, opts = {}) { + const pathOnly = + typeof path === "string" ? path.trim().replace(/\?.*$/, "") || String(path).trim() : ""; const res = await fetch(path, { ...opts, credentials: "same-origin", @@ -821,7 +830,19 @@ async function api(path, opts = {}) { data = { raw: text }; } if (!res.ok) { - const msg = data.error || data.raw || res.statusText; + let msg = typeof data.error === "string" ? data.error : ""; + if (!msg) msg = typeof data.raw === "string" ? data.raw : ""; + if (!msg) msg = res.statusText; + const hint = typeof data.hint === "string" && data.hint.trim() ? data.hint.trim() : ""; + if (hint) { + msg = msg ? `${msg} — ${hint}` : hint; + } else if ( + res.status === 404 && + /^\/api\/mtproto\b/.test(pathOnly) && + /^not\s*found\s*$/i.test(String(msg).trim()) + ) { + msg = `${msg.trim()} — нет эндпоинта GET /api/mtproto/* на этом HTTP-ответчике (часто: старый образ панели, другой контейнер на вашем домене, или nginx с белым списком location вместо location /api/). Проверьте на том же хосту:порту, что панель, GET /health (поле version) и пересоберите образ amnezia-admin.`; + } throw new Error(msg); } return data; @@ -1361,6 +1382,15 @@ function renderWarpPanel(data) { warpStatusLine.textContent = parts.join(" · "); const selection = new Set((w.selectedAllowedIps || []).map(String)); + /** Адреса из старого clients.list без соответствующего peer (смена IP, другой инстанс) — иначе «Применить» шлёт мусор вроде 10.x.1/32 интерфейса сервера. */ + const validWarpPeerIps = new Set(); + for (const c of data.clients) { + if (!c.activeInConf) continue; + parseIpv4Cidrs(c.allowedIps).forEach((ip) => validWarpPeerIps.add(ip)); + } + for (const ip of [...selection]) { + if (!validWarpPeerIps.has(ip)) selection.delete(ip); + } function redrawChecks() { warpClientListEl.innerHTML = ""; diff --git a/public/index.html b/public/index.html index 126952c..96f9d8a 100644 --- a/public/index.html +++ b/public/index.html @@ -140,6 +140,7 @@ Если WARP нужен: выход в интернет через Cloudflare для выбранных клиентов (правила маршрутизации и NAT в контейнере). Один раз на хосте VPS под root: в каталоге с репозиторием выполните bash scripts/warp-amnezia.sh install. Затем отметьте клиентов ниже и нажмите «Применить» (контейнер AWG перезапустится). Установить или удалить WARP с хоста можно кнопками ниже (пароль root по SSH — как при синхронизации времени). Убрать вручную: bash scripts/warp-amnezia.sh uninstall — см. README. + Если в шапке раздела виден выход через Cloudflare, а на устройстве проверка IP показывает не его: отправляйте нужный трафик через VPN в приложении Amnezia (часто 0.0.0.0/0 или режим «весь интернет»); иначе часть сайтов может идти в обход контейнера. После «Применить маршрутизацию» имеет смысл переподключить туннель на клиенте.

@@ -160,7 +161,7 @@

- В базовой панели (FREE). Отдельный Docker‑контейнер (образ Telegram), публикация порта на хосте VPS. + Во всех редакциях панели (FREE и PRO). Отдельный Docker‑контейнер (образ Telegram), публикация порта на хосте VPS. Ссылка вида tg://proxy?… может появляться без env, если панель открыта по публичному IP/DNS; для фиксированного хоста задайте MTPRO_PUBLIC_HOST или CLIENT_CONFIG_ENDPOINT (переменные контейнера панели, см. install.sh README). Сохраните секрет, который показывается один раз после установки — в интерфейсе он маскируется. diff --git a/server.js b/server.js index 88d5483..caf139e 100644 --- a/server.js +++ b/server.js @@ -755,6 +755,20 @@ async function warpInterfaceUp(rt) { } } +/** После `docker restart` правила iptables/ip rule живут только в патче start.sh; дублируем применение на «живом» контейнере. */ +async function waitForWarpInterface(rt, { timeoutMs = 90000, stepMs = 1500 } = {}) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { + if (await warpInterfaceUp(rt)) return true; + } catch { + /* контейнер ещё не отвечает на exec */ + } + await new Promise((r) => setTimeout(r, stepMs)); + } + return false; +} + async function warpLoadSelectedIps(rt) { try { const raw = await rt.dockerReadFile(rt.profile.warpClientsList); @@ -878,9 +892,18 @@ async function warpPatchStartSh(rt, ips) { async function warpPersistAndRestart(rt, selectedIps) { await rt.backupRemoteFiles(); await warpSaveSelectedIps(rt, selectedIps); - await warpApplyRouting(rt, selectedIps); await warpPatchStartSh(rt, selectedIps); await dockerRestartContainer(rt.profile.container); + const needWarpRules = selectedIps.length > 0; + if (needWarpRules) { + const up = await waitForWarpInterface(rt); + if (!up && (await warpFileExists(rt, rt.profile.warpConf))) { + throw new Error( + "После перезапуска AWG интерфейс warp не поднялся за отведённое время. Откройте «Вывод wg show warp», нажмите «Поднять WARP», затем «Применить маршрутизацию» снова.", + ); + } + } + await warpApplyRouting(rt, selectedIps); } function activePeerAllowedIpSet(conf) { @@ -2241,24 +2264,44 @@ app.get("/api/time-sync-capabilities", requireAuth, (_req, res) => { }); }); +function mtprotoLogsTailPayload(req) { + const snap = mtprotoSnapshot(hostFromRequest(req)); + if (!snap.exists || !snap.running) return { logsTail: "" }; + const l = dockerSpawnSync(["logs", "--tail", "100", MTPRO_CONTAINER], 12_000); + let logsTail = ""; + if (l.code === 0) logsTail = l.stdout.trim().slice(-4500); + return { logsTail }; +} + app.get("/api/mtproto/status", requireAuth, (req, res) => { if (effectiveUiHidden().mtproto) { return res.status(403).json({ error: MSG_UI_MTProto_OFF }); } const snap = mtprotoSnapshot(hostFromRequest(req)); - res.json({ ...snap }); + const withLogs = + typeof req.query.withLogs === "string" && + (req.query.withLogs === "1" || req.query.withLogs === ""); + if (!withLogs) { + res.json({ ...snap }); + return; + } + const { logsTail } = mtprotoLogsTailPayload(req); + res.json({ ...snap, logsTail, logsFetched: true }); }); app.get("/api/mtproto/logs", requireAuth, (req, res) => { if (effectiveUiHidden().mtproto) { return res.status(403).json({ error: MSG_UI_MTProto_OFF }); } - const snap = mtprotoSnapshot(hostFromRequest(req)); - if (!snap.exists || !snap.running) return res.json({ logsTail: "" }); - const l = dockerSpawnSync(["logs", "--tail", "100", MTPRO_CONTAINER], 12_000); - let logsTail = ""; - if (l.code === 0) logsTail = l.stdout.trim().slice(-4500); - res.json({ logsTail }); + res.json(mtprotoLogsTailPayload(req)); +}); + +/** Алиас к `/logs` (редко нужен для обхода прокси, где путь содержит `logs` фильтруется). */ +app.get("/api/mtproto/tail", requireAuth, (req, res) => { + if (effectiveUiHidden().mtproto) { + return res.status(403).json({ error: MSG_UI_MTProto_OFF }); + } + res.json(mtprotoLogsTailPayload(req)); }); app.post("/api/mtproto/install", requireAuth, (req, res) => { @@ -3068,7 +3111,16 @@ if (fs.existsSync(pub)) { ); } -app.use((_req, res) => { +app.use((req, res) => { + if (typeof req.path === "string" && req.path.startsWith("/api/")) { + res.status(404).json({ + error: "Not found", + path: `${req.originalUrl || req.path}`, + hint: + "Эндпоинта нет на этом процессе. Проверьте версию приложения через GET /health на том же хосту:порту, что панель, и что HTTP-прокси (если есть) проксирует префикс /api/*. После перехода на PRO пересоберите образ amnezia-admin из актуального релиза репозитория.", + }); + return; + } res.status(404).send("Not found"); });