Добрый день! Как вы поняли из названия, вас ждет очередная статья про NetFlow, но на этот раз с необычной стороны — со стороны реализации NetFlow сенсора на FPGA. Да, на хабре много статей по теме NetFlow: начиная с подробного разбора и HOWTO по настройке, до прикладного применения в отлове вирусной атаки и учете трафика. Но это статья не о том, как NetFlow пользоваться, она о том, как его реализовать. Задача создания NetFlow сенсора интересна в том плане, что вам одновременно необходима гибкость, чтобы поддерживать изменяемые в реальном времени шаблоны NetFlow и в то-же время очень высокая производительность, чтобы эффективно обрабатывать трафик и работать с памятью. Там, где я работаю (я FPGA программист в НТЦ Метротек), мы используем платформы, которые позволяют делать выбор между программными и аппаратными реализациями. Но в большей части наших задач, программная часть занимается в основном управлением, а основную работу принимает на себя FPGA. Именно поэтому создание NetFlow сенсора, как чего-то более интересного в плане Software Hardware Co-design, показалось нам подходящим поводом поделиться нашей работой с Вами, написав эту статью. Чтобы не терять нить повествования и определиться с терминами, расскажу про NetFlow, пусть и очень кратко, потому что эта информация есть везде. NetFlow это протокол, который придумали Cisco Systems. Зачем? Чтобы можно было удаленно следить за трафиком в сети. То есть в локальной сети есть некоторый L3-свитч с функцией NetFlow сенсора и где-то в другом месте коллектор, который знает все о том, что в сети происходит. При этом не должно создаваться лишней нагрузки на сеть данными, которые мы пересылаем в коллектор, то есть простое зеркалирование трафика на анализатор не подходит. Элементы в NetFlow: Сенсор (он же Exporter) — устройство, которое собирает информацию о потоках в сети.
Обычно это L3-коммутатор или маршрутизатор, который достаточно редко (обычно раз в секунду) отправляет информацию о потоках в коллектор. Что еще нужно знать про NetFlow: Export пакеты — это те пакеты, которые отправляет сенсор на коллектор.
В них вся суть NetFlow. Вы, наверное, слышали про то, что NetFlow бывает разных версий — так вот, формат этих export пакетов — главное различие между версиями. Версии NetFlow: v1, v5, v8, v9. v9 — самая распространённая, остальные или дают ограниченную функциональность (v1, v5), или излишне сложные (v8).
В версии v9 сенсором определяется, какую информацию он может отдать, а коллектор под это подстраивается. Сенсор вместе с данными отправляет шаблон, по которому понятно, как с этими данными работать. Шаблоны очень гибкие. Подробнее можно почитать рекомендацию по NetFlow v9 в RFC 3954.
Есть еще IPFIX — на данный момент функционально тот же NetFlow v9.
Только NetFlow курируется Cisco, а IPFIX стандартизован RFC. Из этих полей делаем вывод:  И не забываем — данное определение потока не задано жестко стандартом. По мере наших нужд можно изменять понятие потока, например сагрегировав потоки по направлениям. Теперь, когда про NetFlow все известно, сформулируем ТЗ: Нужен NetFlow сенсор, такой, чтобы: Целевые платформы обе гетерогенные: CPU + FPGA. Зонды основаны на Altera SoC (Cyclone V) ARM процессор + небольшая FPGA. Все это на одном кристалле. В B100 это мощный процессор (Intel Core I7) + большая FPGA (Stratix V). Между ними PCIe.  Поэтому свободы в плане Hardware/Software Co-design у нас много. Но начнем с объяснения, почему очевидные варианты реализации не подходят. Мы не используем FPGA совсем. Используем программное решение для NetFlow. Получаем очень большую гибкость, но что с производительностью? Таким образом можно сделать вывод — да, такая реализация возможна, но: Но кое-что полезное из рассмотрения программной реализации мы все-таки вынесли — проекты в которых можно подсмотреть нюансы реализации NetFlow сенсора: Теперь перейдем к другой крайности — все на FPGA: Но встает очень важный вопрос насчет гибкости создания NetFlow пакетов. Вариант 1 — мы отказываемся от возможности изменять шаблоны NetFlow v9. Мы пишем генератор на FPGA, который может отправлять данные только по одному шаблону.
Если мы захотим сменить шаблон — придется переписывать этот FPGA модуль. Такой подход нас не устраивает, потому что разные коллекторы могут по-разному работать с NetFlow и иногда не совсем хорошо воспринимают различные поля: например некоторые поля NetFlow v9 пакета имеют размер, который может быть переопределен шаблоном, но не все коллекторы к этому готовы, они ждут, что это поле будет константного размера. Вариант 2 — мы реализуем динамическое изменение шаблонов в FPGA. Это возможно, но такой модуль будет занимать очень много ресурсов. Помимо этого его разработка займет очень много времени как на сам модуль, так и на его отладку. И еще один минус такой реализации: в стандарте говорится, что для транспорта NetFlow export пакетов может быть использован не только UDP, но SCTP. А это значит, что если мы захотим поддержку такой функциональности — мы должны будем реализовать половину сетевого стека на FPGA. Теперь, когда мы убедились, что только FPGA или только програмные реализации для данной задачи не подходят, перейдем к совместному решению. Чтобы было удобнее искать решение, сделаем следующее: На этой схеме представлены основные функциональные блоки NetFlow сенсора: Что нам дало такое разбиение: Теперь, имея в виду эту схемку, перейдем к вариантам: Пока мы точно знаем следующее: Вся часть аккумуляции от приема пакета до записи в память должна происходить на FPGA.  Предположим, на этом вся работа FPGA заканчивается: память, где хранятся потоки — общая. К ней имеет доступ soft и он сам производит проверку потоков в памяти и их вычитывание. В этом случае у нас остается проблема большой нагрузки на CPU при большом числе потоков, но и вдобавок к этому, появляется новая проблема — коллизии доступа к памяти: FPGA может захотеть обновить информацию о потоке, который soft собрался вычитывать, в этом случае будет очень просто потерять часть сведений о потоке. У данной проблемы есть решения, но они потребуют дополнительного усложнения архитектуры. Тогда идем чуть дальше — память остается общей, но проверкой потоков занимается FPGA (регулярно вычитывает и проверяет таймеры потоков). Как только поток необходимо экспортировать — FPGA сообщает в soft указатель на этот поток. Нагрузку с CPU мы сняли, но проблема коллизий не решилась. Идем еще дальше — когда FPGA вычитало поток из памяти и поняло, что это поток на экспорт — оно само удаляет поток из памяти и отправляет его в soft (используя любой доступный интерфейс).
В таком случае память больше не должна быть общей, коллизии мы можем решать внутри FPGA на этапе разрешения доступа к памяти. А формированием export пакетов NetFlow занимается программная часть, которая может быть изменена на работу с любым протоколом. Последний вариант нас нас устроил. Можно переходить к созданию proof of concept на базе SoC платформы. Перед реализацией предъявим к ней некоторые требования: Все это привело нас к следующей реализации: Avalon Streaming Interface — интерфейс для передачи потоков данных внутри FPGA. Используется в IP-core от Altera. Может быть использован в варианте непрерывного потока данных или пакетной передачи данных (используются сигналы начала/конца пакета). Подробнее можно почитать тут: Avalon Interface Specifications.pdf NetDMA — тоже наша IP-core — DMA контроллер, который принимает Avalon-ST пакеты и пишет их в память по дескрипторам. Для того, чтобы снабжать этот DMA дескрипторами, у нас уже есть драйвер. Сейчас этот DMA и драйвер используются в нашем сетевом стеке. Simple NetFlow Accumulator — та самая IP-core NetFlow сенсора. Она занимается детектированием потока. Чтением и обновлением информации в памяти по каждому принятому пакету, постоянной проверкой памяти на наличие потоков на экспорт и арбитражем запросов к памяти при добавлении и проверки.  Для хранения данных используется самая простая структура — хеш-таблица без механизма решения коллизий.  Результат вычисления хэш-функции потока мы рассматриваем как сдвиг в памяти, по которому надо расположить соответственные сведения. При добавлении нового потока мы считаем от него хеш, вычитываем из памяти по нужному индексу и проверяем, что это действительно он. Если мы ошиблись (случилась коллизия хеш-функции), то заставляем ядро проверки NetFlow вычитать этот поток из памяти. Да, его таймеры еще не истекли и он еще должен бы храниться в памяти, но таков наш самый простой метод борьбы с коллизиями. Минусы такого подхода: Первая проблема решаемая: чем ниже вероятность коллизий, тем меньше возможный всплеск нагрузки.  Вторая проблема может быть решена в программной части, если там будет реализован свой небольшой буфер для хранения таких преждевременных отправок. В качестве хеш-функции в данный момент используется CRC32 от полинома 0x04C11DB7 (это тот, который используется в Ethernet). Конечно у нас была идея подобрать более интересную хеш-функцию, которая давала бы меньше коллизий. Но моделирование показало, что с CRC32 все в порядке. Для моделирования мы сделали простейшую хеш таблицу на python, которая содержит  позиций (такой размер показался нам наиболее подходящим для первой реализации, потому на нем и тестируем хеш-функции). То есть от полученного значения хеш-функции используется только 20 бит, чтобы определить позицию в таблице. Мы также написали скрипт-генератор, который создает  уникальных слова по 17 байт (именно столько получается данных из полей, уникальных для потока). Эти данные мы добавляем в нашу таблицу, с разными хеш-функциями: Полиномы CRC32 0xEDB88321 и 0x82608EDB — это преобразования полинома 0x04C11DB7. В одном из прошлых проектов мы использовали такие CRC функции для быстрой генерации большого числа разных хешей. Решили их проверить заодно и здесь. LookUp3 была выбрана потому, что это функция специально создана для хеш-таблиц и содержит в себе она вполне терпимые для FPGA опрерации — сложение и сдвиг. MurMurHash3 была выбрана не только из-за классного названия, но и как пример мультипликативной функции, которую будет достаточно дорого реализовывать в FPGA, но которая должна, в теории, дать лучшее заполнение таблицы. После добавления каждого нового слова в каждую из таблиц мы сохраняли уровень заполненности таблицы, чтобы построить такой график: Как видно из графика — на наших данных никаких значимых различия нет. Так что CRC32 вполне подходит нам, пока мы не решим изменить размер данных, от которых считаем хеш, или пока не захотим защититься от умышленной атаки на заполенение нашей таблицы. Для тестовой платформы была выбран одина из платформ с SoC'ом на борту и двумя сетевыми интерфейсами, смотрящими в FPGA. Пакеты, которые попадут в NetFlow, берутся из транзита — когда пакет отправляется с одного порта на другой через FPGA. Включение и выключение траниза уже реализовано на этой платформе. Таким образом, можно создавать нагрузку до 2 Гб/c и при этом контролировать число отправленных на NetFlow пакетов. На выбранной платформе у FPGA и CPU общая память. Чтобы отделить ту память, с которой будет работать только FPGA (для хранения пакетов), ядру Linux при загрузке ограничивается видимая область памяти, так, чтобы нам хватило оставшейся для хранения 1 миллиона потоков ~ 150 МБ. Для создания самих export пакетов написали свою простую python утилиту, которая с помощью библиотеки scapy слушает сетевой интерфейс, по которому приходят данные о потоках. С помощью этой же библиотеки она формирует NetFlow Export пакеты и отправляет их. Данная реализация NetFlow, в теории, "упирается" только в пропускную способность памяти (в одной из статей мой коллега Des333 производил практический расчет этой пропускной способности на SoC платформе и получил цифры в 20 Гб/c, подробнее можно почитать в его статье тут) Хватит ли нам 20 Гб/c? Добавление каждого пакета для нас — это чтение 45 байт из памяти и запись 76. В худшем случае Ethernet трафик на 1G интерфейсе может создавать нагрузку в 1488095 пакетов в секунду (пакеты по 64 байта на line rate). Таким образом, мы создадим нагрузку на память в 1.44 Гб/c. Остальную пропускную способность можем отдать на проверку потоков. Но в текущей реализации производительность намного меньше: на практике мы не справляемся с худшим случаем line rate (пакеты по 64 байта) и часть пакетов не попадает в статистику. Оценить проблему в цифрах можно в симуляции, где придельная, при текущих настройках памяти, нагрузка без потерь ~ 1420000 пакетов в секунду. Это соответствует line rate в случае пакетов размером 69 байт. Связано это с тем, что аккумулятор работает последовательно — он сначала полностью обрабатывает каждый пакет, а только потом берется за следующий. Помимо этого, задержка DDR3-памяти получилась достаточно большой (интерконнект, автоматически сгенерированный средой разработки + использование IP-core Stream SDRAM Ctrl дало ~15 тактов задержки
между запросом на чтение и получением данных на частоте 62.5 МГц). Решением данной проблемы является использование конвейера при добавлении данных в память. То есть мы запрашиваем данные на чтение, и, пока ждем ответа, запрашиваем чтение для следующего потока (и так до 15 раз). Также можно увеличить частоту, на которой работает Simple NetFlow IP-core и весь доступ к памяти. Это только PoC и в связи с этим имеется ряд ограничений, таких как: Мы не следим за концом потока по IP флагам (только по таймерам), хотя NetFlow подразумевает, что это наша задача; На этом, в общем-то и все. Спасибо, что дочитали до конца. Буду рад ответить на возникшие вопросы в комментариях. P.S. Кстати, мои коллеги из системной группы успели преобразовать те наброски из python утилиты для тестирования в приличный драйвер для Linux и демон на Rust'е, который занимается формированием пакетов для NetFlow.  Возможно когда-нибдуь они об этом что-нибудь напишут. Но пока они отдыхают после статьи про другую нашу совместную работу — ускорение AES шифрования. Если интересно, можно почитать тут. Архитектор программного обеспечения