From 129f29d6be3672b07b3bda1d5ef6e9704b4364a8 Mon Sep 17 00:00:00 2001 From: Ivan Vasilev Date: Sat, 26 Sep 2026 21:43:45 +0300 Subject: [PATCH] Keep the node's own panel when it joins a core A server that already works as panel + VPN can now also be a node of another panel without losing its own: `awg-ctrl join` keeps awg-ui running next to the agent and writes NODE_PANEL=on. `--no-panel` stops it as before. Both panels talk to the same awg-ctrl, which stays the source of truth, so they do not get in each other's way. The CLI runs awg-ctrl, awg-ui and awg-agent on such a node, all behind the awg-ctrl health gate; a node installed by install.sh has no UI_PASS and still runs only awg-ctrl + agent. unjoin also drops NODE_PANEL. --- cli/src/index.ts | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/cli/src/index.ts b/cli/src/index.ts index a1715eb..ab741c3 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -141,7 +141,13 @@ type SvcName = keyof typeof SERVICES; // панель без собственного VPN (только awg-ui). Иначе — обычная установка. const IS_NODE = !!process.env.CORE_HOST; const NO_LOCAL = (process.env.LOCAL_NODE ?? "on").toLowerCase() === "off"; -const ALL: SvcName[] = IS_NODE ? ["awgctrl", "agent"] : NO_LOCAL ? ["ui"] : ["awgctrl", "ui"]; +// Нода со своей панелью: работающий сервер «панель + VPN» подключили к главной панели, не +// выключая его собственную (join без --no-panel). Обе панели ходят в один awg-ctrl — он и +// есть источник правды, поэтому друг другу они не мешают. У ноды, поставленной install.sh, +// панели нет (UI_PASS не задан). +const NODE_PANEL = IS_NODE && !!process.env.UI_PASS && (process.env.NODE_PANEL ?? "on").toLowerCase() !== "off"; +const ALL: SvcName[] = IS_NODE ? (NODE_PANEL ? ["awgctrl", "ui", "agent"] : ["awgctrl", "agent"]) + : NO_LOCAL ? ["ui"] : ["awgctrl", "ui"]; function readPid(name: SvcName): number | null { const { pidFile } = SERVICES[name]; @@ -319,12 +325,12 @@ async function startAll() { if (!ALL.includes("awgctrl")) { for (const n of ALL) await start(n); return; } await start("awgctrl"); - const next: SvcName = IS_NODE ? "agent" : "ui"; + const rest = ALL.filter(n => n !== "awgctrl"); if (!isRunning("awgctrl") || !(await waitForHealth("awgctrl"))) { - console.error(` ${grey("■")} awg-ctrl не отвечает — ${SERVICES[next].label} не запущен (см. ${SERVICES.awgctrl.logFile})`); + console.error(` ${grey("■")} awg-ctrl не отвечает — ${rest.map(n => SERVICES[n].label).join(", ")} не запущен (см. ${SERVICES.awgctrl.logFile})`); return; } - await start(next); + for (const n of rest) await start(n); } async function stopAll() { for (const n of ALL) await stop(n); } @@ -347,11 +353,12 @@ function restartFresh(): void { spawnSync(process.execPath, [...process.execArgv, process.argv[1], "restart"], { stdio: "inherit" }); } -// `join `: превратить эту установку в ноду. Панель (awg-ui) на этой машине -// останавливается, вместо неё запускается агент; awg-ctrl и пользователи остаются как были. -async function joinCore(link?: string) { - if (!link?.startsWith("awgjoin://")) { - console.error(" Usage: awg-ctrl join (ссылку даёт панель: «Добавить сервер»)"); +// `join [--no-panel]`: подключить эту установку к главной панели как ноду. Рядом +// запускается агент; awg-ctrl и пользователи остаются как были. Своя панель (awg-ui) продолжает +// работать, если она здесь есть, — с --no-panel она останавливается. +async function joinCore(link?: string, flag?: string) { + if (!link?.startsWith("awgjoin://") || (flag !== undefined && flag !== "--no-panel")) { + console.error(" Usage: awg-ctrl join [--no-panel] (ссылку даёт панель: «Добавить сервер»)"); process.exit(1); } let j: { v?: number; h?: string; p?: number; pin?: string; n?: number; s?: string }; @@ -364,15 +371,18 @@ async function joinCore(link?: string) { if (!ok) { console.error(" Ссылка неверного формата или от другой версии панели"); process.exit(1); } if (IS_NODE && !await confirmYes(` Эта нода уже подключена к ${process.env.CORE_HOST}. Перепривязать?`)) return; + const keepPanel = flag !== "--no-panel" && !!process.env.UI_PASS; let content = existsSync(envFile) ? readFileSync(envFile, "utf8") : ""; const set: Record = { CORE_HOST: j.h!, CORE_PORT: String(j.p), CORE_PIN: j.pin!, NODE_ID: String(j.n), JOIN_SECRET: j.s!, + NODE_PANEL: keepPanel ? "on" : "off", }; for (const [k, v] of Object.entries(set)) content = updateEnvVar(content, k, v); writeFileSync(envFile, content, { mode: 0o600 }); - await stop("ui"); // панель на ноде не нужна - console.log(`\n ${green("✓")} Настроено: core ${j.h}:${j.p}, нода #${j.n}`); + if (!keepPanel) await stop("ui"); + console.log(`\n ${green("✓")} Настроено: core ${j.h}:${j.p}, нода #${j.n}` + + (keepPanel ? " · своя панель продолжает работать" : process.env.UI_PASS ? " · своя панель остановлена" : "")); restartFresh(); } @@ -380,7 +390,7 @@ async function unjoinCore() { if (!IS_NODE) { console.log(" Эта установка не подключена к core"); return; } await stop("agent"); let content = existsSync(envFile) ? readFileSync(envFile, "utf8") : ""; - for (const k of ["CORE_HOST", "CORE_PORT", "CORE_PIN", "NODE_ID", "JOIN_SECRET"]) content = removeEnvVar(content, k); + for (const k of ["CORE_HOST", "CORE_PORT", "CORE_PIN", "NODE_ID", "JOIN_SECRET", "NODE_PANEL"]) content = removeEnvVar(content, k); writeFileSync(envFile, content, { mode: 0o600 }); console.log(` ${green("✓")} Нода отвязана. Удалите её в панели (сервер) и запустите: awg-ctrl restart`); // Нода, поставленная install.sh с ролью «нода», панели не имеет — без пароля в неё не войти. @@ -489,9 +499,9 @@ async function main() { return; } - const USAGE = "Usage: cli |unjoin>"; + const USAGE = "Usage: cli [--no-panel]|unjoin>"; - const [,,, sub] = process.argv; + const [,,, sub, arg] = process.argv; switch (cmd) { case "start": await startAll(); break; @@ -503,7 +513,7 @@ async function main() { else if (sub === "pass") await setCredentials("pass"); else await setCredentials(); break; - case "join": await joinCore(sub); break; + case "join": await joinCore(sub, arg); break; case "unjoin": await unjoinCore(); break; default: console.log(USAGE); process.exit(1); }