FastNetMon

Saturday 26 October 2019

Пожалуйста, не используйте DNS протокол для вашего приложения!

Отличный заголовок, не правда ли? Я думаю многие знают, что уже почти 3.5 года я работаю исключительно с протоколом DNS и за это время у меня накопилось довольно большое количество способов выстрелить себе в ногу на пустом, казалось бы, месте.

Протокол DNS отлично подходит для задачи, чтобы привести посетителей на Ваш сайт / ресурс, но на этом его задача должна заканчиваться. Если вам требуется раздача какой-то конфигурации (почему бы не сделать это используя TXT запись?), списка доступных узлов для приложения (также известного как service discovery в быту) или что-то еще не связанное напрямую с браузерами конечного клиента (также известного как eyeballs), то, пожалуйста, не используйте DNS.

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

Мы убедились, что реализация протокола DNS для нашего языка отсутствует и мы начали реализацию собственного клиента. Общеизвестно, что DNS - это один из тех немногих протоколов, который использует UDP протокол и мы уже подготовили UDP клиент, устанавливющий соединение с удаленным узлом (на котором ожидается работающий рекурсивный DNS резолвер) по порту 53.

Следующий шаг, попытаться сказать DNS серверу, что мы конкретно от него хотим, тут перед нашим взглядом предстает картина (любезно взятая с inacon.de):


Выглядит страшно, но мы видим поле Questions и можем догадаться, что именно туда нам нужно добавить наш запрос (естественно, закодированный в особенном формате).

Первая проблема, с которой вы можете столкнуться - это то, что слово Question здесь во множественном числе. Нет, возможности запросить более 1й записи в реально работающих реализация DNS серверов в 99% случаев нету, поэтому задать вы можете сугубо один вопрос.

Но что если Ваше приложение поддерживает dual stack (IPv4 + IPv6 адреса) и вам не хочется делать два последовательных запроса (A и потом AAAA, хотя я бы рекомендовал обратную последовательность). Очевидно, на выручку приходит тип запроса ANY, который вернет все записи (на самом деле - не все, далеко не все). 

Это еще одна ловушка! Многие провайдеры DNS отказываются от типа ANY и существует RFC, стандартизирующий это поведение. Из крупных провайдеров, например, 1.1.1.1 не поддерживает этот тип и вы получите NOTIMP в ответ. Поэтому вам придется делать разрешение имен последовательно.

Итак, мы добрались до момента, когда мы создали DNS запрос, требующий у сервера A запись о домене stableit.ru. Как же ответит нам сервер и ответит ли? Начнем с того, что он может не ответить, но точная установка лимита, как долго ждать ответа и сколько раз повторять его - очень сложная задача. В дикой среде (простите, не могу перевести in the wild) Интернета эта цифра может варьироваться от сотен миллисекунд до нескольких тысяч миллисекунд. Поэтому будьте очень аккуратны с лимитом.

Вуаля! Удача! Сервер ответил! Возвращаемся к картинке упомянутой ранее, там есть поля Answer и очевидно ответ будет там! Как же понять, что поиск запрошенного имени был успешен? Смотрим внимательнее и видим поле rCode. Что делает квалифицированный программист? Он идет на сайт IANA и смотрит документированные значения для этого поля.

Еще пару секунд поиска и о чудо - мы нашли NoError! Очевидно, таким образом сервер сообщает, что поиск имени был успешный! Ура! 

Теперь снова смотрим картину со структурой пакета и пытаемся найти, где именно искать ответ. В чем разница между answer, authority и additional? Совершенно не ясно, но методом тыка мы можем обнаружить, что ответ нам пришел в answer. 

Всегда ли это будет так? Нет! В протоколе DNS совершенно нормальным (также известным как NODATA) считается вернуть rCode установленный в NoError и пустую секцию Answer! При этом в секции authority будет совершенно не нужная нам запись SOA.

Итак, мы приходим к выводу, что определить успешность DNS запроса можно двумя проверками: rCode == NoError и секция anwer не пуста.

Ура! Мы почти близки к работающему стабильно DNS клиенту и радостно едем с офиса домой, где решаем продолжить работу. И вот незадача - клиент перестал работать и падает с фатальной ошибкой, потому что rCode == NoError, секция Answer не пуста, но в ней нету A записи! В ней какая-то "CNAME", указывающая на другой сервер и никаких следов IP адреса! Вот тут-то мы и приближаемся к развязке и причинам моего предостережения от использования DNS для приложений кроме случаев, где иных вариантов не существует.

Да, возврат CNAME совершенно легитимен и используется различными реализациями рекурсивных серверов, кроме одного CNAME также может быть возвращена цепочка CNAME или даже CNAME и А запись. Каждый из этих случаев должен быть обработан и время от времени требуется еще следующий (рекурсивный) запрос, чтобы получить IP адрес целевого сервера из CNAME. 

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

Полгода спустя вы начинаете замечать, что свеже добавленные 15-ть серверов к 15ти существуюшим (которые ваше приложение-клиент получает силами DNS запроса) не получают трафика вообще! В чем дело? После часов-дней поиска, вы обнаружите, что DNS сервер возвращает 15 (зависит от случая и длины зоны, но порядок числа остается). Итак, знакомьтесь мы уперлись в лимит длины DNS ответа - 512 байт. Что в этом случае делать? Расчехлять навыки работы с протоколом TCP и реализовывать логику, чтобы в случае, когда по протоколу UDP сервер вернул ответ с флагом TC, то запрос должен быть повторен по TCP, который не имеет размеров на число записей.

Мои поздравления, мы учли почти все довольно очевидные проблемы в реализации надежного и стабильно работающего со всем многообразии авторитативных и рекурсивных DNS серверов клиента!  

К сожалению, при последующем улучшении вашего клиента, например, при попытке добавить кэш вы столкнетесь с тем, что продолжительность кэширования для успешных запросов (когда сервер ответил с записями) и неуспешных - должна быть разной и только удаленный DNS сервер может предоставить эту информацию (тут вы удивитесь тому, что каждый ответ в RRSet может иметь свой собственный TTL, но использовать его нельзя, см секцию 5.2).

Ах да, еще. Почти невозможно отличить временный сбой DNS сервера от постоянного, так как в обоих случаях Вы будете видеть SERVFAIL.

Надеюсь, после всего этого страха Вы не будете использовать протокол DNS там, где он не нужен и будете раздавать конфигурацию вашим мобильным (или серверным) приложениям в формате JSON используя хорошо зарекомендовавший себя протокол HTTP.

Спасибо за внимание!