Смекни!
smekni.com

Диссертации утверждена распоряжением по нгу № от 200 г (стр. 4 из 9)

5 Работа с базой данных.

Хотя в начале разработки база данных Berkeley DB была выбрана как инструмент управления статистиками, было решено реализовать отдельный модуль, предоставляющий интерфейс управления статистиками и инкапсулирующий работу с базой данных с тем, чтобы при необходимости можно было легко перейти на другую СУБД. К тому же, C++ API, включенный в стандартную поставку Berkeley DB, является лишь очень тонкой надстройкой над API на С. Игнорируются многие возможности, доступные в С++, такие как исключения, умные указатели, шаблоны, перегрузка операторов и т.д. Вся логика работы, присущая API на С (в том числе необходимость обработки «голых» указателей), осталась неизменной. Это чревато серьезными ошибками, особенно когда остальной код активно использует исключения. Поэтому интерфейс управления статистиками необходим также для сглаживания недостатков Berkeley DB C++ API. Речь не идет о реализации полноценного интерфейса для Berkeley DB, такая задача не ставилась. Были выбраны лишь те возможности, которые необходимы для работы со статистиками.

Вся работа со статистиками происходит через шаблонный класс DBController. Класс принимает четыре шаблонных параметра: тип ключа, тип данных, стратегия управления типом ключа и стратегия управления типом данных. Стратегия предоставляет функции сериализации и десериализации данных определенного типа. Была написана стратегия для встроенных типов (не указателей) и STL-строк под названием StdStrategy. Она используется по умолчанию для типов ключа и данных шаблона DBController.

Методы DBController позволяют записывать и считывать данные по ключу и проверять наличие ключа в базе данных. От реализации доступа к записям через итераторы решено было отказаться. Однако DBController имеет шаблонный метод template<T> foreach(T& op), получающий в качестве параметра функтор (указатель на функцию или экземпляр класса с перегруженным оператором вызова — operator()), который должен принимать два аргумента (ключ и значение) и будет вызван для каждой записи в базе данных.

Реализация шаблона DBController основана на идиоме скрытой реализации pimpl (pointer to implementation, [16]). Объявлен класс DBContRawImpl. Он ответственен за всю работу непосредственно с базой данных, его методы используются шаблоном DBController. Его определение и реализация находятся в отдельном файле. Таким образом, DBContRawImpl может быть изменен без перекомпиляции кода, использующего шаблон DBController. Это позволяет при необходимости изменить используемую СУБД, изменив лишь файл реализации DBContRawImpl или вообще подменив его другим файлом. Стратегия StdStrategy и интерфейс шаблона DBController приведены в приложении А.

6 Сбор статистики.

За сбор статистики отвечает компонент под названием коллектор. Он занимается перехватом пакетов и записью их в базу данных. Захват производится средствами популярной библиотеки pcap (Packet Capture [11], ее использует подавляющее большинство снифферов и им подобного ПО, в том числе широко распространенная утилита tcpdump [12]), а работа с базой данных ведется при помощи шаблона DBController.

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

Эту проблему, будь она единственной, можно решить, используя конвейеры Linux/UNIX [13]: запустить tcpdump и направить его вывод на вход утилиты конверсии. Но проблема, связанная с разбором текстового представления данных — остается. Алгоритм разбора был бы нетривиален и изобиловал ветвлениями. Обеспечить его надежность гораздо сложнее, чем использовать библиотеку pcap напрямую. Поэтому в данном случае самописный легковесный сниффер — лучшее решение.

7 Архитектура генератора правил.

Основным компонентом созданной в рамках проекта системы является генератор — программа, которая по данной статистике строит дерево правил и переводит его в формат iptables. В этом же разделе будет представлена общая архитектура программы. Особенности строения отдельных правил будут разобраны в следующих главах.

В математическом описании алгоритма каждому узлу приписано определенное подмножество статистики, а каждому ребру — ЭВ или его дополнение. В генераторе правил ребер как таковых нет. Дерево представлено набором узлов, каждый из которых имеет указатели на своих потомков и своего предка. Каждому узлу приписан набор ключей соединений из статистики (массив целых чисел). Правило представляет собой описатель определенного свойства соединения (протокол, адрес, порт). Экземпляры классов правил содержат в себе пороговые значения, их можно считать представлением ЭВ в системе. Каждая вершина дерева содержит список правил, описывающих свойства множества соединений, приписанного ей. В корне дерева находятся правила, описывающие все возможные соединения. Для обеспечения расширяемости системы правила должны быть максимально отделены от остальной программы, но вместе с тем иметь полный доступ к соединениям, приписанным вершинам (но о самих вершинах они знать не должны).

Правила не должны иметь дело с контроллером статистик. Единственные данные, которыми они оперируют — это наборы соединений. Поэтому был определен интерфейс[1] Set, описывающий набор соединений и с произвольным доступом по локальным номерам и используемый правилами. DBVectorSet — класс, реализующий интерфейс Set. В методах этого класса локальный номер переводится в глобальный номер соединения в статистике, затем по этому номеру из базы данных извлекается описатель соединения. Правилам в качестве наборов соединений передаются указатели на экземпляры класса DBVectorSet. Интерфейс Set на языке С++ приведен в приложении А.

Само правило определяется как класс, реализующий интерфейс Rule. Основной его метод — compute. Он проводит поиск порогового значения, обеспечивающего наилучшее деление данного набора соединений данным правилом. Метод получает в качестве параметра множество соединений (указатель Set*), и возвращает числовое значение разности в весах двух частей, полученных при делении. Чем меньше разность — тем лучше. Полученные характеристики деления (пороговое значение, левое и правое подмножества, а также два дочерних правила, описывающие свойства левого и правого подмножеств) сохраняются в объекте правила и могут быть получены в дальнейшем с помощью специальных методов. Если не удалось найти такое пороговое значение, при котором хотя бы одно соединение из переданного множества удовлетворяет ЭВ, то будем говорить, что правило не прошло. Если для любого возможного порогового значения ЭВ удовлетворяют все соединения, то будем говорить, что правило прошло, но не дало деления (невырожденного). Для определения того, какое именно из правил вершины использовалось при делении, служит флаг активности правила. Также у правила есть метод, позволяющий получить его параметры в текстовом виде, пригодном для использования в командах iptables. В дополнение ко всему, определена процедура клонирования, позволяющая получить точную копию объекта правила. Интерфейс Rule на языке С++ приведен в приложении А.

Работа генератора состоит из двух основных частей: построение дерева и перевод дерева в формат iptables.

Построение дерева.

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

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

  1. Ни одно правило не дало деления. Это означает, что мы имеем дело с нормальным листом, и все его правила также помечаются как активные.
  2. Есть правила, дающие невырожденное деление. Тогда выбирается то из них, метод compute которого вернул наименьшее значение. Обозначим его через AR. Это правило помечается как активное. Создается два новых узла — левый и правый потомки текущего. Все неактивные правила клонируются в них без изменений. Активное правило не клонируется. Вместо этого левому потомку приписывается его левое дочернее правило, а правому потомку — его правое дочернее правило. Также левому потомку приписывается подмножество соединений, удовлетворяющее левому дочернему правилу AR (левое подмножество AR), а правому потомку приписываются все остальные соединения (правое подмножество AR). Так в программе реализуется деление множества соединений при помощи конкретного ЭВ. Две новые вершины добавляются в очередь.

Как видно, активным является то правило, по которому происходит деление. Нужно пояснить, зачем все правила листьев помечаются активными, если деления не происходит. Необходимость этого действия вытекает из того, что в статистике нет аномальных пакетов, и все листья должны быть однозначно идентифицированы либо как нормальные, либо как аномальные (см. алгоритм). Опишем ситуацию на примере. Пусть множество состоит из двух соединений — Conn1 и Conn2, и их параметры различаются только адресом отправителя: для Conn1 это A1, а для Conn2 — A2. Очевидно, что деление произойдет именно по этому полю. Мы получим две вершины, по одному соединению в каждой. И будет известно, какой адрес отправителя у обоих соединений (т.к. именно по этому правилу произошло деление). Но все остальные правила просто клонируются, поэтому нет пока никаких данных, например, о портах соединений. У правил портов этих вершин будет вызван метод compute, который не найдет деления, но обнаружит, что порт всего один. Если не определить правило портов как активное, эта информация будет утеряна, и в левый лист в дальнейшем будут попадать соединения с любым значением порта, имеющие адрес отправителя A1 (аналогично для правого листа). Что неверно, т.к. допускает попадание в нормальные листья аномальных соединений. Если же сделать правило портов активным, мы будем иметь информацию, что в этих двух листьях должны находиться соединения только с данным значением порта.