Files
vpn-proxy/README.md
Dmitriy Petrov ec8e748a43
Some checks failed
Build and Deploy Gateway / build-and-deploy (push) Failing after 13s
Add routed build and deploy flow for gateway image
2026-05-09 10:32:18 +03:00

376 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```
ПК-приложения, которым нужен 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` настраивает ядро:
```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 # только если 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-адрес:
```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_CACHE=false # безопасное значение по умолчанию
DIRECT_BYPASS_SET=vpn_direct_bypass # имя ipset
DIRECT_BYPASS_TTL=3600 # TTL в секундах
```
## Профили устройств
Управляются из UI на вкладке **Маршрутизация** и сохраняются в `devices.json`:
```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:
```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-кэш, только если `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
---
## Быстрый старт
```bash
# Сборка фронтенда
npm install && npm run build
# Запуск контейнера
docker compose -f docker-compose.gateway.yml up -d
```
Если Docker Hub отвечает таймаутом на `debian:bookworm-slim`, можно собрать через read-through mirror:
```bash
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 на другом:
```bash
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`.
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 и порты.