Release v6.0.1. The changes themselves were merged in #353-#356, #358 and #360; this PR carries the version bump, the date and the signatures. The full list is in the `[6.0.1]` section of both changelogs. What the release brings: - `--jc` takes 0 to 122, so the junk packets, `I1`-`I5` and the handshake fit the 128-packet batch of amneziawg-go clients (amneziawg-go#111). A running server with a large `Jc` is not refused: `check`, `diagnose`, `restore`, step 6 of the installer and `add`/`regen` warn and explain how to lower it. - The `--diagnostic` header, `--help` and the fallback warning no longer tie the installer to 2.0; the kernel check and the validation summary name the generation actually set up. - README, INSTALL_VPS and ADVANCED start from the 3.1 path; cascade, WARP, contributor and release docs follow. - The client table and the router notes are updated to the 15 October 2026 snapshot, with MTU advice for the 3.1 profile. The version cascade: - `SCRIPT_VERSION`, `AWG_COMMON_VERSION`, the `# Version:` and `# Date:` headers of the six scripts: 6.0.1, 2026-10-18. - The four helper SHA pins recomputed for the new bytes. - The installer version badge and the facts block in both READMEs, the pinned raw URLs in ADVANCED and CASCADE (both languages). - Both changelogs: `[Unreleased]` becomes `[6.0.1] - 2026-10-18` with a one-line summary, a new empty `[Unreleased]` above it, and the compare links. - ADVANCED (both languages) lists `restore` and step 6 of the installer among the commands that warn about `Jc`. - The six release signatures for v6.0.1.
56 KiB
Каскад из двух серверов: раздельный выход для российского и зарубежного трафика
English version · Документация · Сайт проекта
Эта инструкция собирает из двух установок amneziawg-installer каскад: клиент подключается к одному серверу, российский трафик уходит в интернет напрямую с этого сервера, а весь остальной - через второй сервер за границей. Так российские сайты открываются с российского IP и без лишнего крюка через заграницу, а зарубежные идут через чистый зарубежный выход.
Основу схемы предложил @glfenix в обсуждении #120. Я её проверил на стенде, поправил пару мест и оформил в инструкцию. Спасибо за разбор.
Это сценарий для тех, кому нужен раздельный выход. Он сложнее обычной установки на одну коробку и в сам установщик не входит: каскад из нескольких серверов - другой масштаб, в одном скрипте его держать неправильно. Поэтому здесь - отдельная пошаговая инструкция.
Содержание
- Что это даёт и когда нужно
- Как это работает
- Что понадобится
- Шаг 1. Установка пакета на оба сервера
- Шаг 2. Сервер AWG1 (выход): клиент для связи
- Шаг 3. Сервер AWG0 (вход): туннель к выходу
- Шаг 4. Скрипт маршрутизации
- Шаг 5. Автозапуск после перезагрузки
- Проверка
- Обновление списка российских сетей
- Диагностика
- Безопасность
- Как убрать каскад
- Ограничения и нюансы
Что это даёт и когда нужно
Обычная установка отправляет в туннель весь трафик клиента - и зарубежный, и российский. Российские сайты в этом случае открываются с зарубежного IP и через лишний крюк, а часть из них вообще не пускает иностранные адреса.
Каскад делит трафик по месту назначения:
- трафик к российским сетям уходит в интернет напрямую с сервера-входа (российские сайты видят российский IP и открываются быстро);
- весь остальной трафик идёт через второй сервер за границей (зарубежные сайты видят зарубежный IP).
Когда это полезно: нужен одновременно быстрый доступ к российским ресурсам с российского адреса и зарубежный выход для всего прочего, причём с одного подключения на клиенте.
Когда не нужно: если хватает обычной установки на один сервер, или раздельную маршрутизацию проще задать на самом клиенте через список AllowedIPs (split-tunnel в ADVANCED.md). Каскад оправдан, когда деление трафика хочется держать на сервере и одинаково для всех клиентов.
Как это работает
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 и загружается в
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(см. Диагностику). - IPv6 выключен на сервере (так ставит установщик по умолчанию). Каскад работает по IPv4; при включённом серверном IPv6 трафик пойдёт мимо деления. IPv6 на клиентском устройстве - отдельный разговор, см. Ограничения.
- На AWG0 дополнительно пакеты
curlиipset(на минимальном образе их может не быть).
Подсети двух серверов должны различаться. В примерах: AWG0 - 172.16.17.1/24, AWG1 - 172.16.61.1/24.
Шаг 1. Установка пакета на оба сервера
Поставьте amneziawg-installer на каждый сервер по основной инструкции и запустите неинтерактивно с нужными подсетями:
# на 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 доставьте недостающие пакеты:
apt update && apt install -y curl ipset
Шаг 2. Сервер AWG1 (выход): клиент для связи
На AWG1 создайте клиента, через которого к нему будет подключаться AWG0:
bash /root/awg/manage_amneziawg.sh add ru_host
Скрипт создаст /root/awg/ru_host.conf. Скопируйте этот файл на AWG0 любым удобным способом (scp, буфер обмена). Внутри он выглядит так:
[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, и без них туннель не поднимется.
[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 маршрутов он не добавляет.
Закройте права и проверьте, что туннель поднимается:
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. Скрипт идемпотентный - его можно запускать повторно (он же обновляет список российских сетей).
#!/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.0.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 руками бесполезно, следующий запуск скрипта соберёт набор заново и затрёт добавленное.
chmod +x /root/awg/awg-routing.sh
bash /root/awg/awg-routing.sh
Что делает скрипт по шагам:
- Берёт блокировку
/root/awg/awg-routing.lock. Если скрипт уже работает (например, юнитawg-routingдоканчивает запуск после перезагрузки), второй экземпляр ждёт до 5 минут и отрабатывает следом, а не ломается на полпути. - Скачивает список российских сетей во временный файл и подменяет рабочий только при успешной загрузке. Источники по очереди: ipdeny (актуальный список), при его недоступности - снимок из этого репозитория (
cascade/ru.zone), затем уже лежащий локальный список. Сорванная закачка не обнулит уже загруженный список. - Загружает сети в
ipsetчерез временный набор, добавляет туда же вашиEXTRA_RU_NETSи атомарно подменяет рабочий (ipset swap) - без окна, когда набор пустой. - Создаёт таблицу маршрутизации для помеченного трафика и правило
ip ruleпо метке. Таблица задаётся номером, файлrt_tablesне нужен. - Прокладывает маршрут к самому AWG1 вне туннеля, иначе пакеты к нему закольцуются.
- Метит трафик: к российским сетям - напрямую (
RETURN), остальное - меткой для ухода вawg1. - Добавляет NAT для трафика, уходящего через
awg1.
Шаг 5. Автозапуск после перезагрузки
Сервер awg0 установщик поднимает и включает сам. Туннель awg1 (сервис awg-quick@awg1) и скрипт маршрутизации ваши, их включаем здесь. Осталось, чтобы после обоих туннелей отрабатывал скрипт маршрутизации. Это делает один oneshot-юнит, который привязан к обоим туннелям и поэтому стартует строго после них - отдельный порядок между awg0 и awg1 задавать не нужно.
Создайте /etc/systemd/system/awg-routing.service:
[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 установщик уже включил):
systemctl daemon-reload
systemctl enable awg-quick@awg1 awg-routing
Requires здесь жёсткий: если awg1 не поднимется, скрипт маршрутизации не запустится (клиенты при этом всё равно подключатся к awg0, но без раздельного выхода - весь трафик пойдёт напрямую). Теперь после перезагрузки туннели и маршрутизация поднимаются сами, руками ничего трогать не нужно.
Проверка
На AWG0 убедитесь, что правило, таблица и список на месте:
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 С ОГОВОРКОЙ с числом непринятых адресов - каскад при этом применён, но именно эти адреса пойдут через заграничное плечо.
Проверьте, что трафик расходится правильно:
# к российскому адресу (Яндекс) - напрямую через внешний интерфейс (часть "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, создайте оба вида трафика и сравните:
# на 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:
echo '0 5 * * 1 root systemctl restart awg-routing' > /etc/cron.d/awg-routing-refresh
Наложения планового запуска на ручной можно не опасаться: скрипт берёт блокировку, и тот, кто пришёл вторым, дождётся своей очереди.
Вариант с systemd timer
Предложил @MrSokol в обсуждении #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:
[Unit]
Description=Периодическое обновление списка российских сетей
[Timer]
OnCalendar=*-*-* 01:00:00
RandomizedDelaySec=15m
Persistent=false
Unit=awg-routing.service
[Install]
WantedBy=timers.target
Примените и включите:
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 за поправку.
Если вы включили только таймер, а сервис в автозапуск не добавляли, сначала добавьте: без загрузочного прогона пропущенное срабатывание действительно теряется, и вот тогда 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 устройства не идёт мимо туннеля (см. Ограничения). Дописывать сервисы определения адреса в наборru- путь ненадёжный: их адреса зависят от резолвера и меняются сами. Разбор с цифрами - в ADVANCED.md. - Отдача (upload) намного ниже приёма (download). Если приём идёт нормально, а отдача проседает почти в ноль, дело обычно не в MTU (он режет сегменты в обе стороны, тогда страдал бы и приём) и не в процессоре (он бы просадил обе стороны разом). Живой приём доказывает, что вход AWG0 отдаёт клиенту на скорости; узкое место - именно плечо AWG0 -> AWG1, по которому уходит отдача наружу (приём его не использует). Кандидаты: шейпинг исходящего у хостера входного сервера в сторону AWG1, кривой пиринг до сети выхода, либо входной лимит на самом AWG1. Замерьте это плечо напрямую: на AWG1 запустите
iperf3 -s, с AWG0 -iperf3 -c <IP AWG1>(отдача AWG0 -> AWG1) иiperf3 -c <IP AWG1> -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.xCGNAT), не равный вашему внешнему IP, сервер за провайдерским NAT - тогда вход каскада снаружи недоступен, и нужен реальный публичный IP на обоих серверах (см. Что понадобится).
Безопасность
- Не выключайте UFW целиком (
ufw disable) ради скорости - на пропускную способность фаервол практически не влияет, основную нагрузку дают шифрование и пересылка. Оставьте минимум: пересылку и нужные порты, остальное закрыто. - Сервер-вход AWG0 видит весь трафик клиентов до ухода в
awg1- это точка доверия, держите к нему доступ под контролем.
Как убрать каскад
На AWG0 подставьте свои значения из начала /root/awg/awg-routing.sh: CLIENT_SUBNET и AWG1_ENDPOINT, а если меняли, то и AWG1_IF, TABLE_ID, FWMARK. Затем выполните:
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 из раздела Диагностика, уберите и их.
На AWG1 удалите клиента, которого выпускали для AWG0:
bash /root/awg/manage_amneziawg.sh remove ru_host
Если потом собираетесь удалять и сам установщик (--uninstall), сначала выполните шаги выше. Установщик останавливает только awg0, про awg1, юнит и таймер каскада он не знает, а /etc/amnezia и /root/awg удаляет целиком. Пока поднят awg1, модуль не выгрузится, а после перезагрузки включённые юниты каскада остались бы без своих файлов.
Ограничения и нюансы
- Каскад не входит в установщик и не управляется им - это отдельная ручная настройка поверх двух обычных установок.
- Схема работает по IPv4: список российских сетей IPv6-записей не содержит вовсе. Держите IPv6 выключенным на сервере (установщик так и ставит по умолчанию), иначе серверный IPv6-трафик пойдёт мимо деления. IPv6 на клиентском устройстве - отдельный уровень, установщик его не трогает. При установке ровно по флагам из шага 1 (полный туннель, без
--allow-ipv6-tunnel) он уходит в туннель вместе с остальным трафиком и наружу не идёт, а вот с раздельной маршрутизацией на клиенте пошёл бы мимо, со своим настоящим адресом. Проверка с устройства:curl -6 ifconfig.co- вернулся домашний адрес, значит IPv6 идёт мимо туннеля. Подробнее - в ADVANCED.md. - Деление по адресу назначения видно снаружи: сайт может сравнить адрес соединения с тем, что сообщит скрипт на его странице, и увидеть расхождение. Это свойство любой схемы с делением, а не дефект каскада - см. Диагностику и разбор в ADVANCED.md.
- Отдельно настраивать 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) и заменитеawg1.confна AWG0 (шаг 3). Если пересобран AWG0, вдобавок выпустите заново профили всех его клиентов и пройдите шаги 3-5 ещё раз.
Схему предложил @glfenix в #120. Вопросы и улучшения - туда же или в Issues.