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

Схема 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.77dev 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. Подготовка окружения

# 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. Диагностика

Проверять строго по цепочке снизу вверх:

  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.77dev 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.serviceudp2raw-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 занижаем.
  • Дубли правил nftables. Если nft list ruleset показывает удвоенные строки — файл применяли nft -f поверх без flush. Лечится одним sudo nft -f /etc/nftables.conf со свежего файла (он начинается с flush) или ребутом.

Description
конфигурации для развёртывания VPN-туннеля между удалённым сервером (Ubuntu 24.04) и Raspberry Pi 4 Маскирует UDP-трафик WireGuard под обычные TCP-пакеты
Readme 72 KiB
Languages
desktop 100%