# Схема 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.17–172.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 есть), но данные идут пачками с 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 гарантированно поднимется раньше туннеля. - **«Не все сайты открываются» при рабочем туннеле** — почти всегда 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) или ребутом.