On a server with an existing installation the menu gets a third choice, «Удалить»; UNINSTALL=y runs it without the menu. After typing «удалить» it offers a backup of /etc/amnezia and cli.env to /root, then removes the services and the systemd unit, brings awg1 down, drops the shaping and torrent nft tables, closes the ports it had opened in UFW, firewalld or iptables, removes the Prometheus that `awg-ctrl monitor install` set up (only ours), and deletes /opt/awg-control, /etc/amnezia, the awg-ctrl command, the sysctl file and the install logs, which hold the panel password. AmneziaWG packages, module and PPA go only on a separate yes. kill_related moved above the menu so the uninstall can use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
Forgetting
Панель управления и API для VPN на базе AmneziaWG
Warning
Данный проект предназначен только для использования в законных целях.
Архитектура
Один статический бинарник awg-control (Go) в /opt/awg-control/ — в нём все роли;
команда awg-ctrl — ссылка на него:
| Компонент | Роль |
|---|---|
awg-ctrl (awg-control ctrl) |
Привилегированный REST-бэкенд. Управляет пирами через awg/awg-quick, ведёт SQLite-базу, генерирует vpn:// ключи. Слушает только loopback. |
awg-ui (awg-control ui) |
Веб-панель (фронтенд на React, awg-ui/dist), аутентификация (JWT), прокси к awg-ctrl, публичный API /api/v1 и своя база его ключей. Единственный сервис, доступный по сети. |
awg-agent (awg-control agent) |
Только на ноде: исходящее WSS-соединение к панели, исполняет её запросы на локальном awg-ctrl. |
cli (awg-ctrl …) |
Менеджер процессов: 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-модуля).
- Архитектура: x86_64 или aarch64.
Совместимость установщик проверяет сам перед любыми изменениями.
Установка
На сервере, от root:
bash <(curl -Ls https://raw.githubusercontent.com/maeneko/forgetting/main/install.sh)
Не от root — через sudo bash -c: sudo bash <(curl …) падает с
/dev/fd/63: No such file or directory, потому что sudo закрывает дескриптор
с текстом скрипта.
sudo bash -c "$(curl -Ls https://raw.githubusercontent.com/maeneko/forgetting/main/install.sh)"
Установщик по умолчанию ставит сервер на AmneziaWG 3.1. Если модуль в ядре старше 3.x или ядро старше 5.5, установка остановится с диагностикой — поставить на 2.0 можно так:
AWG_GEN=auto bash <(curl -Ls https://raw.githubusercontent.com/maeneko/forgetting/main/install.sh)
Перевести такой сервер на 3.1 позже можно кнопкой «Перейти на AWG 3.1» в панели — переустановка не нужна, пользователи сохраняются, ключи перевыпускаются автоматически.
Источник архива и имя панели не зашиты в код — задаются переменными
REPO_BASE, REPO_FALLBACK, ARCHIVE_URL, BRAND перед запуском установщика
(подробности — в комментариях install.sh и в .env).
Удаление
Тот же установщик: на сервере с установкой — пункт 3 «Удалить», без меню —
UNINSTALL=y (подтверждение «удалить» тогда не спрашивается):
UNINSTALL=y bash <(curl -Ls https://raw.githubusercontent.com/maeneko/forgetting/main/install.sh)
Удаляются сервисы и юнит systemd, интерфейс awg1, nft-таблицы шейпинга и блокировки
торрентов, открытые установщиком порты (UFW, firewalld или iptables), Prometheus из
awg-ctrl monitor install, /opt/awg-control, /etc/amnezia (пользователи, ключи, базы),
команда awg-ctrl, sysctl-файл и логи установки (в них пароль панели). Перед этим установщик
предлагает сохранить копию данных в /root/forgetting-backup-<время>.tar.gz. AmneziaWG
(пакеты, модуль, PPA) — по отдельному вопросу; общие зависимости (dkms, build-essential,
iptables, заголовки ядра) остаются. Ноду после удаления уберите и из списка серверов панели.
Секретный путь панели
Установщик выдаёт панели случайный путь: она открывается по http://IP:PORT/<путь>/, а на
любой другой адрес отвечает 404 page not found, как пустой веб-сервер. Порт панели открыт
в интернет, и без пути сканер узнавал её по странице логина. Путь хранится в UI_PREFIX
в /opt/awg-control/cli/cli.env; полный адрес показывают awg-ctrl status и
awg-ctrl credentials. Внешнему API нужен тот же путь: http://HOST:PORT/<путь>/api/v1/…
(в примерах ниже HOST:PORT — вместе с путём).
- При переустановке с сохранением данных путь остаётся прежним. Свой путь —
UI_PREFIX=<8–64 символа A–Z a–z 0–9 _ ->, без пути —UI_PREFIX=offперед запуском установщика (или правкаcli.envиawg-ctrl restart). - С этой же машины (loopback) путь не нужен: CLI, локальный Prometheus и nginx ниже ходят как раньше.
- При переносе панели (вкладка «Перенос»,
awg-ctrl migrate) новая панель принимает и свой путь, и путь старой (UI_PREFIX_MOVED) — закладки и API через ретранслятор продолжают работать. Пока выдан код переноса,POST /migrate/v1/importпринимается и без пути.
Панель за nginx
Панель работает и в корне домена, и в любой подпапке — пути в ней относительные,
настраивать сам префикс не нужно. Пример для https://example.com/forgetting/
(39708 — PORT панели из /opt/awg-control/cli/cli.env):
location = /forgetting { return 301 /forgetting/; }
location ^~ /forgetting/ {
proxy_pass http://127.0.0.1:39708/; # слэш в конце срезает префикс
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Адрес клиента панель берёт из X-Forwarded-For / X-Real-IP только от доверенного
прокси: с этой же машины (loopback) — всегда, с других адресов — если они перечислены
в TRUST_PROXY в cli.env (IP или сети через запятую, например
TRUST_PROXY=10.0.0.5,192.168.1.0/24), после чего awg-ctrl restart. От остальных
заголовки игнорируются. Без этого все входы шли бы с адреса прокси и делили один
лимит неудачных попыток.
Сам порт панели после этого можно закрыть снаружи (ufw deny <PORT>/tcp) — порты
подписки (SUB_PORT) и хаба нод (NODE_PORT) должны оставаться открытыми: клиенты
и ноды ходят на них напрямую, не через nginx.
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: у пира нет изменяемых полей, «перевыпустить»
ключ = удалить и создать заново.
У ключа — набор серверов, которые он видит (панель → «API-ключи» → шестерёнка;
0 — сервер самой панели, остальные — подключённые ноды). Пользователи vpn://
заводятся на сервере ключа по умолчанию; другой сервер из набора выбирается
заголовком X-Server-Id: <id>, сервер вне набора → 404.
Мастер-ключи sen://
Мастер-ключ — ссылка sen://… для приложения SenAWG с лимитом устройств: каждое
устройство само регистрируется по ссылке и получает пира на всех серверах ключа,
его приватный ключ сервер не видит. API-ключ видит и меняет мастер-ключ, только
если все его серверы входят в набор API-ключа; остальные для него не
существуют (404).
curl -s -X POST http://HOST:PORT/api/v1/masterkeys \
-H "X-Api-Key: awgk_xxx" -H "Content-Type: application/json" \
-d '{"label":"Семья","device_limit":3}' | jq -r .link
| Метод | Что делает |
|---|---|
POST /api/v1/masterkeys |
Создаёт ключ. Тело { label, device_limit?, servers? } (лимит 1–100, по умолчанию 3; без servers — весь набор API-ключа). 201 с { id, uuid, label, device_limit, devices, servers, created_at, link, tls }; 403 — сервер вне набора, 503 — подписка не настроена (ключ не создаётся) |
GET /api/v1/masterkeys |
Список: { keys: [{ id, uuid, label, device_limit, devices, servers, created_at, deleting? }] }. uuid — постоянный идентификатор ключа: не меняется ни при перевыпуске ссылки, ни при правке метки. Везде, где в пути :id, можно передать и uuid |
GET /api/v1/masterkeys/:id |
Ключ, ссылка и устройства одним запросом: те же поля + link, tls, device_list (devices — счётчик) |
PATCH /api/v1/masterkeys/:id |
{ label?, device_limit?, servers? } — смена серверов сразу добавляет/снимает пиров устройств |
POST /api/v1/masterkeys/:id/rotate |
Новая ссылка { id, link, tls }; старая не принимает новые устройства, подключённые работают дальше |
POST /api/v1/masterkeys/:id/rekey |
Все устройства сменят ключи WireGuard при следующем опросе (IP и PSK сохраняются) |
DELETE /api/v1/masterkeys/:id |
{ success, id, pending } — ключ и устройства отключаются сразу; pending — серверы (ноды не в сети), которые ещё не сняли пиров, до тех пор ключ в списке с deleting |
GET /api/v1/masterkeys/:id/devices |
{ devices: [{ id, device_name, platform, version, online, lastHandshake, rx, tx, last_seen, servers_ok, servers_total, … }] } — статистика по всем серверам ключа |
DELETE /api/v1/masterkeys/:id/devices/:device |
Отвязывает устройство, место освобождается |
POST /api/v1/masterkeys/:id/devices/:device/rekey |
Устройство сменит ключ при следующем опросе |
POST /api/v1/masterkeys/:id/devices/:device/psk |
Новый PSK: { id, pending } — pending серверов ждут ноду |
| Код | Когда |
|---|---|
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 (с полем
servers), PATCH /ui/apikeys/:id { "servers": [0, 2] } — сменить набор, не
перевыпуская ключ, и DELETE /ui/apikeys/:id. Подробно — awg-ui/API.md.
Авторизация панели
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.4.7" } — требует JWT панели (без авторизации выдавал бы панель сканерам). Вордмарк в шапке после входа |
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.
Мастер-ключи
Те же операции, что в /api/v1/masterkeys, но без ограничения набором серверов.
| Метод | Что делает |
|---|---|
GET /ui/masterkeys |
{ enabled, tls, keys: [{ id, uuid, label, device_limit, devices, servers, created_at, deleting? }], defaults }; :id в путях — id или uuid; defaults — серверы новых ключей или null |
POST /ui/masterkeys |
{ label, device_limit?, servers? } → 201; без servers — defaults, а нет их — сервер по умолчанию |
POST /ui/masterkeys/servers |
«Серверы для всех»: { add?, remove?, defaults?, dry_run? } — add появляются у всех живых ключей, remove снимаются у всех, прочие серверы ключей не трогаются; defaults (null — сбросить) — набор новых ключей. → { changed, total, hidden: [{ id, label, keys }] }, hidden — ключи API, которые перестанут видеть столько мастер-ключей (сервер вне их области). 409, если у ключа не останется серверов |
PATCH /ui/masterkeys/:id |
{ label?, device_limit?, servers? } |
GET /ui/masterkeys/:id/link |
{ link, tls } — ссылка sen://… |
GET /ui/masterkeys/:id/devices |
Устройства со статусом и трафиком |
POST /ui/masterkeys/:id/rotate, /rekey |
Новая ссылка / смена ключей у всех устройств |
DELETE /ui/masterkeys/:id |
{ success, id, pending } |
DELETE /ui/devices/:id |
Отвязать устройство |
POST /ui/devices/:id/rekey, /psk |
Смена ключа / PSK у устройства |
Управление
awg-ctrl # меню
awg-ctrl start|stop|restart|status
awg-ctrl credentials [user|pass]
awg-ctrl join '<awgjoin://…>' [--no-panel] / unjoin
awg-ctrl rehome <хост[:порт]> # нода: панель переехала на новый адрес
awg-ctrl migrate export [файл] # перенос панели на отдельный сервер без VPN
awg-ctrl migrate import <архив> # (порядок — docs/node-protocol.md, «Перенос панели»)
awg-ctrl migrate cancel
Через systemd (автозапуск на загрузке):
systemctl start|stop|restart awg-control
Лицензия
MIT © 2026 Ivan Vasilev