v1.3.0: AWG 3.1/3.0/2.0/1.x/WireGuard from the panel, real cascade, server domain, light/dark theme

- client .conf: only obfuscation keys present on the server (AWG 1.x got S3/S4/I1 -> no traffic)
- install.sh no longer puts panel-managed containers into AWG_PROFILES (wrong instance on switch)
- UDP port busy check via docker instead of ss inside the panel container
- default profile never hijacks a managed container

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Бобырев Андрей
2026-10-23 00:42:16 +03:00
parent 6e05a366c9
commit 75345ebc03
8 changed files with 364 additions and 126 deletions

View File

@@ -629,6 +629,7 @@ loginForm.addEventListener("submit", async (ev) => {
await loadTimeSyncCaps();
await loadClients();
await loadInstances();
await loadSettings();
} catch (e) {
loginError.textContent = String(e.message || e);
}
@@ -647,6 +648,7 @@ logoutBtn.addEventListener("click", async () => {
refreshBtn.addEventListener("click", () => {
loadClients();
loadInstances();
loadSettings();
});
const cascadeForm = document.querySelector("#cascade-form");
@@ -1730,6 +1732,7 @@ async function boot() {
await loadTimeSyncCaps();
await loadClients();
await loadInstances();
await loadSettings();
} else {
showLogin();
loginPassword.focus();
@@ -1738,3 +1741,50 @@ async function boot() {
initMtprotoLinkCopy();
boot();
// ── Тема, адрес сервера, вышестоящий сервер каскада ──
document.querySelector("#theme-toggle")?.addEventListener("click", () => {
const next = document.documentElement.dataset.theme === "light" ? "dark" : "light";
document.documentElement.dataset.theme = next;
try { localStorage.setItem("theme", next); } catch { /* private mode */ }
});
async function loadSettings() {
const input = document.querySelector("#settings-endpoint");
const hint = document.querySelector("#settings-hint");
if (!input) return;
try {
const st = await api("/api/settings");
input.value = st.endpointHost || "";
if (hint) hint.textContent = `Сейчас в конфигах клиентов: ${st.effectiveEndpoint || "адрес из браузера"}. Новый адрес действует на все выгрузки .conf, в том числе старых клиентов.`;
} catch { /* старый сервер без /api/settings */ }
}
document.querySelector("#settings-form")?.addEventListener("submit", async (ev) => {
ev.preventDefault();
const endpointHost = document.querySelector("#settings-endpoint")?.value.trim() || "";
try {
const r = await api("/api/settings", { method: "POST", body: JSON.stringify({ endpointHost }) });
setStatus(r.resolved ? `Адрес сохранён: ${endpointHost} → ${r.resolved.join(", ")}` : "Адрес сохранён.", false);
await loadSettings();
} catch (e) {
setStatus(String(e.message || e), true);
}
});
document.querySelector("#upstream-form")?.addEventListener("submit", async (ev) => {
ev.preventDefault();
const configText = document.querySelector("#upstream-conf")?.value || "";
const b = ev.target.querySelector("button[type=submit]");
if (b) b.disabled = true;
try {
setStatus("Подключаю вышестоящий сервер…", false);
const r = await api("/api/cascade/upstream", { method: "POST", body: JSON.stringify({ configText }) });
setStatus(r.handshake ? "Каскад подключён: рукопожатие есть. Отметьте клиентов ниже и нажмите «Применить»." : "Туннель поднят, но рукопожатия пока нет — проверьте Endpoint и UDP-порт вышестоящего сервера.", !r.handshake);
await loadClients();
} catch (e) {
setStatus(String(e.message || e), true);
} finally {
if (b) b.disabled = false;
}
});

View File

@@ -7,7 +7,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=1.2.28">
<script>try{var t=localStorage.getItem("theme")||(matchMedia("(prefers-color-scheme: light)").matches?"light":"dark");document.documentElement.dataset.theme=t}catch(e){}</script>
<link rel="stylesheet" href="/styles.css?v=1.3.0">
</head>
<body class="page">
<aside class="panel-promo-strip" role="complementary" aria-label="Поддержка и донат">
@@ -58,6 +59,7 @@
</div>
<div class="token-box">
<div class="session-actions">
<button type="button" id="theme-toggle" class="btn ghost full" title="Светлая / тёмная тема">◐ Тема</button>
<button type="button" id="logout" class="btn ghost full">Выйти</button>
</div>
<details class="pw-change">
@@ -121,7 +123,7 @@
<summary class="fold-summary">
<span class="fold-arrow" aria-hidden="true"></span>
<span class="fold-titles">
<span class="fold-h">Cloudflare WARP</span>
<span class="fold-h">Cloudflare WARP / каскад</span>
<span class="muted fold-meta" id="warp-status-line"></span>
</span>
</summary>
@@ -132,6 +134,11 @@
Один раз на хосте 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.
</p>
<form id="upstream-form" class="cascade-form">
<label for="upstream-conf"><strong>Каскад через другой сервер</strong> — вставьте клиентский .conf (AmneziaWG или WireGuard) другого сервера. Он станет выходом вместо WARP: отмеченные ниже клиенты пойдут в интернет через тот сервер.</label>
<textarea id="upstream-conf" rows="6" placeholder="[Interface]&#10;PrivateKey = …&#10;[Peer]&#10;Endpoint = …" autocomplete="off"></textarea>
<button type="submit" class="btn primary">Подключить вышестоящий сервер</button>
</form>
<div id="warp-actions" class="warp-actions"></div>
<div id="warp-client-list" class="warp-client-list"></div>
<details class="raw warp-raw">
@@ -198,8 +205,10 @@
<label for="instance-variant">Протокол</label>
<select id="instance-variant" class="form-select">
<option value="awg2">AmneziaWG 2.0 (рекомендуется)</option>
<option value="awg">AmneziaWG</option>
<option value="legacy">AmneziaWG Legacy</option>
<option value="awg31">AmneziaWG 3.1 — новейший, свежее приложение Amnezia</option>
<option value="awg3">AmneziaWG 3.0</option>
<option value="awg">AmneziaWG 1.x — роутеры Keenetic/OpenWrt, старые приложения</option>
<option value="legacy">WireGuard (Legacy) — любой WG-клиент, без обфускации</option>
</select>
</div>
<div class="instance-form-row">
@@ -208,7 +217,15 @@
</div>
<button type="submit" class="btn primary">+ Развернуть инстанс</button>
</form>
<p class="muted instance-hint">Создаёт серверный контейнер AmneziaWG из публичного образа, генерирует ключи и NAT. Затем в «Пользователях» выберите инстанс и создавайте клиентов.</p>
<p class="muted instance-hint">Создаёт серверный контейнер AmneziaWG из публичного образа, генерирует ключи и NAT. Затем в «Пользователях» выберите инстанс и создавайте клиентов. Можно держать несколько версий рядом на разных портах: 3.1/2.0 — для телефонов и ПК, 1.x или WireGuard — для роутеров. Не забудьте открыть UDP-порт в фаерволе хостинга.</p>
<form id="settings-form" class="instance-form">
<div class="instance-form-row">
<label for="settings-endpoint">Домен или IP сервера в конфигах клиентов</label>
<input id="settings-endpoint" type="text" placeholder="vpn.example.com — пусто: IP из установки" autocomplete="off">
</div>
<button type="submit" class="btn primary">Сохранить адрес</button>
</form>
<p id="settings-hint" class="muted instance-hint"></p>
</div>
</details>
@@ -406,6 +423,6 @@
</div>
</dialog>
<script src="/app.js?v=1.2.28" type="module"></script>
<script src="/app.js?v=1.3.0" type="module"></script>
</body>
</html>

View File

@@ -8,6 +8,10 @@
--danger: #fb7185;
--ok: #4ade80;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
--field: #0a0f16;
--text-soft: #cbd5f5;
--overlay: rgba(10, 15, 22, 0.92);
color-scheme: dark;
font-family: "DM Sans", system-ui, sans-serif;
}
@@ -15,6 +19,28 @@
box-sizing: border-box;
}
/* Светлая тема: те же переменные, переключатель в шапке (localStorage "theme"). */
html[data-theme="light"] {
--bg: #f4f6fb;
--card: #ffffff;
--line: rgba(15, 23, 42, 0.12);
--text: #0f172a;
--muted: #526077;
--accent: #0369a1;
--danger: #be123c;
--ok: #15803d;
--shadow: 0 12px 40px rgba(15, 23, 42, 0.12);
--field: #ffffff;
--text-soft: #1e293b;
--overlay: rgba(255, 255, 255, 0.94);
color-scheme: light;
}
html[data-theme="light"] .panel-promo-strip {
background: linear-gradient(90deg, #e0f2fe, #eef2ff);
border-bottom-color: rgba(3, 105, 161, 0.25);
}
html[data-theme="light"] .panel-promo-a { color: #92400e; }
body.page {
margin: 0;
background: radial-gradient(circle at 20% 20%, rgba(125, 211, 252, 0.08), transparent 35%),
@@ -123,7 +149,7 @@ h1 {
border: 1px solid var(--line);
border-radius: 14px;
padding: 1rem;
background: #0c121b;
background: var(--field);
}
.token-box label {
@@ -191,7 +217,7 @@ h1 {
padding: 0.65rem 0.75rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font: inherit;
margin-bottom: 0.75rem;
@@ -255,7 +281,7 @@ h1 {
padding: 0.55rem 0.65rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font: inherit;
}
@@ -265,7 +291,7 @@ h1 {
padding: 0.65rem 0.75rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 0.85rem;
@@ -294,7 +320,7 @@ h1 {
padding: 0.45rem 0.55rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font: inherit;
font-size: 0.85rem;
@@ -407,7 +433,7 @@ h1 {
padding: 0.45rem 0.55rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font: inherit;
font-size: 0.85rem;
@@ -715,7 +741,7 @@ tr:last-child td {
.ip {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 0.85rem;
color: #dbeafe;
color: var(--text-soft);
}
.badge {
@@ -736,7 +762,7 @@ tr:last-child td {
.badge.off {
border-color: rgba(148, 163, 184, 0.35);
color: #e2e8f0;
color: var(--text-soft);
background: rgba(148, 163, 184, 0.06);
}
@@ -833,11 +859,11 @@ tr:last-child td {
.cascade-snippet {
white-space: pre-wrap;
word-break: break-word;
background: #0a0f16;
background: var(--field);
border: 1px solid var(--line);
padding: 0.75rem 0.95rem;
border-radius: 12px;
color: #cbd5f5;
color: var(--text-soft);
font-size: 0.78rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
margin: 0.5rem 0 0.85rem;
@@ -884,11 +910,11 @@ tr:last-child td {
.raw pre {
white-space: pre-wrap;
word-break: break-word;
background: #0a0f16;
background: var(--field);
border: 1px solid var(--line);
padding: 0.75rem;
border-radius: 12px;
color: #cbd5f5;
color: var(--text-soft);
font-size: 0.78rem;
}
@@ -927,7 +953,7 @@ tr:last-child td {
padding: 0.55rem 0.65rem;
border-radius: 10px;
border: 1px solid var(--line);
background: #0a0f16;
background: var(--field);
color: var(--text);
font: inherit;
}
@@ -975,7 +1001,7 @@ tr:last-child td {
margin-top: auto;
padding: 1.25rem clamp(1rem, 3vw, 1.5rem) 1.5rem;
border-top: 1px solid var(--line);
background: rgba(10, 15, 22, 0.92);
background: var(--overlay);
}
.support-line {
@@ -1227,3 +1253,12 @@ tr:last-child td {
.instance-empty { padding: 12px; }
.inst-src { font-size: 0.6rem; color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: 2px 7px; vertical-align: middle; }
/* Светлая тема: пастельные надписи тёмной темы на белом не читаются */
html[data-theme="light"] .clock-zone-diff--accent,
html[data-theme="light"] .cascade-guide a,
html[data-theme="light"] .cascade-guide a:hover { color: #0369a1; }
html[data-theme="light"] .btn.warn { color: #9f1239; }
html[data-theme="light"] .status.err { color: #b91c1c; }
html[data-theme="light"] .badge.on { color: #166534; }
html[data-theme="light"] body.page { background: var(--bg); }