Перейти до вмісту

A5. Ітеративний DNS-резолвер

середнійспирається на модуль 13

getaddrinfo ховає за собою довгий ланцюжок: питання кореневому серверу, відповідь «не знаю, спитайте test.», питання серверу test., ще одна делегація, нарешті авторитетна відповідь. Модуль 13 описує цей шлях і формат повідомлення, тут ви пройдете його кодом. Резолвер, який ви напишете, не використовує жодної бібліотеки DNS: лише сокети й байти.

Після цієї роботи ви зможете:

  • зібрати DNS-запит і розібрати відповідь: заголовок, секції питань, відповідей, авторитетних серверів і додаткових записів, зокрема записи A, AAAA, NS, CNAME, MX, TXT і SOA;
  • читати імена зі стисненням (вказівники) і не потрапити в цикл, який підсовує шкідливий сервер;
  • іти за делегаціями від кореня, використовувати glue і розв’язувати сервери імен, для яких його немає;
  • йти за ланцюжком CNAME між зонами, повторювати запит по TCP, коли відповідь обрізано, й відкидати відповіді з чужим ідентифікатором;
  • зберігати відповіді в кеші за TTL і пояснити, навіщо кешувати делегації.

Так влаштовані Unbound, Knot Resolver і systemd-resolved у «повному» режимі. Після роботи повідомлення на кшталт SERVFAIL чи «lame delegation» у логах перестануть бути загадкою.

Написати програму resolver: на C або на Python, за вибором. Чекер запускає виконуваний файл, тому для Python потрібен shebang і chmod +x. Будь-які бібліотеки, що розбирають чи будують DNS (dnspython, getdns, c-ares, res_query, getaddrinfo для розв’язування імен), заборонені. Сокети й стандартні struct, inet_ntop та подібні дозволені.

Terminal window
./resolver 127.0.0.10 www.shop.test A # одне питання; корінь — перший аргумент
./resolver 127.0.0.10 www.shop.test # тип за замовчуванням — A
./resolver 127.0.0.10 # питання зі stdin, один на рядок

Порт завжди 53. Корінь передається адресою, а не береться зі списку: так чекер підставляє свою ієрархію замість справжньої.

Вивід (один рядок на запис, розділювачі — будь-які пробіли):

alias.shop.test. 300 IN CNAME www.shop.test.
www.shop.test. 300 IN A 192.0.2.10
  • спершу ланцюжок CNAME у порядку проходження, потім кінцеві записи запитаного типу;
  • імена малими літерами з крапкою наприкінці; TTL — скільки записові лишилося жити (з кешу це менше, ніж віддав сервер);
  • дані: A і AAAA — як inet_ntop; NS і CNAME — ім’я; MX — пріоритет і ім’я (10 mx.shop.test.); TXT — рядки в подвійних лапках через пропуск;
  • імені немає: слово NXDOMAIN, код виходу 1;
  • ім’я є, а запису такого типу немає: слово NODATA, код виходу 1;
  • помилка (недосяжні сервери, цикл, пошкоджена відповідь): ERROR: причина, код виходу 2, за обмежений час.

Режим stdin. Без питання в аргументах програма читає рядки ім'я [тип]. Після відповіді на кожне питання вона друкує рядок з однієї крапки . і скидає буфер виводу. Кеш живе між питаннями.

Що має вміти:

  • іти від переданого кореня, не підставляючи жодних інших адрес: у запитах до авторитетних серверів прапорець RD (бажана рекурсія) не ставиться;
  • генерувати випадковий ідентифікатор запиту й приймати лише відповіді з таким самим ідентифікатором, від того ж сервера і з тим самим питанням; чужі відповіді тихо пропускати й чекати справжню;
  • використовувати glue з розділу Additional, а для серверів імен без glue розв’язувати їхні імена окремим питанням;
  • не довіряти забагато: приймати з Additional лише адреси імен, що входять в набір NS і лежать усередині делегованої зони;
  • після CNAME розв’язувати ціль із самого початку (іншою зоною, якщо треба) і перервати цикл CNAME;
  • при встановленому бітові TC повторити запит до того ж сервера по TCP (перед повідомленням два байти довжини);
  • на таймаут сервера (приблизно 1,5 с) пробувати наступний із набору NS;
  • кешувати відповіді за TTL, а також NS і адреси серверів імен, і не повторювати запит, відповідь на який уже є; завершений TTL прибирає запис;
  • не зависати й не падати на пошкоджених відповідях, зокрема на циклі у вказівниках стиснення.

Чого робити не треба. EDNS0, DNSSEC, IPv6-транспорт, відповіді типів поза переліченими, негативний кеш (можна, але чекер його не перевіряє), паралельні запити, асинхронність.

Готово, коли:

  • sudo ./check.sh ./resolver проходить усі перевірки;
  • dig +norec @127.0.0.10 www.shop.test у тому самому просторі імен показує ту саму відповідь, що й ваш резолвер;
  • у звіті є відповіді на запитання з етапів 3, 5 і 6.
  • У модулі 13 прочитайте про ієрархію, делегації, glue, CNAME й кеш. Формат повідомлення DNS описують документи, перелічені в джерелах модуля; dig +norec +noall +answer +authority +additional покаже ті самі секції вже розібраними.
  • Розпакуйте архів курсу: чекер лежить у labs/a5-dns-resolver/check.sh, а тестова ієрархія в dnsworld.py.
  • Знадобляться python3, ip і права root (простори імен і порт 53), для порівняння dig із пакета bind9-dnsutils. Підійде ВМ, WSL2 чи контейнер із CAP_NET_ADMIN.
  1. Тестова ієрархія.

    Чекер створює простір імен lab-a5 і запускає в ньому сервер, який грає роль семи справжніх (кореня, двох TLD й чотирьох авторитетних). Без перевірки його піднімає sudo ./check.sh --world:

    Адреса Роль Що віддає
    127.0.0.10 корінь . делегує test. і example.
    127.0.0.20 TLD test. делегує shop.test., site.test. (без glue), multi.test. (два NS)
    127.0.0.21 TLD example. делегує dns-host.example.
    127.0.0.30 shop.test. записи A, AAAA, MX, TXT, CNAME та кілька пасток
    127.0.0.31 site.test. і dns-host.example. один хостинг на дві зони
    127.0.0.40, .41 multi.test. перший сервер мовчить, другий відповідає

    Подивіться на делегацію руками:

    Terminal window
    sudo ip netns exec lab-a5 dig +norec @127.0.0.10 www.shop.test A
    sudo ip netns exec lab-a5 dig +norec @127.0.0.20 www.shop.test A
    sudo ip netns exec lab-a5 dig +norec @127.0.0.30 www.shop.test A

    У відповіді кореня немає Answer, зате є Authority (NS) і Additional (glue); біта aa немає: це делегація.

  2. Запит і відповідь.

    Побудуйте запит: 12 байтів заголовка (ідентифікатор, прапорці, чотири лічильники), ім’я у форматі «довжина-мітка» і два числа: тип і клас 1 (IN). Надішліть UDP на порт 53 кореня й розберіть відповідь, поки що без стиснення імен: виведіть коди відповіді, прапорці AA і TC, кількість записів у кожній секції.

  3. Стиснення імен.

    Справжні відповіді й відповіді чекера стискають імена: два байти з двома старшими бітами 11 і 14-бітним зсувом від початку повідомлення замінюють решту імені. Вказівник може бути всередині імені, а не лише на початку, і стиснуті імена лежать не тільки в заголовках записів, а й у даних NS, CNAME, MX, SOA.

    Запишіть у звіт: чому вказівник має вказувати назад, і що сталося б у вашому коді на evil.shop.test, де вказівник веде на самого себе.

  4. Ходіння по ієрархії.

    Починайте з кореня. Відповідь із секцією Answer — кінець. Відповідь без Answer, з NS в Authority і без aa — делегація: візьміть адреси з Additional і продовжуйте з ними. Відповідь із кодом 3 (NXDOMAIN) — кінець. Порожня відповідь з aa — NODATA.

  5. CNAME, glue, TCP, таймаути.

    CNAME у відповіді означає, що запитаного типу в цього імені немає, але є псевдонім: запитайте ціль, починаючи з найкращої відомої делегації, а не з того самого сервера. Делегація без glue (site.test.) вимагає розв’язати ім’я сервера, а це знову той самий алгоритм, тож нехай функція буде рекурсивною, але з обмеженням глибини й числа запитів. big.shop.test не вміщається у 512 байтів: сервер ставить TC, а повний запит іде по TCP. Підроблені й чужі відповіді відкидайте.

    Запишіть у звіт: навіщо перевіряти адресу відправника, ідентифікатор і питання у відповіді, і чому це досі лише частковий захист (скільки бітів невідомо зловмиснику).

  6. Кеш.

    Ключ — (ім'я, тип), значення — записи й момент, коли вони закінчаться (час отримання плюс найменший TTL). Окремо кешуйте NS із адресами: тоді друге ім’я в тій самій зоні піде прямо до її серверів, минаючи корінь і TLD. Режим stdin дає змогу це побачити й виміряти.

    Запишіть у звіт: скільки запитів у мережу потребує холодне питання www.shop.test, а скільки друге питання mail.shop.test і чому.

Terminal window
cd labs/a5-dns-resolver
sudo ./check.sh ./resolver

Чекер піднімає lab-a5 із ієрархією з таблиці, запускає ваш резолвер всередині й зазирає в журнал запитів, який веде тестовий сервер. Він перевіряє:

  • порядок відвіданих серверів для www.shop.test (корінь, test., shop.test.), TTL у виводі, відсутність RD;
  • типи A, AAAA, MX, TXT і NS; NXDOMAIN і NODATA з кодом виходу 1; обрізану відповідь із повторним запитом по TCP;
  • CNAME в межах зони й між зонами, делегацію без glue, мертвий сервер серед NS;
  • підроблену відповідь із чужим ID, вказівник стиснення на самого себе, цикл CNAME, недосяжний корінь;
  • режим stdin: відповідь із кешу без запитів у мережу, делегація з кешу, закінчення TTL (запис із TTL 2 с через 3 с береться знову);
  • випадковість ідентифікаторів запитів.

Чекер не залежить від мови, розкладки рядків виводу (пробіли, табуляції), порядку, у якому ви перебираєте сервери імен, і від того, чи вмієте ви щось понад вимоги (наприклад, негативний кеш).

Вказівник стиснення читається як довжина мітки. Старші біти 11 означають вказівник, а не довжину. Мітка має довжину щонайбільше 63.

Позиція після імені. Після вказівника ім’я закінчується, а наступне поле починається відразу за двома байтами вказівника, а не там, куди він вів.

CNAME шукають лише в Answer запитаного типу. Запит на A для псевдоніма повертає запис CNAME, а не A. Перевіряйте обидва типи.

Glue береться без розбору. Сервер може вкласти в Additional будь-яку адресу. Приймайте лише ті імена, що є в NS і лежать усередині делегованої зони, інакше отруєний кеш лишиться в пам’яті надовго.

Завжди корінь. Якщо кожне питання починається з кореня, кеш не працює: спершу шукайте в кеші найближчу відому делегацію.

Рекурсія без межі. Резолвер, який розв’язує ім’я сервера, а для нього ще одне, і так по колу, чекер помітить як зависання. Обмежте глибину й загальне число запитів на одне питання.

Час життя рахується від початку програми. Запис закінчується через його TTL після отримання, і в кеші друкується залишок, а не початкове значення.

Тайм-аут на recv без повтору. Мовчазний сервер не може зупиняти резолвер: після таймауту пробуйте наступний NS із набору.

Додати EDNS0 із більшим розміром UDP-повідомлення (і переконатися, що big.shop.test поміщається без TCP), негативний кеш за SOA, випадкову адресу вихідного порту, паралельні запити до всіх серверів набору й вибір найшвидшого. Поставити перед ним dnsmasq чи unbound і порівняти поведінку на тих самих пастках. Потім пройти B5, де ви піднімаєте справжній авторитетний сервер і рекурсивний резолвер.