Ни DNS-запросы, ни DNS-ответы в классической DNS никак не защищены: данные DNS-транзакций передаются в открытом виде. DNS-over-HTTPS (или DNS Queries over HTTPS, DoH) - самая распространённая сейчас технология для защиты DNS-трафика конечного пользователя от прослушивания и подмены. Несмотря на то что фактическую защиту в DoH обеспечивает TLS, технология не имеет никакого отношения к DNS-over-TLS (DoT). Не перепутайте. Например, реализация DoH ни на сервере, ни на клиенте не требует поддержки DoT (и наоборот). Да, DoH и DoT схожи по верхнеуровневой задаче: и первая, и вторая - это технологии защиты DNS-транзакций, однако у DoH иная архитектура и поэтому более узкая область применения, чем у DoT.
В отличие от DoT, DoH подходит строго для использования "на последней миле", то есть между приложением или операционной системой на компьютере пользователя и сервером, осуществляющим поиск информации в DNS - рекурсивным DNS-резолвером. HTTP в DNS-over-HTTPS слишком плотно "прилегает" к DNS, чтобы применение данной технологии на участке от рекурсивного резолвера до авторитативных серверов выглядело хотя бы минимально обоснованным. Собственно, исходный RFC и рассматривает только сценарий "последней мили", где DoH оказывается максимально полезной.
Чтобы разобраться, где именно тут "последняя миля", что это за авторитативные серверы и рекурсивные резолверы, нужно вспомнить основы DNS. Как сервис поиска данных, DNS базируется на двух упомянутых логических объектах: рекурсивных резолверах и авторитативных серверах. Рекурсивные резолверы собирают информацию, нужную для ответа на DNS-запрос, получая ответы авторитативных серверов. Классическая DNS работает по UDP и, несколько реже, по TCP. Авторитативные серверы (их ещё иногда называют "авторитетными") - это серверы, отвечающие за адресацию в конкретной DNS-зоне, то есть в области иерархии имён DNS, обозначенной DNS-именем. Пример такого имени: example.com. Авторитативным DNS-сервер становится в процессе делегирования, когда вышестоящий авторитативный сервер назначает ответственные серверы для некоторой DNS-зоны. Так, для example.com. авторитативными серверами являются b.iana-servers.net. и a.iana-servers.net., что несложно выяснить при помощи утилиты dig из пакета BIND:
Серверы, обозначенные a и b, назначены ответственными за example.com. на авторитативных серверах вышестоящей зоны - com. (Обратите внимание, что здесь и далее используется полная форма записи доменного имени, - FQDN, - обязательно оканчивающаяся точкой справа; эта точка отделяет корневой домен.)
Рекурсивный резолвер - это DNS-сервер, выполняющий рекурсивный опрос авторитативных DNS-серверов с целью поиска нужной записи. Почему опрос "рекурсивный" - скоро станет понятно. У рекурсивного резолвера есть собственный кеш, однако, если ответ на поступивший запрос в кеше не обнаруживается, то резолвер начинает поиск в глобальной DNS по довольно сложному алгоритму. Если система работает без ошибок, то рекурсивный резолвер получает от авторитативного сервера либо нужный DNS-ответ о целевом имени, либо так называемый "делегирующий ответ" с именами других авторитативных серверов, которым следует переадресовать запрос. Отсюда и появляется "рекурсия" в названии процесса. Рекурсивный резолвер обычно работает за пределами "локальной машины", в качестве внешнего сервиса. Часто сервис рекурсивного резолвера предоставляет провайдер интернет-доступа. Но есть и более массовые сервисы, работающие для Интернета в целом. Примером такого сервиса является Google Public DNS 8.8.8.8, который, кроме прочих технологий, поддерживает DoH.
На локальной машине DNS-запросы обрабатывает stub-резолвер - существенно более простая программа, которая только перенаправляет запросы рекурсивному резолверу и принимает от него ответы. В качестве stub-резолвера может выступать и элемент большего приложения, например веб-браузера.
Именно между оконечным stub-резолвером и рекурсивным резолвером находится та самая "последняя миля", для защиты которой применяется DoH. То есть, по HTTPS к рекурсивному резолверу отправляет DNS-запросы stub-резолвер (или его логический эквивалент). Несмотря на то, что системный stub-резолвер может работать через DoH, типовым примером внедрения DoH на клиенте всё ещё является веб-браузер, который умеет самостоятельно формировать DNS-запросы и непосредственно направлять их резолверу, поддерживающему DoH. Доступ DoH может предоставлять любой резолвер, для этого не обязательно размещаться на мощностях Google. Так что DoH могла бы быть развернута и на резолвере ближайшего провайдера интернет-доступа, но на практике такое встречается не так часто, как хотелось бы.
В точности как и DoT, DoH защищает только DNS-трафик, но не сами DNS-данные. То есть, DoH предоставляет защищённый канал, но вовсе не защищает состав ответов и запросов DNS. Если локальный веб-браузер использует DoH для выполнения запросов к рекурсивному резолверу, то имена в запросах и значения записей не будут простым способом доступны третьей стороне, прослушивающей трафик. Но если в данные DNS-транзакций вмешивается непосредственно рекурсивный резолвер или эти данные модифицируются на пути к резолверу от внешних DNS-серверов, то DoH тут никак не поможет: для защиты непосредственно DNS-данных нужно использовать DNSSEC. И работает DoH только на том "хопе", где включен, то есть на "последней миле" - от приложения на локальной машине до резолвера. Это важный аспект: отправка локального запроса к рекурсивному резолверу, при промахе в кеше, порождает DNS-трафик на внешние авторитативные серверы, и на эти DNS-запросы действие клиентского DoH не распространяется в принципе - они будут идти через промежуточные узлы в открытом виде.
Принципиальная схема работы с DoH простая. Запрос отправляется так же, как и любой другой HTTP-запрос. Схема и адрес для приёма DoH должны быть каким-то способом настроены в DoH-клиенте: можно ввести вручную, можно встроить в дистрибутив, как это сделано в браузерах; теоретически, можно даже раздать через DHCP, как предлагается, на правах идеи, в RFC. Конечно, IP-адрес хорошо бы тоже знать заранее, поскольку попытка получить этот адрес из DNS до запуска DoH заметно снижает пользу последнего (см., впрочем, ниже про валидацию адресов). Типичное имя точки приёма: dns-query (но может различаться от сервиса к сервису). Для приёма DoH спецификацией закреплено название параметра GET - dns. Так что GET-запрос выглядит так:
Здесь %DNS-message-base64url% - это закодированное Base64url DNS-сообщение (собственно, запрос).
То есть, отправляем DNS-сообщение в качестве параметра GET, а в качестве HTTP-ответа получаем DNS-сообщение с результатом.
Возможна работа через POST, логика, в общем-то, та же, но полезные данные, как и предписывает POST, передаются не в URL и закодированными Base64url, а непосредственно в исходных байтах в теле запроса. Тут всё выглядит логично и знакомо всем, кто имел дело с HTTP.
DoH-сервер, соответствующий спецификации, должен поддерживать и GET, и POST. Далее в этой статье рассматривается только вариант с GET, поскольку он более иллюстративный, кроме того, в HTTP GET-запросы гораздо лучше сочетаются с механизмами кеширования.
Из-за того что интерфейс совпадает с обычным использованием HTTP, за реализацию DoH нередко ошибочно принимают другие REST и тому подобные API для работы с сервисом DNS-резолвера. Например, такой API с JSON предоставляет Google Public DNS по адресу https://dns.google/resolve - отправляем GET-запрос с именем домена и прочими параметрами по HTTPS, получаем результаты из DNS сразу в JSON. Удобно. Подход данного API, сам по себе, выглядит весьма логичным. Вот только это не современный DoH, несмотря на название страницы описания на сайте developers.google.com. К сожалению, DoH, согласно спецификации, работает совсем не так, а у того же Google Public DNS для подлинного DoH предусмотрен отдельный вход: https://dns.google/dns-query.
Архитектурно, DoH представляет собой переплетение из DNS и HTTP(S). Данная технология вовсе не является туннелем, как решения DNS-over-TLS. Семантика DoH позволяет параметрам HTTPS "просачиваться" в DNS, а параметрам DNS - "просачиваться" в обратную сторону, в HTTPS. Не самая очевидная особенность. Транзакционная схема HTTP в DoH прямо влияет на логику обработки DNS-запросов. И при этом DNS-запросы тут всё равно передаются в исходном, низкоуровневом формате DNS, но ещё и упаковываются в Base64, а на состав полей запроса накладываются требования, обусловленные особенностями HTTP. Например, наследуются алгоритмы кеширования уровня HTTP: то есть ответы на GET-, POST-запросы DoH и приходят с привычными статусами HTTP, и кешироваться могут как результаты прочих HTTP-транзакций, а не транзакций DNS (кешировать рекомендуется только GET-вариант).
Впрочем, такой архитектурный подход имеет и преимущества. Главное из них это то, что DoH-трафик оказывается неотличим от прочего HTTPS-трафика: тут тот же номер порта и транспортный протокол (443 и TCP), те же внутренние маркеры и те же запросы, а отличается лишь медиа-тип (Accept: application/dns-message - см. ниже), но он-то как раз снаружи не виден. Можно даже сказать, что DoH, таким образом, реализует минимальные меры для сокрытия факта своего использования - маскируется под работу с веб-сайтом. Впрочем, основное удобство HTTPS, с этой точки зрения, в том, что такой вариант позволяет DNS-информации лучше проходить через различные брандмауэры, расставленные по локальным периметрам и между сетями. Способ, как говорится, "обоюдоострый", но зато достаточно эффективный для того, чтобы его начали использовать даже некоторые "программы-зловреды", ибо в их жизненном цикле очень важны и успешный проход трафика сквозь сетевые барьеры, и сокрытие состава запроса.
Базовые шаги при использовании DoH для взаимодействия с рекурсивным резолвером следующие:
Практика
В практической части воспользуемся сервисом Google Public DNS для определения IP-адресов (A-записи), соответствующих имени dns.google - то есть, не только адреса, но и имя этого сервиса Google послужит в качестве примера. Запросы отправляются при помощи curl. Трафик смотрим при помощи tshark. Исходное DNS-сообщение, так как оно элементарное, подготовлено вручную, прямой сборкой нужных байтов - это хакерский подход (в исходном смысле термина "хакер").
Начнём с DNS-сообщения. Здесь используется только малая часть возможностей и самый простой формат. Байтовый дамп (шестнадцатеричный) приведён ниже с краткими комментариями.
Вообще, про формат и особенности DNS-сообщений можно написать очень много содержательного текста, однако к DoH это не имеет отношения, поэтому придётся оставить подробный разбор для других статей, а в этой - лишь краткие сведения о некоторых параметрах, влияющих на семантику DoT.
Первый из таких параметров - это два начальных байта сообщения: 16-битный идентификатор транзакции (ID). В классической DNS это простой механизм, позволяющий сопоставить запрос с ответом (этот же механизм считаеся базовым инструментом защиты от "упреждающего" спуфинга, так как для отправки ответа сразу с правильным ID нужно видеть запрос, но эффекивность его не велика, поскольку ID можно и угадать). В DoH соответствие ответа и запроса обеспечивает HTTP, так что в поле ID рекомендуется записывать значение ноль. Но можно и не ноль. В нашем сообщении-примере указано 0x1001. Смысл рекомендации писать ноль состоит в том, что HTTP-кеширование не различает внутреннюю семантику запросов и ответов, а обрабатывает их как HTTP, поэтому, если использовать различные ID, то в HTTP-кеше появятся ответы, идентичные по DNS-составу, но отличающиеся из-за разных значений ID. И тем не менее, DoH-сервером в ответе должно быть использовано такое же значение ID, как в DNS-запросе. Этот момент мы проверим ниже, рассматривая DoH-ответ.
Следом за ID указаны флаги, а вот блок из четырёх 16-битных значений после них - это индексы, определяющие количество записей в разных секциях сообщения. DNS-сообщение обязательно содержит четыре секции, сверху вниз: "Запрос" (Question), "Ответ" (Answer), "Серверы имён" (NS - Name Servers/Authority), "Дополнительно" (additional). В нашем сообщении заполняется только Question. Так что остальные - пустые. Заполненную секцию Answer мы увидим в ответе, а остальные - тут не рассматриваются.
После списка длин секций идёт сам запрос. Он единственный, имя - dns.google., указано в DNS-формате: отдельные лейблы имени (dns, google) закодированы в ASCII-строки, каждую из которых предваряет байт с длиной записи. В конце - корневой домен, который обозначается одним нулевым байтом. Например:
0x03 0x64 0x6e 0x73 - три байта (длина, первый байт), соответствующие dns.
Строку google (шесть букв, третья "o") и корневой домен читателю предлагается найти самостоятельно. Здесь использованы строчные буквы. DNS предполагает, что строчные и заглавные буквы, которые в ASCII оличаются одним битом, равны по значению. Что, впрочем, не мешает пытаться использовать различную запись для повышения количества энтропии в DNS-сообщениях. Метод называется рандомизацией регистра символов и используется тем же Google Public DNS, что очередной раз демонстирует высокую сложность DNS-протоколов, при том что сама система многим кажется "очень простой".
Два завершающих 16-битных блока обозначают, что для имени запрашивается запись типа A (IPv4) класса IN (Internet).
DoH-запрос всё равно отправляется по IP-адресу. И этот IP-адрес, в рамках данного эксперимента, известен заранее: 8.8.8.8. Отправляем запрос с помощью curl:
Обратите внимание на то, что используется HTTP/2 - это минимальная рекомендуемая версия HTTP для DoH. Дамп DNS-сообщения с запросом закодирован в строку Base64url. Проделать это кодирование можно, например, так:
В файле req-1.hex - дамп в hextext (комментарии, конечно, нужно удалить); в байты данные преобразует утилита xxd -r -p (-r - из текста - в байты; -p - формат записи, plain hexdump). Обратите внимание, что Base64url отличается от Base64 в деталях, там в алфавите "+" заменяется на "-", а "/" на "_", кроме того, нет дополнения, плюсы и слэши тут не возникают, а вот дополнение "==", которое будет выведено утилитой base64 в конце строки, нужно удалить.
Как указано, результат curl выводит в файл dns.google.bin, но мы посмотрим на ответ непосредственно в трафике (дамп записывается tcpdump). Заглянуть в трафик можно при помощи tshark, но для того, чтобы tshark смог расшифровать TLS-трафик, нужны сессионные ключи. Для получения сессионных ключей перед вызовом curl нужно задать переменную окружения SSLKEYLOGFILE=keys.log, где keys.log - файл, в который curl (а точнее - OpenSSL) выведет сессионные ключи.
Вызов tshark:
Здесь doh.pcap - это файл дампа трафика, -o tls.keylog_file:keys.log - опция, показывающая, где брать файл с ключами, -O - перечень протоколов (обратите внимание, что тут http2); -S и -x - это, соответственно, разделитель вывода и выбор формата с hex-дампом и ASCII (то есть, дело вкуса, а к разбору DoH-трафика прямого отношения не имеет).
Поскролив выдачу tshark, нетрудно увидеть DNS-ответ, присланный, по запросу curl, сервисом 8.8.8.8 внутри HTTP/2-сессии. Вот он:
Тут сразу можно заметить совпадающий ID: пусть спецификация рекомендует записывать ноль, но в запросе было указано 0x1001, и это помогло убедиться, что ответ - честный, с совпадающим значением ID. Содержательная часть ответа - две записи в блоке Answer с IPv4-адресами узлов. Обратите внимание, что использование ID тут показывает, насколько в DoH взаимосвязаны HTTP и DNS. ID приходит из DNS-запроса, попадает в DNS-ответ, но при этом реальная синхронизация запроса и ответа остаётся за HTTP, да ещё и ID может помешать кешированию, как отмечено выше.
Посмотрим на некоторые заголовки HTTP-ответа. Прежде всего, Content-Type содержит значение application/dns-message - то есть, это выделенный тип для DoH, поэтому клиент может убедиться, что, - скорее всего, - получил ответ верного типа: не забывайте, что это всё равно HTTP, так что каких-то разграничений протоколов по номерам портов, например, тут не может быть в принципе.
Заголовки, имеющие отношение к кешированию, установлены в соответствии с параметрами DNS-ответа. Например, Cache-Control - содержит значение, совпадающее с TTL из DNS-ответа. Это ещё один пример переплетения протоколов в DoH - сервер не может просто переслать DNS-ответ, как это было бы в случае туннеля, а должен этот ответ разобрать в контексте DNS (извлечь TTL) и соответственно оформить параметры HTTP. Заголовок:
Вообще, DNS-ответ может содержать разные значения TTL для разных записей, а в HTTP - кеш для запроса общий. Поэтому спецификация предписывает использовать на уровне HTTP минимальное значение TTL из присланных в DNS-ответе.
Поскольку DoH - это про доступ к DNS, то отдельный интерес представляет использование имён при работе с данным протоколом. Так, тестовый запрос, который мы использовали выше, отправлялся curl без указания расширения TLS с именем сервера (SNI). Это не помешало работе с сервисом. DoH-резолвер 8.8.8.8 ответил с серверным TLS-сертификатом, который содержит богатый набор хостнеймов и IP-адресов четвёртой и шестой версий в блоке имён:
То есть, если проводить валидацию имени, как предписывает спецификация HTTPS, то этот сертификат - подходит: мы обращались по IP-адресу 8.8.8.8 - он указан в сертификате. Если бы доступ происходил по имени хоста (домена), то проверять нужно было бы совпадение имени, вне зависимости от IP-адреса (это важно), и в DNS-именах тут есть dns.google. Но представьте, на минуточку, что для первоначального обнаружения сервиса DoH по имени используется "обычный" DNS, а после, при отправке запросов, соединение устанавливается уже по IP-адресу, и сверяется тоже IP-адрес. В таком случае, третья сторона может подменить DNS-ответ, указав собственный IP-адрес, для которого будет получен доверенный TLS-сертификат. (То есть, это сертификат именно для IP-адреса. Сертификат можно получить потому, что данная сторона управляет этим IP-адресом.) Тогда, если DoH-клиент при подключении уже без имени сервера, а только по IP-адресу, будет проверять совпадение именно IP-адреса в сертификате, то соединение будет воспринято как доверенное. Естественно, с чужим IP, а тем более с доменным именем, такой фокус, при штатной работе УЦ, не сработает. Заметьте, поскольку в DNS никаких TLS-сертификатов нет, то DoH-клиент должен тщательно сверять и имена, и адреса, и в сертификатах, и при выборе IP-адресов.
Подведём итог. DoH, благодаря использованию TLS, защищает DNS-трафик от прослушивания, а валидация серверного сертификата, необходимая в HTTPS, позволяет защитить соединение и от подмены. При этом DoH работает только на том хопе, где внедрён, и это почти всегда будет "последняя миля", от локального подключения до рекурсивного резолвера. Современные пакеты рекурсивных резолверов поддерживают доступ по HTTPS, пример - Unbound. Кроме того, технология DoH уже широко распространена на клиентах, - особенно, если речь о вебе, - что облегчает её внедрение. DoH не защищает сами DNS-данные - чтобы защитить содержательную часть DNS, необходимо использовать DNSSEC.
В отличие от DoT, DoH подходит строго для использования "на последней миле", то есть между приложением или операционной системой на компьютере пользователя и сервером, осуществляющим поиск информации в DNS - рекурсивным DNS-резолвером. HTTP в DNS-over-HTTPS слишком плотно "прилегает" к DNS, чтобы применение данной технологии на участке от рекурсивного резолвера до авторитативных серверов выглядело хотя бы минимально обоснованным. Собственно, исходный RFC и рассматривает только сценарий "последней мили", где DoH оказывается максимально полезной.
Чтобы разобраться, где именно тут "последняя миля", что это за авторитативные серверы и рекурсивные резолверы, нужно вспомнить основы DNS. Как сервис поиска данных, DNS базируется на двух упомянутых логических объектах: рекурсивных резолверах и авторитативных серверах. Рекурсивные резолверы собирают информацию, нужную для ответа на DNS-запрос, получая ответы авторитативных серверов. Классическая DNS работает по UDP и, несколько реже, по TCP. Авторитативные серверы (их ещё иногда называют "авторитетными") - это серверы, отвечающие за адресацию в конкретной DNS-зоне, то есть в области иерархии имён DNS, обозначенной DNS-именем. Пример такого имени: example.com. Авторитативным DNS-сервер становится в процессе делегирования, когда вышестоящий авторитативный сервер назначает ответственные серверы для некоторой DNS-зоны. Так, для example.com. авторитативными серверами являются b.iana-servers.net. и a.iana-servers.net., что несложно выяснить при помощи утилиты dig из пакета BIND:
Код:
$ dig -t NS example.com +short
a.iana-servers.net.
b.iana-servers.net.
Серверы, обозначенные a и b, назначены ответственными за example.com. на авторитативных серверах вышестоящей зоны - com. (Обратите внимание, что здесь и далее используется полная форма записи доменного имени, - FQDN, - обязательно оканчивающаяся точкой справа; эта точка отделяет корневой домен.)
Рекурсивный резолвер - это DNS-сервер, выполняющий рекурсивный опрос авторитативных DNS-серверов с целью поиска нужной записи. Почему опрос "рекурсивный" - скоро станет понятно. У рекурсивного резолвера есть собственный кеш, однако, если ответ на поступивший запрос в кеше не обнаруживается, то резолвер начинает поиск в глобальной DNS по довольно сложному алгоритму. Если система работает без ошибок, то рекурсивный резолвер получает от авторитативного сервера либо нужный DNS-ответ о целевом имени, либо так называемый "делегирующий ответ" с именами других авторитативных серверов, которым следует переадресовать запрос. Отсюда и появляется "рекурсия" в названии процесса. Рекурсивный резолвер обычно работает за пределами "локальной машины", в качестве внешнего сервиса. Часто сервис рекурсивного резолвера предоставляет провайдер интернет-доступа. Но есть и более массовые сервисы, работающие для Интернета в целом. Примером такого сервиса является Google Public DNS 8.8.8.8, который, кроме прочих технологий, поддерживает DoH.
На локальной машине DNS-запросы обрабатывает stub-резолвер - существенно более простая программа, которая только перенаправляет запросы рекурсивному резолверу и принимает от него ответы. В качестве stub-резолвера может выступать и элемент большего приложения, например веб-браузера.
Именно между оконечным stub-резолвером и рекурсивным резолвером находится та самая "последняя миля", для защиты которой применяется DoH. То есть, по HTTPS к рекурсивному резолверу отправляет DNS-запросы stub-резолвер (или его логический эквивалент). Несмотря на то, что системный stub-резолвер может работать через DoH, типовым примером внедрения DoH на клиенте всё ещё является веб-браузер, который умеет самостоятельно формировать DNS-запросы и непосредственно направлять их резолверу, поддерживающему DoH. Доступ DoH может предоставлять любой резолвер, для этого не обязательно размещаться на мощностях Google. Так что DoH могла бы быть развернута и на резолвере ближайшего провайдера интернет-доступа, но на практике такое встречается не так часто, как хотелось бы.
В точности как и DoT, DoH защищает только DNS-трафик, но не сами DNS-данные. То есть, DoH предоставляет защищённый канал, но вовсе не защищает состав ответов и запросов DNS. Если локальный веб-браузер использует DoH для выполнения запросов к рекурсивному резолверу, то имена в запросах и значения записей не будут простым способом доступны третьей стороне, прослушивающей трафик. Но если в данные DNS-транзакций вмешивается непосредственно рекурсивный резолвер или эти данные модифицируются на пути к резолверу от внешних DNS-серверов, то DoH тут никак не поможет: для защиты непосредственно DNS-данных нужно использовать DNSSEC. И работает DoH только на том "хопе", где включен, то есть на "последней миле" - от приложения на локальной машине до резолвера. Это важный аспект: отправка локального запроса к рекурсивному резолверу, при промахе в кеше, порождает DNS-трафик на внешние авторитативные серверы, и на эти DNS-запросы действие клиентского DoH не распространяется в принципе - они будут идти через промежуточные узлы в открытом виде.
Принципиальная схема работы с DoH простая. Запрос отправляется так же, как и любой другой HTTP-запрос. Схема и адрес для приёма DoH должны быть каким-то способом настроены в DoH-клиенте: можно ввести вручную, можно встроить в дистрибутив, как это сделано в браузерах; теоретически, можно даже раздать через DHCP, как предлагается, на правах идеи, в RFC. Конечно, IP-адрес хорошо бы тоже знать заранее, поскольку попытка получить этот адрес из DNS до запуска DoH заметно снижает пользу последнего (см., впрочем, ниже про валидацию адресов). Типичное имя точки приёма: dns-query (но может различаться от сервиса к сервису). Для приёма DoH спецификацией закреплено название параметра GET - dns. Так что GET-запрос выглядит так:
Код:
GET /dns-query?dns=%DNS-message-base64url%
Здесь %DNS-message-base64url% - это закодированное Base64url DNS-сообщение (собственно, запрос).
То есть, отправляем DNS-сообщение в качестве параметра GET, а в качестве HTTP-ответа получаем DNS-сообщение с результатом.
Возможна работа через POST, логика, в общем-то, та же, но полезные данные, как и предписывает POST, передаются не в URL и закодированными Base64url, а непосредственно в исходных байтах в теле запроса. Тут всё выглядит логично и знакомо всем, кто имел дело с HTTP.
DoH-сервер, соответствующий спецификации, должен поддерживать и GET, и POST. Далее в этой статье рассматривается только вариант с GET, поскольку он более иллюстративный, кроме того, в HTTP GET-запросы гораздо лучше сочетаются с механизмами кеширования.
Из-за того что интерфейс совпадает с обычным использованием HTTP, за реализацию DoH нередко ошибочно принимают другие REST и тому подобные API для работы с сервисом DNS-резолвера. Например, такой API с JSON предоставляет Google Public DNS по адресу https://dns.google/resolve - отправляем GET-запрос с именем домена и прочими параметрами по HTTPS, получаем результаты из DNS сразу в JSON. Удобно. Подход данного API, сам по себе, выглядит весьма логичным. Вот только это не современный DoH, несмотря на название страницы описания на сайте developers.google.com. К сожалению, DoH, согласно спецификации, работает совсем не так, а у того же Google Public DNS для подлинного DoH предусмотрен отдельный вход: https://dns.google/dns-query.
Архитектурно, DoH представляет собой переплетение из DNS и HTTP(S). Данная технология вовсе не является туннелем, как решения DNS-over-TLS. Семантика DoH позволяет параметрам HTTPS "просачиваться" в DNS, а параметрам DNS - "просачиваться" в обратную сторону, в HTTPS. Не самая очевидная особенность. Транзакционная схема HTTP в DoH прямо влияет на логику обработки DNS-запросов. И при этом DNS-запросы тут всё равно передаются в исходном, низкоуровневом формате DNS, но ещё и упаковываются в Base64, а на состав полей запроса накладываются требования, обусловленные особенностями HTTP. Например, наследуются алгоритмы кеширования уровня HTTP: то есть ответы на GET-, POST-запросы DoH и приходят с привычными статусами HTTP, и кешироваться могут как результаты прочих HTTP-транзакций, а не транзакций DNS (кешировать рекомендуется только GET-вариант).
Впрочем, такой архитектурный подход имеет и преимущества. Главное из них это то, что DoH-трафик оказывается неотличим от прочего HTTPS-трафика: тут тот же номер порта и транспортный протокол (443 и TCP), те же внутренние маркеры и те же запросы, а отличается лишь медиа-тип (Accept: application/dns-message - см. ниже), но он-то как раз снаружи не виден. Можно даже сказать, что DoH, таким образом, реализует минимальные меры для сокрытия факта своего использования - маскируется под работу с веб-сайтом. Впрочем, основное удобство HTTPS, с этой точки зрения, в том, что такой вариант позволяет DNS-информации лучше проходить через различные брандмауэры, расставленные по локальным периметрам и между сетями. Способ, как говорится, "обоюдоострый", но зато достаточно эффективный для того, чтобы его начали использовать даже некоторые "программы-зловреды", ибо в их жизненном цикле очень важны и успешный проход трафика сквозь сетевые барьеры, и сокрытие состава запроса.
Базовые шаги при использовании DoH для взаимодействия с рекурсивным резолвером следующие:
- клиент DoH определяет параметры обращения к серверу: URI и метод (GET или POST);
- для HTTPS требуется TLS, поэтому клиент DoH устанавливает TLS-соединение с сервером DoH;
- клиент формирует DNS-сообщение, содержащее DNS-запрос; по формату - это точно такое же сообщение, как использовалось бы и обычным DNS-клиентом при работе по UDP;
- клиент отправляет HTTP-запрос, который содержит DNS-запрос в качестве полезной нагрузки, через TLS-соединение с сервером;
- HTTP-сервер извлекает DNS-запрос и передаёт его DNS-серверу, который опрашивает DNS и подготавливает DNS-ответ;
- HTTP-сервер помещает DNS-сообщение внутрь HTTP-ответа и направляет ответ клиенту;
- клиент получает HTTP-ответ, проверяет статус и, если возможно, извлекает DNS-сообщение;
- клиент закрывает HTTPS-соединение, если нет других запросов.
Практика
В практической части воспользуемся сервисом Google Public DNS для определения IP-адресов (A-записи), соответствующих имени dns.google - то есть, не только адреса, но и имя этого сервиса Google послужит в качестве примера. Запросы отправляются при помощи curl. Трафик смотрим при помощи tshark. Исходное DNS-сообщение, так как оно элементарное, подготовлено вручную, прямой сборкой нужных байтов - это хакерский подход (в исходном смысле термина "хакер").
Начнём с DNS-сообщения. Здесь используется только малая часть возможностей и самый простой формат. Байтовый дамп (шестнадцатеричный) приведён ниже с краткими комментариями.
Код:
1001 // ID транзакции, здесь указано 0x1001 (см. ниже).
0120 // Флаги, задающие параметры сообщения (указаны: рекурсия (RD), поддержка аутентифицированных данных (AD)).
0001 // Количество записей в блоке запроса (Question) - одна запись.
0000 // Количество записей в блоке ответа (Answer) - нет записей, ноль.
0000 // Ноль записей в блоке серверов имён (NS).
0000 // Ноль записей в блоке дополнительных данных (Additional)
03646e7306676f6f676c6500 // Запрашиваемое имя (см. ниже).
0001 // Тип запроса (так называемый QTYPE).
0001 // Класс запроса (так называемый QCLASS).
Вообще, про формат и особенности DNS-сообщений можно написать очень много содержательного текста, однако к DoH это не имеет отношения, поэтому придётся оставить подробный разбор для других статей, а в этой - лишь краткие сведения о некоторых параметрах, влияющих на семантику DoT.
Первый из таких параметров - это два начальных байта сообщения: 16-битный идентификатор транзакции (ID). В классической DNS это простой механизм, позволяющий сопоставить запрос с ответом (этот же механизм считаеся базовым инструментом защиты от "упреждающего" спуфинга, так как для отправки ответа сразу с правильным ID нужно видеть запрос, но эффекивность его не велика, поскольку ID можно и угадать). В DoH соответствие ответа и запроса обеспечивает HTTP, так что в поле ID рекомендуется записывать значение ноль. Но можно и не ноль. В нашем сообщении-примере указано 0x1001. Смысл рекомендации писать ноль состоит в том, что HTTP-кеширование не различает внутреннюю семантику запросов и ответов, а обрабатывает их как HTTP, поэтому, если использовать различные ID, то в HTTP-кеше появятся ответы, идентичные по DNS-составу, но отличающиеся из-за разных значений ID. И тем не менее, DoH-сервером в ответе должно быть использовано такое же значение ID, как в DNS-запросе. Этот момент мы проверим ниже, рассматривая DoH-ответ.
Следом за ID указаны флаги, а вот блок из четырёх 16-битных значений после них - это индексы, определяющие количество записей в разных секциях сообщения. DNS-сообщение обязательно содержит четыре секции, сверху вниз: "Запрос" (Question), "Ответ" (Answer), "Серверы имён" (NS - Name Servers/Authority), "Дополнительно" (additional). В нашем сообщении заполняется только Question. Так что остальные - пустые. Заполненную секцию Answer мы увидим в ответе, а остальные - тут не рассматриваются.
После списка длин секций идёт сам запрос. Он единственный, имя - dns.google., указано в DNS-формате: отдельные лейблы имени (dns, google) закодированы в ASCII-строки, каждую из которых предваряет байт с длиной записи. В конце - корневой домен, который обозначается одним нулевым байтом. Например:
0x03 0x64 0x6e 0x73 - три байта (длина, первый байт), соответствующие dns.
Строку google (шесть букв, третья "o") и корневой домен читателю предлагается найти самостоятельно. Здесь использованы строчные буквы. DNS предполагает, что строчные и заглавные буквы, которые в ASCII оличаются одним битом, равны по значению. Что, впрочем, не мешает пытаться использовать различную запись для повышения количества энтропии в DNS-сообщениях. Метод называется рандомизацией регистра символов и используется тем же Google Public DNS, что очередной раз демонстирует высокую сложность DNS-протоколов, при том что сама система многим кажется "очень простой".
Два завершающих 16-битных блока обозначают, что для имени запрашивается запись типа A (IPv4) класса IN (Internet).
DoH-запрос всё равно отправляется по IP-адресу. И этот IP-адрес, в рамках данного эксперимента, известен заранее: 8.8.8.8. Отправляем запрос с помощью curl:
Код:
curl --http2 https://8.8.8.8/dns-query?dns=EAEBIAABAAAAAAAAA2RucwZnb29nbGUAAAEAAQ --header "Accept: application/dns-message" --output dns.google.bin
Обратите внимание на то, что используется HTTP/2 - это минимальная рекомендуемая версия HTTP для DoH. Дамп DNS-сообщения с запросом закодирован в строку Base64url. Проделать это кодирование можно, например, так:
Код:
cat req-1.hex | xxd -r -p | base64 > req-1.b64
В файле req-1.hex - дамп в hextext (комментарии, конечно, нужно удалить); в байты данные преобразует утилита xxd -r -p (-r - из текста - в байты; -p - формат записи, plain hexdump). Обратите внимание, что Base64url отличается от Base64 в деталях, там в алфавите "+" заменяется на "-", а "/" на "_", кроме того, нет дополнения, плюсы и слэши тут не возникают, а вот дополнение "==", которое будет выведено утилитой base64 в конце строки, нужно удалить.
Как указано, результат curl выводит в файл dns.google.bin, но мы посмотрим на ответ непосредственно в трафике (дамп записывается tcpdump). Заглянуть в трафик можно при помощи tshark, но для того, чтобы tshark смог расшифровать TLS-трафик, нужны сессионные ключи. Для получения сессионных ключей перед вызовом curl нужно задать переменную окружения SSLKEYLOGFILE=keys.log, где keys.log - файл, в который curl (а точнее - OpenSSL) выведет сессионные ключи.
Вызов tshark:
Код:
tshark -r doh.pcap -o tls.keylog_file:keys.log -O tls,http2,dns -S "-----PACKET-----" -x
Здесь doh.pcap - это файл дампа трафика, -o tls.keylog_file:keys.log - опция, показывающая, где брать файл с ключами, -O - перечень протоколов (обратите внимание, что тут http2); -S и -x - это, соответственно, разделитель вывода и выбор формата с hex-дампом и ASCII (то есть, дело вкуса, а к разбору DoH-трафика прямого отношения не имеет).
Поскролив выдачу tshark, нетрудно увидеть DNS-ответ, присланный, по запросу curl, сервисом 8.8.8.8 внутри HTTP/2-сессии. Вот он:
Код:
Domain Name System (response)
Transaction ID: 0x1001
Flags: 0x81a0 Standard query response, No error
1... .... .... .... = Response: Message is a response
.000 0... .... .... = Opcode: Standard query (0)
.... .0.. .... .... = Authoritative: Server is not an authority for domain
.... ..0. .... .... = Truncated: Message is not truncated
.... ...1 .... .... = Recursion desired: Do query recursively
.... .... 1... .... = Recursion available: Server can do recursive queries
.... .... .0.. .... = Z: reserved (0)
.... .... ..1. .... = Answer authenticated: Answer/authority portion was authenticated by the server
.... .... ...0 .... = Non-authenticated data: Unacceptable
.... .... .... 0000 = Reply code: No error (0)
Questions: 1
Answer RRs: 2
Authority RRs: 0
Additional RRs: 0
Queries
dns.google: type A, class IN
Name: dns.google
[Name Length: 10]
[Label Count: 2]
Type: A (Host Address) (1)
Class: IN (0x0001)
Answers
dns.google: type A, class IN, addr 8.8.8.8
Name: dns.google
Type: A (Host Address) (1)
Class: IN (0x0001)
Time to live: 267 (4 minutes, 27 seconds)
Data length: 4
Address: 8.8.8.8
dns.google: type A, class IN, addr 8.8.4.4
Name: dns.google
Type: A (Host Address) (1)
Class: IN (0x0001)
Time to live: 267 (4 minutes, 27 seconds)
Data length: 4
Address: 8.8.4.4
Тут сразу можно заметить совпадающий ID: пусть спецификация рекомендует записывать ноль, но в запросе было указано 0x1001, и это помогло убедиться, что ответ - честный, с совпадающим значением ID. Содержательная часть ответа - две записи в блоке Answer с IPv4-адресами узлов. Обратите внимание, что использование ID тут показывает, насколько в DoH взаимосвязаны HTTP и DNS. ID приходит из DNS-запроса, попадает в DNS-ответ, но при этом реальная синхронизация запроса и ответа остаётся за HTTP, да ещё и ID может помешать кешированию, как отмечено выше.
Посмотрим на некоторые заголовки HTTP-ответа. Прежде всего, Content-Type содержит значение application/dns-message - то есть, это выделенный тип для DoH, поэтому клиент может убедиться, что, - скорее всего, - получил ответ верного типа: не забывайте, что это всё равно HTTP, так что каких-то разграничений протоколов по номерам портов, например, тут не может быть в принципе.
Код:
Header: content-type: application/dns-message
Name Length: 12
Name: content-type
Value Length: 23
Value: application/dns-message
content-type: application/dns-message
[Unescaped: application/dns-message]
Representation: Literal Header Field with Incremental Indexing - Indexed Name
Index: 31
Заголовки, имеющие отношение к кешированию, установлены в соответствии с параметрами DNS-ответа. Например, Cache-Control - содержит значение, совпадающее с TTL из DNS-ответа. Это ещё один пример переплетения протоколов в DoH - сервер не может просто переслать DNS-ответ, как это было бы в случае туннеля, а должен этот ответ разобрать в контексте DNS (извлечь TTL) и соответственно оформить параметры HTTP. Заголовок:
Код:
Header: cache-control: private, max-age=267
Name Length: 13
Name: cache-control
Value Length: 20
Value: private, max-age=267
cache-control: private, max-age=267
[Unescaped: private, max-age=267]
Representation: Literal Header Field with Incremental Indexing - Indexed Name
Index: 24
Вообще, DNS-ответ может содержать разные значения TTL для разных записей, а в HTTP - кеш для запроса общий. Поэтому спецификация предписывает использовать на уровне HTTP минимальное значение TTL из присланных в DNS-ответе.
Поскольку DoH - это про доступ к DNS, то отдельный интерес представляет использование имён при работе с данным протоколом. Так, тестовый запрос, который мы использовали выше, отправлялся curl без указания расширения TLS с именем сервера (SNI). Это не помешало работе с сервисом. DoH-резолвер 8.8.8.8 ответил с серверным TLS-сертификатом, который содержит богатый набор хостнеймов и IP-адресов четвёртой и шестой версий в блоке имён:
Код:
dNSName: dns.google, dns.google.com, *.dns.google.com, 8888.google, dns64.dns.google
iPAddress: 8.8.8.8, 8.8.4.4, 2001:4860:4860::8888, 2001:4860:4860::8844, 2001:4860:4860::6464, 2001:4860:4860::64
То есть, если проводить валидацию имени, как предписывает спецификация HTTPS, то этот сертификат - подходит: мы обращались по IP-адресу 8.8.8.8 - он указан в сертификате. Если бы доступ происходил по имени хоста (домена), то проверять нужно было бы совпадение имени, вне зависимости от IP-адреса (это важно), и в DNS-именах тут есть dns.google. Но представьте, на минуточку, что для первоначального обнаружения сервиса DoH по имени используется "обычный" DNS, а после, при отправке запросов, соединение устанавливается уже по IP-адресу, и сверяется тоже IP-адрес. В таком случае, третья сторона может подменить DNS-ответ, указав собственный IP-адрес, для которого будет получен доверенный TLS-сертификат. (То есть, это сертификат именно для IP-адреса. Сертификат можно получить потому, что данная сторона управляет этим IP-адресом.) Тогда, если DoH-клиент при подключении уже без имени сервера, а только по IP-адресу, будет проверять совпадение именно IP-адреса в сертификате, то соединение будет воспринято как доверенное. Естественно, с чужим IP, а тем более с доменным именем, такой фокус, при штатной работе УЦ, не сработает. Заметьте, поскольку в DNS никаких TLS-сертификатов нет, то DoH-клиент должен тщательно сверять и имена, и адреса, и в сертификатах, и при выборе IP-адресов.
Подведём итог. DoH, благодаря использованию TLS, защищает DNS-трафик от прослушивания, а валидация серверного сертификата, необходимая в HTTPS, позволяет защитить соединение и от подмены. При этом DoH работает только на том хопе, где внедрён, и это почти всегда будет "последняя миля", от локального подключения до рекурсивного резолвера. Современные пакеты рекурсивных резолверов поддерживают доступ по HTTPS, пример - Unbound. Кроме того, технология DoH уже широко распространена на клиентах, - особенно, если речь о вебе, - что облегчает её внедрение. DoH не защищает сами DNS-данные - чтобы защитить содержательную часть DNS, необходимо использовать DNSSEC.