↓ До тексту
  1. Статті/

dnsdist проти флуду: rate-limit і динамічні блоки

cat /etc/passwd
Автор
cat /etc/passwd
Технічний блог про Linux, DevOps та Open Source технології.
Зміст
Відмовостійкий DNS - частина серії статей.
Частина 4: ця стаття (ти тут)

Продовжуємо серію про відмовостійкий DNS. У частині 3 ми поставили dnsdist спереду - з балансуванням і failover. Але є ще одна загроза, з якої, власне, усе й починалось: резолвери (чи зловмисники) заливають сервер запитами, pdns не встигає, і страждають усі. Авторитативний pdns сам погано вміє обмежувати - а от dnsdist для цього й створений.

Додаємо два шари захисту: ліміт запитів на один IP (з трюком, що форсує TCP) і динамічні блоки, які самі банять набридливий IP на час.

Що потрібно зробити?
#

Налаштувати в dnsdist захист від флуду, а тоді - перевірити його наживо: з сусідньої машини залити сервер потоком запитів (dnsperf) і побачити, що потік відсікається, абʼюзер потрапляє в тимчасовий бан, а легітимний клієнт з іншого IP працює як ні в чому не бувало. Двопанельний запис: ліворуч ns1 (наш DNS, якого захищаємо), праворуч ns2 (грає резолвер, що заливає).

З чим працюю?
#

Дві ноди з попередніх частин. Тепер ns1 - це «захисник» (dnsdist + pdns), а ns2 зіграє роль резолвера, що заливає ns1 запитами. Генеруватимемо навантаження через dnsperf:

1
2
3
# на ns2 (клієнт/навантаження)
apt install -y dnsperf
printf 'www.example.com A\napi.example.com A\nexample.com SOA\n' > queries.txt

Далі те саме, що в записі, тільки вже можна копіювати.

Спершу - без захисту
#

Заливаємо ns1 потоком 800 запитів/с протягом 8 секунд:

1
2
# на ns2
dnsperf -s 10.10.60.103 -d queries.txt -c 5 -Q 800 -l 8
1
2
3
Queries sent:         6400
Queries completed:    6400 (100.00%)
Queries per second:   799.99

Усі 6400 дійшли. dnsdist (з кешем пакетів із частини 3) це тягне, але нічого не фільтрує - а в реального авторитативного сервера за таким флудом може стояти значно важчий бекенд.

Захист: ліміт на IP + динамічні блоки
#

Додаємо в /etc/dnsdist/dnsdist.conf два правила:

1
2
3
4
5
6
7
8
9
-- понад 100 qps з одного IP -> TC=1 (клієнт перейде на TCP, спуфлений UDP відсіється)
addAction(MaxQPSIPRule(100), TCAction())

-- динамічні блоки: IP, що суне понад 20 qps за 5с, банимо на 20с
local dbr = dynBlockRulesGroup()
dbr:setQueryRate(20, 5, "flood", 20, DNSAction.Drop)
function maintenance()
  dbr:apply()
end

Два різні підходи:

  • MaxQPSIPRule(100) + TCAction() - миттєвий запобіжник. Щойно один IP переступає 100 qps, dnsdist відповідає йому з прапором TC=1 («відповідь не вмістилась, спитай по TCP»). Чесний резолвер слухняно перепитає по TCP - це дешево. А ось спуфлений UDP-флуд (із підробленою адресою джерела) TCP-рукостискання не подужає й відсіється сам. Класичний трюк проти amplification.
  • dynBlockRulesGroup + setQueryRate - розумніший шар. dnsdist веде облік запитів на кожен IP і в maintenance() (викликається раз на секунду) тимчасово банить тих, хто перевищив поріг. Тут: понад 20 qps за 5-секундне вікно - бан на 20 секунд з Drop (мовчазне відкидання). За 20 секунд бан спадає сам.

Пороги (100, 20/5/20) - для демо занижені, щоб флуд спрацював одразу. Під свій трафік підбирайте вищі: подивіться звичайний qps на один IP і беріть із запасом.

1
systemctl restart dnsdist

Той самий флуд - із захистом
#

1
2
# на ns2 - та сама команда
dnsperf -s 10.10.60.103 -d queries.txt -c 5 -Q 800 -l 8
1
2
3
Queries sent:         1086
Queries completed:    886 (81.58%)
Queries per second:   110.74

Картина інша: dnsperf зумів видавити лише ~110 qps замість 800, а всього «долетіло» 886 відповідей замість 6400. Потік до pdns обрізано в рази - бо щойно ns2 переступив поріг, його запити почали відкидатись, і сам клієнт від браку відповідей сповільнився.

Кого забанило
#

Дивимось активні блоки з консолі:

1
2
# на ns1
dnsdist -c -e 'showDynBlocks()'
1
2
What               Seconds   Blocks Warning    Action    Reason
10.10.60.104/32          6      200 false      Drop      flood

IP ns2 - у бані (Drop), ще на 6 секунд. А тепер головне - легітимний клієнт з іншого IP:

1
2
3
# на ns1 (запит з localhost - інше джерело)
dig +short @127.0.0.1 www.example.com
# 203.0.113.20

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

Граблі
#

  • dnsperf - closed-loop. Його клієнти чекають на відповідь, перш ніж слати наступний запит. Тому коли dnsdist починає відкидати запити, падає й сама швидкість dnsperf (звідси «1086 надіслано» замість 6400) - це нормально й добре показує ефект.
  • maintenance() - це callback dnsdist, який викликається раз на секунду. Саме в ньому треба викликати dbr:apply(), інакше динамічні блоки не застосовуються.
  • Drop vs Truncate. DNSAction.Drop мовчки відкидає (абʼюзер не отримує нічого). Можна натомість DNSAction.Truncate - тоді навіть забаненим шлемо TC=1 (хай ломляться по TCP, якщо це чесний клієнт за NAT із сусідом-флудером). Вибір залежить від того, кого ви очікуєте по той бік.
  • Пороги не з голови. Занизькі - заб’єте легітимні сплески (наприклад, великий резолвер за CGNAT). Зависокі - флуд пролізе. Міряйте свій трафік.

Що маємо на цей момент
#

Відмовостійкий authoritative DNS уже складається з чотирьох кроків:

  1. PowerDNS + MariaDB + API - свій сервер, зони в базі, керування через REST.
  2. Реплікація MariaDB - другий сервер на репліці, дані синхронні по приватній мережі.
  3. dnsdist: балансування і failover - вхід витримує падіння pdns чи цілої ноди.
  4. dnsdist проти флуду (ця частина) - вхід витримує й UDP-перевантаження.

Дані надійні (реплікація), вхід відмовостійкий (dnsdist + два NS) і захищений від флуду запитів. Той самий підхід - «логіку синхронізації віддаємо базі, а вхід - спеціалізованому балансувальнику» - масштабується і на три, і на пʼять серверів.

Хардинг на цьому не закінчується. Тут ми відсікали UDP-флуд (багато запитів), а є ще атаки по TCP - коли вичерпують не запити, а зʼєднання (slowloris, лавина конектів): для них у dnsdist свої важелі - ліміти одночасних зʼєднань на клієнта, запитів на конект і його тривалості. Плюс зону можна підписати DNSSEC (бекенд у нас уже з gmysql-dnssec=yes). Теми для наступних частин.

Відмовостійкий DNS - частина серії статей.
Частина 4: ця стаття (ти тут)

Related

dnsdist: балансування і failover для PowerDNS

·6 хвилин
dnsdist перед кожним pdns: балансування на локальний бекенд, резервний сусід по приватній мережі, health-check і автоматичне виведення хворої ноди. Вимикаємо pdns наживо - запити далі резолвляться через сусіда. Плюс кеш пакетів. Двопанельний запис. Третя частина серії про відмовостійкий authoritative DNS.

PowerDNS: реплікація MariaDB між двома DNS-серверами

·6 хвилин
Другий DNS-сервер на репліці MariaDB: master-slave з GTID по окремій приватній мережі, pdns на репліці read-only. Зміна через API на майстрі миттєво на репліці. Живий двопанельний запис. Друга частина серії про відмовостійкий authoritative DNS.

PowerDNS: свій авторитативний DNS на MariaDB з REST API

·6 хвилин
Свій DNS-сервер на PowerDNS 5.1 з бекендом MariaDB і REST API: офіційне репо (не застарілий 4.9), схема gmysql, перша зона й керування записами через API. Живий запис з усіма конфігами у vim. Перша частина серії про відмовостійкий authoritative DNS.