mirror of
https://github.com/andrey271192/amnezia_web-PRO.git
synced 2026-09-20 14:42:00 +00:00
fix: auto-discover amnezia client sources
This commit is contained in:
@@ -42,6 +42,8 @@
|
||||
|
||||
Начиная с **v1.2.37** парсер `clientsTable` поддерживает не только массив, но и object/map/nested-форматы разных установок Amnezia. Для диагностики источников есть `GET /api/clients/source-report?profileId=...`: показывает текущий контейнер, пути, количество `[Peer]`, количество строк `clientsTable`, missing/table-only счётчики и короткие префиксы ключей без публикации полных ключей.
|
||||
|
||||
Начиная с **v1.2.38** панель сама выбирает рабочие пути внутри контейнера: среди `/opt/amnezia/awg/awg0.conf`, `/opt/amnezia/awg/wg0.conf`, `/opt/amnezia/wireguard/wg0.conf` берётся конфиг с максимальным числом `[Peer]`; среди `/opt/amnezia/awg/clientsTable` и `/opt/amnezia/wireguard/clientsTable` берётся таблица с максимальным числом клиентов. Это закрывает установки, где приложение Amnezia создало старых клиентов в `wg0.conf`, а не в `awg0.conf`.
|
||||
|
||||
## Лендинг не поднимается (порт 80 занят)
|
||||
|
||||
При ошибке bind `:80` используйте при установке **`SKIP_LANDING=1`** или **`LANDING_PORT=8081`** — админка на `HOST_PORT` (например 8080) от этого не зависит.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "amnezia-admin",
|
||||
"version": "1.2.37",
|
||||
"version": "1.2.38",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "amnezia-admin",
|
||||
"version": "1.2.37",
|
||||
"version": "1.2.38",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.21.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amnezia-admin",
|
||||
"version": "1.2.37",
|
||||
"version": "1.2.38",
|
||||
"private": false,
|
||||
"description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)",
|
||||
"license": "MIT",
|
||||
|
||||
139
server.js
139
server.js
@@ -814,21 +814,52 @@ async function discoverAwgContainer(confPath, preferredName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function uniqueList(items) {
|
||||
return [...new Set(items.filter(Boolean).map(String))];
|
||||
}
|
||||
|
||||
function ifaceFromConfPath(filePath, fallback) {
|
||||
const base = path.basename(String(filePath || ""));
|
||||
const m = base.match(/^([A-Za-z0-9_.-]+)\.conf$/);
|
||||
return m ? m[1] : fallback;
|
||||
}
|
||||
|
||||
function createRuntime(profile) {
|
||||
let resolvedContainer = null;
|
||||
let resolvedConfPath = null;
|
||||
let resolvedClientsPath = null;
|
||||
const confPath = profile.confPath;
|
||||
const clientsPath = profile.clientsPath;
|
||||
const iface = profile.iface;
|
||||
const wgBinary = profile.wgBinary;
|
||||
const pskPath = profile.pskPath;
|
||||
const confCandidates = uniqueList([
|
||||
confPath,
|
||||
"/opt/amnezia/awg/awg0.conf",
|
||||
"/opt/amnezia/awg/wg0.conf",
|
||||
"/opt/amnezia/wireguard/wg0.conf",
|
||||
]);
|
||||
const clientsCandidates = uniqueList([
|
||||
clientsPath,
|
||||
"/opt/amnezia/awg/clientsTable",
|
||||
"/opt/amnezia/wireguard/clientsTable",
|
||||
]);
|
||||
|
||||
async function resolveContainer() {
|
||||
if (resolvedContainer) return resolvedContainer;
|
||||
if (profile.container && (await containerHasFile(profile.container, confPath))) {
|
||||
if (profile.container) {
|
||||
for (const candidate of confCandidates) {
|
||||
if (await containerHasFile(profile.container, candidate)) {
|
||||
resolvedContainer = profile.container;
|
||||
return resolvedContainer;
|
||||
}
|
||||
const found = await discoverAwgContainer(confPath, profile.container);
|
||||
}
|
||||
}
|
||||
let found = null;
|
||||
for (const candidate of confCandidates) {
|
||||
found = await discoverAwgContainer(candidate, profile.container);
|
||||
if (found) break;
|
||||
}
|
||||
if (found) {
|
||||
resolvedContainer = found;
|
||||
if (found !== profile.container) {
|
||||
@@ -844,6 +875,59 @@ function createRuntime(profile) {
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveConfPath() {
|
||||
if (resolvedConfPath) return resolvedConfPath;
|
||||
const container = await resolveContainer();
|
||||
let best = null;
|
||||
for (const candidate of confCandidates) {
|
||||
try {
|
||||
const { stdout } = await execDocker(["exec", container, "cat", candidate]);
|
||||
const parsed = splitAwgConf(stdout);
|
||||
const score = parsed.peers.length * 10 + (candidate === confPath ? 1 : 0);
|
||||
if (!best || score > best.score) best = { path: candidate, score };
|
||||
} catch {
|
||||
/* missing */
|
||||
}
|
||||
}
|
||||
if (!best) {
|
||||
throw new Error(`Не найден wg/awg config file в контейнере ${container}: ${confCandidates.join(", ")}`);
|
||||
}
|
||||
resolvedConfPath = best.path;
|
||||
if (resolvedConfPath !== confPath) {
|
||||
console.log(`→ AWG config авто: «${resolvedConfPath}» вместо «${confPath}» (${profile.label || profile.id})`);
|
||||
}
|
||||
return resolvedConfPath;
|
||||
}
|
||||
|
||||
async function resolveClientsPath() {
|
||||
if (resolvedClientsPath) return resolvedClientsPath;
|
||||
const container = await resolveContainer();
|
||||
let best = null;
|
||||
for (const candidate of clientsCandidates) {
|
||||
try {
|
||||
const { stdout } = await execDocker(["exec", container, "cat", candidate]);
|
||||
let parsed = [];
|
||||
try {
|
||||
parsed = parseClientsTable(stdout);
|
||||
} catch {
|
||||
parsed = [];
|
||||
}
|
||||
const score = parsed.length * 10 + (candidate === clientsPath ? 1 : 0);
|
||||
if (!best || score > best.score) best = { path: candidate, score };
|
||||
} catch {
|
||||
/* missing */
|
||||
}
|
||||
}
|
||||
if (!best) {
|
||||
throw new Error(`Не найден clientsTable в контейнере ${container}: ${clientsCandidates.join(", ")}`);
|
||||
}
|
||||
resolvedClientsPath = best.path;
|
||||
if (resolvedClientsPath !== clientsPath) {
|
||||
console.log(`→ clientsTable авто: «${resolvedClientsPath}» вместо «${clientsPath}» (${profile.label || profile.id})`);
|
||||
}
|
||||
return resolvedClientsPath;
|
||||
}
|
||||
|
||||
async function dockerExec(cmd) {
|
||||
const container = await resolveContainer();
|
||||
const { stdout, stderr } = await execDocker(["exec", container, "sh", "-c", cmd]);
|
||||
@@ -873,27 +957,33 @@ function createRuntime(profile) {
|
||||
|
||||
async function backupRemoteFiles() {
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
await dockerExec(`cp '${confPath}' '${confPath}.bak-admin-${stamp}' 2>/dev/null || true`);
|
||||
const activeConfPath = await resolveConfPath();
|
||||
const activeClientsPath = await resolveClientsPath();
|
||||
await dockerExec(`cp '${activeConfPath}' '${activeConfPath}.bak-admin-${stamp}' 2>/dev/null || true`);
|
||||
await dockerExec(
|
||||
`cp '${clientsPath}' '${clientsPath}.bak-admin-${stamp}' 2>/dev/null || true`
|
||||
`cp '${activeClientsPath}' '${activeClientsPath}.bak-admin-${stamp}' 2>/dev/null || true`
|
||||
);
|
||||
}
|
||||
|
||||
async function applySyncconf() {
|
||||
const activeConfPath = await resolveConfPath();
|
||||
const activeIface = ifaceFromConfPath(activeConfPath, iface);
|
||||
await dockerExec(
|
||||
`wg-quick strip '${confPath}' > /tmp/wg-admin-strip.conf && ${wgBinary} syncconf ${iface} /tmp/wg-admin-strip.conf`
|
||||
`wg-quick strip '${activeConfPath}' > /tmp/wg-admin-strip.conf && ${wgBinary} syncconf ${activeIface} /tmp/wg-admin-strip.conf`
|
||||
);
|
||||
}
|
||||
|
||||
async function loadState() {
|
||||
const activeConfPath = await resolveConfPath();
|
||||
const activeClientsPath = await resolveClientsPath();
|
||||
const [confText, tableText] = await Promise.all([
|
||||
dockerReadFile(confPath),
|
||||
dockerReadFile(clientsPath),
|
||||
dockerReadFile(activeConfPath),
|
||||
dockerReadFile(activeClientsPath),
|
||||
]);
|
||||
const conf = splitAwgConf(confText);
|
||||
const clients = parseClientsTable(tableText);
|
||||
const peerByKey = new Map(conf.peers.map((p) => [p.publicKey, p]));
|
||||
return { confText, conf, clients, peerByKey };
|
||||
return { confText, conf, clients, peerByKey, confPath: activeConfPath, clientsPath: activeClientsPath };
|
||||
}
|
||||
|
||||
async function inferPskFromConf(conf) {
|
||||
@@ -916,8 +1006,17 @@ function createRuntime(profile) {
|
||||
applySyncconf,
|
||||
loadState,
|
||||
inferPskFromConf,
|
||||
confPath,
|
||||
clientsPath,
|
||||
get confPath() {
|
||||
return resolvedConfPath || confPath;
|
||||
},
|
||||
get iface() {
|
||||
return ifaceFromConfPath(resolvedConfPath || confPath, iface);
|
||||
},
|
||||
resolveConfPath,
|
||||
resolveClientsPath,
|
||||
get clientsPath() {
|
||||
return resolvedClientsPath || clientsPath;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2313,7 +2412,7 @@ app.get("/api/clients", requireAuth, async (req, res) => {
|
||||
try {
|
||||
let wgShow = "";
|
||||
try {
|
||||
wgShow = await rt.dockerExec(`${rt.profile.wgBinary} show ${rt.profile.iface}`);
|
||||
wgShow = await rt.dockerExec(`${rt.profile.wgBinary} show ${rt.iface}`);
|
||||
} catch {
|
||||
wgShow = "";
|
||||
}
|
||||
@@ -2321,7 +2420,7 @@ app.get("/api/clients", requireAuth, async (req, res) => {
|
||||
const warpSelected = new Set(
|
||||
warpMeta.supported && warpMeta.installed ? warpMeta.selectedAllowedIps : [],
|
||||
);
|
||||
const { conf, clients, peerByKey } = await rt.loadState();
|
||||
const { conf, clients, peerByKey, confPath: activeConfPath, clientsPath: activeClientsPath } = await rt.loadState();
|
||||
const rows = [];
|
||||
const seenClientIds = new Set();
|
||||
for (const c of clients) {
|
||||
@@ -2395,6 +2494,9 @@ app.get("/api/clients", requireAuth, async (req, res) => {
|
||||
profileId: rt.profile.id,
|
||||
profileLabel: rt.profile.label,
|
||||
container: (await rt.resolveContainer().catch(() => rt.profile.container)),
|
||||
confPath: activeConfPath || rt.confPath,
|
||||
clientsPath: activeClientsPath || rt.clientsPath,
|
||||
iface: rt.iface,
|
||||
protocol: "AmneziaWG",
|
||||
peerCount: conf.peers.length,
|
||||
clients: rows,
|
||||
@@ -2453,19 +2555,21 @@ app.get("/api/clients/source-report", requireAuth, async (req, res) => {
|
||||
const rt = runtimeFromExportRequest(req);
|
||||
try {
|
||||
const container = await rt.resolveContainer();
|
||||
const activeConfPath = await rt.resolveConfPath();
|
||||
const activeClientsPath = await rt.resolveClientsPath();
|
||||
let confText = "";
|
||||
let tableText = "";
|
||||
let conf = { peers: [] };
|
||||
let clients = [];
|
||||
let clientsError = null;
|
||||
try {
|
||||
confText = await rt.dockerReadFile(rt.confPath);
|
||||
confText = await rt.dockerReadFile(activeConfPath);
|
||||
conf = splitAwgConf(confText);
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: `Не удалось прочитать ${rt.confPath}: ${String(e.message || e)}` });
|
||||
return res.status(500).json({ error: `Не удалось прочитать ${activeConfPath}: ${String(e.message || e)}` });
|
||||
}
|
||||
try {
|
||||
tableText = await rt.dockerReadFile(rt.clientsPath);
|
||||
tableText = await rt.dockerReadFile(activeClientsPath);
|
||||
clients = parseClientsTable(tableText);
|
||||
} catch (e) {
|
||||
clientsError = String(e.message || e);
|
||||
@@ -2478,8 +2582,9 @@ app.get("/api/clients/source-report", requireAuth, async (req, res) => {
|
||||
profileId: rt.profile.id,
|
||||
profileLabel: rt.profile.label,
|
||||
container,
|
||||
confPath: rt.confPath,
|
||||
clientsPath: rt.clientsPath,
|
||||
confPath: activeConfPath,
|
||||
iface: rt.iface,
|
||||
clientsPath: activeClientsPath,
|
||||
confBytes: Buffer.byteLength(confText, "utf8"),
|
||||
clientsTableBytes: Buffer.byteLength(tableText, "utf8"),
|
||||
peerCount: conf.peers.length,
|
||||
|
||||
Reference in New Issue
Block a user