HTTP/3 давно перестал быть экзотикой: все популярные браузеры умеют новый протокол, а крупные проекты отдают по нему страницы годами. На стороне собственного сервера картина скромнее. Штатный nginx из пакетов Debian или Ubuntu почти всегда собран без модуля QUIC, поэтому обойтись одной строчкой в конфиге не выйдет: веб-сервер придётся пересобрать из исходников, подобрать подходящую SSL-библиотеку, открыть UDP на 443-м порту и правильно анонсировать новую возможность браузерам. Ниже разобран весь путь от apt и git до зелёной метки h3 в инструментах разработчика.
Протокол QUIC и его плюсы в сравнении с классическим TCP соединением
HTTP/1.1 и HTTP/2 работают поверх TCP, и у TCP есть врождённая слабость: когда один пакет теряется в пути, застревают все данные, идущие следом, даже если они относятся к независимым запросам. Это называют блокировкой головы очереди. На быстром домашнем канале потери редки и проблема незаметна, но на перегруженной сотовой вышке каждый повторный обмен ощущается в каждой вкладке браузера.
QUIC решает задачу радикально. Транспорт переехал на UDP, а внутри одного соединения живут независимые потоки: потеря пакета в одном тормозит только его, остальные спокойно едут дальше. Рукопожатие TLS встроено в установление соединения, поэтому первый запрос уходит быстрее, а при повторном визите клиента работает режим 0-RTT, когда полезные данные летят уже в первом пакете. Соединение переживает и смену сети, например переход со Wi-Fi на мобильный интернет, без обрыва и повторной загрузки страницы.
Протокол вырос из экспериментов Google, затем IETF стандартизировала его: транспорт описан в RFC 9000, а сам HTTP/3 в RFC 9114. Nginx поддерживает именно стандартизованный вариант, собранный по инструкции ниже сервер говорит с браузерами на одном языке.
Поддержка HTTP/3 в Nginx и требования к SSL библиотеке
Поддержка QUIC и HTTP/3 появилась в nginx с версии 1.25.0 и живёт в модуле ngx_http_v3_module. Разработчики помечают модуль как экспериментальный, но за прошедшее время он добрался и до стабильной ветки, а его применяют на нагруженных проектах. Модуль не входит в сборку по умолчанию, его включают параметром --with-http_v3_module, поэтому пакеты из дистрибутивов о QUIC чаще всего не знают.
Главный подводный камень - SSL-библиотека. Минимум, при котором модуль собирается, это OpenSSL 1.1.1, но модный режим 0-RTT, управляемый директивой ssl_early_data, требует уже OpenSSL 3.5.1 и выше. С более старыми версиями nginx компилируется с внутренним слоем совместимости и просто не отдаёт ранние данные. До nginx 1.29.1 режим 0-RTT с OpenSSL не работал вовсе, независимо от значения директивы, поэтому старые сборки здесь не помогут. Кто не хочет ставить OpenSSL 3.5, собирает nginx против BoringSSL, QuicTLS или LibreSSL, там поддержка ранних данных есть без дополнительных условий.
Осенью 2026 актуальны две ветки: основная 1.31.6 и стабильная 1.30.5. Для QUIC лучше брать свежую: в ветке 1.29.x правили именно ошибки нового транспорта. Прежде чем пересобирать, полезно посмотреть, что уже стоит на сервере: команда nginx -V печатает аргументы конфигурирования, и если среди них нет --with-http_v3_module, сервер о QUIC не знает.
Сборка Nginx с модулем ngx_http_v3_module и библиотекой BoringSSL
Понадобятся компилятор, инструменты сборки и исходники двух проектов: самого nginx и SSL-библиотеки. BoringSSL собирается с помощью CMake и Ninja, поэтому начальный набор зависимостей выглядит так:
sudo apt update
sudo apt install -y build-essential cmake ninja-build golang \
git libpcre2-dev zlib1g-dev
Библиотека загружается и собирается в пару команд:
cd /usr/local/src
git clone https://boringssl.googlesource.com/boringssl
cd boringssl
cmake -B build -GNinja
ninja -C build
Дальше исходники nginx и конфигурирование с точным указанием путей к заголовкам и бинарникам BoringSSL. Рецепт из официальной документации выглядит так:
cd /usr/local/src
curl -O https://nginx.org/download/nginx-1.31.6.tar.gz
tar xf nginx-1.31.6.tar.gz
cd nginx-1.31.6
./configure --with-debug --with-http_v3_module \
--with-cc-opt="-I../boringssl/include" \
--with-ld-opt="-L../boringssl/build -lstdc++"
make -j$(nproc)
sudo make install
Пара тонкостей. Флаг --with-debug оставляет подробные журналы отладки, они незаменимы в борьбе с QUIC, а на производительности не сказываются. Параметр -lstdc++ нужен потому, что часть BoringSSL написана на C++. Тем, кому мил привычный интерфейс OpenSSL, подходит QuicTLS: библиотека собирается как обычная OpenSSL, а в configure подставляют свои пути к ней, схема та же. LibreSSL тоже рабочий вариант, все альтернативы перечислены в документации nginx. Номер версии в адресе архива меняют на актуальный из журнала изменений.
Настройка серверного блока с QUIC на 443 порту и анонсированием HTTP/3 браузерам
QUIC работает поверх UDP, и самое непривычное в настройке то, что один и тот же порт слушается дважды: как ssl для TCP и как quic для UDP. Рабочий минимальный блок выглядит так:
server {
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.3;
add_header Alt-Svc 'h3=":443"; ma=86400';
location / {
root /var/www/example.com;
}
}
Разбор по строчкам. Параметр reuseport обязателен, когда worker_processes больше единицы: без него несколько рабочих процессов конфликтуют за один UDP-сокет. Директива http2 on позволяет держать HTTP/2 для старых клиентов и HTTP/3 для новых одновременно. QUIC требует TLS 1.3, поэтому версию протокола указывают явно.
Заголовок Alt-Svc сообщает браузеру о поддержке HTTP/3. Первую страницу клиент забирает по HTTP/2 поверх TCP, видит в ответе анонс h3 на том же порту и запоминает его на сутки, ведь ma=86400 задаёт время хранения в секундах. Следующие запросы браузер пробует отправить по QUIC, а если UDP заблокирован на пути, тихо возвращается к TCP. Поэтому после правки конфигурации страницу обновляют дважды: первая загрузка раскладывает анонс, вторая уже ходит новым транспортом.
Сразу стоит включить журналирование протокола, иначе потом придётся гадать вслепую, ходит ли кто-нибудь по HTTP/3:
log_format quic '$remote_addr [$time_local] "$request" '
'$status $body_bytes_sent "$http3" '
'"$http_user_agent"';
access_log /var/log/nginx/access.log quic;
Переменная $http3 равна h3 для соединений по новому протоколу, hq для служебного режима и пустой строке, когда запрос пришёл по TCP.
Директивы управления QUIC от quic_retry до quic_gso и их настройка
Модуль принёс с собой россыпь директив, и по умолчанию они настроены консервативно. Короткая шпаргалка по главным выглядит так:
- quic_retry on включает проверку адреса клиента и защищает от амплификации, когда ответ на поддельный пакет летит крупнее самого запроса;
- ssl_early_data on разрешает 0-RTT, скорость растёт, но ранние запросы уязвимы к повторам, поэтому операции входа не должны опираться только на них;
- quic_gso on включает пакетную отправку через сегментацию и снижает нагрузку на процессор при больших исходящих потоках;
- quic_host_key закрепляет секретный ключ для токенов, чтобы они не отмирали при каждой перезагрузке сервера;
- http3_max_concurrent_streams задаёт число одновременных потоков соединения, по умолчанию 128, чего обычному сайту хватает с запасом.
Теперь детали. Амплификация - типовая беда UDP-протоколов: сервер обязан убедиться, что инициатор соединения владеет заявленным адресом, и пакеты Retry именно этим занимаются. Плата за проверку - один дополнительный обмен при первом соединении, для повторных визитов есть токены. При включённом quic_retry полезно задать и quic_host_key: по умолчанию ключ генерируется заново при каждом перезапуске и перечитывании конфигурации, из-за чего все выданные токены отмирают.
GSO расшифровывается как Generic Segmentation Offload: несколько QUIC-пакетов уходят одним системным вызовом, а ядро само нарезает их на сетевые кадры подходящего размера. Выигрыш проявляется на отдаче тяжёлых файлов и потоковом видео, но настройка работает только когда сетевая карта и драйвер поддерживают UDP-сегментацию. Ещё одна директива с похожей судьбой - quic_bpf: на Linux 5.7 и новее она через eBPF привязывает соединение к процессору и позволяет клиенту менять адрес без разрыва, то есть настоящая миграция соединения работает только с ней.
Остаются штрихи. Буфер потока http3_stream_buffer_size по умолчанию 64k, для медленных клиентов с большими ответами его иногда поднимают, а http3_hq включает упрощённый протокол для тестов совместимости, в рабочей эксплуатации он не нужен.
Проверка работы HTTP/3 в браузере и в логах веб сервера
Самый наглядный способ - инструменты разработчика в Chrome или Edge. Клавиша F12, вкладка Network, правый клик по заголовку таблицы и галочка Protocol. После загрузки страницы в новой колонке появляется короткая метка: h2 значит пока HTTP/2, h3 значит новый транспорт заработал. Если после первой загрузки горит h2, страницу обновляют ещё раз: анонс Alt-Svc браузер получил только что и со второго захода пробует QUIC. В Firefox есть свой экран диагностики по адресу about:networking, в разделе HTTP видна версия каждого соединения.
Серверная сторона проверяется двумя путями. Первый - журнал access_log, где переменная $http3 показывает протокол каждого запроса. Второй - консольный клиент curl, собранный с поддержкой нового транспорта:
curl --http3-only -I https://example.com
Флаг --http3-only запрещает откат на старые протоколы, поэтому команда либо напечатает HTTP/3 200, либо упадёт с ошибкой, и это однозначный ответ. Штатные пакеты curl в большинстве дистрибутивов собраны без QUIC, так что понадобится специальная сборка или компиляция из исходников с библиотекой ngtcp2. Разработчики nginx советуют отладку начинать с простого консольного клиента: браузеры капризны к сертификатам и молча откатываются на TCP, а утилита называет причину отказа.
Диагностика проблем с QUIC от закрытого UDP порта до неудачной сборки
Классическая картина: конфигурация правильная, nginx отвечает, но в браузере упорно h2. В девяти случаях из десяти виноват порт 443 по UDP, закрытый где-то по дороге. Проверить, слушает ли его сервер, просто:
sudo ss -lun | grep 443
Если строки с nginx в списке нет, QUIC просто не включился и конфигурацию смотрят заново. Если слушает, но браузер молчит, дело в сети. Сначала локальный файрвол:
sudo ufw allow 443/udp
sudo ufw status verbose
У облачных провайдеров UDP часто закрыт на уровне группы безопасности, и открывать нужно именно в панели облака, локальный ufw тут не помощник.
Вторая по частоте причина - несовпадение порта в Alt-Svc с фактическим. Заголовок анонсирует порт, на котором работает QUIC, и если nginx слушает 8443, а анонс говорит 443, браузер стучится в пустоту. Третья причина - сборка. Команда nginx -V показывает, с какой библиотекой сервер слинкован, и когда в системе лежат сразу несколько версий SSL, легко получить бинарник, собранный не с той. Модуль в сборке есть, а ранние данные не работают - типичный симптом: сервер собран с OpenSSL старше 3.5.1.
Для глубокой диагностики nginx собирают с --with-debug и включают подробный журнал:
error_log /var/log/nginx/error.log debug;
Все сообщения QUIC помечены префиксом quic, их удобно отфильтровать обычным grep. Для расследования на уровне пакетов существуют макросы NGX_QUIC_DEBUG_PACKETS и NGX_QUIC_DEBUG_CRYPTO, их передают компилятору через --with-cc-opt при конфигурировании. Журнал выходит громоздким, зато показывает каждый пакет и точную причину отказа.
Четвёртая причина молчания - сертификат. QUIC строже относится к цепочкам и времени: если браузер показывал замочек с пограничной цепочкой по HTTP/2, HTTP/3-соединение может тихо не состояться и откатиться, не оставив видимой ошибки. Перед включением нового транспорта сертификаты проверяют строгими инструментами.
Переводить боевой узел на HTTP/3 стоит без спешки: сначала сборка на тестовой машине, неделя наблюдения за журналами, затем осторожное включение на рабочем сервере. Выигрыш на мобильном трафике реален, конфигурация занимает десяток строк, а откат делается удалением одной строчки listen из блока server. Сервер, который сам рассказывает браузерам о новом транспорте, встречает посетителей быстрее.