← Все заметки

tls · network · pqc · debugging

Белый экран на новых смартфонах: TLS, постквантовые ключи и невидимый сбой

Разбор реального сетевого инцидента: почему новые телефоны видели белый экран, старые устройства работали, а nginx не писал ничего в логи.

Однажды у билетной системы одного публичного объекта начались странные жалобы: на новых iPhone и свежих Android сайт то открывается, то зависает белым экраном. Старые устройства работают. В логах nginx пусто, будто проблемные пользователи вообще не приходили.

Первая версия со стороны провайдера системы была ожидаемая: блокировки, DPI, мобильные операторы, что-то где-то режется. Версия удобная, но проверяемая. Я решил не спорить словами и разобрать ситуацию по пакетам.

Первый факт: сервер ничего не видит

В access-логе и error-логе nginx нет обращений с проблемных телефонов. Это важный сигнал: если бы запрос дошел до веб-сервера и тот его отверг, мы бы увидели хотя бы след ошибки.

Значит, проблема происходит раньше HTTP. Где-то на TCP/TLS-этапе.

tcpdump показывает место обрыва

Снимаю tcpdump на сервере во время реального сбоя. Картина повторяется в каждом соединении:

client -> server: SYN
server -> client: SYN-ACK
client -> server: ACK
client -> server: TLS ClientHello, segment 1, about 1388 bytes
server -> client: ACK
client -> server: ... nothing

TCP-рукопожатие проходит. Первая часть TLS ClientHello доходит. Вторая часть того же ClientHello не приходит вообще.

Без полного ClientHello сервер не может продолжить TLS-рукопожатие, браузер не получает страницу, пользователь видит белый экран.

Почему ClientHello вдруг стал большим

На новых клиентах TLS ClientHello может включать гибридный постквантовый обмен ключами вроде X25519MLKEM768. В отличие от классического X25519, где публичный ключ маленький, ML-KEM добавляет примерно килобайт данных. Первое сообщение клиента становится заметно больше и может перестать помещаться в один сетевой сегмент.

Старые устройства отправляют маленький ClientHello, он проходит одним куском. Новые отправляют большой, он режется на два сегмента. И где-то в пути второй сегмент тихо исчезает.

Может, виноват сервер?

Есть известный класс багов: сервер или промежуточный компонент получает фрагментированный ClientHello, но не умеет корректно дочитать его до конца. Этому посвящен проект tldr.fail: он проверяет, ломается ли сервер на большом и специально разбитом ClientHello.

В нашем случае проверка с чистой сети дала нормальный результат: сервер корректно отвечал ServerHello. Дополнительные проверки с проводного интернета и внешнего VPS тоже проходили стабильно.

Еще важнее: tcpdump снимался на сервере до nginx и до прикладной логики. Второй сегмент не появлялся на сетевой карте. Значит, сервер его не ронял. Он его просто не получал.

Почему это похоже на middlebox

Сбой проявлялся избирательно: в основном на мобильных сетях и новых устройствах. Сервер не знает, что клиент сидит в мобильной сети, если пакет уже пришел. Он видит IP, TCP и TLS. Если бы сервер ломался на большом ClientHello, похожее поведение было бы с разных сетей.

А здесь симптом указывает на путь: между телефоном и сервером есть оборудование, которое плохо переносит большой или фрагментированный ClientHello и молча теряет продолжение.

До CDN:
телефон -> мобильная сеть -> middlebox -> origin
                              ^ второй сегмент ClientHello пропал

После CDN:
телефон -> мобильная сеть -> CDN edge -> origin
                              ^ TLS завершается ближе к клиенту

Почему CDN помогает

Серверного фикса в такой ситуации обычно нет: размер ClientHello задает клиент, а чужое сетевое оборудование мы не контролируем.

Рабочее решение - вынести TLS-терминацию на CDN:

  • edge-узлы CDN стоят на хорошо проверенных маршрутах;
  • CDN-инфраструктура уже готова к большим TLS ClientHello;
  • TLS завершается ближе к пользователю, а опасный участок до origin исчезает;
  • крупные CDN давно занимаются постквантовой совместимостью на TLS-уровне.

После перевода сайта за CDN симптом ушел.

Почему логи молчат

Это неприятная часть таких инцидентов: классические web-логи не видят проблему. HTTP-запроса не было, nginx ничего не обработал, application log тоже пустой.

Что помогает мониторить такие случаи:

  • tcpdump или eBPF на сетевом уровне;
  • синтетические проверки из реальных мобильных сетей;
  • Prometheus blackbox_exporter с разных точек;
  • Network Error Logging, если вы готовы принимать браузерные отчеты;
  • внешние чекеры вроде Cloudflare Radar Post-Quantum checker.

Мораль

  1. Версия “это провайдер блокирует” может быть верной, но ее надо доказывать пакетами.
  2. Если в nginx нет логов, это не значит, что пользователь ошибается. Возможно, HTTP просто не начался.
  3. Новая криптография может ломаться не в математике, а в старом сетевом железе.
  4. Если проблема в чужом пути, иногда правильный ремонт - не чинить origin, а перенести точку входа.

Ссылки