Изучаем netfilter: пишем свой match-модуль на базе xt_string для поиска нескольких шаблонов

  • Tutorial

Введение


Недавно заметил, что на хабре мало информации по разработке модулей ядра. Всё что я нашёл:


Всегда удивляло то, что люди, более-менее знающие C, боятся и избегают даже читать ядерный код, как будто он на 60% состоит из ассемблера (который на самом деле тоже не такой уж сложный). Собственно я планирую написать серию статей, посвящённую разработке или доработке существующих модулей netfilter и iptables.

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

Что будем делать


Как сказано в названии статьи — мы напишем простой модуль iptables на базе xt_string. Xt_string — это модуль netfilter, умеет искать последовательность байт в пакете. Однако ему, на мой взгляд, не хватает способности осуществлять поиск нескольких последовательностей байт в заданном порядке. Ну, а так как лицензия GPL, то что мешает ему эту возможность придать?


Собственно в этой статье такой модуль мы и запилим, назовём его xt_wildstring, который можно будет использовать для толстого пиараследующим образом:

iptables -I FORWARD -p tcp --dport 80 --tcp-flags ACK,PSH ACK,PSH -m wildstring --wildstring "reductor*price*carbonsoft.ru" -j DROP.

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

Кратко про устройство netfilter и iptables


Как правило, модуль iptables состоит из двух частей — kernelspace и userspace. В kernelspace находится модуль ядра Linux, который можно динамически подгрузить и использовать. Он-то и и работает с пакетами, когда мы добавляем правило в iptables. В userspace находится уже модуль iptables, который позволяет создавать правила и передавать их ядру Linux.

Модули netfilter можно разделить на три категории:
  • Хуки — по сути дефолтные цепочки и таблицы, которые подставляются на пути пакета сквозь ядро
  • Матчи — модули, которые возвращают true или false, позволяют использовать условия, например, определить к какому протоколу принадлежит пакет
  • Таргеты — модули, которые производят над пакетом некое действие, самые известные — ACCEPT / DROP, хотя на самом деле их гораздо больше

Где в исходниках находятся эти модули:

Netfilter является частью исходников ядра Linux и в версии 2.6.32 находится в нескольких каталогах:
/usr/src/linux/net/netfilter/ — большинство match-модулей.
/usr/src/linux/net/ipv4/netfilter/ — часть target-модулей.
/usr/src/linux/include/linux/netfilter/ — заголовки и тех и других модулей.

Модули iptables располагаются в каталоге
/usr/src/iptables/extensions/

Заголовки модулей kernelspace и userspace обязательно должны совпадать, поэтому лучше, если это будет один файл.

А теперь перейдём от теории к практике


Мы не будем изобретать велосипед, не для того GPL придумали. Возьмём модуль xt_string из последнего ядра CentOS 6, как одного из наиболее стабильных на данный момент.

Про настройку системы сборки модуля и стенда вышло больно много информации, поэтому скрыл её под спойлер. Если возникнет непонимание или интерес к тому, где и что собирается, запускается и тестируется — имеет смысл заглянуть под него.

Настройки системы сборки и стенда для тестирования.
Готовим систему сборки и отладки

Да, многие мечтают об удобной IDE для разработки Linux Kernel. Но, увы и ах, ничего стоящего я не находил. Одна из причин тому относительно простая — в случае сегфолта в ядре мы получим Kernel Panic и потратим много времени на перезагрузку, если паника произойдёт на нашей рабочей машине. Поэтому разработка, как правило, ведётся в виртуальной машине, либо на отдельном стенде, в случае если код пишется под специфичное железо. Однако наш модуль универсален, так что ставим виртуалки.

Ставим CentOS на две виртуальные машины

Собственно чтобы наш мозг не простаивал во время Kernel Panic при неудачах, а они гарантированно будут, поступим следующим образом. Установим две виртуальные машины, у которых будет доступ в Интернет и друг к другу. Одна будет сборщиком модуля, а вторая стендом для проверки.

На сборщике получаем исходники linux и iptables

Кстати, на сборщике нам потребуется несколько хороших и полезных программ.

yum install git ncurses-devel make gcc rpm-build indent


Теперь добавляем себе в закладки один из самых полезных репозиториев для разрабатывающего под CentOS человека:

http://vault.centos.org/6.4/os/Source/SPackages/

Отсюда мы будем брать src.rpm ядра Linux и Iptables.

rpm -i http://vault.centos.org/6.4/os/Source/SPackages/kernel-2.6.32-358.el6.src.rpm
rpm -i http://vault.centos.org/6.4/os/Source/SPackages/iptables-1.4.7-9.el6.src.rpm


Затем идём в /root/rpmbuild/SPECS/ и разворачиваем исходники с наложением патчей от CentOS.

rpmbuild -bp iptables.spec
rpmbuild -bp kernel.spec


В /root/rpmbuild/BUILD/ у нас появятся папки с исходниками ядра Linux и iptables.

Теперь надо хотя бы один раз собрать ядро целиком, чтобы иметь возможность пересобирать только папку net/netfilter/ при внесении изменений в наш модуль. Для удобства и привычности сделаем симлинки:

ln -s /root/rpmbuild/BUILD/kernel-2.6.32-358.el6/linux-2.6.32-358.el6.x86_64/ /usr/src/linux
ln -s /root/rpmbuild/BUILD/iptables-1.4.7/ /usr/src/iptables/


Идём в /usr/src/linux. Для начала сгенерируем конфиг.

make menuconfig


Сохраняем его и собираем всё ядро. Кстати, возможно на rpmbuild или make произойдёт зависание на gpg: keyring `./pubring.gpg' created. Чтобы этого избежать, скажем что random у нас — это urandom.

rm -f /dev/random
ln -s /dev/urandom /dev/random


И собственно сборка:

make prepare
make -j 3 
make modules_install


Вообще было бы неплохо исходники модуля хранить всё в GIT-репозитории, у меня он располагается в ~/GIT/wildstring/.

Перезагрузка стенда при kernel panic

Можно делать это двумя способами, на мой взгляд, наиболее правильный – выставить параметр /proc/sys/kernel/panic в 2. Но вывод паники нам важен, поэтому при необходимости можно воспользоваться скриптом на хост-системе в духе:

name=centos_test
ip=<ip_стенда>
while true; do
	if ! ping -qc 1 $ip; then
		virt-viewer $name
		sleep 2
		scrot
		virsh destroy $name
		virsh start $name
		sleep 60
	fi
done


Проверка работоспособности модуля

#!/bin/bash

test_wildstring() {
	iptables -F OUTPUT
	rmmod xt_wildstring
	insmod xt_wildstring
	iptables -I OUTPUT -p tcp –dport 80 -m wildstring “opensource*carbonsoft” -j DROP
	wget -t 1 -T 1 http://carbonsoft.ru/opensource/
	Iptables -nvL OUTPUT
}

test_wildstring

if [ “$1” = 'while' ]; then
	while true; do
		test_wildstring
		sleep 1
	done
fi


Который можно юзать так:

Единоразовый запуск:
./test_wildstring.sh


Бесконечный цикл:
./test_wildstring.sh while



Копируем string из linux и iptables

Находим нужные нам модули и копируем их в наш репозторий.

cp -v /usr/src/linux/net/netfilter/xt_string.c ~/GIT/wildstring/xt_wildstring.c
mkdir -p ~/GIT/wildstring/include/linux/netfilter/
cp -v /usr/src/linux/include/linux/netfilter/xt_string.h ~/GIT/wildstring/include/linux/netfilter/xt_wildstring.h


Пишем Makefile

Опишем сборку модуля ядра, модуля iptables, а также выравнивание кода, подчистку рабочей папки и ещё пару целей.

obj-m += xt_wildstring.o

all: module lib

module:
	cp include/linux/netfilter/xt_wildstring.h /usr/src/linux/include/linux/netfilter/xt_wildstring.h
	make -C /lib/modules/2.6.32/build M=$(PWD) modules
lib:
	cp libxt_wildstring.c /usr/src//iptables/extensions
	cp include/linux/netfilter/xt_wildstring.h /usr/src/iptables/include/linux/netfilter/xt_wildstring.h
	make -C /usr/src/iptables/extensions
	cp /usr/src/iptables/extensions/libxt_wildstring.so libxt_wildstring.so
userspace:
	gcc userspace_wildstring.c -o userspace
	./userspace
	rm -f userspace
install:
	scp xt_wildstring.ko root@10.90.140.160:
	scp libxt_wildstring.so root@10.90.140.160:/lib64/xtables-1.4.7/
clean:
	rm -f *~ *.ko *.so *.mod.c *.ko.unsigned *.o modules.order Module.symvers
indent:
	Lindent *.c include/linux/netfilter/xt_wildstring.h


Комментарии к Makefile:
  • 2.6.32 — захардкодили, так как uname -r = 2.6.32-358.0.1.el6.x86_64, а этих исходников у меня под рукой нет, соответственно и симлинк симлинк /lib/modules/2.6.32-358.0.1.el6.x86_64/build работать не будет.
  • Поскольку я не гуру makefile, и не придумал красивого и правильного способа собирать libxt_wildstring.so так, как xt_wildstring.ko, то я решил не заморачиваться и написать эту цель простыми bash-командами.
  • Для того чтобы scp в цели install работал без пароля нужно сгенерировать на системе сборки SSH-ключи и подкинуть их к тестовому стенду.
  • Команда Lindent копируется из /usr/src/linux/scripts/Lindent в /usr/local/bin, поскольку часто используется. Рекомендую использовать её всегда при написании кода в ядре Linux, так как со своим уставом в чужой монастырь не ходят. Лучше даже перед каждым коммитом.


Убираем лишнее в .gitignore

Untracked-файлы в git status несколько напрягают, поэтому создадим ~/GIT/wildstring/.gitignore:

*.o
*.so
.*
*.ko
*.ko.unsigned
modules.order
Module.symvers
*.mod.c
!.gitignore

Переименовываем в wildstring

Чтобы модуль не конфликтовал с оригиналом, имеет смысл переименовать его и все его функции с string на wildstring. Важный момент — править нужно всё: и заголовок, и userspace модуль, и kernelspace модуль. В этом деле grep спасёт отца русской демократии:

grep -ri string xt_wildstring.c | grep -vi wildstring


Расширяем структуру match info

И снова немного теории: каждый match-модуль имеет свою структуру match-info, которая формируется на основе параметров передаваемых из userspace. Она описывается в заголовочном файле (xt_wildstring.h).

Стандартный xt_string.h выглядит следующим образом
#ifndef _XT_STRING_H
#define _XT_STRING_H

#include <linux/types.h>

#define XT_STRING_MAX_PATTERN_SIZE 128
#define XT_STRING_MAX_ALGO_NAME_SIZE 16

enum {
	XT_STRING_FLAG_INVERT		= 0x01,
	XT_STRING_FLAG_IGNORECASE	= 0x02
};

struct xt_string_info
{
	__u16 from_offset; //сдвиг от начала данных в пакете – откуда начинаем поиск.
	__u16 to_offset; //сдвиг от начала данных в пакете – до куда продолжаем поиск.
	char	algo[XT_STRING_MAX_ALGO_NAME_SIZE]; //используемый алгоритм.
	char 	pattern[XT_STRING_MAX_PATTERN_SIZE]; //то, что мы ищем, шаблон.
	__u8 patlen; //длина шаблона, заполняется автоматически.
	union {
		struct {
			__u8 invert; //флаг инверсии модуля ! -m string –string “something”
		} v0;

		struct {
			__u8 flags; //не помню точно что это.
		} v1;
	} u;

	/* Used internally by the kernel 
	 * конфиг текстового поиска.
	 *вообще довольно забавное по назначению поле, но кто говорил что
	 *конфигоманией страдают только java-программисты?
	 *возрадуемся по крайней мере тому, что он не в xml.
	 */
	struct ts_config __attribute__((aligned(8))) *config; 
};

#endif /*_XT_STRING_H*/




Размножим несколько полей структуры xt_wildstring_info в xt_wildstring.h

Для начала добавим указатели на подстроки. Именно указатели, а не массивы символов, как в оригинале, поскольку второй и третий указатель могут быть пустыми, то есть в модуль будет передан шаблон без звёздочек. По аналогии добавляем для них переменные для хранения длины подстрок + по структуре параметров текстового поиска в пакете на каждый шаблон. В итоге структура стала выглядеть следующим образом:

#ifndef _XT_WILDSTRING_H
#define _XT_WILDSTRING_H

#include <linux/types.h>

#define XT_WILDSTRING_MAX_PATTERN_SIZE 128
#define XT_WILDSTRING_MAX_ALGO_NAME_SIZE 16

enum {
	XT_WILDSTRING_FLAG_INVERT		= 0x01,
	XT_WILDSTRING_FLAG_IGNORECASE	= 0x02
};

struct xt_wildstring_info
{
	__u16 from_offset;
	__u16 to_offset;
	char	algo[XT_WILDSTRING_MAX_ALGO_NAME_SIZE];
	char 	pattern[XT_WILDSTRING_MAX_PATTERN_SIZE];
	/* указатели на шаблоны */
	char 	*pattern_part1;
	char 	*pattern_part2;
	char 	*pattern_part3;
	__u8 patlen;
	/* длины шаблонов */
	__u8 patlen_part1;
	__u8 patlen_part2;
	__u8 patlen_part3;
	union {
		struct {
			__u8 invert;
		} v0;

		struct {
			__u8 flags;
		} v1;
	} u;

	/* Used internally by the kernel */
	/* оригинальный конфиг по идее уже не нужен */
	struct ts_config __attribute__((aligned(8))) *config;
	struct ts_config __attribute__((aligned(8))) *config_part1;
	struct ts_config __attribute__((aligned(8))) *config_part2;
	struct ts_config __attribute__((aligned(8))) *config_part3;
};
#endif


Начинаем пользоваться новыми полями хедера

Переходим к xt_wildstring.c.

Теперь то, что мы добавили в хедер пора и использовать. Для начала доведём до работоспособности подготовку и уничтожение конфигов поиска.

Здесь опять немного теории – как правило, структура match-модуля содержит следующие функции и структуры:

  • init – инициализация модуля при его подгрузке;
  • exit – уничтожение модуля при его загрузке;
  • mt – функция проверяющая пакет;
  • mt_check – функция, проверяющая корректность вызова модуля при добавлении правила;
  • mt_destroy – функция, подчищающая ресурсы при удалении правила;
  • mt_reg — структура указателей на функции mt_check, mt и mt_destroy + дополнительную информацию о модуле;


В оригинальном xt_string добавление и удаление правила происходит следующим образом:

В string_mt_check (добавлении) на основе строки и алгоритма поиска генерируется структура ts_config, (ts – text search). Функция поиска по данным пакета (skb_find_text) использует её в качестве параметра. Очистка памяти, занимаемой этой структурой (функция string_mt_destroy) проводится функцией textsearch_destroy, вызываемой при удалении правила из цепочки.

Добавляем пару textsearch_prepare в xt_wildstring_check

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

static bool
wildstring_mt(const struct sk_buff *skb, const struct xt_match_param *par)
{
	return false;
#if 0
	...
#endif
}


Для начала подготовим наши ts_conf в функции xt_wildstring_check, которая вызывается в момент добавления правила в iptables. Скопируем указатель на начало строки во временную переменную, и будем проходиться по нему функцией strsep, занимающейся разбиением строки по заданному набору символов. Если токен нашёлся — вычисляем его длину и используем его для подготовки параметров текстового поиска.

s = (char *) conf->pattern;
conf->pattern_part1 = strsep(&s, delim);
if (!conf->pattern_part1)
	return false; //первый элемент в любом случае должен быть
conf->patlen_part1 = strlen(conf->pattern_part1);
ts_conf = textsearch_prepare(conf->algo, conf->pattern_part1,
		conf->patlen_part1, GFP_KERNEL, flags);
if (IS_ERR(ts_conf))
	return false;
conf->config_part1 = ts_conf;


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

И уничтожаем их в wildstring_mt_destroy

Эта функция вызывается в момент удаления правила из iptables. Для уничтожения параметров при удалении правила размножим destroy.

static void wildstring_mt_destroy(const struct xt_mtdtor_param *par)
{
	struct xt_wildstring_info *conf = WILDSTRING_TEXT_PRIV(par->matchinfo);

	if (conf->pattern_part1)
		textsearch_destroy(conf->config_part1);
	if (conf->pattern_part2)
		textsearch_destroy(conf->config_part2);
	if (conf->pattern_part3)
		textsearch_destroy(conf->config_part3);
}


Доводим до ума match

И вот модуль стал успешно загружаться-выгружаться, а правила добавляться-удаляться, и никаких Kernel Panic. Теперь вернёмся к ранее закомментированной функции wildstring_mt и добавим в неё поиск всех переданных в функцию шаблонов.
Во-первых, нам понадобится переменная для сохранения длины сдвига, на котором удалось найти нужную подстроку.

unsigned int skb_find = 0;


Вообще не самое удачное название, гораздо понятнее было бы что-то в духе tmp_from_offset или wildstring_from_offset, но всё уже есть в коммитах на гитхабе, так что, увы, поздно. Теперь вместо того чтобы возвращать результат первого поиска, мы его присвоим нашей новой переменной, проанализируем и если ничего не найдено — вернём false, и так до тех пор пока мы не пройдёмся по всем заданным шаблонам.

memset(&state, 0, sizeof(struct ts_state));
skb_find = skb_find_text((struct sk_buff *)skb, conf->from_offset,
		conf->to_offset, conf->config_part1, &state);
if (skb_find == UINT_MAX)
	return false;


И так повторяем для config_part2 и config_part3, с той разницей, что наличие pattern_part2 и pattern_part3 надо проверять и в случае отсутствия — возвращать true.

Добиваем и проверяем

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

Тестируем на паре тестовых примеров с помощью wget или curl. При создании правила важно помнить о том, что в HTTP-пакете GET находится перед HOST, и шаблон придётся писать чуть-чуть задом наперёд:
  • «something*html*example.com»
  • «pron*avi*yoursite»
  • «reductor*scheme*carbonsoft.ru»


То есть добавляем правило:

iptables -I OUTPUT -p tcp –dport 80 -m wildstring “reductor*scheme*carbonsoft” -j DROP


и пробуем скачать страничку:

wget -t 1 -T 1 http://www.carbonsoft.ru/products/reductor/carbon-reductor/#scheme

Бинго — мы обломались и iptables -nvL OUTPUT показывает увеличившийся счётчик пакетов.

Почему не списки?


Внимательный и опытный читатель, возможно воскликнет, да что там, заорёт — мол зачем такие извращения и костыли, когда можно использовать списки и добавлять/удалять в него структурку, состоящую из pattern, patlen и config, а потом проходиться по этому списку for_each_entry. Но — целью статьи является показать устройство модуля netfilter, а работа со списками в ядре linux добавила бы в модуль ещё одну дополнительную сущность, которую надо понимать. Ну и к тому же, надо же оставить что-нибудь читателю для самостоятельных упражнений.

Завершение


Собственно вот мы и научились делать модули ядра для netfilter, разве это не прекрасно?
Вообще использовать модуль можно не только для HTTP, но и для многих других протоколов, примеры, пожалуй, позже добавлю в комментариях.

Исходники можно взять в разделе opensource на нашем сайте.

Carbon Soft

25,70

Продукты для бизнеса и людей, ценящих свое время.

Поделиться публикацией
Комментарии 14
    0
    А есть пример правила для других протоколов?
    Типа торрент заблокировать?
      0
      Блокировать торренты не стоит, правильнее урезать с помощью connpkts/connlimit (именно урезать, а не отрезать).

      Про другие протоколы — сегодня чуть позже дополню статью.
        0
        ipp2p из xtables-addons
        0
        Ну другие мне не очень интересно.
        А торренты через l7 модуль если фильтровать — вешают ядро роутера.
        Может с этим былоб лучше.
          0
          Ну, могу попробовать подсказать как легче всего в конкретной ситуации фильтровать.

          Во-первых — задача именно заблокировать и на 100% или порезать, чтобы не ели канал?
          Во-вторых — какая ширина канала/количество юзеров?

          P.S: чтобы l7 не вешал ядро, можно выносить его в userspace (см. l7-filter-userspace), правда так производительность на большом канале падает сильно.
          P.P.S: forum.nag.ru/forum/index.php?showtopic=55025&st=1060 хорошо раскрыта тема, в основном беда вроде как с utp бывает.
          0
          Кажется понял как можно проверить.
          А где бинарики скачать чтоб потестить?
            0
            У вас CentOS 6 с 2.6.32-358 x86_64?
            Если да, могу в личные сообщения скинуть бинарь.
            0
            поставлю такой не вопрос, скинь плиз
              +1
              Так и был разрушен миф о необходимости дорогущего DPI и невозможности мелким интернет-провайдерам отфильтровать запрещенные сайты по URL, а не по IP :)
                0
                Ну, списки запрещённых сайтов не по дням, а по часам растут, пачкой правил с такими поисками можно потери пакетов до 90% довести и все абоненты убегут :)
                  0
                  Пачка правил в iptables действительно не самое лучшее решение, одно правило подгружающее список из файла при инициализации будет оптимальнее. Реализовывал подобный math модуль только с более детальным разбором http, чтобы точно определить где URL, а где содержимое, а систему загрузки и хранения базы недолго думая подсмотрел у плагина GeoIP, получился такой симбиоз. Для большого оператора конечно не пойдет в плане нагрузки, но для мелких все реально сделать.
                    0
                    > math
                    match, полагаю.

                    А не пробовали под нагрузкой использовать? Я когда свой DPI разрабатывал тоже думал что (да боже, на этом костыле от силы 200 юзеров работать смогут), а оказалось 10Гб/сек при 10000 URL фильтрует запросто на относительно бюджетном железе.
                      0
                      Какая нагрузка была — со всем справлялось, а синтетически хз как её верно воссоздать :))
                      Я сначала на отдельной железке просто маршрутизацией выделил «подозрительные» ip адреса находящиеся в реестре. Затем этот трафик прилетел на железку с iptables, где уже и разбирался по URL. Причем для ускорения процесса базу данных черного списка оформил в виде упорядоченного хэша по host'ам. Соответственно прилетает http, из него выдираем host, хэшируем несложной функцией, смотрим есть ли такой хэш в базе, если есть — то уже сравниваются url'ы.
                      Одна беда — пока работал так и не придумал как сделать вывод заглушки, соединения просто DROP'ались, а пользователь оставался в неведении почему так произошло.
                        +1
                        Одна беда — пока работал так и не придумал как сделать вывод заглушки, соединения просто DROP'ались, а пользователь оставался в неведении почему так произошло.


                        У меня почти то же самое + пара плюшек с быстрым поиском GET и субдоменов.

                        А я для этого отдельный TARGET-модуль с tcp-session-hijack и отсылкой синтезированного HTTP 302 сделал.
                        Но мути с этим было — огого. Кстати, вместо DROP, возможно правильнее вам было использовать -j REJECT with-tcp tcp-reset, до написания своего модуля я его использовал. Не совсем логично, но по крайней мере загрузка страницы у пользователя не висит.

                        Вот пока что думаю как лучше с HTTPS поступать, в идеале на маршрутизаторе DNAT трафика идущего на IP адреса на сервер с заглушкой сделать, наверное, но я DPI не на самом маршрутизаторе держу, а сканирую зеркало трафика с коммутатора.

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

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