Открой DevTools на любом серьезном сайте в 2026 и посмотри на колонку Protocol. Ты увидишь зоопарк: часть запросов идет по h2, часть по h3, а если клиент совсем старый или сеть кривая - по http/1.1. Это не баг, это норма. Один и тот же шлюз обязан одновременно говорить на трех протоколах, потому что ты не управляешь клиентами. У кого то корпоративный прокси режет UDP, у кого то браузер 2019 года, у кого то мобильник скачет между Wi-Fi и LTE по дороге на работу.
Старая боль HTTP/1.1 - это head-of-line blocking на уровне соединения. Браузер открывал по 6 TCP-соединений на домен, городил domain sharding, склеивал CSS в один файл - все ради того, чтобы обойти ограничение "одно соединение - один запрос за раз". HTTP/2 убрал это мультиплексированием. HTTP/3 пошел дальше и убрал head-of-line blocking уже на уровне транспорта, переехав с TCP на UDP. В этом уроке разберем механику обоих, актуальный синтаксис 2026 года (он реально менялся, и старые гайды врут), и как заставить h1, h2 и h3 мирно жить на одном порту 443.
Сразу уточню по версиям, чтобы ты не настраивал по мертвым статьям. На момент урока stable - это nginx 1.30.0 (вышла 14 апреля 2026), mainline - ветка 1.31. HTTP/3 в nginx стабилен с 1.25.0 в mainline и приехал в stable 1.30, лежит в официальных пакетах и docker-образах. Никакого "экспериментально, не для прода" - это устаревшая формулировка из 2022 года.

nginx http2: мультиплексирование и почему http2 on - это директива, а не параметр
HTTP/2 - это бинарный протокол поверх того же TLS поверх TCP. Ключевая идея - один TCP-коннект, внутри которого живут много независимых потоков (streams). Каждый запрос и ответ режется на фреймы (HEADERS, DATA, SETTINGS), фреймы помечены номером стрима и едут вперемешку в одном соединении. Сервер собирает их обратно. Это и есть мультиплексирование: десять картинок грузятся параллельно по одному коннекту, а не по шести.
Плюс HPACK - сжатие заголовков. В HTTP/1.1 каждый запрос тащил полный набор заголовков текстом (Cookie, User-Agent, Accept на сотни байт каждый раз). HPACK держит динамическую таблицу уже виденных заголовков и шлет вместо повтора короткий индекс. На страницах с десятками запросов это экономит реально много.
Теперь про синтаксис, на котором спотыкаются все. До nginx 1.25.1 HTTP/2 включали параметром в listen:
Код: Выделить всё
# УСТАРЕЛО, так больше не пишут
listen 443 ssl http2;
Код: Выделить всё
server {
listen 443 ssl;
http2 on;
server_name cyberlake.ru;
ssl_certificate /etc/ssl/cyberlake/fullchain.pem;
ssl_certificate_key /etc/ssl/cyberlake/privkey.pem;
}
Проверка, что HTTP/2 реально поднялся:
Код: Выделить всё
curl -sI --http2 https://cyberlake.ru/ -o /dev/null -w '%{http_version}\n'
# ожидаем: 2
Если ты читал старые статьи про HTTP/2, там обязательно был server push: сервер сам пихал клиенту CSS и JS, не дожидаясь запроса. Звучало красиво, на практике провалилось. Директивы http2_push, http2_push_preload и http2_max_concurrent_pushes УДАЛЕНЫ из nginx. Браузеры тоже выпилили поддержку: Chrome убрал push еще в 106-й версии. Не ищи эти директивы и не вставляй - конфиг просто не пройдет nginx -t.
Проблема push была фундаментальной: сервер не знает, что уже лежит в кэше браузера. Он пушил файлы, которые клиент и так имел, и сжигал полосу впустую (over-pushing). Замена 2026 года - это 103 Early Hints, поддержка которых появилась в nginx (директива early_hints, в ветке 1.29 и в stable 1.30).
Механика принципиально другая и честная. Сервер думает над основным ответом (ходит в базу, рендерит), и пока думает - шлет промежуточный ответ со статусом 103 и заголовком Link с rel=preload или rel=preconnect. Это не данные, это ПОДСКАЗКА. Браузер сам решает, тянуть ему ресурс или он уже в кэше. Никакого over-push.
Код: Выделить всё
location / {
early_hints on;
proxy_pass http://app_backend;
# бэкенд может вернуть промежуточный 103 с заголовком:
# HTTP/1.1 103 Early Hints
# Link: </css/app.css>; rel=preload; as=style
# Link: </js/app.js>; rel=preload; as=script
}
nginx http3 и quic: транспорт поверх UDP, 0-RTT и устойчивость к смене сети
HTTP/3 - это HTTP поверх QUIC, а QUIC - это транспорт поверх UDP. Зачем уходить от TCP, который работал десятилетиями. Из за главной проблемы HTTP/2: head-of-line blocking на уровне TCP. Да, h2 мультиплексирует стримы, но все они едут в одном TCP-потоке. Потерялся один пакет - TCP останавливает ВСЕ стримы, пока не дождется ретрансмита, потому что TCP гарантирует строгий порядок байт. На плохой мобильной сети один потерянный пакет тормозит всю страницу.
QUIC решает это радикально. Стримы в QUIC независимы на уровне транспорта: потеря пакета в стриме картинки не блокирует стрим с HTML. UDP не требует порядка, а порядок и надежность QUIC реализует сам, поштучно для каждого стрима. Бонусом QUIC встроил TLS 1.3 прямо в рукопожатие - шифрование не опционально, оно часть протокола.
Две killer-фичи QUIC для реальной эксплуатации:
- Connection migration. У QUIC-соединения есть Connection ID, не привязанный к паре IP:порт. Телефон ушел из Wi-Fi в LTE - IP сменился, но соединение живет по тому же Connection ID, без нового рукопожатия. С TCP это всегда был разрыв и пересоединение.
- 0-RTT через ssl_early_data. При повторном подключении к уже знакомому серверу клиент может отправить первый запрос ВМЕСТЕ с рукопожатием, не ожидая полного round-trip. Латентность первого байта падает. Но осторожно - 0-RTT данные подвержены replay-атакам, поэтому пускай по 0-RTT только идемпотентные запросы (GET), не платежи.
Код: Выделить всё
server {
# h1 и h2 поверх TCP
listen 443 ssl;
# h3 поверх QUIC/UDP, reuseport обязателен для нескольких воркеров
listen 443 quic reuseport;
http2 on;
http3 on;
server_name cyberlake.ru;
ssl_certificate /etc/ssl/cyberlake/fullchain.pem;
ssl_certificate_key /etc/ssl/cyberlake/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; # QUIC требует TLS 1.3
# разрешаем 0-RTT (по желанию, с оговоркой про replay)
ssl_early_data on;
location / {
# говорим браузеру: этот же origin доступен по h3 на 443/UDP
add_header Alt-Svc 'h3=":443"; ma=86400' always;
proxy_pass http://app_backend;
}
}
И не забудь про firewall: HTTP/3 - это UDP/443, а его по привычке закрывают. Если порт 443/UDP не открыт, h3 молча не заработает, а h2 будет жить - ты даже не сразу заметишь.
ECH (Encrypted Client Hello) приехал в nginx 1.30. Он шифрует ClientHello, в частности SNI - имя хоста, к которому ты идешь. Раньше SNI летел открытым текстом, и провайдер видел, на какой именно сайт ты заходишь, даже при HTTPS. ECH прячет и это. Включается через OpenSSL-интеграцию и требует, чтобы публичный ECH-конфиг был опубликован в DNS (HTTPS RR). Это передовой край приватности 2026 года, но требует свежего OpenSSL и согласованного DNS - вслепую не включай.
Безопасность: держи nginx обновленным. CVE-2026-40460 - это спуфинг адреса при connection migration в HTTP/3 (атакующий может выдать себя за мигрировавшего клиента). Лечится обновлением, поэтому stable 1.30.x с патчами - не прихоть, а гигиена. Отдельно помни про CVE-2026-42945 в rewrite-модуле - не связан напрямую с h3, но это еще один довод держать сборку свежей.
Практика: проверяем, что h2 и h3 реально отвечают
Конфиг написать мало - надо доказать, что протоколы работают. Для HTTP/3 нужен curl, собранный с поддержкой HTTP/3 (флаг --http3 или --http3-only).
Код: Выделить всё
# проверяем версию протокола h3
curl -sI --http3 https://cyberlake.ru/ -o /dev/null -w 'proto=%{http_version}\n'
# ожидаем: proto=3
# смотрим, шлет ли сервер Alt-Svc (через обычный h2-запрос)
curl -sI --http2 https://cyberlake.ru/ | grep -i alt-svc
# alt-svc: h3=":443"; ma=86400
# если curl без h3 - поднимем тестовый контейнер
docker run --rm ymuski/curl-http3 curl -sI --http3 https://cyberlake.ru/
Типичные грабли
- Пишешь listen 443 ssl http2; и удивляешься предупреждению. Это устаревший синтаксис - переноси на отдельную директиву http2 on;.
- Забыл reuseport на quic-листенере. С одним воркером проедет, но в проде с worker_processes auto QUIC будет терять соединения - ядро не знает, какому воркеру отдать пакет.
- Нет add_header Alt-Svc - браузеры в принципе не перейдут на HTTP/3. h3 настроен, но никто им не пользуется.
- Закрыт UDP/443 на фаерволе или у облачного провайдера в security group. h3 молча мертв, h2 работает - диагностируется только curl --http3.
- Ищешь http2_push - его нет, удален. Нужен функционал предзагрузки - это early_hints on и 103 Early Hints.
- Включил ssl_early_data on и пускаешь по 0-RTT POST с платежом. 0-RTT уязвим к replay - только идемпотентные методы.
- reuseport указан в нескольких server-блоках на один адрес. Он должен быть ровно ОДИН раз на listen-адрес, иначе конфликт сокета.
Повтори руками, по шагам:
- Шаг 1. Проверь версию: nginx -v - должно быть 1.30.x или новее. Старее - HTTP/3 в stable может не быть.
- Шаг 2. Возьми server-блок из практики выше, подставь свой сертификат (для теста сгодится самоподписанный).
- Шаг 3. Открой UDP/443: sudo ufw allow 443/udp (плюс уже открытый 443/tcp).
- Шаг 4. Проверь конфиг: nginx -t. Должно быть syntax is ok. Если ругается на http3 - модуль ngx_http_v3_module не собран, бери официальный пакет или docker-образ nginx.
- Шаг 5. Перезагрузи: nginx -s reload.
- Шаг 6. curl -sI --http2 https://localhost/ -k и убедись, что в ответе есть alt-svc.
- Шаг 7. curl -sI --http3 https://localhost/ -k -w '%{http_version}\n' - ожидай 3. Нет h3 в curl - запусти docker-образ с curl+http3 из практики.
- Шаг 8. Убери reuseport, поставь worker_processes 4;, перезагрузи и открой страницу несколько раз - словишь нестабильность h3. Верни reuseport - все чинится. Так ты прочувствуешь, зачем он нужен.
- Почему в 2026 правильно писать http2 on; отдельной директивой, а не listen ... http2, и какую проблему виртуального хостинга это решает.
- Что заменило удаленный server push и в чем принципиальное преимущество 103 Early Hints перед push с точки зрения кэша браузера.
- Зачем на одном порту 443 одновременно нужны listen 443 ssl и listen 443 quic, и какую роль играет заголовок Alt-Svc.
- За что отвечает параметр reuseport на quic-листенере и что сломается без него при нескольких воркерах.
HTTP/2 дал мультиплексирование и HPACK поверх TCP, но застрял на head-of-line blocking транспорта. HTTP/3 поверх QUIC/UDP это лечит, плюс дарит connection migration и 0-RTT. В 2026 синтаксис четкий: http2 on; и http3 on; - отдельные директивы, два listen на 443 (ssl для TCP, quic reuseport для UDP), add_header Alt-Svc для апгрейда. Server push мертв, его место заняли 103 Early Hints. HTTP/3 - это production, а не эксперимент. Держи nginx обновленным (CVE-2026-40460), открывай UDP/443 и проверяй curl --http3.