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.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 занят)
|
## Лендинг не поднимается (порт 80 занят)
|
||||||
|
|
||||||
При ошибке bind `:80` используйте при установке **`SKIP_LANDING=1`** или **`LANDING_PORT=8081`** — админка на `HOST_PORT` (например 8080) от этого не зависит.
|
При ошибке 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",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.37",
|
"version": "1.2.38",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "amnezia-admin",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.37",
|
"version": "1.2.38",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.2"
|
"express": "^4.21.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amnezia-admin",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.37",
|
"version": "1.2.38",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)",
|
"description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
139
server.js
139
server.js
@@ -814,21 +814,52 @@ async function discoverAwgContainer(confPath, preferredName) {
|
|||||||
return null;
|
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) {
|
function createRuntime(profile) {
|
||||||
let resolvedContainer = null;
|
let resolvedContainer = null;
|
||||||
|
let resolvedConfPath = null;
|
||||||
|
let resolvedClientsPath = null;
|
||||||
const confPath = profile.confPath;
|
const confPath = profile.confPath;
|
||||||
const clientsPath = profile.clientsPath;
|
const clientsPath = profile.clientsPath;
|
||||||
const iface = profile.iface;
|
const iface = profile.iface;
|
||||||
const wgBinary = profile.wgBinary;
|
const wgBinary = profile.wgBinary;
|
||||||
const pskPath = profile.pskPath;
|
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() {
|
async function resolveContainer() {
|
||||||
if (resolvedContainer) return resolvedContainer;
|
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;
|
resolvedContainer = profile.container;
|
||||||
return resolvedContainer;
|
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) {
|
if (found) {
|
||||||
resolvedContainer = found;
|
resolvedContainer = found;
|
||||||
if (found !== profile.container) {
|
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) {
|
async function dockerExec(cmd) {
|
||||||
const container = await resolveContainer();
|
const container = await resolveContainer();
|
||||||
const { stdout, stderr } = await execDocker(["exec", container, "sh", "-c", cmd]);
|
const { stdout, stderr } = await execDocker(["exec", container, "sh", "-c", cmd]);
|
||||||
@@ -873,27 +957,33 @@ function createRuntime(profile) {
|
|||||||
|
|
||||||
async function backupRemoteFiles() {
|
async function backupRemoteFiles() {
|
||||||
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
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(
|
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() {
|
async function applySyncconf() {
|
||||||
|
const activeConfPath = await resolveConfPath();
|
||||||
|
const activeIface = ifaceFromConfPath(activeConfPath, iface);
|
||||||
await dockerExec(
|
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() {
|
async function loadState() {
|
||||||
|
const activeConfPath = await resolveConfPath();
|
||||||
|
const activeClientsPath = await resolveClientsPath();
|
||||||
const [confText, tableText] = await Promise.all([
|
const [confText, tableText] = await Promise.all([
|
||||||
dockerReadFile(confPath),
|
dockerReadFile(activeConfPath),
|
||||||
dockerReadFile(clientsPath),
|
dockerReadFile(activeClientsPath),
|
||||||
]);
|
]);
|
||||||
const conf = splitAwgConf(confText);
|
const conf = splitAwgConf(confText);
|
||||||
const clients = parseClientsTable(tableText);
|
const clients = parseClientsTable(tableText);
|
||||||
const peerByKey = new Map(conf.peers.map((p) => [p.publicKey, p]));
|
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) {
|
async function inferPskFromConf(conf) {
|
||||||
@@ -916,8 +1006,17 @@ function createRuntime(profile) {
|
|||||||
applySyncconf,
|
applySyncconf,
|
||||||
loadState,
|
loadState,
|
||||||
inferPskFromConf,
|
inferPskFromConf,
|
||||||
confPath,
|
get confPath() {
|
||||||
clientsPath,
|
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 {
|
try {
|
||||||
let wgShow = "";
|
let wgShow = "";
|
||||||
try {
|
try {
|
||||||
wgShow = await rt.dockerExec(`${rt.profile.wgBinary} show ${rt.profile.iface}`);
|
wgShow = await rt.dockerExec(`${rt.profile.wgBinary} show ${rt.iface}`);
|
||||||
} catch {
|
} catch {
|
||||||
wgShow = "";
|
wgShow = "";
|
||||||
}
|
}
|
||||||
@@ -2321,7 +2420,7 @@ app.get("/api/clients", requireAuth, async (req, res) => {
|
|||||||
const warpSelected = new Set(
|
const warpSelected = new Set(
|
||||||
warpMeta.supported && warpMeta.installed ? warpMeta.selectedAllowedIps : [],
|
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 rows = [];
|
||||||
const seenClientIds = new Set();
|
const seenClientIds = new Set();
|
||||||
for (const c of clients) {
|
for (const c of clients) {
|
||||||
@@ -2395,6 +2494,9 @@ app.get("/api/clients", requireAuth, async (req, res) => {
|
|||||||
profileId: rt.profile.id,
|
profileId: rt.profile.id,
|
||||||
profileLabel: rt.profile.label,
|
profileLabel: rt.profile.label,
|
||||||
container: (await rt.resolveContainer().catch(() => rt.profile.container)),
|
container: (await rt.resolveContainer().catch(() => rt.profile.container)),
|
||||||
|
confPath: activeConfPath || rt.confPath,
|
||||||
|
clientsPath: activeClientsPath || rt.clientsPath,
|
||||||
|
iface: rt.iface,
|
||||||
protocol: "AmneziaWG",
|
protocol: "AmneziaWG",
|
||||||
peerCount: conf.peers.length,
|
peerCount: conf.peers.length,
|
||||||
clients: rows,
|
clients: rows,
|
||||||
@@ -2453,19 +2555,21 @@ app.get("/api/clients/source-report", requireAuth, async (req, res) => {
|
|||||||
const rt = runtimeFromExportRequest(req);
|
const rt = runtimeFromExportRequest(req);
|
||||||
try {
|
try {
|
||||||
const container = await rt.resolveContainer();
|
const container = await rt.resolveContainer();
|
||||||
|
const activeConfPath = await rt.resolveConfPath();
|
||||||
|
const activeClientsPath = await rt.resolveClientsPath();
|
||||||
let confText = "";
|
let confText = "";
|
||||||
let tableText = "";
|
let tableText = "";
|
||||||
let conf = { peers: [] };
|
let conf = { peers: [] };
|
||||||
let clients = [];
|
let clients = [];
|
||||||
let clientsError = null;
|
let clientsError = null;
|
||||||
try {
|
try {
|
||||||
confText = await rt.dockerReadFile(rt.confPath);
|
confText = await rt.dockerReadFile(activeConfPath);
|
||||||
conf = splitAwgConf(confText);
|
conf = splitAwgConf(confText);
|
||||||
} catch (e) {
|
} 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 {
|
try {
|
||||||
tableText = await rt.dockerReadFile(rt.clientsPath);
|
tableText = await rt.dockerReadFile(activeClientsPath);
|
||||||
clients = parseClientsTable(tableText);
|
clients = parseClientsTable(tableText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clientsError = String(e.message || e);
|
clientsError = String(e.message || e);
|
||||||
@@ -2478,8 +2582,9 @@ app.get("/api/clients/source-report", requireAuth, async (req, res) => {
|
|||||||
profileId: rt.profile.id,
|
profileId: rt.profile.id,
|
||||||
profileLabel: rt.profile.label,
|
profileLabel: rt.profile.label,
|
||||||
container,
|
container,
|
||||||
confPath: rt.confPath,
|
confPath: activeConfPath,
|
||||||
clientsPath: rt.clientsPath,
|
iface: rt.iface,
|
||||||
|
clientsPath: activeClientsPath,
|
||||||
confBytes: Buffer.byteLength(confText, "utf8"),
|
confBytes: Buffer.byteLength(confText, "utf8"),
|
||||||
clientsTableBytes: Buffer.byteLength(tableText, "utf8"),
|
clientsTableBytes: Buffer.byteLength(tableText, "utf8"),
|
||||||
peerCount: conf.peers.length,
|
peerCount: conf.peers.length,
|
||||||
|
|||||||
Reference in New Issue
Block a user