Dmitriy Petrov 62f50d9c28
Some checks failed
Build and Deploy Gateway / build-and-deploy (push) Failing after 13s
Allow special characters in rule-set tags
2026-05-09 10:23:57 +03:00

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 по умолчанию VPN
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.

Цепочка событий:

  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-адрес:

    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_CACHE=false             # безопасное значение по умолчанию
DIRECT_BYPASS_SET=vpn_direct_bypass   # имя ipset
DIRECT_BYPASS_TTL=3600                # TTL в секундах

Профили устройств

Управляются из UI на вкладке Маршрутизация и сохраняются в devices.json:

{
  "defaultTransparentMode": "vpn",
  "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]   ← соединение
  1. [router]-строка → имя правила сохраняется с TTL 500 мс
  2. Следующая строка с --> подхватывает имя в поле matchedRule
  3. Тип трафика: direct / vpn / block по outbound
  4. Direct + IPv4 → добавление в ipset bypass-кэш, только если DIRECT_BYPASS_CACHE=true

Группировка и сортировка

(category, host, port, matchedRule) объединяются в группу с счётчиком:

  • По частоте — самые частые наверху (по умолчанию)
  • По времени — последние наверху

Проверка маршрута

Вкладка Проверка позволяет узнать, по какому правилу пойдёт трафик к хосту/IP/порту — без реального подключения. Node.js (routeMatcher.js) симулирует ту же логику, что и sing-box:

  1. private IP → direct
  2. global custom rules
  3. geoip-ru / geosite-category-ru → direct
  4. tproxy-in + device default
  5. mixed-in + proxy default
  6. final → direct

Быстрый старт

# Сборка фронтенда
npm install && npm run build

# Запуск контейнера
docker compose -f docker-compose.gateway.yml up -d

Если Docker Hub отвечает таймаутом на debian:bookworm-slim, можно собрать через read-through mirror:

BASE_IMAGE=mirror.gcr.io/library/debian:bookworm-slim \
docker compose -f docker-compose.gateway.yml build

docker compose -f docker-compose.gateway.yml up -d

UI доступен на http://<gateway-ip>:3456.

На роутере указать шлюз по умолчанию (или нужные подсети) на IP контейнера.


Переменные окружения

Переменная По умолчанию Описание
PORT 3456 Порт веб-интерфейса
BASE_IMAGE debian:bookworm-slim Базовый Docker image для сборки; можно заменить на mirror
SINGBOX_VERSION 1.12.13 Версия sing-box для Docker build
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 записей (секунды)
RULE_SET_DOWNLOAD_DETOUR vpn Через какой outbound sing-box скачивает remote rule-set; vpn = выбранный сервер
PROXY_BIND_IP 0.0.0.0 Bind для HTTP/SOCKS в LAN; можно сузить до IP gateway
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 и порты.
Description
No description provided
Readme 1.5 MiB
Server + Mac Latest
2026-05-19 14:48:00 +03:00
Languages
JavaScript 83.7%
CSS 8.5%
Shell 7.1%
Dockerfile 0.6%