сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Сети

HTTP-коды прокси и TLS-рукопожатие

Коды шлюза и защищённое соединение

Собрал маленький стенд: прокси и бэкенд за ним. Прокси - посредник, который принимает запрос клиента и передаёт его дальше; бэкенд - то самое приложение, ради которого всё затевалось. Направил прокси на порт, где никто не слушает, - получил 502 за 13 миллисекунд. Заставил бэкенд думать 8 секунд при разрешённых двух - получил 504 ровно через 2018 миллисекунд, причём сам бэкенд в это время жив и на другие запросы отвечает. Два кода, две совершенно разные причины.

Стержень: код шлюза говорит, что случилось с бэкендом; защищённое соединение требует и правильного имени, и полной цепочки сертификатов.

// Формулировки: «чем 502 отличается от 504?», «как выбирается сертификат, если сайтов на адресе много?», «в браузере открывается, curl ругается»

502 против 504 и третий случай, который хуже обоих

502 означает, что прокси не смог получить от бэкенда осмысленный ответ: соединение отвергнуто, оборвано или пришёл мусор вместо заголовков. На стенде я направил прокси на порт, где никого нет, и получил 502 за 13 миллисекунд. Идти с этим надо в логи приложения и в его перезапуски: это упавший процесс, убитый рабочий, кончившиеся соединения.

504 означает, что бэкенд не ответил за отведённое прокси время. На стенде я поставил разрешённое время ожидания 2 секунды, а бэкенд отвечал 8: ровно на 2018 миллисекунде прокси отдал 504. Идти с этим надо в долгие запросы, очереди и загрузку. Заметь разницу в диагностике: при 502 бэкенд обычно лежит, при 504 он жив и просто не успевает.

// А теперь третий случай, который в жизни встречается чаще, чем хотелось бы. Я заставил бэкенд ответить заголовками, пообещать 100 байт тела и оборвать связь после четырёх. Прокси не отдал ни 502, ни 504: он честно ждал обещанное, и клиент отвалился по своему пределу через 20 секунд. Мораль: не всякая поломка превращается в код ошибки, и пределы ожидания нужны на каждом участке - у клиента, у прокси, у приложения. Полезно и смотреть, КТО именно отдал код: свой прокси, облачный балансировщик или внешняя сеть доставки содержимого, стоящая перед сайтом, - у каждого свои пределы, и они запросто короче, чем у приложения.

502
бэкенд отверг соединение, оборвал его или ответил мусором
504
бэкенд не ответил за время, отведённое прокси

Из чего складывается время защищённого запроса

Разложил обычный запрос к нашему сайту по стадиям. Поиск адреса по имени 0,0020 секунды, установка соединения 0,0021, установка шифрования 0,0365, первый байт ответа 0,0375. То есть само рукопожатие шифрования съело около 34 миллисекунд - почти всё время запроса, и это на канале, где соединение ставится за две миллисекунды.

Дальше самое полезное. Отправил три запроса подряд по одному соединению: первый занял 48,8 миллисекунды, второй 0,76, третий 0,71. Разница в шестьдесят раз, и вся она - в том, что рукопожатие делается один раз. Именно поэтому соединения переиспользуют, а не открывают на каждый запрос, и поэтому же прокси держит отдельный пул соединений к бэкендам.

// Рядом живёт ещё один незаметный расход. Запрос по незащищённому протоколу наш сервер не обслуживает, а отвечает переадресацией: код 301 и адрес с защищённым протоколом. Значит, клиент, который пришёл по старой ссылке, платит за лишний круг. Поэтому ссылки сразу пишут с защищённым протоколом, а браузеру говорят помнить это правило.

рукопожатие
обмен, в котором стороны договариваются о шифровании и проверяют сертификат
переиспользование соединения
слать следующие запросы по уже открытому соединению

Имя в рукопожатии и полная цепочка

Заголовок с именем сайта едет уже внутри шифрованного канала, а сертификат нужен раньше. Поэтому имя кладут открытым текстом в самое начало рукопожатия - это расширение SNI (server name indication). Проверил на своём стенде с двумя сайтами на одном адресе и порту: просишь shop.test - сервер предъявляет сертификат shop.test, просишь blog.test - сертификат blog.test, не называешь имени вовсе - получаешь сертификат сайта по умолчанию. Отсюда десятки сайтов на одном адресе, и отсюда же то, что имя сайта видно наблюдателю в сети, даже когда содержимое зашифровано.

Второе - цепочка. Сертификат сайта подписан промежуточным центром, тот - корневым, и только корневой лежит в хранилище доверия у клиента. Построил такую цепочку сам и поднял два сервера: первый отдаёт только сертификат сайта, второй - сертификат вместе с промежуточным. Клиент, который доверяет корню, на первом падает с «unable to get local issuer certificate», а на втором спокойно получает страницу. Цепочку без промежуточного звена просто не из чего собрать.

// Почему тогда браузер открывает такой сайт, а curl падает? Я проверил и это: если дать клиенту промежуточный сертификат заранее, он открывает страницу и на первом сервере. Браузеры как раз держат такой запас и умеют дотянуть недостающее звено по ссылке из самого сертификата. А curl, Java и Python этого не делают. Поэтому «в браузере работает» ничего не доказывает, а сервер обязан отдавать полную цепочку. Проверка одной командой: openssl s_client с ключом showcerts покажет, сколько сертификатов сервер прислал. Наш прод отдаёт четыре.

SNI
имя сайта, отправленное открытым текстом в начале рукопожатия
промежуточный сертификат
звено между сертификатом сайта и корневым; сервер обязан его отдавать
хранилище доверия
список корневых сертификатов, которым клиент верит без вопросов

Как отвечать: «В браузере сайт открывается, curl падает с ошибкой доверия. Что не так?»

Почти наверняка сервер отдаёт только свой сертификат без промежуточного. Клиент строит цепочку от сертификата сайта до корня из своего хранилища, и без среднего звена она не собирается - curl говорит «unable to get local issuer certificate». Браузер это прощает: у него есть запас ранее виденных промежуточных сертификатов и умение дотянуть недостающий по ссылке из самого сертификата. Я специально ставил такой стенд: сервер с одним сертификатом падает у клиента, который знает только корень, и открывается у того, кому промежуточный дали заранее. Лечится тем, что в конфигурации веб-сервера указывают файл с полной цепочкой, а проверяется командой openssl s_client с ключом showcerts - она покажет, сколько сертификатов реально прислал сервер.

Ответ объясняет, почему разные клиенты ведут себя по-разному, и даёт и проверку, и лечение. Проверенный на стенде пример отличает практика от пересказа.

На чём валятся

  • Путают 502 и 504 и ищут причину не в том месте: при 502 бэкенд лежит, при 504 он жив и не успевает.
  • Отдают только сертификат сайта и считают, что проблема в клиенте.
  • Отключают проверку сертификата вместо починки цепочки.
  • Забывают, что предел ожидания у балансировщика может быть короче, чем у приложения.
  • Считают, что защищённое соединение скрывает и имя сайта. Имя едет открытым текстом.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.

  1. #dvo_net_http_tls1 / 5
    Прокси отдаёт клиентам то 502, то 504. В чём разница для диагностики?
    A)502 — ошибка клиента, 504 — ошибка сервера приложения
    B)502 отдаёт сам сервис, 504 добавляет балансировщик
    C)502 — обрыв или мусор от апстрима, 504 — молчание
    D)502 про TLS, 504 про исчерпание пула соединений
    показать ответ и разбор
    +C)502 — обрыв или мусор от апстрима, 504 — молчание

    // разбор: 502 Bad Gateway — прокси до апстрима достучался, но получил обрыв или невалидный ответ: приложение упало, воркер умер, апстрим закрыл соединение. 504 Gateway Timeout — апстрим не ответил за отведённое время: висит на запросе к БД, перегружен, залип. Первое ведёт к логам и падениям приложения, второе — к таймаутам, длинным запросам и очередям.

  2. #dvo_net_http_tls2 / 5
    На одном IP живут десятки HTTPS-сайтов с разными сертификатами. Как сервер понимает, какой сертификат отдать?
    A)По заголовку Host из первого HTTP-запроса
    B)По записи PTR обратной зоны для адреса клиента
    C)По отдельному порту, выделенному каждому домену
    D)По расширению SNI в ClientHello, до шифрования
    показать ответ и разбор
    +D)По расширению SNI в ClientHello, до шифрования

    // разбор: Заголовок Host едет уже внутри зашифрованного соединения, а сертификат нужен раньше — потому имя хоста клиент кладёт в SNI открытым текстом в ClientHello. Сервер выбирает по нему сертификат и виртуальный хост. Отсюда два следствия: имя сайта видно наблюдателю в сети, а клиент без поддержки SNI получит сертификат по умолчанию и ошибку имени.

  3. #dvo_net_http_tls3 / 5
    В браузере сайт открывается, а curl и приложение падают с «unable to get local issuer certificate». Что не так на сервере?
    A)Сертификат выписан на другое имя, браузер это игнорирует
    B)Сервер отдаёт только листовой сертификат, без промежуточных
    C)Сертификат просрочен, но браузер держит его в кэше
    D)Сервер требует клиентский сертификат, а curl его не шлёт
    показать ответ и разбор
    +B)Сервер отдаёт только листовой сертификат, без промежуточных

    // разбор: Клиент проверяет цепочку до корня из своего хранилища. Браузеры умеют дотягивать недостающий промежуточный сертификат по ссылке из расширения AIA (или берут из своего кэша), а curl, Java и Python — нет: им нужна полная цепочка от сервера. Лечится сборкой fullchain в конфиге веб-сервера. Проверка: openssl s_client -connect host:443 -showcerts.

  4. #dvo_net_http_tls4 / 5
    Мониторинг показал всплеск именно 5xx (не 4xx). На чьей стороне искать причину первым делом?
    A)5xx — это ошибки клиента, значит дело в неверных запросах
    B)И 4xx, и 5xx означают сетевые обрывы, смотреть надо канал
    C)5xx — это редиректы, клиент просто ушёл не туда
    D)5xx — ошибки сервера, копать у себя; 4xx были бы про запрос клиента
    показать ответ и разбор
    +D)5xx — ошибки сервера, копать у себя; 4xx были бы про запрос клиента

    // разбор: Классы статусов HTTP: 4xx — ошибка клиента (плохой запрос, авторизация, not found) — смотреть на вызывающего/вход; 5xx — ошибка сервера (необработанное исключение, упавшая зависимость, таймаут) — смотреть на свой сервис и его зависимости. Всплеск 5xx = сервер сыпется; всплеск 4xx = клиенты шлют плохие запросы (или ты сменил API). Направление разбора разное — разноси дашборды по классам.

  5. #dvo_net_http_tls5 / 5
    TLS терминируется на балансировщике, дальше идёт http. Приложение зациклилось на редиректе http→https. Почему?
    A)Сертификат на балансировщике невалиден, браузер ходит по кругу
    B)Приложение видит http и редиректит; нужен X-Forwarded-Proto
    C)Надо включить TLS и на самом приложении, иначе редирект вечный
    D)Балансировщик вырезает заголовок Location из ответа сервера
    показать ответ и разбор
    +B)Приложение видит http и редиректит; нужен X-Forwarded-Proto

    // разбор: TLS снят на балансировщике, до приложения доходит plain http; приложение, видя «не https», редиректит на https — запрос снова приходит через LB как http → петля. LB должен передавать X-Forwarded-Proto: https, а приложение — доверять этому заголовку при определении схемы. Тогда оно понимает, что исходный запрос уже был https, и перестаёт редиректить.

дальше

Теорию прочитали. Навык ставится повторением

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