mirror of
https://github.com/andrey271192/amnezia_web-PRO.git
synced 2026-11-04 00:43:15 +00:00
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:
18
README.md
18
README.md
@@ -6,6 +6,24 @@
|
|||||||
|
|
||||||
Base project **[amnezia_web](https://github.com/andrey271192/amnezia_web)** remains a lighter open panel. This repository contains the full PRO feature set by default.
|
Base project **[amnezia_web](https://github.com/andrey271192/amnezia_web)** remains a lighter open panel. This repository contains the full PRO feature set by default.
|
||||||
|
|
||||||
|
## Что нового в 1.3.0
|
||||||
|
|
||||||
|
- **Установка протоколов прямо с сайта** (раздел «Протоколы / инстансы»), несколько версий рядом на разных портах:
|
||||||
|
| Вариант | Для кого |
|
||||||
|
|---|---|
|
||||||
|
| AmneziaWG 3.1 | новейший: защита заголовков, случайные хвосты; свежее приложение Amnezia |
|
||||||
|
| AmneziaWG 3.0 | HeaderProtectionKey, паддинг |
|
||||||
|
| AmneziaWG 2.0 | рекомендуемый по умолчанию (S3/S4) |
|
||||||
|
| AmneziaWG 1.x | роутеры Keenetic/OpenWrt и старые приложения (без S3/S4) |
|
||||||
|
| WireGuard (Legacy) | любой WG-клиент, без обфускации |
|
||||||
|
- **Исправлено:** конфиг клиента для AWG 1.x получал лишние S3/S4/I1 — рукопожатие было, интернета нет. Теперь в .conf попадают только параметры, которые есть на сервере.
|
||||||
|
- **Исправлено:** при обновлении установщик записывал контейнеры панели в `AWG_PROFILES`, и переключение инстанса создавало клиента не там.
|
||||||
|
- **Исправлено:** проверка занятого UDP-порта смотрела внутрь контейнера панели и ничего не находила.
|
||||||
|
- **Домен сервера** — поле «Домен или IP сервера в конфигах клиентов»: подставляется в Endpoint всех выгрузок, домен проверяется на резолв.
|
||||||
|
- **Настоящий каскад** — в разделе «Cloudflare WARP / каскад» вставьте клиентский .conf другого сервера: отмеченные клиенты выходят в интернет через него (переживает перезапуск контейнера).
|
||||||
|
- **Светлая / тёмная тема** — кнопка «◐ Тема» в шапке (по умолчанию как в системе).
|
||||||
|
- Старые установки не затрагиваются: контейнеры, поставленные приложением Amnezia, остаются как были.
|
||||||
|
|
||||||
## Что открывается по какому порту
|
## Что открывается по какому порту
|
||||||
|
|
||||||
| Адрес | Назначение |
|
| Адрес | Назначение |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amnezia-admin",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.39",
|
"version": "1.3.0",
|
||||||
"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",
|
||||||
|
|||||||
@@ -629,6 +629,7 @@ loginForm.addEventListener("submit", async (ev) => {
|
|||||||
await loadTimeSyncCaps();
|
await loadTimeSyncCaps();
|
||||||
await loadClients();
|
await loadClients();
|
||||||
await loadInstances();
|
await loadInstances();
|
||||||
|
await loadSettings();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loginError.textContent = String(e.message || e);
|
loginError.textContent = String(e.message || e);
|
||||||
}
|
}
|
||||||
@@ -647,6 +648,7 @@ logoutBtn.addEventListener("click", async () => {
|
|||||||
refreshBtn.addEventListener("click", () => {
|
refreshBtn.addEventListener("click", () => {
|
||||||
loadClients();
|
loadClients();
|
||||||
loadInstances();
|
loadInstances();
|
||||||
|
loadSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
const cascadeForm = document.querySelector("#cascade-form");
|
const cascadeForm = document.querySelector("#cascade-form");
|
||||||
@@ -1730,6 +1732,7 @@ async function boot() {
|
|||||||
await loadTimeSyncCaps();
|
await loadTimeSyncCaps();
|
||||||
await loadClients();
|
await loadClients();
|
||||||
await loadInstances();
|
await loadInstances();
|
||||||
|
await loadSettings();
|
||||||
} else {
|
} else {
|
||||||
showLogin();
|
showLogin();
|
||||||
loginPassword.focus();
|
loginPassword.focus();
|
||||||
@@ -1738,3 +1741,50 @@ async function boot() {
|
|||||||
|
|
||||||
initMtprotoLinkCopy();
|
initMtprotoLinkCopy();
|
||||||
boot();
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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>
|
</head>
|
||||||
<body class="page">
|
<body class="page">
|
||||||
<aside class="panel-promo-strip" role="complementary" aria-label="Поддержка и донат">
|
<aside class="panel-promo-strip" role="complementary" aria-label="Поддержка и донат">
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="token-box">
|
<div class="token-box">
|
||||||
<div class="session-actions">
|
<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>
|
<button type="button" id="logout" class="btn ghost full">Выйти</button>
|
||||||
</div>
|
</div>
|
||||||
<details class="pw-change">
|
<details class="pw-change">
|
||||||
@@ -121,7 +123,7 @@
|
|||||||
<summary class="fold-summary">
|
<summary class="fold-summary">
|
||||||
<span class="fold-arrow" aria-hidden="true"></span>
|
<span class="fold-arrow" aria-hidden="true"></span>
|
||||||
<span class="fold-titles">
|
<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 class="muted fold-meta" id="warp-status-line"></span>
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
@@ -132,6 +134,11 @@
|
|||||||
Один раз на хосте VPS под root: в каталоге с репозиторием выполните <code class="inline">bash scripts/warp-amnezia.sh install</code>.
|
Один раз на хосте 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.
|
Затем отметьте клиентов ниже и нажмите «Применить» (контейнер AWG перезапустится). Установить или удалить WARP с хоста можно кнопками ниже (пароль root по SSH — как при синхронизации времени). Убрать вручную: <code class="inline">bash scripts/warp-amnezia.sh uninstall</code> — см. README.
|
||||||
</p>
|
</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] PrivateKey = … [Peer] Endpoint = …" autocomplete="off"></textarea>
|
||||||
|
<button type="submit" class="btn primary">Подключить вышестоящий сервер</button>
|
||||||
|
</form>
|
||||||
<div id="warp-actions" class="warp-actions"></div>
|
<div id="warp-actions" class="warp-actions"></div>
|
||||||
<div id="warp-client-list" class="warp-client-list"></div>
|
<div id="warp-client-list" class="warp-client-list"></div>
|
||||||
<details class="raw warp-raw">
|
<details class="raw warp-raw">
|
||||||
@@ -198,8 +205,10 @@
|
|||||||
<label for="instance-variant">Протокол</label>
|
<label for="instance-variant">Протокол</label>
|
||||||
<select id="instance-variant" class="form-select">
|
<select id="instance-variant" class="form-select">
|
||||||
<option value="awg2">AmneziaWG 2.0 (рекомендуется)</option>
|
<option value="awg2">AmneziaWG 2.0 (рекомендуется)</option>
|
||||||
<option value="awg">AmneziaWG</option>
|
<option value="awg31">AmneziaWG 3.1 — новейший, свежее приложение Amnezia</option>
|
||||||
<option value="legacy">AmneziaWG Legacy</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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="instance-form-row">
|
<div class="instance-form-row">
|
||||||
@@ -208,7 +217,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn primary">+ Развернуть инстанс</button>
|
<button type="submit" class="btn primary">+ Развернуть инстанс</button>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -406,6 +423,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="/app.js?v=1.2.28" type="module"></script>
|
<script src="/app.js?v=1.3.0" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
--danger: #fb7185;
|
--danger: #fb7185;
|
||||||
--ok: #4ade80;
|
--ok: #4ade80;
|
||||||
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
|
--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;
|
font-family: "DM Sans", system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,6 +19,28 @@
|
|||||||
box-sizing: border-box;
|
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 {
|
body.page {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: radial-gradient(circle at 20% 20%, rgba(125, 211, 252, 0.08), transparent 35%),
|
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: 1px solid var(--line);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: #0c121b;
|
background: var(--field);
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-box label {
|
.token-box label {
|
||||||
@@ -191,7 +217,7 @@ h1 {
|
|||||||
padding: 0.65rem 0.75rem;
|
padding: 0.65rem 0.75rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #0a0f16;
|
background: var(--field);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
@@ -255,7 +281,7 @@ h1 {
|
|||||||
padding: 0.55rem 0.65rem;
|
padding: 0.55rem 0.65rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #0a0f16;
|
background: var(--field);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
@@ -265,7 +291,7 @@ h1 {
|
|||||||
padding: 0.65rem 0.75rem;
|
padding: 0.65rem 0.75rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #0a0f16;
|
background: var(--field);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: "JetBrains Mono", ui-monospace, monospace;
|
font-family: "JetBrains Mono", ui-monospace, monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -294,7 +320,7 @@ h1 {
|
|||||||
padding: 0.45rem 0.55rem;
|
padding: 0.45rem 0.55rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #0a0f16;
|
background: var(--field);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -407,7 +433,7 @@ h1 {
|
|||||||
padding: 0.45rem 0.55rem;
|
padding: 0.45rem 0.55rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #0a0f16;
|
background: var(--field);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -715,7 +741,7 @@ tr:last-child td {
|
|||||||
.ip {
|
.ip {
|
||||||
font-family: "JetBrains Mono", ui-monospace, monospace;
|
font-family: "JetBrains Mono", ui-monospace, monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #dbeafe;
|
color: var(--text-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
@@ -736,7 +762,7 @@ tr:last-child td {
|
|||||||
|
|
||||||
.badge.off {
|
.badge.off {
|
||||||
border-color: rgba(148, 163, 184, 0.35);
|
border-color: rgba(148, 163, 184, 0.35);
|
||||||
color: #e2e8f0;
|
color: var(--text-soft);
|
||||||
background: rgba(148, 163, 184, 0.06);
|
background: rgba(148, 163, 184, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,11 +859,11 @@ tr:last-child td {
|
|||||||
.cascade-snippet {
|
.cascade-snippet {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
background: #0a0f16;
|
background: var(--field);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
padding: 0.75rem 0.95rem;
|
padding: 0.75rem 0.95rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: #cbd5f5;
|
color: var(--text-soft);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
margin: 0.5rem 0 0.85rem;
|
margin: 0.5rem 0 0.85rem;
|
||||||
@@ -884,11 +910,11 @@ tr:last-child td {
|
|||||||
.raw pre {
|
.raw pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
background: #0a0f16;
|
background: var(--field);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: #cbd5f5;
|
color: var(--text-soft);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,7 +953,7 @@ tr:last-child td {
|
|||||||
padding: 0.55rem 0.65rem;
|
padding: 0.55rem 0.65rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #0a0f16;
|
background: var(--field);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
@@ -975,7 +1001,7 @@ tr:last-child td {
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding: 1.25rem clamp(1rem, 3vw, 1.5rem) 1.5rem;
|
padding: 1.25rem clamp(1rem, 3vw, 1.5rem) 1.5rem;
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
background: rgba(10, 15, 22, 0.92);
|
background: var(--overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-line {
|
.support-line {
|
||||||
@@ -1227,3 +1253,12 @@ tr:last-child td {
|
|||||||
.instance-empty { padding: 12px; }
|
.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; }
|
.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); }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Deploy / remove an AmneziaWG server instance from public images — no Amnezia app.
|
# Deploy / remove an AmneziaWG server instance from public images — no Amnezia app.
|
||||||
# Variants: awg2 (AmneziaWG 2.0, awg-go 2.0.0), awg (classic, awg-go 0.2.18),
|
# Variants: awg31 (AmneziaWG 3.1), awg3 (3.0), awg2 (2.0, awg-go 2.0.0),
|
||||||
# legacy (kernel WireGuard, amnezia-wg).
|
# awg (1.x for routers/old apps, awg-go 0.2.18), legacy (plain WireGuard, amnezia-wg).
|
||||||
# Usage:
|
# Usage:
|
||||||
# awg-instance.sh create <variant> <port> [name]
|
# awg-instance.sh create <variant> <port> [name]
|
||||||
# awg-instance.sh remove <name>
|
# awg-instance.sh remove <name>
|
||||||
@@ -12,6 +12,9 @@ INSTANCES_DIR="${INSTANCES_DIR:-/opt/amnezia-instances}"
|
|||||||
IMG_AWG2="${IMG_AWG2:-amneziavpn/amneziawg-go:2.0.0}"
|
IMG_AWG2="${IMG_AWG2:-amneziavpn/amneziawg-go:2.0.0}"
|
||||||
IMG_AWG="${IMG_AWG:-amneziavpn/amneziawg-go:0.2.18}"
|
IMG_AWG="${IMG_AWG:-amneziavpn/amneziawg-go:0.2.18}"
|
||||||
IMG_LEGACY="${IMG_LEGACY:-amneziavpn/amnezia-wg:latest}"
|
IMG_LEGACY="${IMG_LEGACY:-amneziavpn/amnezia-wg:latest}"
|
||||||
|
# 3.x pinned by digest: tags on Docker Hub get rewritten (tools inside differ from the tag name)
|
||||||
|
IMG_AWG3="${IMG_AWG3:-amneziavpn/amneziawg-go:3.0.20260805@sha256:8447c91637c37536dd99b8bbd4420c819ac9f330f047804197291625bfb0ea8a}"
|
||||||
|
IMG_AWG31="${IMG_AWG31:-amneziavpn/amneziawg-go:3.1.20260828@sha256:cbafc02b8373a83f428272db6d8001b37bc02e6211cbd8c0cb4e2e3759b12b72}"
|
||||||
|
|
||||||
err() { echo "ERROR: $*" >&2; exit 1; }
|
err() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
@@ -28,10 +31,12 @@ rand_magic() { # single large uint32 (AmneziaWG H header), NOT a range
|
|||||||
|
|
||||||
image_for() {
|
image_for() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
awg31) echo "$IMG_AWG31" ;;
|
||||||
|
awg3) echo "$IMG_AWG3" ;;
|
||||||
awg2) echo "$IMG_AWG2" ;;
|
awg2) echo "$IMG_AWG2" ;;
|
||||||
awg) echo "$IMG_AWG" ;;
|
awg) echo "$IMG_AWG" ;;
|
||||||
legacy) echo "$IMG_LEGACY" ;;
|
legacy) echo "$IMG_LEGACY" ;;
|
||||||
*) err "unknown variant: $1 (awg2|awg|legacy)" ;;
|
*) err "unknown variant: $1 (awg31|awg3|awg2|awg|legacy)" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +68,8 @@ cmd_create() {
|
|||||||
[[ -n "$name" ]] || err "bad name"
|
[[ -n "$name" ]] || err "bad name"
|
||||||
docker inspect "$name" >/dev/null 2>&1 && err "container $name already exists"
|
docker inspect "$name" >/dev/null 2>&1 && err "container $name already exists"
|
||||||
# port free?
|
# port free?
|
||||||
if ss -lun 2>/dev/null | grep -qE "[:.]${port}\b"; then err "udp port $port busy"; fi
|
# this script runs inside the panel container: its own `ss` does not see host ports, docker does
|
||||||
|
if docker ps --format '{{.Ports}}' | tr ',' '\n' | grep -qE ":${port}->[0-9]+/udp"; then err "udp port $port busy (другой контейнер)"; fi
|
||||||
|
|
||||||
echo "→ pulling $img"
|
echo "→ pulling $img"
|
||||||
docker pull -q "$img" >/dev/null
|
docker pull -q "$img" >/dev/null
|
||||||
@@ -87,22 +93,37 @@ cmd_create() {
|
|||||||
{
|
{
|
||||||
echo "[Interface]"
|
echo "[Interface]"
|
||||||
echo "PrivateKey = $priv"
|
echo "PrivateKey = $priv"
|
||||||
echo "Address = ${net}.0/24"
|
echo "Address = ${net}.1/24"
|
||||||
echo "ListenPort = $port"
|
echo "ListenPort = $port"
|
||||||
if [[ "$variant" != "legacy" ]]; then
|
if [[ "$variant" != "legacy" ]]; then
|
||||||
|
# 3.x header protection rejects junk sizes <= 12
|
||||||
|
local smin=15
|
||||||
echo "Jc = $(rand_range 3 10)"
|
echo "Jc = $(rand_range 3 10)"
|
||||||
echo "Jmin = 10"
|
echo "Jmin = 10"
|
||||||
echo "Jmax = 50"
|
echo "Jmax = 50"
|
||||||
echo "S1 = $(rand_range 15 60)"
|
echo "S1 = $(rand_range $smin 60)"
|
||||||
echo "S2 = $(rand_range 15 60)"
|
echo "S2 = $(rand_range $smin 60)"
|
||||||
if [[ "$variant" == "awg2" ]]; then
|
if [[ "$variant" != "awg" ]]; then
|
||||||
echo "S3 = $(rand_range 5 40)"
|
echo "S3 = $(rand_range $smin 40)"
|
||||||
echo "S4 = $(rand_range 1 40)"
|
echo "S4 = $(rand_range $smin 40)"
|
||||||
fi
|
fi
|
||||||
echo "H1 = $(rand_magic)"
|
echo "H1 = $(rand_magic)"
|
||||||
echo "H2 = $(rand_magic)"
|
echo "H2 = $(rand_magic)"
|
||||||
echo "H3 = $(rand_magic)"
|
echo "H3 = $(rand_magic)"
|
||||||
echo "H4 = $(rand_magic)"
|
echo "H4 = $(rand_magic)"
|
||||||
|
if [[ "$variant" == awg3* ]]; then
|
||||||
|
echo "HeaderProtectionKey = $(docker run --rm "$img" awg genkey | tr -d '\r\n')"
|
||||||
|
echo "ContentPaddingAddition = 10-100"
|
||||||
|
echo "RekeyAfterTime = 100-120"
|
||||||
|
echo "RekeyTimeout = 3-7"
|
||||||
|
echo "RejectAfterTime = 150-180"
|
||||||
|
echo "KeepaliveTimeout = 5-15"
|
||||||
|
echo "MaxHandshakeAttempts = 15-20"
|
||||||
|
fi
|
||||||
|
if [[ "$variant" == "awg31" ]]; then
|
||||||
|
echo "RandomTrailers = on"
|
||||||
|
echo "DisableCookies = on"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
} > "$conf"
|
} > "$conf"
|
||||||
|
|
||||||
@@ -114,6 +135,13 @@ cmd_create() {
|
|||||||
# start script run inside container
|
# start script run inside container
|
||||||
local usimpl=""
|
local usimpl=""
|
||||||
if [[ "$variant" != "legacy" ]]; then usimpl="export WG_QUICK_USERSPACE_IMPLEMENTATION=amneziawg-go"; fi
|
if [[ "$variant" != "legacy" ]]; then usimpl="export WG_QUICK_USERSPACE_IMPLEMENTATION=amneziawg-go"; fi
|
||||||
|
# 3.x is userspace-only: a loaded amneziawg kernel module (2.0 on the same host) does not know 3.x keys
|
||||||
|
if [[ "$variant" == awg3* ]]; then
|
||||||
|
usimpl="$usimpl"$'\n'"$(cat <<'PATCH'
|
||||||
|
sed -i -e 's/if ! cmd ip link add "$INTERFACE" type amneziawg; then/if true; then/' -e 's|^\([[:space:]]*\)\[\[ -e /sys/module/amneziawg \]\].*|\1:|' /usr/bin/awg-quick
|
||||||
|
PATCH
|
||||||
|
)"
|
||||||
|
fi
|
||||||
cat > "$dir/start.sh" <<EOF
|
cat > "$dir/start.sh" <<EOF
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set +e
|
set +e
|
||||||
@@ -156,7 +184,7 @@ cmd_remove() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
docker ps --format '{{.Names}} {{.Ports}}' | grep -E '^amnezia-(awg2|awg|legacy)-|^amnezia-' || true
|
docker ps --format '{{.Names}} {{.Ports}}' | grep -E '^amnezia-' || true
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ if [[ -z "${AWG_PROFILES:-}" ]] && [[ -f "${AWG_PROFILE_SNAPSHOT}" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
__awg_names="$(docker ps --format '{{.Names}}' 2>/dev/null | awk '/^amnezia-awg/ { print }' | sort || true)"
|
# Контейнеры, развёрнутые самой панелью (/opt/amnezia-instances/<имя>), панель знает сама — в AWG_PROFILES их не кладём.
|
||||||
|
__awg_names="$(docker ps --format '{{.Names}}' 2>/dev/null | awk '/^amnezia-awg/ { print }' | while read -r __n; do [[ -d "/opt/amnezia-instances/${__n}" ]] || echo "${__n}"; done | sort || true)"
|
||||||
__awg_multi_count="$(printf '%s\n' "${__awg_names}" | sed '/^$/d' | wc -l | tr -d '[:space:]')"
|
__awg_multi_count="$(printf '%s\n' "${__awg_names}" | sed '/^$/d' | wc -l | tr -d '[:space:]')"
|
||||||
__profile_container_count=0
|
__profile_container_count=0
|
||||||
if [[ -n "${AWG_PROFILES:-}" ]]; then
|
if [[ -n "${AWG_PROFILES:-}" ]]; then
|
||||||
|
|||||||
275
server.js
275
server.js
@@ -3,6 +3,7 @@ import { spawn, spawnSync } from "child_process";
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import dns from "dns";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { healClientsMeta, readClientsMeta, writeClientsMeta } from "./scripts/clients-meta.mjs";
|
import { healClientsMeta, readClientsMeta, writeClientsMeta } from "./scripts/clients-meta.mjs";
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@ function parseProfilesFromEnv() {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "awg",
|
id: "awg",
|
||||||
|
isDefault: true,
|
||||||
label: process.env.AWG_PROFILE_LABEL || "AmneziaWG",
|
label: process.env.AWG_PROFILE_LABEL || "AmneziaWG",
|
||||||
container: process.env.AWG_CONTAINER || "amnezia-awg",
|
container: process.env.AWG_CONTAINER || "amnezia-awg",
|
||||||
confPath: process.env.AWG_CONF_PATH || "/opt/amnezia/awg/awg0.conf",
|
confPath: process.env.AWG_CONF_PATH || "/opt/amnezia/awg/awg0.conf",
|
||||||
@@ -137,25 +139,36 @@ function parseProfilesFromEnv() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ENV_PROFILES = parseProfilesFromEnv();
|
const ENV_PROFILES = parseProfilesFromEnv();
|
||||||
|
let defaultProfileMissing = false; // set by createRuntime when the default profile resolves to nothing
|
||||||
|
|
||||||
const INSTANCES_DIR = process.env.INSTANCES_DIR || "/opt/amnezia-instances";
|
const INSTANCES_DIR = process.env.INSTANCES_DIR || "/opt/amnezia-instances";
|
||||||
const INSTANCES_FILE = `${process.env.DATA_DIR || "/data"}/instances.json`;
|
const INSTANCES_FILE = `${process.env.DATA_DIR || "/data"}/instances.json`;
|
||||||
const INSTANCE_SCRIPT = `${process.env.APP_DIR || "/app"}/scripts/awg-instance.sh`;
|
const INSTANCE_SCRIPT = `${process.env.APP_DIR || "/app"}/scripts/awg-instance.sh`;
|
||||||
|
|
||||||
const INSTANCE_VARIANTS = {
|
const INSTANCE_VARIANTS = {
|
||||||
|
awg31: {
|
||||||
|
label: "AmneziaWG 3.1",
|
||||||
|
desc: "Новейшая версия: защита заголовков, случайные хвосты пакетов. Нужно свежее приложение Amnezia.",
|
||||||
|
iface: "awg0", binary: "awg",
|
||||||
|
},
|
||||||
|
awg3: {
|
||||||
|
label: "AmneziaWG 3.0",
|
||||||
|
desc: "Защита заголовков (HeaderProtectionKey), паддинг. Приложение Amnezia 4.8.x и новее.",
|
||||||
|
iface: "awg0", binary: "awg",
|
||||||
|
},
|
||||||
awg2: {
|
awg2: {
|
||||||
label: "AmneziaWG 2.0",
|
label: "AmneziaWG 2.0",
|
||||||
desc: "Новая версия протокола на основе awg-go. Расширенная обфускация (S3, S4).",
|
desc: "Новая версия протокола на основе awg-go. Расширенная обфускация (S3, S4).",
|
||||||
iface: "awg0", binary: "awg",
|
iface: "awg0", binary: "awg",
|
||||||
},
|
},
|
||||||
awg: {
|
awg: {
|
||||||
label: "AmneziaWG",
|
label: "AmneziaWG 1.x (роутеры)",
|
||||||
desc: "Версия протокола на основе awg-go. Обфускация S1, S2.",
|
desc: "Обфускация Jc/S1/S2/H1–H4 без S3/S4 — для Keenetic, OpenWrt и старых приложений.",
|
||||||
iface: "awg0", binary: "awg",
|
iface: "awg0", binary: "awg",
|
||||||
},
|
},
|
||||||
legacy: {
|
legacy: {
|
||||||
label: "AmneziaWG Legacy",
|
label: "WireGuard (Legacy)",
|
||||||
desc: "Оригинальная версия на ядре WireGuard. Совместима с клиентами старых версий.",
|
desc: "Чистый WireGuard без обфускации — любой WG-клиент и любые роутеры. Легко блокируется DPI.",
|
||||||
iface: "wg0", binary: "wg",
|
iface: "wg0", binary: "wg",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -219,10 +232,16 @@ function profileIdentityKey(p) {
|
|||||||
// Effective profile set = env profiles + managed instances (deduped by id and target files).
|
// Effective profile set = env profiles + managed instances (deduped by id and target files).
|
||||||
function getProfiles() {
|
function getProfiles() {
|
||||||
const managed = loadManagedProfiles();
|
const managed = loadManagedProfiles();
|
||||||
|
const managedContainers = new Set(managed.map((m) => m.container));
|
||||||
const seenIds = new Set();
|
const seenIds = new Set();
|
||||||
const seenTargets = new Set();
|
const seenTargets = new Set();
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const p of [...ENV_PROFILES, ...managed]) {
|
// install.sh used to put panel-managed containers into AWG_PROFILES too: the env copy then
|
||||||
|
// shadowed the managed one (wrong id, no variant). Managed instance wins.
|
||||||
|
const envProfiles = ENV_PROFILES.filter((p) => !managedContainers.has(p.container));
|
||||||
|
// Default profile (no AWG_PROFILES) that found no container: hide it once the panel has its own instances.
|
||||||
|
const env = managed.length && defaultProfileMissing ? envProfiles.filter((p) => !p.isDefault) : envProfiles;
|
||||||
|
for (const p of [...env, ...managed]) {
|
||||||
const id = String(p.id || "").trim();
|
const id = String(p.id || "").trim();
|
||||||
const target = profileIdentityKey(p);
|
const target = profileIdentityKey(p);
|
||||||
if ((id && seenIds.has(id)) || (target !== "|||" && seenTargets.has(target))) continue;
|
if ((id && seenIds.has(id)) || (target !== "|||" && seenTargets.has(target))) continue;
|
||||||
@@ -546,6 +565,30 @@ async function dockerRestartContainer(container) {
|
|||||||
throw new Error("Контейнер не ответил после restart");
|
throw new Error("Контейнер не ответил после restart");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// awg-quick brings up both AmneziaWG and plain WireGuard configs; old images only have wg-quick.
|
||||||
|
const QUICK_SH = "$(command -v awg-quick || echo wg-quick)";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upstream for a real cascade: a client .conf of ANOTHER server (AWG or WG) becomes the "warp"
|
||||||
|
* interface of this instance. Routing of selected clients reuses the WARP machinery.
|
||||||
|
*/
|
||||||
|
function buildUpstreamConf(text) {
|
||||||
|
const sections = parseWireGuardSections(text);
|
||||||
|
const iface = sections.interface || {};
|
||||||
|
const peer = sections.peer || {};
|
||||||
|
if (!iface.PrivateKey || !peer.PublicKey || !peer.Endpoint) {
|
||||||
|
throw new Error("Нужен клиентский конфиг другого сервера: [Interface] с PrivateKey и [Peer] с PublicKey и Endpoint.");
|
||||||
|
}
|
||||||
|
const lines = ["[Interface]", `PrivateKey = ${iface.PrivateKey}`];
|
||||||
|
if (iface.Address) lines.push(`Address = ${String(iface.Address).split(",")[0].trim()}`);
|
||||||
|
for (const k of AWG_OBF_KEYS) if (iface[k]) lines.push(`${k} = ${iface[k]}`);
|
||||||
|
lines.push(`MTU = ${iface.MTU || 1280}`, "Table = off", "", "[Peer]", `PublicKey = ${peer.PublicKey}`);
|
||||||
|
if (peer.PresharedKey) lines.push(`PresharedKey = ${peer.PresharedKey}`);
|
||||||
|
lines.push("AllowedIPs = 0.0.0.0/0", `Endpoint = ${peer.Endpoint}`, "PersistentKeepalive = 25", "");
|
||||||
|
for (const l of lines) if (/['`$\\]/.test(l)) throw new Error("Недопустимые символы в конфиге.");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
async function warpFileExists(rt, remotePath) {
|
async function warpFileExists(rt, remotePath) {
|
||||||
try {
|
try {
|
||||||
await execDocker(["exec", await rt.resolveContainer(), "test", "-f", remotePath]);
|
await execDocker(["exec", await rt.resolveContainer(), "test", "-f", remotePath]);
|
||||||
@@ -637,7 +680,7 @@ function buildWarpBootBlock(warpConf, ips) {
|
|||||||
return (
|
return (
|
||||||
"# --- WARP-MANAGER BEGIN ---\n\n" +
|
"# --- WARP-MANAGER BEGIN ---\n\n" +
|
||||||
`if [ -f '${warpConf}' ]; then\n` +
|
`if [ -f '${warpConf}' ]; then\n` +
|
||||||
` wg-quick up '${warpConf}' || true\n` +
|
` ${QUICK_SH} up '${warpConf}' || true\n` +
|
||||||
` sleep 3\n` +
|
` sleep 3\n` +
|
||||||
`fi\n\n` +
|
`fi\n\n` +
|
||||||
routing +
|
routing +
|
||||||
@@ -745,7 +788,7 @@ async function warpSummaryForRt(rt) {
|
|||||||
let wgShowWarp = "";
|
let wgShowWarp = "";
|
||||||
if (installed && running) {
|
if (installed && running) {
|
||||||
try {
|
try {
|
||||||
wgShowWarp = await rt.dockerExec("wg show warp 2>/dev/null || true");
|
wgShowWarp = await rt.dockerExec("awg show warp 2>/dev/null || wg show warp 2>/dev/null || true");
|
||||||
} catch {
|
} catch {
|
||||||
wgShowWarp = "";
|
wgShowWarp = "";
|
||||||
}
|
}
|
||||||
@@ -800,7 +843,8 @@ async function containerHasFile(name, filePath) {
|
|||||||
// Lets the panel work even if AWG_CONTAINER points at the wrong name
|
// Lets the panel work even if AWG_CONTAINER points at the wrong name
|
||||||
// (e.g. configured "amnezia-awg" but Amnezia created "amnezia-awg2").
|
// (e.g. configured "amnezia-awg" but Amnezia created "amnezia-awg2").
|
||||||
async function discoverAwgContainer(confPath, preferredName) {
|
async function discoverAwgContainer(confPath, preferredName) {
|
||||||
const names = await listRunningContainerNames();
|
const managed = new Set(loadManagedProfiles().map((m) => m.container));
|
||||||
|
const names = (await listRunningContainerNames()).filter((n) => n === preferredName || !managed.has(n));
|
||||||
const score = (n) => {
|
const score = (n) => {
|
||||||
if (preferredName && n === preferredName) return 0;
|
if (preferredName && n === preferredName) return 0;
|
||||||
if (/^amnezia-?awg/i.test(n)) return 1;
|
if (/^amnezia-?awg/i.test(n)) return 1;
|
||||||
@@ -852,6 +896,7 @@ function createRuntime(profile) {
|
|||||||
if (profile.container) {
|
if (profile.container) {
|
||||||
for (const candidate of confCandidates) {
|
for (const candidate of confCandidates) {
|
||||||
if (await containerHasFile(profile.container, candidate)) {
|
if (await containerHasFile(profile.container, candidate)) {
|
||||||
|
if (profile.isDefault) defaultProfileMissing = false;
|
||||||
resolvedContainer = profile.container;
|
resolvedContainer = profile.container;
|
||||||
return resolvedContainer;
|
return resolvedContainer;
|
||||||
}
|
}
|
||||||
@@ -862,6 +907,7 @@ function createRuntime(profile) {
|
|||||||
found = await discoverAwgContainer(candidate, profile.container);
|
found = await discoverAwgContainer(candidate, profile.container);
|
||||||
if (found) break;
|
if (found) break;
|
||||||
}
|
}
|
||||||
|
if (profile.isDefault) defaultProfileMissing = !found;
|
||||||
if (found) {
|
if (found) {
|
||||||
resolvedContainer = found;
|
resolvedContainer = found;
|
||||||
if (found !== profile.container) {
|
if (found !== profile.container) {
|
||||||
@@ -996,7 +1042,7 @@ function createRuntime(profile) {
|
|||||||
const activeIface = ifaceFromConfPath(activeConfPath, iface);
|
const activeIface = ifaceFromConfPath(activeConfPath, iface);
|
||||||
const activeBinary = await resolveWgBinary();
|
const activeBinary = await resolveWgBinary();
|
||||||
await dockerExec(
|
await dockerExec(
|
||||||
`wg-quick strip '${activeConfPath}' > /tmp/wg-admin-strip.conf && ${activeBinary} syncconf ${activeIface} /tmp/wg-admin-strip.conf`
|
`(awg-quick strip '${activeConfPath}' 2>/dev/null || wg-quick strip '${activeConfPath}') > /tmp/wg-admin-strip.conf && ${activeBinary} syncconf ${activeIface} /tmp/wg-admin-strip.conf`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1186,26 +1232,45 @@ function defaultPeerClientName(peer) {
|
|||||||
return ip ? `Клиент ${ip}` : `Peer ${String(peer?.publicKey || "").slice(0, 10)}…`;
|
return ip ? `Клиент ${ip}` : `Peer ${String(peer?.publicKey || "").slice(0, 10)}…`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Совпадает с defaults Amnezia Desktop (protocolConstants awg, desktop MTU). */
|
|
||||||
const AWG_EXPORT_DEFAULTS = {
|
// Client-side obfuscation keys, in the order Amnezia writes them. 3.x keys must match the server.
|
||||||
Jc: "3",
|
const AWG_OBF_KEYS = [
|
||||||
Jmin: "10",
|
"Jc", "Jmin", "Jmax", "S1", "S2", "S3", "S4", "H1", "H2", "H3", "H4", "I1", "I2", "I3", "I4", "I5",
|
||||||
Jmax: "30",
|
"HeaderProtectionKey", "ContentPaddingAddition", "RekeyAfterTime", "RekeyTimeout", "RejectAfterTime",
|
||||||
S1: "15",
|
"KeepaliveTimeout", "MaxHandshakeAttempts", "RandomTrailers", "DisableCookies",
|
||||||
S2: "18",
|
];
|
||||||
S3: "20",
|
const AWG_OBF_ALIASES = {
|
||||||
S4: "23",
|
Jc: ["junk_packet_count", "junkPacketCount"], Jmin: ["junk_packet_min_size", "junkPacketMinSize"],
|
||||||
H1: "1020325451",
|
Jmax: ["junk_packet_max_size", "junkPacketMaxSize"], S1: ["init_packet_junk_size", "initPacketJunkSize"],
|
||||||
H2: "3288052141",
|
S2: ["response_packet_junk_size", "responsePacketJunkSize"], S3: ["cookie_reply_packet_junk_size", "cookieReplyPacketJunkSize"],
|
||||||
H3: "1766607858",
|
S4: ["transport_packet_junk_size", "transportPacketJunkSize"], H1: ["init_packet_magic_header", "initPacketMagicHeader"],
|
||||||
H4: "2528465083",
|
H2: ["response_packet_magic_header", "responsePacketMagicHeader"], H3: ["underload_packet_magic_header", "underloadPacketMagicHeader"],
|
||||||
I1: "<r 2><b 0x858000010001000000000669636c6f756403636f6d0000010001c00c000100010000105a00044d583737>",
|
H4: ["transport_packet_magic_header", "transportPacketMagicHeader"],
|
||||||
I2: "",
|
I1: ["special_junk_1", "specialJunk1"], I2: ["special_junk_2", "specialJunk2"], I3: ["special_junk_3", "specialJunk3"],
|
||||||
I3: "",
|
I4: ["special_junk_4", "specialJunk4"], I5: ["special_junk_5", "specialJunk5"],
|
||||||
I4: "",
|
HeaderProtectionKey: ["header_protection_key"],
|
||||||
I5: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obfuscation lines for a client .conf. The server [Interface] is the source of truth: a key the
|
||||||
|
* server does not have (e.g. S3/S4 on AWG 1.x) must not appear in the client, otherwise the
|
||||||
|
* handshake passes but data packets are dropped. Keys only in last_config (I1..I5 written by the
|
||||||
|
* Amnezia app) are kept. Returns "" for plain WireGuard servers.
|
||||||
|
*/
|
||||||
|
function awgObfuscationLines(lc, ifaceMap) {
|
||||||
|
if (!ifaceMap.Jc && !pickLc(lc, "Jc", "junk_packet_count", "junkPacketCount")) return "";
|
||||||
|
const lines = [];
|
||||||
|
for (const k of AWG_OBF_KEYS) {
|
||||||
|
const isServerKey = !/^I\d$/.test(k);
|
||||||
|
let v = isServerKey ? ifaceMap[k] : undefined;
|
||||||
|
if (v == null || String(v).trim() === "") v = pickLc(lc, k, ...(AWG_OBF_ALIASES[k] || []));
|
||||||
|
if (!isServerKey && (v == null || String(v).trim() === "")) v = ifaceMap[k];
|
||||||
|
if (isServerKey && ifaceMap.Jc && (ifaceMap[k] == null || ifaceMap[k] === "")) v = undefined;
|
||||||
|
if (v != null && String(v).trim() !== "") lines.push(`${k} = ${String(v).trim()}`);
|
||||||
|
}
|
||||||
|
return lines.length ? `${lines.join("\n")}\n` : "";
|
||||||
|
}
|
||||||
|
|
||||||
function parseLastConfigFromClientRow(row) {
|
function parseLastConfigFromClientRow(row) {
|
||||||
const ud = row?.userData;
|
const ud = row?.userData;
|
||||||
if (!ud || typeof ud !== "object") return null;
|
if (!ud || typeof ud !== "object") return null;
|
||||||
@@ -1490,8 +1555,28 @@ function tunnelClientIpv4(peer, lc, row) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Panel settings editable from the UI (domain for client Endpoint etc.).
|
||||||
|
const SETTINGS_FILE = path.join(process.env.DATA_DIR || "/data", "settings.json");
|
||||||
|
function loadPanelSettings() {
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
|
||||||
|
return o && typeof o === "object" ? o : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function savePanelSettings(next) {
|
||||||
|
fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true });
|
||||||
|
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2));
|
||||||
|
}
|
||||||
|
/** Domain/IP set in the UI wins over CLIENT_CONFIG_ENDPOINT (installer puts the raw IP there). */
|
||||||
|
function configuredEndpointHost() {
|
||||||
|
const ui = String(loadPanelSettings().endpointHost || "").trim();
|
||||||
|
return ui || process.env.CLIENT_CONFIG_ENDPOINT?.trim() || "";
|
||||||
|
}
|
||||||
|
|
||||||
function resolveExportEndpointHost(lc, req) {
|
function resolveExportEndpointHost(lc, req) {
|
||||||
const env = process.env.CLIENT_CONFIG_ENDPOINT?.trim();
|
const env = configuredEndpointHost();
|
||||||
if (env) return env;
|
if (env) return env;
|
||||||
const hn = pickLc(lc, "hostName", "hostname", "host");
|
const hn = pickLc(lc, "hostName", "hostname", "host");
|
||||||
if (hn && String(hn).trim()) return String(hn).trim();
|
if (hn && String(hn).trim()) return String(hn).trim();
|
||||||
@@ -1546,70 +1631,11 @@ async function buildClientConfExport(rt, lc, ifaceMap, req, row, conf) {
|
|||||||
const mtuVal = pickLc(lc, "mtu", "MTU");
|
const mtuVal = pickLc(lc, "mtu", "MTU");
|
||||||
const mtuLine = mtuVal ? `MTU = ${String(mtuVal).trim()}\n` : "";
|
const mtuLine = mtuVal ? `MTU = ${String(mtuVal).trim()}\n` : "";
|
||||||
|
|
||||||
if (activeBinary === "awg") {
|
|
||||||
const Jc = String(pickLc(lc, "Jc", "junk_packet_count", "junkPacketCount") ?? AWG_EXPORT_DEFAULTS.Jc);
|
|
||||||
const Jmin = String(pickLc(lc, "Jmin", "junk_packet_min_size", "junkPacketMinSize") ?? AWG_EXPORT_DEFAULTS.Jmin);
|
|
||||||
const Jmax = String(pickLc(lc, "Jmax", "junk_packet_max_size", "junkPacketMaxSize") ?? AWG_EXPORT_DEFAULTS.Jmax);
|
|
||||||
const S1 = String(pickLc(lc, "S1", "init_packet_junk_size", "initPacketJunkSize") ?? AWG_EXPORT_DEFAULTS.S1);
|
|
||||||
const S2 = String(pickLc(lc, "S2", "response_packet_junk_size", "responsePacketJunkSize") ?? AWG_EXPORT_DEFAULTS.S2);
|
|
||||||
const S3 = String(
|
|
||||||
pickLc(lc, "S3", "cookie_reply_packet_junk_size", "cookieReplyPacketJunkSize") ?? AWG_EXPORT_DEFAULTS.S3,
|
|
||||||
);
|
|
||||||
const S4 = String(
|
|
||||||
pickLc(lc, "S4", "transport_packet_junk_size", "transportPacketJunkSize") ?? AWG_EXPORT_DEFAULTS.S4,
|
|
||||||
);
|
|
||||||
const H1 = String(pickLc(lc, "H1", "init_packet_magic_header", "initPacketMagicHeader") ?? AWG_EXPORT_DEFAULTS.H1);
|
|
||||||
const H2 = String(
|
|
||||||
pickLc(lc, "H2", "response_packet_magic_header", "responsePacketMagicHeader") ?? AWG_EXPORT_DEFAULTS.H2,
|
|
||||||
);
|
|
||||||
const H3 = String(
|
|
||||||
pickLc(lc, "H3", "underload_packet_magic_header", "underloadPacketMagicHeader") ?? AWG_EXPORT_DEFAULTS.H3,
|
|
||||||
);
|
|
||||||
const H4 = String(
|
|
||||||
pickLc(lc, "H4", "transport_packet_magic_header", "transportPacketMagicHeader") ?? AWG_EXPORT_DEFAULTS.H4,
|
|
||||||
);
|
|
||||||
const I1 = String(pickLc(lc, "I1", "special_junk_1", "specialJunk1") ?? AWG_EXPORT_DEFAULTS.I1);
|
|
||||||
const I2 = String(pickLc(lc, "I2", "special_junk_2", "specialJunk2") ?? AWG_EXPORT_DEFAULTS.I2);
|
|
||||||
const I3 = String(pickLc(lc, "I3", "special_junk_3", "specialJunk3") ?? AWG_EXPORT_DEFAULTS.I3);
|
|
||||||
const I4 = String(pickLc(lc, "I4", "special_junk_4", "specialJunk4") ?? AWG_EXPORT_DEFAULTS.I4);
|
|
||||||
const I5 = String(pickLc(lc, "I5", "special_junk_5", "specialJunk5") ?? AWG_EXPORT_DEFAULTS.I5);
|
|
||||||
const iLines = [
|
|
||||||
["I1", I1], ["I2", I2], ["I3", I3], ["I4", I4], ["I5", I5],
|
|
||||||
]
|
|
||||||
.filter(([, v]) => String(v ?? "").trim() !== "")
|
|
||||||
.map(([k, v]) => `${k} = ${v}`)
|
|
||||||
.join("\n");
|
|
||||||
const iBlock = iLines ? `${iLines}\n` : "";
|
|
||||||
|
|
||||||
return `[Interface]
|
|
||||||
Address = ${tunnelIp}/32
|
|
||||||
DNS = ${dns1}, ${dns2}
|
|
||||||
PrivateKey = ${priv.trim()}
|
|
||||||
Jc = ${Jc}
|
|
||||||
Jmin = ${Jmin}
|
|
||||||
Jmax = ${Jmax}
|
|
||||||
S1 = ${S1}
|
|
||||||
S2 = ${S2}
|
|
||||||
S3 = ${S3}
|
|
||||||
S4 = ${S4}
|
|
||||||
H1 = ${H1}
|
|
||||||
H2 = ${H2}
|
|
||||||
H3 = ${H3}
|
|
||||||
H4 = ${H4}
|
|
||||||
${iBlock}${mtuLine}[Peer]
|
|
||||||
PublicKey = ${String(serverPub).trim()}
|
|
||||||
PresharedKey = ${String(psk).trim()}
|
|
||||||
AllowedIPs = ${peerAllowed}
|
|
||||||
Endpoint = ${endpointHost}:${port}
|
|
||||||
PersistentKeepalive = ${keepAlive}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `[Interface]
|
return `[Interface]
|
||||||
Address = ${tunnelIp}/32
|
Address = ${tunnelIp}/32
|
||||||
DNS = ${dns1}, ${dns2}
|
DNS = ${dns1}, ${dns2}
|
||||||
PrivateKey = ${priv.trim()}
|
PrivateKey = ${priv.trim()}
|
||||||
${mtuLine}[Peer]
|
${awgObfuscationLines(lc, ifaceMap)}${mtuLine}[Peer]
|
||||||
PublicKey = ${String(serverPub).trim()}
|
PublicKey = ${String(serverPub).trim()}
|
||||||
PresharedKey = ${String(psk).trim()}
|
PresharedKey = ${String(psk).trim()}
|
||||||
AllowedIPs = ${peerAllowed}
|
AllowedIPs = ${peerAllowed}
|
||||||
@@ -1652,7 +1678,7 @@ async function awgGenKeypair(rt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function obfuscationFieldsFromServerHead(ifaceMap) {
|
function obfuscationFieldsFromServerHead(ifaceMap) {
|
||||||
const keys = ["Jc", "Jmin", "Jmax", "S1", "S2", "S3", "S4", "H1", "H2", "H3", "H4", "I1", "I2", "I3", "I4", "I5"];
|
const keys = AWG_OBF_KEYS;
|
||||||
const out = {};
|
const out = {};
|
||||||
for (const k of keys) {
|
for (const k of keys) {
|
||||||
const v = ifaceMap[k];
|
const v = ifaceMap[k];
|
||||||
@@ -2427,6 +2453,40 @@ app.post("/api/change-password", requireAuth, (req, res) => {
|
|||||||
res.json({ ok: true, message: "Пароль изменён. Войдите снова." });
|
res.json({ ok: true, message: "Пароль изменён. Войдите снова." });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/api/settings", requireAuth, (_req, res) => {
|
||||||
|
const st = loadPanelSettings();
|
||||||
|
res.json({
|
||||||
|
endpointHost: st.endpointHost || "",
|
||||||
|
envEndpoint: process.env.CLIENT_CONFIG_ENDPOINT?.trim() || "",
|
||||||
|
effectiveEndpoint: configuredEndpointHost(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/settings", requireAuth, requireProTier, async (req, res) => {
|
||||||
|
const raw = String(req.body?.endpointHost ?? "").trim();
|
||||||
|
let host = "";
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
host = assertCascadeEndpointHost(raw);
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(400).json({ error: String(e.message || e) });
|
||||||
|
}
|
||||||
|
if (!/^[A-Za-z0-9.-]+$/.test(host) && !/^[0-9a-fA-F:]+$/.test(host)) {
|
||||||
|
return res.status(400).json({ error: "Домен или IP: только латиница, цифры, точки и дефисы." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let resolved = null;
|
||||||
|
if (host && !/^[\d.]+$/.test(host) && host.includes(".")) {
|
||||||
|
try {
|
||||||
|
resolved = (await dns.promises.lookup(host, { all: true })).map((a) => a.address);
|
||||||
|
} catch {
|
||||||
|
return res.status(400).json({ error: `Домен ${host} не резолвится. Создайте A-запись на IP этого сервера и повторите.` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
savePanelSettings({ ...loadPanelSettings(), endpointHost: host });
|
||||||
|
res.json({ ok: true, endpointHost: host, resolved });
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/api/protocols", requireAuth, (req, res) => {
|
app.get("/api/protocols", requireAuth, (req, res) => {
|
||||||
const rt = runtimeForRequest(req);
|
const rt = runtimeForRequest(req);
|
||||||
const hintSingle =
|
const hintSingle =
|
||||||
@@ -2757,7 +2817,7 @@ app.post("/api/clients/create", requireAuth, requireProTier, async (req, res) =>
|
|||||||
}
|
}
|
||||||
const tunnelIp = normalizeCascadeTunnelIp(conf, ifaceMap, req.body?.tunnelIp);
|
const tunnelIp = normalizeCascadeTunnelIp(conf, ifaceMap, req.body?.tunnelIp);
|
||||||
const listenPort = ifaceMap.ListenPort ? Number(ifaceMap.ListenPort) : NaN;
|
const listenPort = ifaceMap.ListenPort ? Number(ifaceMap.ListenPort) : NaN;
|
||||||
const envHost = process.env.CLIENT_CONFIG_ENDPOINT?.trim();
|
const envHost = configuredEndpointHost();
|
||||||
const hdrHost =
|
const hdrHost =
|
||||||
typeof req.headers.host === "string" ? req.headers.host.split(":")[0].trim() : "";
|
typeof req.headers.host === "string" ? req.headers.host.split(":")[0].trim() : "";
|
||||||
const endpointHost = envHost || (hdrHost && hdrHost !== "localhost" ? hdrHost : "");
|
const endpointHost = envHost || (hdrHost && hdrHost !== "localhost" ? hdrHost : "");
|
||||||
@@ -2992,8 +3052,8 @@ app.post("/api/warp/start", requireAuth, requireProTier, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await rt.dockerExec(`wg-quick down '${rt.profile.warpConf}' 2>/dev/null || true`);
|
await rt.dockerExec(`${QUICK_SH} down '${rt.profile.warpConf}' 2>/dev/null || true`);
|
||||||
await rt.dockerExec(`wg-quick up '${rt.profile.warpConf}'`);
|
await rt.dockerExec(`${QUICK_SH} up '${rt.profile.warpConf}'`);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -3001,6 +3061,31 @@ app.post("/api/warp/start", requireAuth, requireProTier, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post("/api/cascade/upstream", requireAuth, requireProTier, async (req, res) => {
|
||||||
|
if (UI_HIDDEN.warp || UI_HIDDEN.cascade) {
|
||||||
|
return res.status(403).json({ error: MSG_UI_CASCADE_OFF });
|
||||||
|
}
|
||||||
|
const rt = runtimeForRequest(req);
|
||||||
|
try {
|
||||||
|
const conf = buildUpstreamConf(String(req.body?.configText || ""));
|
||||||
|
await rt.backupRemoteFiles();
|
||||||
|
await rt.dockerExec(`mkdir -p '${rt.profile.warpDir}'`);
|
||||||
|
await rt.dockerExec(`${QUICK_SH} down '${rt.profile.warpConf}' 2>/dev/null || true`);
|
||||||
|
await rt.dockerWriteFile(rt.profile.warpConf, conf);
|
||||||
|
await rt.dockerExec(`chmod 600 '${rt.profile.warpConf}'`);
|
||||||
|
const up = await rt.dockerExec(`${QUICK_SH} up '${rt.profile.warpConf}' 2>&1`);
|
||||||
|
const selected = await warpLoadSelectedIps(rt);
|
||||||
|
await warpApplyRouting(rt, selected);
|
||||||
|
await warpPatchStartSh(rt, selected);
|
||||||
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
const show = await rt.dockerExec("awg show warp 2>/dev/null || wg show warp 2>/dev/null || true");
|
||||||
|
res.json({ ok: true, handshake: /latest handshake/.test(show), show: show.slice(0, 2000), output: up.slice(-1500) });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
res.status(400).json({ error: String(e.message || e).slice(0, 1500) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post("/api/warp/stop", requireAuth, requireProTier, async (req, res) => {
|
app.post("/api/warp/stop", requireAuth, requireProTier, async (req, res) => {
|
||||||
if (UI_HIDDEN.warp) {
|
if (UI_HIDDEN.warp) {
|
||||||
return res.status(403).json({ error: MSG_UI_WARP_OFF });
|
return res.status(403).json({ error: MSG_UI_WARP_OFF });
|
||||||
@@ -3010,7 +3095,7 @@ app.post("/api/warp/stop", requireAuth, requireProTier, async (req, res) => {
|
|||||||
return res.status(400).json({ error: "WARP не установлен." });
|
return res.status(400).json({ error: "WARP не установлен." });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await rt.dockerExec(`wg-quick down '${rt.profile.warpConf}' 2>/dev/null || true`);
|
await rt.dockerExec(`${QUICK_SH} down '${rt.profile.warpConf}' 2>/dev/null || true`);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -3261,6 +3346,8 @@ function runInstanceScript(args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectVariantFromHead(head, iface) {
|
function detectVariantFromHead(head, iface) {
|
||||||
|
if (/^RandomTrailers\s*=/m.test(head)) return "awg31";
|
||||||
|
if (/^HeaderProtectionKey\s*=/m.test(head)) return "awg3";
|
||||||
if (iface === "wg0" || !/^S1\s*=/m.test(head)) return "legacy";
|
if (iface === "wg0" || !/^S1\s*=/m.test(head)) return "legacy";
|
||||||
if (/^S3\s*=/m.test(head) || /^S4\s*=/m.test(head)) return "awg2";
|
if (/^S3\s*=/m.test(head) || /^S4\s*=/m.test(head)) return "awg2";
|
||||||
return "awg";
|
return "awg";
|
||||||
@@ -3269,6 +3356,8 @@ function detectVariantFromHead(head, iface) {
|
|||||||
function inferVariantFromProfile(prof) {
|
function inferVariantFromProfile(prof) {
|
||||||
const raw = `${prof.variant || ""} ${prof.id || ""} ${prof.container || ""} ${prof.label || ""}`.toLowerCase();
|
const raw = `${prof.variant || ""} ${prof.id || ""} ${prof.container || ""} ${prof.label || ""}`.toLowerCase();
|
||||||
if (raw.includes("legacy") || prof.iface === "wg0") return "legacy";
|
if (raw.includes("legacy") || prof.iface === "wg0") return "legacy";
|
||||||
|
if (raw.includes("awg31") || raw.includes("3.1")) return "awg31";
|
||||||
|
if (raw.includes("awg3") || raw.includes("3.0")) return "awg3";
|
||||||
if (raw.includes("awg2") || raw.includes("2.0")) return "awg2";
|
if (raw.includes("awg2") || raw.includes("2.0")) return "awg2";
|
||||||
return "awg";
|
return "awg";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user