Summary

Один VPN-сервер за границей перестал устраивать: часть российских сервисов начала отказывать зарубежным IP, а всё остальное по-прежнему нужно открывать «оттуда». Я разделил задачу между двумя узлами. Российский сервер - публичный вход и центр управления. Латвийский - скрытый выход. А между ними сидит Mihomo и для каждого запроса решает, по какой дороге ему ехать.

VPN RU-LV - architecture.excalidraw

С чего всё началось

Классический VPN устроен просто: клиент подключается к серверу, сервер выпускает трафик в интернет. Пока сервер один, любая проблема с его IP - это проблема всех клиентов сразу.

В какой-то момент выяснилось, что так жить неудобно. Российские сервисы - маркетплейсы, банки, госуслуги - всё чаще недоверчиво смотрят на зарубежные адреса. А заграничные сервисы, наоборот, прекрасно открываются только оттуда. Получается странная ситуация: либо VPN включён и ломается половина родного интернета, либо выключен и ломается вторая половина.

Решение лежит на поверхности, но его нужно аккуратно собрать: пусть «российское» идёт напрямую через российский сервер, а «остальное» - через зарубежный. И пользователю не нужно ничего переключать руками.

Два узла и один туннель

В схеме два сервера, у каждого своя роль.

RU - публичный вход. Сюда подключаются клиенты, здесь живут все управляющие сервисы: веб-панель для выдачи конфигов, обратный прокси, DNS, диспетчер маршрутов и файловый менеджер. Если что-то нужно посмотреть или починить - оно здесь.

LV - скрытый выход. Это просто «дверь» в другой внешний IP. Наружу у неё не торчит почти ничего: она принимает трафик только от RU по внутреннему туннелю. Важный принцип - LV не должен превращаться во второй публичный комбайн. Всё, что на нём поднято, доступно только через RU.

Между ними - обычный site-to-site туннель. Клиентов я обслуживаю через AmneziaWG (он лучше маскируется), а вот межсерверный канал оказался честнее делать на простом WireGuard: ему не нужна обфускация, зато нужна скорость. Об этом отдельная история - про неё я написал в блоге.

Mihomo - диспетчер на перекрёстке

Схема: куда Mihomo отправляет запрос клиента

Самое интересное в схеме - Mihomo. Это не просто прокси, а слой политик: он знает гео-базы, списки доменов и подсетей и решает, куда отправить каждое соединение.

Я перенёс в него логику, которая раньше жила на домашнем роутере Keenetic: группы «YouTube», «Discord», «Telegram», «Steam», «AI», «Заблокированные сервисы», собственные правила. Выходов у группы несколько:

  • RU-DIRECT - выйти напрямую через российский сервер;
  • LV-WG - уйти в туннель на Латвию;
  • балансировщик и «самый быстрый» - когда выходов в Латвии несколько, Mihomo сам меряет задержку и выбирает лучший.

Практические плюсы такого подхода:

  • пользовательские правила лежат в двух понятных файлах (custom-ru, custom-lv), а не размазаны по firewall;
  • итоговый конфиг собирается скриптом из шаблона и кусочков, поэтому его можно читать и проверять в git;
  • есть веб-панели, где видно, куда сейчас ушло каждое соединение, и можно переключить группу одним кликом.

Одна техническая деталь, о которой стоит знать. Я запускал Mihomo не в режиме виртуального интерфейса, а в режиме «шлюза» (redir и tproxy). Так же работает Keenetic. Для сервера это проще и предсказуемее, а все выходы - это просто direct, привязанный к нужному сетевому интерфейсу.

Принцип «наружу - только Caddy»

Любая платформа, у которой есть админка, рано или поздно начинает светить админкой в интернет. Я решил не давать ей такого шанса.

  • Публично открыты только порты 80 и 443, и принадлежат они Caddy.
  • Панели управления слушают внутренние адреса и доступны только из админского сегмента VPN.
  • Домены для сервисов навешиваются в Caddy Proxy Manager, а не пробрасываются портами.
  • Для LV действует то же правило: если нужен маленький reverse proxy, он слушает только адрес внутри туннеля.

Здесь есть неочевидная вещь. Так как у RU и LV нет общей Docker-сети, контейнеры не могут обращаться друг к другу по имени. Любая связь «RU → LV» идёт по настоящему адресу и порту. Кажется неудобством, но это дисциплинирует: схема перестаёт быть магией и становится списком явных маршрутов.

Реальный деплой никогда не похож на схему

На бумаге всё красиво. На живом сервере - совсем другая история:

На бумагеВ реальности
Ставим Docker ComposeВ Ubuntu 22.04 нужен пакет docker-compose-v2, а не docker-compose-plugin
Собираем модуль AmneziaWGУ старого ядра уже нет заголовков, модуль собран под новое, нужен перезапуск
Кладём хэш пароля в .envЗнак $ в bcrypt-хэше ломает и compose, и source в bash
Пишем скрипты на WindowsПереводы строк CRLF - и Linux «не видит» скрипт
awg-quick читает /etc/wireguardНа самом деле ищет в /etc/amnezia/amneziawg

Каждая такая мелочь отнимает вечер. Поэтому вывод простой: проекту нужен runbook, а не только docker-compose.yml. Если инструкция говорит «поддерживаем Ubuntu 22.04», она должна уметь пройти свежий сервер с нуля, вместе с заголовками ядра и перезагрузкой.

Как в эту схему вписалась Remnawave

Следующим шагом я добавил панель Remnawave с дополнительными протоколами. Тут важно не то, что их можно включить, а как это встроено.

Панель, нода и страница подписки живут на LV во внутренней Docker-сети. Порт ноды наружу не публикуется. Обратный прокси на RU обращается к ним по приватному адресу туннеля. Не нарушается ни одно правило: RU - публичный, LV - спрятанный.

Чего я сознательно не делал

  • Не включал NAT для домашних сетей. Офисные и домашние подсети подключены как routed-сети с явными маршрутами. Так проще отлаживать: видно реальный адрес отправителя.
  • Не тащил AmneziaWG 2.0 в Remnawave. Панель построена вокруг Xray, а ключи и AllowedIPs ей чужды. Чтобы подружить их, пришлось бы форкнуть и backend, и frontend, и ноду. Не стоит.
  • Не стал накручивать сложные правила в firewall. Когда возникал соблазн «заткнуть дыру» правилом iptables, я возвращал политику в Mihomo. Маршрутизация должна жить в одном месте.

Главная мысль

Хороший VPN «для себя» - это не один туннель. Это набор явных путей, правил, границ публикации и проверяемых способов всё восстановить. Проект оказался где-то между домашней сетью и настоящей SRE-платформой: Docker Compose, systemd, policy routing, reverse proxy, несколько выходов и постепенный переход к более богатым протоколам.

Если хотите увидеть, какие грабли ждут на этом пути, читайте продолжение: детектив про DNS.