VPN Proxy Gateway
Самохостируемый прозрачный VPN-шлюз на базе sing-box.
Разворачивается в 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
ПК-приложения, которым нужен VPN явно:
Windows app → ProxiFyre/Proxifier → gateway:8080 → sing-box mixed-in → global rules → default VPN
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 настраивает ядро:
# 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 # только если DIRECT_BYPASS_CACHE=true
-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 | Global custom rules | direct / VPN / block для всех inbound |
| 3 | rule_set: [geoip-ru, geosite-category-ru] |
direct |
| 4 | Device defaults для tproxy-in |
direct / VPN / block |
| 5 | Proxy default для mixed-in |
по умолчанию VPN |
| 6 | Transparent default для unknown devices | по умолчанию direct |
| 7 | Всё остальное (final) |
direct |
Конфиг генерируется динамически через buildGatewayConfig() из подписки + сохранённых правил. Перед применением выполняется sing-box check.
3. Bypass Mode (весь трафик напрямую)
Кнопка "Весь трафик напрямую" в дашборде. При активации buildGatewayConfig() вызывается с { bypassAll: true } — в конфиге убираются все rule_set, final: "direct". Удобно для диагностики или когда VPN не нужен.
Direct Bypass Cache (ipset)
Оптимизация выключена по умолчанию: DIRECT_BYPASS_CACHE=false. Причина — dst-IP cache обходит sing-box до проверки global rules, а значит может нарушить требования вида AI → VPN или blocked → block.
Если явно включить DIRECT_BYPASS_CACHE=true, IP-адреса, которые sing-box уже отправил напрямую, кэшируются в ядре и больше не проходят через userspace.
Цепочка событий:
-
sing-box маршрутизирует соединение как
direct, пишет в лог:
[TCP] 192.168.1.5:54321 --> 203.0.113.10:443 outbound/direct[direct] -
Node.js парсит строку (regex
-->+outbound/). Еслиcategory === "direct"и назначение — IPv4-адрес:ipset add vpn_direct_bypass 203.0.113.10 timeout 3600 -exist -
Следующий пакет к
203.0.113.10обрабатывается iptables до передачи в sing-box:-m set --match-set vpn_direct_bypass dst → RETURNПакет уходит напрямую на уровне ядра — нулевые накладные расходы userspace sing-box.
-
Запись истекает через TTL (по умолчанию 1 час).
DIRECT_BYPASS_CACHE=false # безопасное значение по умолчанию
DIRECT_BYPASS_SET=vpn_direct_bypass # имя ipset
DIRECT_BYPASS_TTL=3600 # TTL в секундах
Профили устройств
Управляются из UI на вкладке Маршрутизация и сохраняются в devices.json:
{
"defaultTransparentMode": "direct",
"proxyDefaultMode": "vpn",
"devices": [
{
"id": "gaming-pc",
"name": "Gaming PC",
"ip": "192.168.1.50",
"mac": "",
"mode": "direct",
"enabled": true
},
{
"id": "phone",
"name": "Phone",
"ip": "192.168.1.60",
"mode": "vpn",
"enabled": true
}
]
}
| Mode | Что делает |
|---|---|
direct |
fallback устройства после global rules → direct |
vpn |
fallback устройства после global rules → выбранный VPN |
block |
fallback устройства после global rules → block |
rules |
не задаёт fallback устройства; используется transparent default |
mixed-in не зависит от режима устройства: если приложение явно пошло на gateway:8080, сначала применяются global rules, затем proxyDefaultMode (по умолчанию VPN).
Кастомные правила маршрутизации
Управляются из вкладки Маршрутизация. Сохраняются в custom-rules.json.
Правила применяются в порядке отображения в UI — first match wins. Custom rules являются global rules: они применяются для tproxy-in, mixed-in, ПК, телефона и unknown devices до любых fallback-режимов.
| Поле | Тип | Описание |
|---|---|---|
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:
{ "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] ← соединение
[router]-строка → имя правила сохраняется с TTL 500 мс- Следующая строка с
-->подхватывает имя в полеmatchedRule - Тип трафика:
direct/vpn/blockпо outbound - Direct + IPv4 → добавление в ipset bypass-кэш, только если
DIRECT_BYPASS_CACHE=true
Группировка и сортировка
(category, host, port, matchedRule) объединяются в группу с счётчиком:
- По частоте — самые частые наверху (по умолчанию)
- По времени — последние наверху
Проверка маршрута
Вкладка Проверка позволяет узнать, по какому правилу пойдёт трафик к хосту/IP/порту — без реального подключения. Node.js (routeMatcher.js) симулирует ту же логику, что и sing-box:
- private IP → direct
- global custom rules
- geoip-ru / geosite-category-ru → direct
tproxy-in+ device defaultmixed-in+ proxy default- final → direct
Быстрый старт
# Сборка фронтенда
npm install && npm run build
# Запуск контейнера
docker compose -f docker-compose.gateway.yml up -d
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_CACHE |
false |
Включить dst-IP bypass cache; по умолчанию выключен |
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 |
PROXY_FIREWALL |
true |
Закрыть PROXY_PORT не из allowed CIDR |
PROXY_ALLOWED_CIDRS |
10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 |
Кто может подключаться к mixed proxy |
REST API
| Метод | Путь | Описание |
|---|---|---|
GET |
/api/state |
Полное состояние системы |
POST |
/api/subscription |
Загрузить подписку по URL |
POST |
/api/apply |
Применить сервер ({ selectedTag }) |
GET |
/api/servers |
Список серверов из кэша |
GET/PUT |
/api/rules |
Кастомные правила |
GET/PUT |
/api/devices |
Профили устройств и default fallback |
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 |
Симулировать маршрут |
POST |
/api/servers/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 и порты.