ВЛАДИМИР МЕШКОВ
Пакетный фильтр
Если вы используете сетевой анализатор с выводом информации на консоль, то одна из проблем, с которой вы столкнетесь, заключается в том, что анализатор не будет успевать отображать все данные, поступившие из сети, и часть пакетов будет для вас потеряна.
Это особенно актуально, если трафик очень плотный. Вторая проблема – вас могут интересовать только пакеты, адресованные выделенным хостам, а не все подряд. Решение заключается в фильтрации входящих сетевых пакетов по какому-либо определенному признаку, например по адресной части. Одним из вариантов решения является использование оператора условия if, однако данное решение неэффективно. В этом случае ядру приходится вытаскивать полный пакет из сети, на что отнимается процессорное время, затем анализатор вынужден «экзаменовать» заголовок каждого пакета перед принятием решения – отображать данные или нет. Оптимальным является решение отсеять лишние пакеты как можно раньше, на уровне драйвера сетевой карты. Ядро Linux позволяет сделать это при помощи пакетного фильтра.
Описание языка BPF
Пакетный фильтр представляет собой последовательность инструкций, составленных в кодах псевдо-машинного языка, который называется BPF – Berkeley Packet Filter. Этот язык был разработан Стивом Маккеном (Steve McCanne) и Ван Якобсоном (Van Jacobson). BPF похож на язык ассемблер. В нем, как и в ассемблере, есть регистры, инструкции для загрузки и хранения операндов, выполнения арифметико-логических операций, условных и безусловных переходов. Для работы с операндами в BPF используются регистр-аккумулятор (или просто аккумулятор), индексный регистр, ячейка памяти и внутренний программный счетчик.
Формат инструкции языка BPF определяет следующая структура:
struct sock_filter {
__u16 code;
__u8 jt;
__u8 jf;
__u32 k;
};
которая определена в файле .
Назначение полей структуры следующее:
- поле k – числовое значение операнда, с которым работает инструкция;
- поля jt (jump true) и jf (jump false) – меняют порядок выполнения инструкций;
- поле code – код выполняемой инструкции.
Существует 8 типов инструкций. Вначале их перечислим, а потом остановимся отдельно на каждом. Итак, вот эти 8 типов: BPF_LD, BPF_LDX, BPF_ST, BPF_STX, BPF_ALU, BPF_JMP, BPF_RET, BPF_MISC.
BPF_LD
Инструкция BPF_LD служит для загрузки в аккумулятор следующих величин:
- константы (BPF_IMM);
- блока данных, расположенных по фиксированному смещению (BPF_ABS);
- блока данных, расположенных по смещению, которое является переменной величиной (BPF_IND);
- длины блока данных (BPF_LEN);
- значения, находящегося в ячейке памяти (BPF_MEM).
Для значений BPF_IND и BPF_ABS размер загружаемых данных должен быть задан как слово (BPF_W), полуслово (BPF_H), байт (BPF_B). Здесь имеется в виду машинное слово, которое равно числу разрядов в регистрах общего назначения. Для 32-х разрядных процессоров это значение равно 4 байта.
Примеры использования данной инструкции.
BPF_LD+BPF_W+BPF_ABS A <- P [ k : 4 ]
В аккумулятор загружается 4 байта из блока данных. Смещение в блоке данных задается константой k.
BPF_LD+BPF_H+BPF_ABS A <- P [ k : 2 ]
В аккумулятор загружается 2 байта из блока данных. Смещение в блоке данных задается константой k.
BPF_LD+BPF_B+BPF_ABS A <- P [ k : 1 ]
В аккумулятор загружается 1 байт из блока данных. Смещение в блоке данных задается константой k.
BPF_LD+BPF_W+BPF_IND A <- P [ X + k : 4 -]
В аккумулятор загружается 4 байта из блока данных. Смещение в блоке данных задается суммой переменных X и константы k. Переменная X является значением, находящимся в индексном регистре.
BPF_LD+BPF_H+BPF_IND A <- P [ X + k : 2 ]
В аккумулятор загружается 2 байта из блока данных. Смещение в блоке данных задается суммой переменных X и константы k. Переменная X является значением, находящимся в индексном регистре.
BPF_LD+BPF_B+BPF_IND A <- P [ X + k : 1 ]
В аккумулятор загружается 1 байт из блока данных. Смещение в блоке данных задается суммой переменных X и константы k. Переменная X является значением, находящимся в индексном регистре.
BPF_LD+BPF_W+BPF_LEN A <- len
В аккумулятор загружается длина блока данных.
BPF_LD+BPF_IMM A <- k
В аккумулятор загружается константа k.
BPF_LD+BPF_MEM A <- M [ k ]
В аккумулятор загружается значение, находящееся в ячейке памяти с адресом k.
BPF_ LDX
Инструкция BPF_LDX служит для загрузки в индексный регистр следующих величин:
- константы (BPF_IMM);
- значения, находящегося в ячейке памяти (BPF_MEM);
- длины блока данных (BPF_LEN).
Примеры использования данной инструкции.
BPF_LDX+BPF_W+BPF_IMM X <- k
В индексный регистр загружается константа k, размер которой составляет 4 байта.
BPF_LDX+BPF_W+BPF_MEM X <- M [ k ]
В индексный регистр загружается значение, находящееся в ячейке памяти с адресом k.
BPF_LDX+BPF_W+BPF_LEN X <- len
В индексный регистр загружается длина блока данных. Следующая инструкция позволяет быстро определить размер заголовка IP-пакета:
BPF_LDX+BPF_B+BPF_MSH X <- 4 * ( P [ k : 1 ] & 0xF )
Длина заголовка находится в младших 4 битах нулевого байта IP-пакета и содержит количество 32-битных слов в заголовке. Поскольку минимальный размер заголовка 20 байт (т.е. пять 32-битных слов), то минимальное значение этого поля равно 5. Старшие 4 бита содержат версию протокола и для IPv4 это значение равно 4. Итак, предположим, что в нулевом байте находится значение 0x45. Эту величину мы загружаем в индексный регистр и осуществляем операцию логического И: 0x45 & 0xF = 0x5. Умножение на 4 эквивалентно логическому сдвигу на 2 бита в сторону старших разрядов, при этом значения старших разрядов теряются. В итоге в индексном регистре будет находиться значение 0x5 * 4 = 0x14, в десятичном представлении – 20. Это и есть длина заголовка IP-пакета в байтах.
BPF_ST
Инструкция BPF_ST служит для загрузки аккумулятора в ячейку памяти:
BPF_ST M [ k ] <- A
Значение k определяет адрес ячейки памяти.
BPF_STX
Инструкция BPF_STX служит для загрузки индексного регистра в ячейку памяти:
BPF_STX M [ k ] <- X
Значение k определяет адрес ячейки памяти.
BPF_ALU
Инструкция BPF_ALU выполняет арифметико-логические между аккумулятором и индексным регистром или константой. Результат сохраняется в аккумуляторе.
BPF_ALU+BPF_ADD+BPF_K A <- A + k
BPF_ALU+BPF_SUB+BPF_K A <- A — k
BPF_ALU+BPF_MUL+BPF_K A <- A * k
BPF_ALU+BPF_DIV+BPF_K A <- A / k
BPF_ALU+BPF_AND+BPF_K A <- A & k
BPF_ALU+BPF_OR+BPF_K A <- A | k
BPF_ALU+BPF_LSH+BPF_K A <- A << k
BPF_ALU+BPF_RSH+BPF_K A <- A >> k
BPF_ALU+BPF_ADD+BPF_X A <- A + X
Здесь я не вижу необходимости в комментариях, все прозрачно.
BPF_JMP
Инструкция BPF_JMP изменяет порядок выполнения программы фильтрации. Данная инструкция может осуществлять как условный, так и безусловный переход между инструкциями. При безусловном переходе (BPF_JA, jump always) смещение задается 32-битным значением, при условном – 8-битным.
BPF_JMP+BPF_JA pc += k
Безусловный переход по смещению, заданному 32-разрядным значением k.
BPF_JMP+BPF_JGT+BPF_K pc += ( A > k ) ? jt : jf
Сравнение значений аккумулятора и константы k. Условный переход по смещению, заданному в поле jt при выполнении условия A > k.
BPF_JMP+BPF_JGE+BPF_K pc += ( A >= k ) ? jt : jf
BPF_JMP+BPF_JEQ+BPF_K pc += ( A == k ) ? jt : jf
BPF_JMP+BPF_JSET+BPF_K pc += ( A & k ) ? jt : jf
BPF_JMP+BPF_JGT+BPF_X pc += ( A > X ) ? jt : jf
Сравнение значений аккумулятора и индексного регистра. Условный переход по смещению, заданному в поле jt при выполнении условия A > X.
BPF_JMP+BPF_JGE+BPF_X pc += ( A >= X ) ? jt : jf
BPF_JMP+BPF_JEQ+BPF_K pc += ( A == X ) ? jt : jf
BPF_JMP+BPF_JSET+BPF_K pc += ( A & X ) ? jt : jf
BPF_RET
Программа фильтрации выполняется для каждого пакета, поступающего на сетевой интерфейс. Результатом работы фильтра является целое положительное число, показывающее, сколько байт в принятом пакете будет доступно для дальнейшей обработки приложению пользователя. Если принятый пакет не удовлетворяет условиям фильтрации, он отбрасывается и программой фильтрации возвращается нулевое значение. Инструкция BPF_RET завершает выполнение программы фильтрации и возвращает число байт в пакете, доступных для дальнейшей обработки.
BPF_RET+BPF_A
Возвращаемый результат находится в аккумуляторе.
BPF_RET+BPF_K
Результат возвращается в виде константы.
BPF_MISC
Инструкция BPF_MISC служит для копирования значения индексного регистра в аккумулятор и наоборот.
BPF_MISC+BPF_TAX X <- A
BPF_MISC+BPF_TXA A <- X
Пример реализации пакетного фильтра
Здесь мы с помощью фильтра модифицируем исходный код анализатора сетевого трафика (см. «Анализатор сетевого трафика», «Системный администратор» №1, октябрь 2002 г.). Все изменения необходимо внести только в главную функцию. Перед тем как приступить к реализации пакетного фильтра, необходимо определить условия фильтрации. В нашем примере условия следующие: обрабатываться будут пакеты IP-протокола, адрес отправителя – 192.168.1.2, транспортный протокол – TCP, порт источника – 23.
Алгоритм реализации следующий:
- определить необходимые переменные и заголовочные файлы;
- составить программу фильтрации в кодах языка BPF;
- привязать полученный фильтр к сокету.
Переменные и заголовочные файлы
Нам необходим один заголовочный файл:
# include
и структура:
struct sock_fprog *Filter;
– непосредственно сам подключаемый фильтр.
Программа фильтрации
Программа фильтрации представляет собой массив структур struct sock_filter. При ее составлении воспользуемся макросами BPF_STMP и BPF_JUMP, которые определены в заголовочном файле .
struct sock_filter BPF_code [ ] = {
BPF_STMT(BPF_LD+BPF_H+BPF_ABS, 12),
В принятом Ethernet-кадре по смещению, равному 12 байт (6 байт MAC-адреса источника + 6 байт MAC-адреса назначения), находится 2-х байтовый идентификатор протокола сетевого уровня. Эти 2 байта мы загружаем в аккумулятор.
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ETH_P_IP, 0, 8),
Проверяем соответствие значения, загруженного в аккумулятор, идентификатору IP-протокола (ETH_P_IP = 0x800). При выполнении условия переходим к следующей инструкции (jt = 0). В противном случае смещаемся на 8 структур вниз (jf = 8) и выходим из программы фильтрации с возвратом нулевого значения. Это значит, что данный пакет отброшен.
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, 26),
Загружаем в аккумулятор 4-байтовое значение, находящееся по смещению 26 в принятом пакете. Это значение соответствует IP-адресу источника.
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0xC0A80102, 0, 6),
Проверяем соответствие значения, загруженного в аккумулятор, IP-адресу 192.168.1.2. Значение 0xC0A80102 – это шестнадцатиричное представление данного IP-адреса. Однако в сетевом формате адрес 192.168.1.2 выглядит как 0x201A8C0. Это связано с порядком передачи в сети – передача начинается с бита младшего разряда. Если адрес не совпадает – выходим из программы фильтрации.
BPF_STMT(BPF_LD+BPF_B+BPF_ABS, 23),
Загружаем в аккумулятор 1 байт, находящийся по смещению 23. В этом поле содержится идентификатор протокола транспортного уровня. Для протокола TCP это значение равно 6.
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, IPPROTO_TCP, 0, 4),
Проверяем соответствие транспортного протокола.
Далее нам необходимо проверить поле «Порт источника» на соответствие значению 23. Для этого необходимо сперва установить длину заголовка IP-пакета.
BPF_STMT(BPF_LDX+BPF_B+BPF_MSH, 14),
В индексный регистр будет загружено значение длины заголовка IP-пакета. По смещению, равному сумме длин Ethernet-заголовка и IP-заголовка, будет находиться поле «Порт источника». Загрузим его в аккумулятор:
BPF_STMT(BPF_LD+BPF_H+BPF_IND, 14),
и проверим полученное значение:
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0x17, 0, 1),
Выходим из программы фильтрации.
BPF_STMT(BPF_RET+BPF_K,1500),
BPF_STMT(BPF_RET+BPF_K,0),
};
Заполним поля структуры struct sock_fprog *Filter :
Filter -> len = 11; - значение поля len равно числу структур в массиве BPF_code [ ]
Filter -> filter = BPF_code; - указатель на массив структур BPF_code [ ]
Привязка фильтра к сокету выполняется при помощи вызова setsockopt следующим образом:
if ( setsockopt ( e0_r, SOL_SOCKET, SO_ATTACH_FILTER, Filter, sizeof (*Filter) ) < 0 ) {
perror ( "SO_ATTACH_FILTER" );
close ( e0_r );
exit (1);
}
В приведенном примере есть один недостаток: исходные данные, такие как IP-адрес, номер порта, вводятся в текст программы. При необходимости изменения условий фильтрации придется каждый раз вносить изменения в исходный текст и перекомпилировать программу. Модифицируем программу фильтрации для возможности гибкой настройки фильтра на новое условие, например, на новый IP-адрес. IP-адрес будем вводить в командной строке (аргумент argv [1]). В массиве BPF_code третий элемент перепишем в виде:
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0, 0, 4),
В этой инструкции мы обнуляем поле, содержащее IP-адрес. Для хранения введенного IP-адреса нам потребуются дополнительные переменные:
union {
u_long net;
char point [4];
} addr;
В поле net объединения addr будет храниться введенный IP-адрес в сетевом формате. Заполним поле net:
addr. net = inet_addr (argv [1]);
Теперь необходимо зеркально перевернуть значение IP-адреса в сетевом формате. Сделаем это, поменяв местами 0-й и 3-й байты, 1-й и 2-й:
addr. point [0] ^= addr. point [3];
addr. point [3] ^= addr. point [1];
addr. point [0] ^= addr. point [3];
addr. point [1] ^= addr. point [2];
addr. point [2] ^= addr. point [1];
addr. point [1] ^= addr. point [2];
Заполним необходимое поле в массиве BPF_code:
BPF_code [3]. k = addr. net;
Таким образом, если мы введем адрес 192.168.1.2, поле BPF_code [3]. k будет содержать значение 0xC0A80102. Для контроля можно отобразить это значение:
printf( « %x «, BPF_code[3]. k );
После этого подключаем фильтр к сокету и работаем.
Использование tcpdump
Для упрощения задачи составления программы фильтрации можно воспользоваться утилитой tcpdump. Давайте рассмотрим, как это делается. Предположим, нам необходим пакетный фильтр, принимающий IP-пакеты, адресатом или отправителем которых является хост с IP-адресом 192.168.9.1. Вводим следующую команду:
tcpdump -dd host 192.168.9.1
На консоли будет отображен результат работы программы:
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 4, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 8, 0, 0xc0a80901 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 6, 7, 0xc0a80901 },
{ 0x15, 1, 0, 0x00000806 },
{ 0x15, 0, 5, 0x00008035 },
{ 0x20, 0, 0, 0x0000001c },
{ 0x15, 2, 0, 0xc0a80901 },
{ 0x20, 0, 0, 0x00000026 },
{ 0x15, 0, 1, 0xc0a80901 },
{ 0x6, 0, 0, 0x00000044 },
{ 0x6, 0, 0, 0x00000000 },
|
Это и есть искомая программа фильтрации.
Вот в принципе все, о чем я хотел рассказать. Обо всех замечаниях и предложениях пишите на ubob@mail.ru.