Проверено на живом стенде: 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), сниппеты и таблицы приведены в соответствие.
Схема WireGuard + udp2raw
Проект содержит конфигурации для развёртывания VPN-туннеля между удалённым сервером (Ubuntu 24.04) и Raspberry Pi 4.
Решение маскирует UDP-трафик WireGuard под обычные TCP-пакеты (режим faketcp), что позволяет обходить ограничения провайдеров, блокирующих или урезающих UDP на интерфейсе usbnet.
Порт входа —
25565/tcp, а не443. Порт443на сервере занят nginx (reverse-proxy для Giteatotmaxim.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, шлюз и DNS192.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=/ privKMcPBI6LAY/NC1zFNYopsvNXqQ8w8ssluDj4yOh3skk= - Клиент: pub
WWFhoYfIgIhGOsvdJ/F9eWx7hPksrV6riiqo5lFPExI=/ priveKiGDY62/9pgFrwzoNIb7URRLPgEHC1W7zHwIIDYQVw=
⚠️ Перед боевым запуском смените дефолтный пароль
MY_CONFIDENTIAL_PASSв аргументе-kобеих служб.service. Он должен быть одинаковым на сервере и клиенте.
1. Релиз udp2raw
- Архив:
https://github.com/wangyu-/udp2raw/releases/download/20230206.0/udp2raw_binaries.tar.gz - Версия:
20230206.0
Из архива нужны только два файла:
udp2raw_amd64— на сервер (x86_64).udp2raw_arm64(64-битная RPi OS,aarch64) илиudp2raw_arm_asm_aes(32-битная,armv7l) — на Raspberry Pi. Уточнить:uname -m.
2. Развёртывание на СЕРВЕРЕ (Ubuntu)
Шаг 2.0. Подготовка окружения
# 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
sudo apt update && sudo apt install wireguard -y
Шаг 2.2. Установка бинарника udp2raw
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):
[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):
[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. Запуск (сначала транспорт, потом туннель)
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
# iptables обязателен: на чистой nft-системе udp2raw не сможет добавить правило -a
sudo apt update && sudo apt install wireguard iptables -y
iptables -V # должно печатать версию (nf_tables)
Шаг 3.2. Установка бинарника udp2raw
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). Бэкап и применение:
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 (без дублей)
Ключевые строки, которые должны присутствовать:
# 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):
[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 — тот же, что на сервере):
[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. Запуск (сначала транспорт, потом туннель)
sudo systemctl daemon-reload
sudo systemctl start udp2raw-client # или enable --now — для автозагрузки
sudo systemctl start wg-quick@wg0
sudo wg show
4. Диагностика
Проверять строго по цепочке снизу вверх:
- Транспорт udp2raw:
sudo systemctl status udp2raw-{server,client}— в логахnow listening/client_ready, без циклических перезапусков. - Туннель WireGuard:
sudo wg show— естьlatest handshake: N seconds ago, счётчикиtransferрастут. - Отсутствие петли (клиент):
ip route get 95.174.102.77→dev usbnet, а неdev wg0. Иначе не отработалPreUp. - Сетевой уровень: с Pi
ping 10.66.66.1. Идёт пинг до сервера — туннель жив. - Выход через туннель: с устройства за
wlan0curl -s https://ipinfo.io/ip→ должен вернуть95.174.102.77(адрес сети сервера). - Доступность входа: с клиента
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 не нужен. Для боевого режима достаточно включить обе службы на каждом конце: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 занижаем.
- Многие сайты используют HTTP/3 (QUIC поверх UDP 443), который не подчиняется MSS-clamp. В
-
Дубли правил nftables. Если
nft list rulesetпоказывает удвоенные строки — файл применялиnft -fповерх без flush. Лечится однимsudo nft -f /etc/nftables.confсо свежего файла (он начинается с flush) или ребутом.