Files
vpn-raspberry/README.md
Maxim 538414f2c8 Стабильная работа туннеля: 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),
сниппеты и таблицы приведены в соответствие.
2026-07-08 11:35:46 +03:00

289 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Схема WireGuard + udp2raw
Проект содержит конфигурации для развёртывания VPN-туннеля между **удалённым сервером (Ubuntu 24.04)** и **Raspberry Pi 4**.
Решение маскирует UDP-трафик WireGuard под обычные TCP-пакеты (режим `faketcp`), что позволяет обходить ограничения провайдеров, блокирующих или урезающих UDP на интерфейсе `usbnet`.
> **Порт входа — `25565/tcp`, а не `443`.** Порт `443` на сервере занят nginx (reverse-proxy для Gitea `totmaxim.ru`); `udp2raw` в режиме `faketcp` через правило `-a` перехватил бы весь TCP на своём порту и поломал nginx. Поэтому транспорт вынесен на выделенный `25565/tcp`.
---
## 0. Справочные данные
### Архитектура и сетевые параметры
| Параметр | Значение |
|---|---|
| Публичный вход сервера | `95.174.102.77:25565` (TCP, `udp2raw`; `443` занят nginx/Gitea) |
| Внутренний порт WireGuard (сервер) | `127.0.0.1:51820` |
| Внутренний порт udp2raw (клиент RPi) | `127.0.0.1:51821` |
| Подсеть VPN | `10.66.66.0/24` (сервер `10.66.66.1`, клиент `10.66.66.2`) |
| MTU туннеля | `1200` (симметрично на обоих концах) |
### Фактическая топология (важно для маршрутизации)
**Сервер (Ubuntu 24.04) — за NAT, без прямого публичного IP:**
- Внешний интерфейс: `enp2s0 = 192.168.31.163/24`, шлюз `192.168.31.1`.
- `95.174.102.77` — WAN-адрес роутера `192.168.31.1`, а НЕ адрес самого сервера.
- На хосте работает Docker (бриджи `172.17172.21.0.0/16`, `docker0`) со своими правилами nftables/iptables — поэтому правила WG изолированы в отдельной таблице `wgfwd` и маскарадят **только** `10.66.66.0/24`.
**Клиент (Raspberry Pi 4) — роутер для локальной сети:**
- Uplink: `usbnet = 192.168.200.13/24`, шлюз и DNS `192.168.200.1` (мобильный канал; провайдер режет UDP → маскировка под TCP).
- Downstream: `wlan0 = 192.168.77.0/24` — трафик клиентов уходит в интернет через туннель.
- Прочие интерфейсы (`eth0 10.1.1.33`, `eth1 192.168.2.10`) в схеме VPN не участвуют.
### Обязательные предусловия окружения
| # | Требование | Где | Проверка |
|---|---|---|---|
| 1 | Проброс `TCP 25565 → 192.168.31.163:25565` на роутере `95.174.102.77` | Роутер сервера | с клиента: `nc -vz 95.174.102.77 25565` |
| 2 | `net.ipv4.ip_forward = 1` (закрепить в `/etc/sysctl.d/`) | Сервер + Клиент | `sysctl net.ipv4.ip_forward` |
| 3 | Порт `25565/tcp` на хосте свободен; `443` оставить за nginx | Сервер | `sudo ss -ltnp 'sport = :25565'` |
| 4 | Одинаковый пароль `-k` в обеих службах `udp2raw` | Сервер + Клиент | сверить `.service` |
| 5 | Маршрут `95.174.102.77/32 via 192.168.200.1 dev usbnet` при поднятом `wg0` | Клиент | `ip route get 95.174.102.77``dev usbnet`, НЕ `wg0` |
| 6 | Установлен пакет `iptables` (udp2raw добавляет правило `-a` только через него) | Клиент | `iptables -V` |
| 7 | udp2raw на сервере слушает `192.168.31.163`, а НЕ `0.0.0.0` (иначе `bind fail` из-за docker-бриджей) | Сервер | см. `-l` в `.service` |
> **Почему #5 критично:** на клиенте `AllowedIPs = 0.0.0.0/0`, поэтому `wg-quick` заворачивает весь трафик в `wg0`. Без закреплённого маршрута сам транспорт `udp2raw` (пакеты к `95.174.102.77:25565`) тоже уйдёт в `wg0` — получится **петля**, и туннель порвётся. Маршрут ставится автоматически через `PreUp` в `client/wg0.conf`.
### Ключи WireGuard
- **Сервер:** pub `dVIwnwxkjan/K278vzhOjp5A79N98j0O1zsYqT24qzs=` / priv `KMcPBI6LAY/NC1zFNYopsvNXqQ8w8ssluDj4yOh3skk=`
- **Клиент:** pub `WWFhoYfIgIhGOsvdJ/F9eWx7hPksrV6riiqo5lFPExI=` / priv `eKiGDY62/9pgFrwzoNIb7URRLPgEHC1W7zHwIIDYQVw=`
> ⚠️ Перед боевым запуском смените дефолтный пароль `MY_CONFIDENTIAL_PASS` в аргументе `-k` обеих служб `.service`. Он должен быть **одинаковым** на сервере и клиенте.
---
## 1. Релиз udp2raw
- Архив: `https://github.com/wangyu-/udp2raw/releases/download/20230206.0/udp2raw_binaries.tar.gz`
- Версия: `20230206.0`
Из архива нужны только два файла:
1. `udp2raw_amd64` — на **сервер** (x86_64).
2. `udp2raw_arm64` (64-битная RPi OS, `aarch64`) или `udp2raw_arm_asm_aes` (32-битная, `armv7l`) — на **Raspberry Pi**. Уточнить: `uname -m`.
---
## 2. Развёртывание на СЕРВЕРЕ (Ubuntu)
### Шаг 2.0. Подготовка окружения
```bash
# 1) Проброс на роутере: TCP 25565 -> 192.168.31.163:25565 (проверить заранее)
# 2) Включаем и закрепляем форвардинг (wg-quick сам его НЕ включает):
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-wg.conf
sudo sysctl --system
# 3) Порт 25565 на хосте должен быть свободен (443 остаётся за nginx):
sudo ss -ltnp 'sport = :25565'
```
### Шаг 2.1. Установка WireGuard
```bash
sudo apt update && sudo apt install wireguard -y
```
### Шаг 2.2. Установка бинарника udp2raw
```bash
mkdir -p ~/download && cd ~/download
wget https://github.com/wangyu-/udp2raw/releases/download/20230206.0/udp2raw_binaries.tar.gz
tar -xvf udp2raw_binaries.tar.gz
sudo install -m 755 udp2raw_amd64 /usr/local/bin/udp2raw # именно ФАЙЛОМ, не каталогом
udp2raw -h >/dev/null && echo OK
```
### Шаг 2.3. Конфигурации
`/etc/wireguard/wg0.conf` (права `600`):
```ini
[Interface]
Address = 10.66.66.1/24
ListenPort = 51820
PrivateKey = KMcPBI6LAY/NC1zFNYopsvNXqQ8w8ssluDj4yOh3skk=
MTU = 1200
# Изолированная таблица wgfwd: masquerade ТОЛЬКО 10.66.66.0/24 (не конфликтует с Docker)
# + MSS-clamp на входе в туннель. PostDown сносит всё одной командой.
PostUp = nft add table ip wgfwd
PostUp = nft add chain ip wgfwd postrouting { type nat hook postrouting priority 100 \; policy accept \; }
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]
PublicKey = WWFhoYfIgIhGOsvdJ/F9eWx7hPksrV6riiqo5lFPExI=
AllowedIPs = 10.66.66.2/32
```
`/etc/systemd/system/udp2raw-server.service` (не забыть заменить пароль в `-k`):
```ini
[Unit]
Description=udp2raw Server (FakeTCP)
After=network.target
[Service]
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 --fix-gro -a
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
### Шаг 2.4. Запуск (сначала транспорт, потом туннель)
```bash
sudo systemctl daemon-reload
sudo systemctl start udp2raw-server # или enable --now — для автозагрузки
sudo systemctl start wg-quick@wg0
sudo wg show
sudo nft list table ip wgfwd
```
---
## 3. Развёртывание на КЛИЕНТЕ (Raspberry Pi 4)
### Шаг 3.1. Установка WireGuard и iptables
```bash
# iptables обязателен: на чистой nft-системе udp2raw не сможет добавить правило -a
sudo apt update && sudo apt install wireguard iptables -y
iptables -V # должно печатать версию (nf_tables)
```
### Шаг 3.2. Установка бинарника udp2raw
```bash
mkdir -p ~/download && cd ~/download
wget https://github.com/wangyu-/udp2raw/releases/download/20230206.0/udp2raw_binaries.tar.gz
tar -xvf udp2raw_binaries.tar.gz
uname -m # aarch64 -> udp2raw_arm64 ; armv7l -> udp2raw_arm_asm_aes
# ВАЖНО: класть ФАЙЛОМ по пути /usr/local/bin/udp2raw (не каталогом!)
sudo install -m 755 udp2raw_arm64 /usr/local/bin/udp2raw
udp2raw -h >/dev/null && echo OK
```
### Шаг 3.3. Firewall (nftables)
Правила уже собраны в `client/nftables.conf` (полный ruleset малинки + MSS-clamp). Бэкап и применение:
```bash
sudo cp /etc/nftables.conf /etc/nftables.conf.bak.$(date +%F)
sudo cp client/nftables.conf /etc/nftables.conf
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
# table ip nat, chain postrouting:
ip saddr 192.168.77.0/24 oifname "wg0" masquerade
```
> **`flush ruleset` + anti-RST.** Файл начинается с `flush ruleset` (убирает дубли при повторном `nft -f`). Но flush сносит и правило, которое udp2raw ставит флагом `-a` (drop входящего faketcp, иначе ядро шлёт RST и туннель рвётся при живом хендшейке). Поэтому anti-RST вписан прямо в `chain input` **первой строкой** — тогда `nft -f` атомарно восстанавливает его, и порядок применения перестаёт быть хрупким.
### Шаг 3.4. Конфигурации
`/etc/wireguard/wg0.conf` (права `600`; перед копированием сверить шлюз `ip route show default`):
```ini
[Interface]
Address = 10.66.66.2/24
PrivateKey = eKiGDY62/9pgFrwzoNIb7URRLPgEHC1W7zHwIIDYQVw=
MTU = 1200
# КРИТИЧНО (анти-петля): маршрут к серверу через реальный uplink ДО подъёма default в wg0.
# 192.168.200.1 — шлюз usbnet; при смене канала поправить здесь.
PreUp = ip route replace 95.174.102.77/32 via 192.168.200.1 dev usbnet
PostDown = ip route del 95.174.102.77/32 2>/dev/null || true
[Peer]
PublicKey = dVIwnwxkjan/K278vzhOjp5A79N98j0O1zsYqT24qzs=
Endpoint = 127.0.0.1:51821
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
```
`/etc/systemd/system/udp2raw-client.service` (пароль в `-k` — тот же, что на сервере):
```ini
[Unit]
Description=udp2raw Client (FakeTCP)
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 --fix-gro -a
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
### Шаг 3.5. Запуск (сначала транспорт, потом туннель)
```bash
sudo systemctl daemon-reload
sudo systemctl start udp2raw-client # или enable --now — для автозагрузки
sudo systemctl start wg-quick@wg0
sudo wg show
```
---
## 4. Диагностика
Проверять строго по цепочке снизу вверх:
1. **Транспорт udp2raw:** `sudo systemctl status udp2raw-{server,client}` — в логах `now listening` / `client_ready`, без циклических перезапусков.
2. **Туннель WireGuard:** `sudo wg show` — есть `latest handshake: N seconds ago`, счётчики `transfer` растут.
3. **Отсутствие петли (клиент):** `ip route get 95.174.102.77``dev usbnet`, а не `dev wg0`. Иначе не отработал `PreUp`.
4. **Сетевой уровень:** с Pi `ping 10.66.66.1`. Идёт пинг до сервера — туннель жив.
5. **Выход через туннель:** с устройства за `wlan0` `curl -s https://ipinfo.io/ip` → должен вернуть `95.174.102.77` (адрес сети сервера).
6. **Доступность входа:** с клиента `nc -vz 95.174.102.77 25565` — порт открыт (проброс на роутере).
---
## 5. Известные нюансы и тюнинг
- **⚠️ 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):
- клиент — первой строкой `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 гарантированно поднимется раньше туннеля.
- **«Не все сайты открываются» при рабочем туннеле** — почти всегда MTU/PMTUD:
- Многие сайты используют 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) или ребутом.