Rewrite the README

Sections were added one feature at a time and ended up in arrival order,
with the stealth notes inside "Installation". The README now has a feature
summary and contents, an architecture section with the ports each server
opens, installation with roles and a table of installer variables, update /
reinstall / uninstall, a section on several servers and moving the panel
(it pointed to docs/node-protocol.md, which is not in the repo), one
"protection against detection" section covering randomized parameters, the
quiet ports and what they do not cover, the connection log, monitoring,
nginx, the API with the secret path in the base URL and the new connlog and
metrics routes, CLI commands and the main cli.env settings.

Also fixed on the way: the subscription is HTTPS by default, and a plain
update does not add a secret path to an older install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
This commit is contained in:
Claude
2026-10-28 12:13:40 +00:00
parent 1a54d05cca
commit ff2d71600c
+280 -147
View File
@@ -1,7 +1,30 @@
# Forgetting
Панель управления и API для VPN на базе [AmneziaWG](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module)
Панель управления и API для VPN на базе [AmneziaWG](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module)
> [!WARNING]
> Данный проект предназначен только для использования в законных целях.
- **VPN-пользователи** с ключами `vpn://` для AmneziaVPN и **мастер-ключи** `sen://` для SenAWG:
одна ссылка на несколько устройств, каждое регистрируется само.
- **Несколько серверов** под одной панелью: ноды подключаются к ней сами, панель можно
вынести на отдельный сервер без VPN.
- **Журнал подключений**: кто, когда и сколько был в VPN, кто давно не подключался.
- **Мониторинг**: встроенный Prometheus и графики в панели, метрики для своего Prometheus.
- **Защита от обнаружения**: параметры обфускации, порты и пути у каждого сервера свои, а
открытые порты отвечают сканеру как пустой веб-сервер.
## Содержание
- [Архитектура](#архитектура)
- [Требования](#требования)
- [Установка](#установка) · [Параметры установщика](#параметры-установщика) · [Обновление и удаление](#обновление-и-удаление)
- [Несколько серверов](#несколько-серверов)
- [Защита от обнаружения](#защита-от-обнаружения)
- [Журнал подключений](#журнал-подключений)
- [Мониторинг](#мониторинг)
- [Панель за nginx](#панель-за-nginx)
- [API](#api)
- [Управление](#управление) · [Настройки `cli.env`](#настройки-clienv)
## Архитектура
Один статический бинарник `awg-control` (Go) в `/opt/awg-control/` — в нём все роли;
@@ -9,17 +32,34 @@
| Компонент | Роль |
|---|---|
| **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-ctrl** (`awg-control ctrl`) | Привилегированный REST-бэкенд. Управляет пирами через `awg`/`awg-quick`, ведёт SQLite-базу (`users.db`) и журнал подключений, генерирует `vpn://` ключи. Слушает только loopback. |
| **awg-ui** (`awg-control ui`) | Веб-панель (React, `awg-ui/dist`), вход по JWT, прокси к awg-ctrl, публичный API `/api/v1`, подписка `sen://`, хаб нод и `/metrics`. База — `ui.db`. |
| **awg-agent** (`awg-control agent`) | Только на ноде: исходящее WSS-соединение к панели, исполняет её запросы на локальном awg-ctrl. |
| **cli** (`awg-ctrl …`) | Менеджер процессов: `start`/`stop`/`restart`/`status`, смена логина/пароля панели, интерактивное TUI-меню. |
| **cli** (`awg-ctrl …`) | Менеджер процессов, логин и пароль панели, привязка нод, перенос панели, мониторинг, TUI-меню. |
Внутренняя связь awg-ui → awg-ctrl подписывается асимметрично (Ed25519): приватный ключ только у awg-ui, awg-ctrl проверяет подпись публичным.
Внутренняя связь awg-ui → awg-ctrl подписывается асимметрично (Ed25519): приватный ключ только
у awg-ui, awg-ctrl проверяет подпись публичным. Данные — в `/etc/amnezia/amneziawg/` (вне
каталога установки, переустановку переживают), настройки сервисов — в
`/opt/awg-control/cli/cli.env`.
Порты наружу (номера у каждого сервера случайные):
| Порт | Что | Кто открывает |
|---|---|---|
| `SERVER_PORT`/udp | VPN-клиенты | везде, где есть VPN |
| `UI_PORT`/tcp | панель и API, под секретным путём | панель |
| `SUB_PORT`/tcp | подписка `sen://` (по умолчанию HTTPS) | панель |
| `NODE_PORT`/tcp | хаб нод (WSS) | панель, если принимает ноды |
Нода открывает только UDP для клиентов: к панели она подключается сама.
## Требования
- **ОС**: Ubuntu 24.04+ (Тесты проходили на Ubuntu 24.04.4 LTS) или Debian 11–13 (поддержка экспериментальная: PPA amnezia подключается вручную, серия подбирается по релизу Debian и переопределяется через `AMNEZIA_PPA_SUITE`)
- **ОС**: 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).
- **Виртуализация**: KVM или железо (не OpenVZ/LXC/Docker/WSL — нужен загружаемый
kernel-модуль AmneziaWG).
- **Заголовки ядра** под текущее ядро (для сборки DKMS-модуля).
- **Архитектура**: x86_64 или aarch64.
@@ -34,103 +74,180 @@ bash <(curl -Ls https://raw.githubusercontent.com/maeneko/forgetting/main/instal
```
Не от root — через `sudo bash -c`: `sudo bash <(curl …)` падает с
`/dev/fd/63: No such file or directory`, потому что `sudo` закрывает дескриптор
с текстом скрипта.
`/dev/fd/63: No such file or directory`, потому что `sudo` закрывает дескриптор с текстом скрипта.
```bash
sudo bash -c "$(curl -Ls https://raw.githubusercontent.com/maeneko/forgetting/main/install.sh)"
```
Установщик по умолчанию ставит сервер на AmneziaWG 3.1. Если модуль в ядре
старше 3.x или ядро старше 5.5, установка остановится с диагностикой — поставить
на 2.0 можно так:
Установщик спросит роль сервера:
```bash
AWG_GEN=auto bash <(curl -Ls https://raw.githubusercontent.com/maeneko/forgetting/main/install.sh)
```
| Роль | Что ставится |
|---|---|
| **Панель + VPN** (`standalone`) | Всё на одной машине; к ней можно подключать ноды |
| **Только панель** (`core`) | Панель без VPN; серверы подключаются к ней нодами |
| **Нода** (`node`) | VPN без панели; подключается к панели по строке `awgjoin://…` («Добавить сервер» в панели) |
Перевести такой сервер на 3.1 позже можно кнопкой «Перейти на AWG 3.1» в панели —
переустановка не нужна, пользователи сохраняются, ключи перевыпускаются
автоматически.
В конце установщик печатает адрес панели **вместе с секретным путём** —
`http://IP:PORT/<путь>/` — логин и пароль. Без пути панель отвечает `404`, поэтому адрес
сохраните целиком; позже его покажут `awg-ctrl status` и `awg-ctrl credentials`.
Источник архива и имя панели не зашиты в код — задаются переменными
`REPO_BASE`, `REPO_FALLBACK`, `ARCHIVE_URL`, `BRAND` перед запуском установщика
(подробности — в комментариях `install.sh` и в `.env`).
Порты на самом сервере установщик открывает сам (UFW, firewalld или iptables — что
активно). **Фаервол хостера** (панель провайдера, security group) отсюда не виден: открой там
порты из итога установки, иначе клиенты не подключатся, а на сервере всё будет выглядеть
исправным.
### Удаление
Сервер ставится на AmneziaWG 3.1. Если модуль в ядре старше 3.x или ядро старше 5.5,
установка остановится с диагностикой — поставить на 2.0 можно через `AWG_GEN=auto`. Перевести
сервер на 3.1 позже можно кнопкой «Перейти на AWG 3.1» в панели: пользователи сохраняются,
ключи перевыпускаются автоматически.
Тот же установщик: на сервере с установкой — пункт **3 «Удалить»**, без меню —
`UNINSTALL=y` (подтверждение «удалить» тогда не спрашивается):
### Параметры установщика
```bash
UNINSTALL=y bash <(curl -Ls https://raw.githubusercontent.com/maeneko/forgetting/main/install.sh)
```
Задаются переменными окружения перед запуском, например
`ROLE=node JOIN='awgjoin://…' bash <(curl -Ls …/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, заголовки ядра) остаются. Ноду после удаления уберите и из списка серверов панели.
| Переменная | Что делает |
|---|---|
| `ROLE` | `standalone`, `core` или `node` — без вопроса |
| `JOIN` | строка `awgjoin://…` для ноды |
| `AWG_GEN` | `3.1` (по умолчанию), `auto` — 3.1, если ядро позволяет, иначе 2.0; `2.0` — не трогать модуль |
| `AWG_PIN` | `y` (по умолчанию) — закрепить версию пакетов AmneziaWG (`apt-mark hold`) |
| `AWG_PORT` | UDP-порт VPN вместо случайного |
| `AWG_MTU` | MTU туннеля вместо случайного (1280–1500) |
| `UI_PREFIX` | свой секретный путь панели (8–64 символа `A–Z a–z 0–9 _ -`); `off` — без пути |
| `SUB_PORT`, `SUB_TLS`, `NODE_PORT` | порт и HTTPS подписки, порт хаба нод — без вопросов |
| `UNINSTALL` | `y` — удалить, см. ниже |
| `AMNEZIA_PPA_SUITE` | серия PPA для Debian (`focal`/`jammy`/`noble`) |
| `REPO_BASE`, `REPO_FALLBACK`, `ARCHIVE_URL`, `AWG_LOCAL_TARBALL` | откуда брать архив релиза (своё зеркало, локальный файл) |
| `BRAND`, `CHANNEL` | имя продукта в баннерах и панели |
### Секретный путь панели
### Обновление и удаление
Установщик выдаёт панели случайный путь: она открывается по `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` принимается и без пути.
1. **Обновить** — новая версия, настройки, пользователи и роль сохраняются. `cli.env` не
меняется, поэтому секретного пути у установки, где его не было, не появится — включить:
`UI_PREFIX=<путь>` в `cli.env` и `awg-ctrl restart` (или переустановка с сохранением данных,
она выдаст путь сама).
2. **Переустановить** — с нуля; пользователей и ключ сервера можно сохранить. Тогда сохраняются
и порт, параметры обфускации, MTU и секретный путь — они зашиты в выданные ключи и ссылки.
3. **Удалить** — полностью (без меню: `UNINSTALL=y`, подтверждение «удалить» тогда не
спрашивается).
### Порт подписки sen://
Удаление снимает сервисы и юнит systemd, интерфейс `awg1`, nft-таблицы шейпинга и блокировки
торрентов, открытые установщиком порты, Prometheus из `awg-ctrl monitor install`,
`/opt/awg-control`, `/etc/amnezia` (пользователи, ключи, базы), команду `awg-ctrl`, sysctl-файл
и логи установки (в них пароль панели). Перед этим установщик предлагает сохранить копию данных
в `/root/forgetting-backup-<время>.tar.gz`. AmneziaWG (пакеты, модуль, PPA) — по отдельному
вопросу; общие зависимости (dkms, build-essential, iptables, заголовки ядра) остаются. Ноду
после удаления уберите и из списка серверов панели.
Подписка отвечает по протоколу только тем, кто доказал знание секрета: `peek`/`register` —
с секретом мастер-ключа из ссылки, остальные запросы — с подписью ключом устройства. На всё
прочее (другие пути и методы, битое тело, чужой секрет, подобранный id устройства, неверная
подпись) — `404 page not found`, как пустой веб-сервер. Удалённому устройству `410` уходит,
только если запрос подписан его ключом (они хранятся в `revoked_devices`); устройства,
удалённые до этой версии, получают тот же тихий 404. Самоподписанные сертификаты подписки
(`SUB_TLS=on`) и хаба нод создаются со случайным именем вместо общих `sen` / `awg-node`:
клиенты и ноды сверяют ключ, а не имя, так что прежние сертификаты продолжают работать.
## Несколько серверов
### Порт хаба нод
**Подключить сервер.** В панели «Добавить сервер» выдаёт строку `awgjoin://…` (действует час).
На новом сервере — установщик с ролью «Нода» и этой строкой, или на уже установленном —
`awg-ctrl join '<awgjoin://…>'`. Агент ноды сам подключается к хабу панели по WSS; доверие —
закреплённый отпечаток сертификата хаба и ключ ноды. В панели нода появляется в списке
серверов, ею управляют так же, как своим. `awg-ctrl unjoin` — отвязать.
Агент ноды подписывает своим ключом уже сам запрос на WebSocket (заголовки `X-Node-Id`,
`X-Node-Ts`, `X-Node-Sig`, при первой привязке — ещё секрет из строки `awgjoin://`). Хаб
проверяет подпись до апгрейда и без неё отвечает `404 page not found`, как пустой сервер:
одним подключением к `/node/v1` его больше не узнать. Агенты прежних версий подписи не шлют —
хаб пускает их, пока в панели есть нода, ни разу не подключавшаяся по-новому, и пишет об этом
в лог. Обновили все ноды — путь без подписи закрывается сам. `HUB_LEGACY=on` / `off` в
`cli.env` (и `awg-ctrl restart`) — принудительно. Часы ноды могут расходиться с панелью до
суток.
**Перенести панель на отдельный сервер** (например, без VPN). Вкладка «Перенос» или CLI:
### Внешний Prometheus
1. На новом сервере — установка с ролью «Только панель».
2. На новой панели — код переноса `awgmove://…`; на старой — вставить его (или
`awg-ctrl migrate export` на старом и `awg-ctrl migrate import <архив>` на новом).
3. Старый сервер становится нодой #0 новой панели и пересылает туда свои прежние порты:
ссылки `sen://`, ноды и внешний API продолжают работать, закладки панели — тоже (новая
панель принимает и свой секретный путь, и старый).
4. Когда вкладка «Перенос» покажет, что новый адрес знают все устройства и ноды, старый
сервер можно выключать. Ноды, переехавшие вручную, — `awg-ctrl rehome <хост[:порт]>`.
Prometheus на ноде («Подключить Prometheus» у панели без своего VPN) от секретного пути не
зависит: он забирает метрики у агента на `127.0.0.1`, а тот — у панели внутри TLS-соединения
с хабом. Снаружи этот трафик не отличить от обычной связи ноды с панелью.
`awg-ctrl migrate cancel` — отменить на старом сервере, пока перенос не завершён.
Для стороннего сервера мониторинга — вкладка «Монитор» → «Внешний Prometheus»: выдать токен «только метрики» и скопировать готовый
фрагмент `prometheus.yml`. Адрес в нём берётся от страницы панели, поэтому уже содержит
секретный путь (`metrics_path: /<путь>/metrics`) или подпапку nginx. Без пути `/metrics`
снаружи отвечает 404. Панель по голому HTTP передаёт токен и метрики открытым текстом —
внешний скрейп лучше вести через nginx с TLS или по VPN.
## Защита от обнаружения
### Панель за nginx
Раньше у всех установок были одинаковые порт, параметры обфускации и ответы на запросы — одно
правило DPI находило их все. Теперь общего у серверов нет:
Панель работает и в корне домена, и в любой подпапке — пути в ней относительные,
настраивать сам префикс не нужно. Пример для `https://example.com/forgetting/`
(`39708` — `PORT` панели из `/opt/awg-control/cli/cli.env`):
**VPN.** На новой установке случайны:
- UDP-порт (20000–59999), если не задан `AWG_PORT`;
- параметры обфускации `Jc/Jmin/Jmax`, `S1–S4` (размеры handshake-пакетов разные — по длине
тип пакета не узнать), `H1–H4` (непересекающиеся диапазоны);
- `I1` — пакет-маскировка перед handshake: DNS-запрос к случайному популярному домену (формат
проверяется до записи — его принимают и kernel-модуль, и amneziawg-go);
- MTU в диапазоне 1320–1376.
При переустановке с сохранением данных всё это берётся из прежнего `awg1.conf`: параметры
зашиты в выданные ключи. Поэтому **серверы, поставленные до рандомизации, сохраняют старые общие
значения** — чтобы уйти от них, нужна переустановка без сохранения данных и раздача новых
ключей. Исключение — `I1`: его сервер не сверяет, и если в конфиге его не было, при переустановке
он генерируется новым, не ломая старые ключи.
**Открытые порты** отвечают тому, кто не доказал право на ответ, как пустой веб-сервер на Go —
`404 page not found`:
| Порт | Кому отвечает по протоколу |
|---|---|
| Панель | запросам под секретным путём (`UI_PREFIX`); с loopback — и без него |
| Подписка `sen://` | `peek`/`register` с секретом мастер-ключа из ссылки; остальным запросам — с подписью ключом устройства |
| Хаб нод | агенту, подписавшему своим ключом сам запрос на WebSocket |
Подробности:
- `GET /ui/brand` (имя и версия) — только после входа; экран логина не показывает имя продукта.
- Удалённому устройству подписка отвечает `410` (снять конфиг), только если запрос подписан его
ключом (`revoked_devices`); устройства, удалённые до этой версии, получают тихий `404`.
- Пока выдан код переноса, `POST /migrate/v1/import` принимается и без секретного пути.
- Агенты нод прежних версий подписи не шлют: хаб пускает их, пока в панели есть нода, ни разу
не подключавшаяся по-новому, и пишет об этом в лог. Обновили все ноды — путь без подписи
закрывается сам (`HUB_LEGACY` в `cli.env` — принудительно). Часы ноды могут расходиться с
панелью до суток.
- Самоподписанные сертификаты подписки и хаба получают случайное имя вместо общих `sen` /
`awg-node`. Клиенты и ноды сверяют ключ, а не имя; прежние сертификаты остаются со старым именем.
**Чего это не закрывает.** Секретный путь и тихий `404` защищают от активного сканирования.
Панель по умолчанию работает по голому HTTP — её трафик (пути, JSON, токены) виден в сети.
Ставьте её за nginx с TLS ([ниже](#панель-за-nginx)) или заходите по VPN. Подписка `sen://`
по умолчанию HTTPS, хаб — всегда TLS.
## Журнал подключений
Вкладка «Подключения» показывает по выбранному серверу:
- **События** — кто подключился к VPN и отключился, когда, сколько длилась сессия и сколько
скачал и отдал. Поиск — по имени пользователя, мастер-ключу, устройству или внутреннему IP.
- **Кто когда был** — все пользователи и устройства `sen://` сервера: онлайн сейчас, когда
были на связи последний раз и кто не подключался ни разу с начала журнала.
Журнал ведёт awg-ctrl каждого сервера (`conn_log` в `users.db`, 90 дней). У WireGuard нет событий
подключения, поэтому раз в 30 с он сверяет время handshake и счётчики трафика: «подключился» —
время первого handshake, «отключился» — последняя активность (точность около 30 с). Перезапуск
awg-ctrl сессии не рвёт, падение интерфейса отключением не считается. Внешний IP клиентов не
записывается. Серверы на прежней версии журнала не ведут — вкладка попросит их обновить.
## Мониторинг
**Встроенный Prometheus.** Вкладка «Монитор» → «Подключить Prometheus»: панель просит awg-ctrl
поставить его (`awg-ctrl monitor install`, только Debian/Ubuntu). Он слушает только
`127.0.0.1:9090`, хранит 31 день и забирает метрики **всех серверов** панели. Графики — скорость,
устройства онлайн, нагрузка, CPU и память — появляются через минуту-две. Удалить — там же или
`awg-ctrl monitor remove`.
У панели без своего VPN Prometheus ставится на ноду (выберите её в списке серверов). Он
забирает метрики у агента на `127.0.0.1`, а агент — у панели внутри TLS-соединения с хабом;
панель читает графики через того же агента. Снаружи этот трафик не отличить от обычной связи
ноды с панелью, секретный путь ему не нужен. Агенты старше 0.4.6 этого не умеют.
**Свой Prometheus.** Вкладка «Монитор» → «Внешний Prometheus»: выдать токен «только метрики»
(он не открывает ни панель, ни API) и скопировать готовый фрагмент `prometheus.yml`. Адрес в нём
берётся от страницы панели, поэтому уже содержит секретный путь
(`metrics_path: /<путь>/metrics`) или подпапку nginx. По голому HTTP токен и метрики идут
открытым текстом — скрейпьте через nginx с TLS или по VPN. Метрики по отдельным пирам
выключены (`METRICS_PEERS=on` в `cli.env` — включить).
## Панель за nginx
Панель работает и в корне домена, и в любой подпапке — пути в ней относительные. Пример для
`https://example.com/forgetting/` (`39708` — `UI_PORT` из `/opt/awg-control/cli/cli.env`):
```nginx
location = /forgetting { return 301 /forgetting/; }
@@ -143,35 +260,34 @@ location ^~ /forgetting/ {
}
```
Адрес клиента панель берёт из `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`. От остальных
заголовки игнорируются. Без этого все входы шли бы с адреса прокси и делили один
лимит неудачных попыток.
nginx на этой же машине ходит к панели по loopback, поэтому секретный путь ему не нужен —
подпапка в nginx и становится путём панели (выберите неочевидную). Адрес клиента панель берёт
из `X-Forwarded-For` / `X-Real-IP` только от доверенного прокси: с loopback — всегда, с других
адресов — если они перечислены в `TRUST_PROXY`. Без этого все входы шли бы с адреса прокси и
делили один лимит неудачных попыток.
Сам порт панели после этого можно закрыть снаружи (`ufw deny <PORT>/tcp`) — порты
подписки (`SUB_PORT`) и хаба нод (`NODE_PORT`) должны оставаться открытыми: клиенты
и ноды ходят на них напрямую, не через nginx.
Сам порт панели после этого можно закрыть снаружи (`ufw deny <UI_PORT>/tcp`). Порты подписки
(`SUB_PORT`) и хаба нод (`NODE_PORT`) должны оставаться открытыми: клиенты и ноды ходят на них
напрямую, не через nginx.
## API
Наружу смотрят две независимые поверхности. **Публичный API** `/api/v1` —
стабильный контракт для сторонних программ, авторизация по API-ключу.
**Роуты панели** — всё, что умеет веб-интерфейс, авторизация по JWT. Базовый
URL в обоих случаях — адрес сервера и порт, выбранный при установке.
Наружу смотрят две независимые поверхности. **Публичный API** `/api/v1` — стабильный контракт
для сторонних программ, авторизация по API-ключу. **Роуты панели** — всё, что умеет
веб-интерфейс, авторизация по JWT.
В примерах `HOST:PORT` — адрес панели **вместе с секретным путём**:
`http://203.0.113.9:39708/k3f9xQ2mTq8vLw0a`. Без пути снаружи любой запрос получает `404`.
> [!WARNING]
> Роуты панели отдают приватные поля пиров (`vpn_key`, `psk_key`) — это не
> публичный контракт, в отличие от `/api/v1`. Не выставляй её порт в интернет
> без TLS и ограничения по адресам.
> Роуты панели отдают приватные поля пиров (`vpn_key`, `psk_key`) — это не публичный контракт,
> в отличие от `/api/v1`. По голому HTTP они идут открытым текстом: ставьте панель за TLS.
### Публичный API (`/api/v1`)
Ключ создаётся во вкладке «API-ключи» и уходит в заголовке `X-Api-Key`.
Открытое значение показывается один раз при создании — на сервере лежит только
SHA-256 и префикс для показа в списке. Наружу отдаются только публичные поля:
`psk_key` и `pub_key` не покидают сервер.
Ключ создаётся во вкладке «API-ключи» и уходит в заголовке `X-Api-Key`. Открытое значение
показывается один раз при создании — на сервере лежит только SHA-256 и префикс для показа в
списке. Наружу отдаются только публичные поля: `psk_key` и `pub_key` не покидают сервер.
```bash
curl -s -X POST http://HOST:PORT/api/v1/users \
@@ -186,22 +302,20 @@ curl -s -X POST http://HOST:PORT/api/v1/users \
| `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: у пира нет изменяемых полей, «перевыпустить»
ключ = удалить и создать заново.
`gen` — поколение AmneziaWG, на параметрах которого собран ключ (`"2"` или `"3.1"`). Операции
только CRD: у пира нет изменяемых полей, «перевыпустить» ключ = удалить и создать заново.
У ключа — **набор серверов**, которые он видит (панель → «API-ключи» → шестерёнка;
`0` — сервер самой панели, остальные — подключённые ноды). Пользователи `vpn://`
заводятся на сервере ключа по умолчанию; другой сервер из набора выбирается
заголовком `X-Server-Id: <id>`, сервер вне набора → `404`.
У ключа — **набор серверов**, которые он видит (панель → «API-ключи» → шестерёнка; `0` — сервер
самой панели, остальные — подключённые ноды). Пользователи `vpn://` заводятся на сервере ключа
по умолчанию; другой сервер из набора выбирается заголовком `X-Server-Id: <id>`, сервер вне
набора → `404`.
#### Мастер-ключи `sen://`
Мастер-ключ — ссылка `sen://…` для приложения SenAWG с лимитом устройств: каждое
устройство само регистрируется по ссылке и получает пира на всех серверах ключа,
его приватный ключ сервер не видит. API-ключ видит и меняет мастер-ключ, только
если **все** его серверы входят в набор API-ключа; остальные для него не
существуют (`404`).
Мастер-ключ — ссылка `sen://…` для приложения SenAWG с лимитом устройств: каждое устройство
само регистрируется по ссылке и получает пира на всех серверах ключа, его приватный ключ сервер
не видит. API-ключ видит и меняет мастер-ключ, только если **все** его серверы входят в набор
API-ключа; остальные для него не существуют (`404`).
```bash
curl -s -X POST http://HOST:PORT/api/v1/masterkeys \
@@ -229,12 +343,11 @@ curl -s -X POST http://HOST:PORT/api/v1/masterkeys \
| `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`.
Ключи лежат в собственной БД панели (`/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`.
### Авторизация панели
@@ -249,19 +362,20 @@ TOKEN=$(curl -s -X POST http://HOST:PORT/login \
| `POST /login` | `{ "token": "<JWT>" }`, срок 24 часа. `401` — неверная пара; `429` — больше 5 неудачных попыток с одного IP за 15 минут |
| `POST /logout` | `{ "ok": true }`, токен отзывается немедленно |
Дальше в каждый запрос: `Authorization: Bearer $TOKEN`. Ниже — роуты, которые
дёргает сама панель; в отличие от `/api/v1` их форма может меняться.
Дальше в каждый запрос: `Authorization: Bearer $TOKEN`; сервер — заголовком `X-Server-Id`
(без него — сервер по умолчанию). Ниже — роуты, которые дёргает сама панель; в отличие от
`/api/v1` их форма может меняться.
### Состояние
| Метод | Ответ |
|---|---|
| `GET /ui/brand` | `{ "brand": "Forgetting", "channel": "Beta", "version": "0.4.7" }` — требует JWT панели (без авторизации выдавал бы панель сканерам). Вордмарк в шапке после входа |
| `GET /ui/brand` | `{ "brand": "Forgetting", "channel": "Beta", "version": "0.4.7" }` |
| `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`, а не хранится отдельно.
`gen` — поколение AmneziaWG, на параметрах которого сейчас работает интерфейс: `"2"` или
`"3.1"`. Оно выводится из `awg1.conf`, а не хранится отдельно.
### Пользователи
@@ -281,8 +395,16 @@ curl -s -X POST http://HOST:PORT/api/users \
```
Имея `vpn_key`, конфиг `.conf` можно получить локально: убрать префикс `vpn://`,
base64url-декодировать, отбросить первые 4 байта (длина), распаковать
`zlib inflate` → JSON; текст конфига лежит в `last_config.config`.
base64url-декодировать, отбросить первые 4 байта (длина), распаковать `zlib inflate` → JSON;
текст конфига лежит в `last_config.config`.
### Журнал подключений
| Метод | Ответ |
|---|---|
| `GET /ui/connlog` | Журнал сервера из `X-Server-Id`: `{ events: [{ id, at, event, name, owner, ip, key?, device?, duration?, rx?, tx? }], peers: [{ name, owner, ip, online, last_seen, key?, device? }], since, keep_days }`. `event` — `connect` или `disconnect` (у него `duration`, `rx`, `tx` — со стороны сервера); `key`/`device` — мастер-ключ и устройство `sen://`. Параметры: `limit` (до 500), `before=<id>` — следующая страница, `q` — поиск по имени, владельцу, IP. `501` — сервер старой версии |
Без имён `sen://` тот же журнал отдаёт awg-ctrl: `GET /api/connlog`.
### Интерфейс AWG
@@ -292,10 +414,9 @@ base64url-декодировать, отбросить первые 4 байта
| `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.
`POST /awg/upgrade` отвечает `409`, если сервер уже на 3.1, модуль в ядре не 3.x или ядро
старше 5.5, и `500` с текстом причины, если ядро не приняло 3.1-конфиг — в этом случае прежний
конфиг возвращается из копии, а интерфейс поднимается обратно на 2.0.
### Мастер-ключи
@@ -314,32 +435,27 @@ base64url-декодировать, отбросить первые 4 байта
| `DELETE /ui/devices/:id` | Отвязать устройство |
| `POST /ui/devices/:id/rekey`, `/psk` | Смена ключа / PSK у устройства |
## Журнал подключений
### Метрики
Вкладка «Подключения» показывает по выбранному серверу:
- **События** — кто подключился к VPN и отключился, когда, сколько длилась сессия и сколько
скачал и отдал. Поиск — по имени пользователя, мастер-ключу, устройству или внутреннему IP.
- **Кто когда был** — все пользователи и устройства sen:// сервера: онлайн сейчас, когда были на
связи последний раз и кто не подключался ни разу с начала журнала.
Журнал ведёт awg-ctrl каждого сервера (`conn_log` в `users.db`, 90 дней). У WireGuard нет событий
подключения, поэтому раз в 30 с он сверяет время handshake и счётчики трафика: «подключился» —
время первого handshake, «отключился» — последняя активность (точность около 30 с). Перезапуск
awg-ctrl сессии не рвёт, падение интерфейса отключением не считается. Внешний IP клиентов не
записывается. Тот же журнал по API: `GET /api/connlog?limit=&before=&q=`. Серверы на прежней
версии журнала не ведут — вкладка попросит их обновить.
| Метод | Что делает |
|---|---|
| `GET /ui/metrics-token` | `{ exists, prefix?, created_at?, last_used?, per_peer }` — токен для своего Prometheus |
| `POST /ui/metrics-token` | Выпустить или перевыпустить: `201` с `token` (показывается один раз); старый перестаёт работать |
| `DELETE /ui/metrics-token` | Отозвать |
| `GET /metrics` | Экспозиция Prometheus по всем серверам; `Authorization: Bearer awgm_…` |
## Управление
```bash
awg-ctrl # меню
awg-ctrl start|stop|restart|status
awg-ctrl credentials [user|pass]
awg-ctrl start|stop|restart|status # 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 peers [drop <область>] # пиры sen:// на этом сервере по панелям
awg-ctrl monitor install|status|remove # встроенный Prometheus
awg-ctrl migrate export [файл] # перенос панели (см. «Несколько серверов»)
awg-ctrl migrate import <архив>
awg-ctrl migrate cancel
```
@@ -349,5 +465,22 @@ awg-ctrl migrate cancel
systemctl start|stop|restart awg-control
```
### Настройки `cli.env`
`/opt/awg-control/cli/cli.env` пишет установщик; после правки — `awg-ctrl restart`. Чаще всего
нужны:
| Переменная | Что делает |
|---|---|
| `UI_PORT`, `UI_USER`, `UI_PASS` | порт, логин и пароль панели (`awg-ctrl credentials`) |
| `UI_PREFIX` | секретный путь панели; пусто — без пути |
| `UI_PREFIX_MOVED` | путь панели, переехавшей сюда: принимается вместе со своим |
| `SUB_PORT`, `SUB_TLS`, `SUB_HOST` | подписка `sen://`: порт, HTTPS, адрес в ссылках |
| `NODE_PORT` | хаб нод; пусто — ноды не принимаются |
| `HUB_LEGACY` | `on` / `off` — пускать ли агентов нод без подписи; пусто — пока не обновлены все |
| `TRUST_PROXY` | IP или сети reverse-proxy, кроме loopback: от них верим `X-Forwarded-For` |
| `METRICS_PEERS` | `on` — метрики по отдельным пирам |
| `AUDIT` | `off` — выключить периодический аудит пиров |
## Лицензия
[MIT](LICENSE) © 2026 Ivan Vasilev