П/ВИН

Свой VPN сервер 2026: каскад VLESS Reality с гео-роутингом

·9 мин чтения

19 платных подписок, один выход в Чехии и больше года стабильного аптайма - вот как у меня выглядел свой VPN сервер в 2026, когда я решил закрыть накопившиеся архитектурные долги одним заходом. Изначально PASHAVIN был обычной связкой Marzban + Xray поверх единственного чешского узла, всё функционировало, но в этот раз я включил каскад через RU-мост, обновил Xray до свежей версии, поднял умную маршрутизацию для российских сайтов и попутно навёл порядок в документации для пользователей. Получилось то, что у больших коммерческих VPN называется «split tunneling по гео», только полностью на стороне сервера, клиенту не нужно ничего настраивать руками.

Что было: классическая прямая схема

Исходно у меня было два VPS, но работали они независимо. RU-сервер (Datachip, Москва) — стоял отдельно и в схеме VPN не участвовал, он крутил мост для VLESS-bridge и SNI-маршрутизацию для софтфона по sip.pashavin.ru. Чешский сервер (Datacheap) — собственно работал точкой выхода: Marzban-панель на 8000-м порту, Xray-core на 8443 с конфигом VLESS+Reality+XTLS-Vision, домен exit1.pashavin.ru. Клиенты импортировали в v2rayNG, Streisand или v2box подписку и подключались напрямую на чешский IP.

За полтора года я несколько раз ловил жалобы клиентов: то Telegram-провайдер начнёт деградировать соединения до зарубежных IP, то конкретный РКН-фильтр срежет половину UDP-трафика к Чехии, то домен exit1.pashavin.ru слишком явно «светит» свой адрес. Параллельно с этим выяснилось, что я плачу 350₽ за РФ-сервер, который, по сути, ничего полезного не делал для VPN — а это ресурс, который очень хочется использовать.

Идея каскада: бридж как первая точка входа

Классическая идея в любительских VPN-конфигах — направить клиента сначала на доверенный российский endpoint, который уже сам соединяется с зарубежным выходом. Преимуществ несколько: трафик внутри РФ идёт от пользователя до своей же машины внутри страны, и DPI/RKN видят соединение «российский клиент → российский IP», что выглядит куда менее подозрительно. Дальше с RU-моста уже идёт исходящий канал на EU-выход — и тут уже почти неважно, как он маркируется, лишь бы он стоял стабильно.

Я выбрал следующую схему:

Клиент → bridge-1 (RU, :443) → exit-1 (EU, :8443) → Интернет

На стороне бриджа поначалу стоял nginx stream с SNI-демультиплексором: трафик с SNI sip.pashavin.ru шёл в локальный софтфон-эндпоинт на 8443, а весь остальной — пробрасывался на чешский Xray. Это самый простой вариант — фактически TCP-релэй с минимальным оверхедом.

Чтобы не сломать активных пользователей одним движением, я сделал двухшаговую миграцию через Marzban. У Marzban есть механизм ProxyHost — для одного inbound можно описать несколько вариантов хостов в подписке. Я добавил второй host (vpn.pashavin.ru:443 через бридж), не убирая старый прямой (exit1.pashavin.ru:8443). UUID, Reality-ключи и SNI совпадают полностью — клиент видит, по сути, одну и ту же VLESS-сессию, меняется только entry-point.

После проверки нескольких клиентов через каскад я отключил старый host, прислал broadcast пятерым платным подписчикам через бот: «меняем серверы, ближайший час возможны ошибки», переименовал хост в PASHAVIN - FREE IP и оставил подписку отдавать единственный конфиг — каскадный. Всё прошло за десять минут без потери сессий.

Апгрейд Xray-core и страховка от отката

Параллельно с каскадом я обновил Xray на чешском сервере: с версии 24.12.31 на 26.3.27. Это важная фоновая работа — между релизами были изменения в Reality-handshake и улучшения по антифинальной обфускации, которые DPI начали ловить на старых билдах. Перед обновлением сделал бэкап бинарника в /var/lib/marzban/xray-core/xray.backup-<дата> и базы Marzban в /var/lib/marzban/db.sqlite3.backup-<timestamp>. Marzban стартанул чисто, ноды поднялись, бот зарегистрировал хэндлеры. На обновление + проверку ушло около пяти минут.

Зачем я об этом пишу: это пример той самой «дешёвой страховки», которая много раз спасала мне ночи. Любая работа с VPN-инфрой, от которой зависят живые пользователи, должна сопровождаться явным бэкапом артефактов, на которые можно откатиться одной командой. Если бы Xray 26.3.27 неожиданно отказал, я бы за 30 секунд вернул прошлую версию и разбирался уже в спокойной обстановке.

Главная новая фича: умный роутинг для РФ-сайтов

Самая интересная часть — это серверный split tunneling. После включения каскада возникла классическая проблема: пользователь во Вьетнаме хочет открыть госуслуги, но трафик через чешский exit уходит с европейского IP — и Госуслуги начинают подозревать, нагружают капчей или вообще не пускают. То же самое с банками, РЖД, контентными сайтами вроде Кинопоиска. Решать это правилами в клиенте — нерабочий вариант: у меня семья и платные клиенты, никто не будет ковыряться в YAML-роутинге Clash или sing-box.

Правильное решение — терминировать VLESS-туннель уже на бридже и принимать routing-решение там же, на сервере. То есть nginx stream, который тупо прокидывает байты, нужно заменить на полноценный Xray на бридже, у которого:

  • inbound — VLESS Reality на :443 с теми же ключами и UUID, что и на мастере
  • outbound DIRECT (freedom) — для российских адресов: трафик выходит через сам бридж, юзер видится Госуслугам как клиент из Москвы
  • outbound TO_EXIT — VLESS Reality client, который коннектится на exit:8443 и выпускает трафик в Чехии
  • route-правила, которые матчат geosite:category-gov-ru, geosite:category-bank-ru, geosite:category-travel-ru, geoip:ru → DIRECT, всё остальное → TO_EXIT

То есть это standalone Xray, а не marzban-node — потому что нужен полный контроль над port-mapping, чего ноды нативно не дают. Пользователей я синхронизирую через Marzban API: при изменении подписки скрипт перегенерирует конфиг на бридже с актуальным списком клиентов и делает graceful reload.

Переключение было самым стрессовым моментом: bridge-1:443 уже был занят nginx-стримом, нужно было его остановить и сразу поднять Xray. Downtime прикинул в 5–10 секунд. По факту получилось ещё меньше — Xray стартует мгновенно, сложности были только с тем, чтобы не сломать существующий SNI-демультиплексор для софтфона. Решение оказалось простым: оставить nginx stream на :443 как «фронт», а Xray слушать на 127.0.0.1:8445, и nginx делает SNI-маршрутизацию:

Client → bridge:443 (nginx SNI demux)
         ├─ SNI=sip.pashavin.ru → 127.0.0.1:8443 (UIS softphone)
         └─ default → 127.0.0.1:8445 (xray)

Это два инструмента в правильных ролях: nginx умеет SNI-демультиплексирование лучше всех, Xray умеет VLESS Reality + routing лучше всех. Не нужно лепить всё в одну сущность.

Результат: что получилось в логах

После финализации я погонял трассировку на routing-логах бриджа — посмотреть, действительно ли правила работают так, как задумывались. И вот что увидел:

СайтRouting-правилоEgress IP
gosuslugi.rugeosite:category-gov-ru → DIRECTbridge RU
sber.rugeosite:category-bank-ru → DIRECTbridge RU
rzd.rugeosite:category-travel-ru → DIRECTbridge RU
ifconfig.medefault → TO_EXITexit CZ

Это именно то, что хотелось: российские сервисы видят родной IP бриджа, остальной интернет — европейский exit. Для пользователя это абсолютно прозрачно — он импортировал в клиент один VLESS-конфиг, запустил, открыл сайт. Никаких ручных переключений, ничего настраивать не надо.

Плюс автоматически решилась проблема пользователей за границей: тот же клиент во Вьетнаме теперь открывает Госуслуги через RU-мост (видится как Москва) и Netflix через exit (видится как Чехия) — одной и той же подпиской. До этого приходилось людям объяснять, что для РФ-сервисов VPN надо выключать.

Документация и санитария репозитория

По ходу пьесы вычистил docs/user-guide-android.md, docs/user-guide-ios.md, docs/user-guide-desktop.md — они устарели на несколько месяцев и расходились с актуальными инструкциями в личном кабинете пользователя. ЛК сам по себе уже превратился в полноценный источник истины — там есть QR-коды, deeplinks, скачивание APK, инструкции по четырём платформам. Параллельные markdown-файлы только запутывали и собирали ошибки. Решение — удалить файлы, оставить ЛК как единый источник для конечного пользователя.

Одновременно сделал первый коммит проекта в GitHub: github.com/bugle-c/vpn-pashavin (приватный). В репо лежат KNOWLEDGE.md с архитектурой, DESIGN.md с обоснованием решений, шаблоны конфигов, кастомизация Telegram-бота, шаблоны ЛК, скрипты управления и план миграции. В .gitignore отдельно прописал .env*, *.key, .claude/, tasks/ — секреты в репо не должны попадать никогда, токены живут только в /opt/marzban/.env на сервере, а в репо есть только placeholder вроде YOUR_BOT_TOKEN в template-файлах.

Также отдельная история — клиент mom и ещё пара бессрочных аккаунтов: они физически не могут переключить подписку (планшет родителей, дядя, у которого нет даже Telegram для оповещений). Marzban архитектурно не даёт «один host для одного юзера, другой для остальных» — все хосты глобальны для inbound. Решение оказалось простым: просто никогда не удалять DNS-запись exit1.pashavin.ru → IP exit-сервера и оставить старый порт 8443 слушающим. Кэшированный VLESS-конфиг у мамы продолжает работать на прямом коннекте бессрочно, а вся остальная подписка отдаёт уже каскадный конфиг. Чуть-чуть архитектурного компромисса ради реальных людей.

Технологическая база и полезные ссылки

Практически вся история построена вокруг четырёх инструментов: панель управления подписками Marzban, сам ядро Xray-core, реверс-прокси nginx со stream-модулем и протокол VLESS с Reality-обфускацией. Reality — это сравнительно свежий способ маскировать VLESS-туннель под произвольный «чистый» TLS-хост, что делает DPI-обнаружение принципиально сложнее, чем для классического Trojan или Shadowsocks. Если интересно почитать про сам протокол и трейдоффы — рекомендую разобраться по официальной документации Xray, там разложено и про XTLS-Vision, и про настройку маршрутизации.

Выводы

Первый и самый важный вывод за этот проект — серверные решения почти всегда лучше клиентских, если у вас живые непрофильные пользователи. Любое «настрой себе routing-правила в YAML» на стороне клиента — это билет в техподдержку и нескончаемые жалобы «у меня не работает». Если можно сделать так, что клиент импортирует один конфиг и сразу всё работает — нужно делать именно так, даже если это требует поднять полноценный Xray на промежуточном узле и синхронизировать конфигурацию между двумя серверами.

Второй урок — архитектурные правки лучше делать инкрементально, не убивая старую схему до подтверждения новой. Двухшаговая миграция через два host'а в Marzban, оставление DNS старого exit'а после переключения, бэкапы бинарников и базы перед апдейтом — это всё стоит копеек по времени, но позволяет откатиться за минуту, если что-то пошло не так. Я уже несколько раз обжигался, когда «гордо чистый» переезд оборачивался ночными звонками: лучше неделю иметь две параллельные схемы и явно их понимать, чем один день красивого однопроходного коммита.

Третий вывод — разделение зон ответственности между nginx и Xray работает лучше, чем попытка сделать одну универсальную сущность. nginx-stream идеален для SNI-демультиплексирования (один порт 443, два разных backend'а по имени домена), Xray — для VLESS-приёма и routing'а по гео. Когда я попытался впихнуть SNI-маршрутизацию софтфона прямо в Xray, конфиг превратился в нечто нечитаемое; обратное разделение «nginx как фронт, Xray как один из backend'ов на лупбэке» получилось куда чище и расширяемее. Этот же подход легко масштабировать дальше: завтра понадобится третий backend на этом же порту — добавляю SNI-блок в nginx, и всё.

Четвёртый — документация для пользователей должна жить в одном месте, и это место должно быть динамическим. Markdown-файлы в репозитории неизбежно расходятся с реальностью через 2–3 месяца, потому что обновлять их параллельно с UI ЛК никто не будет. Личный кабинет с актуальными QR, deeplinks и динамическими ссылками на APK — единственная схема, которая выживает в долгую. Старые docs/user-guide-*.md я не пытался синхронизировать заново — просто удалил, и ЛК стал единым источником.

И последнее — умный роутинг по гео решает проблему, которой раньше не было, но которая появляется автоматически в момент, когда у вас становится больше одного географического сценария использования. У VPN-клиента за границей и VPN-клиента в РФ принципиально разные требования к выходу: первому нужен российский IP для РФ-сервисов, второму — европейский IP для зарубежных. Если на сервере держать общую логику геомаршрутизации, то одна подписка покрывает оба сценария абсолютно одинаково, и пользователю не нужно даже знать, что внутри что-то делится. Это та самая «инженерная элегантность», ради которой стоит закладываться на standalone Xray вместо более простой nginx-stream-схемы.

Паша Вин
Паша Вин

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.