# Каскад из двух серверов: раздельный выход для российского и зарубежного трафика [English version](CASCADE.en.md) · [Документация](README.md) · [Сайт проекта](https://bivlked.github.io/amneziawg-installer/ru/) Эта инструкция собирает из двух установок `amneziawg-installer` каскад: клиент подключается к одному серверу, российский трафик уходит в интернет напрямую с этого сервера, а весь остальной - через второй сервер за границей. Так российские сайты открываются с российского IP и без лишнего крюка через заграницу, а зарубежные идут через чистый зарубежный выход. > Основу схемы предложил [@glfenix](https://github.com/glfenix) в [обсуждении #120](https://github.com/bivlked/amneziawg-installer/discussions/120). Я её проверил на стенде, поправил пару мест и оформил в инструкцию. Спасибо за разбор. > Это сценарий для тех, кому нужен раздельный выход. Он сложнее обычной установки на одну коробку и в сам установщик не входит: каскад из нескольких серверов - другой масштаб, в одном скрипте его держать неправильно. Поэтому здесь - отдельная пошаговая инструкция. ## Содержание - [Что это даёт и когда нужно](#what) - [Как это работает](#how) - [Что понадобится](#prereq) - [Шаг 1. Установка пакета на оба сервера](#step1) - [Шаг 2. Сервер AWG1 (выход): клиент для связи](#step2) - [Шаг 3. Сервер AWG0 (вход): туннель к выходу](#step3) - [Шаг 4. Скрипт маршрутизации](#step4) - [Шаг 5. Автозапуск после перезагрузки](#step5) - [Проверка](#verify) - [Обновление списка российских сетей](#update) - [Диагностика](#trouble) - [Безопасность](#security) - [Как убрать каскад](#uninstall) - [Ограничения и нюансы](#limits) ## Что это даёт и когда нужно Обычная установка отправляет в туннель весь трафик клиента - и зарубежный, и российский. Российские сайты в этом случае открываются с зарубежного IP и через лишний крюк, а часть из них вообще не пускает иностранные адреса. Каскад делит трафик по месту назначения: - трафик к российским сетям уходит в интернет напрямую с сервера-входа (российские сайты видят российский IP и открываются быстро); - весь остальной трафик идёт через второй сервер за границей (зарубежные сайты видят зарубежный IP). Когда это полезно: нужен одновременно быстрый доступ к российским ресурсам с российского адреса и зарубежный выход для всего прочего, причём с одного подключения на клиенте. Когда не нужно: если хватает обычной установки на один сервер, или раздельную маршрутизацию проще задать на самом клиенте через список `AllowedIPs` ([split-tunnel в ADVANCED.md](ADVANCED.md#allowedips-adv)). Каскад оправдан, когда деление трафика хочется держать на сервере и одинаково для всех клиентов. ## Как это работает ```mermaid flowchart LR C["Клиент (AmneziaWG)"] -->|"весь трафик"| A0["Сервер AWG0 - РФ, вход"] A0 -->|"к российским сетям - напрямую"| RU["Российские сайты\nвидят IP AWG0"] A0 -->|"остальное - через туннель awg1"| A1["Сервер AWG1 - заграница, выход"] A1 --> WORLD["Зарубежные сайты\nвидят IP AWG1"] ``` - **AWG0 (вход)** - сервер, к которому подключаются клиенты. Он же принимает решение, куда направить трафик. Здесь живёт вся логика каскада. - **AWG1 (выход)** - обычный сервер за границей. AWG0 подключается к нему как клиент (туннель `awg1`). - Список российских сетей берётся из зоны [ipdeny](https://www.ipdeny.com/ipblocks/) и загружается в `ipset`. Трафик к адресам из этого списка идёт напрямую, остальной помечается меткой и уходит в туннель `awg1`. Клиенты добавляются на AWG0 как обычно (`manage add имя`) - никакой особой настройки на стороне клиента не нужно, деление трафика целиком на сервере. ## Что понадобится - **Два VPS** на чистом Debian 13 или Ubuntu 24.04 LTS / 26.04, root-доступ. Debian 12 тоже поддержан, но обычная поддержка у него кончилась, и для новых серверов его брать не стоит. - AWG0 (вход) - лучше в России или рядом, чтобы российские сайты открывались с близкого адреса и без крюка. - AWG1 (выход) - за границей, обычный VPS. - **Одно поколение протокола на обоих серверах.** Установщик по умолчанию ставит AmneziaWG 3.1, но на штатном Debian 12 (ядро 6.1) и на ARM сам ставит 2.0. AWG0 подключается к AWG1 как клиент, поэтому его модуль и утилиты должны понимать поколение AWG1: берите оба сервера на x86_64 с Debian 13 или Ubuntu 24.04/26.04 (там установщик выдаёт 3.1) либо ставьте оба с `--protocol=2.0`. В любом случае сверьте поколение в итоговом отчёте обоих серверов: установщик переходит на 2.0 и сам, например при старых утилитах `awg`. Клиентам, которые подключаются к AWG0, нужно приложение с поддержкой его поколения. - На обоих установлен `amneziawg-installer` (как - ниже). - **Реальный публичный IP на обоих VPS** (не за NAT/CGNAT). Скрипт маршрутизации строит путь к выходу через шлюз по умолчанию, поэтому сервер за провайдерским NAT обычно не подойдёт. Быстрая проверка: внешний адрес из `curl -s ifconfig.me` совпадает с адресом на интерфейсе (`ip -4 addr`) - значит публичный IP именно на сервере. Тревожный признак - именно приватный адрес на самом интерфейсе (`10.x`, `100.64.x`), не равный внешнему: это провайдерский NAT, вход каскада будет недоступен снаружи. А вот публичный `/32` с приватным шлюзом (как у Hetzner) - нормально: скрипт такой случай обрабатывает через `onlink` (см. [Диагностику](#trouble)). - IPv6 выключен **на сервере** (так ставит установщик по умолчанию). Каскад работает по IPv4; при включённом серверном IPv6 трафик пойдёт мимо деления. IPv6 на клиентском устройстве - отдельный разговор, см. [Ограничения](#limits). - На AWG0 дополнительно пакеты `curl` и `ipset` (на минимальном образе их может не быть). Подсети двух серверов должны различаться. В примерах: AWG0 - `172.16.17.1/24`, AWG1 - `172.16.61.1/24`. ## Шаг 1. Установка пакета на оба сервера Поставьте `amneziawg-installer` на каждый сервер по [основной инструкции](README.md#quickstart) и запустите неинтерактивно с нужными подсетями: ```bash # на AWG1 (выход) bash install_amneziawg.sh --yes --disallow-ipv6 --route-all --subnet=172.16.61.1/24 # на AWG0 (вход) bash install_amneziawg.sh --yes --disallow-ipv6 --route-all --subnet=172.16.17.1/24 ``` Установка перезагрузит сервер дважды (с `--yes` без вопроса). После каждой перезагрузки запустите ту же команду снова, закончится она строкой об успешном завершении. Итоговый отчёт называет поколение протокола (строка «Поколение протокола»): на обоих серверах оно должно совпасть. Установщик сам настраивает пересылку и NAT (`iptables -I FORWARD -i awg0 -j ACCEPT` и `MASQUERADE` на внешний интерфейс), а UFW пропускает обратный трафик по состоянию соединения. Поэтому серверный конфиг `awg0.conf` дальше править не нужно - каскадный скрипт добавит только то, чего нет. На AWG0 доставьте недостающие пакеты: ```bash apt update && apt install -y curl ipset ``` ## Шаг 2. Сервер AWG1 (выход): клиент для связи На AWG1 создайте клиента, через которого к нему будет подключаться AWG0: ```bash bash /root/awg/manage_amneziawg.sh add ru_host ``` Скрипт создаст `/root/awg/ru_host.conf`. Скопируйте этот файл на AWG0 любым удобным способом (`scp`, буфер обмена). Внутри он выглядит так: ```ini [Interface] PrivateKey = ... Address = 172.16.61.4/32 DNS = 1.1.1.1, 1.0.0.1 MTU = 1280 Jc = ... (параметры обфускации; у сервера 3.1 ещё HeaderProtectionKey и ContentPaddingAddition) [Peer] PublicKey = ... Endpoint = ВНЕШНИЙ_IP_AWG1:39743 AllowedIPs = 0.0.0.0/0, ::/0 PersistentKeepalive = 33 ``` Запомните `Endpoint` (внешний IP AWG1) - он понадобится в скрипте маршрутизации. ## Шаг 3. Сервер AWG0 (вход): туннель к выходу На AWG0 положите конфиг от AWG1 как `/etc/amnezia/amneziawg/awg1.conf`, добавьте в секцию `[Interface]` строку `Table = off` и уберите строку `DNS = ...` (на сервере она не нужна и тянет лишнюю зависимость). `Table = off` говорит `awg-quick` не добавлять маршруты автоматически - маршрутизацией займётся наш скрипт. Остальное копируйте целиком, не перепечатывая: `S1`-`S4` и `H1`-`H4` (а на сервере 3.1 ещё `HeaderProtectionKey`) обязаны совпадать с AWG1, и без них туннель не поднимется. ```ini [Interface] PrivateKey = ... Address = 172.16.61.4/32 MTU = 1280 Table = off Jc = ... (параметры обфускации; у сервера 3.1 ещё HeaderProtectionKey и ContentPaddingAddition) [Peer] PublicKey = ... Endpoint = ВНЕШНИЙ_IP_AWG1:39743 AllowedIPs = 0.0.0.0/0, ::/0 PersistentKeepalive = 33 ``` `::/0` оставьте как есть: при `Table = off` маршрутов он не добавляет. Закройте права и проверьте, что туннель поднимается: ```bash chmod 600 /etc/amnezia/amneziawg/awg1.conf systemctl start awg-quick@awg1 awg show awg1 ``` В выводе `awg show awg1` должна появиться строка `latest handshake` - значит связь с AWG1 есть. Пинг адреса `172.16.61.1` на этом шаге не пройдёт, и это нормально: маршрут к нему пойдёт через нашу таблицу маршрутизации из следующего шага, а не напрямую. ## Шаг 4. Скрипт маршрутизации Сохраните на AWG0 файл `/root/awg/awg-routing.sh`. Скрипт идемпотентный - его можно запускать повторно (он же обновляет список российских сетей). ```bash #!/bin/bash # awg-routing.sh - каскадный сплит-роутинг на сервере AWG0 (вход). # RU-трафик клиентов идёт напрямую через WAN, остальное - через туннель awg1 на сервер-выход AWG1. # Скрипт идемпотентен: его можно запускать повторно (в т.ч. по расписанию для обновления RU-сетей). set -euo pipefail # ===== параметры (поправь под свою установку) ===== CLIENT_SUBNET="172.16.17.0/24" # подсеть клиентов AWG0 (см. Address в /etc/amnezia/amneziawg/awg0.conf) AWG1_IF="awg1" # имя интерфейса туннеля к серверу-выходу AWG1_ENDPOINT="CHANGE_ME" # внешний IP сервера AWG1 (Endpoint из awg1.conf, без порта) TABLE_ID=100 # номер таблицы маршрутизации для трафика "на выход" FWMARK="0x1" # метка для трафика, уходящего через awg1 RULE_PRIO=10000 # приоритет правила ip rule (нестандартный, чтобы не конфликтовать) RU_ZONE_URL="https://www.ipdeny.com/ipblocks/data/aggregated/ru-aggregated.zone" RU_ZONE_FALLBACK_URL="https://raw.githubusercontent.com/bivlked/amneziawg-installer/v6.1.1/cascade/ru.zone" AWG_DIR="/root/awg" EXTRA_RU_NETS="" # свои адреса/сети, которые вести через РФ (через пробел), # напр. "203.0.113.7 198.51.100.0/24". Нужно для .ru-сайтов # за зарубежным CDN: по адресу они выглядят как заграница # ================================================== RU_ZONE="$AWG_DIR/ru.zone" mkdir -p "$AWG_DIR" # Блокировка: два одновременных запуска мешают друг другу. Оба наперегонки подменяют один и тот же # набор ru через ru_tmp, и проигравший падает с "The set with the given name does not exist"; вдобавок # оба пишут в общий "$RU_ZONE.tmp", а trap первого удаляет файл второго. Типичный случай - юнит # awg-routing стартует после перезагрузки, а вы тут же запускаете скрипт руками (issue #212). # Здесь второй экземпляр ЖДЁТ, а не ломается. exec 9>"$AWG_DIR/awg-routing.lock" flock -w 300 9 || { echo "ERROR: другой экземпляр awg-routing.sh не завершился за 5 минут - прерываюсь" >&2; exit 1; } trap 'rm -f "$RU_ZONE.tmp"' EXIT # не оставлять временный файл списка при выходе/прерывании [ "$AWG1_ENDPOINT" != "CHANGE_ME" ] || { echo "ERROR: впиши AWG1_ENDPOINT (внешний IP сервера AWG1)" >&2; exit 1; } # Определяем выход к AWG1 по самому маршруту до endpoint (надёжнее парсинга default - работает и на # on-link/point-to-point/multi-homed). На первом запуске это путь через WAN (туннель ещё не вмешивается). EP_ROUTE="$(ip -4 route get "$AWG1_ENDPOINT" 2>/dev/null | head -1)" WAN_IF="$(printf '%s' "$EP_ROUTE" | grep -oP '\bdev \K\S+' || true)" WAN_GW="$(printf '%s' "$EP_ROUTE" | grep -oP '\bvia \K\S+' || true)" { [ -n "$WAN_IF" ] && [ "$WAN_IF" != "$AWG1_IF" ]; } \ || { echo "ERROR: не удалось определить WAN-интерфейс к AWG1 (или он указывает в туннель)" >&2; exit 1; } # Предупреждение про IPv6: схема только для IPv4, при включённом IPv6 он пойдёт мимо деления. if [ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null || echo 1)" = "0" ] \ && ip -6 route show default 2>/dev/null | grep -q .; then echo "WARN: на сервере включён IPv6 - IPv6-трафик пойдёт мимо каскада. Отключи IPv6 (установщик: --disallow-ipv6)." >&2 fi # 1) Обновить список RU-сетей. Источники по порядку: ipdeny (актуальный) -> снимок в репозитории # (если ipdeny недоступен) -> уже лежащий локальный список. Рабочий файл заменяем только при # успешной непустой загрузке, поэтому сорванная закачка не обнулит прежний список. fetch_ru_zone() { # $1 = URL; качает и ПРОВЕРЯЕТ содержимое, 0 при успехе # Таймауты обязательны: скрипт запускается юнитом при загрузке, а у юнита ограничено время. # Без них зависшая закачка съела бы весь бюджет, и systemd убил бы юнит до применения правил. # --retry-max-time ограничивает СУММУ попыток, иначе три повтора по 30 с дают 93 с на один URL. curl -fsS --retry 2 --retry-max-time 45 --connect-timeout 10 --max-time 30 \ -o "$RU_ZONE.tmp" "$1" || return 1 # 🔴 Проверять надо СОДЕРЖИМОЕ, а не только непустоту. curl -f отсекает только код >= 400, # поэтому заглушка провайдера, страница DPI или капча, отданные с кодом 200, прошли бы как # валидный список, затёрли рабочий файл и сломали деление до ручного вмешательства. local valid total valid=$(grep -cE '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$' "$RU_ZONE.tmp" || true) # Считаем НЕПУСТЫЕ строки, а не все: иначе отсутствие завершающего перевода строки или # пустая строка в конце отвергали бы годный список, хотя загрузчик ниже их спокойно терпит. total=$(grep -c '[^[:space:]]' "$RU_ZONE.tmp" || true) if [ "$valid" -lt 1000 ] || [ "$valid" -ne "$total" ]; then echo "WARN: $1 отдал не список RU-сетей (валидных строк $valid из $total) - не беру" >&2 return 1 fi } if fetch_ru_zone "$RU_ZONE_URL"; then mv -f "$RU_ZONE.tmp" "$RU_ZONE" elif fetch_ru_zone "$RU_ZONE_FALLBACK_URL"; then mv -f "$RU_ZONE.tmp" "$RU_ZONE" echo "WARN: ipdeny недоступен - взял снимок RU-сетей из репозитория (может немного отставать от актуального)" >&2 else echo "WARN: не удалось скачать список ни с ipdeny, ни из репозитория - использую прежний локальный" >&2 fi # Без списка не продолжаем: пустой ipset отправит ВЕСЬ трафик за границу (деление молча сломается). [ -s "$RU_ZONE" ] || { echo "ERROR: список RU-сетей пуст и нигде не найден - прерываюсь, чтобы не сломать деление" >&2; exit 1; } # 2) Загрузить RU-сети в ipset через временный сет (атомарная замена, без "пустого окна"). ipset create ru hash:net -exist ipset create ru_tmp hash:net -exist ipset flush ru_tmp while read -r net; do [ -n "$net" ] && ipset add ru_tmp "$net" -exist done < "$RU_ZONE" # Свои адреса добавляем в тот же временный набор ДО подмены - тогда они переживают запуск скрипта. # Дописывать в готовый ru бесполезно: следующий запуск соберёт набор заново и затрёт добавленное. # set -f отключает раскрытие шаблонов: без него звёздочка в переменной превратилась бы в имена файлов. # Ошибочная запись НЕ роняет скрипт: при запуске из юнита это оставило бы сервер без деления вовсе, # а так теряется только само исключение, и об этом сказано в предупреждении. extra_total=0; extra_bad=0 set -f for net in $EXTRA_RU_NETS; do extra_total=$((extra_total + 1)) ipset add ru_tmp "$net" -exist || { echo "WARN: EXTRA_RU_NETS: не добавил '$net' - ожидается адрес или сеть в формате CIDR" >&2 extra_bad=$((extra_bad + 1)) } done set +f ipset swap ru_tmp ru ipset destroy ru_tmp # 3) Таблица + правило: помеченный трафик уходит на выход через awg1 (по номеру таблицы, rt_tables не нужен). ip route replace default dev "$AWG1_IF" table "$TABLE_ID" ip rule del fwmark "$FWMARK" table "$TABLE_ID" 2>/dev/null || true ip rule add fwmark "$FWMARK" table "$TABLE_ID" priority "$RULE_PRIO" # 4) Маршрут к самому AWG1 держим вне туннеля (иначе пакеты к нему закольцуются в awg1). replace = идемпотентно. if [ -n "$WAN_GW" ]; then # На VPS со шлюзом вне подсети сервера (напр. Hetzner, интерфейс /32) обычный replace падает # с "Nexthop has invalid gateway" - тогда повторяем с onlink (шлюз доступен прямо на интерфейсе). ip route replace "$AWG1_ENDPOINT" via "$WAN_GW" dev "$WAN_IF" 2>/dev/null \ || ip route replace "$AWG1_ENDPOINT" via "$WAN_GW" dev "$WAN_IF" onlink else ip route replace "$AWG1_ENDPOINT" dev "$WAN_IF" fi # Пересылку (FORWARD) и NAT прямого RU-трафика (-o WAN) уже настроил установщик в awg0.conf, # обратный трафик пропускает UFW по RELATED,ESTABLISHED. Здесь добавляем только маркировку и NAT на awg1. # 5) Маркировка трафика клиентской подсети, входящего через awg0: RU-сети напрямую (RETURN), остальное метим. # -s CLIENT_SUBNET - чтобы не задеть другой трафик, если позже добавишь ещё подсеть/пир. # RETURN должен стоять перед MARK; -I ... 1 держит его первым, -C делает шаг идемпотентным. iptables -t mangle -C PREROUTING -i awg0 -s "$CLIENT_SUBNET" -m set --match-set ru dst -j RETURN 2>/dev/null \ || iptables -t mangle -I PREROUTING 1 -i awg0 -s "$CLIENT_SUBNET" -m set --match-set ru dst -j RETURN iptables -t mangle -C PREROUTING -i awg0 -s "$CLIENT_SUBNET" -j MARK --set-mark "$FWMARK" 2>/dev/null \ || iptables -t mangle -A PREROUTING -i awg0 -s "$CLIENT_SUBNET" -j MARK --set-mark "$FWMARK" # 6) NAT для трафика клиентов, уходящего на выход через awg1 (для прямого RU NAT -o WAN уже есть от установщика). iptables -t nat -C POSTROUTING -s "$CLIENT_SUBNET" -o "$AWG1_IF" -j MASQUERADE 2>/dev/null \ || iptables -t nat -A POSTROUTING -s "$CLIENT_SUBNET" -o "$AWG1_IF" -j MASQUERADE # Финальная строка не должна лгать: если часть своих адресов не добавилась, они пойдут # в дальнее плечо, и молчаливое "OK" увело бы разбор не туда. if [ "$extra_bad" -gt 0 ]; then echo "OK С ОГОВОРКОЙ: каскад применён, но из EXTRA_RU_NETS не добавлено $extra_bad из $extra_total - эти адреса пойдут через заграничное плечо" >&2 else echo "OK: каскадный роутинг применён (WAN=$WAN_IF, gw=${WAN_GW:-on-link}, выход=$AWG1_IF, table=$TABLE_ID)" fi ``` Впишите в начале свои `CLIENT_SUBNET` и `AWG1_ENDPOINT`, сделайте файл исполняемым и запустите. `CLIENT_SUBNET` - это сеть (с нулём на конце), а не адрес сервера: если в `awg0.conf` стоит `Address = 172.16.17.1/24`, то `CLIENT_SUBNET="172.16.17.0/24"`. `AWG1_ENDPOINT` - внешний IP сервера AWG1 (из `Endpoint` в `awg1.conf`, без порта). `EXTRA_RU_NETS` заполнять не обязательно, он для отдельных адресов, которые надо вести через российское плечо вопреки списку. Типичный случай - сайт в зоне `.ru` за зарубежным CDN: по адресу назначения он выглядит как заграница, поэтому уходит в дальнее плечо. Держите такие адреса именно здесь: добавлять их в готовый набор `ru` руками бесполезно, следующий запуск скрипта соберёт набор заново и затрёт добавленное. ```bash chmod +x /root/awg/awg-routing.sh bash /root/awg/awg-routing.sh ``` Что делает скрипт по шагам: 0. Берёт блокировку `/root/awg/awg-routing.lock`. Если скрипт уже работает (например, юнит `awg-routing` доканчивает запуск после перезагрузки), второй экземпляр ждёт до 5 минут и отрабатывает следом, а не ломается на полпути. 1. Скачивает список российских сетей во временный файл и подменяет рабочий только при успешной загрузке. Источники по очереди: ipdeny (актуальный список), при его недоступности - снимок из этого репозитория (`cascade/ru.zone`), затем уже лежащий локальный список. Сорванная закачка не обнулит уже загруженный список. 2. Загружает сети в `ipset` через временный набор, добавляет туда же ваши `EXTRA_RU_NETS` и атомарно подменяет рабочий (`ipset swap`) - без окна, когда набор пустой. 3. Создаёт таблицу маршрутизации для помеченного трафика и правило `ip rule` по метке. Таблица задаётся номером, файл `rt_tables` не нужен. 4. Прокладывает маршрут к самому AWG1 вне туннеля, иначе пакеты к нему закольцуются. 5. Метит трафик: к российским сетям - напрямую (`RETURN`), остальное - меткой для ухода в `awg1`. 6. Добавляет NAT для трафика, уходящего через `awg1`. ## Шаг 5. Автозапуск после перезагрузки Сервер `awg0` установщик поднимает и включает сам. Туннель `awg1` (сервис `awg-quick@awg1`) и скрипт маршрутизации ваши, их включаем здесь. Осталось, чтобы после обоих туннелей отрабатывал скрипт маршрутизации. Это делает один oneshot-юнит, который привязан к обоим туннелям и поэтому стартует строго после них - отдельный порядок между `awg0` и `awg1` задавать не нужно. Создайте `/etc/systemd/system/awg-routing.service`: ```ini [Unit] Description=Каскадный сплит-роутинг для AWG0 After=awg-quick@awg0.service awg-quick@awg1.service network-online.target Requires=awg-quick@awg0.service awg-quick@awg1.service Wants=network-online.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=/root/awg/awg-routing.sh # TimeoutStartSec здесь намеренно НЕ задан. У Type=oneshot таймаут запуска по умолчанию # отключён (проверено: systemctl show -p TimeoutStartUSec на таком юните отдаёт infinity, # тогда как у Type=simple там менеджерские 90 секунд). Поэтому ожидание блокировки до 5 минут # и медленная заливка списка на слабой машине юнит не убивают, а любое конечное значение, # наоборот, ввело бы убийство на середине и оставило деление применённым наполовину. [Install] WantedBy=multi-user.target ``` Включите автозапуск (`awg-quick@awg0` установщик уже включил): ```bash systemctl daemon-reload systemctl enable awg-quick@awg1 awg-routing ``` `Requires` здесь жёсткий: если `awg1` не поднимется, скрипт маршрутизации не запустится (клиенты при этом всё равно подключатся к `awg0`, но без раздельного выхода - весь трафик пойдёт напрямую). Теперь после перезагрузки туннели и маршрутизация поднимаются сами, руками ничего трогать не нужно. ## Проверка На AWG0 убедитесь, что правило, таблица и список на месте: ```bash ip rule show | grep fwmark # 10000: from all fwmark 0x1 lookup 100 ip route show table 100 # default dev awg1 scope link ipset list ru | grep "Number of entries" # Number of entries: 8600+ (число российских сетей) # российский адрес действительно в списке ipset test ru 77.88.55.242 # Warning: 77.88.55.242 is in set ru. # свои адреса из EXTRA_RU_NETS проверяются так же, поимённо ipset test ru 203.0.113.7 ``` Если вместо числа записей вы увидели `ipset v7.17: The set with the given name does not exist`, набора `ru` в ядре нет, а без него деление не работает. Сама команда говорит только о текущем отсутствии набора и не рассказывает, почему так вышло: набор мог не создаться из-за оборванного запуска скрипта, а мог и не появиться вовсе, если после перезагрузки юнит `awg-routing` не стартовал. Оборванный запуск при этом задевает больше, чем кажется: блок `ipset` идёт раньше маршрутов, поэтому в нём не применялись ни таблица, ни маркировка, ни NAT. При этом `ip rule` и `ip route show table 100` могут показывать всё как надо - они держатся в ядре от прошлого удачного запуска и о текущем состоянии ничего не говорят. Проверьте юнит (`systemctl status awg-routing`), запустите скрипт ещё раз и дочитайте вывод до конца. Успешный запуск заканчивается строкой `OK: каскадный роутинг применён`. Если часть ваших адресов из `EXTRA_RU_NETS` не принялась, последней будет строка `OK С ОГОВОРКОЙ` с числом непринятых адресов - каскад при этом применён, но именно эти адреса пойдут через заграничное плечо. Проверьте, что трафик расходится правильно: ```bash # к российскому адресу (Яндекс) - напрямую через внешний интерфейс (часть "via ШЛЮЗ" зависит от хостера) ip route get 77.88.55.242 # 77.88.55.242 via ШЛЮЗ dev eth0 ... # к зарубежному адресу с меткой - через туннель awg1 ip route get 1.1.1.1 mark 0x1 # 1.1.1.1 dev awg1 table 100 ... ``` Самая наглядная проверка - со счётчиками. Обнулите их, затем с клиента, подключённого к AWG0, создайте оба вида трафика и сравните: ```bash # на AWG0: обнулить счётчики iptables -t mangle -Z PREROUTING # на клиенте: зарубежный сайт (через выход AWG1) и российский (напрямую) curl https://ifconfig.me # покажет ВНЕШНИЙ_IP_AWG1 (сервер-выход) ping -c3 77.88.55.242 # российский адрес (Яндекс) # на AWG0: счётчики покажут само деление iptables -t mangle -L PREROUTING -n -v # RETURN ... match-set ru dst <- пакеты к РФ выросли (напрямую) # MARK ... MARK set 0x1 <- остальные выросли (через awg1) ``` ## Обновление списка российских сетей Список российских сетей со временем меняется. Скрипт перечитывает его при каждом запуске, поэтому достаточно периодически его перезапускать. Например, раз в неделю через cron: ```bash echo '0 5 * * 1 root systemctl restart awg-routing' > /etc/cron.d/awg-routing-refresh ``` Наложения планового запуска на ручной можно не опасаться: скрипт берёт блокировку, и тот, кто пришёл вторым, дождётся своей очереди. ### Вариант с systemd timer > Предложил [@MrSokol](https://github.com/MrSokol) в [обсуждении #120](https://github.com/bivlked/amneziawg-installer/discussions/120). Если не хотите держать на сервере cron, то же самое делает таймер systemd. Он не лучше и не хуже строки в cron, просто расписание лежит рядом с юнитом. 🔴 **Одна обязательная правка, без которой обновление не будет происходить никогда.** В `awg-routing.service` поменяйте `RemainAfterExit=yes` на `RemainAfterExit=no`. Таймер запускает юнит командой `start`, а `start` уже активного юнита systemd считает выполненным и ничего не делает. С `RemainAfterExit=yes` юнит после первой же отработки остаётся `active (exited)` навсегда, поэтому таймер будет срабатывать вхолостую, причём совершенно незаметно: `systemctl list-timers` покажет свежий `LAST`, а в журнале от момента срабатывания не окажется ни строки (проверено на systemd 255). Создайте `/etc/systemd/system/awg-routing.timer`: ```ini [Unit] Description=Периодическое обновление списка российских сетей [Timer] OnCalendar=*-*-* 01:00:00 RandomizedDelaySec=15m Persistent=false Unit=awg-routing.service [Install] WantedBy=timers.target ``` Примените и включите: ```bash systemctl daemon-reload systemctl restart awg-routing systemctl enable --now awg-routing.timer systemctl list-timers awg-routing.timer ``` `systemctl restart awg-routing` в середине не для красоты. Если каскад уже работал, юнит прямо сейчас находится в состоянии `active (exited)`, и `daemon-reload` этого не меняет: он перечитывает файл, но состояние уже запущенного юнита оставляет как есть. То есть без явного перезапуска таймер продолжит срабатывать вхолостую до следующей перезагрузки сервера, ровно как описано выше. `restart` заодно сразу обновляет список, так что вы увидите, что схема работает, а не будете ждать ночи. `Persistent=false` стоит намеренно, догонять пропущенный запуск не нужно. Юнит `awg-routing` включён в автозапуск (`WantedBy=multi-user.target`, вы включали его вместе с `awg-quick@awg1`), поэтому список обновляется при каждой загрузке сервера ещё до всякого таймера. С включённым `Persistent` вы получили бы вдобавок внеплановый прогон сразу после загрузки, то есть лишнее обращение к источнику списка. Спасибо [@MrSokol](https://github.com/MrSokol) за поправку. Если вы включили только таймер, а сервис в автозапуск не добавляли, сначала добавьте: без загрузочного прогона пропущенное срабатывание действительно теряется, и вот тогда `Persistent` был бы нужен. Отдельно имейте в виду, что `Requires=` на оба туннеля жёсткий: если `awg1` при старте не поднялся, загрузочного прогона тоже не будет, и здесь ни таймер, ни `Persistent` не помогут. `RandomizedDelaySec` разбрасывает обращения во времени, чтобы все, кто настроился по этому гайду, не приходили за списком в одну и ту же секунду. И имейте в виду: с `RemainAfterExit=no` команда `systemctl status awg-routing` между запусками показывает `inactive (dead)`. Это нормально, отработавший oneshot-юнит активным не остаётся, а маршруты при этом на месте. Смотрите на строку последнего запуска в том же выводе или на `systemctl list-timers`. ## Диагностика - **Все сайты, включая российские, идут через заграницу.** Проверьте, что список загрузился: `ipset list ru | grep "Number of entries"` должно быть не ноль. Если `www.ipdeny.com` заблокирован у вашего провайдера, скрипт сам возьмёт снимок сетей из репозитория (`cascade/ru.zone`) - в его выводе будет строка про репозиторий. Ноль записей значит, что не сработал ни один источник: проверьте доступ к `www.ipdeny.com` и к `raw.githubusercontent.com` и запустите скрипт ещё раз. - **Туннель к AWG1 не поднимается** (`awg show awg1` без `latest handshake`). Проверьте `Endpoint` в `awg1.conf`, что порт AWG1 открыт, и что сам сервис на AWG1 работает (`systemctl status awg-quick@awg0` на AWG1). - **После перезагрузки каскад не работает.** Проверьте автозапуск: `systemctl is-enabled awg-quick@awg1 awg-routing` (оба `enabled`) и что юнит `awg-routing.service` отработал после перезагрузки (`systemctl status awg-routing`). - **Конкретный российский сайт всё равно открывается через заграницу.** Скорее всего он размещён не на российском IP (часто бывает с сайтами за Cloudflare и зарубежными CDN). Деление идёт по IP назначения, поэтому такой сайт уходит в туннель - это ожидаемо. - **YouTube тормозит и буферизует, Google Play не загружает приложения, а сервисы Google отдают российскую выдачу - хотя выход у каскада зарубежный.** Часть узлов Google и YouTube - их кэши и CDN - стоит на российских IP и попадает в список RU. Деление идёт по адресу назначения, поэтому соединение с таким узлом уходит напрямую через вход, и Google видит российский адрес. Подмена DNS тут надёжно не лечит: сети Google во многом общие и анонсируются из разных мест, часть адресов всё равно остаётся российской. Чтобы Google и YouTube всегда шли через зарубежный выход, нужен актуальный список их сетей (не только AS15169 - кэш-узлы GGC часто лежат в сетях провайдеров): пометьте их ДО правила RETURN или вовсе исключите из набора ru - тогда они уйдут через выход, а не напрямую. Минус: весь Google при этом идёт через выход всегда. - **Российский сайт открывается с российского адреса, но всё равно ведёт себя так, будто вы под VPN.** Адрес посетителя сайт может узнать не из соединения, а от скрипта на странице, который спрашивает его у стороннего сервиса определения IP. Такого сервиса в наборе `ru` почти наверняка нет, поэтому запрос к нему уйдёт через зарубежный выход, и скрипт сообщит сайту адрес AWG1. У сайта окажется два разных адреса, и расхождение само по себе признак. Домашний адрес при этом не раскрывается - оба адреса ваши серверные, если только IPv6 устройства не идёт мимо туннеля (см. [Ограничения](#limits)). Дописывать сервисы определения адреса в набор `ru` - путь ненадёжный: их адреса зависят от резолвера и меняются сами. Разбор с цифрами - в [ADVANCED.md](ADVANCED.md#split-detect-adv). - **Отдача (upload) намного ниже приёма (download).** Если приём идёт нормально, а отдача проседает почти в ноль, дело обычно не в MTU (он режет сегменты в обе стороны, тогда страдал бы и приём) и не в процессоре (он бы просадил обе стороны разом). Живой приём доказывает, что вход AWG0 отдаёт клиенту на скорости; узкое место - именно плечо AWG0 -> AWG1, по которому уходит отдача наружу (приём его не использует). Кандидаты: шейпинг исходящего у хостера входного сервера в сторону AWG1, кривой пиринг до сети выхода, либо входной лимит на самом AWG1. Замерьте это плечо напрямую: на AWG1 запустите `iperf3 -s`, с AWG0 - `iperf3 -c ` (отдача AWG0 -> AWG1) и `iperf3 -c -R` (приём). Низкая первая цифра при нормальной второй подтверждает, что душит участок AWG0 -> AWG1, а не сами серверы. - **Скрипт `awg-routing.sh` падает с `Error: Nexthop has invalid gateway`.** Значит шлюз по умолчанию лежит вне подсети сервера: на интерфейсе `/32`-адрес, а шлюз где-то снаружи - ядро не считает такой шлюз on-link и отвергает маршрут. Свежая версия скрипта это уже обрабатывает: при такой ошибке повторяет добавление маршрута с флагом `onlink`, поэтому на VPS вроде Hetzner (у них реальный публичный IP на `/32` и приватный шлюз типа `172.31.1.1`) каскад просто работает - обновите `awg-routing.sh` из гайда, если копия старая. Но сначала убедитесь, что у сервера действительно есть публичный IP: адрес из `ip -4 addr` должен совпадать с `curl -s ifconfig.me`. Если на интерфейсе приватный адрес (`10.x`, `100.64.x` CGNAT), не равный вашему внешнему IP, сервер за провайдерским NAT - тогда вход каскада снаружи недоступен, и нужен реальный публичный IP на обоих серверах (см. [Что понадобится](#prereq)). ## Безопасность - Не выключайте UFW целиком (`ufw disable`) ради скорости - на пропускную способность фаервол практически не влияет, основную нагрузку дают шифрование и пересылка. Оставьте минимум: пересылку и нужные порты, остальное закрыто. - Сервер-вход AWG0 видит весь трафик клиентов до ухода в `awg1` - это точка доверия, держите к нему доступ под контролем. ## Как убрать каскад На AWG0 подставьте свои значения из начала `/root/awg/awg-routing.sh`: `CLIENT_SUBNET` и `AWG1_ENDPOINT`, а если меняли, то и `AWG1_IF`, `TABLE_ID`, `FWMARK`. Затем выполните: ```bash CLIENT_SUBNET="172.16.17.0/24" AWG1_IF="awg1" AWG1_ENDPOINT="ВНЕШНИЙ_IP_AWG1" TABLE_ID=100 FWMARK="0x1" systemctl disable --now awg-routing.timer 2>/dev/null systemctl disable --now awg-routing "awg-quick@$AWG1_IF" rm -f /etc/systemd/system/awg-routing.service /etc/systemd/system/awg-routing.timer /etc/cron.d/awg-routing-refresh systemctl daemon-reload iptables -t mangle -D PREROUTING -i awg0 -s "$CLIENT_SUBNET" -m set --match-set ru dst -j RETURN iptables -t mangle -D PREROUTING -i awg0 -s "$CLIENT_SUBNET" -j MARK --set-mark "$FWMARK" iptables -t nat -D POSTROUTING -s "$CLIENT_SUBNET" -o "$AWG1_IF" -j MASQUERADE ip rule del fwmark "$FWMARK" table "$TABLE_ID" 2>/dev/null ip route del default dev "$AWG1_IF" table "$TABLE_ID" 2>/dev/null ip route del "$AWG1_ENDPOINT" 2>/dev/null ipset destroy ru_tmp 2>/dev/null ipset destroy ru rm -f "/etc/amnezia/amneziawg/$AWG1_IF.conf" /root/awg/awg-routing.sh /root/awg/ru.zone /root/awg/ru.zone.tmp /root/awg/awg-routing.lock ``` Правила `iptables` удаляются до `ipset destroy`: пока на набор `ru` ссылается правило, удалить его нельзя. Строка с `ru_tmp` убирает временный набор, если скрипт когда-то прервали на середине. Строки для таймера и cron безвредны, если у вас был только один из вариантов. Если вы добавляли свои правила сверх скрипта, например для Google из раздела [Диагностика](#trouble), уберите и их. На AWG1 удалите клиента, которого выпускали для AWG0: ```bash bash /root/awg/manage_amneziawg.sh remove ru_host ``` Если потом собираетесь удалять и сам установщик (`--uninstall`), сначала выполните шаги выше. Установщик останавливает только `awg0`, про `awg1`, юнит и таймер каскада он не знает, а `/etc/amnezia` и `/root/awg` удаляет целиком. Пока поднят `awg1`, модуль не выгрузится, а после перезагрузки включённые юниты каскада остались бы без своих файлов. ## Ограничения и нюансы - Каскад не входит в установщик и не управляется им - это отдельная ручная настройка поверх двух обычных установок. - Схема работает по IPv4: список российских сетей IPv6-записей не содержит вовсе. Держите IPv6 выключенным на сервере (установщик так и ставит по умолчанию), иначе серверный IPv6-трафик пойдёт мимо деления. IPv6 на клиентском устройстве - отдельный уровень, установщик его не трогает. При установке ровно по флагам из [шага 1](#step1) (полный туннель, без `--allow-ipv6-tunnel`) он уходит в туннель вместе с остальным трафиком и наружу не идёт, а вот с раздельной маршрутизацией на клиенте пошёл бы мимо, со своим настоящим адресом. Проверка с устройства: `curl -6 ifconfig.co` - вернулся домашний адрес, значит IPv6 идёт мимо туннеля. Подробнее - в [ADVANCED.md](ADVANCED.md#split-detect-adv). - Деление по адресу назначения видно снаружи: сайт может сравнить адрес соединения с тем, что сообщит скрипт на его странице, и увидеть расхождение. Это свойство любой схемы с делением, а не дефект каскада - см. [Диагностику](#trouble) и разбор в [ADVANCED.md](ADVANCED.md#split-detect-adv). - Отдельно настраивать MTU не нужно: двойная инкапсуляция укладывается в стандартный размер, большие передачи проходят без потерь (замерено на серверах 2.0; для 3.1 с его добавочным паддингом это верно по расчёту, отдельного замера пока не было). - DNS-запросы клиента идут через зарубежный выход - это не влияет на работу и не создаёт утечки. - Переустановка установщика с `--force` каскад не ломает: его логика живёт в `/root/awg/awg-routing.sh` и юните `awg-routing`, а не в `awg0.conf`. Но шаг 7 перезапускает `awg-quick@awg0`, а шаг 4 заново наливает правила UFW, поэтому после переустановки прогоните `bash /root/awg/awg-routing.sh` ещё раз (он для того и сделан идемпотентным) и проверьте `ip rule`. Правила UFW, если добавляли свои руками, стоит перепроверить там же. - Поколение протокола переустановкой с `--force` не меняется: для этого нужны `--uninstall` и новая установка, а с ними меняются и ключи. Если так пересобран AWG1, выпустите `ru_host` заново ([шаг 2](#step2)) и замените `awg1.conf` на AWG0 ([шаг 3](#step3)). Если пересобран AWG0, вдобавок выпустите заново профили всех его клиентов и пройдите шаги 3-5 ещё раз. --- Схему предложил [@glfenix](https://github.com/glfenix) в [#120](https://github.com/bivlked/amneziawg-installer/discussions/120). Вопросы и улучшения - туда же или в [Issues](https://github.com/bivlked/amneziawg-installer/issues).