Введение
Недавно заметил, что на хабре мало информации по разработке модулей ядра. Всё что я нашёл:
- Учимся писать модуль ядра (Netfilter) или Прозрачный прокси для HTTPS
- «Linux Kernel Hacking — это просто!» или «Где найти документацию?»
- Ещё 2-3 статьи
Всегда удивляло то, что люди, более-менее знающие 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 на две виртуальные машины
Собственно чтобы наш
На сборщике получаем исходники 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 -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).
#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 на нашем сайте.
Автор: weirded