Агент для AmneziaWG и WireGuard. Поднимает интерфейс, раздаёт конфиги, считает трафик, режет скорость и показывает, кто подключён.
| Протокол 3.1 | HeaderProtectionKey, RandomTrailers, DisableCookies — полный набор, который понимают тулзы в образе |
| Сигнатурные пакеты | Настоящие CPS-датаграммы под DNS и QUIC, у каждого клиента свои |
| Лимиты трафика | Квота с автосбросом раз в день, неделю, месяц или год |
| Шейпер скорости | Потолок на клиента через tc: HTB на скачивание, policer на отдачу |
| Сроки действия | Дата, после которой конфиг перестаёт работать |
| История расхода | Приём и отдача по дням, без дырок на днях простоя |
| Два режима | AmneziaWG с обфускацией или обычный WireGuard — одним переключателем |
| Веб-панель | Тёмная и светлая тема, живая статистика, QR, переименование в строке |
Три причины, и все нашлись на живых установках.
Протокол. Тулзы в образе умеют 3.1, а панель писала в конфиг только набор 2.0 — сервер поднимался заведомо слабее, чем мог.
Сигнатурные пакеты. I1..I5 — это датаграмма перед хендшейком,
похожая на обычный DNS-запрос или QUIC Initial: фильтру проще пропустить
знакомый протокол, чем разбирать непонятный шум. Панель подставляла туда
случайные числа (I1 = 104), которых тулзы не понимают.
Лицензия. amnezia-wg-easy — потомок wg-easy 14 под CC-BY-NC-SA, то есть NonCommercial. Продавать на нём сервис нельзя.
Переключатель VPN_PROTO — и тот же контейнер работает как AmneziaWG или
как обычный WireGuard:
-e VPN_PROTO=awg # по умолчанию: вся обфускация, протокол до 3.1
-e VPN_PROTO=wg # обычный WireGuard, ни одной лишней строки в конфигеВ режиме wg строк Jc, S1..S4, H1..H4, I1..I5 и
HeaderProtectionKey в конфиге нет вовсе — wg-quick на незнакомом
параметре падает, а не игнорирует его. Команды идут через wg/wg-quick
вместо awg/awg-quick, панель показывает «WireGuard» вместо «AWG 3.1»,
всё остальное — лимиты, скорость, сроки, история, API — работает
одинаково.
Отличий по сути ровно два: набор строк в конфиге и имя утилиты. Оба бинарника лежат в одном образе, поэтому это переключатель, а не второй образ и не второй репозиторий.
docker run -d --name awg-agent \
-e PASSWORD_HASH="$(htpasswd -nbB admin 'пароль' | cut -d: -f2)" \
-e WG_HOST=1.2.3.4 \
-v /opt/awg:/etc/amnezia/amneziawg \
-p 51820:51820/udp -p 51821:51821/tcp \
--cap-add NET_ADMIN --cap-add SYS_MODULE --cap-add NET_RAW \
--sysctl net.ipv4.ip_forward=1 --device /dev/net/tun \
ghcr.io/forestsnet/awg-agent:latestПанель — на http://1.2.3.4:51821, пароль тот, из которого считали хэш.
Осторожно с compose. В
docker-compose.ymlдоллары в bcrypt-хэше экранируются удвоением ($$2y$$05$$…). Иначе интерполяция съест куски хэша, в контейнер уедет огрызок — и не подойдёт ни один пароль.
Ручки повторяют amnezia-wg-easy, поэтому клиент, написанный под неё, работает с агентом без правок:
| Метод | Путь |
|---|---|
POST |
/api/session |
GET DELETE |
/api/session |
GET POST |
/api/wireguard/client |
DELETE |
/api/wireguard/client/{key} |
POST |
/api/wireguard/client/{key}/enable · /disable |
PUT |
/api/wireguard/client/{key}/name |
GET |
/api/wireguard/client/{key}/configuration · /qrcode.svg |
GET |
/api/health · /api/release |
GET |
/api/backup · /api/backup/archive |
POST |
/api/backup/restore |
{key} — id или имя: клиент ищется и так, и так. Кука сессии
называется connect.sid, как её ждут старые клиенты: новые сборки панели
переименовали её в wg0.sid, и всё, что искало прежнее имя, молча
переставало видеть сессию.
Агент несёт встроенный торрент-блокировщик (btguard, nft) и, по желанию, персональные логи клиента. Управление — по API:
| Метод | Путь | Что |
|---|---|---|
GET PUT |
/api/torrent |
конфиг блокировщика: enabled (вкл/выкл nft сразу), webhook_url/webhook_secret, node_name, block_duration — бан за торрент, сек (0 — без бана). В ответе GET ещё banned (кто в бане и сколько осталось) и nftAvailable. Секрет наружу не отдаётся — только webhookSecretSet |
PUT |
/api/wireguard/client/{key}/torrent |
исключить клиента из блокировки: {"exempt": true}; «без бана»: {"noban": true} — торрент режется и уходит в отчёт, но бана нет. Поле, которого нет в запросе, не меняется |
POST |
/api/wireguard/client/{key}/unban |
снять бан за торрент досрочно |
PUT |
/api/wireguard/client/{key}/contact |
контакты для отчётов: {"telegram_id": "…", "email": "…"} |
PUT |
/api/wireguard/client/{key}/log |
хранить персональный лог: {"enabled": true} |
GET |
/api/wireguard/client/{key}/log?limit=N |
хвост лога (свежие сверху) + logEnabled + sizeBytes |
DELETE |
/api/wireguard/client/{key}/log |
очистить логи клиента |
Глобально включить блокировщик — PUT /api/torrent {"enabled": true},
выключить — false (nft-таблица снимается сразу). Отчёты о торренте
уходят вебхуком в формате Remnawave — см. WEBHOOKS.md.
Бан за торрент — как в Remnawave: поймали на торренте, и клиент на
block_duration секунд теряет весь доступ (трафик режется в обе стороны),
снять досрочно — POST …/unban. 0 — без бана, режутся только
торрент-пакеты. Отдельному клиенту бан можно выключить флагом noban, не трогая
срок у остальных. Бан — по адресу клиента в туннеле, а не по внешнему:
внешний у мобильных операторов общий на сотни людей (CGNAT) и меняется со
сменой сети. Правила — в таблице inet: торрент ловится и по IPv6 (у
клиента ULA-адрес, при глобальном v6 у хоста трафик уходит через NAT66).
Персональные логи (по галочке на клиенте) пишутся пофайлово в JSONL
(<WG_PATH>/user-logs/<id>.log): сессии подключений, торрент-хиты и срезы
трафика (rx/tx + дельта раз в USERLOG_SNAPSHOT_SECONDS). Ротация — в
процессе, по размеру (USERLOG_MAX_BYTES, USERLOG_KEEP сжатых поколений
.gz): агент живёт в контейнере, host-logrotate ему недоступен. По
умолчанию логи выключены — включаются на конкретном клиенте (в панели —
галочка «Хранить логи» + просмотр/скачивание/очистка).
Скрипт переносит клиентов, ключи, PSK и параметры обфускации из amnezia-wg-easy и поднимает агент на том же порту и в той же подсети — уже розданные конфиги продолжают работать, перевыпускать не нужно:
# разведать (ничего не меняет)
curl -fsSL https://raw.githubusercontent.com/forestsnet/awg-agent/HEAD/migrate-from-wg-easy.sh | sudo bash -s -- --dry-run
# мигрировать
curl -fsSL https://raw.githubusercontent.com/forestsnet/awg-agent/HEAD/migrate-from-wg-easy.sh | sudo bash
# откат на amnezia-wg-easy
curl -fsSL https://raw.githubusercontent.com/forestsnet/awg-agent/HEAD/migrate-from-wg-easy.sh | sudo bash -s -- --rollbackСкрипт сам находит старый контейнер (amnezia-wg-easy или wg-easy), переносит его окружение (порты, подсеть, пароль панели) и wg0.json вместе с параметрами обфускации (включая S3/S4), останавливает панель (не удаляя — для мгновенного отката) и поднимает агента в /opt/vpn-agent/<proto>-agent-N — так же, как его ставит бот, поэтому дальше работает «Обновить агент». Проверяет /api/health, число клиентов и вход по паролю, при сбое откатывается сам. Детали и ручная процедура — MIGRATION.md.
При первом старте агент читает wg0.json от amnezia-wg-easy или wg-easy и переносит клиентов вместе с ключами, адресами и параметрами обфускации (набор 2.0 как есть, с S3/S4, если они были) — подмена контейнера никого не отключает и конфиги не ломает.
| Метод | Путь | Что делает |
|---|---|---|
GET |
/api/backup |
Состояние целиком: ключи сервера и клиентов |
GET |
/api/backup/archive |
То же zip'ом — кнопкой из панели |
POST |
/api/backup/restore |
Поднять состояние из копии |
Приватные ключи существуют ровно в одном экземпляре — на самой машине.
Дамп базы бота знает, у кого какой конфиг, но не знает ключей: без копии
смерть VPS означает раздать новые конфиги всем, кто на нём сидел. Ходить
за clients.json по SSH на каждый из десятка серверов не нужно — бот
забирает состояние этой ручкой в свой общий бэкап.
Восстановление заменяет всё, что было на агенте, и проверяет совместимость до того, как что-то трогает: копия от AmneziaWG на обычном WireGuard и копия из другой подсети отвергаются с объяснением. Свой секрет сессий агент оставляет — иначе восстановление выкинуло бы из панели того, кто его и запустил.
| Метод | Путь | Тело |
|---|---|---|
PUT |
/api/wireguard/client/{key}/quota |
{"bytes": 10737418240, "period": "month"} |
POST |
/api/wireguard/client/{key}/quota/reset |
— |
PUT |
/api/wireguard/client/{key}/rate |
{"bps": 5000000} |
PUT |
/api/wireguard/client/{key}/expires |
{"at": "2026-12-31T23:59:59"} |
GET |
/api/wireguard/client/{key}/usage?days=30 |
— |
GET |
/api/usage?days=30 |
— |
bytes: 0 снимает лимит, bps: 0 — ограничение скорости,
period: "none" — считать, но не сбрасывать, at: null — бессрочно.
Перебрал лимит или вышел срок — агент убирает пира из интерфейса сам, даже если панель закрыта. Начался новый период — возвращает. Выключенного руками лимиты не трогают: решение оператора сильнее расписания.
Почему считаем сами. awg show отдаёт счётчики пира с момента, как
он появился в интерфейсе: рестарт контейнера или выключение клиента их
обнуляют. Агент раз в QUOTA_TICK_SECONDS берёт прирост и складывает в
своё поле — лимит нельзя сбросить, переподняв интерфейс.
Скорость режется tc прямо на wg-интерфейсе: скачивание — классами
HTB по адресу назначения, отдача — policer'ом на ingress. Правила
перестраиваются целиком, а не правятся по одному: diff-логика на tc —
верный способ забыть снять чужое правило и потом гадать, почему у клиента
скорость чужого тарифа.
| Переменная | По умолчанию | Смысл |
|---|---|---|
PASSWORD_HASH |
— | bcrypt-хэш пароля. Без него агент не стартует |
VPN_PROTO |
awg |
awg — AmneziaWG, wg — обычный WireGuard |
PORT |
51821 |
Порт панели и API |
WEBUI_HOST |
0.0.0.0 |
Адрес, на котором слушает панель |
WG_HOST |
— | Адрес сервера для Endpoint в конфигах |
WG_PORT |
51820 |
UDP-порт |
WG_CONFIG_PORT |
= WG_PORT |
Порт в конфиге клиента, если наружу проброшен другой |
WG_INTERFACE |
wg0 |
Имя интерфейса |
WG_DEVICE |
авто | Интерфейс для NAT. Не задан — берём маршрут по умолчанию (в контейнере это eth0) |
WG_SUBNET |
— | Подсеть целиком: 10.8.0.0/16 — 65 534 адреса вместо 253 |
WG_DEFAULT_ADDRESS |
10.8.0.x |
Подсеть /24, если WG_SUBNET не задана |
WG_DEFAULT_DNS |
1.1.1.1 |
DNS в конфиге клиента |
WG_ALLOWED_IPS |
0.0.0.0/0, ::/0 |
Что заворачивать в туннель |
WG_PERSISTENT_KEEPALIVE |
25 |
Keepalive |
WG_MTU |
1280 (awg) / 1420 (wg) |
MTU. Замер: WireGuard добавляет к пакету 72 байта, AmneziaWG 3.1 — 131, то есть потолки при пути 1500 это 1428 и 1369. У awg дефолтные 1420 выше потолка: крупные пакеты пропадают, а соединение при этом «есть» |
AWG_PROTO |
3 |
Поколение AmneziaWG: 2 или 3 |
AWG_RANDOM_TRAILERS |
1 |
RandomTrailers (3.1) |
AWG_DISABLE_COOKIES |
0 |
DisableCookies (3.1) |
QUOTA_TICK_SECONDS |
10 |
Как часто считать трафик и применять лимиты |
HISTORY_DAYS |
90 |
Сколько дней хранить историю расхода |
SHAPER_ENABLED |
1 |
Резать скорость через tc |
WG_AUTO_UP |
1 |
Поднимать интерфейс при старте |
TORRENT_ENABLED |
1 |
Стартовое состояние блокировщика (дальше правится по API) |
TORRENT_WEBHOOK_URL |
— | Куда слать отчёты о торренте (пусто — только дроп) |
TORRENT_WEBHOOK_SECRET |
— | Секрет HMAC для подписи отчётов |
TORRENT_NODE_NAME |
= WG_HOST |
Имя ноды в отчёте |
TORRENT_BLOCK_DURATION |
3600 |
Бан за торрент, сек (0 — без бана, только дроп торрента). Дальше правится по API |
USERLOG_MAX_BYTES |
5242880 |
Порог ротации персонального лога (5 МБ) |
USERLOG_KEEP |
5 |
Сколько сжатых поколений лога хранить |
USERLOG_SNAPSHOT_SECONDS |
900 |
Как часто писать срез трафика в лог |
Клиент старее 3.0 к серверу с
HeaderProtectionKeyне подключится вовсе: заголовки шифрованы, пакеты молча отбрасываются.AWG_PROTO=3имеет смысл там, где все клиенты свежие.
| Поколение | Что добавляет |
|---|---|
| 1.x | Jc, Jmin, Jmax, S1, S2, H1..H4 |
| 2.0 | S3, S4, сигнатурные пакеты I1..I5 |
| 3.0 | HeaderProtectionKey — шифрование заголовков |
| 3.1 | RandomTrailers, DisableCookies |
Совпадать у сервера и клиента обязаны Jc/Jmin/Jmax, S1..S4, H1..H4
и HeaderProtectionKey. Сигнатурные пакеты — нет: их шлёт инициатор,
принимающая сторона просто отбрасывает. Поэтому каждому клиенту свой:
одинаковый I1 у всех и есть та примета, по которой их вычисляют.
python3 -m tests.test_agent # проверки без докера
docker build -t awg-agent:dev . # образПанель — три файла без сборщика (web/): один запрос на каждый, работает
сразу. Иконки — lucide, вшиты путями: тянуть библиотеку с CDN на сервер
клиента незачем.
MIT — берите, встраивайте, продавайте. Единственное условие: сохраняйте указание авторства, см. NOTICE.
Сделано в ForestsNet.