# VPN Proxy Gateway Самохостируемый прозрачный VPN-шлюз на базе [sing-box](https://sing-box.sagernet.org/). Разворачивается в Docker (LXC, VPS), перехватывает трафик всей локальной сети через iptables TProxy — без клиентов на устройствах. Веб-интерфейс на React даёт полное управление: подписки, выбор сервера, кастомные правила маршрутизации, просмотр трафика в реальном времени. --- ## Архитектура ``` Клиент (ПК/телефон) │ 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 # Сборка фронтенда npm install && npm run build # Запуск контейнера docker compose -f docker-compose.gateway.yml up -d ``` UI доступен на `http://: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` | Полное состояние системы | | `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 трафика | --- ## Структура проекта ``` ├── 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 и порты.