feat: добавлены правила маршрутизации по устройствам и управление ими через API
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s

Refs: None
This commit is contained in:
2026-05-09 09:12:03 +03:00
parent b3fad00f80
commit 4bb8507e3f
7 changed files with 506 additions and 42 deletions

328
README.md
View File

@@ -1,57 +1,301 @@
# VPN Proxy Gateway
Контейнер запускается в `network_mode: host`, применяет TProxy-правила на хосте и
запускает `sing-box` как прозрачный gateway для устройств в локальной сети.
Самохостируемый прозрачный VPN-шлюз на базе [sing-box](https://sing-box.sagernet.org/).
Разворачивается в Docker (LXC, VPS), перехватывает трафик всей локальной сети через iptables TProxy — без клиентов на устройствах.
## Возможности
Веб-интерфейс на React даёт полное управление: подписки, выбор сервера, кастомные правила маршрутизации, просмотр трафика в реальном времени.
- Web UI на Vite + React, всё на русском.
- Один Node control-server без отдельного backend framework.
- Парсинг subscription URL: JSON config, base64 список, plain-text VLESS links.
- Подписка маскируется в UI после загрузки, кнопка «Забыть подписку» — стирает
кэш, останавливает sing-box и удаляет конфиг.
- Управление жизненным циклом sing-box из UI: остановить, перезапустить, сбросить
конфиг, посмотреть сгенерированный `config.json` (read-only).
- Live-логи sing-box через SSE (фильтр по уровню, пауза, очистка).
- Routing lists с автосохранением, drag-n-drop порядка (first match wins),
валидацией CIDR/портов/доменов и шаблонами (LoL, Discord, Telegram, YouTube,
Steam, реклама).
- Генерация sing-box config с safety private-direct, кастомными правилами и
RU geosite/geoip direct.
- Docker entrypoint с idempotent TProxy setup/cleanup.
- Healthcheck в compose: `curl http://127.0.0.1:${PORT}/api/state`.
---
## Архитектура
```
Клиент (ПК/телефон)
│ TCP/UDP трафик
[Роутер] → маршрут по умолчанию → LXC/VPS (gateway)
iptables mangle PREROUTING → цепочка VPN_PROXY_TPROXY
├─ ipset vpn_direct_bypass (dst IP) → RETURN ← bypass-кэш ядра
├─ приватные CIDR (RFC1918, ...) → RETURN
└─ TCP/UDP → TPROXY :7895
sing-box (tproxy inbound :7895)
роутинг по правилам
┌──────────┼──────────┐
▼ ▼ ▼
direct VPN out block
```
**Node.js API-сервер** (`src/server/index.js`) работает внутри того же контейнера:
управляет процессом sing-box, парсит его логи, экспортирует REST API и SSE-стримы для веб-интерфейса.
---
## Стек
| Слой | Технология |
| ---------------- | ------------------------------------------------------------- |
| Контейнер | Docker, `network_mode: host`, `CAP_NET_ADMIN` + `CAP_NET_RAW` |
| Перехват трафика | iptables TProxy + iproute2 policy routing |
| Bypass-кэш | ipset `hash:ip` с TTL |
| VPN-ядро | sing-box (VLESS/VLESS-Reality/VMess/Trojan/Hysteria2/SS) |
| API-сервер | Node.js 18, plain `http` (без фреймворков) |
| Веб-интерфейс | React 18 + Vite 7, SPA |
---
## Как работает прозрачное проксирование
### 1. TProxy и policy routing
При старте контейнера `entrypoint.sh` настраивает ядро:
```bash
# Policy routing: пакеты с меткой TPROXY_MARK уходят через loopback
ip rule add fwmark 1 table 100
ip route replace local 0.0.0.0/0 dev lo table 100
# Цепочка iptables (порядок правил — критичен)
iptables -t mangle -N VPN_PROXY_TPROXY
-m addrtype --dst-type LOCAL → RETURN # ответы самого sing-box
-m mark --mark 1 → RETURN # уже помеченные пакеты
-m set --match-set vpn_direct_bypass → RETURN # bypass-кэш (см. ниже)
-d 10.0.0.0/8, 192.168.0.0/16, ... → RETURN # приватные адреса
-p tcp → TPROXY :7895 mark 1
-p udp → TPROXY :7895 mark 1
iptables -t mangle -A PREROUTING -j VPN_PROXY_TPROXY
```
При остановке контейнера (`SIGTERM`) все правила iptables удаляются идемпотентно.
ipset-кэш намеренно **не** очищается — записи истекают по TTL.
### 2. Маршрутизация внутри sing-box
Каждый пакет проходит правила в порядке приоритета — **первое совпадение побеждает**:
| Приоритет | Условие | Действие |
| --------- | ------------------------------------------- | ---------------------------------------- |
| 1 | `ip_is_private: true` | `direct` (защита LAN) |
| 2 | Правила по устройствам (source IP) | `direct` / `vpn` / `block` |
| 3 | Кастомные правила пользователя | `direct` / `vpn` / `block` |
| 4 | `rule_set: [geoip-ru, geosite-category-ru]` | `direct` (если `ROUTING_RU_DIRECT=true`) |
| 5 | Всё остальное (`final`) | выбранный VPN-outbound |
Конфиг генерируется динамически через `buildGatewayConfig()` из подписки + сохранённых правил. Перед применением выполняется `sing-box check`.
### 3. Bypass Mode (весь трафик напрямую)
Кнопка "Весь трафик напрямую" в дашборде. При активации `buildGatewayConfig()` вызывается с `{ bypassAll: true }` — в конфиге убираются все rule_set, `final: "direct"`. Удобно для диагностики или когда VPN не нужен.
---
## Direct Bypass Cache (ipset)
Оптимизация для прямого трафика: IP-адреса, которые sing-box уже отправил напрямую, кэшируются в ядре и больше не проходят через userspace.
**Цепочка событий:**
1. sing-box маршрутизирует соединение как `direct`, пишет в лог:
`[TCP] 192.168.1.5:54321 --> 203.0.113.10:443 outbound/direct[direct]`
2. Node.js парсит строку (regex `-->` + `outbound/`). Если `category === "direct"` и назначение — IPv4-адрес:
```bash
ipset add vpn_direct_bypass 203.0.113.10 timeout 3600 -exist
```
3. Следующий пакет к `203.0.113.10` обрабатывается iptables **до** передачи в sing-box:
```
-m set --match-set vpn_direct_bypass dst → RETURN
```
Пакет уходит напрямую на уровне ядра — нулевые накладные расходы userspace sing-box.
4. Запись истекает через TTL (по умолчанию 1 час).
```
DIRECT_BYPASS_SET=vpn_direct_bypass # имя ipset
DIRECT_BYPASS_TTL=3600 # TTL в секундах
```
---
## Кастомные правила маршрутизации
Управляются из вкладки **Правила**. Сохраняются в `custom-rules.json`.
Правила применяются в порядке отображения в UI — **first match wins**.
| Поле | Тип | Описание |
| ---------------- | ---------------------------- | ------------------------------------------- |
| `name` | string | Название правила |
| `enabled` | bool | Вкл/выкл |
| `outbound` | `direct` \| `vpn` \| `block` | Куда отправить трафик |
| `domains` | string[] | Точные домены (`example.com`) |
| `domainSuffixes` | string[] | Суффикс домена (`.example.com` + поддомены) |
| `domainKeywords` | string[] | Keyword в имени хоста |
| `ipCidrs` | string[] | IP-диапазоны CIDR |
| `ports` | string[] | Порты или диапазоны (`443`, `8000-9000`) |
| `networks` | `tcp` \| `udp` | Протокол |
| `ruleSets` | string[] | Ссылки на remote rule-set |
UI автоматически детектирует конфликты — когда правило полностью перекрывается предыдущим.
### Remote Rule Sets
В **Настройках** можно добавить произвольные rule-set:
```json
{ "tag": "gaming-servers", "url": "https://...", "format": "binary" }
```
sing-box скачивает их при старте, кэширует в `cache.db`. Ключ кэша — SHA-1 от URL.
---
## Подписки
Поддерживаемые форматы:
- **JSON-конфиг sing-box** — объект с полем `outbounds[]`
- **Base64-список VLESS-ссылок** — декодируется, каждая ссылка парсится
- **Прямые VLESS URI** (`vless://uuid@host:port?...#tag`)
После загрузки пользователь выбирает сервер → генерируется конфиг → `sing-box check` → перезапуск.
Подписка кэшируется в `subscription-cache.json` — при рестарте контейнера конфиг автоматически пересоздаётся из кэша без повторного скачивания.
---
## Просмотр трафика
Вкладка **Трафик** в разделе Логи. Данные приходят через SSE (`/api/traffic/stream`).
### Парсинг логов sing-box
Node.js читает stderr sing-box и извлекает трафик двумя шагами:
```
[router] match[2][my-rule] => outbound/direct[direct] ← имя правила
[TCP] 192.168.1.5:PORT --> example.com:443 outbound/vpn[tag] ← соединение
```
1. `[router]`-строка → имя правила сохраняется с TTL 500 мс
2. Следующая строка с `-->` подхватывает имя в поле `matchedRule`
3. Тип трафика: `direct` / `vpn` / `block` по outbound
4. Direct + IPv4 → добавление в ipset bypass-кэш
### Группировка и сортировка
`(category, host, port, matchedRule)` объединяются в группу с счётчиком:
- **По частоте** — самые частые наверху (по умолчанию)
- **По времени** — последние наверху
---
## Проверка маршрута
Вкладка **Проверка** позволяет узнать, по какому правилу пойдёт трафик к хосту/IP/порту — без реального подключения. Node.js (`routeMatcher.js`) симулирует ту же логику, что и sing-box:
1. private IP → direct
2. custom rules (first-match)
3. geoip-ru / geosite-category-ru → "вероятно direct" (без локальной БД точно неизвестно)
4. final → VPN
---
## Быстрый старт
```bash
cp .env.example .env
docker compose -f docker-compose.gateway.yml up -d --build
# Сборка фронтенда
npm install && npm run build
# Запуск контейнера
docker compose -f docker-compose.gateway.yml up -d
```
UI будет доступен на хосте по `http://<gateway-host>:3456`.
UI доступен на `http://<gateway-ip>:3456`.
На роутере указать шлюз по умолчанию (или нужные подсети) на IP контейнера.
---
## Переменные окружения
| Переменная | По умолчанию | Описание |
| ------------------- | -------------------- | -------------------------------------- |
| `PORT` | `3456` | Порт веб-интерфейса |
| `PROXY_PORT` | `8080` | HTTP/SOCKS mixed inbound |
| `TPROXY_PORT` | `7895` | TProxy inbound sing-box |
| `DATA_DIR` | `/var/lib/vpn-proxy` | Директория данных (volume) |
| `ROUTING_RU_DIRECT` | `true` | geoip-ru/geosite-ru → direct |
| `LOG_LEVEL` | `info` | Уровень логов sing-box |
| `DIRECT_BYPASS_SET` | `vpn_direct_bypass` | Имя ipset bypass-кэша |
| `DIRECT_BYPASS_TTL` | `3600` | TTL записей (секунды) |
| `PROXY_BIND_IP` | `127.0.0.1` | Bind для HTTP/SOCKS; `0.0.0.0` для LAN |
---
## REST API
| Метод | Путь | Назначение |
| --------- | ----------------------------------- | ------------------------------------------------------------------ |
| GET | `/api/state` | состояние, список серверов, кастомные правила, masked subscription |
| GET | `/api/config` | текущий sing-box config |
| GET | `/api/logs` | последние 200 строк логов |
| GET | `/api/logs/stream` | SSE-поток логов sing-box |
| GET / PUT | `/api/rules` | список кастомных правил |
| POST | `/api/subscription/fetch` | загрузить подписку |
| DELETE | `/api/subscription` | удалить подписку, остановить sing-box |
| POST | `/api/apply` | применить выбранный сервер |
| POST | `/api/singbox/{stop,restart,clear}` | управление процессом |
| Метод | Путь | Описание |
| --------- | ---------------------- | ------------------------------------ |
| `GET` | `/api/state` | Полное состояние системы |
| `POST` | `/api/subscription` | Загрузить подписку по URL |
| `POST` | `/api/apply` | Применить сервер (`{ selectedTag }`) |
| `GET` | `/api/servers` | Список серверов из кэша |
| `GET/PUT` | `/api/rules` | Кастомные правила |
| `GET/PUT` | `/api/rule-sets` | Кастомные remote rule-set |
| `POST` | `/api/singbox/start` | Запустить sing-box |
| `POST` | `/api/singbox/stop` | Остановить sing-box |
| `POST` | `/api/singbox/restart` | Перезапустить sing-box |
| `POST` | `/api/bypass` | `{ enabled }` — bypass mode |
| `GET` | `/api/direct-cache` | Состояние ipset bypass-кэша |
| `DELETE` | `/api/direct-cache` | Сбросить bypass-кэш |
| `POST` | `/api/route-check` | Симулировать маршрут |
| `GET` | `/api/ping` | TCP-пинг до хоста |
| `GET` | `/api/logs/stream` | SSE системных логов |
| `GET` | `/api/traffic/stream` | SSE трафика |
## Важные ограничения
---
- IPv4 TProxy first. IPv6 routing будет отдельным этапом.
- DNS-перехват пока не включен. Для корректного gateway-сценария лучше выдать
клиентам DNS через роутер/DHCP.
- Контейнер должен запускаться с `network_mode: host`, `NET_ADMIN`, `NET_RAW`.
- Mixed proxy по умолчанию слушает `127.0.0.1` (для дома). Чтобы открыть для LAN,
установи `PROXY_BIND_IP=0.0.0.0` в `.env`.
- Gateway не видит process name на клиентском ПК, поэтому правила для игр
задаются через домены, suffix, IP CIDR и порты.
- `_archive/` игнорируется git, потому что там лежит старая реализация и runtime state.
## Структура проекта
```
├── Dockerfile # debian + sing-box + ipset + node
├── entrypoint.sh # iptables/ipset setup → запуск node
├── docker-compose.gateway.yml
├── src/
│ ├── server/
│ │ ├── index.js # HTTP-сервер, управление sing-box, SSE
│ │ ├── singbox.js # генерация конфига sing-box
│ │ ├── subscription.js # парсинг подписок (JSON/VLESS/base64)
│ │ ├── routeMatcher.js # симулятор маршрутизации
│ │ ├── ping.js # TCP-пинг и DNS-resolve
│ │ └── config.js # настройки из env
│ └── web/
│ ├── App.jsx # корневой компонент, глобальный state
│ ├── api.js # обёртка fetch для API
│ └── components/
│ ├── OverviewPage.jsx # дашборд, bypass-toggle
│ ├── LogsPage.jsx # трафик + системные логи
│ ├── RoutingPage.jsx # кастомные правила
│ ├── ServersPage.jsx # подписка и выбор сервера
│ ├── SettingsPage.jsx # rule-sets и настройки
│ └── RouteChecker.jsx # проверка маршрута
└── docs/
└── roadmap.md
```
## Ограничения
- TProxy только IPv4. IPv6 — в roadmap.
- DNS-перехват не включён; выдавайте клиентам DNS через DHCP роутера.
- Gateway не видит имя процесса на клиентском ПК — правила для игр задаются через домены, CIDR и порты.