Генерация трафика в юзерспейсе


    Генерация трафика посредством MoonGen + DPDK + Lua в представлении художника

    Нейтрализация DDoS-атак в реальных условиях требует предварительных тестирования и проверки различных техник. Сетевое оборудование и ПО должно быть протестировано в искусственных условиях близких к реальным — с интенсивными потоками трафика, имитирующего атаки. Без таких экспериментов крайне затруднительно получить достоверную информацию о специфических особенностях и ограничениях, имеющихся у любого сложного инструмента.

    В данном материале мы раскроем некоторые методы генерации трафика, используемые в Qrator Labs.

    ПРЕДУПРЕЖДЕНИЕ

    Мы настойчиво рекомендуем читателю не пытаться использовать упомянутые инструменты для атак на объекты реальной инфраструктуры. Организация DoS-атак преследуется по закону и может вести к суровому наказанию. Qrator Labs проводит все тесты в изолированном лабораторном окружении.

    Современный технический уровень


    Показательной задачей в нашей области является насыщение 10G Ethernet-интерфейса небольшими пакетами, что подразумевает обработку 14.88 Mpps (миллионов пакетов в секунду). Здесь и далее мы рассматриваем сетевые пакеты Ethernet наименьшего размера — 64 байта, — поскольку нашим основным интересом является максимизация количества переданных пакетов в единицу времени. Простой подсчет показывает, что у нас есть всего около 67 наносекунд для обработки одного такого пакета.

    Просто для сравнения — это время близко к тому, что требуется современному процессору для получения кусочка данных из памяти в случае промаха в кэш. Все становится еще сложнее, когда мы начинаем работать с 40G и 100G Ethernet-интерфейсами и пытаемся полностью насытить их вплоть до line rate (максимально возможной заявленной производительности сетевого устройства).

    Так как в обычном случае поток данных проходит через приложение в пользовательском пространстве (userspace), далее через ядро, попадая, наконец, в сетевой контроллер (NIC), первой и наиболее прямолинейной идеей является попытка настроить генерацию пакетов прямо в ядре. Примером подобного решения является ядерный модуль pktgen [2]. Данный способ позволяет заметно улучшить производительность, но недостаточно гибок, так как малейшее изменение исходного кода в ядре ведет к длительному циклу сборки, перезагрузке модулей ядра или даже всей системы и, собственно, тестирования, что снижает общую продуктивность (то есть требует от программиста больше времени и усилий).

    Другим возможным подходом является получение прямого доступа из userspace к буферам памяти сетевого контроллера. Этот путь более сложен, однако сто́ит усилий в целях достижения более высокой производительности. Недостатки включают в себя высокую сложность и низкую гибкость. Примерами такого подхода являются технологии netmap, PF_RING и DPDK [4].

    Еще одним эффективным, хотя и весьма затратным способом добиться высокой производительности является использование не универсального, а специализированного оборудования. Пример: Ixia.

    Также существуют решения основанные на DPDK с использованием скриптов, что повышает гибкость в управлении параметрами генератора, а также позволяет варьировать тип генерируемых пакетов в процессе запуска. Ниже мы опишем собственный опыт с одним из таких инструментов — MoonGen.

    Архитектура MoonGen


    Отличительными особенностями MoonGen являются:

    1. Обработка данных DPDK в userspace, это основная причина прироста производительности;
    2. Стек Lua [5] с простыми скриптами на верхнем уровне и привязками к библиотеке DPDK, написанной на языке C, на нижнем;
    3. Благодаря технологии JIT (just in time) Lua-скрипты работают достаточно быстро, что несколько противоречит общепринятым представлениям об эффективности скриптовых языков.

    MoonGen может восприниматься как Lua-обертка вокруг библиотеки DPDK. По меньшей мере следующие операции DPDK видны на уровне пользовательского интерфейса Lua:

    • Конфигурирование сетевых контроллеров;
    • Аллокация и прямой доступ к пулам и буферам памяти, которые, в целях оптимизации, должны выделяться непрерывными выровненными областями;
    • Прямой доступ к RSS-очередям сетевых контроллеров;
    • API для управления вычислительными потоками, учитывающие неоднородность доступа к памяти (NUMA и CPU affinity) [12].



    Архитектура MoonGen, схема из материала [1].

    MoonGen


    MoonGen — это скриптовый высокоскоростной генератор пакетов, основанный на библиотеке DPDK. Скрипты Lua контролируют полностью весь процесс: созданный пользователем скрипт занимается созданием, модификацией и отправкой пакетов. Благодаря очень быстрому LuaJIT и библиотеке обработки пакетов DPDK, такая архитектура позволяет насытить 10-гигабитный Ethernet-интерфейс 64-байтными пакетами, используя только одно ядро центрального процессора. MoonGen позволяет достичь такой скорости даже в случае, когда Lua-скрипт модифицирует каждый пакет. При этом не используются трюки вроде переиспользования одного и того же буфера сетевого контроллера.

    MoonGen может также принимать пакеты, то есть проверять, какие пакеты были отброшены тестируемой системой. Так как прием пакетов управляется исключительно пользовательским Lua скриптом, он может быть использован и для создания более сложных тестовых скриптов. Например, возможно использование двух экземпляров MoonGen для установки соединения друг с другом. Подобная конфигурация может использоваться, в частности, для тестирования так называемых мидлбоксов (оборудования между точкой отправки и приема трафика), например файрволлов. MoonGen фокусируется на четырех основных направлениях:

    • Высокая производительность и многоядерное масштабирование: более 20 миллионов пакетов в секунду на одном ядре CPU;
    • Гибкость: каждый пакет генерируется в реальном времени на основе созданного пользователем скрипта Lua;
    • Точные отметки времени: на обычном (commodity) железе временна́я разметка производится с миллисекундной точностью;
    • Точный контроль интервалов между отправляемыми пакетами: надежная генерация требуемых паттернов и типов трафика на обычном железе.

    DPDK


    DPDK расшифровывается как Data Plane Development Kit и состоит из библиотек, основными функциями которых является повышение производительности генерации сетевых пакетов на широком разнообразии архитектур центральных процессоров.

    В мире, где компьютерные сети становятся фундаментом человеческой коммуникации, производительность, пропускная способность и задержки становятся все более критичными параметрами работы таких систем как беспроводные сети и кабельная инфраструктура, включая все их индивидуальные составляющие части: маршрутизаторы, балансировщики нагрузки, файрволы; а также сферы применений: передача медиа (стриминг), VoIP и др.

    DPDK — это легковесный и удобный способ построения тестов и скриптов. Передача данных в рамках userspace — это то, что мы наблюдаем не так часто, в основном потому, что большинство приложений коммуницирует с сетевым оборудованием через операционную систему и стек ядра, что является противоположностью модели DPDK.

    Lua


    Основной целью существования Lua является предоставление простых и гибких выразительных средств, расширяемых под конкретные текущие задачи, вместо набора примитивов, применимого лишь в одной парадигме программирования. В результате, базовый язык очень легок — весь интерпретатор занимает лишь 180 кБ в скомпилированном виде и легко адаптируется к широкому спектру возможных реализаций.

    Lua — это динамический язык. Он настолько компактен, что может быть размещен практически на каком угодно устройстве. Lua поддерживает небольшой набор типов: булевы значения, числа (с плавающей точкой двойной точности) и строки. Обычные структуры данных, такие как массивы, множества и списки, могут быть представлены единственной существующей в Lua встроенной структурой данных — таблицей, которая представляет собой гетерогенный ассоциативный массив.

    Lua задействует компиляцию JIT (just in time), поэтому, будучи скриптовым языком, показывает производительность сравнимую с компилируемыми языками, такими как C [10].

    Почему MoonGen


    Являясь компанией, специализирующейся на нейтрализации DDoS-атак, Qrator Labs нуждается в надежном способе создавать, модернизировать и тестировать собственные решения по безопасности. Именно для последнего — тестирования, необходимы различные способы генерации трафика, имитирующие реальные атаки. Тем не менее, не так просто сымитировать опасную, при этом прямолинейную, атаку флудом на 2-3 уровнях модели OSI, в первую очередь из-за трудностей с достижением высокой производительности в генерации пакетов.

    Другими словами, для компании, занимающейся непрерывной доступностью и нейтрализацией DDoS, симуляция различных DoS-атак в изолированном лабораторном окружении — это способ понять, как в реальности поведет себя различное оборудование, входящее в состав аппаратных комплексов компании.

    MoonGen — это хороший способ генерировать близкие к предельным для сетевого контроллера значения трафика на минимуме ядер центрального процессора. Передача данных в рамках userspace значительно поднимает производительность рассматриваемого стека (MoonGen + DPDK), по сравнению со многими другими вариантами генерации высоких значений трафика. Использование чистого DPDK требует значительно бо́льших усилий, поэтому не нужно удивляться нашему стремлению к оптимизации работы. Мы также поддерживаем клон [7] оригинального репозитория MoonGen с целью расширения функциональности и имплементации собственных тестов.

    С целью достижения максимальной гибкости, логика генерации пакетов задается пользователем с помощью скрипта Lua, что является одной из основных особенностей работы MoonGen. В случае относительно простой обработки пакетов это решение работает достаточно быстро для насыщения 10G-интерфейса на одном ядре CPU. Типичный способ модификации входящих пакетов и создания новых — это работа с пакетами одного типа, в которых меняются лишь некоторые из полей.

    В качестве примера может служить тест l3-tcp-syn-ack-flood, описанный ниже. Отметим, что любая модификация пакета может быть произведена в том же самом буфере, где оказался сгенерированный или полученный на предыдущем этапе пакет. Действительно, такого рода преобразования пакетов выполняются очень быстро, так как не задействуют дорогие операции, вроде системных вызовов, доступа к потенциально не закэшированным участкам памяти и тому подобных.

    Тесты на оборудовании Qrator Labs


    Qrator Labs проводит все тесты в лаборатории на различном оборудовании. В данном случае нами были задействованы следующие контроллеры сетевых интерфейсов:

    • Intel 82599ES 10G
    • Mellanox ConnectX-4 40G
    • Mellanox ConnectX-5 100G

    Отметим отдельно, что при работе с сетевыми контроллерами, работающими на стандартах выше 10G, проблема производительности встает все острее. На сегодняшний день не представляется возможным насыщение интерфейса 40G одним ядром, хотя небольшим количеством ядер это уже реально.

    В случае сетевых контроллеров производства Mellanox возможно изменение некоторых параметров и настроек устройства с помощью tuning guide [3], предоставляемого производителем. Это позволяет поднять производительность, а в некоторых особенных случаях — глубже изменить поведение NIC. Другие производители могут иметь похожие документы для собственных высокопроизводительных устройств, предназначенных к профессиональному использованию. Даже если вы не можете найти такой документ в открытом доступе, всегда имеет смысл связаться с производителем напрямую. В нашем случае представители компании Mellanox были очень любезны и, помимо предоставления документации, быстро отвечали на возникающие у нас вопросы, благодаря чему удалось добиться утилизации полосы на 100%, что было для нас очень важно.

    Тест TCP SYN flood


    L3-tcp-syn-ack-flood — это пример имитации атаки типа SYN flood [6]. Это расширенная Qrator Labs версия теста l3-tcp-syn-flood из основного репозитория MoonGen, которая хранится в нашем клоне репозитория.

    Наш тест может запускать три вида процессов:

    1. Генерировать с нуля поток пакетов TCP SYN, варьируя требуемые поля, такие как source IP address, source port number и др.;
    2. Создавать валидный ответ ACK на каждый полученный SYN-пакет согласно протоколу TCP;
    3. Создавать валидный ответ SYN-ACK на каждый полученный ACK-пакет согласно протоколу TCP.

    Для примера, внутренний (соответственно, самый «горячий») цикл кода для создания ACK-ответов выглядит следующим образом:

    local tx = 0
    local rx = rxQ:recv(rxBufs)
    for i = 1, rx do
    	local buf = rxBufs[i]
    	local pkt = buf:getTcpPacket(ipv4)
    	if pkt.ip4:getProtocol() == ip4.PROTO_TCP and
    		pkt.tcp:getSyn() and
    		(pkt.tcp:getAck() or synack)
    	then
    		local seq = pkt.tcp:getSeqNumber()
    		local ack = pkt.tcp:getAckNumber()
    
    		pkt.tcp:unsetSyn()
    		pkt.tcp:setAckNumber(seq+1)
    		pkt.tcp:setSeqNumber(ack)
    		
    		local tmp = pkt.ip4.src:get()
    		pkt.ip4.src:set(pkt.ip4.dst:get())
    		pkt.ip4.dst:set(tmp)
    
    		… -- some more manipulations with packet fields
    
    		tx = tx + 1
    		txBufs[tx] = buf
    	end
    end
    if tx > 0 then
    	txBufs:resize(tx)
    	txBufs:offloadTcpChecksums(ipv4) -- offload checksums to NIC
    	txQ:send(txBufs)
    end

    Общая идея создания ответного пакета заключается в следующем. Для начала, необходимо вынуть пакет из очереди RX, затем проверить, совпадает ли тип пакета с ожидаемым. В случае совпадения — подготовить ответ, модифицируя некоторые поля оригинального пакета. Наконец, поместить созданный пакет в очередь TX, используя тот же буфер. Для повышение производительности, вместо того чтобы по очереди брать и модифицировать пакеты один за одним, мы агрегируем их, извлекая из очереди RX все доступные пакеты, создаем соответствующие ответы и помещаем их все в очередь TX. Несмотря на достаточное большое количество манипуляций над одним пакетом, производительность остается высокой, в первую очередь благодаря тому, что Lua JIT компилирует все эти операции в небольшое количество процессорных инструкций. Множество других тестов, не только TCP SYN/ACK, работают по тому же принципу.

    Таблица ниже демонстрирует результаты теста SYN flood (генерация SYN без попыток ответа) с использованием Mellanox ConnectX-4. Этот NIC обладает двумя портами 40G с теоретическим потолком производительности в 59.52 Mpps на одном порту и 2 * 50 Mpps для двух портов. Конкретная реализация подключения NIC к PCIe несколько ограничивает пропускную способность (давая 2 * 50 вместо ожидаемых 2 * 59.52).
    cores per port 1 port, Mpps 2 ports, Mpps per each port
    1 20 19
    2 38 36
    3 56.5 47
    4 59.5 50

    SYN flood test; NIC: Mellanox Technologies MT27700 Family (ConnectX-4), dual 40G port; CPU: Intel® Xeon® Silver 4114 CPU @ 2.20GHz

    Следующая таблица показывает результаты того же самого теста SYN flood, проведенного на Mellanox ConnectX-5 с одним портом 100G.
    cores Mpps
    1 35
    2 69
    3 104
    4 127
    5 120
    6 131
    7 132
    8 144

    SYN flood test; NIC: Mellanox Technologies MT27800 Family (ConnectX-5), single 100G port; CPU: Intel® Xeon® Silver 4114 CPU @ 2.20GHz

    Отметьте, что во всех случаях мы достигаем более чем 96% от теоретического потолка производительности на небольшом количестве ядер процессора.

    Захват входящего трафика и сохранение в PCAP-файлы


    Другим примером теста является rx-to-pcap, который пытается захватить весь входящий трафик и сохранить в определенное количество PCAP-файлов [8]. Хотя конкретно этот тест и не касается генерации пакетов как таковой, он служит демонстрацией того факта, что самым слабым звеном в организации передачи данных через userspace является файловая система. Даже виртуальная файловая система tmpfs значительно замедляет поток. В данном случае 8 ядер центрального процессора необходимы для утилизации 14.88 Mpps, в то время как лишь одного ядра достаточно для получения (и сброса, либо перенаправления) того же объема трафика.

    Следующая таблица демонстрирует количество трафика (в Mpps), который был получен и сохранен в PCAP-файлы, находящиеся в файловой системе ext2 на SSD (вторая колонка) или на файловой системе tmpfs (третья колонка).
    cores on SSD, Mpps on tmpfs, Mpps
    1 1.48 1.62
    2 4 4.6
    3 6.94 8.1
    4 9.75 11.65
    5 12.1 13.8
    6 13.38 14.47
    7 14.4 14.86
    8 14.88 14.88

    Rx-to-pcap test; NIC: Intel 82599ES 10-Gigabit; CPU: Intel® Xeon® CPU E5-2683 v4 @ 2.10GHz

    Модификация MoonGen: менеджер заданий tman


    Мы бы также хотели представить читателю собственное расширение функционала MoonGen, предоставляющее другой способ запустить группу задач для тестирования. Основная идея здесь заключается в разделении общей конфигурации и специфических для каждой задачи настроек, позволив запускать произвольное количество различных заданий (то есть скриптов Lua) одновременно. В нашем клоне репозитория MoonGen представлена имплементация MoonGen с менеджером заданий [9], здесь мы лишь кратко перечислим его основные функции.

    Новый интерфейс командной строки позволяет запускать одновременно несколько заданий разного типа. Базовый сценарий выглядит следующим образом:

    ./build/tman [tman options...] [-- <task1-file> [task1 options...]] [-- <task2-file> [task2 options...]] [-- ...]

    Кроме того, ./build/tman -h выдает подробную справку.

    Однако существует некоторое ограничение — обычные файлы заданий Lua несовместимы с интерфейсом tman. Файл задания tman должен четко определять следующие объекты:

    • Функцию configure(parser), описывающую параметры задания;
    • Функцию task(taskNum, txInfo, rxInfo, args), описывающую собственно процесс-задание. Здесь txInfo и rxInfo являются массивами соответственно RX и TX очередей; args содержит параметры менеджера заданий и самого задания.
    • Примеры можно посмотреть в examples/tman.

    Использование менеджера заданий дает большую гибкость в запуске гетерогенных тестов.

    Выводы


    Метод, который предлагает MoonGen, оказался хорошо подходящим нашим целям и удовлетворил сотрудников полученными результатами. Мы получили инструмент, обладающий высокой производительностью, при этом сохранив и тестовое окружение, и язык достаточно простыми. Высокая производительность данного сетапа достигается благодаря двум основным особенностям: прямым доступом к буферам контроллера сетевого интерфейса и технике компиляции Just-In-Time в Lua.

    Как правило, достижение теоретического потолка производительности контроллера сетевого интерфейса — вполне посильная задача. Как мы продемонстрировали, одного ядра может быть достаточно для насыщения порта 10G, в то время как с бо́льшим количеством ядер не представляет особой проблемы и полная загрузка порта 100G.

    Мы особенно благодарны команде Mellanox за помощь при работе с их оборудованием и команде MoonGen за их реакцию в исправлении ошибок.

    Материалы


    1. MoonGen: A Scriptable High-Speed Packet Generator — Paul Emmerich et al., Internet Measurement Conference 2015 (IMC'15), 2015
    2. Pktgen
    3. Mellanox tuning guide
    4. Data Plane Development Kit
    5. Lua
    6. SYN flood
    7. Qrator Labs’ clone of MoonGen repository
    8. PCAP file format
    9. Task manager
    10. Lua performance
    11. Network Functions Virtualization Whitepaper
    12. NUMA, non-uniform memory access
    • +37
    • 5,4k
    • 3
    Qrator Labs
    214,00
    DDoS Attacks Mitigation & Continuous Availability
    Поделиться публикацией

    Комментарии 3

      +1
      Касательно rx-to-pcap, не пробовали писать прямо на диск в обход файловой системы? Тогда в теории ограничение будет только по производительности диска.

      Впрочем, 100G записать на один диск всё равно не получится — PCIEx16 версии 4 пропускает до 31.5 Гбит, и их надо 4, а версия 5, которая даёт 63 Гбит — еще не стандартизирована даже.
        +2
        Скорее всего можно получить скорость SATA или какой там будет интерфейс, но мы не проверяли.
        Только про PCIe у вас выкладки неправильные: Gen4 x16 дает 31.5 Гбайт/с (байт, а не бит), иначе бы сама сетевушка 100G не смогла столько передавать. Вышеупомянутый ConnectX-5 работает на предыдущем поколении PCIe — Gen3 x16, это 15.8 Гбайт/с, что (с учетом накладных расходов протокола) немного больше требуемых 100Гбит/с.
          0
          Честно говоря, ошибся — подумал, что раз 100G это биты и у SATA пишут биты, то и у PCIe тоже будут написаны биты. Но да, в таком случае NIC бы не передавал 100G, что абсурдно.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое