Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1db3824cb4 | |||
| d9cad87fc8 | |||
| 63a2df893d | |||
| 63e653b710 | |||
| 5647d339b1 | |||
| 63c4fa930d |
@@ -0,0 +1,22 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
max_line_length = 100
|
||||
|
||||
[*.{yml,yaml,json,ts,tsx,js,css,html}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
.venv*/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
|
||||
# UI
|
||||
node_modules/
|
||||
ui/dist/
|
||||
ui/.vite/
|
||||
|
||||
# Runtime / состояние шлюза (живёт в томах, не в репозитории)
|
||||
data/
|
||||
config/
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
# Секреты и конфиги клиентов
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.conf
|
||||
!deploy/*.conf.example
|
||||
|
||||
# IDE / OS
|
||||
.idea/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
@@ -0,0 +1,24 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.9
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.11.2
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^backend/
|
||||
additional_dependencies: [pydantic>=2.9]
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: check-yaml
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
args: [--maxkb=512]
|
||||
@@ -0,0 +1,44 @@
|
||||
PY ?= python3
|
||||
VENV ?= .venv
|
||||
BIN := $(VENV)/bin
|
||||
IMAGE ?= awg-dash:dev
|
||||
COMPOSE_DEV := deploy/docker-compose.dev.yml
|
||||
|
||||
.PHONY: help venv lint fmt test build dev-up dev-down dev-logs test-netns
|
||||
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:' $(MAKEFILE_LIST) | sed 's/:.*//' | sort -u
|
||||
|
||||
venv:
|
||||
$(PY) -m venv $(VENV)
|
||||
$(BIN)/pip install -U pip
|
||||
$(BIN)/pip install -r requirements-dev.txt
|
||||
|
||||
lint:
|
||||
$(BIN)/ruff check backend
|
||||
$(BIN)/mypy backend
|
||||
|
||||
fmt:
|
||||
$(BIN)/ruff format backend
|
||||
$(BIN)/ruff check --fix backend
|
||||
|
||||
test:
|
||||
$(BIN)/pytest
|
||||
|
||||
build:
|
||||
@test -f deploy/Dockerfile || { echo 'deploy/Dockerfile ещё нет — задача #18'; exit 1; }
|
||||
docker build -f deploy/Dockerfile -t $(IMAGE) .
|
||||
|
||||
dev-up:
|
||||
@test -f $(COMPOSE_DEV) || { echo '$(COMPOSE_DEV) ещё нет — задача #21'; exit 1; }
|
||||
docker compose -f $(COMPOSE_DEV) up -d --build
|
||||
|
||||
dev-down:
|
||||
docker compose -f $(COMPOSE_DEV) down
|
||||
|
||||
dev-logs:
|
||||
docker compose -f $(COMPOSE_DEV) logs -f --tail=100
|
||||
|
||||
test-netns:
|
||||
@test -d tests/netns || { echo 'tests/netns ещё нет — задача #21'; exit 1; }
|
||||
bash tests/netns/run.sh
|
||||
@@ -1,40 +1,64 @@
|
||||
# awg-dash
|
||||
|
||||
# awg-dash 🚀
|
||||
|
||||
**awg-dash** — это легковесная и удобная веб-панель (Dashboard) для визуального управления сервером **Amnezia WireGuard (AWG)**. Она позволяет создавать, удалять и просматривать конфигурации пользователей в один клик через современный веб-интерфейс, защищенный от блокировок.
|
||||
|
||||
[](https://opensource.org)
|
||||
[](https://github.com)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Особенности / Features
|
||||
|
||||
- 🛠️ **Полная поддержка Amnezia WireGuard:** Тонкая настройка параметров обфускации (`Jc`, `Jmin`, `Jmax`, `S1`, `S2`, `H1`, `H2`, `H3`, `H4`) прямо из панели.
|
||||
- 👥 **Управление пользователями:** Создание, удаление, временная заморозка и управление лимитами пиров (клиентов).
|
||||
- 📊 **Мониторинг в реальном времени:** Графики потребления трафика, статус подключения клиентов и общая нагрузка на сервер.
|
||||
- 📱 **QR-коды и конфиги:** Мгновенная генерация QR-кодов и скачивание `.conf` файлов для импорта в официальное приложение Amnezia.
|
||||
- 🔒 **Безопасность:** Авторизация по паролю/токену, поддержка сессий и возможность работы за обратным прокси (Nginx, Caddy).
|
||||
- 🐳 **Docker-first:** Быстрый запуск одной командой через Docker Compose.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Технологический стек / Tech Stack
|
||||
|
||||
- **Backend:** Node.js (Express) / Python (FastAPI) / Go *(выберите нужное)*
|
||||
- **Frontend:** Vue.js / React / Простой HTML+JS *(выберите нужное)*
|
||||
- **База данных:** SQLite / JSON-file (минималистично, без тяжелых БД)
|
||||
- **Ядро:** Amnezia WireGuard (модифицированная утилита `wg`)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Быстрый старт / Quick Start
|
||||
|
||||
### Способ 1: Через Docker Compose (Рекомендуемый)
|
||||
|
||||
1. Клонируйте репозиторий:
|
||||
```bash
|
||||
git clone https://github.com
|
||||
cd awg-dash
|
||||
```
|
||||
VPN-шлюз **AmneziaWG** с интеллектуальной маршрутизацией: один Docker-образ = туннель + policy routing + split-tunnel (GeoIP / DNS) + REST API v1 + Web UI (тёмная тема).
|
||||
|
||||
- Эпик и декомпозиция: Gitea issue **#15** (`embeddevio/awg-dash`).
|
||||
- Исполнение: Hermes Kanban, борда **`awg-dash`** (`hermes kanban --board awg-dash list`).
|
||||
- PoC-референс: `embeddevio/amneziawg-docker-gateway` (bash + iptables, `docs/technical-specification.md`).
|
||||
|
||||
## Архитектура
|
||||
|
||||
Два процесса в одном образе:
|
||||
|
||||
| Процесс | Пользователь | Ответственность |
|
||||
|---|---|---|
|
||||
| `awg-agent` | root | awg/wg-интерфейс, iptables/ipset, dnsmasq, policy routing, метрики |
|
||||
| `awg-api` | непривилегированный | FastAPI: REST API v1, WebSocket, статика UI |
|
||||
|
||||
Обмен — unix-socket `/run/awg/agent.sock`; состояние — SQLite в `/data`, конфиги `.conf` в `/config`.
|
||||
Принятые решения зафиксированы в `docs/adr/`.
|
||||
|
||||
## Быстрый старт (после релиза 1.0)
|
||||
|
||||
```bash
|
||||
docker run -dit --name=vpn-gateway \
|
||||
--network=host \
|
||||
--device=/dev/net/tun:/dev/net/tun \
|
||||
--cap-add=NET_ADMIN --cap-add=SYS_MODULE \
|
||||
--restart always \
|
||||
-v /srv/awg/config:/config -v /srv/awg/data:/data \
|
||||
registry.example.com/vpn-gateway:latest
|
||||
```
|
||||
|
||||
Web UI: `http://<host>:8080`. До релиза образ собирается локально: `make build`.
|
||||
|
||||
## Разработка
|
||||
|
||||
```bash
|
||||
make venv # локальное окружение (.venv)
|
||||
make lint # ruff + mypy
|
||||
make test # pytest + coverage (порог 70%)
|
||||
make build # docker build образа (задача #18)
|
||||
make dev-up # dev-стенд (задача #21)
|
||||
```
|
||||
|
||||
Сетевые тесты выполняются в netns-стенде (`tests/netns/`), без реального VPN-провайдера.
|
||||
|
||||
## Структура репозитория
|
||||
|
||||
| Путь | Назначение |
|
||||
|---|---|
|
||||
| `backend/awg_agent/` | привилегированный агент: туннель, маршрутизация, GeoIP, DNS, метрики |
|
||||
| `backend/awg_api/` | FastAPI-приложение (REST + WS + статика UI) |
|
||||
| `backend/tests/` | unit/integration-тесты (pytest) |
|
||||
| `ui/` | React + TypeScript + Tailwind + shadcn-ui |
|
||||
| `deploy/` | Dockerfile, compose-профили, entrypoint, Grafana-дашборд |
|
||||
| `docs/adr/` | архитектурные решения |
|
||||
| `tests/netns/` | сетевой тестовый стенд |
|
||||
|
||||
## Процесс разработки
|
||||
|
||||
1 задача = 1 issue = 1 ветка `feat/<issue>-slug` = 1 PR.
|
||||
|
||||
Гейты мержа: CI green, coverage нового кода ≥ 70 %, security scan low/none, ревью владельца роли.
|
||||
Merge в `main` и деплой в prod — только TechLead после гейтов. Все операции с репозиторием — через Gitea MCP.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# awg_agent — привилегированный агент
|
||||
|
||||
Запускается как root в контейнере: всё, что требует NET_ADMIN и доступа к ядру.
|
||||
|
||||
| Модуль | Ответственность | Задача |
|
||||
|---|---|---|
|
||||
| `vpn/` | парсинг/валидация `.conf`, up/down/restart туннеля, `awg show` → статус | #22 |
|
||||
| `watchdog/` | контроль handshake, авто-восстановление, fail-closed | #23 |
|
||||
| `netfilter/` | iptables/ipset, policy routing, atomic swap, MSS clamping, IPv6 | #24 |
|
||||
| `geoip/` | RIPE Stat, кэш-снапшот, расписание, fallback | #25 |
|
||||
| `dns/` | dnsmasq, домены → ipset, защита от DNS-утечки | #26 |
|
||||
| `routing/` | режимы (GeoIP / DNS / All-VPN / Direct-only), reconcile, исключения | #27 |
|
||||
| `metrics/` | RX/TX, direct vs VPN, история, агрегаты | #28 |
|
||||
| `devices/` | активные устройства LAN и их политики | #29 |
|
||||
|
||||
Интерфейс для `awg_api` — unix-socket `/run/awg/agent.sock` (см. ADR-002).
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Привилегированный агент awg-dash: туннель, маршрутизация, GeoIP, DNS, метрики."""
|
||||
|
||||
__version__ = "0.0.1"
|
||||
|
||||
__all__ = ["__version__"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# awg_api — control plane
|
||||
|
||||
Непривилегированный процесс: FastAPI + REST API v1 + WebSocket + отдача статики UI.
|
||||
|
||||
| Модуль | Ответственность | Задача |
|
||||
|---|---|---|
|
||||
| `app.py` | lifespan, конфиг, логирование, error model, статика | #31 |
|
||||
| `routers/` | эндпоинты по OpenAPI (status, config, routing, devices, logs, auth) | #32 |
|
||||
| `ws/` | `/api/v1/metrics/stream` | #33 |
|
||||
| `security/` | токен, CSRF, rate limit, маскировка секретов | #34 |
|
||||
| `store/` | SQLite (WAL), миграции, audit log, apply/rollback | #20 |
|
||||
|
||||
Контракт — `docs/openapi.yaml` (задача #19), TS-клиент генерируется в `ui/src/api/generated/`.
|
||||
@@ -0,0 +1,5 @@
|
||||
"""FastAPI-приложение awg-dash: REST API v1, WebSocket, статика UI."""
|
||||
|
||||
__version__ = "0.0.1"
|
||||
|
||||
__all__ = ["__version__"]
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Скелетные тесты: пакеты импортируются, версия объявлена."""
|
||||
|
||||
import awg_agent
|
||||
import awg_api
|
||||
|
||||
|
||||
def test_versions_match() -> None:
|
||||
assert awg_agent.__version__ == awg_api.__version__
|
||||
|
||||
|
||||
def test_public_api_declared() -> None:
|
||||
assert "__version__" in awg_agent.__all__
|
||||
assert "__version__" in awg_api.__all__
|
||||
@@ -0,0 +1,12 @@
|
||||
# deploy — сборка и развёртывание
|
||||
|
||||
| Артефакт | Назначение | Задача |
|
||||
|---|---|---|
|
||||
| `Dockerfile` | multi-stage: amneziawg-go + awg-tools → runtime alpine | #18 |
|
||||
| `entrypoint.sh` | супервизор: `awg-agent` (root) + `awg-api` | #18 |
|
||||
| `docker-compose.dev.yml` | dev-стенд: порт 8080, тома `/config` `/data`, healthcheck | #21 |
|
||||
| `docker-compose.test.yml` | стенд для тестов | #21 |
|
||||
| `grafana/` | дашборд JSON (опциональный профиль monitoring) | #46 |
|
||||
| `awg0.conf.example` | пример клиентского конфига | #22 |
|
||||
|
||||
Запуск по ТЗ (после релиза): `--network=host`, `--device=/dev/net/tun`, `--cap-add=NET_ADMIN --cap-add=SYS_MODULE`, `--restart always`.
|
||||
@@ -0,0 +1,21 @@
|
||||
# NNN. Название решения
|
||||
|
||||
- **Статус:** предложено | принято | устарело
|
||||
- **Дата:** YYYY-MM-DD
|
||||
- **Контекст/issue:** #N
|
||||
|
||||
## Контекст
|
||||
|
||||
Что вынуждает принять решение и какие есть ограничения (хостовое окружение, привилегии, один шлюз, целевой UX).
|
||||
|
||||
## Решение
|
||||
|
||||
Что именно решили, одним абзацем.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
Что рассмотрели и почему отклонили.
|
||||
|
||||
## Последствия
|
||||
|
||||
Положительные и отрицательные следствия; что решение накладывает на задачи эпика.
|
||||
@@ -0,0 +1,27 @@
|
||||
# 001. Backend — Python 3.12 + FastAPI
|
||||
|
||||
- **Статус:** принято
|
||||
- **Контекст/issue:** #15, #31
|
||||
|
||||
## Контекст
|
||||
|
||||
Нужен control plane: REST API, WebSocket, планировщик, парсинг конфигов, работа с SQLite.
|
||||
Низкоуровневая работа сети и шифрования выполняется не нашим кодом, а бинарями `awg` и `amneziawg-go`,
|
||||
трафик маршрутизирует ядро.
|
||||
|
||||
## Решение
|
||||
|
||||
Control plane — **Python 3.12 + FastAPI (async)**. Go остаётся только там, где он уже есть:
|
||||
`amneziawg-go` и `amneziawg-tools` собираются в multi-stage Dockerfile (ADR-002).
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Go целиком** — быстрее на сетевом I/O, но маршрутизацией занимается ядро, а не процесс;
|
||||
выигрыш не оправдывает рост стоимости контракта с UI (схемы, генерация клиента, миграции, веб-фреймворк).
|
||||
- **Node.js** — преимуществ перед Python на нагрузке в единичного пользователя нет.
|
||||
|
||||
## Последствия
|
||||
|
||||
- OpenAPI-first: FastAPI отдаёт схему, что прямо соответствует задаче #19.
|
||||
- В образе появляется Python runtime (плюс 40–60 MB к размеру).
|
||||
- Дисциплина async: внешние команды — только `asyncio.create_subprocess_exec`, блокирующего кода в event loop нет.
|
||||
@@ -0,0 +1,32 @@
|
||||
# 002. Модульный монолит в одном образе, два процесса
|
||||
|
||||
- **Статус:** принято
|
||||
- **Контекст/issue:** #15, #18
|
||||
|
||||
## Контекст
|
||||
|
||||
ТЗ требует единый Docker-образ с минимальными параметрами запуска. Часть операций требует root
|
||||
(интерфейс awg, iptables/ipset, dnsmasq на порту 53), а веб-часть должна быть скомпрометирована безопасно — без root.
|
||||
|
||||
## Решение
|
||||
|
||||
Один образ, два процесса под супервизором `deploy/entrypoint.sh`:
|
||||
|
||||
| Процесс | Права | Ответственность |
|
||||
|---|---|---|
|
||||
| `awg-agent` | root, NET_ADMIN | туннель, netfilter, routing, dnsmasq, метрики |
|
||||
| `awg-api` | непривилегированный | FastAPI, REST/WS, статика UI |
|
||||
|
||||
Обмен — unix-socket `/run/awg/agent.sock`, общее состояние — SQLite в `/data` и конфиги в `/config`.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Микросервисы** (отдельные контейнеры agent/api/dns) — общая сеть, координация и дублирование
|
||||
привилегий; выгода масштабирования на одном шлюзе отсутствует.
|
||||
- **Один процесс под root** — проще, но даёт веб-слою полные права; неприемлемо.
|
||||
|
||||
## Последствия
|
||||
|
||||
- Супервизор обязан пробрасывать сигналы и коды выхода, иначе Docker не заметит падения процесса.
|
||||
- Права на unix-socket входят в security-проверки (#44).
|
||||
- Перезапуск API не разрывает туннель.
|
||||
@@ -0,0 +1,28 @@
|
||||
# 003. Декларативный reconcile вместо императивных скриптов
|
||||
|
||||
- **Статус:** принято
|
||||
- **Контекст/issue:** #15, #24, #27
|
||||
|
||||
## Контекст
|
||||
|
||||
PoC (`entrypoint.sh`) применяет правила императивно и только при старте: изменения не откатываются,
|
||||
GeoIP не обновляется без рестарта, частичная ошибка оставляет систему в неопределённом состоянии.
|
||||
|
||||
## Решение
|
||||
|
||||
Единый reconcile-цикл: desired state (SQLite) -> diff -> идемпотентное применение -> проверка ->
|
||||
rollback при ошибке. Операции netfilter идемпотентны (проверка `-C` перед `-A`), замена больших
|
||||
наборов атомарна (`ipset create tmp`, `swap`, `destroy`), применение конфига транзакционно
|
||||
(validate -> stage -> apply -> rollback).
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Скрипты по событию** (как в PoC) — быстро написать, но невозможно гарантировать сходимость после сбоя.
|
||||
- **Готовый IaC-инструмент** — избыточен, требует внешнего рантайма в образе и не покрывает ipset/dnsmasq.
|
||||
|
||||
## Последствия
|
||||
|
||||
- Старт контейнера = reconcile с нуля: состояние всегда восстановимо.
|
||||
- Нужны тесты на идемпотентность (повторный apply не создаёт дублей) — #41.
|
||||
- Сложнее скрипта: оправдано требованиями FR-2 (смена режимов и правил без рестарта).
|
||||
- Diff reconcile логируется и отдаётся в UI (`/logs`) и audit log.
|
||||
@@ -0,0 +1,28 @@
|
||||
# 004. GeoIP: RIPE Stat + локальный кэш + абстракция провайдера
|
||||
|
||||
- **Статус:** принято
|
||||
- **Контекст/issue:** #15, #25
|
||||
|
||||
## Контекст
|
||||
|
||||
Для split-tunnel нужны списки префиксов страны (в PoC — около 11 400 записей RIPE).
|
||||
Варианты: RIPE Stat (бесплатно), ip2location и MaxMind (точнее, но лицензия и платный доступ).
|
||||
|
||||
## Решение
|
||||
|
||||
Основной источник — **RIPE Stat** (`country-resource-list`, IPv4 и IPv6): бесплатно и уже проверено в PoC.
|
||||
Обязательные элементы: локальный кэш-снапшот в `/data/geoip/`, расписание обновления, ручное
|
||||
обновление, явный статус источника (API или кэш с датой). Внутри — интерфейс `GeoProvider`, чтобы
|
||||
добавить ip2location/MaxMind без переделки маршрутизации.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **MaxMind GeoLite2** — точнее по ASN, но требует регистрации и обновления лицензии; оставляем как опцию.
|
||||
- **Собственный список в репозитории** — устаревает и не покрывает смену маршрутной политики.
|
||||
|
||||
## Последствия
|
||||
|
||||
- Старт не зависит от внешней сети: при недоступности API работаем на кэше (#25, тест #41).
|
||||
- Fallback не должен молча менять политику split-tunnel: неявные переходы в режим «всё в VPN» или
|
||||
«всё напрямую» запрещены, поведение задаётся явно (#23).
|
||||
- Загрузка 11k+ префиксов — измеримая операция; время старта и swap — часть AC #24 и #25.
|
||||
@@ -0,0 +1,27 @@
|
||||
# 005. DNS: встроенный dnsmasq и перехват порта 53
|
||||
|
||||
- **Статус:** принято
|
||||
- **Контекст/issue:** #15, #26
|
||||
|
||||
## Контекст
|
||||
|
||||
DNS-based split-tunnel требует знать, какие IP принадлежат доменам из списка. Если клиенты сами
|
||||
резолвят домены (свой DNS, DoH, другой upstream), шлюз не узнаёт об адресах и правило не срабатывает.
|
||||
|
||||
## Решение
|
||||
|
||||
В образе поднимается **встроенный dnsmasq** (процесс агента):
|
||||
- домены из списка → `ipset` (через `--ipset`), трафик на эти адреса идёт в туннель;
|
||||
- перехват порта 53 от LAN-клиентов шлюзом — устраняет DNS-утечку и лечит клиентов с ручным DNS;
|
||||
- upstream-форвардинг и кэш по TTL; опция «внешний DNS» остаётся для нестандартных схем.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Отдельный DNS-контейнер** — лишняя зависимость и точка отказа, ломает требование единого образа.
|
||||
- **Только DNS клиентов** — не работает без ручной настройки каждого устройства (противоречит FR-2).
|
||||
|
||||
## Последствия
|
||||
|
||||
- Порт 53 занят контейнером: в README/runbook нужно предупреждение (конфликт с локальным резольвером хоста).
|
||||
- Обязательно тестировать домены с CNAME и динамическими IP (#26, #41).
|
||||
- Список доменов и его размеры ограничиваются явно (валидация в API, #30).
|
||||
@@ -0,0 +1,29 @@
|
||||
# 006. Состояние: SQLite (WAL) + /config + /data
|
||||
|
||||
- **Статус:** принято
|
||||
- **Контекст/issue:** #15, #20
|
||||
|
||||
## Контекст
|
||||
|
||||
Нужно хранить правила маршрутизации, исключения, режим, расписание GeoIP, метаданные конфигов,
|
||||
устройства, audit log и историю метрик. Профиль нагрузки: единичный пользователь, десятки записей в секунду,
|
||||
важна надёжность и отсутствие внешних зависимостей.
|
||||
|
||||
## Решение
|
||||
|
||||
- **SQLite в режиме WAL** в `/data/awg-dash.db` — всё структурированное состояние и audit log;
|
||||
- `/config` — файлы `.conf`, права 0600, секреты в подкаталоге с ограниченным доступом;
|
||||
- `/data` — кэш GeoIP и история метрик с retention.
|
||||
|
||||
Оба каталога объявляются томами образа: без них конфигурация не переживёт пересоздание контейнера.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Postgres/Redis** — лишний сервис в образе для единичного пользователя.
|
||||
- **Только файлы JSON** — нет транзакций, аудита и атомарного apply/rollback.
|
||||
|
||||
## Последствия
|
||||
|
||||
- Миграции схемы обязательны с первого дня (#20) и проверяются при старте.
|
||||
- Backup/restore = копирование `/data` и `/config` — отражается в runbook (#45).
|
||||
- История метрик растёт: нужен retention и периодическая очистка (#28).
|
||||
@@ -0,0 +1,26 @@
|
||||
# 007. Web-сервер: встроенный uvicorn, без nginx
|
||||
|
||||
- **Статус:** принято
|
||||
- **Контекст/issue:** #15, #31
|
||||
|
||||
## Контекст
|
||||
|
||||
UI — SPA, API — REST и WebSocket, всё в одном контейнере. Нагрузка: LAN, единичный пользователь.
|
||||
Установка должна быть в одну команду `docker run` без дополнительных контейнеров.
|
||||
|
||||
## Решение
|
||||
|
||||
Один uvicorn-процесс `awg-api` отдаёт и API, и собранную статику UI, и WebSocket. Сборка UI выполняется
|
||||
на этапе билда образа и попадает в `ui/dist`. Закладывается совместимость с внешним reverse-proxy:
|
||||
поддержка base-path и заголовков `X-Forwarded-*`, без обязательной настройки хоста.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **nginx в образе** — плюс к размеру и к конфигу, преимуществ при такой нагрузке нет.
|
||||
- **Отдельный контейнер для UI** — нарушает требование единого образа.
|
||||
|
||||
## Последствия
|
||||
|
||||
- Заголовки кэширования статики и `index.html` настраиваются в FastAPI (иначе UI кэшируется после обновления).
|
||||
- WebSocket идёт через тот же порт: учитывается при настройке reverse-proxy в runbook (#45).
|
||||
- Docker HEALTHCHECK опирается на `/api/v1/health` того же процесса (#31).
|
||||
@@ -0,0 +1,31 @@
|
||||
# 008. Безопасность API/UI: токен, CSRF, rate limit, маскировка секретов
|
||||
|
||||
- **Статус:** принято
|
||||
- **Контекст/issue:** #15, #34
|
||||
|
||||
## Контекст
|
||||
|
||||
Приложение управляет VPN-туннелем и принимает приватные ключи AmneziaWG. Оно доступно из LAN,
|
||||
а в перспективе может быть выставлено через reverse-proxy. В ТЗ этот слой не описан, но без него
|
||||
загрузка `.conf` через веб-интерфейс превращается в дыру.
|
||||
|
||||
## Решение
|
||||
|
||||
- API — токен доступа в заголовке (Bearer), токен только в виде хеша в БД;
|
||||
- UI — сессия + CSRF-токен на мутирующие запросы;
|
||||
- приватные ключи (`PrivateKey`, `PresharedKey`) — `writeOnly` в OpenAPI, маска в UI и логах,
|
||||
файлы 0600 в отдельном каталоге;
|
||||
- rate limit и lockout на `/auth`, ограничение допустимых источников, bind по умолчанию на LAN;
|
||||
- audit log всех изменений конфигурации.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Без аутентификации, доступ только по LAN** — не защищает от заражённого устройства в сети и от
|
||||
ошибочной публикации порта.
|
||||
- **OIDC-провайдер** — не для локального однопользовательского приложения.
|
||||
|
||||
## Последствия
|
||||
|
||||
- Начальный токен генерируется при первом старте и выводится в лог один раз — это шаг в runbook (#45).
|
||||
- Маскировка секретов проверяется автотестами (#41) и security-прогоном (#44).
|
||||
- UI обязан корректно вести себя при 401 (переход на форму входа) — часть AC #35.
|
||||
@@ -0,0 +1,16 @@
|
||||
# ADR — архитектурные решения awg-dash
|
||||
|
||||
Один файл = одно решение. Формат: контекст -> решение -> последствия.
|
||||
|
||||
| ADR | Решение |
|
||||
|---|---|
|
||||
| [001](001-backend-python-fastapi.md) | Backend — Python 3.12 + FastAPI |
|
||||
| [002](002-single-image-modular-monolith.md) | Модульный монолит в одном образе, 2 процесса |
|
||||
| [003](003-reconcile-desired-state.md) | Декларативный reconcile вместо императивных скриптов |
|
||||
| [004](004-geoip-ripe-provider.md) | GeoIP: RIPE Stat + локальный кэш + абстракция провайдера |
|
||||
| [005](005-dns-dnsmasq.md) | DNS: встроенный dnsmasq + перехват порта 53 |
|
||||
| [006](006-state-sqlite.md) | Состояние: SQLite (WAL) + /config + /data |
|
||||
| [007](007-embedded-web-server.md) | Web-сервер: встроенный uvicorn, без nginx |
|
||||
| [008](008-api-ui-security.md) | Безопасность API/UI: токен, CSRF, rate limit, маскировка секретов |
|
||||
|
||||
Новые решения добавляются со следующим номером и ссылкой на issue, для которого решение принято.
|
||||
@@ -1,158 +0,0 @@
|
||||
# awg-dash: Руководство по работе с командой разработки
|
||||
|
||||
> Команда состоит из 5 агентов Hermes с изолированными профилями.
|
||||
> Репозиторий: `embeddevio/awg-dash` (Gitea)
|
||||
> MCP сервер: `http://127.0.0.1:8100/mcp`
|
||||
|
||||
---
|
||||
|
||||
## 1. Роли
|
||||
|
||||
| Роль | Профиль | Запуск | Ответственность |
|
||||
|------|---------|--------|-----------------|
|
||||
| **TechLead** | `techlead-agent` | `hermes --profile techlead-agent chat` | Архитектура, декомпозиция эпиков, контроль качества, мерж, релизы |
|
||||
| **Backend** | `backend-engineer` | `hermes --profile backend-engineer chat` | REST API, бизнес-логика, модели, unit-тесты |
|
||||
| **Frontend** | `frontend-engineer` | `hermes --profile frontend-engineer chat` | UI/UX, дашборды, e2e-тесты |
|
||||
| **QA** | `qa-engineer` | `hermes --profile qa-engineer chat` | Тест-планы, автотесты, регресс, security |
|
||||
| **DevOps** | `devops` | `hermes --profile devops chat` | CI/CD, Docker, деплой, мониторинг |
|
||||
|
||||
Маршрутизация: при создании issue с лейблом `backend`/`frontend`/`qa`/`devops`/`epic` Hermes направляет задачу соответствующему профилю автоматически.
|
||||
|
||||
---
|
||||
|
||||
## 2. Быстрый старт
|
||||
|
||||
```bash
|
||||
# Запустить TechLead агента (отвечает за архитектуру и координацию)
|
||||
tmux new-session -d -s techlead 'hermes --profile techlead-agent chat'
|
||||
|
||||
# Подключиться к сессии
|
||||
tmux attach -t techlead
|
||||
|
||||
# Когда нужен бэкенд — запустить в другом окне
|
||||
tmux new-session -d -s backend 'hermes --profile backend-engineer chat'
|
||||
|
||||
# Остальные профили по мере необходимости
|
||||
tmux new-session -d -s frontend 'hermes --profile frontend-engineer chat'
|
||||
tmux new-session -d -s qa 'hermes --profile qa-engineer chat'
|
||||
tmux new-session -d -s devops 'hermes --profile devops chat'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Процесс разработки
|
||||
|
||||
```
|
||||
TechLead: Эпик → Subtasks with AC
|
||||
├── Backend: API + тесты → PR
|
||||
├── Frontend: UI компоненты → PR
|
||||
├── QA: Тест-план → e2e → Review PRs
|
||||
├── DevOps: CI/CD + деплой в staging
|
||||
└── TechLead: Code Review → Merge (если все gates зелёные)
|
||||
```
|
||||
|
||||
### 3.1 Эпик (создаёт TechLead)
|
||||
|
||||
1. Зайти в профиль TechLead: `hermes --profile techlead-agent chat`
|
||||
2. Дать задачу. Агент создаст issue с лейблом `epic`.
|
||||
3. В теле issue — GIVEN/WHEN/THEN acceptance criteria.
|
||||
4. Агент создаст задачи-потомки с лейблами ролей.
|
||||
|
||||
**ID лейблов (использовать числа, не строки):**
|
||||
|
||||
| ID | Лейбл |
|
||||
|----|-------|
|
||||
| 1 | `epic` |
|
||||
| 2 | `backend` |
|
||||
| 3 | `frontend` |
|
||||
| 4 | `devops` |
|
||||
| 5 | `qa` |
|
||||
| 6 | `blocker` |
|
||||
| 7 | `release` |
|
||||
| 8 | `hotfix` |
|
||||
|
||||
### 3.2 Разработка (Backend / Frontend)
|
||||
|
||||
1. Зайти в профиль (`hermes --profile backend-engineer chat`).
|
||||
2. Сообщить номер issue — агент сам прочитает задачу.
|
||||
3. Агент создаёт ветку `feat/N-slug`, пишет код, тесты, открывает PR.
|
||||
4. **Важно:** агент не сливает PR. Только TechLead мержит.
|
||||
|
||||
### 3.3 QA
|
||||
|
||||
1. Зайти в профиль QA: `hermes --profile qa-engineer chat`
|
||||
2. Попросить проверить PR #N. Агент прочитает diff, напишет тест-план, выполнит тесты.
|
||||
3. Результат — комментарий на PR с pass/fail.
|
||||
|
||||
### 3.4 DevOps
|
||||
|
||||
1. Зайти в профиль DevOps: `hermes --profile devops chat`
|
||||
2. Сообщить о мерже — агент собирает образ, деплоит в staging.
|
||||
|
||||
### 3.5 Code Review и Merge (TechLead)
|
||||
|
||||
**Gate для мержа:**
|
||||
- [ ] CI зелёный (lint + тесты)
|
||||
- [ ] coverage ≥ 70%
|
||||
- [ ] Security scan — low/none
|
||||
- [ ] QA approve
|
||||
- [ ] Хотя бы 1 review от владельца роли
|
||||
|
||||
**Gate для релиза:**
|
||||
- [ ] Все задачи эпика закрыты
|
||||
- [ ] Regression pass
|
||||
- [ ] Smoke tests green
|
||||
- [ ] Документация обновлена
|
||||
|
||||
---
|
||||
|
||||
## 4. Labels reference
|
||||
|
||||
Можно установить лейбл через Gitea MCP:
|
||||
|
||||
```
|
||||
issue_write(method='add_labels', owner='embeddevio', repo='awg-dash',
|
||||
issue_number=N, labels=[2]) # backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Создание задачи вручную
|
||||
|
||||
```python
|
||||
# Через MCP инструмент
|
||||
mcp__gitea__issue_write(
|
||||
method='create',
|
||||
owner='embeddevio',
|
||||
repo='awg-dash',
|
||||
title='Frontend: status indicator',
|
||||
body='**AC:** GIVEN tunnel active WHEN page loads THEN green indicator shown',
|
||||
labels=[3] # frontend
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Правила для всех
|
||||
|
||||
- Все операции с репозиторием — только через MCP (не curl, не shell git).
|
||||
- Никто не пушит в `main` напрямую.
|
||||
- Никто не сливает свой PR (кроме TechLead после прохождения gates).
|
||||
- PR #1 (TechLead bootstrap) — не мержить, это шаблоны и конфиги.
|
||||
- Если задача кажется непонятной — агент откроет issue с уточняющими вопросами.
|
||||
- Критическая проблема → лейбл `blocker` → TechLead подключается.
|
||||
|
||||
---
|
||||
|
||||
## 7. Переключение между профилями
|
||||
|
||||
```bash
|
||||
# Из текущей сессии можно выйти (/exit) и зайти в другой профиль
|
||||
# Или через отдельный tmux:
|
||||
|
||||
tmux new-session -d -s backend 'hermes --profile backend-engineer chat'
|
||||
tmux attach -t backend
|
||||
|
||||
# Просмотр запущенных сессий агентов
|
||||
tmux ls
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
[project]
|
||||
name = "awg-dash"
|
||||
version = "0.0.1"
|
||||
description = "AmneziaWG gateway with intelligent routing: REST API + Web UI + split-tunnel"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py312"
|
||||
extend-exclude = ["ui", ".venv", "backend/tests"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "I", "N", "UP", "B", "ASYNC", "S", "RUF"]
|
||||
ignore = ["S101"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"backend/tests/*" = ["S", "D"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.12"
|
||||
mypy_path = "backend"
|
||||
files = ["backend"]
|
||||
strict = true
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["backend/tests"]
|
||||
pythonpath = ["backend"]
|
||||
addopts = "-q --cov=awg_agent --cov=awg_api --cov-report=term-missing --cov-fail-under=70"
|
||||
asyncio_mode = "auto"
|
||||
@@ -0,0 +1,8 @@
|
||||
-r requirements.txt
|
||||
|
||||
ruff>=0.6.9
|
||||
mypy>=1.11
|
||||
pytest>=8.3
|
||||
pytest-cov>=5.0
|
||||
pytest-asyncio>=0.24
|
||||
pre-commit>=3.8
|
||||
@@ -0,0 +1,12 @@
|
||||
# Runtime (контроль-плоскость)
|
||||
fastapi>=0.115
|
||||
uvicorn[standard]>=0.32
|
||||
pydantic>=2.9
|
||||
pydantic-settings>=2.5
|
||||
aiosqlite>=0.20
|
||||
httpx>=0.27
|
||||
structlog>=24.4
|
||||
prometheus-client>=0.21
|
||||
|
||||
# Сетевой слой вызывается через бинари (awg, iptables, ipset, dnsmasq),
|
||||
# поэтому python-зависимостей для сети не требуется.
|
||||
@@ -0,0 +1,13 @@
|
||||
# tests/netns — сетевой тестовый стенд
|
||||
|
||||
Задача **#21**, используется тестами #41 и #43.
|
||||
|
||||
Цель: проверять split-tunnel без реального VPN-провайдера и без доступа к продакшн-сети.
|
||||
|
||||
План харнесса:
|
||||
1. Создать netns: `client`, `gw` (шлюз с awg-интерфейсом), `uplink` (имитация ISP).
|
||||
2. Поднять мок WireGuard-endpoint (userspace) вместо реального сервера.
|
||||
3. Запустить шлюз в netns с теми же правилами, что в контейнере.
|
||||
4. Проверить, куда ушёл пакет: интерфейс/метка/счётчики ipset и iptables.
|
||||
|
||||
Точки входа: `tests/netns/run.sh` (вызывается через `make test-netns`).
|
||||
@@ -0,0 +1,14 @@
|
||||
# ui — Web UI (React + TypeScript + Tailwind + shadcn-ui)
|
||||
|
||||
Тёмная тема, 6 страниц из ТЗ. Каркас — задача **#35**, страницы — #36–#39, E2E — #42.
|
||||
|
||||
| Маршрут | Страница | Задача |
|
||||
|---|---|---|
|
||||
| `/` | Dashboard: статус, трафик, handshake, аптайм | #36 |
|
||||
| `/routing` | редактор правил, ipset, домены, режимы, DnD+diff | #37 |
|
||||
| `/config` | загрузка `.conf`, валидация, маскировка ключей | #38 |
|
||||
| `/devices` | устройства и политики | #39 |
|
||||
| `/logs` | журнал, счётчики RETURN vs MARK | #39 |
|
||||
| `/settings` | GeoIP-расписание, обновления, интеграции | #39 |
|
||||
|
||||
API-клиент генерируется из OpenAPI (#19) в `src/api/generated/`. Взаимодействие: REST + WebSocket.
|
||||
Reference in New Issue
Block a user