feat: добавлены правила маршрутизации по устройствам и управление ими через API
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s
Refs: None
This commit is contained in:
328
README.md
328
README.md
@@ -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 и порты.
|
||||
|
||||
Reference in New Issue
Block a user