A deleted master key could keep letting its devices in: ui.db learns what is on a server only from awg-ctrl's reply, and a node that executed a request whose 504/502 reply got lost, or a device deleted while a reconcile pass was in flight, left a peer nothing remembered to remove. - awg-ctrl: PUT /api/peers/sync takes a panel's full list of wanted keys and removes every other peer of that panel's area. Removal is driven only by such an explicit list, never by time or inactivity: a panel that is offline sends nothing, so nothing is deleted. Registered before PUT /api/peers/:pub, with its own 512 kB body limit. - Every panel has a stable id (ui.db, mirrored to awg-ui/panel.id) and adds peers as "<panel>/m<key>/d<device>", so two panels on one awg-ctrl never touch each other's peers. Legacy "m1/d2" peers are claimed only when their key is in the panel's list. - Each reconcile pass ends with the sync, built from ui.db at that moment. Its reply also self-heals: a device whose key is missing on a server (a rekey rolled back after a lost reply) is added again. - Deleting a master key stops it at once but keeps it listed as "Удаляется — ждёт: …" until every server confirms, instead of saying "deleted" while an offline node still lets the devices in. - Deleting a node reconciles it first; if it is offline or has not confirmed a deleted key, it takes ?force=1 and a warning. Tombstones are no longer dropped silently. - /sub/v1: an unknown device gets a signed 410 revoked that does not count towards the failure limit (dead clients behind one node NAT used to lock everyone out), every response echoes the request's ts and device, and 401 reasons and the first 429 per window are logged. config and rev are unchanged, byte-identical on one server. - CLI: `awg-ctrl peers [drop <area|legacy>]` lists and removes subscription peers by area; `join --no-panel` asks whether to keep or drop the panel's own peers (NODE_PEERS=keep|drop).
Forgetting
Панель управления и API для VPN на базе AmneziaWG
Warning
Данный проект предназначен только для использования в законных целях.
Архитектура
Три компонента, устанавливаются в /opt/awg-control/:
| Компонент | Роль |
|---|---|
| awg-ctrl | Привилегированный REST-бэкенд. Управляет пирами через awg/awg-quick, ведёт SQLite-базу, генерирует vpn:// ключи. Слушает только loopback. |
| awg-ui | Express + React. Веб-панель, аутентификация (JWT), прокси к awg-ctrl, публичный API /api/v1 и своя база его ключей. Единственный сервис, доступный по сети. |
| cli | Менеджер процессов: start/stop/restart/status, смена логина/пароля панели, интерактивное TUI-меню. |
Внутренняя связь awg-ui → awg-ctrl подписывается асимметрично (Ed25519): приватный ключ только у awg-ui, awg-ctrl проверяет подпись публичным.
Требования
- ОС: Ubuntu 24.04+ (Тесты проходили на Ubuntu 24.04.4 LTS) или Debian 11–13 (поддержка экспериментальная: PPA amnezia подключается вручную, серия подбирается по релизу Debian и переопределяется через
AMNEZIA_PPA_SUITE) - Права: root.
- Ядро: kvm (не OpenVZ/LXC/Docker/WSL — нужен загружаемый kernel-модуль AmneziaWG).
- Заголовки ядра под текущее ядро (для сборки DKMS-модуля).
- Node.js 20.x (ставится установщиком автоматически).
Совместимость установщик проверяет сам перед любыми изменениями.
Установка
На сервере, от root:
bash <(curl -Ls https://git.ma7neko.ru/maeneko/forgetting/raw/branch/main/install.sh)
Установщик по умолчанию ставит сервер на AmneziaWG 3.1. Если модуль в ядре старше 3.x или ядро старше 5.5, установка остановится с диагностикой — поставить на 2.0 можно так:
AWG_GEN=auto bash <(curl -Ls https://git.ma7neko.ru/maeneko/forgetting/raw/branch/main/install.sh)
Перевести такой сервер на 3.1 позже можно кнопкой «Перейти на AWG 3.1» в панели — переустановка не нужна, пользователи сохраняются, ключи перевыпускаются автоматически.
Источник архива и имя панели не зашиты в код — задаются переменными
REPO_BASE, REPO_FALLBACK, ARCHIVE_URL, BRAND перед запуском установщика
(подробности — в комментариях install.sh и в .env).
API
Наружу смотрят две независимые поверхности. Публичный API /api/v1 —
стабильный контракт для сторонних программ, авторизация по API-ключу.
Роуты панели — всё, что умеет веб-интерфейс, авторизация по JWT. Базовый
URL в обоих случаях — адрес сервера и порт, выбранный при установке.
Warning
Роуты панели отдают приватные поля пиров (
vpn_key,psk_key) — это не публичный контракт, в отличие от/api/v1. Не выставляй её порт в интернет без TLS и ограничения по адресам.
Публичный API (/api/v1)
Ключ создаётся во вкладке «API-ключи» и уходит в заголовке X-Api-Key.
Открытое значение показывается один раз при создании — на сервере лежит только
SHA-256 и префикс для показа в списке. Наружу отдаются только публичные поля:
psk_key и pub_key не покидают сервер.
curl -s -X POST http://HOST:PORT/api/v1/users \
-H "X-Api-Key: awgk_xxx" -H "Content-Type: application/json" \
-d '{"name":"alice"}' | jq -r .vpn_key
| Метод | Что делает |
|---|---|
POST /api/v1/users |
Создаёт пира. Тело { "name": "alice" }. 201 с { name, ip, gen, vpn_key }; 400 — имя не прошло валидацию, 409 — уже есть |
GET /api/v1/users |
Список со счётчиками: { users: [{ name, ip, key_gen, online, lastHandshake, rx, tx }] } |
GET /api/v1/users/:name |
{ name, ip, gen, vpn_key }; 404 — не найден |
DELETE /api/v1/users/:name |
{ "success": true, "name": "alice" } |
gen — поколение AmneziaWG, на параметрах которого собран ключ ("2" или
"3.1"). Операции только CRD: у пира нет изменяемых полей, «перевыпустить»
ключ = удалить и создать заново.
| Код | Когда |
|---|---|
401 {"error":"API key required"} |
заголовка нет или он не начинается с awgk_ |
401 {"error":"Invalid API key"} |
ключ не найден — удалён или неверен |
502 {"error":"awg-ctrl недоступен"} |
awg-ui посредничает, а awg-ctrl не отвечает |
Ключи лежат в собственной БД панели (/etc/amnezia/amneziawg/ui.db) — вне
каталога установки, поэтому переустановка их не трогает. Сама awg-ctrl про
внешние ключи ничего не знает: awg-ui ходит к ней своей внутренней авторизацией.
Управлять ключами можно и без панели, по JWT: GET/POST /ui/apikeys и
DELETE /ui/apikeys/:id.
Авторизация панели
TOKEN=$(curl -s -X POST http://HOST:PORT/login \
-H "Content-Type: application/json" \
-d '{"user":"admin","pass":"***"}' | jq -r .token)
| Метод | Ответ и ошибки |
|---|---|
POST /login |
{ "token": "<JWT>" }, срок 24 часа. 401 — неверная пара; 429 — больше 5 неудачных попыток с одного IP за 15 минут |
POST /logout |
{ "ok": true }, токен отзывается немедленно |
Дальше в каждый запрос: Authorization: Bearer $TOKEN. Ниже — роуты, которые
дёргает сама панель; в отличие от /api/v1 их форма может меняться.
Состояние
| Метод | Ответ |
|---|---|
GET /ui/brand |
{ "brand": "Forgetting", "channel": "Beta", "version": "0.3.2" } — без авторизации, панель берёт отсюда вордмарк на экране логина |
GET /health |
{ "status", "server", "ip", "gen", "awg": { "status", "peers", "module", "tools" } }. 200, если интерфейс поднят, иначе 503 и status: "degraded" |
GET /awg/status |
{ "up": true, "peers": 3, "publicKey": "…" } |
gen — поколение AmneziaWG, на параметрах которого сейчас работает интерфейс:
"2" или "3.1". Оно выводится из awg1.conf, а не хранится отдельно.
Пользователи
| Метод | Что делает |
|---|---|
POST /api/users |
Создаёт пира. Тело { "name": "alice" }, имя — a–z A–Z 0–9 _ -, до 32 символов. 201 с { name, ip, pub_key, psk_key, vpn_key, key_gen, vpn_key_prev }; 400 — имя не прошло валидацию, 409 — уже есть |
GET /api/users |
Список: { users: [{ name, ip, pub_key, vpn_key, key_gen, online, lastHandshake }] } |
GET /api/users/stats |
То же без ключей, но со счётчиками: { users: [{ name, ip, key_gen, online, lastHandshake, rx, tx }] } |
POST /api/users/:name |
Возвращает пользователя целиком, включая vpn_key. Метод именно POST, чтобы ключ не оседал в логах и истории как параметр GET. 404 — не найден |
DELETE /api/users/:name |
{ "success": true, "name": "alice" } |
POST /api/users/reissue |
Пересобирает vpn:// всем на текущих параметрах: { total, reissued, regenerated: [], backup }. IP и ключевая пара сохраняются, клиентам нужно заново импортировать ключ; backup — путь к снимку базы, снятому перед проходом |
curl -s -X POST http://HOST:PORT/api/users \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"alice"}' | jq -r .vpn_key
Имея vpn_key, конфиг .conf можно получить локально: убрать префикс vpn://,
base64url-декодировать, отбросить первые 4 байта (длина), распаковать
zlib inflate → JSON; текст конфига лежит в last_config.config.
Интерфейс AWG
| Метод | Что делает |
|---|---|
POST /awg/start |
Поднимает интерфейс, если он лежит; возвращает его статус |
POST /awg/restart |
awg-quick down/up + ресинк пиров, { "success": true }. Соединения клиентов кратковременно рвутся |
POST /awg/upgrade |
Переводит сервер с 2.0 на 3.1: правит awg1.conf, перезапускает интерфейс и перевыпускает все ключи. { gen, backup, bumpedS: [], reissue: { total, reissued, … } } |
POST /awg/upgrade отвечает 409, если сервер уже на 3.1, модуль в ядре не
3.x или ядро старше 5.5, и 500 с текстом причины, если ядро не приняло
3.1-конфиг — в этом случае прежний конфиг возвращается из копии, а интерфейс
поднимается обратно на 2.0.
Управление
awg-ctrl
Через systemd (автозапуск на загрузке):
systemctl start|stop|restart awg-control
Лицензия
MIT © 2026 Ivan Vasilev