Как HTTP(S) используется для DNS: DNS-over-HTTPS на практике

Добро пожаловать на наш форум!

Спасибо за посещение нашего сообщества. Пожалуйста, зарегистрируйтесь или войдите, чтобы получить доступ ко всем функциям.


Gibby

Автор
Команда проекта

Регистрация
Сообщений
1,846
Репутация
49
Сделок
Ни 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:


Код:
$ 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 могла бы быть развернута и на резолвере ближайшего провайдера интернет-доступа, но на практике такое встречается не так часто, как хотелось бы.


0.gif
DNS-пакеты в трубе HTTP/TLS
В точности как и 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 для взаимодействия с рекурсивным резолвером следующие:

  1. клиент DoH определяет параметры обращения к серверу: URI и метод (GET или POST);
  2. для HTTPS требуется TLS, поэтому клиент DoH устанавливает TLS-соединение с сервером DoH;
  3. клиент формирует DNS-сообщение, содержащее DNS-запрос; по формату - это точно такое же сообщение, как использовалось бы и обычным DNS-клиентом при работе по UDP;
  4. клиент отправляет HTTP-запрос, который содержит DNS-запрос в качестве полезной нагрузки, через TLS-соединение с сервером;
  5. HTTP-сервер извлекает DNS-запрос и передаёт его DNS-серверу, который опрашивает DNS и подготавливает DNS-ответ;
  6. HTTP-сервер помещает DNS-сообщение внутрь HTTP-ответа и направляет ответ клиенту;
  7. клиент получает HTTP-ответ, проверяет статус и, если возможно, извлекает DNS-сообщение;
  8. клиент закрывает HTTPS-соединение, если нет других запросов.
Обратите внимание, что первый, четвёртый и последующие шаги - прямо используют семантику HTTP. Пусть здесь и передаётся DNS-сообщение, но оно всегда передаётся по HTTP, как полезная нагрузка, которая либо составляет запрос (например, GET-параметр), либо содержится в HTTP-ответе. Это фундаментальная особенность DoH.

Практика

В практической части воспользуемся сервисом 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.
 
Сверху