Зачем QUIC
HTTP/2 решил большую часть проблем HTTP/1.1 (мультиплексирование, бинарность, header compression). Но HTTP/2 поверх TCP имеет фундаментальный изъян: head-of-line blocking на уровне транспорта.
Если в TCP-stream'е потерян один пакет, весь поток ждёт ретрансмита. HTTP/2 умеет parallel-streams в приложении, но всё это идёт через один TCP-сокет, и потерянный пакет блокирует все streams сразу.
В мобильных сетях это особенно болезненно: высокий jitter, частые потери. И HTTP/2 на мобиле часто медленнее HTTP/1.1.
QUIC решает это, двигая транспорт в user-space над UDP:
- Мультиплексирование на уровне самого транспорта
- TLS 1.3 встроен (нет отдельного handshake'а)
- Connection ID вместо 4-tuple (соединение переживает смену IP)
- 0-RTT resume
Чем QUIC отличается от TCP
| Признак | TCP+TLS | QUIC |
|---|---|---|
| Транспорт | в kernel | user-space (поверх UDP) |
| TLS | отдельный layer (RFC 5246) | встроен (TLS 1.3 message в QUIC frame) |
| Handshake до encrypted-данных | 2-3 RTT | 1 RTT (или 0-RTT для известных) |
| Stream multiplexing | нет (один поток) | да, независимые streams |
| HOL-blocking | да (на уровне TCP) | нет (между streams) |
| Connection ID | 4-tuple (src/dst IP:port) | random ID, переживает смену IP |
| Где крутится | OS (sendfile-friendly) | user-space (CPU нагрузка выше) |
Handshake QUIC vs TCP+TLS
TCP+TLS 1.3:
client server
│ ── SYN ────────────►│
│ ◄── SYN/ACK ────────│ <- 1 RTT TCP
│ ── ACK + ClientHello ─►│
│ ◄── ServerHello + Cert │ <- 2 RTT для encrypted
│ ── Finished + HTTP req ►│
Total: ~2 RTT до данных
QUIC:
client server
│ ── Initial(ClientHello) UDP ──►│
│ ◄── Initial(ServerHello+Cert) ─│ <- 1 RTT
│ ── Handshake done + HTTP req ─►│
Total: ~1 RTT
С 0-RTT (resume известного сервера), данные можно слать сразу с первым пакетом. Резко меньше задержка для repeat-visit'ов.
0-RTT, и его опасности
При повторном connect'е клиент использует session-ticket с прошлой сессии и шлёт early data ещё до подтверждения handshake'а.
Замечательно для performance, но:
- Replay-attack: атакующий может перехватить early-data и переслать. Если запрос меняет state (POST), реплей опасен.
- Server-side нужно ограничить: only idempotent-методы (GET, HEAD), либо полный replay-protection через nonce.
В nginx:
ssl_early_data on;
# И блокировать non-idempotent методы:
if ($request_method !~ ^(GET|HEAD)$) {return 405;
}
Stream multiplexing
В QUIC есть streams, независимые двунаправленные потоки внутри одного connection'а. Каждый stream имеет свой ID, свои retransmit'ы, свой flow control.
QUIC Connection
├── Stream 0 (control)
├── Stream 4 (HTTP request 1), потерял пакет, ретрансмитит
├── Stream 8 (HTTP request 2), продолжает работать
└── Stream 12 (HTTP request 3), продолжает работать
В TCP всё это было бы заблокировано на одном пакете. В QUIC только Stream 4.
Connection migration
Connection identifier, random byte-string в QUIC header'е, не привязанный к IP. Клиент сменил Wi-Fi на 4G:
- source IP меняется
- source UDP port меняется
- Connection ID остаётся тот же
Сервер видит новый IP с тем же CID → молча обновляет 4-tuple, соединение продолжается. TCP бы порвался и пришлось бы переустанавливать.
Полезно для:
- Mobile-приложений (метро/улица)
- Long-running uploads
- WebRTC over QUIC
QPACK, header compression без HOL
HTTP/2 использует HPACK, компрессия с общим dictionary. Проблема: оба endpoint'а должны знать состояние dictionary, иначе декодирование сломается. На потерянных пакетах в HTTP/2-over-TCP это работало (TCP гарантирует order). В QUIC не работает (streams независимы).
QPACK, переработка HPACK: dictionary в отдельном stream'е, с координацией updates. Сложнее, но позволяет header-compression без HOL.
HTTP/3, это HTTP-семантика над QUIC
HTTP/3, это «как HTTP/2 запросы и ответы упакованы в QUIC streams». Семантика та же:
- Methods (GET/POST/...), URL, headers, body
- Status codes (200/404/...)
Отличия от HTTP/2:
- QPACK вместо HPACK (header compression)
- Server Push не поддерживается (snimали с roadmap)
- Каждый HTTP request в своём QUIC stream
Для приложения разница минимальна, те же curl https://...,
тот же fetch() в браузере. Транспорт делает за вас.
ALPN-discovery, как клиент узнаёт что сервер умеет h3
Браузер сначала идёт по HTTPS/TCP, при handshake получает
заголовок Alt-Svc:
Alt-Svc: h3=":443"; ma=86400
Это говорит «попробуй h3 на UDP/443 в следующий раз». Браузер кэширует и при следующем запросе сразу использует QUIC.
Альтернатива, HTTPS DNS RR (RFC 9460): сервер декларирует поддержку h3 в DNS:
example.com. 300 IN HTTPS 1 . alpn="h3,h2"
Поддержка в стеке
| Софт | Версия с h3 | Note |
|---|---|---|
| nginx | 1.25+ | полная поддержка |
| Caddy | 2.6+ | default-on |
| Apache | mod_http3 (експ.) | в работе |
| HAProxy | 2.6+ | через quictls |
| curl | 7.66+ с --http3 | требует quiche/msh3 backend |
| wget2 | 2.0+ | да |
| Cloudflare/Fastly/Cloud LB | да, default | прозрачно |
| Chrome / Firefox / Safari | да, по умолчанию | feature-detection |
Включить HTTP/3 в nginx
server {listen 443 ssl; # TCP/TLS
listen 443 quic reuseport; # UDP/QUIC
ssl_protocols TLSv1.3; # QUIC требует TLS 1.3
ssl_certificate /etc/ssl/fullchain.pem;
ssl_certificate_key /etc/ssl/privkey.pem;
add_header Alt-Svc 'h3=":443"; ma=86400';
ssl_early_data on; # 0-RTT
# ... остальное идентично TCP
}
Не забыть открыть UDP/443 в firewall'е!
ufw allow 443/udp
iptables -A INPUT -p udp --dport 443 -j ACCEPT
Производительность
Реальные цифры (зависят от RTT и потерь):
- Low RTT, low loss (датацентр): QUIC чуть медленнее TCP (CPU overhead user-space)
- High RTT, low loss (CDN, edge): QUIC выигрывает за счёт меньшего handshake'а
- High RTT, high loss (мобильная сеть): QUIC выигрывает значительно, нет HOL blocking
Cloudflare замеряли: QUIC в среднем на 5-15% быстрее на page-load для мобильных, до 30% на плохих сетях. Для cable/fiber, статпогрешность.
CPU: QUIC потребляет на 30-50% больше CPU чем TCP, потому что user-space (нет kernel sendfile, нет TSO/GSO до недавнего offload). В 2026 многие NIC и kernel-version'ы получают QUIC offload (UDP-segmentation offload + TLS-offload), разрыв сокращается.
Запрет middlebox-вмешательства
TCP открыт для middlebox-инспекции (заголовки в plaintext, sequence numbers видно). QUIC зашифровано почти полностью: только connection-ID и часть header'а в открытом виде. Middlebox не может:
- Modify packets (broken)
- Track state (только видит UDP-flow)
- DPI на содержимое
С точки зрения op-team:
- Firewall должен пропускать UDP/443 (часто блокировано «по привычке» enterprise-проксями)
- NAT-таблицы должны правильно ageing UDP-flow (default 30s мало для QUIC, лучше 5+ min)
- DPI/IDS теряет visibility, security ratio shifted
Когда что-то пошло не так
- Браузер не использует h3, нет
Alt-Svcheader'а или DNS HTTPS RR. Curl:curl --http3 -v https://example.com. quictlsне собирается, nginx требует BoringSSL/quictls (форки OpenSSL с QUIC API). Стандартный OpenSSL до 3.5 не поддерживает.- UDP/443 не работает, firewall (90% случаев), либо load balancer не пропускает UDP. AWS ALB не поддерживает h3, нужен NLB.
- 0-RTT replay-атака, смотри предупреждения выше. Никогда не разрешай non-idempotent через 0-RTT.
- CPU нагрузка на QUIC-сервере высокая, включить kernel UDP-GSO, TLS-offload где доступно.
- Connection migration не работает, некоторые middlebox'ы дропают пакеты с непривычным CID. Чаще всего работает прозрачно.
- Старые клиенты не могут подключиться, QUIC v1 ≠ draft-29. nginx 1.25+ только v1, старые curl-builds, draft-29.
Когда выбирать QUIC
- Mobile-heavy traffic, реальный win
- High-RTT клиенты (другие континенты), handshake выигрывает
- Many small requests, мультиплексирование без HOL
Когда не выбирать или подождать:
- Внутренний RPC в датацентре, TCP+TLS proven, QUIC-overhead не оправдан
- Long-running streams (file upload, video), TCP неплохо справляется
- Если стек/firewall/observability не готовы, добавит сложности