- PVSM.RU - https://www.pvsm.ru -

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

Введение

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

Всегда удивляло то, что люди, более-менее знающие 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 на две виртуальные машины

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

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

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

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

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

http://vault.centos.org/6.4/os/Source/SPackages/ [4]

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

rpm -i http://vault.centos.org/6.4/os/Source/SPackages/kernel-2.6.32-358.el6.src.
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

Сохраняем его и собираем всё ядро.

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 [5]).

Стандартный 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 [6].

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

Здесь опять немного теории – как правило, структура 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 [7]

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

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

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

Завершение

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

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

Автор: weirded

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/news/47582

Ссылки в тексте:

[1] Учимся писать модуль ядра (Netfilter) или Прозрачный прокси для HTTPS: http://habrahabr.ru/post/138328/

[2] «Linux Kernel Hacking — это просто!» или «Где найти документацию?»: http://habrahabr.ru/post/26391/

[3] мозг: http://www.braintools.ru

[4] http://vault.centos.org/6.4/os/Source/SPackages/: http://vault.centos.org/6.4/os/Source/SPackages/

[5] xt_wildstring.h: https://github.com/carbonsoft/wildstring/blob/master/include/linux/netfilter/xt_wildstring.h

[6] xt_wildstring.c: https://github.com/carbonsoft/wildstring/blob/master/xt_wildstring.c

[7] http://www.carbonsoft.ru/products/reductor/carbon-reductor/#scheme: http://www.carbonsoft.ru/products/reductor/carbon-reductor/#scheme

[8] читатель, возможно воскликнет, да что там, заорёт: http://bash.im/quote/403306

[9] разделе opensource на нашем сайте: http://www.carbonsoft.ru/opensource/?utm_source=habrahabr&utm_medium=article&utm_content=wildstring&utm_campaign=habr&utm_url=http://habrahabr.ru/

[10] Источник: http://habrahabr.ru/post/200854/