Туннелирование WireGuard поверх HTTP/2: скрытие VPN-трафика от DPI
Содержание
- Зачем это вообще нужно
- Как устроен WireGuard
- HTTP/2 как транспорт
- Протокол инкапсуляции
- Нарезаем пакет на фрагменты по 4096 байт
- Добавляем 4 байта длины и 4 байта порядкового номера
- Настройка серверной части
- Клиентская часть
- Обход DPI: маскировка под браузер
- Тестирование производительности
- Проблема с MTU и фрагментацией
- Кейс: обход блокировки в корпоративной сети
- Кейс: стабильность при плохом канале
- Кейс: противодействие активному анализу
- Ограничения и грабли
- Что дальше
- Практические советы
Зачем это вообще нужно
WireGuard — быстрый, современный, но его UDP-пакеты палятся любой мало-мальски настроенной DPI-системой. Фильтры вроде nDPI или xDPI распознают handshake по фиксированным полям — тип сообщения, длина, зарезервированные биты. После первого же пакета соединение режется или троттлится до состояния "хуже, чем ничего".
HTTP/2 при этом — легальный, массовый протокол. Его трафик никто не блокирует, потому что иначе рухнет половина интернета. Задача — спрятать WireGuard внутри HTTP/2-соединения, чтобы DPI видел обычный браузерный трафик.
Как устроен WireGuard
Протокол минималистичный. Один тип handshake-сообщения, один тип данных, один тип подтверждения. Каждый пакет — до 65535 байт, но обычно MTU 1420. Структура:
```
type (1 байт) | reserved (3 байта) | sender_index (4 байта) | payload
```
Type 1 — handshake initiation, type 2 — handshake response, type 4 — data. Все пакеты шифруются через ChaCha20-Poly1305. Ключи — Curve25519.
Проблема в том, что первый пакет (initiation) имеет фиксированный размер 148 байт. Всегда. Это как отпечаток пальца. DPI видит UDP-пакет 148 байт на нестандартном порту — всё, приехали.
HTTP/2 как транспорт
HTTP/2 работает поверх TCP. Фреймы — до 16384 байт по умолчанию (настраивается через SETTINGS_MAX_FRAME_SIZE, максимум 16777215). У нас есть потоковая передача данных — идеально для туннеля.
Ключевая фишка — multiplexing. Одно TCP-соединение, внутри — куча потоков. DPI видит обычный HTTPS-трафик, а мы внутри гоняем WireGuard-пакеты.
Протокол инкапсуляции
Простой вариант — каждый WireGuard-пакет кладём в DATA-фрейм HTTP/2. Но это палево: размеры пакетов совпадают с оригинальными, а DPI может считать статистику. Лучше — нарезать на куски по 4096 байт и перемешивать с мусором.
Реализация на Python с использованием h2-библиотеки:
```python
import h2.connection
import h2.config
import socket
import struct
H2_CONFIG = h2.config.H2Configuration(client_side=True, header_encoding='utf-8')
def wrap_wg_packet(packet: bytes, conn: h2.connection.H2Connection, stream_id: int) -> None:
Нарезаем пакет на фрагменты по 4096 байт
for i in range(0, len(packet), 4096):
chunk = packet[i:i+4096]
Добавляем 4 байта длины и 4 байта порядкового номера
framed = struct.pack('!II', len(chunk), i // 4096) + chunk
conn.send_data(stream_id, framed)
```
Настройка серверной части
Сервер принимает HTTP/2-соединение, извлекает WireGuard-пакеты и передаёт их в локальный сокет WireGuard. Конфиг nginx для терминирования TLS:
```nginx
server {
listen 443 ssl http2;
server_name vpn.example.com;
ssl_certificate /etc/letsencrypt/live/vpn.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vpn.example.com/privkey.pem;
ssl_protocols TLSv1.3;
ssl_ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384;
location /wg {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
}
```
Клиентская часть
Клиент — Python-скрипт, который читает пакеты из WireGuard-интерфейса через TUN-устройство и отправляет их через HTTP/2-соединение:
```python
import socket
import h2.connection
import h2.events
import threading
def read_tun(tun_fd, conn, stream_id):
while True:
packet = os.read(tun_fd, 65535)
wrap_wg_packet(packet, conn, stream_id)
conn.flush()
def main():
sock = socket.create_connection(('vpn.example.com', 443))
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('vpn.example.com', 443))
config = h2.config.H2Configuration(client_side=True)
conn = h2.connection.H2Connection(config=config)
conn.initiate_connection()
sock.sendall(conn.data_to_send())
headers = [
(':method', 'POST'),
(':path', '/wg'),
(':scheme', 'https'),
(':authority', 'vpn.example.com'),
]
stream_id = conn.get_next_available_stream_id()
conn.send_headers(stream_id, headers)
sock.sendall(conn.data_to_send())
tun_fd = open('/dev/net/tun', 'rb', buffering=0)
t = threading.Thread(target=read_tun, args=(tun_fd, conn, stream_id))
t.start()
```
Обход DPI: маскировка под браузер
Просто HTTP/2 — недостаточно. DPI смотрит на TLS-отпечатки (JA3/JA4). Если клиент использует нестандартный стек — сразу бан. Нужно имитировать реальный браузер.
Для этого используем TLS-библиотеку с кастомными настройками. Например, BoringSSL с параметрами Chrome. Или — на крайний случай — nghttp2 с подменой ALPN-списка.
Дополнительно — отправляем в потоке мусорные DATA-фреймы с рандомными размерами. Это сбивает статистические анализаторы, которые считают распределение размеров пакетов.
Тестирование производительности
Замеры на VPS с 1 Гбит/с каналом, CPU — 2 ядра Xeon E5-2680 v4:
| Метод | Скорость загрузки | Задержка (ping) | CPU load |
|-------|-------------------|-----------------|----------|
| Оригинальный WireGuard | 850 Мбит/с | 5 мс | 15% |
| WireGuard + HTTP/2, фрейм 16 КБ | 720 Мбит/с | 12 мс | 35% |
| WireGuard + HTTP/2, фрейм 4 КБ | 540 Мбит/с | 18 мс | 52% |
| OpenVPN + TCP 443 | 280 Мбит/с | 45 мс | 40% |
Потери от инкапсуляции — около 15% на больших фреймах. Но зато трафик неотличим от HTTPS.
Проблема с MTU и фрагментацией
WireGuard-пакеты — до 1420 байт. HTTP/2-фреймы — до 16384. При нарезке на 4096-байтные куски теряем до 3% пропускной способности из-за заголовков.
Решение — использовать динамический размер фрейма. Если средний размер пакета меньше 2048 байт (типичный сценарий для веб-сёрфинга), нарезаем на 2048. Для больших передач — увеличиваем до 8192. Адаптивный алгоритм:
```python
adaptive_threshold = 4096
def get_frame_size(current_throughput):
if current_throughput > 500 * 1024 * 1024:
return 8192
elif current_throughput > 100 * 1024 * 1024:
return 4096
else:
return 2048
```
Кейс: обход блокировки в корпоративной сети
Пример: компания с DPI-фильтром на базе nDPI 4.2. Фильтр режет UDP-трафик на нестандартных портах и блокирует WireGuard по подписи handshake-пакета.
Настройка: клиент — Windows 10 с TUN-драйвером WireGuard, сервер — Debian 12 с nginx 1.24 и кастомным HTTP/2-прокси. Весь трафик идёт через порт 443, TLS 1.3, ALPN — h2.
Результат: DPI видит обычный HTTPS-трафик. Никаких блокировок, скорость — 320 Мбит/с на канале 500 Мбит/с. Пинг до внутренних ресурсов — 11 мс против 4 мс без туннеля.
Кейс: стабильность при плохом канале
Мобильный оператор с QoS-политиками. UDP-трафик троттлится до 64 Кбит/с после 100 МБ. TCP-трафик — без ограничений.
Решение: HTTP/2-туннель с keep-alive фреймами каждые 30 секунд. При потере TCP-соединения — автоматическое переподключение с новым TLS-сеансом. Средняя скорость — 45 Мбит/с на канале 100 Мбит/с, стабильность — 99.2% (потери пакетов не превышают 0.3%).
Кейс: противодействие активному анализу
Провайдер с системой активного анализа трафика — отправляет HTTP-запросы на подозрительные IP и анализирует ответы. Если сервер отвечает не так, как ожидается от веб-сервера — блокировка.
Решение: на сервере — полноценный nginx с реальным сайтом. Для туннеля — отдельный endpoint, который маскируется под API-запрос. Ответы на не-туннельные запросы — стандартные JSON-ошибки. DPI видит легитимный API.
Ограничения и грабли
HTTP/2 — TCP. WireGuard — UDP. Любая потеря пакета в TCP вызывает retransmission и головную боль. На каналах с потерями >1% скорость падает в разы. Для мобильных сетей — обязательно включать BBR вместо CUBIC.
Ещё одна проблема — head-of-line blocking. Один потерянный пакет задерживает все последующие. Для интерактивного трафика (SSH, игры) это критично. Решение — отдельные HTTP/2-потоки для разных типов данных.
Что дальше
Эволюция DPI не стоит на месте. Уже существуют системы, которые анализируют поведение потоков — время между пакетами, размеры, частоту. HTTP/2-туннель спасает от статических фильтров, но против поведенческого анализа нужен более хитрый подход.
Идея — смешивать реальный браузерный трафик с туннельными данными. Клиент открывает несколько HTTP/2-соединений: одно — настоящий Chrome, другое — туннель. DPI видит два соединения с одного IP, но не может определить, какое из них фейковое.
Ещё направление — использование HTTP/3 (QUIC). Он работает поверх UDP, но с TLS 1.3 внутри. Для DPI это ещё сложнее — QUIC-трафик массовый, и блокировать его никто не будет. WireGuard-пакеты внутри QUIC-стримов — фактически неотличимы от обычного видео-трафика.
Практические советы
Используйте nginx как фронтенд, а не пишите свой HTTP/2-сервер. Он уже умеет терминировать TLS, управлять потоками, обрабатывать ошибки. Ваш код — только проксирование данных между nginx и локальным WireGuard-сокетом.
Не забывайте про keep-alive. HTTP/2-соединение должно жить долго, иначе DPI заподозрит неладное — браузеры не пересоздают соединения каждые 5 минут.
Тестируйте с реальным DPI. nDPI, xDPI, Cisco NBAR — все по-разному анализируют трафик. То, что работает против одного, может не работать против другого.