Введение
Недавно заметил, что на хабре мало информации по разработке модулей ядра. Всё что я нашёл:
- Учимся писать модуль ядра (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 и потратим много времени на перезагрузку, если паника произойдёт на нашей рабочей машине. Поэтому разработка, как правило, ведётся в виртуальной машине, либо на отдельном стенде, в случае если код пишется под специфичное железо. Однако наш модуль универсален, так что ставим виртуалки.
Собственно чтобы наш мозг не простаивал во время Kernel Panic при неудачах, а они гарантированно будут, поступим следующим образом. Установим две виртуальные машины, у которых будет доступ в Интернет и друг к другу. Одна будет сборщиком модуля, а вторая стендом для проверки.
Кстати, на сборщике нам потребуется несколько хороших и полезных программ.
Теперь добавляем себе в закладки один из самых полезных репозиториев для разрабатывающего под CentOS человека:
http://vault.centos.org/6.4/os/Source/SPackages/
Отсюда мы будем брать src.rpm ядра Linux и Iptables.
Затем идём в /root/rpmbuild/SPECS/ и разворачиваем исходники с наложением патчей от CentOS.
В /root/rpmbuild/BUILD/ у нас появятся папки с исходниками ядра Linux и iptables.
Теперь надо хотя бы один раз собрать ядро целиком, чтобы иметь возможность пересобирать только папку net/netfilter/ при внесении изменений в наш модуль. Для удобства и привычности сделаем симлинки:
Идём в /usr/src/linux. Для начала сгенерируем конфиг.
Сохраняем его и собираем всё ядро. Кстати, возможно на rpmbuild или make произойдёт зависание на gpg: keyring `./pubring.gpg' created. Чтобы этого избежать, скажем что random у нас — это urandom.
И собственно сборка:
Вообще было бы неплохо исходники модуля хранить всё в GIT-репозитории, у меня он располагается в ~/GIT/wildstring/.
Можно делать это двумя способами, на мой взгляд, наиболее правильный – выставить параметр /proc/sys/kernel/panic в 2. Но вывод паники нам важен, поэтому при необходимости можно воспользоваться скриптом на хост-системе в духе:
Который можно юзать так:
Единоразовый запуск:
Бесконечный цикл:
Готовим систему сборки и отладки
Да, многие мечтают об удобной 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 на нашем сайте.