Стабильная работа туннеля: anti-RST, --fix-gro, QUIC-блок, автозагрузка

Проверено на живом стенде: 0% потерь на 10.66.66.1 и 8.8.8.8, выход через
сервер (curl -> 95.174.102.77).

Транспорт (устранение разрывов faketcp):
- anti-RST вписан в конфиги вместо хрупкого -a+flush:
  клиент — первой строкой chain input (ip saddr 95.174.102.77 tcp sport 25565 drop),
  сервер — в таблице wgfwd (ip daddr 192.168.31.163 tcp dport 25565 drop, переживает ufw reload).
  Причина: RST от ядра убивал CGNAT-маппинг оператора -> direction timeout и смена порта.
- --fix-gro на обоих концах: чинит дроп GRO «супер-пакетов» (huge packet > max_data_len).

Клиент:
- nftables.conf: flush ruleset (убирает дубли), reject QUIC/UDP 443 (откат браузера на TCP).
- udp2raw-client.service: After=nftables.service (anti-RST применяется до старта udp2raw).

Автозагрузка/порядок (обе службы udp2raw):
- After=network-online.target, Before=wg-quick@wg0.service.

README: раздел 5 «Известные нюансы» (anti-RST vs flush, CGNAT/RST, GRO),
сниппеты и таблицы приведены в соответствие.
This commit is contained in:
Maxim
2026-07-08 11:35:46 +03:00
parent 888fd86dfa
commit 538414f2c8
5 changed files with 67 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
# Схема WireGuard + udp2raw # Схема WireGuard + udp2raw
Проект содержит конфигурации для развёртывания отказоустойчивого VPN-туннеля между **удалённым сервером (Ubuntu 24.04)** и **Raspberry Pi 4**. Проект содержит конфигурации для развёртывания VPN-туннеля между **удалённым сервером (Ubuntu 24.04)** и **Raspberry Pi 4**.
Решение маскирует UDP-трафик WireGuard под обычные TCP-пакеты (режим `faketcp`), что позволяет обходить ограничения провайдеров, блокирующих или урезающих UDP на интерфейсе `usbnet`. Решение маскирует UDP-трафик WireGuard под обычные TCP-пакеты (режим `faketcp`), что позволяет обходить ограничения провайдеров, блокирующих или урезающих UDP на интерфейсе `usbnet`.
@@ -32,7 +32,7 @@
- Downstream: `wlan0 = 192.168.77.0/24` — трафик клиентов уходит в интернет через туннель. - Downstream: `wlan0 = 192.168.77.0/24` — трафик клиентов уходит в интернет через туннель.
- Прочие интерфейсы (`eth0 10.1.1.33`, `eth1 192.168.2.10`) в схеме VPN не участвуют. - Прочие интерфейсы (`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 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 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 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 PostDown = nft delete table ip wgfwd
[Peer] [Peer]
@@ -131,7 +134,7 @@ After=network.target
Type=simple Type=simple
User=root User=root
# Слушать КОНКРЕТНЫЙ IP 192.168.31.163 (на 0.0.0.0 — bind fail из-за docker-бриджей) # Слушать КОНКРЕТНЫЙ 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 Restart=always
RestartSec=5 RestartSec=5
@@ -186,7 +189,11 @@ sudo nft -f /etc/nftables.conf # применяется с flush (без д
Ключевые строки, которые должны присутствовать: Ключевые строки, которые должны присутствовать:
```nft ```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: # table inet filter, chain forward:
iifname "wlan0" oifname "wg0" udp dport 443 reject # блок QUIC → откат на TCP
iifname "wlan0" oifname "wg0" accept iifname "wlan0" oifname "wg0" accept
iifname "wg0" oifname "wlan0" ct state established,related 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 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 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. Конфигурации ### Шаг 3.4. Конфигурации
@@ -229,7 +236,7 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
User=root 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 Restart=always
RestartSec=5 RestartSec=5
@@ -263,18 +270,19 @@ sudo wg show
## 5. Известные нюансы и тюнинг ## 5. Известные нюансы и тюнинг
- **Автозагрузка + порядок.** Для боевого режима на обоих концах: `sudo systemctl enable udp2raw-<role> wg-quick@wg0`. Чтобы после ребута WG стартовал строго за udp2raw, добавить drop-in `sudo systemctl edit wg-quick@wg0`: - **⚠️ 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 есть), но данные идут пачками с 30100% потерь, у выживших идеальный RTT. Флаг `-a` гасит это drop-правилом при старте udp2raw — **но `flush ruleset` его сносит**, и любой `nft -f` на живом туннеле убивает связь. Поэтому anti-RST вписан прямо в конфиги (идемпотентно к flush):
```ini - клиент — первой строкой `chain input`: `ip saddr 95.174.102.77 tcp sport 25565 drop`;
[Unit] - сервер — в таблице `wgfwd` (PostUp): `ip daddr 192.168.31.163 tcp dport 25565 drop` (переживает `ufw reload`).
After=udp2raw-client.service
Wants=udp2raw-client.service Флаг `-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: - **«Не все сайты открываются» при рабочем туннеле** — почти всегда MTU/PMTUD:
- Многие сайты используют HTTP/3 (QUIC поверх UDP 443), который **не подчиняется MSS-clamp**. Лечится либо снижением MTU туннеля (пробовать `1200 → 1160 → 1120` на обоих концах симметрично), либо блокировкой QUIC, чтобы браузер откатывался на TCP: - Многие сайты используют 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` на обоих концах).
```nft
# в chain forward клиента, до accept-правил wg0:
iifname "wlan0" oifname "wg0" udp dport 443 reject
```
- Проверка pMTU напрямую: с клиента `ping -M do -s 1172 10.66.66.1` (1172+28=1200). Если не проходит — MTU занижаем. - Проверка 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) или ребутом. - **Дубли правил nftables.** Если `nft list ruleset` показывает удвоенные строки — файл применяли `nft -f` поверх без flush. Лечится одним `sudo nft -f /etc/nftables.conf` со свежего файла (он начинается с flush) или ребутом.

View File

@@ -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 { table ip nat {
chain postrouting { chain postrouting {
@@ -11,6 +14,15 @@ table ip nat {
table inet filter { table inet filter {
chain input { chain input {
type filter hook input priority filter; policy drop; 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 ct state established,related accept
iifname "lo" accept iifname "lo" accept
ip protocol icmp accept ip protocol icmp accept
@@ -28,6 +40,11 @@ table inet filter {
iifname "wlan0" oifname "usbnet" accept iifname "wlan0" oifname "usbnet" accept
iifname "usbnet" oifname "wlan0" ct state established,related 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 "wlan0" oifname "wg0" accept
iifname "wg0" oifname "wlan0" ct state established,related accept iifname "wg0" oifname "wlan0" ct state established,related accept

View File

@@ -1,13 +1,21 @@
[Unit] [Unit]
Description=udp2raw Client (FakeTCP) 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] [Service]
Type=simple Type=simple
User=root User=root
# Слушает локальный UDP-порт 51821 и транслирует в TCP на внешний вход сервера 95.174.102.77:25565 # Слушает локальный UDP-порт 51821 и транслирует в TCP на внешний вход сервера 95.174.102.77:25565
# ВАЖНО: Пароль в параметре -k должен строго совпадать с паролем на сервере! # ВАЖНО: Пароль в параметре -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 Restart=always
RestartSec=5 RestartSec=5

View File

@@ -1,6 +1,11 @@
[Unit] [Unit]
Description=udp2raw Server (FakeTCP) 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] [Service]
Type=simple Type=simple
@@ -12,7 +17,8 @@ User=root
# иначе из-за множества docker-бриджей udp2raw падает с "bind fail". # иначе из-за множества docker-бриджей udp2raw падает с "bind fail".
# Если LAN-адрес сервера сменится — поправить здесь И в пробросе на роутере. # Если LAN-адрес сервера сменится — поправить здесь И в пробросе на роутере.
# ВАЖНО #2: Замените "MY_CONFIDENTIAL_PASS" на ваш секретный пароль для udp2raw # ВАЖНО #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 Restart=always
RestartSec=5 RestartSec=5

View File

@@ -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. # чтобы large-TCP не «залипал» из-за MTU 1200 и заблокированного PMTUD.
PostUp = nft add chain ip wgfwd mangle { type filter hook forward priority -150 \; policy accept \; } 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 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 PostDown = nft delete table ip wgfwd
[Peer] [Peer]