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-диапазона8080–8090
Установщик интерактивно спросит 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 nativesing-box.exeon127.0.0.1:1080plus ProxiFyre/WinPacketFilter.ProxiFyre only: ProxiFyre/WinPacketFilter only, pointed at an existing SOCKS5 proxy such as127.0.0.1:8080or192.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.
Цепочка событий:
-
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": "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] ← соединение
[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
Если 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 и порты.