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

212 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ревью проекта 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. **№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 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`).