Свой VPN на одном заграничном сервере хорош, пока не открываешь российские сайты. Часть из них иностранные адреса просто не пускает — банки, Госуслуги, иногда стриминг. Остальные открываются, но с лишним крюком через заграницу и с чужим IP.
В прошлой статье я показал решение в общих чертах — каскад из двух серверов — и отправил за подробностями в гайд. Вопросы по схеме с тех пор приходят регулярно, так что вот полный разбор по шагам: как я собрал каскад и обо что спотыкался. Суть такая: клиент подключается к одному серверу, российский трафик уходит в интернет напрямую с него, а всё остальное — через второй сервер за границей. Одно подключение на телефоне, вся логика деления живёт на сервере. Это тот же сплит-туннелинг, только серверный: клиентам ничего настраивать не нужно.
Про авторство сразу: идею предложил один из пользователей установщика (glfenix на GitHub) в обсуждении проекта, я собрал схему на двух чистых серверах, погонял, поправил пару мест и довёл до пошаговой инструкции. Спасибо ему за разбор.
Что понадобится
-
Два VPS на чистом Debian 12/13 или Ubuntu 24.04/25.10, root на обоих.
-
Вход — в России или рядом: к нему подключаются клиенты, и российские сайты открываются с близкого адреса.
-
Выход — обычный зарубежный VPS.
-
-
Реальный публичный IP на обоих. Сервер за провайдерским NAT (CGNAT) на роль входа не годится — снаружи он недоступен. Быстрая проверка: адрес из
curl -s ifconfig.meсовпадает с адресом на интерфейсе (ip -4 addr). Отдельный случай Hetzner с приватным шлюзом разберу ниже, в граблях. -
IPv6 выключен — установщик так ставит по умолчанию. Каскад работает по IPv4, при включённом IPv6 трафик пойдёт мимо деления.
-
Разные подсети у серверов. У меня вход —
172.16.17.1/24, выход —172.16.61.1/24.
Как каскад делит трафик
Механику я подробно разбирал в прошлой статье, тут коротко, чтобы шаги были понятны. Сервер-вход смотрит на адрес назначения каждого пакета. Адрес в списке российских сетей — пакет уходит в интернет напрямую с входа. Нет — помечается меткой и уезжает через туннель на сервер-выход. Список российских сетей берётся из открытой зоны ipdeny и грузится в ipset. Клиенты добавляются на вход как обычно, manage add имя, на их стороне настраивать нечего.
Дальше — сборка.
Шаг 1. Ставим установщик на оба сервера
Оба сервера — обычные установки amneziawg-installer (он разворачивает AmneziaWG 2.0 с обфускацией и сразу настраивает файрвол). Ставим по основной инструкции и запускаем неинтерактивно с нужными подсетями:
# на сервере-выходе (заграница)bash install_amneziawg.sh --yes --disallow-ipv6 --route-all --subnet=172.16.61.1/24# на сервере-входе (Россия или рядом)bash install_amneziawg.sh --yes --disallow-ipv6 --route-all --subnet=172.16.17.1/24
Установщик сам поднимает пересылку и NAT, так что серверный конфиг дальше править не нужно — скрипт маршрутизации добавит только то, чего нет. На входе доставим два пакета, которых может не быть на минимальном образе:
apt update && apt install -y curl ipset
Шаг 2. На выходе — клиент для входа
Сервер-вход будет подключаться к выходу как обычный клиент. Создаём его на выходе:
bash /root/awg/manage_amneziawg.sh add ru_host
Получаем файл /root/awg/ru_host.conf. Копируем его на вход любым способом (scp, буфер). Внутри — обычный клиентский конфиг: Endpoint (внешний IP выхода) и AllowedIPs = 0.0.0.0/0. Endpoint запомните, он понадобится в скрипте.
Шаг 3. На входе — туннель к выходу
Кладём этот конфиг на вход как /etc/amnezia/amneziawg/awg1.conf и правим две строки:
-
в секцию
[Interface]добавляемTable = off— чтобыawg-quickне прописывал маршруты сам, маршрутизацией займётся наш скрипт; -
строку
DNS = ...убираем, на сервере она не нужна.
Закрываем права и поднимаем туннель:
chmod 600 /etc/amnezia/amneziawg/awg1.confsystemctl start awg-quick@awg1awg show awg1
В выводе awg show awg1 должна появиться строка latest handshake — связь с выходом есть. Пинг внутреннего адреса выхода на этом шаге не пройдёт, и это нормально: маршрут к нему появится в отдельной таблице на следующем шаге.
Шаг 4. Скрипт маршрутизации
Вся логика деления — один bash-скрипт на входе, /root/awg/awg-routing.sh. Он идемпотентный: можно запускать повторно, при каждом запуске он заодно обновляет список российских сетей. Целиком тащить его в статью не буду — в гайде по каскаду он лежит готовый и копируется как есть. Разберу суть по кускам.
Список российских сетей заливается в ipset через временный набор с атомарной подменой, чтобы не было окна, когда набор пуст:
ipset create ru hash:net -exist# ... заливаем сети из скачанного списка во временный набор ru_tmp ...ipset swap ru_tmp ru
Отдельная таблица маршрутизации и правило по метке: помеченный трафик уходит через туннель awg1.
ip route replace default dev awg1 table 100ip rule add fwmark 0x1 table 100 priority 10000
Маркировка трафика клиентов: к российским сетям — напрямую (RETURN до метки), остальное помечаем. Порядок важен, RETURN стоит раньше MARK:
iptables -t mangle -I PREROUTING 1 -i awg0 -s 172.16.17.0/24 -m set --match-set ru dst -j RETURNiptables -t mangle -A PREROUTING -i awg0 -s 172.16.17.0/24 -j MARK --set-mark 0x1
И маршрут к самому выходу держим вне туннеля, иначе пакеты к нему закольцуются в awg1.
Два места в скрипте появились после того, как схема у меня ломалась. Первое: если список российских сетей не скачался и оказался пустым, скрипт останавливается с ошибкой, а не продолжает. Пустой ipset — тихая катастрофа: RETURN не ловит ничего, и весь трафик, включая российский, уходит за границу. Лучше явная ошибка, чем молча сломанное деление. Второе — про /32-шлюз, о нём ниже.
Вписываем в начало скрипта свою клиентскую подсеть и внешний IP выхода, дальше:
chmod +x /root/awg/awg-routing.shbash /root/awg/awg-routing.sh
Шаг 5. Автозапуск после перезагрузки
Главная боль исходной схемы: после ребута маршруты не вставали, всё приходилось поднимать руками. Лечится одним systemd-юнитом, привязанным к обоим туннелям, — он стартует строго после них.
/etc/systemd/system/awg-routing.service:
[Unit]Description=Каскадный сплит-роутингAfter=awg-quick@awg0.service awg-quick@awg1.service network-online.targetRequires=awg-quick@awg0.service awg-quick@awg1.serviceWants=network-online.target[Service]Type=oneshotRemainAfterExit=yesExecStart=/root/awg/awg-routing.sh[Install]WantedBy=multi-user.target
systemctl daemon-reloadsystemctl enable awg-quick@awg1 awg-routing
Requires тут жёсткий: не поднялся туннель к выходу — не запустится и маршрутизация (клиенты подключатся, но без раздельного выхода, весь трафик пойдёт напрямую). Я специально ребутнул вход и проверил: туннели, таблица, список — всё встало само, без единого ручного действия.
Список российских сетей со временем меняется, поэтому раз в неделю юнит стоит перезапускать — заодно обновится и список:
echo '0 5 * * 1 root systemctl restart awg-routing' > /etc/cron.d/awg-routing-refresh
Проверка, что деление работает
Проще всего убедиться по счётчикам. На входе обнуляем их, с клиента создаём оба вида трафика и смотрим:
# на входе: обнулить счётчикиiptables -t mangle -Z PREROUTING# на клиенте: зарубежный сайт (через выход) и российский адрес (напрямую)curl https://ifconfig.me # покажет внешний IP выходаping -c3 77.88.55.242 # российский адрес (Яндекс)# на входе: счётчики покажут само делениеiptables -t mangle -L PREROUTING -n -v# RETURN (match-set ru dst) <- пакеты к РФ выросли (напрямую)# MARK (MARK set 0x1) <- остальные выросли (через выход)
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
ссылка на оригинал статьи https://habr.com/ru/articles/1056220/