From 38e76da84f9e4e7e1603ca730e1ff15e43e69400 Mon Sep 17 00:00:00 2001 From: Ivan Vasilev Date: Sat, 29 Aug 2026 02:01:02 +0300 Subject: [PATCH] Make AmneziaWG 3.1 the default generation install.sh now defaults to AWG_GEN=3.1: an incompatible module or kernel fails with diagnostics instead of silently falling back to 2.0 and handing out a server with weaker obfuscation. AWG_GEN=auto keeps the old fallback for old kernels, AWG_GEN=2.0 still forces 2.0. Servers already installed on 2.0 no longer need a reinstall: POST /awg/upgrade (button in the users tab) backs up awg1.conf, rewrites [Interface] with HeaderProtectionKey and the 3.1 block install.sh writes, raises any S1-S4 below 12 (header protection needs a 12-byte nonce and the module only reports "Invalid argument"), restarts the interface and reissues every vpn:// key. Bringing the interface up is the only real proof the kernel accepted the params, so a failure restores the previous conf and brings 2.0 back up. Reissue is not optional: 2.0-era keys stop working the moment the generation changes. restartAwg() now re-reads the obfuscation params, generation and keepalive from awg1.conf before syncing peers. The conf can change while the interface is down, and a stale CONFIG.gen meant new keys were built on the old params. README: install from git.ma7neko.ru (Gitea) instead of raw.githubusercontent, and document the 3.1 default with the AWG_GEN=auto escape hatch. --- README.md | 14 ++- awg-ctrl/index.ts | 165 +++++++++++++++++++++++++++++++- awg-ui/src/tabs/users/index.tsx | 28 ++++++ install.sh | 39 ++++---- 4 files changed, 227 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 1898dd2..c313896 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,20 @@ На сервере, от root: ```bash -bash <(curl -Ls https://raw.githubusercontent.com/maeneko/forgetting/main/install.sh) +bash <(curl -Ls https://git.ma7neko.ru/maeneko/forgetting/raw/branch/main/install.sh) ``` + +Установщик по умолчанию ставит сервер на AmneziaWG 3.1. Если модуль в ядре +старше 3.x или ядро старше 5.5, установка остановится с диагностикой — поставить +на 2.0 можно так: + +```bash +AWG_GEN=auto bash <(curl -Ls https://git.ma7neko.ru/maeneko/forgetting/raw/branch/main/install.sh) +``` + +Перевести такой сервер на 3.1 позже можно кнопкой «Перейти на AWG 3.1» в панели — +переустановка не нужна, пользователи сохраняются, ключи перевыпускаются +автоматически. ## Управление ```bash diff --git a/awg-ctrl/index.ts b/awg-ctrl/index.ts index 7688570..45599d3 100644 --- a/awg-ctrl/index.ts +++ b/awg-ctrl/index.ts @@ -3,6 +3,7 @@ // LICENSE file in the root directory of this source tree. import fs, { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; import path from "path"; +import os from "os"; import crypto from "crypto"; import { execSync, spawnSync } from "child_process"; import * as zlib from "zlib"; @@ -222,6 +223,26 @@ function readAwgVersions(): { module: string; tools: string } { // в 2.0 это одно число. Уходит и в серверные [Peer], и в клиентский конфиг. const KEEPALIVE_BY_GEN: Record<"2" | "3.1", string> = { "2": "25", "3.1": "25-35" }; +// Значения 3.1-параметров для перевода уже работающего 2.0-сервера (POST +// /awg/upgrade) — те же, что пишет install.sh (дефолты клиента AmneziaVPN, +// protocolConstants.h), в порядке AWG3_KEYS. RandomTrailers/DisableCookies не +// включаем — как и инсталлятор. HeaderProtectionKey сюда не входит: он +// генерируется на месте (awg genpsk) и фиксируется на весь срок жизни сервера. +const AWG31_DEFAULTS: Record = { + ContentPaddingAddition: "10-100", + RekeyAfterTime: "100-120", + RekeyTimeout: "3-7", + RejectAfterTime: "150-180", + KeepaliveTimeout: "5-15", + MaxHandshakeAttempts: "15-20", +}; + +// При заданном HeaderProtectionKey ни один из S1–S4 не может быть меньше 12 +// (HEADER_PROTECTION_NONCE_SIZE). Модуль на нарушение отвечает только +// «Invalid argument»; причина видна лишь при +// `echo "module amneziawg +p" > /sys/kernel/debug/dynamic_debug/control`. +const MIN_S_FOR_HEADER_PROTECTION = 12; + const runtimeConfig = initConfig(); const AWG_PARAMS = readAwgParams(); const AWG_GEN = genOf(AWG_PARAMS); @@ -601,6 +622,17 @@ function ensureInterfaceUp() { if (!up) throw new Error(`Interface ${CONFIG.interface} is not up. Run: awg-quick up ${CONFIG.interface}`); } +// Параметры обфускации и поколение живут в awg1.conf, а не в процессе: +// перечитываем их после любой правки конфига. Иначе awg-ctrl продолжит +// собирать vpn:// ключи на старых параметрах — например, на 2.0 уже после +// перехода интерфейса на 3.1. +function reloadAwgParams(): "2" | "3.1" { + CONFIG.awgParams = readAwgParams(); + CONFIG.gen = genOf(CONFIG.awgParams); + CONFIG.keepalive = KEEPALIVE_BY_GEN[CONFIG.gen]; + return CONFIG.gen; +} + function restartAwg() { logger.info("AWG restart: down"); const down = spawnSync("awg-quick", ["down", CONFIG.interface]); @@ -608,11 +640,126 @@ function restartAwg() { logger.info("AWG restart: up"); const up = spawnSync("awg-quick", ["up", CONFIG.interface]); if (up.status !== 0) throw new Error(`awg-quick up failed: ${up.stderr?.toString()}`); + // Конфиг мог измениться, пока интерфейс лежал (переход на 3.1, ручная + // правка awg1.conf) — перечитываем ДО syncPeers: keepalive из CONFIG уходит + // в [Peer]-блоки, которые rebuildConf() пересобирает прямо сейчас. + const gen = reloadAwgParams(); syncPeers(); const versions = readAwgVersions(); CONFIG.awgModule = versions.module; CONFIG.awgTools = versions.tools; - logger.info("AWG restart: done", versions); + logger.info("AWG restart: done", { ...versions, gen }); +} + +// ─────────────────────────── переход 2.0 → 3.1 ─────────────────────────── +// 3.1 — основное поколение (install.sh ставит его по умолчанию). Сервера, +// установленные раньше, переводятся отсюда, без переустановки: панель дёргает +// POST /awg/upgrade. + +// Header protection использует библиотечный chacha-API (chacha_init/ +// chacha20_crypt), которого нет до ядра 5.5 — там модуль 3.x просто не +// соберётся (upstream issue #210). +function kernelSupportsHeaderProtection(): boolean { + const [maj, min] = os.release().split(".").map(n => Number.parseInt(n, 10)); + if (!Number.isFinite(maj) || !Number.isFinite(min)) return false; + return maj > 5 || (maj === 5 && min >= 5); +} + +// Пред-проверка перед тем, как трогать конфиг: решает ЗАГРУЖЕННЫЙ модуль +// (readAwgVersions читает /sys/module/amneziawg/version), а не тот, что лежит +// на диске после apt — обслуживает интерфейс именно загруженный. +function checkGen31Support(): string | null { + const { module } = readAwgVersions(); + if (!module.startsWith("3.")) + return `В ядре загружен модуль amneziawg ${module || "неизвестной версии"} — для 3.1 нужна 3.x. ` + + "Обнови пакет (apt) и перезагрузи сервер, затем повтори переход."; + if (!kernelSupportsHeaderProtection()) + return `Ядро ${os.release()} старше 5.5 — header protection в нём не работает (upstream issue #210).`; + return null; +} + +const AWG3_KEY_LINE = new RegExp(`^\\s*(${AWG3_KEYS.join("|")})\\s*=`); + +// Готовит текст awg1.conf для 3.1: выкидывает старые 3.x-строки (конфиг могли +// править руками), поднимает S1–S4 до допустимых при header protection и +// дописывает 3.x-блок в конец [Interface]. [Peer]-блоки не трогаются — +// личности пиров переход не меняет. +function confWithGen31(conf: string, headerKey: string): { conf: string; bumpedS: string[] } { + const peerAt = conf.search(/^\[Peer\]/m); + let rest = peerAt >= 0 ? conf.slice(peerAt) : ""; + const head = (peerAt >= 0 ? conf.slice(0, peerAt) : conf) + .split("\n") + .filter(l => !AWG3_KEY_LINE.test(l)); + + // Пустые строки и «# имя» в хвосте [Interface] относятся уже к первому + // [Peer] (их пишет rebuildConf) — отделяем, чтобы 3.x-строки встали внутрь + // [Interface], а не между комментарием и его пиром. + const tail: string[] = []; + while (head.length && (head[head.length - 1].trim() === "" || head[head.length - 1].trim().startsWith("#"))) + tail.unshift(head.pop()!); + rest = tail.join("\n").trim() ? `${tail.join("\n").trim()}\n${rest}` : rest; + + let iface = head.join("\n"); + + // S ниже 12 (или вовсе отсутствующий) ядро с header protection не примет. + // Правка безопасна ровно потому, что следом идёт перевыпуск ключей: обе + // стороны получают новые значения одновременно. + const bumpedS: string[] = []; + for (const k of ["S1", "S2", "S3", "S4"] as const) { + const re = new RegExp(`^\\s*${k}\\s*=\\s*(\\d+)\\s*$`, "m"); + const cur = Number(iface.match(re)?.[1]); + if (Number.isFinite(cur) && cur >= MIN_S_FOR_HEADER_PROTECTION) continue; + const val = Math.max(DEFAULT_AWG_PARAMS[k], MIN_S_FOR_HEADER_PROTECTION); + iface = re.test(iface) + ? iface.replace(re, `${k} = ${val}`) + : `${iface.trimEnd()}\n${k} = ${val}`; + bumpedS.push(`${k}: ${Number.isFinite(cur) ? cur : "нет"} → ${val}`); + } + + const awg3 = [ + `HeaderProtectionKey = ${headerKey}`, + ...Object.entries(AWG31_DEFAULTS).map(([k, v]) => `${k} = ${v}`), + ].join("\n"); + + return { conf: `${iface.trimEnd()}\n${awg3}\n${rest ? `\n${rest}` : ""}`, bumpedS }; +} + +interface UpgradeResult { gen: "2" | "3.1"; backup: string; bumpedS: string[]; reissue: ReissueResult } + +// Перевод интерфейса на 3.1. Единственная настоящая проверка, что ядро приняло +// параметры, — подъём интерфейса, поэтому шаги такие: бэкап конфига → запись +// 3.1 → restartAwg() (он же перечитает параметры) → перевыпуск ключей. Если +// ядро отказало, возвращаем прежний конфиг и поднимаем интерфейс обратно: +// операция либо применяется целиком, либо не оставляет следов. +function upgradeToGen31(): UpgradeResult { + const confFile = path.join(CONFIG.confDir, `${CONFIG.interface}.conf`); + const before = readFileSync(confFile, "utf8"); + const backup = `${confFile}.bak-${new Date().toISOString().replace(/[:.]/g, "-")}`; + writeFileSync(backup, before, { mode: 0o600 }); + + const { conf, bumpedS } = confWithGen31(before, run("awg genpsk")); + writeFileSync(confFile, conf, { mode: 0o600 }); + logger.info("upgrade: conf rewritten for 3.1", { backup, bumpedS }); + + try { + restartAwg(); + if (CONFIG.gen !== "3.1") throw new Error("интерфейс поднялся, но конфиг всё ещё 2.0"); + } catch (e) { + logger.error("upgrade: kernel rejected 3.1, rolling back", { error: e }); + writeFileSync(confFile, before, { mode: 0o600 }); + try { restartAwg(); } catch (e2) { logger.error("upgrade: rollback restart failed", { error: e2 }); } + throw new Error( + `Ядро не приняло конфиг 3.1 — вернули прежний (копия: ${backup}). ` + + 'Причина видна в dmesg после `echo "module amneziawg +p" > /sys/kernel/debug/dynamic_debug/control`.', + ); + } + + // Ключи, выданные на 2.0-параметрах, после смены поколения не подключатся, + // поэтому перевыпускаем сразу — сервер не должен оставаться в состоянии + // «3.1 поднят, у всех нерабочие ключи». Личности пиров сохраняются. + const reissue = reissueAll(); + logger.info("upgrade: done", { gen: CONFIG.gen, reissued: reissue.reissued, bumpedS }); + return { gen: CONFIG.gen, backup, bumpedS, reissue }; } function startInterface() { @@ -789,6 +936,22 @@ app.post("/awg/restart", auth, handler((_req, res) => { res.json({ success: true }); })); +// Перевод сервера с 2.0 на 3.1 — включая перевыпуск всех vpn:// ключей. +// Ошибку отдаём текстом: панель показывает её оператору, а причины отказа +// (старый модуль, старое ядро, отказ ядра на подъёме) требуют разных действий. +app.post("/awg/upgrade", auth, handler((_req, res) => { + if (CONFIG.gen === "3.1") { + res.status(409).json({ error: "Сервер уже работает на AmneziaWG 3.1" }); return; + } + const blocker = checkGen31Support(); + if (blocker) { res.status(409).json({ error: blocker }); return; } + try { + res.json(upgradeToGen31()); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : "Не удалось перейти на 3.1" }); + } +})); + app.get("/awg/status", auth, handler((_req, res) => { res.json(getInterfaceStatus()); })); diff --git a/awg-ui/src/tabs/users/index.tsx b/awg-ui/src/tabs/users/index.tsx index 5429cf9..45efcb8 100644 --- a/awg-ui/src/tabs/users/index.tsx +++ b/awg-ui/src/tabs/users/index.tsx @@ -19,6 +19,7 @@ export default function UsersPage({ token, showMsg }: PageProps) { // Поколение, на котором сейчас работает интерфейс. Ключи, выданные на другом // поколении, уже не подключатся — их надо перевыпустить. const [serverGen, setServerGen] = useState('2'); + const [upgrading, setUpgrading] = useState(false); const statsRef = useRef | null>(null); const stale = users.filter(u => u.key_gen !== serverGen); @@ -62,6 +63,28 @@ export default function UsersPage({ token, showMsg }: PageProps) { } }, [stale.length, serverGen, token, loadUsers, showMsg]); + // Перевод сервера с 2.0 на 3.1 без переустановки: awg-ctrl дописывает + // 3.x-параметры в awg1.conf, перезапускает интерфейс (с откатом, если ядро + // их не примет) и сразу перевыпускает ключи — старые собраны на + // 2.0-параметрах и работать не будут. + const upgradeGen = useCallback(async () => { + if (!confirm( + 'Перевести сервер на AmneziaWG 3.1?\n\n' + + 'Интерфейс будет перезапущен, а все vpn:// ключи перевыпущены — ' + + 'клиентам придётся импортировать ключ заново.', + )) return; + setUpgrading(true); + try { + const r = await apiFetch('POST', '/awg/upgrade', token); + showMsg(`Сервер переведён на ${genLabel(r.gen)}, перевыпущено ключей: ${r.reissue?.reissued ?? 0}`); + await loadUsers(token); + } catch (e: any) { + showMsg(e?.response?.data?.error ?? 'Не удалось перейти на 3.1'); + } finally { + setUpgrading(false); + } + }, [token, loadUsers, showMsg]); + const createUser = useCallback(async () => { if (!newName) return; const u = await apiFetch('POST', '/api/users', token, { name: newName }); @@ -120,6 +143,11 @@ export default function UsersPage({ token, showMsg }: PageProps) { + {serverGen !== '3.1' && ( + + )} {stale.length > 0 && (