docs(cascade): retry route with onlink for out-of-subnet gateway VPS (#158)
Some checks failed
Docs consistency / docs-consistency (push) Has been cancelled
ShellCheck / Lint and syntax check (push) Has been cancelled

Fix awg-routing.sh failing with 'Nexthop has invalid gateway' on VPS whose default gateway is outside the server subnet (e.g. Hetzner /32). Retry the AWG1 host route with onlink; clarify the guide prose (public /32 + private gateway works via onlink vs genuine provider NAT). RU + EN. Reported in discussion #120.
This commit is contained in:
Ivan Bondarev 2026-07-05 09:48:37 +03:00 committed by GitHub
parent 0256987097
commit f12a93ddf4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 12 additions and 6 deletions

View file

@ -63,7 +63,7 @@ Clients are added on AWG0 as usual (`manage add name`) - no special setup on the
- AWG0 (entry) - ideally in or near Russia, so Russian sites open from a nearby address without a detour.
- AWG1 (exit) - abroad, a normal VPS.
- `amneziawg-installer` installed on both (see below).
- **A real public IP on both VPS** (not behind NAT/CGNAT). The routing script builds the path to the exit through the default gateway, so a server behind NAT (or with a nonstandard gateway setup) usually will not work. Quick check: the external address from `curl -s ifconfig.me` matches the address on the interface (`ip -4 addr`), and the default gateway (`ip route`) is not in a private range (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10` CGNAT). A red flag: a `/32` address with a private gateway (for example `gateway 10.0.0.1`) - the script then usually fails with `Error: Nexthop has invalid gateway` (see [Troubleshooting](#trouble)).
- **A real public IP on both VPS** (not behind NAT/CGNAT). The routing script builds the path to the exit through the default gateway, so a server behind provider NAT usually will not work. Quick check: the external address from `curl -s ifconfig.me` matches the address on the interface (`ip -4 addr`) - i.e. the public IP is on the server itself. A red flag is specifically a private address on the interface itself (`10.x`, `100.64.x`) that differs from your external one: that is provider NAT and the cascade entry will be unreachable from outside. A public `/32` with a private gateway (as on Hetzner) is fine - the script handles that case via `onlink` (see [Troubleshooting](#trouble)).
- IPv6 disabled (the installer does this by default). The cascade works over IPv4; with IPv6 on, that traffic would bypass the split.
- On AWG0 also the `curl` and `ipset` packages (a minimal image may lack them).
@ -218,7 +218,10 @@ ip rule add fwmark "$FWMARK" table "$TABLE_ID" priority "$RULE_PRIO"
# 4) Keep the route to AWG1 itself outside the tunnel (otherwise packets to it loop into awg1). replace = idempotent.
# If the default route has a gateway - via it; if not (point-to-point/on-link) - straight out the WAN.
if [ -n "$WAN_GW" ]; then
ip route replace "$AWG1_ENDPOINT" via "$WAN_GW" dev "$WAN_IF"
# On a VPS whose gateway is outside the server's subnet (e.g. Hetzner, a /32 interface) a plain
# replace fails with "Nexthop has invalid gateway" - then retry with onlink (gateway is on the link).
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
@ -355,7 +358,7 @@ echo '0 5 * * 1 root systemctl restart awg-routing' > /etc/cron.d/awg-routing-re
- **A specific Russian site still opens through the foreign exit.** It is most likely not hosted on a Russian IP (common for sites behind Cloudflare and foreign CDNs). The split is by destination IP, so such a site goes into the tunnel - this is expected.
- **YouTube buffers and stalls, Google Play won't download apps, and Google services serve Russian results - even though your cascade exit is abroad.** Some of Google's and YouTube's nodes - their caches and CDN - sit on Russian IPs and land in the RU list. The split is by destination address, so a connection to such a node goes directly via the entry, and Google sees a Russian address. Changing DNS does not reliably help here: Google's networks are largely shared and announced from many places, and some addresses still count as Russian. To make Google and YouTube always go through the foreign exit you need an up-to-date list of their networks (not just AS15169 - GGC cache nodes often live in ISP networks): mark them before the RETURN rule, or drop them from the ru set - then they leave via the exit instead of going direct. The downside: all of Google then always goes through the exit.
- **Upload is much slower than download.** If download is fine but upload drops to almost nothing, the cause is usually not MTU (it caps segments in both directions, so download would suffer too) and not the CPU (it would slow both directions at once). A healthy download proves that the entry server AWG0 can send to the client at full speed; the bottleneck is the AWG0 -> AWG1 leg that carries the upload outward (download does not use it). Candidates: the entry host shaping outbound traffic towards AWG1, poor peering to the exit network, or an inbound limit on AWG1 itself. Measure that leg directly: run `iperf3 -s` on AWG1, and from AWG0 run `iperf3 -c <AWG1 IP>` (upload AWG0 -> AWG1) and `iperf3 -c <AWG1 IP> -R` (download). A low first number with a normal second confirms that the AWG0 -> AWG1 leg is the choke, not the servers themselves.
- **The `awg-routing.sh` script fails with `Error: Nexthop has invalid gateway`.** Usually such a VPS has no public address of its own: the interface has a `/32` address with a private default gateway (`10.x`, `192.168.x`, `100.64.x` CGNAT) - for example `gateway 10.0.0.1` on the address `83.220.x.x/32` (typical of a VPS behind provider NAT). The script adds a route to the exit through that gateway, but with a `/32` address the gateway `10.0.0.1` is not in any connected subnet and is not treated as on-link, so the kernel rejects the nexthop. The cascade needs a real public IP on both servers (see [What you need](#prereq)): check `ip -4 addr` and `ip route`, and if the address is a private one, switch to a plan or provider with a real IP.
- **The `awg-routing.sh` script fails with `Error: Nexthop has invalid gateway`.** It means the default gateway sits outside the server's subnet: the interface has a `/32` address and the gateway is somewhere off it - the kernel does not treat such a gateway as on-link and rejects the route. The current version of the script already handles this: on that error it retries adding the route with the `onlink` flag, so on a VPS like Hetzner (a real public IP on a `/32` and a private gateway such as `172.31.1.1`) the cascade just works - update `awg-routing.sh` from the guide if your copy is old. But first make sure the server really has a public IP: the address from `ip -4 addr` should match `curl -s ifconfig.me`. If the interface has a private address (`10.x`, `100.64.x` CGNAT) that differs from your external IP, the server is behind provider NAT - then the cascade entry is unreachable from outside and you need a real public IP on both servers (see [What you need](#prereq)).
<a id="security"></a>
## Security

View file

@ -63,7 +63,7 @@ flowchart LR
- AWG0 (вход) - лучше в России или рядом, чтобы российские сайты открывались с близкого адреса и без крюка.
- AWG1 (выход) - за границей, обычный VPS.
- На обоих установлен `amneziawg-installer` (как - ниже).
- **Реальный публичный IP на обоих VPS** (не за NAT/CGNAT). Скрипт маршрутизации строит путь к выходу через шлюз по умолчанию, поэтому сервер за NAT (или с нестандартной схемой шлюза) обычно не подойдёт. Быстрая проверка: внешний адрес из `curl -s ifconfig.me` совпадает с адресом на интерфейсе (`ip -4 addr`), а шлюз по умолчанию (`ip route`) - не из приватного диапазона (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10` CGNAT). Тревожный признак: адрес вида `/32` с приватным шлюзом (например `gateway 10.0.0.1`) - тогда скрипт обычно падает с `Error: Nexthop has invalid gateway` (см. [Диагностику](#trouble)).
- **Реальный публичный 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 трафик пойдёт мимо деления.
- На AWG0 дополнительно пакеты `curl` и `ipset` (на минимальном образе их может не быть).
@ -217,7 +217,10 @@ ip rule add fwmark "$FWMARK" table "$TABLE_ID" priority "$RULE_PRIO"
# 4) Маршрут к самому AWG1 держим вне туннеля (иначе пакеты к нему закольцуются в awg1). replace = идемпотентно.
if [ -n "$WAN_GW" ]; then
ip route replace "$AWG1_ENDPOINT" via "$WAN_GW" dev "$WAN_IF"
# На 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
@ -354,7 +357,7 @@ echo '0 5 * * 1 root systemctl restart awg-routing' > /etc/cron.d/awg-routing-re
- **Конкретный российский сайт всё равно открывается через заграницу.** Скорее всего он размещён не на российском IP (часто бывает с сайтами за Cloudflare и зарубежными CDN). Деление идёт по IP назначения, поэтому такой сайт уходит в туннель - это ожидаемо.
- **YouTube тормозит и буферизует, Google Play не загружает приложения, а сервисы Google отдают российскую выдачу - хотя выход у каскада зарубежный.** Часть узлов Google и YouTube - их кэши и CDN - стоит на российских IP и попадает в список RU. Деление идёт по адресу назначения, поэтому соединение с таким узлом уходит напрямую через вход, и Google видит российский адрес. Подмена DNS тут надёжно не лечит: сети Google во многом общие и анонсируются из разных мест, часть адресов всё равно остаётся российской. Чтобы Google и YouTube всегда шли через зарубежный выход, нужен актуальный список их сетей (не только AS15169 - кэш-узлы GGC часто лежат в сетях провайдеров): пометьте их ДО правила RETURN или вовсе исключите из набора ru - тогда они уйдут через выход, а не напрямую. Минус: весь Google при этом идёт через выход всегда.
- **Отдача (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`.** Обычно у такого VPS нет своего публичного адреса: на интерфейсе стоит `/32`-адрес с приватным шлюзом по умолчанию (`10.x`, `192.168.x`, `100.64.x` CGNAT) - например `gateway 10.0.0.1` на адресе `83.220.x.x/32` (типично для VPS за провайдерским NAT). Скрипт добавляет маршрут к выходу через этот шлюз, но при `/32`-адресе шлюз `10.0.0.1` не попадает ни в одну связную подсеть и не считается on-link, поэтому ядро отвергает nexthop. Каскаду нужен реальный публичный IP на обоих серверах (см. [Что понадобится](#prereq)): проверьте `ip -4 addr` и `ip route`, и если адрес серый - возьмите тариф или провайдера с реальным IP.
- **Скрипт `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)).
<a id="security"></a>
## Безопасность