2026-05-21 20:28:12 +03:00
2026-05-21 20:21:42 +03:00
2026-05-21 20:13:18 +03:00
2026-05-21 20:34:13 +03:00

VPN Proxy

Локальный Docker-клиент для Mac и прозрачный VPN-шлюз на базе sing-box.

macOS: локальный Docker-клиент

Самый простой режим: контейнер работает как обычный локальный HTTP/SOCKS proxy без TProxy, iptables, network_mode: host и прав NET_ADMIN.

curl -fsSL https://git.dokops.ru/dokril/vpn-proxy/raw/branch/master/scripts/install-macos-client.sh | bash

После запуска по умолчанию:

  • UI: http://127.0.0.1:3456
  • HTTP/SOCKS proxy: 127.0.0.1:8080 по умолчанию; в UI можно выбрать порт из Docker-диапазона 80808090

Установщик интерактивно спросит proxy-порт. Если стандартный UI-порт 3456 занят другим контейнером, установщик попросит выбрать свободный UI-порт. Для неинтерактивного запуска можно задать порты заранее; тогда вопросы не появятся:

curl -fsSL https://git.dokops.ru/dokril/vpn-proxy/raw/branch/master/scripts/install-macos-client.sh | VPN_PROXY_CLIENT_PORT=18080 bash

Если старый gateway/client уже занимает 3456 или выбранный proxy-порт, можно не трогать старый контейнер и поставить новый клиент на другие порты:

curl -fsSL https://git.dokops.ru/dokril/vpn-proxy/raw/branch/master/scripts/install-macos-client.sh | VPN_PROXY_CLIENT_UI_PORT=3457 VPN_PROXY_CLIENT_PORT=18080 bash

После запуска скрипт проверяет, что UI реально ответил на /api/state. Если контейнер сразу упал или порт занят, он покажет docker compose ps и последние логи вместо ложного сообщения о готовности.

В Mac UI есть Домашний режим. Когда он включён, приложения по-прежнему используют выбранный локальный proxy-порт, но весь proxy-трафик идёт напрямую без VPN.

Также Mac-клиент можно связать с серверным gateway. На gateway доступна ручка:

GET http://<gateway-ui-host>:3456/api/shared-proxy

Если gateway запущен и его mixed proxy работает, ручка вернёт available: true и SOCKS5 endpoint общего proxy. В Mac UI укажите адрес gateway UI, например http://192.168.50.111:3456. Клиент проверит ручку и переключит локальный 127.0.0.1:<proxy-port> в режим upstream: весь proxy-трафик пойдёт через общий gateway, локальная VPN-подписка на Mac для этого режима не нужна.

Ручной запуск из checkout:

docker compose -f docker-compose.client.yml up -d --build

Перезапуск и логи:

cd ~/.vpn-proxy-client
docker compose -f docker-compose.client.yml logs -f
docker compose -f docker-compose.client.yml restart

Windows: app proxy client

Windows mode restores the native workflow for Discord, Vesktop, games, and other apps that do not expose proxy settings.

Run PowerShell 7 as Administrator. While this branch is being tested, install from codex-windows-client:

irm https://git.dokops.ru/dokril/vpn-proxy/raw/branch/codex-windows-client/scripts/install-windows-client.ps1 | iex

Installer modes:

  • Full install: local native sing-box.exe on 127.0.0.1:1080 plus ProxiFyre/WinPacketFilter.
  • ProxiFyre only: ProxiFyre/WinPacketFilter only, pointed at an existing SOCKS5 proxy such as 127.0.0.1:8080 or 192.168.50.111:8080.

The installer keeps profile data under C:\Tools\vpn-proxy-windows\data, so rerunning it can replace app files without deleting saved profiles.

Local UI:

http://127.0.0.1:3456

Recovery commands:

& "C:\Tools\vpn-proxy-windows\app\scripts\windows\manage.ps1" -OpenUi
& "C:\Tools\vpn-proxy-windows\app\scripts\windows\manage.ps1" -Status
& "C:\Tools\vpn-proxy-windows\app\scripts\windows\manage.ps1" -RestartServices

The UI manages profiles made of process names, folders, and explicit .exe files. It generates ProxiFyre config and restarts ProxiFyre only when the user applies changes.


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

Если сборку нужно выполнять на контейнере/хосте, который уже ходит через рабочий gateway, а запускать image на другом:

BUILD_HOST=107 DEPLOY_HOST=111 ./scripts/build-on-107-deploy-111.sh

Скрипт собирает image на BUILD_HOST, переносит его на DEPLOY_HOST через docker save | docker load и запускает без docker pull. Если 107/111 не являются SSH-алиасами, укажите реальные адреса, например BUILD_HOST=root@192.168.1.107 DEPLOY_HOST=root@192.168.1.111.

Чтобы не получать циклическую зависимость "собрать gateway можно только через уже работающий gateway", подготовьте runtime base на 107 один раз:

./scripts/build-runtime-base.sh

После этого CI и build-on-107-deploy-111.sh используют локальный vpn-proxy-runtime-base:bookworm-slim: основная сборка gateway больше не делает apt-get, не качает sing-box и не обращается к Docker Hub за base image.

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

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


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

Переменная По умолчанию Описание
APP_MODE gateway gateway или client; compose клиента задаёт client автоматически
CLIENT_UI_PORT 3456 Host-порт UI для docker-compose.client.yml
VPN_PROXY_CLIENT_UI_PORT unset UI-порт для macOS installer; записывается в CLIENT_UI_PORT
VPN_PROXY_CLIENT_PORT unset Proxy-порт для macOS installer; записывает CLIENT_PROXY_PORT_START/END
CLIENT_PROXY_PORT_START 8080 Первый host/container proxy-порт для docker-compose.client.yml
CLIENT_PROXY_PORT_END 8090 Последний host/container proxy-порт для docker-compose.client.yml
SHARED_PROXY_HOST unset Явный host/IP, который gateway отдаёт в /api/shared-proxy; если не задан, берётся Host заголовок запроса
PORT 3456 Порт веб-интерфейса
BASE_IMAGE debian:bookworm-slim Базовый Docker image для сборки; можно заменить на mirror
SINGBOX_VERSION 1.12.13 Версия sing-box для Docker build
INSTALL_RUNTIME_DEPS true Устанавливать runtime-пакеты в Docker build; false для подготовленного runtime base
INSTALL_SINGBOX true Скачивать sing-box в Docker build; false для подготовленного runtime base
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 Полное состояние системы
GET /api/shared-proxy Проверка и параметры общего gateway proxy
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%