diff --git a/README.md b/README.md index 5499c52..60b1c69 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Схема WireGuard + udp2raw -Проект содержит конфигурации для развёртывания отказоустойчивого VPN-туннеля между **удалённым сервером (Ubuntu 24.04)** и **Raspberry Pi 4**. +Проект содержит конфигурации для развёртывания VPN-туннеля между **удалённым сервером (Ubuntu 24.04)** и **Raspberry Pi 4**. Решение маскирует UDP-трафик WireGuard под обычные TCP-пакеты (режим `faketcp`), что позволяет обходить ограничения провайдеров, блокирующих или урезающих UDP на интерфейсе `usbnet`. @@ -32,7 +32,7 @@ - Downstream: `wlan0 = 192.168.77.0/24` — трафик клиентов уходит в интернет через туннель. - Прочие интерфейсы (`eth0 10.1.1.33`, `eth1 192.168.2.10`) в схеме VPN не участвуют. -### ⚠️ Обязательные предусловия окружения +### Обязательные предусловия окружения | # | Требование | Где | Проверка | |---|---|---|---| @@ -113,6 +113,9 @@ PostUp = nft add chain ip wgfwd postrouting { type nat hook postrouting priority PostUp = nft add rule ip wgfwd postrouting ip saddr 10.66.66.0/24 oifname "enp2s0" masquerade PostUp = nft add chain ip wgfwd mangle { type filter hook forward priority -150 \; policy accept \; } PostUp = nft add rule ip wgfwd mangle oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu +# udp2raw anti-RST (дубль к флагу -a, переживает ufw reload). Матч по своему IP+порту. +PostUp = nft add chain ip wgfwd prerst { type filter hook input priority -300 \; policy accept \; } +PostUp = nft add rule ip wgfwd prerst ip daddr 192.168.31.163 tcp dport 25565 drop PostDown = nft delete table ip wgfwd [Peer] @@ -131,7 +134,7 @@ After=network.target Type=simple User=root # Слушать КОНКРЕТНЫЙ IP 192.168.31.163 (на 0.0.0.0 — bind fail из-за docker-бриджей) -ExecStart=/usr/local/bin/udp2raw -s -l 192.168.31.163:25565 -r 127.0.0.1:51820 -k "MY_CONFIDENTIAL_PASS" --raw-mode faketcp -a +ExecStart=/usr/local/bin/udp2raw -s -l 192.168.31.163:25565 -r 127.0.0.1:51820 -k "MY_CONFIDENTIAL_PASS" --raw-mode faketcp --fix-gro -a Restart=always RestartSec=5 @@ -186,7 +189,11 @@ sudo nft -f /etc/nftables.conf # применяется с flush (без д Ключевые строки, которые должны присутствовать: ```nft +# table inet filter, chain input — ПЕРВЫМ правилом (выше ct-accept): +ip saddr 95.174.102.77 tcp sport 25565 drop # udp2raw anti-RST + # table inet filter, chain forward: +iifname "wlan0" oifname "wg0" udp dport 443 reject # блок QUIC → откат на TCP iifname "wlan0" oifname "wg0" accept iifname "wg0" oifname "wlan0" ct state established,related accept iifname "wlan0" oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu # MSS-clamp @@ -195,7 +202,7 @@ iifname "wlan0" oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu ip saddr 192.168.77.0/24 oifname "wg0" masquerade ``` -> `nft -f` применяет файл с очисткой (flush) — это важно, чтобы не накапливались дубли правил при повторном запуске. +> **`flush ruleset` + anti-RST.** Файл начинается с `flush ruleset` (убирает дубли при повторном `nft -f`). Но flush сносит и правило, которое udp2raw ставит флагом `-a` (drop входящего faketcp, иначе ядро шлёт RST и туннель рвётся при живом хендшейке). Поэтому anti-RST вписан прямо в `chain input` **первой строкой** — тогда `nft -f` атомарно восстанавливает его, и порядок применения перестаёт быть хрупким. ### Шаг 3.4. Конфигурации @@ -229,7 +236,7 @@ After=network.target [Service] Type=simple User=root -ExecStart=/usr/local/bin/udp2raw -c -l 127.0.0.1:51821 -r 95.174.102.77:25565 -k "MY_CONFIDENTIAL_PASS" --raw-mode faketcp -a +ExecStart=/usr/local/bin/udp2raw -c -l 127.0.0.1:51821 -r 95.174.102.77:25565 -k "MY_CONFIDENTIAL_PASS" --raw-mode faketcp --fix-gro -a Restart=always RestartSec=5 @@ -263,18 +270,19 @@ sudo wg show ## 5. Известные нюансы и тюнинг -- **Автозагрузка + порядок.** Для боевого режима на обоих концах: `sudo systemctl enable udp2raw- wg-quick@wg0`. Чтобы после ребута WG стартовал строго за udp2raw, добавить drop-in `sudo systemctl edit wg-quick@wg0`: - ```ini - [Unit] - After=udp2raw-client.service - Wants=udp2raw-client.service +- **⚠️ anti-RST vs `flush ruleset` (главная ловушка).** udp2raw в faketcp работает через AF_PACKET, а TCP-стек ядра видит «левые» пакеты на транспортном порту и шлёт **RST**. На мобильном канале это фатально вдвойне: RST уходит через CGNAT оператора → оператор считает TCP-поток закрытым и **убивает NAT-трансляцию** → пакеты клиент↔сервер начинают дропаться → udp2raw ловит `client-->server direction timeout` и переустанавливает faketcp **с новым source-портом** (виден в логе: `using port 14050`, `47011`…). Итог: туннель «поднят» (хендшейк WG есть), но данные идут пачками с 30–100% потерь, у выживших идеальный RTT. Флаг `-a` гасит это drop-правилом при старте udp2raw — **но `flush ruleset` его сносит**, и любой `nft -f` на живом туннеле убивает связь. Поэтому anti-RST вписан прямо в конфиги (идемпотентно к flush): + - клиент — первой строкой `chain input`: `ip saddr 95.174.102.77 tcp sport 25565 drop`; + - сервер — в таблице `wgfwd` (PostUp): `ip daddr 192.168.31.163 tcp dport 25565 drop` (переживает `ufw reload`). + + Флаг `-a` в службах оставлен как подстраховка (закрывает окно до применения nft-правил). Порядок на клиенте: `nftables.service` → `udp2raw-client` (задано `After=nftables.service`; убедитесь, что `nftables.service` включён: `systemctl is-enabled nftables`). +- **GRO / «huge packet ... dropped».** Если в логе udp2raw мелькает `huge packet, data_len NNNN > 1800(max_data_len) dropped` — стек склеивает входящие faketcp-сегменты в «супер-пакеты» больше `max_data_len`, и udp2raw их роняет (потери на крупном трафике). Лечится флагом `--fix-gro` в обеих службах (уже добавлен в `.service`). Альтернатива — выключить GRO на аплинке: `sudo ethtool -K usbnet gro off` (на сервере — `enp2s0`). +- **Автозагрузка + порядок.** Порядок уже зашит в юниты udp2raw (`Before=wg-quick@wg0.service` + `After=network-online.target nftables.service`), отдельный drop-in не нужен. Для боевого режима достаточно включить обе службы на каждом конце: + ```bash + sudo systemctl enable udp2raw-server wg-quick@wg0 # на сервере + sudo systemctl enable udp2raw-client wg-quick@wg0 # на клиенте ``` - (на сервере — `udp2raw-server.service`). + После ребута udp2raw гарантированно поднимется раньше туннеля. - **«Не все сайты открываются» при рабочем туннеле** — почти всегда MTU/PMTUD: - - Многие сайты используют HTTP/3 (QUIC поверх UDP 443), который **не подчиняется MSS-clamp**. Лечится либо снижением MTU туннеля (пробовать `1200 → 1160 → 1120` на обоих концах симметрично), либо блокировкой QUIC, чтобы браузер откатывался на TCP: - ```nft - # в chain forward клиента, до accept-правил wg0: - iifname "wlan0" oifname "wg0" udp dport 443 reject - ``` + - Многие сайты используют HTTP/3 (QUIC поверх UDP 443), который **не подчиняется MSS-clamp**. В `client/nftables.conf` уже добавлено правило `iifname "wlan0" oifname "wg0" udp dport 443 reject` — браузер откатывается на TCP/HTTPS (который зажат MSS-clamp'ом и пролезает). Если этого мало — симметрично снижать MTU туннеля (`1200 → 1160 → 1120` на обоих концах). - Проверка pMTU напрямую: с клиента `ping -M do -s 1172 10.66.66.1` (1172+28=1200). Если не проходит — MTU занижаем. - **Дубли правил nftables.** Если `nft list ruleset` показывает удвоенные строки — файл применяли `nft -f` поверх без flush. Лечится одним `sudo nft -f /etc/nftables.conf` со свежего файла (он начинается с flush) или ребутом. diff --git a/client/nftables.conf b/client/nftables.conf index e51c25c..3a82127 100644 --- a/client/nftables.conf +++ b/client/nftables.conf @@ -1,4 +1,7 @@ -# RASPBERRY PI 4: /etc/nftables.conf +#!/usr/sbin/nft -f +# RASPBERRY PI 4: /etc/nftables.conf +# flush обязателен: без него повторный `nft -f` накапливает дубли правил. +flush ruleset table ip nat { chain postrouting { @@ -11,6 +14,15 @@ table ip nat { table inet filter { chain input { type filter hook input priority filter; policy drop; + + # udp2raw faketcp anti-RST (КРИТИЧНО, должно стоять ПЕРВЫМ — выше ct-accept): + # обратный faketcp от сервера попадает под ct established и доходит до TCP-стека + # ядра, а тот шлёт RST на «левый» TCP -> шторм RST -> туннель встаёт, но данные + # не идут. Прячем эти пакеты от стека; udp2raw читает их через AF_PACKET (до + # netfilter), поэтому drop ему не мешает. Аналог правила, которое ставит флаг -a. + # Держим правило ЗДЕСЬ (а не через -a), чтобы flush ruleset его не стирал. + ip saddr 95.174.102.77 tcp sport 25565 drop + ct state established,related accept iifname "lo" accept ip protocol icmp accept @@ -28,6 +40,11 @@ table inet filter { iifname "wlan0" oifname "usbnet" accept iifname "usbnet" oifname "wlan0" ct state established,related accept + # Блокируем QUIC/HTTP-3 (UDP 443) в туннель: он НЕ подчиняется MSS-clamp, + # крупные датаграммы теряются при MTU 1200 и сайты «висят». + # reject (а не drop) → браузер сразу откатывается на TCP/HTTPS. Ставить ДО accept. + iifname "wlan0" oifname "wg0" udp dport 443 reject + iifname "wlan0" oifname "wg0" accept iifname "wg0" oifname "wlan0" ct state established,related accept diff --git a/client/udp2raw-client.service b/client/udp2raw-client.service index bb5fe6f..4bd1072 100644 --- a/client/udp2raw-client.service +++ b/client/udp2raw-client.service @@ -1,13 +1,21 @@ [Unit] Description=udp2raw Client (FakeTCP) -After=network.target +# network-online (а не network.target): нужен поднятый uplink usbnet до старта. +Wants=network-online.target +# nftables.service должен примениться ДО udp2raw: в /etc/nftables.conf лежит anti-RST +# правило (ip saddr 95.174.102.77 tcp sport 25565 drop). Требует enabled nftables.service. +After=network-online.target nftables.service +# Порядок автозагрузки: udp2raw поднимается ДО туннеля WireGuard (Endpoint=127.0.0.1:51821). +Before=wg-quick@wg0.service [Service] Type=simple User=root # Слушает локальный UDP-порт 51821 и транслирует в TCP на внешний вход сервера 95.174.102.77:25565 # ВАЖНО: Пароль в параметре -k должен строго совпадать с паролем на сервере! -ExecStart=/usr/local/bin/udp2raw -c -l 127.0.0.1:51821 -r 95.174.102.77:25565 -k "MY_CONFIDENTIAL_PASS" --raw-mode faketcp -a +# --fix-gro: стек склеивает входящие faketcp-сегменты в «супер-пакеты» >1800, +# udp2raw их дропает ("huge packet ... dropped") — флаг это чинит. +ExecStart=/usr/local/bin/udp2raw -c -l 127.0.0.1:51821 -r 95.174.102.77:25565 -k "MY_CONFIDENTIAL_PASS" --raw-mode faketcp --fix-gro -a Restart=always RestartSec=5 diff --git a/server/udp2raw-server.service b/server/udp2raw-server.service index b9ba4ce..b38676b 100644 --- a/server/udp2raw-server.service +++ b/server/udp2raw-server.service @@ -1,6 +1,11 @@ [Unit] Description=udp2raw Server (FakeTCP) -After=network.target +# network-online (а не network.target): нужен реально поднятый enp2s0 с адресом +# 192.168.31.163, иначе bind на конкретный IP упадёт при загрузке. +Wants=network-online.target +After=network-online.target +# Порядок автозагрузки: udp2raw поднимается ДО туннеля WireGuard. +Before=wg-quick@wg0.service [Service] Type=simple @@ -12,7 +17,8 @@ User=root # иначе из-за множества docker-бриджей udp2raw падает с "bind fail". # Если LAN-адрес сервера сменится — поправить здесь И в пробросе на роутере. # ВАЖНО #2: Замените "MY_CONFIDENTIAL_PASS" на ваш секретный пароль для udp2raw -ExecStart=/usr/local/bin/udp2raw -s -l 192.168.31.163:25565 -r 127.0.0.1:51820 -k "MY_CONFIDENTIAL_PASS" --raw-mode faketcp -a +# --fix-gro: чинит дроп «супер-пакетов» GRO (huge packet > max_data_len). Ставим на обоих концах. +ExecStart=/usr/local/bin/udp2raw -s -l 192.168.31.163:25565 -r 127.0.0.1:51820 -k "MY_CONFIDENTIAL_PASS" --raw-mode faketcp --fix-gro -a Restart=always RestartSec=5 diff --git a/server/wg0.conf b/server/wg0.conf index 540971d..8b65432 100644 --- a/server/wg0.conf +++ b/server/wg0.conf @@ -22,6 +22,13 @@ PostUp = nft add rule ip wgfwd postrouting ip saddr 10.66.66.0/24 oifname "enp2s # чтобы large-TCP не «залипал» из-за MTU 1200 и заблокированного PMTUD. PostUp = nft add chain ip wgfwd mangle { type filter hook forward priority -150 \; policy accept \; } PostUp = nft add rule ip wgfwd mangle oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu +# udp2raw faketcp anti-RST (дубль к флагу -a): прячем входящий транспорт от TCP-стека, +# иначе ядро шлёт RST на faketcp -> туннель рвётся. Матч по СВОЕМУ IP+порту (стабильно, +# не зависит от динамического мобильного IP клиента). Приоритет -300 = раньше ufw/docker. +# udp2raw читает через AF_PACKET (до netfilter), так что drop ему не мешает. +# В своей таблице wgfwd — переживает ufw reload (в отличие от правила -a в table ip filter). +PostUp = nft add chain ip wgfwd prerst { type filter hook input priority -300 \; policy accept \; } +PostUp = nft add rule ip wgfwd prerst ip daddr 192.168.31.163 tcp dport 25565 drop PostDown = nft delete table ip wgfwd [Peer]