← Назад в базу знаний

- Почему HTTP/3 (QUIC) ломается через SOCKS5-прокси: разбор на уровне UDP и как чинить

- Почему HTTP/3 (QUIC) ломается через SOCKS5-прокси: разбор на уровне UDP и как чинить

Что вообще происходит с QUIC

QUIC работает поверх UDP. Не TCP. Это первое, что ломает мозг, когда настраиваешь прокси по привычке. SOCKS5 умеет три команды: CONNECT (TCP), BIND (TCP), UDP ASSOCIATE. Большинство клиентов и серверов реализуют только первую. Вторая почти нигде не встречается. Третья — редкость, и работает через одно место.

Когда браузер видит заголовок `alt-svc: h3=":443"`, он пробует установить QUIC-соединение. Пакет уходит на UDP/443. Если ваш прокси SOCKS5 не поддерживает UDP ASSOCIATE или клиент не умеет его прокидывать — пакет просто умирает. Браузер ждёт 200-300 мс, не получает ответа, откатывается на TCP с HTTP/2. Иногда откат занимает до 2 секунд, если выставлены агрессивные таймауты. Пользователь видит "сайт тормозит", а в логах — тишина.

Проверить просто. Откройте `chrome://net-export/`, запишите лог, зайдите на любой сайт с h3. В логе увидите `QUIC_SESSION` с состоянием `QUIC_CONNECTION_CLOSED` или `QUIC_HANDSHAKE_FAILED` через 250-400 мс после старта. Это и есть тот самый откат.

Как SOCKS5 на самом деле относится к UDP

RFC 1928 описывает UDP ASSOCIATE так: клиент шлёт запрос с `CMD=0x03`, сервер отвечает адресом и портом релея, клиент шлёт UDP-датаграммы на этот релей с заголовком из 10 байт (RSV, FRAG, ATYP, DST.ADDR, DST.PORT). Реализаций — кот наплакал.

Вот что реально поддерживают популярные решения:

| Решение | Версия | UDP ASSOCIATE | Замечания |

|---|---|---|---|

| Dante | 1.4.3 | Да | Только IPv4-релей, IPv6 через отдельный конфиг |

| 3proxy | 0.9.4 | Да | Нужен флаг `-u`, по умолчанию выключен |

| SSH `-D` | OpenSSH 9.6 | Нет | Только TCP, UDP не проксируется |

| Shadowsocks | 3.3.5 | Частично | Через отдельный UDP-порт, не через SOCKS |

| curl | 8.5 | Нет | `--socks5` только TCP |

| Python `PySocks` | 1.7.1 | Да | Через `socks.socksocket` с `SOCK_DGRAM` |

Если ваш прокси в левой колонке без галочки — HTTP/3 через него не пойдёт. Точка. Браузер будет молча откатываться на HTTP/2, и никто не поймёт, почему CDN с h3 грузится медленнее, чем с h2.

Почему curl и большинство библиотек молчат

`curl --socks5-hostname` открывает TCP-туннель к цели и гонит туда HTTP. Для QUIC нужен `--http3` плюс UDP-проброс, а curl до версии 8.6 вообще не умел комбинировать SOCKS5 и HTTP/3. В 8.6 добавили экспериментальную поддержку, но она требует сборки с `--with-ngtcp2` и работает только если SOCKS-сервер поддерживает UDP ASSOCIATE.

Попробуйте такой запрос на curl 8.5:

```bash

curl --http3 --socks5 127.0.0.1:1080 https://cloudflare.com -v

```

Получите в ответ что-то вроде `HTTP/3 not supported` или тихий фолбэк на h2. В stderr будет `Could not connect to server` через 300 мс. Это не баг curl — это отсутствие UDP-релея на стороне прокси.

Python-стек ещё веселее. `httpx` с `http3=True` требует `aioquic`, а тот не умеет SOCKS5 из коробки. Приходится вручную поднимать UDP-сокет через PySocks и скармливать его в `aioquic.connect()`. Кода — на полсотни строк, и половина — обход грабель с таймаутами.

Что ломается на уровне пакетов

QUIC-хендшейк — это Initial пакет от клиента, ответ Initial+Handshake от сервера, потом 1-RTT. Клиент шлёт первый пакет размером 1200 байт (минимальный Initial по RFC 9000). Если MTU на пути меньше — фрагментация на уровне IP. Через SOCKS5-релей это часто критично: релей принимает датаграмму, разбирает 10-байтный заголовок, пересылает дальше. Если датаграмма фрагментирована — релей видит только первый фрагмент, второй теряется.

Отсюда классика: `QUIC_HANDSHAKE_FAILED` через 250 мс. Клиент отправил Initial, сервер получил только половину, ответа нет. Клиент ретрансмитит с экспоненциальным backoff: 250, 500, 1000 мс. На третьей попытке сдаётся и откатывается на TCP.

Проверить MTU можно так:

```bash

ping -M do -s 1472 -c 3 example.com

```

1472 — это 1500 минус 20 (IP) минус 8 (UDP). Если пакеты теряются — на пути MTU меньше 1500, и QUIC будет страдать. Для SOCKS5-релея критично, чтобы он не резал датаграммы больше 1280 байт (минимальный MTU для IPv6 по RFC 8200). Многие самописные релеи режут на 512 или 1024 — и всё, QUIC не поднимается.

Реальный кейс: nginx как reverse proxy перед SOCKS

Пример: сервер на nginx 1.24 с `listen 443 quic reuseport;` и `proxy_pass` на бэкенд. Пользователь идёт через SOCKS5-прокси на 1080. Браузер шлёт QUIC Initial на UDP/443. Прокси не поддерживает UDP ASSOCIATE — пакет уходит в никуда. Браузер ждёт 300 мс, откатывается на TCP. nginx видит только TCP-соединение, `\$server_protocol` = `HTTP/2.0`. В логах — ни одного h3-запроса, хотя `alt-svc` отдаётся.

Причина: SOCKS5-сервер собран без `--enable-udp`. В конфиге Dante параметр `socksmethod: none` и `external.rotation: none`, а UDP-релей не включён. Решение: добавить в `danted.conf`:

```

socksmethod: none

clientmethod: none

external: eth0

internal: eth0 port = 1080

udp.portrange: 40000-45000

```

И запустить с `-u`. После этого браузер получает Initial-ответ, хендшейк проходит за 80-120 мс, h3 работает. Но есть нюанс: Dante релеит UDP только для IPv4-клиентов. Для IPv6-целей нужен отдельный `udprelay`-блок.

Кейс с IPv6-прокси и dual-stack

Прокси на IPv6-адресе, клиент — dual-stack. Браузер резолвит `example.com` в AAAA, шлёт QUIC Initial на IPv6-адрес через SOCKS5. Прокси принимает UDP-датаграмму, но в заголовке `ATYP=0x04` (IPv6). Не все релеи это понимают — некоторые парсят только `ATYP=0x01` (IPv4) и `0x03` (домен). Датаграмма дропается молча.

Симптом: h3 работает для IPv4-сайтов, но не для тех, у кого только AAAA. Или наоборот. Проверяется через `tcpdump` на стороне прокси:

```bash

tcpdump -i eth0 -n 'udp port 1080' -w socks-udp.pcap

```

В pcap увидите датаграммы с `ATYP=0x04`, которые релей не обрабатывает. Решение — либо апгрейд релея (3proxy 0.9+ умеет), либо форсить IPv4 в браузере через `--disable-ipv6`, либо использовать прокси, который корректно работает с обеими семьями.

Как чинить: варианты по убыванию костыльности

Первый и самый простой — отключить h3 на клиенте. В Chrome: `chrome://flags/#enable-quic` → Disabled. В Firefox: `network.http.http3.enabled` → false. Хреново, но работает. Теряете 15-30% скорости на lossy-сетях, зато никаких откатов.

Второй — поднять UDP-релей отдельно от SOCKS5. Shadowsocks, V2Ray, sing-box умеют это из коробки. Схема: TCP идёт через SOCKS5, UDP — через отдельный туннель. Браузер об этом не знает, но `alt-svc` резолвится в тот же хост, и UDP-пакеты попадают куда надо. Настройка в sing-box:

```json

{

"inbounds": [{

"type": "socks",

"listen": "::",

"listen_port": 1080,

"udp": true

}],

"outbounds": [{

"type": "direct"

}]

}

```

Ключевой момент — `"udp": true`. Без него inbound принимает только TCP.

Третий — прокси, который умеет UDP ASSOCIATE нативно. Таких мало. На рынке с 2015 года есть решения вроде lexic.ml, где IPv6-прокси изначально строился с расчётом на UDP-трафик, включая QUIC. Это не реклама, а техническая деталь: если прокси не умеет UDP с рождения, прикрутить его потом — та ещё задача.

Почему IPv6 всё усложняет

QUIC поверх IPv6 требует, чтобы релей корректно парсил `ATYP=0x04` и не резал датаграммы меньше 1280 байт. Плюс PMTUD (Path MTU Discovery) в IPv6 работает иначе — фрагментация только на источнике, промежуточные роутеры шлют ICMPv6 Packet Too Big. Если релей эти ICMP не пропускает — клиент не узнает про снижение MTU и будет долбить пакетами 1200 байт, которые не проходят.

Ещё нюанс: `alt-svc` для IPv6-хостов часто указывает на отдельный порт. Браузер шлёт Initial на `[2001:db8::1]:443`, релей видит IPv6-адрес в заголовке и должен его распарсить. Половина самописных релеев на этом падает с `IndexError` в логах.

Диагностика: что смотреть в первую очередь

Начните с `chrome://net-internals/#quic` — там видно все QUIC-сессии, их состояние и время до закрытия. Если сессия живёт меньше 500 мс — почти наверняка проблема в UDP-релее.

Дальше — `tcpdump` на стороне прокси. Смотрите, доходят ли датаграммы до релея и уходят ли дальше. Если доходят, но ответа нет — проблема в маршрутизации UDP. Если не доходят — клиент вообще не шлёт UDP через прокси.

Третий шаг — тест через `curl --http3` напрямую (без прокси) и через прокси. Разница в результатах покажет, где именно рвётся.

```bash

curl --http3 -v https://cloudflare.com 2>&1 | grep -i quic

curl --http3 --socks5 127.0.0.1:1080 -v https://cloudflare.com 2>&1 | grep -i quic

```

Первый покажет `QUIC connection established`, второй — либо ошибку, либо таймаут.

Что в итоге

HTTP/3 через SOCKS5 — это всегда компромисс. Либо вы теряете h3 и живёте на h2, либо поднимаете UDP-релей и миритесь с его ограничениями. Третьего пути нет, потому что SOCKS5 по дизайну TCP-центричен, а QUIC — UDP-протокол.

Если h3 критичен — выбирайте прокси с нативной поддержкой UDP ASSOCIATE и проверяйте MTU на всём пути. Если нет — отключайте QUIC на клиенте и не мучайтесь. Полсекунды отката на каждый новый хост — это не катастрофа, но заметно на глаз.

✔️Купить прокси