mirror of
https://github.com/andrey271192/amnezia_web.git
synced 2026-09-20 14:41:59 +00:00
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 <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amnezia-admin",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.23",
|
"version": "1.2.24",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "amnezia_web — базовая панель AmneziaWG (FREE: просмотр и удаление клиентов; полная версия — PRO).",
|
"description": "amnezia_web — базовая панель AmneziaWG (FREE: просмотр и удаление клиентов; полная версия — PRO).",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -467,16 +467,23 @@ async function refreshMtprotoPanel() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = "Загрузка логов…";
|
if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = "Загрузка логов…";
|
||||||
const snap = await api("/api/mtproto/status");
|
const snap = await api("/api/mtproto/status?withLogs=1");
|
||||||
void (async () => {
|
if (snap.logsFetched !== true && snap.exists === true && snap.running === true) {
|
||||||
try {
|
void (async () => {
|
||||||
const lg = await api("/api/mtproto/logs");
|
try {
|
||||||
const tail = typeof lg?.logsTail === "string" ? lg.logsTail : "";
|
const lg = (await api("/api/mtproto/tail").catch(() =>
|
||||||
if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = tail;
|
api("/api/mtproto/logs"),
|
||||||
} catch {
|
)) || { logsTail: "" };
|
||||||
if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = "(лог не загрузился — обновите раздел позже)";
|
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 (mtprotoCalloutEl) {
|
||||||
if (snap.exists) {
|
if (snap.exists) {
|
||||||
@@ -805,6 +812,8 @@ function setPwMsg(text, isErr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function api(path, opts = {}) {
|
async function api(path, opts = {}) {
|
||||||
|
const pathOnly =
|
||||||
|
typeof path === "string" ? path.trim().replace(/\?.*$/, "") || String(path).trim() : "";
|
||||||
const res = await fetch(path, {
|
const res = await fetch(path, {
|
||||||
...opts,
|
...opts,
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
@@ -821,7 +830,19 @@ async function api(path, opts = {}) {
|
|||||||
data = { raw: text };
|
data = { raw: text };
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
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);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
@@ -1361,6 +1382,15 @@ function renderWarpPanel(data) {
|
|||||||
warpStatusLine.textContent = parts.join(" · ");
|
warpStatusLine.textContent = parts.join(" · ");
|
||||||
|
|
||||||
const selection = new Set((w.selectedAllowedIps || []).map(String));
|
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() {
|
function redrawChecks() {
|
||||||
warpClientListEl.innerHTML = "";
|
warpClientListEl.innerHTML = "";
|
||||||
|
|||||||
@@ -140,6 +140,7 @@
|
|||||||
Если WARP нужен: выход в интернет через Cloudflare для <em>выбранных</em> клиентов (правила маршрутизации и NAT в контейнере).
|
Если WARP нужен: выход в интернет через Cloudflare для <em>выбранных</em> клиентов (правила маршрутизации и NAT в контейнере).
|
||||||
Один раз на хосте VPS под root: в каталоге с репозиторием выполните <code class="inline">bash scripts/warp-amnezia.sh install</code>.
|
Один раз на хосте VPS под root: в каталоге с репозиторием выполните <code class="inline">bash scripts/warp-amnezia.sh install</code>.
|
||||||
Затем отметьте клиентов ниже и нажмите «Применить» (контейнер AWG перезапустится). Установить или удалить WARP с хоста можно кнопками ниже (пароль root по SSH — как при синхронизации времени). Убрать вручную: <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.
|
||||||
|
Если в шапке раздела виден выход через Cloudflare, а на устройстве проверка IP показывает не его: отправляйте нужный трафик через VPN в приложении Amnezia (часто <code class="inline">0.0.0.0/0</code> или режим «весь интернет»); иначе часть сайтов может идти в обход контейнера. После «Применить маршрутизацию» имеет смысл переподключить туннель на клиенте.
|
||||||
</p>
|
</p>
|
||||||
<div id="warp-actions" class="warp-actions"></div>
|
<div id="warp-actions" class="warp-actions"></div>
|
||||||
<div id="warp-client-list" class="warp-client-list"></div>
|
<div id="warp-client-list" class="warp-client-list"></div>
|
||||||
@@ -160,7 +161,7 @@
|
|||||||
</summary>
|
</summary>
|
||||||
<div class="panel-fold-body">
|
<div class="panel-fold-body">
|
||||||
<p class="muted warp-intro mtproto-intro">
|
<p class="muted warp-intro mtproto-intro">
|
||||||
<strong>В базовой панели (FREE).</strong> Отдельный Docker‑контейнер (образ Telegram), публикация порта на хосте VPS.
|
<strong>Во всех редакциях панели (FREE и PRO).</strong> Отдельный Docker‑контейнер (образ Telegram), публикация порта на хосте VPS.
|
||||||
Ссылка вида <code class="inline">tg://proxy?…</code> может появляться без env, если панель открыта по публичному IP/DNS; для фиксированного хоста задайте <code class="inline">MTPRO_PUBLIC_HOST</code> или <code class="inline">CLIENT_CONFIG_ENDPOINT</code>
|
Ссылка вида <code class="inline">tg://proxy?…</code> может появляться без env, если панель открыта по публичному IP/DNS; для фиксированного хоста задайте <code class="inline">MTPRO_PUBLIC_HOST</code> или <code class="inline">CLIENT_CONFIG_ENDPOINT</code>
|
||||||
(переменные контейнера панели, см. install.sh README).
|
(переменные контейнера панели, см. install.sh README).
|
||||||
Сохраните <strong>секрет</strong>, который показывается один раз после установки — в интерфейсе он маскируется.
|
Сохраните <strong>секрет</strong>, который показывается один раз после установки — в интерфейсе он маскируется.
|
||||||
|
|||||||
70
server.js
70
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) {
|
async function warpLoadSelectedIps(rt) {
|
||||||
try {
|
try {
|
||||||
const raw = await rt.dockerReadFile(rt.profile.warpClientsList);
|
const raw = await rt.dockerReadFile(rt.profile.warpClientsList);
|
||||||
@@ -878,9 +892,18 @@ async function warpPatchStartSh(rt, ips) {
|
|||||||
async function warpPersistAndRestart(rt, selectedIps) {
|
async function warpPersistAndRestart(rt, selectedIps) {
|
||||||
await rt.backupRemoteFiles();
|
await rt.backupRemoteFiles();
|
||||||
await warpSaveSelectedIps(rt, selectedIps);
|
await warpSaveSelectedIps(rt, selectedIps);
|
||||||
await warpApplyRouting(rt, selectedIps);
|
|
||||||
await warpPatchStartSh(rt, selectedIps);
|
await warpPatchStartSh(rt, selectedIps);
|
||||||
await dockerRestartContainer(rt.profile.container);
|
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) {
|
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) => {
|
app.get("/api/mtproto/status", requireAuth, (req, res) => {
|
||||||
if (effectiveUiHidden().mtproto) {
|
if (effectiveUiHidden().mtproto) {
|
||||||
return res.status(403).json({ error: MSG_UI_MTProto_OFF });
|
return res.status(403).json({ error: MSG_UI_MTProto_OFF });
|
||||||
}
|
}
|
||||||
const snap = mtprotoSnapshot(hostFromRequest(req));
|
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) => {
|
app.get("/api/mtproto/logs", requireAuth, (req, res) => {
|
||||||
if (effectiveUiHidden().mtproto) {
|
if (effectiveUiHidden().mtproto) {
|
||||||
return res.status(403).json({ error: MSG_UI_MTProto_OFF });
|
return res.status(403).json({ error: MSG_UI_MTProto_OFF });
|
||||||
}
|
}
|
||||||
const snap = mtprotoSnapshot(hostFromRequest(req));
|
res.json(mtprotoLogsTailPayload(req));
|
||||||
if (!snap.exists || !snap.running) return res.json({ logsTail: "" });
|
});
|
||||||
const l = dockerSpawnSync(["logs", "--tail", "100", MTPRO_CONTAINER], 12_000);
|
|
||||||
let logsTail = "";
|
/** Алиас к `/logs` (редко нужен для обхода прокси, где путь содержит `logs` фильтруется). */
|
||||||
if (l.code === 0) logsTail = l.stdout.trim().slice(-4500);
|
app.get("/api/mtproto/tail", requireAuth, (req, res) => {
|
||||||
res.json({ logsTail });
|
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) => {
|
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");
|
res.status(404).send("Not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user