fix: runtime auto-discovery of AmneziaWG container

Tester hit 'No such container: amnezia-awg' after a stock install
on a VPS where Amnezia created the WG container as amnezia-awg2.
Install-time detection was not enough.

The panel now resolves the real container at runtime: if the
configured AWG_CONTAINER lacks the conf file (or does not exist),
it scans running containers for one holding awg0.conf, preferring
amnezia-awg* names. All docker exec paths (clients list/create/
enable/disable/delete, WARP, host-setup) and the API container
field use the resolved name. Clear error lists running containers
if none match.

Result: install -> login -> change password -> create client works
with no manual AWG_CONTAINER/AWG_PROFILES on single-instance setups.
This commit is contained in:
andrey271192
2026-06-16 21:11:36 +03:00
parent e498e98e12
commit f1b7fa522c

View File

@@ -450,7 +450,7 @@ async function dockerRestartContainer(container) {
async function warpFileExists(rt, remotePath) { async function warpFileExists(rt, remotePath) {
try { try {
await execDocker(["exec", rt.profile.container, "test", "-f", remotePath]); await execDocker(["exec", await rt.resolveContainer(), "test", "-f", remotePath]);
return true; return true;
} catch { } catch {
return false; return false;
@@ -459,7 +459,7 @@ async function warpFileExists(rt, remotePath) {
async function warpInterfaceUp(rt) { async function warpInterfaceUp(rt) {
try { try {
await execDocker(["exec", rt.profile.container, "ip", "addr", "show", "warp"]); await execDocker(["exec", await rt.resolveContainer(), "ip", "addr", "show", "warp"]);
return true; return true;
} catch { } catch {
return false; return false;
@@ -497,7 +497,7 @@ ip route flush table 100 2>/dev/null || true
exit 0 exit 0
`; `;
try { try {
await dockerExecStdin(rt.profile.container, sh); await dockerExecStdin(await rt.resolveContainer(), sh);
} catch { } catch {
/* ignore */ /* ignore */
} }
@@ -583,7 +583,7 @@ async function warpPatchStartSh(rt, ips) {
"fi", "fi",
"", "",
].join("\n"); ].join("\n");
await dockerExecStdin(rt.profile.container, remote); await dockerExecStdin(await rt.resolveContainer(), remote);
} }
async function warpPersistAndRestart(rt, selectedIps) { async function warpPersistAndRestart(rt, selectedIps) {
@@ -680,25 +680,87 @@ function peerUsesWarp(peer, selectedSet) {
return false; return false;
} }
async function listRunningContainerNames() {
try {
const { stdout } = await execDocker(["ps", "--format", "{{.Names}}"]);
return stdout.split("\n").map((x) => x.trim()).filter(Boolean);
} catch {
return [];
}
}
async function containerHasFile(name, filePath) {
try {
await execDocker(["exec", name, "test", "-f", filePath]);
return true;
} catch {
return false;
}
}
// Find a running container that actually holds the AmneziaWG config file.
// Lets the panel work even if AWG_CONTAINER points at the wrong name
// (e.g. configured "amnezia-awg" but Amnezia created "amnezia-awg2").
async function discoverAwgContainer(confPath, preferredName) {
const names = await listRunningContainerNames();
const score = (n) => {
if (preferredName && n === preferredName) return 0;
if (/^amnezia-?awg/i.test(n)) return 1;
if (/^amnezia/i.test(n)) return 2;
if (/awg|wireguard|wg/i.test(n)) return 3;
return 4;
};
const ordered = [...names].sort((a, b) => score(a) - score(b));
for (const n of ordered) {
if (await containerHasFile(n, confPath)) return n;
}
return null;
}
function createRuntime(profile) { function createRuntime(profile) {
const container = profile.container; let resolvedContainer = 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;
async function resolveContainer() {
if (resolvedContainer) return resolvedContainer;
if (profile.container && (await containerHasFile(profile.container, confPath))) {
resolvedContainer = profile.container;
return resolvedContainer;
}
const found = await discoverAwgContainer(confPath, profile.container);
if (found) {
resolvedContainer = found;
if (found !== profile.container) {
console.log(
`\u2192 AWG container авто: «${found}» (профиль «${profile.label || profile.id}» указывал «${profile.container}»)`,
);
}
return resolvedContainer;
}
const running = (await listRunningContainerNames()).join(", ") || "(нет запущенных)";
throw new Error(
`Контейнер AmneziaWG не найден. Профиль указывает «${profile.container}», но контейнера с таким именем нет и ни в одном запущенном нет файла ${confPath}. Запущенные контейнеры: ${running}. Проверьте, что инстанс Amnezia запущен, или задайте AWG_CONTAINER/AWG_PROFILES.`,
);
}
async function dockerExec(cmd) { async function dockerExec(cmd) {
const container = await resolveContainer();
const { stdout, stderr } = await execDocker(["exec", container, "sh", "-c", cmd]); const { stdout, stderr } = await execDocker(["exec", container, "sh", "-c", cmd]);
return stdout + stderr; return stdout + stderr;
} }
async function dockerReadFile(remotePath) { async function dockerReadFile(remotePath) {
const container = await resolveContainer();
const { stdout } = await execDocker(["exec", container, "cat", remotePath]); const { stdout } = await execDocker(["exec", container, "cat", remotePath]);
return stdout; return stdout;
} }
async function dockerWriteFile(remotePath, content) { async function dockerWriteFile(remotePath, content) {
const container = await resolveContainer();
await execDocker( await execDocker(
[ [
"exec", "exec",
@@ -749,6 +811,7 @@ function createRuntime(profile) {
return { return {
profile, profile,
resolveContainer,
dockerExec, dockerExec,
dockerReadFile, dockerReadFile,
dockerWriteFile, dockerWriteFile,
@@ -1785,7 +1848,7 @@ app.post("/api/warp/host-setup", requireAuth, requireProTier, async (req, res) =
return res.status(400).json({ error: "Ожидается cmd: install или uninstall" }); return res.status(400).json({ error: "Ожидается cmd: install или uninstall" });
} }
const rt = runtimeForRequest(req); const rt = runtimeForRequest(req);
const container = String(rt.profile.container || "").trim(); const container = String((await rt.resolveContainer()) || "").trim();
if (!/^[a-zA-Z0-9_.-]+$/.test(container)) { if (!/^[a-zA-Z0-9_.-]+$/.test(container)) {
return res.status(400).json({ error: "Некорректное имя контейнера в профиле AWG" }); return res.status(400).json({ error: "Некорректное имя контейнера в профиле AWG" });
} }
@@ -1941,7 +2004,7 @@ app.get("/api/clients", requireAuth, async (req, res) => {
res.json({ res.json({
profileId: rt.profile.id, profileId: rt.profile.id,
profileLabel: rt.profile.label, profileLabel: rt.profile.label,
container: rt.profile.container, container: (await rt.resolveContainer().catch(() => rt.profile.container)),
protocol: "AmneziaWG", protocol: "AmneziaWG",
peerCount: conf.peers.length, peerCount: conf.peers.length,
clients: rows, clients: rows,