Syncookied — OpenSource ddos protection system

Коли в нашій компанії LTD BeGet постало завдання прозорою фільтрації атак на 4 рівні моделі OSI, ми написали своє рішення Syncookied. Цим рішенням ми б хотіли поділитися з Internet співтовариством, так як на поточний момент аналогів йому ми не знайшли (або ми про них не знаємо). Є платні рішення на подобі Arbor, F5, SRX, але коштують вони зовсім інших грошей у них використовуються інші технології захисту.

Чому для розробки ми вибрали мову Rust і фреймворк NetMap, з якими труднощами ми зіткнулися в процесі — буде розказано в цій статті.

» GitHub
» GitHub модуль ядра
» Сторінка проекту



Принцип роботи
Більш детальну інформацію про принципи роботи TCP і методи захисту від DDOS атак рівня L4 можна прочитати на сторінки проекту. Але, в будь-якому випадку, описати схему роботи та її переваги в рамках даної статті необхідно.

Syncookied — є логічним продовженням розвитку технології Syncookie. Технологія Syncookie виноситься з ядра операційної системи на окремий сервер. Для цього ми написали модуль до ядра і демон, який спілкується з фаєрволом, передає секретний ключ і синхронізує мітки часу, а також включає Syncookie на сервері у випадку необхідності. Тобто на захищений сервер встановлюється модуль ядра, який обманює ядро змушуючи його думть, що він відправляв Syncookie, і створює файл /proc/tcp_secrets, і демон, який передає дані з цього файлу на фаєрвол.

Принципова схема роботи наведено нижче:



Пояснення:

  • Роутер — маршрутизатор, обробляє вхідний трафік.
  • Фаєрвол — сервер, на якому запущена система фільтрації.
  • Захищений сервер — сервер, трафік на який необхідно фільтрувати.
У звичайному стані роутер і захищений сервер спілкуються безпосередньо, в разі виявлення атаки на роутері прописується статична прив'язка IP адреси сервера на MAC адресою фаєрволу, після цього весь трафік, що йде на захищений сервер, йде на фаєрвол. Фаєрвол при отриманні SYN пакету відправляє в мережу SYN-ACK-пакет від імені сервера, що захищається з IP адреси сервера і, що більш важливо, з валидной з точки зору сервера, що захищається syncookie (так як у нього є секретний ключ і мітка часу від сервера, що захищається), у разі отримання ACK пакету (з кукой), фаєрвол перевіряє його валідність, потім або змінює в пакеті MAC-адресу на MAC адресу захищеного сервера і відправляє пакет назад у мережу, або видаляє його, якщо кука не валидна. Захищений сервер, отримуючи ACK-пакет, відкриває з'єднання і вихідні пакети надсилає безпосередньо на роутер.

На сервері, так само як і в технології SynProxy, ведеться відстеження сполук (про алгоритм роботи можна прочитати більш докладно на сторінці проекту), що дозволяє фільтрувати також інші невалідні пакети — RST, DATA, SYN+ACK, ACK і так далі.

Основна відмінність від аналогічних систем:

  • Асинхронність фільтрації трафіку — дає можливість встановлювати систему близько прикордонних маршрутизаторів, незалежно від шляху зворотного пакета. Тобто в місцях з максимальним розміром каналу.

  • Syncookied не розриває з'єднання після відключення захисту. Так як у пакетів, на відміну від технології SynProxy, не змінюються SEQ номери послідовностей. Саме відключення полягає у видаленні статичної прив'язки MAC адреси IP-адресою сервера, що захищається на роутері, після чого трафік йде безпосередньо на захищений сервер без обробки його фаєрволом.

  • Відкритість рішення, масштабованість і можливість роботи на досить бюджетному залозі без помітних затримок.

  • Система написана на Rust з використанням assembler для ресурсномістких операцій.
Є можливість вказувати додаткові фільтри в конфігурації хоста, фильры пишуться у pcap форматі.

Така система фільтрації підходить не всім, так як з її переваг випливають і недоліки:

  • Це не рішення для установки на захищений сервер і не рішення для захисту одного сервера
  • Передбачається, що є доступ до патентів серверів, що працюють під операційною системою Linux (для установки модуля ядра)
Реалізація
Спочатку система писалася на С++, після створення робочого прототипу і перевірки життєздатності ідеї прийняли рішення переписати все на Rust. Почасти через спрощення коду, частково через бажання звести до мінімуму ризик вистрілити собі в ногу при роботі з пам'яттю. Так як всі ресурсомісткі операції реалізовувалися на assembler, невелика втрата продуктивності не була критичною. Для роботи з мережевою картою був обраний фреймворк NetMap (на хабре є кілька статей з описом), як один з найбільш простих у роботі та легких у вивченні (як виявилося, не без проблем).

Архітектура програми
Кількість виконуваних потоків прив'язана до кількості переривань, що генеруються мережевою картою для вхідних і вихідних черг, між потоками для вхідного і вихідного трафіку створюється дві черги — для пересилається трафіку і трафіку, на який потрібно відповісти. Потоки для вхідного трафіку забирають пакети з мережевої карти, аналізують їх заголовки і на підставі правил фільтрації роблять одну з трьох дій:

  • видаляють пакет.
  • відправляють пакет в чергу для пересилання.
  • відправляють пакет в чергу для відповіді.
Потоки, які обробляють відправку пакетів в мережу, чекають сигналу від мережевої карти про появу місця в черзі для відправки пакетів, далі опитують чергу для пересилання пакетів і, якщо вона порожня, опитують чергу для пакетів, відповідь на які необхідний, створюючи SYN+ACK відповідь і відправляючи його в мережу.

Мінімальна затримка зроблена для пересилаються валідних пакетів, пакети, на які необхідно відповідати, йдуть зі зниженим пріоритетом.

Була ідея реалізувати окремі процеси для обробки черги пакетів потребують відповіді, але, як показала практика, на сучасних процесорах і адекватних мережевих картах це не потрібно, так як при максимальній теоретичної навантаженні ядра не завантажуються на 100%. Так і додаткові черзі призведуть до уповільнення обробки.

Проблеми, з якими ми зіткнулися в netmap
Бібліотека виявилася не такою ідеальною, як ми про неї думали. Спочатку в прототипі реалізовувалася система фільтрації для локального сервера. Виявилося NetMap для зв'язку з мережевою підсистемою ядра використовується тільки одна входить і одна вихідна чергу, що створювало певні складнощі в плані продуктивності.

Для вхідної черги був реалізований прапор NS_FORWARD для автоматичної пересилання пакетів з NIC в HOST RING, для вихідної цього реалізовано не було. Для TX черги додається це досить просто:

Патч для додавання NS_FORWARD
diff --git a/sys/dev/netmap/netmap.c b/sys/dev/netmap/netmap.c
index c1a0733..2bf6a26 100644
--- a/sys/dev/netmap/netmap.c
+++ b/sys/dev/netmap/netmap.c
@@ -548,6 +548,9 @@ SYSEND;

NMG_LOCK_T netmap_global_lock;

+
+static inline void nm_sync_finalize(struct netmap_kring *kring);
+
/*
* mark the ring as stopped, and run through the locks
* to make sure other users get to see it.
@@ -1158,6 +1161,14 @@ netmap_sw_to_nic(struct netmap_adapter *na)
rdst->head = rdst->cur = nm_next(dst_head, dst_lim);
}
/* if (sent) XXX txsync ? */
+ if (sent) {
+ if (nm_txsync_prologue(kdst, rdst) >= kdst->nkr_num_slots) {
+ netmap_ring_reinit(kdst);
+ } else if (kdst->nm_sync(kdst, NAF_FORCE_RECLAIM) == 0) {
+ nm_sync_finalize(kdst);
+ }
+ printk(KERN_ERR "Synced %d packets to NIC ring %d", sent, i);
+ }

Надалі від роботи з HOST RING ми відмовилися.

Для тестування ми використовували дві карти від Intel — X520 і X710. Карта на базі X520 завелася без проблем, але в них під переривання відведено всього 4 біта — в результаті максимум 16 переривань, незважаючи на те, що драйвера можуть показувати і більше. У X710 під переривання відведено вже 8 біт, є віртуалізація і багато всього цікавого. X710 довелося прошивати, так як Intel не спромоглася додати туди підтримку сторонніх SPF+ через опцію модуля ядра, як це зроблено в X520 картах:

insmod ./ixgbe/ixgbe.ko allow_unsupported_sfp=1

Карту прошивали по інструкції, наведеної нижче:

  1. Завантажити github.com/terpstra/xl710-unlocker.
  2. Підставити значення pci в mytool.c
  3. Скомпилить
  4. Сдампить ./mytool 0 0x8000
  5. Знайти, де таке повторюється 4 рази:

    00006870 + 00 => 000b
    00006870 + 01 => 0022 = external SFP+
    00006870 + 02 => 0083 = int: SFI, 1000BASE-KX, SGMII (???)
    00006870 + 03 => 1871 = ext: 0x70 = 10GBASE-{SFP+,LR,SR} 0x1801=crap?
    00006870 + 04 => 0000
    00006870 + 05 => 0000
    00006870 + 06 => 3303 = SFP+ copper (passive, active), 10GBase-{SR,LR}, SFP
    00006870 + 07 => 000b
    00006870 + 08 => 2b0c
    *** This is the important register ***
    (0xb=bits 3+1+0 = enable qualification (3), pause TX+RX capable (1+0),
    0xc = 3+2 = 10GbE + 1GbE)
    00006870 + 09 => 0a00
    00006870 + 0a => 0a1e = default values LESM
    00006870 + 0b => 0003

  6. Впихнути в mypoke.c оффсет і значення
  7. Скомпилить, запустити (воно довго висить, це мабуть ок)
  8. Запустити mytool і подивитися, що все ок змінилося
  9. Скачать
  10. Запустити nvmupdate64e -u звідти (теж довго висить)
  11. Тут або лінк з'явився, яких ви вбили карту =)
Більш нова (ніж у ядрі) версія драйвера з e1000.sf.net з накладеним патчем від netmap тут.

Також залишу тут набір магічних милиць Наші тести показали, що при таких параметрах досягається оптимальна продуктивність:

ядру передати iommu=off
netmap.ko no_timestamp=1
ixgbe.ko InterruptThrottleRate=9560,9560 RSS=12,12 DCA=2,2 allow_unsupported_sfp=1,1 VMDQ=0,0 AtrSampleRate=0,0 FdirPballoc=0,0 MDD=0,0

Проблеми, які ми побороли в Rust
Rust, як мова програмування, визволив нас від великої кількості ускладнень і значно прискорив розробку (якщо знайдуться бажаючі переписати на С++ — це не складе праці), але додав ряд специфічних проблем з продуктивністю, з якими ми успішно боролися протягом місяця. За час розробки ми надіслали 15 пул реквестов, з яких 12 у нас прийняли.

У першій реалізації ми домоглися продуктивності 5М пакетів на 16 ядрах, приблизно в цей же час співробітник Google Eric Dumazet написав розсилку ядра про те, що все полагодив і йому вдалося змусити ванільне ядро обробляти 6М пакетів. Як говориться:


Основні проблеми з продуктивністю, з якими ми зіткнулися:

Netmap generic driver

Спочатку ми використовували generic driver, але він був не найкращим рішенням в плані продуктивності. При використанні generic driver пакет все одно потрапляє в мережевий стек ядра, що досить сумно, так як картина ksoftirqd з червоним кольором у htop триває:



Рішення — використовувати рідні драйвера. Це спеціальні драйвера з доданими хуками для netmap.

Більш нова (ніж у ядрі) версія драйвера з e1000.sf.net з накладеним патчем від netmap:
github.com/polachok/i40e-netmap/tree/master

Використання Host Ring

В нативних драйвери ми зіткнулися з новими проблемами — вони не працювали. Довелося розбиратися в исходниках, читати документацію, і виявилося, що драйвера працюють, не працює Host Ring. У Host Ring є дві проблеми — вона не працює з нативними драйверами і у неї всього одна черга, тобто якщо навіть у мережевий буде 16 черг, Host Ring буде мати одну чергу. Це створює додаткові проблеми синхронізації, блокувань. Ми знайшли правильне рішення в даній ситуації — не використовувати Host Ring =), а відправляти пакети в мережу. В якості альтернативи можна було використовувати tap пристрою, вони підтримують multiqueue, але цю можливість ми поки не реалізували.

Locks

Після відмови від Host Ring та відмови від встановлення на кожен сервер (як ми хотіли спочатку) з'ясувалося, що нам треба мати підтримку декількох захищаються хостів в одному демона, швидку перезавантаження конфига без втрати станів і інші приємні дрібниці. Спробували зробити просто — взяти глобальну структуру загорнути її в mutex — продуктивність впала до 3М пакетів. Після з'ясування виявилося, що std::sync::Mutex із стандартної бібліотеки Rust — це просто обгортки над pthread мютексами і вони працюють досить повільно, так як на кожну блокування доводиться ходити в ядро, перемикати контекст. Знайшли в інтернеті посада, де люди з webkit зробили більш продуктивні блокування, в цей же час, на основі цього посту була зроблена бібліотека parking_lot. У parking_lot блокування організовані дещо по іншому — вони адаптивні, спочатку вони намагаються поспиниться (Spinlock) кілька разів і лише потім йдуть в ядро, вони менше в розмірі, тому що список потоків, які на ньому заблоковані, зберігається окремо. Стало трохи краще, але не сильно.

Вирішили використовувати Thread Local Storage. У кожного потоку може бути власне сховище, в яке ми можемо ходити без блокувань. Ми створили глобальну конфігурацію яка раз в 10 секунд копіюється її в Thread Local Storage. Виходить практично повна відсутність блокувань, вони звичайно є, але розносяться по часу і фактично не помітні.

Channels

З'ясувалося ще одна проблема: у Rust є std::sync::mpsc (канали) — вони приблизно такі ж, як канали в Go. Ми їх використовуємо для того, щоб передавати пакетики з потоків, які приймають, потоки, які відправляють. Фактично, канал — це масив за mutex. Слово mutex в контексті нашої розповіді — сумне слово. Ми взяли канали і перенесли їх на parking_lot: github.com/polachok/mpsc.

Стало трохи краще. mpsc — це multiple producer single consumer, в нашому випадку multiple producer не потрібен, так як з одного RX пакет йде в один TX.

Знайшли бібліотеку з потрібним нам функціоналом bounded-spsc-queue — стало набагато краще, блокування пропали — використовувалися тільки атомарні операції, але з'ясувалося, що відбувається копіювання — довелося трохи допилити бібліотеки.

Побудова пакету щоразу

Наступна проблема, з якою ми зіткнулися: у разі відповіді пакет кожен раз збирається з нуля. Це відображалося красненьким в perf, і довелося це оптимізувати одноразової складанням шаблону пакета, після його копіювання замінюємо в ньому необхідні поля. Виграш продуктивності склав близько 30%.
З більш або менш цікавих pull реквестов можна відзначити:

libpnet
github.com/libpnet/libpnet/pull/178 — підтримка протоколу TCP
github.com/libpnet/libpnet/pull/181 — зміна пакета без аллокаций
github.com/libpnet/libpnet/pull/183 — читання пакета без аллокаций
github.com/libpnet/libpnet/pull/187 — підтримка ARP
rust-lang
github.com/rust-lang/rust/pull/33891 — прискорення порівняння ip-адрес
concurrent-hash-map
github.com/AlisdairO/concurrent-hash-map/pull/4 — додає підтримку кастомних алгоритмів хешування
bpfjit для Rust
github.com/polachok/bpfjit — биндинги JIT-компілятора pcap-фільтрів для Rust
Повний список наших pull реквестовgithub.com/gobwas/influent.rs/pull/9
github.com/gobwas/influent.rs/pull/8
github.com/terminalcloud/rust-scheduler/pull/4
github.com/rust-lang/rust/pull/33891
github.com/libpnet/libpnet/pull/187
github.com/libpnet/libpnet/pull/183
github.com/libpnet/libpnet/pull/182
github.com/libpnet/libpnet/pull/181
github.com/libpnet/libpnet/pull/178
github.com/libpnet/netmap_sys/pull/10
github.com/libpnet/netmap_sys/pull/4
github.com/libpnet/netmap_sys/pull/3
github.com/AlisdairO/concurrent-hash-map/pull/4
github.com/ebfull/pcap/pull/56
github.com/polachok/xl710-unlocker/pull/1
Продуктивність
У відсутність трафіку syncookied для зменшення затримок постійно опитує мережеву карту, що створює невелике навантаження. Паразитна навантаження зменшується із збільшенням кількості вхідних пакетів:



Навантаження, створювана фільтрацією трафіку — 12.755 pps (теоретичний межа при розмірі пакета 74 байта + 4 байта заголовок ethernet)



При фільтрації UDP або застосуванні правил по портам/протоколами навантаження буде відрізнити від навантаження в відсутність трафіку.

Фактично 12 ядер процесора Intel Xeon E5-2680v3 можуть обробляти 10 гігабіт трафіку syn/ack/data флуду. Один фізичний сервер, здатний обробляти більше 40 гігабіт трафіку.

TODO
На поточний момент ми майже закінчили впровадження SynCookied на всій інфраструктурі нашого хостингу, відбили кілька не дуже сильних атак і по можливості покращуємо продукт. У планах додати захист Out of Seq (з цим, я сподіваюся, нам допоможуть колеги), майже реалізували повноцінний SynProxy, додаємо красиві метрики в Influx, думаємо додати режим Auto в реалізацію захисту Syncooked (аналогічно режиму net.ipv4.tcp_syncookies=1, кука надсилається, коли приходить занадто багато Syn пакетів).

Висновок
Ми дуже сподіваємося, що наша робота принесе користь інтернет спільноті.

Якби компанії по захисту від DDOS виклали свої напрацювання в OpenSource, і кожен провайдер/адміністратор/клієнт міг їх використовувати, проблема експлуатації недосконалості мережі Internet зважилася б набагато швидше.

Ми тестували наш систему на синтетичних тестах і слабких DDOS атаки (Як на зло ні однієї нормальної атаки вже 4 місяці так і не прилітало). Для наших завдань система вийшла досить зручною. Хочеться також зауважити, що проект на Rust був реалізований фактично однією людиною, дуже талановитим програмістом Олександром Поляковим за короткий термін, після реалізації прототипу на С++ ідеї та перевірки на працездатність до появи робочої версії пройшло 5 тижнів.

» Вихідний код
» Вихідний код ядра
» Сторінка проекту

Автор ідеї та реалізація прототипу: Маникин redfenix Олексій
Реалізація варіанту на Rust: Поляків polachok Олександр
Створення інфраструктури і тестування: Лосєв mlosev Максим
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.