Історія про зниклі DNS-пакети від техпідтримки Google Cloud

Від редактора блогу Google: Чи цікавилися ви коли-небудь тим, як інженери Google Cloud Technical Solutions (TSE) займаються вашими зверненнями у техпідтримку? У сфері відповідальності інженерів технічної підтримки TSE лежить виявлення та усунення вказаних користувачами джерел проблем. Деякі з цих проблем досить прості, але іноді трапляється звернення, яке потребує уваги відразу кількох інженерів. У цій статті один із співробітників TSE розповість нам про одну дуже хитромудру проблему зі своєї недавньої практики. випадок з пакетами DNS, що пропадають. У ході цієї розповіді ми побачимо, як інженерам вдалося вирішити ситуацію, і що нового вони дізналися в ході усунення помилки. Ми сподіваємося, що ця історія не тільки розповість вам про глибоко вкорінений баг, але й дасть розуміння процесів, що проходять при подачі звернення на підтримку Google Cloud.

Історія про зниклі DNS-пакети від техпідтримки Google Cloud

Усунення несправностей це це й наука, і мистецтво. Все починається з побудови гіпотези про причину нестандартної поведінки системи, після чого вона перевіряється на міцність. Однак, перш ніж сформулювати гіпотезу, ми маємо чітко визначити та точно сформулювати проблему. Якщо питання звучить занадто розпливчасто, то вам доведеться як слід все проаналізувати; у цьому полягає «мистецтво» усунення несправностей.

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

Деякі користувачі вважають, що ми виправимо все ніби механіки в автосервісі, і просто надсилають нам id віртуальної машини, тоді як насправді процес протікає у форматі розмови: збір інформації, формування та підтвердження (або спростування) гіпотез, і, зрештою, рішення Проблема будується на спілкуванні з клієнтом.

Розглянута проблема

Сьогодні перед нами історія з добрим кінцем. Однією з причин успішного вирішення запропонованого кейсу є дуже детальний і точний опис проблеми. Нижче можна побачити копію першого тикета (відредагованого, з метою приховати конфіденційну інформацію):
Історія про зниклі DNS-пакети від техпідтримки Google Cloud
У цьому повідомленні дуже багато корисної для нас інформації:

  • Вказано конкретну VM
  • Вказано саму проблему — не працює DNS
  • Вказано де проблема себе проявляє - VM та контейнер
  • Вказано кроки, які зробив користувач для визначення проблеми

Звернення було зареєстровано як "P1: Critical Impact - Service Unusable in production", що означає постійний контроль ситуації 24/7 за схемою "Follow the Sun" (на посилання можна докладніше почитати про пріоритети користувальних звернень), з передачею її від однієї команди техпідтримки до іншої при кожному зрушенні часових поясів. По суті, до того моменту, як проблема дійшла до нашої команди в Цюріху, вона встигла обігнути земну кулю. До цього часу користувач вжив заходів щодо зниження наслідків, проте побоювався повторення ситуації на продакшні, оскільки основна причина все ще не була виявлена.

На момент, коли тикет дійшов до Цюріха, у нас на руках вже була така інформація:

  • вміст /etc/hosts
  • вміст /etc/resolv.conf
  • Висновок iptables-save
  • Зібраний командою ngrep файл pcap

З цими даними ми були готові розпочати етап «розслідування» та усунення несправностей.

Наші перші кроки

Насамперед ми перевірили логи та статус сервера метаданих і переконалися, що він працює коректно. Сервер метаданих відповідає IP-адресою 169.254.169.254 і, крім іншого, відповідає за контроль над іменами доменів. Ми так само перевіряли ще раз, що файервол коректно працює з VM і не блокує пакети.

Це була якась дивна проблема: перевірка nmap спростувала нашу основну гіпотезу про втрату UDP пакетів, тому ми подумки вивели ще кілька варіантів та способів їхньої перевірки:

  • Чи пропадають пакети вибірково? => Перевірити правила iptables
  • Чи не дуже малий MTU? => Перевірити висновок ip a show
  • Чи торкається проблема тільки UDP-пакети або ж і TCP? => Прогнати dig +tcp
  • Чи повертаються згенеровані dig пакети? => Прогнати tcpdump
  • Чи правильно працює libdns? => Прогнати strace для перевірки передачі пакетів в обидві сторони

Тут ми вирішуємо зателефонувати користувачеві для усунення несправностей наживо.

У ході дзвінка нам вдається перевірити кілька речей:

  • Після кількох перевірок ми виключаємо правила iptables зі списку причин
  • Ми перевіряємо інтерфейси мережі та таблиці маршрутизації, та перевіряємо ще раз коректність MTU
  • Ми виявляємо що dig +tcp google.com (TCP) працює як слід, але ось dig google.com (UDP) не працює
  • Прогнавши tcpdump поки працює dig, ми виявляємо що UDP пакети повертаються
  • Ми проганяємо strace dig google.com і бачимо як dig коректно викликає sendmsg() и recvms(), проте другий переривається по таймууту

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

from scapy.all import *

answer = sr1(IP(dst="169.254.169.254")/UDP(dport=53)/DNS(rd=1,qd=DNSQR(qname="google.com")),verbose=0)
print ("169.254.169.254", answer[DNS].summary())

Цей фрагмент створює DNS-пакет і надсилає запиту серверу метаданих.

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

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

Тим часом користувач люб'язно погоджується надати знімок образу системи. Це дуже хороші новини: можливість самому тестувати систему значно прискорює усунення несправностей, адже більше не треба просити користувача запускати команди, надсилати мені результати та аналізувати їх, я можу все зробити сам!

Колеги починають мені помалу заздрити. За обідом ми обговорюємо звернення, проте ні в кого немає ідей про те, що відбувається. На щастя, сам користувач вже вжив заходів щодо пом'якшення наслідків і нікуди не поспішає, тому ми маємо час препарувати проблему. І так як у нас є образ, ми можемо провести будь-які цікаві для нас тести. Чудово!

Повертаючись на крок назад

Одне з найпопулярніших питань на інтерв'ю на посаду системного інженера звучить так: «Що відбувається, коли ви пінгуєте www.google.com?» Питання шикарне, оскільки кандидату необхідно описати нехай від оболонки до простору користувача, до ядра системи і далі до мережі. Я посміхаюся: іноді питання з інтерв'ю виявляються корисними і в реальному житті.

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

  1. Програма викликає системну бібліотеку, наприклад libdns
  2. libdns перевіряє конфігурацію системи до якого сервера DNS їй звертатися (на діаграмі це 169.254.169.254, сервер метаданих)
  3. libdns використовує системні виклики для створення UDP сокету (SOKET_DGRAM) та передачі UDP пакетів із DNS запитом в обидві сторони
  4. Через інтерфейс sysctl можна налаштувати UDP стек на рівні ядра
  5. Ядро взаємодіє із залізом для передачі пакетів по мережі через мережевий інтерфейс
  6. Гіпервізор ловить та передає пакет серверу метаданих при контакті з ним
  7. Сервер метаданих своїм чаклунством визначає DNS ім'я і таким же методом повертає відповідь

Історія про зниклі DNS-пакети від техпідтримки Google Cloud
Нагадаю, які гіпотези ми вже встигли розглянути:

Гіпотеза: Зламані бібліотеки

  • Тест 1: прогнати в системі strace, перевірити, що dig викликає коректні системні виклики
  • Результат: викликаються коректні системні виклики
  • Тест 2: через srapy перевірити чи можемо ми визначати імена в обхід системних бібліотек
  • Результат: можемо
  • Тест 3: прогнати rpm –V на пакеті libdns та md5sum файлах бібліотеки
  • Результат: код бібліотеки повністю ідентичний коду в робочій операційній системі
  • Тест 4: монтувати образ кореневої системи користувача на VM без подібної поведінки, прогнати chroot, подивитися чи працює DNS
  • Результат: DNS працює коректно

Висновок на основі тестів: проблема не в бібліотеках

Гіпотеза: Існує помилка в налаштуваннях DNS

  • Тест 1: перевірити tcpdump і поспостерігати коректно чи відправляються і повертаються DNS пакети після запуску dig
  • Результат: пакети передаються коректно
  • Тест 2: перевіряти ще раз на сервері /etc/nsswitch.conf и /etc/resolv.conf
  • Результат: все коректно

Висновок на основі тестів: проблема не в конфігурації DNS

Гіпотеза: пошкоджене ядро

  • Тест: встановити нове ядро, перевірити підпис, перезапустити
  • Результат: аналогічна поведінка

Висновок на основі тестів: ядро не пошкоджене

Гіпотеза: некоректна поведінка мережі користувача (або інтерфейсу мережі гіпервізора)

  • Тест 1: перевірити налаштування фаєрволу
  • Результат: фаєрвол пропускає DNS пакети і на хості, і на GCP
  • Тест 2: перехопити трафік та відстежити коректність передачі та повернення DNS запитів
  • Результат: tcpdump підтверджує отримання зворотних пакетів хостом

Висновок на основі тестів: проблема не в мережі

Гіпотеза: не працює сервер метаданих

  • Тест 1: перевірити логії сервера метаданих на аномалії
  • Результат: у логах аномалій немає
  • Тест 2: обійти сервер метаданих через dig @8.8.8.8
  • Результат: дозвіл порушується навіть без використання метаданих сервера

Висновок на основі тестів: проблема не в сервері метаданих

Підсумок: ми протестували всі підсистеми крім налаштувань середовища виконання!

Занурюючись у налаштування середовища виконання ядра

Для налаштування середовища виконання ядра можна скористатися опціями командного рядка (grub) або інтерфейсом sysctl. Я заглянув у /etc/sysctl.conf і подумати тільки, виявив кілька кастомних налаштувань. Відчуваючи ніби я вхопився за щось, я відмілив усі немережеві або не-tcp налаштування, залишившись з налаштувань гори net.core. Потім я звернувся туди, де у VM лежать дозволи хоста і почав застосовувати один за одним, одна за одною налаштування зі зламаною VM, поки не вийшов на злочинця:

net.core.rmem_default = 2147483647

Ось вона, яка ламає DNS конфігурація! Я знайшов знаряддя злочину. Але чому це відбувається? Мені все ще потрібний був мотив.

Настроювання базового розміру буфера DNS пакетів відбувається через net.core.rmem_default. Типове значення варіюється десь у межах 200 КіБ, проте якщо ваш сервер отримує багато DNS пакетів, ви можете збільшити розмір буфера. Якщо в момент надходження нового пакета буфер повний, наприклад тому, що програма недостатньо швидко його обробляє, то ви почнете втрачати пакети. Наш клієнт правильно збільшив розмір буфера, бо побоювався втрат даних, оскільки користувався додатком зі збору метрик через DNS пакети. Значення, яке він виставив, було максимально можливим: 231-1 (якщо виставити 231, ядро ​​поверне INVALID ARGUMENT).

Раптом я зрозумів чому nmap і scapy працювали коректно: вони використовували сирі сокети! Сирі сокети відрізняються від звичайних: вони працюють в обхід iptables, і вони не буферизуються!

Але чому «надто великий буфер» викликає проблеми? Він явно працює не так, як задумано.

До цього моменту я міг відтворити проблему на кількох ядрах та безлічі дистрибутивів. Проблема вже виявлялася на ядрі 3.х і тепер так само виявлялася на ядрі 5.х.

Дійсно, під час запуску

sysctl -w net.core.rmem_default=$((2**31-1))

DNS перестав працювати.

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

Я встановив dropwatch, Інструмент, яким варто було скористатися раніше: він показує куди саме в ядрі потрапляє пакет. Винною виявилася функція udp_queue_rcv_skb. Я скачав вихідні джерела ядра і додав кілька функцій printk щоб відстежувати, куди конкретно потрапляє пакет. Я швидко виявив потрібну умову if, і деякий час просто вирячився на нього, адже саме тоді все нарешті зійшлося в цілісну картину: 231-1, безглузде число, непрацюючий домен ... Справа була в шматку коду в __udp_enqueue_schedule_skb:

if (rmem > (size + sk->sk_rcvbuf))
		goto uncharge_drop;

Зверніть увагу:

  • rmem має тип int
  • size має тип u16 (непідписаний шістнадцятибітний int) та зберігає розмір пакету
  • sk->sk_rcybuf має тип int і зберігає розмір буфера який за визначенням дорівнює значенню net.core.rmem_default

Коли sk_rcvbuf наближається до 231, сумування розміру пакета може призвести до цілісному переповненню. І так як це int, його значення стає негативним, таким чином умова стає істинною коли має бути хибним (більше про це можна дізнатися по за посиланням).

Помилка виправляється тривіально: приведенням до unsigned int. Я застосував виправлення та перезапустив систему, після чого DNS знову запрацював.

смак перемоги

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

Варто визнати, що випадок виявився рідкісним, і на щастя до нас від користувачів рідко надходять такі складні звернення.

Історія про зниклі DNS-пакети від техпідтримки Google Cloud


Джерело: habr.com

Додати коментар або відгук