Почему IPv6-прокси течёт через QUIC/HTTP3
Содержание
- Как Chrome узнаёт про HTTP/3
- Что происходит на уровне сокетов
- Почему IPv6 усугубляет картину
- Что видит прокси-сервер
- Как перекрыть утечку на клиенте
- Блокировка UDP 443 на файрволе
- SOCKS5 и UDP ASSOCIATE
- MTU и фрагментация
- Кейс: корпоративная сеть на 4000 машин
- Кейс: публичный прокси и жалобы на «медленно»
- Кейс: балансировщик режет QUIC
- Что делать, если QUIC нужен
- Таймауты и keep-alive для QUIC через релей
- Итог
Браузер отправил запрос через прокси. TCP-соединение установлено, заголовки ушли, ответ вернулся. А потом в логах прокси — тишина, при этом страница грузится. Классика. Chrome решил, что HTTP/3 быстрее, и пошёл напрямую по UDP 443. Прокси в этой истории — зритель.
Проблема не в IPv6 как таковом. Проблема в том, что прокси-протоколы исторически завязаны на TCP, а QUIC живёт поверх UDP. И если вы не заблокировали UDP на уровне политики, браузер найдёт лазейку.
Как Chrome узнаёт про HTTP/3
Механизм простой и потому коварный. При первом HTTPS-запросе сервер отдаёт заголовок:
```
alt-svc: h3=":443"; ma=86400
```
Это значит: «я умею HTTP/3 на порту 443, доверяй мне сутки». Chrome кэширует эту информацию. При следующем визите к тому же origin он пробует QUIC. Если получается — переключается. Если нет — откатывается на TCP.
Ключевой момент: `alt-svc` не учитывает, что вы сидите за прокси. Браузер воспринимает его как прямое указание. Прокси-настройки применяются к TCP-соединениям, а QUIC-сессия стартует в обход.
Проверить, что сервер отдаёт `alt-svc`, можно обычным curl:
```bash
curl -sI https://example.com | grep -i alt-svc
```
Ответ вида `alt-svc: h3=":443"; ma=86400` — сигнал, что браузер попробует UDP.
Что происходит на уровне сокетов
Когда Chrome решает идти по QUIC, он открывает UDP-сокет и шлёт Initial-пакет на IP сервера. Destination IP — реальный адрес сайта, не адрес прокси. Прокси-конфигурация тут не участвует вообще: она описывает, как установить TCP-туннель, а не UDP-ассоциацию.
Вот как это выглядит в `strace` на клиенте:
```
socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP) = 45
connect(45, {sa_family=AF_INET6, sin6_port=htons(443),
sin6_addr=inet_pton("2606:4700:...")}, 28) = 0
sendto(45, "\xc0\x00\x00\x00\x01...", 1252, 0, ...) = 1252
```
Обратите внимание: `AF_INET6`, `SOCK_DGRAM`, порт 443. Никакого упоминания прокси. Если ваш файрвол пропускает исходящий UDP на 443 — трафик утёк.
Почему IPv6 усугубляет картину
С IPv4 всё чуть проще: многие провайдеры и корпоративные сети исторически режут исходящий UDP, кроме DNS. С IPv6 такой привычки нет. Сети, которые поднимали IPv6 «по-нормальному», часто оставляют политику по умолчанию — а default в большинстве файрволов разрешающий.
Плюс адресация. В IPv6 у клиента обычно глобальный адрес, и NAT нет. Значит, обратный путь для QUIC-пакетов открыт без всяких там hole punching. Сервер видит реальный адрес клиента, отвечает напрямую. Прокси остаётся не у дел.
Ещё нюанс: IPv6-префиксы часто маршрутизируются напрямую, без CGNAT. Это хорошо для latency, плохо для контроля. Если в IPv4 вы могли спрятаться за NAT и надеяться, что UDP не пройдёт, то в IPv6 надежды нет.
Что видит прокси-сервер
Ничего. Это и есть главная проблема. Прокси получает только TCP-соединения. QUIC-сессии идут мимо, и в логах вы видите разрыв: пользователь авторизовался, сделал пару запросов, а потом — тишина на два часа. При этом пользователь активно работает.
Мониторинг прокси показывает падение трафика, алерты срабатывают, дежурный чешет затылок. А браузер просто переключился на HTTP/3 и шлёт всё напрямую.
Пример: сервер на nginx 1.24 с `listen 443 quic reuseport;` и заголовком `add_header Alt-Svc 'h3=":443"; ma=86400';`. Клиент заходит через HTTP-прокси. Первый запрос идёт через прокси по TCP, получает `alt-svc`, и все последующие — уже UDP в обход. В логах прокси — один запрос вместо сотни.
Как перекрыть утечку на клиенте
Самый надёжный способ — отключить QUIC в браузере на уровне политики. Chrome понимает policy-файл:
```json
{
"QuicAllowed": false
}
```
Файл кладётся в `/etc/opt/chrome/policies/managed/quic.json` на Linux, в реестр на Windows. После перезапуска браузера `chrome://policy` покажет применённую политику, а `chrome://net-internals/#quic` — что QUIC отключён.
Firefox управляется через `about:config`: параметр `network.http.http3.enable` в `false`. Для корпоративного развёртывания — `policies.json` с ключом `HTTP3: false`.
Но это работает только если вы контролируете браузер. В BYOD-сценариях пользователь поставит себе что угодно. Значит, нужен сетевой уровень.
Блокировка UDP 443 на файрволе
Правило простое: режем весь исходящий UDP на 443, кроме явно разрешённых адресов. Для nftables:
```bash
nft add rule inet filter output udp dport 443 ip6 daddr != 2001:db8::10 drop
```
Для iptables (ip6tables):
```bash
ip6tables -A OUTPUT -p udp --dport 443 ! -d 2001:db8::10 -j DROP
```
Здесь `2001:db8::10` — адрес вашего прокси, если он умеет UDP-ассоциации (например, SOCKS5 с UDP ASSOCIATE). Всё остальное — в топку.
Побочный эффект: Chrome попытается, получит таймаут, откатится на TCP. Задержка первого запроса — примерно 300 мс (один RTO по умолчанию). Дальше браузер запомнит, что QUIC не работает, и не будет пытаться.
SOCKS5 и UDP ASSOCIATE
SOCKS5 умеет пробрасывать UDP через команду `UDP ASSOCIATE`. Это единственный штатный способ пустить QUIC через прокси. Работает так: клиент открывает TCP-соединение к прокси, шлёт `UDP ASSOCIATE`, получает адрес и порт релея, и дальше шлёт UDP-датаграммы на этот адрес с инкапсуляцией.
Проблема в поддержке. Большинство публичных SOCKS5-прокси `UDP ASSOCIATE` не реализуют. Даже если заявлено — часто ломается на IPv6 или режет пакеты больше MTU. Проверить можно так:
```python
import socket
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
s.connect(("2001:db8::10", 1080))
s.sendall(b"\x05\x01\x00")
assert s.recv(2) == b"\x05\x00"
s.sendall(b"\x05\x03\x00\x01" + b"\x00" * 4 + b"\x00\x00")
resp = s.recv(10)
print("REP:", resp[1])
```
`REP=0x00` — UDP-релей работает. `REP=0x07` — команда не поддерживается. Всё, что между — смотрите RFC 1928.
Если релей есть, QUIC пойдёт через прокси. Но добавляется overhead: каждый датаграм нужно инкапсулировать, а MTU уменьшается на размер заголовка. Для QUIC с его 1200-байтовыми Initial-пакетами это критично.
MTU и фрагментация
QUIC по умолчанию использует Initial-пакеты размером 1200 байт. Это нижняя граница, чтобы проходить через большинство сетей без PMTUD. Но если вы гоните трафик через UDP-релей, к пакету добавляется заголовок SOCKS5 — минимум 10 байт для IPv4, 22 для IPv6.
Итого: 1200 + 22 = 1222 байта полезной нагрузки в UDP. Плюс заголовки IP (40 байт для IPv6) и UDP (8 байт). Суммарно 1270. Если MTU на пути 1280 (минимум для IPv6), остаётся 10 байт запаса. Любая инкапсуляция сверху — и пакеты начнут дропаться.
Решение: либо поднимать MTU на туннелях, либо заставлять QUIC использовать меньший `max_udp_payload_size`. Второе настраивается на сервере, не на клиенте. Так что чаще проще заблокировать UDP 443 и жить спокойно.
Кейс: корпоративная сеть на 4000 машин
Проблема: отдел безопасности заметил, что часть трафика уходит в обход прокси. DLP не видит, что качают сотрудники. Логи прокси показывают 30% от ожидаемого объёма.
Причина: в сети развернули IPv6 через RA, но политику файрвола скопировали с IPv4-сегмента. А там исходящий UDP был закрыт ещё в 2018-м, когда ловили сверхурочный трафик. В IPv6-сегменте default policy — accept.
Технические детали: Chrome 118 на Windows 11, `alt-svc` от Cloudflare и Google, UDP 443 открыт. Замеры показали: 28% HTTPS-трафика шло по HTTP/3 напрямую. Средний размер сессии — 4.2 МБ, из них через прокси — 1.1 МБ.
Решение: добавили правило `ip6tables -A FORWARD -p udp --dport 443 -j DROP` на пограничном маршрутизаторе. Плюс GPO с `QuicAllowed: false` для доменных машин. Через сутки доля трафика через прокси выросла до 97%. Оставшиеся 3% — легитимные сервисы, которым UDP 443 разрешён точечно.
Кейс: публичный прокси и жалобы на «медленно»
Проблема: пользователи публичного IPv6-прокси жаловались, что YouTube тормозит. Скорость — 2-3 Мбит/с вместо ожидаемых 50.
Причина: прокси не поддерживал UDP ASSOCIATE. Браузер пробовал QUIC напрямую, получал ответ от YouTube, но часть пакетов терялась из-за кривой маршрутизации в сети клиента. Chrome упорно не откатывался на TCP, потому что QUIC формально работал.
Технические детали: сервер на Ubuntu 22.04, прокси — 3proxy 0.9.4, IPv6-only. `tcpdump` на клиенте показал: 40% QUIC-пакетов уходит с ретрансмиссиями, RTT скачет от 80 до 900 мс. При этом TCP-сессии через прокси — стабильные 60 мс.
Решение: на прокси добавили заголовок `Alt-Svc: clear` в ответы для HTTPS. Это заставляет браузер забыть про HTTP/3 для данного origin. Плюс в документации прописали: для стабильной работы отключите QUIC в браузере.
```
add_header Alt-Svc 'clear' always;
```
Эффект: жалобы прекратились в течение недели, по мере обновления кэша `alt-svc` у клиентов.
Кейс: балансировщик режет QUIC
Проблема: приложение за HTTP/3-балансировщиком (HAProxy 2.8 с QUIC-поддержкой) работало нестабильно. Часть запросов зависала на 30 секунд.
Причина: балансировщик слушал UDP 443 на IPv6-адресе, но health-check ходил по TCP. Бэкенды считались живыми, хотя QUIC-порт на них был закрыт после обновления.
Технические детали: HAProxy в режиме `mode quic`, два бэкенда за ним. Один бэкенд после рестарта поднял только TCP 443, UDP не стартовал из-за конфликта с systemd-resolved на порту 53. Балансировщик шлёт Initial-пакеты, получает ICMPv6 port unreachable, но по таймауту (30 с) всё равно отдаёт ошибку клиенту.
Решение: перевели health-check на UDP с явным QUIC Initial-пакетом, уменьшили `timeout connect` до 3 с. Плюс на бэкендах зафиксировали порты для systemd-resolved через `DNSStubListener=no`.
```
backend quic_backends
mode quic
timeout connect 3s
server s1 [2001:db8::21]:443 check
server s2 [2001:db8::22]:443 check
```
Что делать, если QUIC нужен
Иногда заблокировать нельзя. Видеоконференции, некоторые CDN, внутренние сервисы на HTTP/3. Тогда нужен прокси с полноценной поддержкой UDP.
Варианты: SOCKS5 с UDP ASSOCIATE (3proxy, Dante), либо туннель на уровне IP (WireGuard, IPsec). Второе надёжнее — весь IP-трафик идёт через туннель, QUIC внутри него, прокси видит всё.
WireGuard-подход проще в эксплуатации: один интерфейс, маршрут `::/0` через туннель, никаких браузерных политик. Минус — это уже не прокси в классическом смысле, а VPN. Для сценариев, где нужен именно HTTP-прокси с логированием, придётся комбинировать: SOCKS5 для UDP плюс HTTP-прокси для TCP, и надеяться, что клиент правильно выберет.
На практике lexic.ml с IPv6-адресами работает именно в такой связке: TCP-туннель для обычного трафика, UDP-релей для QUIC. Но настройка `UDP ASSOCIATE` требует внимания к MTU и таймаутам — об этом ниже.
Таймауты и keep-alive для QUIC через релей
QUIC использует собственный механизм keep-alive — PING-кадры. Интервал задаётся параметром `max_idle_timeout` в транспортных параметрах, по умолчанию 30 секунд. Если UDP-релей держит состояние дольше, чем живёт QUIC-сессия, буферы забиваются мусором.
Настройка релея в 3proxy:
```
socks -p1080 -i2001:db8::10
flush
```
`flush` сбрасывает буферы после каждого пакета — медленно, но надёжно для низкой нагрузки. Для высокой лучше поднять `-b` (буфер) и мониторить дропы.
Проверить потери на пути можно через `mtr` с UDP:
```bash
mtr --udp --port 443 -6 2606:4700::1111
```
Если потери выше 2% — QUIC будет ретрансмитить, latency вырастет. В таком случае лучше откатиться на TCP и не мучить пользователей.
Итог
QUIC не «обходит» прокси злонамеренно. Он просто работает по другой транспортной модели, а прокси-протоколы эту модель не покрывают. Если UDP 443 открыт — браузер уйдёт в QUIC при первой возможности.
Контроль сводится к трём уровням: политика браузера (отключить QUIC), сетевой файрвол (закрыть UDP 443), прокси-инфраструктура (SOCKS5 с UDP ASSOCIATE, если QUIC действительно нужен). Выбор зависит от того, насколько вы контролируете клиентов и какие сервисы должны работать.
Начинать стоит с файрвола. Это единственный уровень, который не зависит от настроек браузера и не ломается при обновлениях Chrome.