Files
awg-profiler-golang/review.md
T
2026-07-18 10:02:43 +03:00

15 KiB
Raw Blame History

Ревью проекта 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;
  • getFirstClientIPx.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. №1 (CSRF) и №3 (права QR) — безопасность.
  2. №2 — валидация /24.
  3. №4 — согласовать Alpine-ветки.
  4. Остальное — по мере необходимости.

Исправлено в ходе ревью

  • 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 clonemake -C srcmake -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).