Каскад на AmneziaWG: собираем два сервера, чтобы российские сайты шли напрямую

Каскад на AmneziaWG: собираем два сервера, чтобы российские сайты шли напрямую

Свой VPN на одном заграничном сервере хорош, пока не открываешь российские сайты. Часть из них иностранные адреса просто не пускает - банки, Госуслуги, иногда стриминг. Остальные открываются, но с лишним крюком через заграницу и с чужим IP.

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

Про авторство сразу: идею предложил один из пользователей установщика (glfenix на GitHub) в обсуждении проекта , я собрал схему на двух чистых серверах, погонял, поправил пару мест и довёл до пошаговой инструкции. Спасибо ему за разбор.

Два VPS на чистом Debian 12/13 или Ubuntu 24.04/25.10, root на обоих. Вход - в России или рядом: к нему подключаются клиенты, и российские сайты открываются с близкого адреса. Выход - обычный зарубежный VPS.

Два VPS на чистом Debian 12/13 или Ubuntu 24.04/25.10, root на обоих.

Вход - в России или рядом: к нему подключаются клиенты, и российские сайты открываются с близкого адреса.

Вход - в России или рядом: к нему подключаются клиенты, и российские сайты открываются с близкого адреса.

Выход - обычный зарубежный VPS.

Выход - обычный зарубежный VPS.

Реальный публичный IP на обоих. Сервер за провайдерским NAT (CGNAT) на роль входа не годится - снаружи он недоступен. Быстрая проверка: адрес из curl -s ifconfig.me совпадает с адресом на интерфейсе ( ip -4 addr ). Отдельный случай Hetzner с приватным шлюзом разберу ниже, в граблях.

Реальный публичный IP на обоих. Сервер за провайдерским NAT (CGNAT) на роль входа не годится - снаружи он недоступен. Быстрая проверка: адрес из curl -s ifconfig.me совпадает с адресом на интерфейсе ( ip -4 addr ). Отдельный случай Hetzner с приватным шлюзом разберу ниже, в граблях.

IPv6 выключен - установщик так ставит по умолчанию. Каскад работает по IPv4, при включённом IPv6 трафик пойдёт мимо деления.

IPv6 выключен - установщик так ставит по умолчанию. Каскад работает по IPv4, при включённом IPv6 трафик пойдёт мимо деления.

Разные подсети у серверов. У меня вход - 172.16.17.1/24 , выход - 172.16.61.1/24 .

Разные подсети у серверов. У меня вход - 172.16.17.1/24 , выход - 172.16.61.1/24 .

Механику я подробно разбирал в прошлой статье, тут коротко, чтобы шаги были понятны. Сервер-вход смотрит на адрес назначения каждого пакета. Адрес в списке российских сетей - пакет уходит в интернет напрямую с входа. Нет - помечается меткой и уезжает через туннель на сервер-выход. Список российских сетей берётся из открытой зоны ipdeny и грузится в ipset . Клиенты добавляются на вход как обычно, manage add имя , на их стороне настраивать нечего.

Дальше - сборка.

Оба сервера - обычные установки amneziawg-installer (он разворачивает AmneziaWG 2.0 с обфускацией и сразу настраивает файрвол). Ставим по основной инструкции и запускаем неинтерактивно с нужными подсетями:

Установщик сам поднимает пересылку и NAT, так что серверный конфиг дальше править не нужно - скрипт маршрутизации добавит только то, чего нет. На входе доставим два пакета, которых может не быть на минимальном образе:

Сервер-вход будет подключаться к выходу как обычный клиент. Создаём его на выходе:

Получаем файл /root/awg/ru_host.conf . Копируем его на вход любым способом (scp, буфер). Внутри - обычный клиентский конфиг: Endpoint (внешний IP выхода) и AllowedIPs = 0.0.0.0/0 . Endpoint запомните, он понадобится в скрипте.

Кладём этот конфиг на вход как /etc/amnezia/amneziawg/awg1.conf и правим две строки:

в секцию [Interface] добавляем Table = off - чтобы awg-quick не прописывал маршруты сам, маршрутизацией займётся наш скрипт;

в секцию [Interface] добавляем Table = off - чтобы awg-quick не прописывал маршруты сам, маршрутизацией займётся наш скрипт;

строку DNS = ... убираем, на сервере она не нужна.

строку DNS = ... убираем, на сервере она не нужна.

Закрываем права и поднимаем туннель:

В выводе awg show awg1 должна появиться строка latest handshake - связь с выходом есть. Пинг внутреннего адреса выхода на этом шаге не пройдёт, и это нормально: маршрут к нему появится в отдельной таблице на следующем шаге.

Вся логика деления - один bash-скрипт на входе, /root/awg/awg-routing.sh . Он идемпотентный: можно запускать повторно, при каждом запуске он заодно обновляет список российских сетей. Целиком тащить его в статью не буду - в гайде по каскаду он лежит готовый и копируется как есть. Разберу суть по кускам.

Список российских сетей заливается в ipset через временный набор с атомарной подменой, чтобы не было окна, когда набор пуст:

Отдельная таблица маршрутизации и правило по метке: помеченный трафик уходит через туннель awg1 .

Маркировка трафика клиентов: к российским сетям - напрямую ( RETURN до метки), остальное помечаем. Порядок важен, RETURN стоит раньше MARK :

И маршрут к самому выходу держим вне туннеля, иначе пакеты к нему закольцуются в awg1 .

Два места в скрипте появились после того, как схема у меня ломалась. Первое: если список российских сетей не скачался и оказался пустым, скрипт останавливается с ошибкой, а не продолжает. Пустой ipset - тихая катастрофа: RETURN не ловит ничего, и весь трафик, включая российский, уходит за границу. Лучше явная ошибка, чем молча сломанное деление. Второе - про /32 -шлюз, о нём ниже.

Вписываем в начало скрипта свою клиентскую подсеть и внешний IP выхода, дальше:

Главная боль исходной схемы: после ребута маршруты не вставали, всё приходилось поднимать руками. Лечится одним systemd-юнитом, привязанным к обоим туннелям, - он стартует строго после них.

/etc/systemd/system/awg-routing.service :

Requires тут жёсткий: не поднялся туннель к выходу - не запустится и маршрутизация (клиенты подключатся, но без раздельного выхода, весь трафик пойдёт напрямую). Я специально ребутнул вход и проверил: туннели, таблица, список - всё встало само, без единого ручного действия.

Список российских сетей со временем меняется, поэтому раз в неделю юнит стоит перезапускать - заодно обновится и список:

Проще всего убедиться по счётчикам. На входе обнуляем их, с клиента создаём оба вида трафика и смотрим:

curl ifconfig.me с клиента показывает IP выхода, а счётчик RETURN растёт на российских адресах - каскад собран правильно. MTU, если что, отдельно занижать не пришлось: двойная инкапсуляция укладывается в стандартные размеры, десятимегабайтная передача через оба туннеля у меня прошла без потерь.

Тут то, обо что застревал я сам, и то, о чём писали люди, повторившие схему.

Весь трафик, включая российский, идёт через заграницу. Первым делом смотрим список РФ-сетей: ipset list ru | grep "Number of entries" , там должно быть несколько тысяч (у меня около 8600). Ноль - список не скачался. На этот случай скрипт и останавливается с ошибкой, а не работает с пустым набором. Если www.ipdeny.com заблокирован у вашего провайдера, скрипт берёт снимок сетей из репозитория проекта.

Какой-то российский сайт всё равно открывается через заграницу. Деление идёт по IP назначения. Я на всякий случай проверил Госуслуги (вместе со входом через ЕСИА) и Сбербанк - их адреса есть в зоне ipdeny, то есть они уходят напрямую через вход и открываются нормально. Утекают через выход обычно сайты, которые физически стоят на зарубежном хостинге или за Cloudflare: их адрес иностранный, в список РФ-сетей не попадает, трафик к нему помечается и уходит на выход - и если сайт не пускает иностранные адреса, он не откроется. Проверить конкретный сайт просто: getent hosts имя.ru , затем по каждому адресу ipset test ru <адрес> . Адрес российский, но его нет в наборе - список у вас неполный или не докачался (см. пункт выше). Адрес зарубежный - это ожидаемо, лечится только на стороне самого сайта.

Google и YouTube тормозят, хотя выход зарубежный. Про это я коротко писал в прошлой статье: часть кэшей Google и YouTube стоит на российских адресах, попадает в список РФ-сетей и уходит напрямую через вход, поэтому Google видит российский IP. Подменой DNS это надёжно не лечится. Если нужно, чтобы Google всегда шёл через выход, его сети гонят на выход мимо RETURN - разбор с командами есть в гайде.

Отдача заметно ниже приёма. Бывает, что приём нормальный, а отдача проседает почти в ноль. На MTU и процессор грешить не спешите: MTU режет пакеты в обе стороны, тогда страдал бы и приём; процессор просадил бы обе стороны разом. Живой приём уже говорит, что вход отдаёт клиенту на скорости, значит узкое место - плечо вход -> выход, по которому уходит отдача наружу; приём это направление не использует. Мерить надо направленно: на выходе iperf3 -s , со входа iperf3 -c <IP выхода> (это отдача) и iperf3 -c <IP выхода> -R (приём). Низкая первая цифра при нормальной второй - душит участок между серверами, а не сами серверы: шейпинг исходящего у хостера входа, кривой пиринг до сети выхода или входной лимит на выходе.

Скрипт падает с Nexthop has invalid gateway . Это про хостеров вроде Hetzner: реальный публичный IP выдан как /32 , а шлюз по умолчанию лежит вне подсети сервера (что-то вроде 172.31.1.1 ). Ядро не считает такой шлюз on-link и отвергает обычный маршрут. Лечится флагом onlink - свежая версия скрипта при такой ошибке сама повторяет добавление маршрута с ним, поэтому на Hetzner каскад просто работает. Но сначала убедитесь, что публичный IP действительно на сервере: адрес из ip -4 addr совпадает с curl -s ifconfig.me . Если на интерфейсе приватный адрес ( 10.x , 100.64.x ), не равный внешнему, - это настоящий CGNAT, и вход снаружи недоступен.

IPv6 идёт мимо деления. Схема работает по IPv4. Если IPv6 включён, IPv6-трафик пойдёт в обход маркировки. Держите его выключенным, установщик так и ставит по умолчанию.

DNS уходит через выход. Клиентский DNS по умолчанию - Cloudflare, адреса зарубежные, поэтому сами DNS-запросы идут через сервер-выход, а уже полученные российские адреса - напрямую. Это штатно и на деление не влияет, просто не пугайтесь при диагностике.

Если раздельный выход нужен только вам одному, каскад - перебор: проще задать список AllowedIPs прямо на клиенте (описано в ADVANCED проекта). Каскад оправдан, когда деление хочется держать на сервере и одинаково для всех, кто подключается. В сам установщик каскад намеренно не входит: несколько серверов - другой масштаб, в одном скрипте такому не место. Поэтому и отдельная инструкция.

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

Полный скрипт, пошаговый разбор и частые вопросы - в гайде по каскаду: https://github.com/bivlked/amneziawg-installer/blob/main/CASCADE.md

Зачем всё это и что ещё ломается в быту - в прошлой статье: https://habr.com/ru/articles/1051054/

Сам проект (установщик и управление): https://github.com/bivlked/amneziawg-installer

← Cybersecurity