Новая веха Интернет-истории начинается на наших глазах: можно считать, что HTTP/3 уже объявлен. В конце октября Mark Nottingham из IETF предложил уже определиться с названием для нового протокола, надо которым IETF корпит с 2015 года. Так вместо QUIC-подобных названий появилось громкое HTTP/3. Западные издания уже писали об этом и даже не один раз. История QUIC началась в недрах Корпорации добра в 2012 году, с тех пор только серверы Google поддерживали HTTP-over-QUIC соединения, однако время идет и вот уже Facebook начал внедрять эту технологию (7 ноября, Facebook и LiteSpeed осуществили первое взаимодействие по HTTP/3); на данный момент доля сайтов, поддерживающих QUIC – 1,2%. Наконец, рабочая группа WebRTC тоже смотрит в сторону QUIC (плюс см. QUIC API), так что в обозримом будущем у нас будут видеозвонки с реалтайм-голосом. Поэтому мы решили, что будет здорово раскрыть подробности IETF QUIC: специально для Хабра мы подготовили перевод лонгрида, расставляющего точки над i. Enjoy!
QUIC (Quick UDP Internet Connections) – это новый, шифрованный по умолчанию, протокол транспортного уровня, который имеет множество улучшений HTTP: как для ускорения трафика, так и для повышения уровня безопасности. Также QUIC имеет долгосрочную цель – в итоге заменить TCP и TLS. В этой статье мы рассмотрим как ключевые фишки QUIC и почему веб выиграет за счет них, так и проблемы поддержки этого абсолютно нового протокола.
По факту существует два протокола с таким именем: Google QUIC (gQUIC), изначальный протокол, который разработали инженеры Google несколько лет назад, который после ряда экспериментов был принят IETF (Internet Engineering Task Force) в целях стандартизации.
IETF QUIC (далее – просто QUIC) уже имеет настолько сильные расхождения с gQUIC, что может считаться отдельным протоколом. От формата пакетов до хендшейка и мэппинга HTTP – QUIC улучшил оригинальную архитектуру gQUIC благодаря сотрудничеству со многим организациями и разработчиками, которые преследуют единую цель: сделать Интернет быстрее и безопаснее.
Итак, какие улучшения предлагает QUIC?
Встроенная безопасность (и производительность)
Одно из самых заметных отличий QUIC от почтенного TCP – это изначально заявленная цель быть транспортным протоколом, который безопасен по умолчанию. QUIC добивается этого с помощью аутентификации и шифрования, которые обычно происходят на уровне выше (например, в TLS), а не в самом транспортном протоколе.
Первоначальный хендшейк в QUIC сочетает привычное трехстороннее общение по TCP с TLS 1.3 хендшейком, который обеспечивает аутентификацию участников, равно как и согласование криптографических параметров. Для тех, кто хорошо знаком с TLS: QUIC заменяет уровень записи TLS своим собственным форматом кадра, но при этом использует хендшейки TLS.
Это не только позволяет соединению быть всегда шифрованным и аутентифицированным, но также быстрее делать первоначальное соединение: рядовой QUIC-хендшейк делает обмен между клиентом и сервером за один проход, в то время как TCP + TLS 1.3 делают два прохода.
Однако QUIC идет дальше и также шифрует метаданные о соединении, которые могут быть легко скомпрометированы третьей стороной. Например, атакующие могут использовать номера пакетов, чтобы направлять пользователей по множеству сетевых путей, когда используется миграция соединения (см. ниже). QUIC шифрует номера пакетов, поэтому они не могут быть скорректированы никем, кроме настоящих участников соединения.
Шифрование также может быть эффективно против «косности» – феномена, которые не дает использовать гибкость протокола на практике из-за неверных предположений в реализациях (ossification – то, что из-за чего долго откладывали выкладку TLS 1.3. Выложили только после нескольких изменений, которые предотвратят нежелательные блоки для новых ревизий TLS).
Блокировка начала очереди (Head-of-line blocking)
Одним из главных улучшений, которое нам принес HTTP/2, это возможность объединять разные HTTP-запросы в одном TCP-соединении. Это позволяет приложениям на HTTP/2 параллельно обрабатывать запросы и лучше использовать сетевой канал.
Конечно, это было значительным шагом вперед. Потому что ранее приложениям нужно было инициировать множество TCP+TLS соединений, если они хотели одновременно обрабатывать несколько HTTP-запросов (например, когда браузеру нужно получить и CSS, и JavaScript чтобы отрисовать страницу). Создание новых соединений требует множественных хендшейков, а также инициализацию окна перегрузки: это означает замедление рендеринга страницы. Объединенные HTTP-запросы позволяют избежать этого.
Однако здесь есть недостаток: так как множественные запросы/ответы передаются по тому же TCP-соединению, они все одинаковы зависимы от потери пакетов, даже если потерянные данные касаются лишь одного из запросов. Это и называется «блокировкой начала очереди».
QUIC идет глубже и дает первоклассную поддержку для объединения запросов, например, разные HTTP-запросы могут быть расценены как разные транспортные QUIC-запросы, но при этом все они будут использовать одно и то же соединение QUIC – то есть дополнительные хендшейки не нужны, есть единой состояние перегрузки, запросы QUIC доставляются независимо – в итоге, в большинстве случаев потеря пакетов затрагивает только один запрос.
Таким образом можно существенно сократить время на, например, полный рендеринг веб-страницы (CSS, JavaScript, картинки и прочие ресурсы), особенно в случае перегруженной сети с высокой потерей пакетов.
Так просто, да?
Чтобы выполнить свои обещания, протокол QUIC должен преодолеть некоторые допущения, которые приняли во многих сетевых приложениях как нечто само собой разумеющееся. Это может затруднить имплементацию и внедрение QUIC.
QUIC спроектирован, чтобы доставляться поверх UDP-датаграмм, дабы облегчить разработку и избежать проблем с сетевыми устройствами, которые отбрасывают пакеты неизвестных протоколов (потому что большинство устройств поддерживают UDP). Также это позволяет QUIC жить в user-space, поэтому, например, браузеры смогут внедрять новые фишки протокола и доносить их до конечных пользователей, не дожидаясь обновлений ОС.
Тем не менее, благая цель – уменьшить сетевые проблемы – делает более трудным защиту пакетов и их правильный роутинг.
Один NAT чтобы всех воедино созвать и единою черною волей сковать
Обычно NAT-роутеры работают с TCP-соединениями, используя кортеж из 4 значений (исходные IP и порт плюс IP и порт назначения), а также отслеживая TCP SYN, ACK и FIN-пакеты, переданные по сети; роутеры могут определять, когда установилось новое соединение и когда закончилось. Поэтому возможно точное управление привязками NAT (связи между внутренними и внешними IP и портами).
В случае QUIC это пока невозможно, т.к. современные NAT-роутеры еще не знают про QUIC, поэтому они обычно делают даунгрейд к дефолтной и менее точной обработке UDP, что означает таймауты произвольной (иногда малой) длительности, которые могут влиять на длительные соединения.
Когда происходит перепривязка (например, из-за таймаута), устройство вне периметра NAT начинает получать пакеты из другого источника, из-за чего невозможно поддерживать соединение используя только лишь кортеж из 4 значений.
И дело не только в NAT! Одна из фишек QUIC называется connection migration и позволяет устройствам по их усмотрению переносить соединения на другие IP-адреса/пути. Например, мобильный клиент сможет перенести QUIC-соединение с мобильной сети на уже известную WiFi-сеть (пользователь зашел в любимую кофейню и т.п.).
QUIC пытается решить эту проблему с помощью концепции connection ID: кусок информации произвольной длины, передаваемый в пакетах QUIC и позволяющий идентифицировать соединение. Конечные устройства могут использовать этот ID, чтобы отслеживать свои соединения без сверки с кортежем. На практике тут должно быть множество ID, которые указывают на одно и то же соединение, к примеру, чтобы избежать соединения разных путей, когда происходит миграция соединения – потому что весь процесс контролируется только конечными устройствами, а не миддлбоксами.
Однако и здесь может быть проблема для операторов связи, которые используют anycast и ECMP-роутинг, где один IP потенциально может идентифицировать сотни или тысячи серверов. Так как пограничные маршрутизаторы в этих сетях еще не знают, как обрабатывать QUIC-трафик, то может случиться так, что UDP-пакеты из одного QUIC-соединения, но с разными кортежами будут отданы разным серверам, что означает разрыв соединения.
Чтобы избежать этого, операторам может понадобиться внедрить более умный балансировщик на 4 уровне. Этого можно добиться программно, не затрагивая сами пограничные роутеры (для примера см. проект Katran от Facebook).
QPACK
Другой полезной особенностью HTTP/2 было сжатие заголовков (HPACK), которое позволяет конечным устройствам уменьшать размер пересылаемых данных за счет отбрасывания ненужного в запросах и ответах.
В частности, помимо прочих техник, HPACK использует динамические таблицы с заголовками, которые уже были отправлены/получены от прошлых HTTP-запросов/ответов, что позволяет устройствам ссылаться в новых запросах/ответах на ранее встречавшиеся заголовки (вместо того, чтобы снова их передавать).
Таблицы HPACK должны быть синхронизированы между кодером (стороной, которая шлет запрос/ответ) и декодером (принимающая сторона), иначе декодер просто не сможет декодировать то, что получает.
В случае HTTP/2 поверх TCP эта синхронизация прозрачна, потому что транспортный уровень (TCP) обеспечивает доставку запросов/ответов в том же порядке, в каком они они были отправлены. То есть отправить декодеру инструкции по обновлению таблиц можно в простом запросе/ответе. Но в случае QUIC все намного сложнее.
QUIC может доставлять множественные HTTP-запросы/ответы по разным направлениям одновременно, что означает, что QUIC гарантирует порядок доставки в рамках одного направления, при это такой гарантии нет в случае множества направлений.
Например, если клиент отправляет HTTP-запрос А в QUIC-потоке А, а также запрос B в потоке B, то из-за перестановки пакетов или сетевых потерь, сервер получит запрос B до запроса А. И если запрос B был закодирован так, как было указано в заголовке запросе А, то сервер просто не сможет декодировать запрос B, так как он еще не видел запрос А.
В протоколе gQUIC эту проблему решили, просто сделав все заголовки (но не тела) HTTP-запросов/ответов последовательными в рамках одного gQUIC-потока. Это дало гарантию, что все заголовки придут в нужном порядке, что бы ни случилось. Это весьма простая схема, с ее помощью существующие решения могут продолжать использовать код, заточенный под HTTP/2; с другой стороны, это увеличивает вероятность блокировки начала очереди, снижать которую как раз и призван QUIC. Поэтому рабочая группа по QUIC из IETF разработала новый маппинг между HTTP и QUIC (HTTP/QUIC), а также новый принцип сжатия заголовков – QPACK.
В последнем драфте спецификаций HTTP/QUIC и QPACK каждый обмен HTTP-запросом/ответом использует свой собственный двунаправленный поток QUIC, так что блокировка начала очереди не возникает. Также, ради поддержки QPACK, каждый участник создает два дополнительных, однонаправленных потока QUIC, один для отправки обновлений таблиц, другой – для подтверждения их получения. Таким образом, кодер QPACK может использовать ссылку на динамическую таблицу только после того, как ее получение подтвердил декодер.
Преломляя отражение
Общая проблема основанных на UDP протоколов – их восприимчивость к атакам отражения, когда атакующий заставляет некий сервер отправлять огромное количество данных жертве. Атакующий подменяет свой IP, чтобы сервер думал, что запрос данных приходил с адреса жертвы.
Эта разновидность атаки может быть очень эффективна, когда ответ сервер несравнимо больше, чем запрос. В таком случае говорят об «усилении».
TCP обычно не используется для таких атак, потому что пакеты в изначальном хендшейке (SYN, SYN+ACK, …) имеют одинаковую длину, поэтому у них нет потенциала для «усиления».
С другой стороны, хендшейк QUIC очень ассиметричен: как и в TLS, сначала сервер QUIC отправляет свою цепочку сертификатов, которая может быть весьма большой, несмотря на то, что клиент должен отправить только несколько байт (сообщение от TLS-клиента ClientHello встроено в пакет QUIC). По этой причине, первоначальный пакет QUIC должен быть увеличен до определенной минимальный длины, даже если содержимое пакета значительно меньше. Как бы то ни было, эта мера все еще не очень эффективна, так как типичный ответ сервера содержит несколько пакетов и поэтому может быть больше чем увеличенный клиентский пакет.
Протокол QUIC также определяет явный механизм верификации источника: сервер, вместо того чтобы отдавать большой ответ, отправляет только retry-пакет с уникальным токеном, который клиент затем отправит серверу в новом пакете. Так у сервера есть бОльшая уверенность, что у клиента не подменный IP-адрес и можно завершить хендшейк. Минус решения – увеличивается время хендшейка, вместо одного прохода уже потребуется два.
Альтернативное решение заключается в уменьшении ответа сервера до размера, при котором атака отражения становится менее эффективной – например, с помощью сертификатов ECDSA (обычно они значительно меньше, чем RSA). Мы также экспериментировали с механизмом сжатия TLS-сертификатов, используя off-the-shelf алгоритмы сжатия вроде zlib и brotli; это фича, которая впервые появилась в gQUIC, но сейчас не поддерживается в TLS.
Производительность UDP
Одна из постоянных проблем QUIC – это ныне существующие железо и софт, которые не способны работать с QUIC. Мы уже рассматривали, как QUIC пытается справляться с сетевыми миддлбоксами вроде роутеров, однако другая потенциально проблемная зона это производительность отправки/получения данных между QUIC-устройствами по UDP. Долгие годы прикладывались усилия, чтобы оптимизировать реализации TCP насколько это возможно, включая встроенные возможности по разгрузке в софте (например, операционные системы) и в железе (сетевые интерфейсы), но ничто из этого не касается UDP.
Однако это лишь вопрос времени, пока реализации QUIC превзойдут эти улучшения и преимущества. Взгляните на недавние усилия внедрить разгрузку UDP на Linux, которая позволила бы приложениям объединять и передавать множественные UDP-сегменты между user-space и kernel-space сетевым стеком по затратам примерно одного сегмента; другой пример – поддержка zerocopy для сокетов в Linux, благодаря которой приложения смогли бы избегать затрат по копированию user-space памяти в kernel-space.
Заключение
Подобно HTTP/2 и TLS 1.3, протокол QUIC должен привнести массу новых фичей, которые повысят производительность и безопасность как веб-сайтов, так и других участник инфраструктуры Интернета. Рабочая группа IETF намеревается выкатить первую версию спецификаций QUIC к концу года, так что пора задуматься, как мы можем взять максимум от преимуществ QUIC.
Автор: nvpushkarskiy2