A5. Ітеративний DNS-резолвер
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»
у логах перестануть бути загадкою.
Завдання
Section titled “Завдання”Написати програму resolver: на C або на Python, за вибором. Чекер запускає
виконуваний файл, тому для Python потрібен shebang і chmod +x.
Будь-які бібліотеки, що розбирають чи будують DNS (dnspython, getdns,
c-ares, res_query, getaddrinfo для розв’язування імен), заборонені.
Сокети й стандартні struct, inet_ntop та подібні дозволені.
./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.
Перед початком
Section titled “Перед початком”- У модулі 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.
-
Тестова ієрархія.
Чекер створює простір імен
lab-a5і запускає в ньому сервер, який грає роль семи справжніх (кореня, двох TLD й чотирьох авторитетних). Без перевірки його піднімаєsudo ./check.sh --world:Адреса Роль Що віддає 127.0.0.10корінь .делегує test.іexample.127.0.0.20TLD test.делегує shop.test.,site.test.(без glue),multi.test.(два NS)127.0.0.21TLD example.делегує dns-host.example.127.0.0.30shop.test.записи A, AAAA, MX, TXT, CNAME та кілька пасток 127.0.0.31site.test.іdns-host.example.один хостинг на дві зони 127.0.0.40,.41multi.test.перший сервер мовчить, другий відповідає Подивіться на делегацію руками:
Terminal window sudo ip netns exec lab-a5 dig +norec @127.0.0.10 www.shop.test Asudo ip netns exec lab-a5 dig +norec @127.0.0.20 www.shop.test Asudo ip netns exec lab-a5 dig +norec @127.0.0.30 www.shop.test AУ відповіді кореня немає Answer, зате є Authority (NS) і Additional (glue); біта
aaнемає: це делегація. -
Запит і відповідь.
Побудуйте запит: 12 байтів заголовка (ідентифікатор, прапорці, чотири лічильники), ім’я у форматі «довжина-мітка» і два числа: тип і клас 1 (
IN). Надішліть UDP на порт 53 кореня й розберіть відповідь, поки що без стиснення імен: виведіть коди відповіді, прапорціAAіTC, кількість записів у кожній секції. -
Стиснення імен.
Справжні відповіді й відповіді чекера стискають імена: два байти з двома старшими бітами
11і 14-бітним зсувом від початку повідомлення замінюють решту імені. Вказівник може бути всередині імені, а не лише на початку, і стиснуті імена лежать не тільки в заголовках записів, а й у данихNS,CNAME,MX,SOA.Запишіть у звіт: чому вказівник має вказувати назад, і що сталося б у вашому коді на
evil.shop.test, де вказівник веде на самого себе. -
Ходіння по ієрархії.
Починайте з кореня. Відповідь із секцією Answer — кінець. Відповідь без Answer, з NS в Authority і без
aa— делегація: візьміть адреси з Additional і продовжуйте з ними. Відповідь із кодом 3 (NXDOMAIN) — кінець. Порожня відповідь зaa—NODATA. -
CNAME, glue, TCP, таймаути.
CNAME у відповіді означає, що запитаного типу в цього імені немає, але є псевдонім: запитайте ціль, починаючи з найкращої відомої делегації, а не з того самого сервера. Делегація без glue (
site.test.) вимагає розв’язати ім’я сервера, а це знову той самий алгоритм, тож нехай функція буде рекурсивною, але з обмеженням глибини й числа запитів.big.shop.testне вміщається у 512 байтів: сервер ставитьTC, а повний запит іде по TCP. Підроблені й чужі відповіді відкидайте.Запишіть у звіт: навіщо перевіряти адресу відправника, ідентифікатор і питання у відповіді, і чому це досі лише частковий захист (скільки бітів невідомо зловмиснику).
-
Кеш.
Ключ —
(ім'я, тип), значення — записи й момент, коли вони закінчаться (час отримання плюс найменший TTL). Окремо кешуйте NS із адресами: тоді друге ім’я в тій самій зоні піде прямо до її серверів, минаючи корінь і TLD. Режим stdin дає змогу це побачити й виміряти.Запишіть у звіт: скільки запитів у мережу потребує холодне питання
www.shop.test, а скільки друге питанняmail.shop.testі чому.
Перевірка
Section titled “Перевірка”cd labs/a5-dns-resolversudo ./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 с береться знову);
- випадковість ідентифікаторів запитів.
Чекер не залежить від мови, розкладки рядків виводу (пробіли, табуляції), порядку, у якому ви перебираєте сервери імен, і від того, чи вмієте ви щось понад вимоги (наприклад, негативний кеш).
Часті помилки
Section titled “Часті помилки”Вказівник стиснення читається як довжина мітки. Старші біти 11 означають
вказівник, а не довжину. Мітка має довжину щонайбільше 63.
Позиція після імені. Після вказівника ім’я закінчується, а наступне поле починається відразу за двома байтами вказівника, а не там, куди він вів.
CNAME шукають лише в Answer запитаного типу. Запит на A для псевдоніма
повертає запис CNAME, а не A. Перевіряйте обидва типи.
Glue береться без розбору. Сервер може вкласти в Additional будь-яку адресу. Приймайте лише ті імена, що є в NS і лежать усередині делегованої зони, інакше отруєний кеш лишиться в пам’яті надовго.
Завжди корінь. Якщо кожне питання починається з кореня, кеш не працює: спершу шукайте в кеші найближчу відому делегацію.
Рекурсія без межі. Резолвер, який розв’язує ім’я сервера, а для нього ще одне, і так по колу, чекер помітить як зависання. Обмежте глибину й загальне число запитів на одне питання.
Час життя рахується від початку програми. Запис закінчується через його TTL після отримання, і в кеші друкується залишок, а не початкове значення.
Тайм-аут на recv без повтору. Мовчазний сервер не може зупиняти
резолвер: після таймауту пробуйте наступний NS із набору.
Далі, якщо цікаво
Section titled “Далі, якщо цікаво”Додати EDNS0 із більшим розміром UDP-повідомлення (і переконатися,
що big.shop.test поміщається без TCP), негативний кеш за SOA,
випадкову адресу вихідного порту, паралельні запити до всіх серверів
набору й вибір найшвидшого. Поставити перед ним dnsmasq чи unbound і порівняти
поведінку на тих самих пастках. Потім пройти B5, де
ви піднімаєте справжній авторитетний сервер і рекурсивний резолвер.