15 KiB
Ревью проекта awg_profiler
Дата: 2026-07-18. Метод: сверка каждого утверждения README.md с кодом (все 13 Go-файлов, оба Dockerfile, compose-файлы, entrypoint'ы, webui).
Общий вердикт: README точен, проект добротный — атомарные записи, продуманная схема статистики, аккуратное разделение CLI/web. Но при сверке нашлись реальные расхождения и проблемы в самом коде.
Точность README: подтверждено кодом
- Окно «онлайн» ≤ 150 с — совпадает (
onlineWindow = 150s, stats.go:29), обновление UI каждые 10 с (setInterval(refreshAll, 10000)), фоновый замер раз в 20 с (time.Tick(20s)). - Накопление статистики: дельты неотрицательны, сброс счётчика распознаётся,
atomic write (temp+rename), битый файл →
.bad— всё как описано (stats.go). - Секреты не отдаются в JSON API (
clientOutбез private/psk), Basic auth черезsubtle.ConstantTimeCompare,die()в web-режиме превращается в HTTP-ошибку — совпадает. go vetчистый,go test ./...— ok (9 тестов).
Найденные проблемы
1. CSRF на мутирующих POST-эндпоинтах (средняя серьёзность)
Web API защищён только Basic auth, а браузер прикладывает эти credentials
автоматически. Cross-origin <form> POST с телом text/plain не требует
preflight, а json.Decoder в хендлерах не проверяет Content-Type — то есть
вредоносная страница может выполнить POST /api/server/stop,
/api/clients (create), /api/server/restart от имени залогиненного
админа. DELETE через форму невозможен, но enable/disable/stop — POST.
Фикс: проверка Content-Type: application/json или заголовка
Origin/Sec-Fetch-Site в обёртке h() (web.go:214) — ~5 строк.
2. Заявлен произвольный CIDR, реально поддерживается только /24
init-server спрашивает «VPN network CIDR» (default 10.0.0.0/24), но:
serverIP()жёстко берётx.y.z.1;getFirstClientIP—x.y.z.2;incrementIPкрутит только последний октет (умирает на .255 → максимум ~252 клиента);awgConfHeaderпишетAddress = %s/24независимо от введённого префикса (awg.go:111).
Введи пользователь 10.0.0.0/16 — конфиг молча станет /24.
Фикс: либо валидация «только /24» на входе, либо честная поддержка префикса.
3. QR-код содержит приватный ключ, но PNG создаётся с правами 0644
<name>.conf пишется с 0600, а <name>.png — вывод qrencode с дефолтным
umask (0644) в каталоге 0755. QR кодирует весь конфиг, включая PrivateKey
и PresharedKey — любой локальный пользователь хоста может его прочитать и
декодировать.
Фикс: chmod 0600 после генерации и/или 0700 на awg_clients/.
4. Противоречие Alpine-веток
Комментарий в Dockerfile: «AmneziaWG is NOT packaged in Alpine's repos»
(потому tools собираются из исходников), но deps.go для Alpine-хоста
выполняет apk add amneziawg-tools — если пакета нет, install-deps на
голом Alpine просто упадёт. Одно из двух утверждений неверно.
Фикс: проверить наличие пакета в Alpine и привести к единому поведению (либо собирать из исходников и на хосте, либо убрать комментарий).
5. docker-compose по умолчанию: web без auth на всех интерфейсах хоста
ports: "8080:8080" публикует UI наружу, а auth закомментирован. Приложение
печатает WARN, README предупреждает — но безопасный дефолт был бы
"127.0.0.1:8080:8080" с комментарием «поменяйте после включения auth».
Мелочи
randMagic(): диапазон получается [5, 2147483646] вместо заявленного [5, 2147483647] (n % 2147483642 + 5), плюс небольшой modulo bias от uint32. Косметика, но спека в комментарии не совпадает с кодом на единицу.handleCreate: еслиawgPeerAddупадёт послеsaveRegistry, клиент останется в реестре, а вызывающему вернётся 400 — частичное состояние без отката.- IP-адреса удалённых клиентов не переиспользуются (кроме последнего) — пул «протекает» при churn'е.
- Тесты покрывают только чистые функции (util/config/stats-accumulate); ни
одного теста на HTTP-хендлеры или registry-операции, хотя они легко
тестируются с
AWG_PROFILER_DIRво временный каталог (паттерн уже есть вTestStateRoundTrip). - Оценка «~20-30 МБ рантайм-слой» в README оптимистична: один бинарник профилировщика — 10.5 МБ, плюс alpine+bash+iproute2+nftables; реально ближе к 40-50 МБ. Стоит поправить или убрать цифру.
Что сделано хорошо
- Раздельные мьютексы:
opLockдля WG-операций,statsLockдля статистики — фоновый замер не блокируется долгим install. die()→ panic → recover в web-режиме: ошибка операции становится HTTP-ответом, а не падением сервера.- Валидация ответа IP-сервисов через
net.ParseIPс лимитом чтения (защита от HTML-ответов вместо адреса). - Hot-add/remove пиров через
awg setбез рестарта интерфейса. entrypointсеет state-флаг только при отсутствии файла — не затирает данные на persistent volume..dockerignoreминимизирует build-контекст.- Дизайн накопления статистики (баз-поинт на диске + неотрицательные дельты) — корректное решение реальной проблемы userspace-рестартов.
Рекомендуемый порядок исправлений
- №1 (CSRF) и №3 (права QR) — безопасность.
- №2 — валидация /24.
- №4 — согласовать Alpine-ветки.
- Остальное — по мере необходимости.
Исправлено в ходе ревью
Dockerfile(Alpine): отсутствовалCOPY webui_glass/ ./webui_glass/—go buildпадал на чистом чекауте из-за//go:embed webui/* webui_glass/*. Строка добавлена, сборка проверена.
Статус доработок (выполнены)
1. CSRF — исправлено
Добавлена функция csrfSafe() (web.go), вызывается из обёртки h():
любой POST/DELETE без Content-Type: application/json отклоняется
415 Unsupported Media Type. HTML-форма физически не может выставить этот
заголовок (только text/plain, application/x-www-form-urlencoded,
multipart/form-data), поэтому blind cross-site form-POST больше не
проходит. GET не тронут (не мутирует состояние).
Клиентская часть (webui/app.js, webui_glass/app.js, идентичны —
обновлены оба) теперь всегда шлёт этот заголовок на POST/DELETE, даже
если тела нет (server/start, enable/disable, stats/reset,
install-deps и т.д. раньше отправлялись вовсе без Content-Type).
Тест: TestCsrfSafe (awg_profiler_test.go).
2. Валидация /24 — исправлено (вариант «запретить не-/24»)
Добавлена requireSlash24() (util.go): парсит CIDR через net.ParseCIDR,
требует IPv4 и ровно /24, требует совпадения введённого адреса с базовым
адресом подсети (иначе подсказывает правильный). Вызывается из
initServer() (CLI, server.go) и initServerWeb() (веб-мастер, webops.go)
сразу после чтения Network. Любой другой префикс теперь явно отклоняется
с понятным сообщением, а не молча превращается в /24.
Тест: TestRequireSlash24.
README дополнен пояснением, что принимается только /24.
3. Права QR-кода — исправлено
createClientConfig() (client.go) теперь делает os.Chmod(pngPath, 0600)
сразу после qrencode, той же логике, что уже применялась к .conf.
Дополнительно (по варианту «и/или 0700 на awg_clients/» из фикса) каталоги
data/ и awg_clients/ в initStorage() (registry.go) и в
saveStatsLocked() (stats.go) теперь создаются с правами 0700 вместо
0755 — они хранят приватные ключи (registry.json, .conf/.png).
Существующие деплойменты, где каталоги уже созданы с 0755, не меняются
автоматически (MkdirAll не трогает права существующих директорий).
4. Противоречие Alpine-веток — исправлено
Проверено через веб-поиск: amneziawg-tools действительно отсутствует в
официальных apk-репозиториях Alpine — значит был неверен deps.go, а не
комментарий в Dockerfile. installDeps() для Alpine больше не делает
apk add amneziawg-tools (пакета нет — команда просто падала бы на живом
хосте); вместо этого новая функция buildAmneziawgToolsFromSource()
(deps.go) собирает awg/awg-quick из исходников тем же способом, что и
tools-builder-стадия в Dockerfile (git clone → make -C src →
make -C src install PREFIX=/usr). Требование к kernel-модулю (нужно
предоставить отдельно) осталось прежним и явно описано в README.
5. docker-compose без auth на всех интерфейсах — исправлено
docker-compose.yml и docker-compose.mint.yml: порт 8080 теперь
публикуется как 127.0.0.1:8080:8080 вместо 8080:8080 — веб-UI по
умолчанию доступен только с самой машины. Комментарий рядом объясняет, что
расширять до всех интерфейсов стоит только вместе с
AWG_WEB_USER/AWG_WEB_PASS. README (Quick Start, вариант A) обновлён:
объяснено, как открыть UI локально или через SSH-туннель, и что менять
перед тем, как открывать порт наружу.
Мелочи — частично исправлены
randMagic()off-by-one — исправлено. Диапазон теперь честные[5, 2147483647](span = 2147483647-5+1), а не[5, 2147483646].handleCreateчастичное состояние — исправлено.awgPeerAdd()(awg.go) больше не вызываетdie()при неудачеawg set— на этом этапе клиент уже сохранён в реестре и добавлен в<iface>.conf(confAppendPeerвызывается раньше), так что живой hot-add — best-effort: при неудаче пишетсяwarn()и создание клиента по-прежнему считается успешным (применится на следующем restart/sync-config).- Тесты на HTTP-хендлеры/registry — частично. Добавлены целевые тесты
на обе новые функции безопасности (
TestCsrfSafe,TestRequireSlash24). Полное покрытие HTTP-хендлеров (handleCreate,handleDeleteи т.д.) в эту доработку не входило — осталось как есть. - IP-адреса удалённых клиентов не переиспользуются — не тронуто осознанно. Изменение схемы выдачи IP — это поведенческое изменение с риском разойтись с форматом реестра bash-версии; оставлено как зафиксированный, но не блокирующий issue.
- Оценка размера образа «~20-30 МБ» — исправлено. Неподтверждённая цифра убрана из README вместо того, чтобы гадать без реальной сборки образа.
Все правки проверены: gofmt -l . чист, go vet ./... чист, go build .
успешен, go test ./... — 11/11 тестов проходят (добавлены TestCsrfSafe,
TestRequireSlash24).