Рубрика:
Сети /
Сети
|
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|
ВЛАДИМИР МЕШКОВ
NETFILTER
NETFILTER – это новый механизм фильтрации сетевых пакетов, появившийся в составе ядра Linux версий 2.4. Данный механизм позволяет отслеживать прохождение пакетов по стеку IP-протокола и при необходимости перехватить, модифицировать, блокировать любой пакет. На базе NETFILTER построен iptables – пакетный фильтр, широко использующийся при построении межсетевых экранов.
Для включения NETFILTER в состав ядра необходимо в конфигурационном файле установить опцию CON-FIG_NETFILTER = y и пересобрать ядро. После этого все пакеты, проходящие по стеку IPv4-протокола, будут обработаны NETFILTER. Рассмотрим для примера главную приемную функцию IPv4-протокола ip_rcv (файл ip_input.c). Найдем в ней следующий код:
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);
Обращение к NETFILTER выполняет макрос NF_HOOK. В вызове макроса указаны протокол (PF_INET), точка перехвата (NF_IP_PRE_ROUTING), поступивший пакет (структура skb), информация о входном и выходном интерфейсах (структура dev и NULL, соответственно). В точке перехвата пакет попадает в ловушку – так называется функция, которая на основе анализа адресной информации решает судьбу пакета: либо он пройдет дальше, либо будет уничтожен. Последний аргумент – функция, которая будет вызвана для дальнейшей обработки поступившего пакета. Эта функция будет вызвана только в том случае, если NETFILTER пропустит пакет.
В случае если NETFILTER в состав ядра не включен (опция CONFIG_NETFILTER = n) или ловушка не установлена, макрос сразу вызовет функцию ip_rcv_finish для дальнейшей обработки пакета.
Цель данной статьи – рассмотреть возможность применения NETFILTER при написании собственных модулей фильтрации сетевого трафика.
Точки перехвата
Рассмотрим схему прохождения пакета по стеку IPv4-протокола (рис.1). Поступивший на сетевой интерфейс пакет попадает в точку PRE_ROUTING. Если пакет адресован локальному хосту, ядро передает его для обработки локальному процессу (точка LOCAL_IN). Если пакет транзитный, из точки PRE_ROUTING он попадает в точку FORWARD и из нее двигается дальше в точку POST_ROUTING. В этой точке объединяются в один поток исходящие пакеты, сформированные локальными процессами (LOCAL_OUT), и транзитные пакеты, поступившие из точки FORWARD.
Рисунок 1. Схема прохождения пакета по стеку Ipv4-протокола
При помощи NETFILTER можно перехватить пакет в любой из этих точек. С этой целью к точке перехвата подключается ловушка (hook).
Если мы хотим отслеживать все пакеты, поступающие на хост (включая транзитные), мы должны подключиться к точке PRE_ROUTING.
Если нас интересуют пакеты, адресованные непосредственно нашему (локальному) хосту, то необходимо подключиться к точке LOCAL_IN и т.д.
Регистрация ловушки
Перед подключением ловушку необходимо зарегистрировать. Это осуществляется путем заполнения структуры nf_hook_ops и вызова функции регистрации ловушки nf_register_hook(). Аргументом этой функции является адрес структуры nf_hook_ops. Структура nf_hook_ops определена в заголовочном файле .
Рассмотрим ее:
struct nf_hook_ops { struct list_head list;
/* User fills in from here down. */ nf_hookfn *hook; int pf; int hooknum;
/* Hooks are ordered in ascending priority. */ int priority; };
Основные поля структуры:
- nf_hookfn *hook – ловушка, т.е. функция, которая будет вызвана для обработки (анализа) пакета. Именно эта функция решает, что сделать с пакетом – отбросить его или принять.
- int pf – протокол. Для IPv4 это значение равно PF_INET.
- int hooknum – точка подключения ловушки.
- int priority – приоритет. К одной точке может быть подключено несколько ловушек. Чтобы установить порядок их вызова, вводится приоритет. Ловушка с самым низким приоритетом первой обработает пакет.
Прототип функции-ловушки также определен в файле и выглядит следующим образом:
typedef unsigned int nf_hookfn(unsigned int hooknum, struct sk_buff **skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *));
Аргументы функции:
- unsigned int hooknum – точка подключения ловушки (определяется в структуре nf_hook_ops).
- struct sk_buff **skb – двойной указатель на структуру sk_buff. Данная структура содержит полную информацию о сетевом пакете. Определена в файле .
- const struct net_device *in, *out – информация о входном и выходном интерфейсе.
Перечень возвращаемых функцией значений перечислен в файле .
Пример использования NETFILTER
Рассмотрим на простом примере, как использовать NETFILTER. Разработаем модуль ядра, выполняющий следующие действия:
- перехват и блокирование IP-пакета, адресованного локальному хосту;
- передачу перехваченного IP-пакета пользовательскому процессу.
Пользовательский процесс, приняв пакет, отображает данные о нем, такие как IP-адреса отправителя и получателя, длину заголовка, длину всего пакета, и сбрасывает содержимое пакета в файл.
Ловушка активизируется в момент открытия устройства пользовательским процессом. При закрытии устройства ловушка отключается.
Модуль
Модуль является символьным устройством. Создадим для него файл устройства командой:
mknod /dev/nf_ip c 76 0
Заголовочные файлы и переменные:
#include <linux/config.h> #include <linux/module.h> #include <linux/netfilter_ipv4.h> #include <linux/ip.h> #include <linux/slab.h> #include <linux/fs.h> #include <linux/types.h> #include <asm/uaccess.h>
Приняв сетевой пакет, модуль заполняет информационную структуру следующего содержания:
struct ip_pkt { __u16 iph_len; __u32 pkt_len; char buff[65536]; } *pkt;
Эта структура будет передана пользовательскому процессу. Назначение полей структуры:
- __u16 iph_len – длина заголовка IP-пакета;
- __u32 pkt_len – длина IP-пакета;
- char buff[65536] – содержимое IP-пакета (заголовок + данные).
Размер буфера buff равен максимальной длине пакета протокола IPv4.
Структура заголовка IP-пакета:
struct iphdr *iph;
Флаг готовности данных для считывания:
int pkt_ready;
Функция-ловушка:
static unsigned int our_hook ( unsigned int hook, struct sk_buff **pskb, const struct net_device *indev, const struct net_device *outdev, int (*okfn)(struct sk_buff *)) {
iph = (*pskb)->nh.iph; pkt->iph_len = iph->ihl<<2; pkt->pkt_len = (*pskb)->len;
memset((*pskb)->data+pkt->iph_len,0,((*pskb)->len)-(pkt->iph_len)); memcpy(pkt->buff,(*pskb)->data,(*pskb)->len);
printk("Indev - %s ",(char *)indev); printk("Outdev - %s ",(char *)outdev);
pkt_ready = 1; return NF_DROP;
}
Аргументы функции были перечислены выше. Функция заполняет поля структуры pkt данными о перехваченном пакете. Эти данные содержатся в структуре struct sk_buff **pskb. Объединение nh данной структуры содержит заголовок сетевого уровня (network layer header), поле len – длину IP-пакета, поле data – содержимое пакета. Этими значениями заполняется структура pkt, причем поле данных IP-пакета обнуляется. После этого устанавливается флаг готовности данных для считывания и ядру дается команда блокировать дальнейшее прохождение данного пакета (return NF_DROP).
Заполним структуру struct nf_hook_ops:
static struct nf_hook_ops our_ops = { {NULL,NULL}, our_hook, PF_INET, NF_IP_LOCAL_IN, NF_IP_PRI_FILTER-1 };
Ловушка подключается к точке LOCAL_IN, на что указывает значение NF_IP_LOCAL_IN поля hooknum структуры nf_hook_ops. Следовательно, перехвачен и блокирован будет пакет, адресованный локальному хосту.
Теперь определимся с функциями устройства (модуля). Пользовательский процесс должен открыть его, прочитать данные и закрыть. Следовательно, структура file_operations для данного устройства выглядит следующим образом:
struct file_operations nf_fops = { read: read_pkt, open: open_pkt, release: close_pkt, };
Рассмотрим эти функции.
- Функция открытия устройства:
static int open_pkt(struct inode *inode, struct file *file) { if(MOD_IN_USE) return -EBUSY; if(MINOR(inode->i_rdev) != 0) return -ENODEV; if((file->f_mode) != 1) return -EBUSY;
pkt=(struct ip_pkt *)kmalloc(sizeof(struct ip_pkt),GFP_ATOMIC); nf_register_hook(&our_ops); pkt_ready = 0; MOD_INC_USE_COUNT; return 0; }
При открытии устройства выделяем память для структуры pkt, регистрируем ловушку вызовом функции nf_register_hook и сбрасываем флаг готовности данных.
- Функция чтения из устройства:
static ssize_t read_pkt(struct file *file, char *buf, size_t count, loff_t *ppos) { if(pkt_ready) { copy_to_user(buf,pkt,sizeof(struct ip_pkt)); count = pkt->pkt_len; file->f_pos += count; pkt_ready = 0; return count; } return 0; }
Если флаг pkt_ready установлен, блок данных (структура pkt) копируется в адресное пространство пользовательского процесса. После этого флаг pkt_ready сбрасывается. Функция возвращает длину принятого IP-пакета.
- Функция закрытия устройства:
static int close_pkt(struct inode *inode, struct file *file) { kfree(pkt); nf_unregister_hook(&our_ops); MOD_DEC_USE_COUNT; return 0; }
При закрытии устройства освобождается память, выделенная для структуры pkt и ловушка отключается путем вызова функции nf_unregister_hook. Аргументом этой функции является адрес структуры struct nf_hook_ops.
- Функции инициализации и выгрузки модуля выполняют стандартную процедуру регистрации и снятия регистрации устройства в системе:
int init_module(void) { if (register_chrdev(76,"nf_ip",&nf_fops)) return -EIO; return 0; }
void cleanup_module(void) { if(MOD_IN_USE) return; unregister_chrdev(76,"nf_ip"); return; }
Приведенный выше код сохраним в файле netf.c. Для получения загружаемого модуля ядра создадим Makefile следующего содержания:
CC = gcc CFLAGS = -O2 -Wall LINUX = /usr/src/linux MODFLAGS = -D__KERNEL__ -DMODULE -I$(LINUX)/include
netf.o: netf.c $(CC) $(CFLAGS) $(MODFLAGS) -c netf.c
Теперь рассмотрим пользовательский процесс.
Пользовательский процесс
Пользовательский процесс после запуска открывает файл устройства и считывает из него IP-пакет, отображает данные о нем и сбрасывает содержимое пакета в файл data.file.
Заголовочные файлы:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <linux/ip.h>
int main () {
Структура, содержащая информацию о принятом пакете:
struct data_pkt { u_short iph_len; u_long len; char buff[65536]; } data;
Структура, описывающая заголовок IP-пакета:
struct iphdr ip;
int count=0; int fddev=0; int d;
puts(" Ждем пакет ... ");
Обнуляем структуры:
memset(&data,0,sizeof(struct data_pkt)); memset(&ip,0,sizeof(struct iphdr));
Открываем устройство:
fddev=open("/dev/nf_ip",O_RDONLY); if(fddev<0) { perror("nf_ip"); exit(0); }
Запускаем цикл ожидания IP-пакета:
for(;;) {
count=read(fddev,(char *)&data,sizeof(struct data_pkt));
if(count < 0) { perror("count"); return (-1); } if(count == 0) continue; if(count > 0) { close(fddev); break; } }
Как только приходит пакет, закрываем устройство и выходим из цикла.
Информируем о приходе пакета и отображаем данные о нем:
printf("пакет получен. "); printf("Длина пакета - %d ",data.len); printf("Длина IP-заголовка - %d ",data.iph_len);
Запишем в файл содержимое полученного пакета (поле buff структуры struct data_pkt):
d=open("data.file",O_CREAT|O_TRUNC|O_RDWR,0600); if(!d) { perror("data.file"); return (-1); }
if(!(write(d,data.buff,data.len))) { perror("data.file"); return (-1); }
close(d);
Первые (data.iph_len) байт массива data.buff – это заголовок принятого IP-пакета. Скопируем его в структуру struct iphdr и отобразим данные:
memcpy(&ip,data.buff,data.iph_len); printf(" Source IP - %s ",inet_ntoa(ip.saddr)); printf("Destin. IP - %s ",inet_ntoa(ip.daddr)); printf("Протокол - %d ",ip.protocol); printf("Длина заголовка - %d ",ip.ihl<<2); printf("Длина пакета - %d ",ntohs(ip.tot_len));
return (0);
}
Приведенный код сохраним в файле pkt_read.c. Получим исполняемый модуль, введя команду:
gcc -o pkt_read pkt_read.c
Теперь проверим, как все это работает. Схема следующая: имеется локальный хост с адресом 223.223.1.3 и удаленный с адресом 223.223.1.10. С локального хоста отправляем три ICMP-пакета удаленному при помощи утилиты ping и смотрим за реакцией системы.
Загружаем модуль (insmod netf.o) и запускаем на выполнение пользовательский процесс (pkt_read).
Удаленному хосту отправляем три ICMP-пакета:
ping -c3 223.223.1.10
Результат работы команды ping:
PING 223.223.1.10 (223.223.1.10): 56 data bytes< br/> Indev - eth0 Outdev - <NULL> 64 bytes from 223.223.1.10: icmp_seq=1 ttl=225 time=0,4 ms 64 bytes from 223.223.1.10: icmp_seq=1 ttl=225 time=0,4 ms --- 223.223.1.10 ping statistics --- 3 packets transmitted, 2 packet received, 33% packet loss |
Как и ожидалось, первый пакет потерян. Наш модуль его заблокировал. Пользовательский процесс выдал следующую информацию об этом пакете:
Ждем пакет ... пакет получен
Длина пакета - 84 Длина IP заголовка - 20 Source IP - 223.223.1.10 Destin. IP - 223.223.1.3 Протокол - 1 Длина заголовка - 20 Длина пакета - 84 |
А теперь посмотрим на содержимое файла data.file. Размер этого файла равен длине пакета (84 байт). Открыв его 16-тиричным редактором, увидим следующее:
00000000 45 00 00 54 | 01 6C 00 00 | FF 01 F8 70 | DF DF 01 0A 00000010 DF DF 01 03 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00 00000020 00 00 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00 00000030 00 00 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00 00000040 00 00 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00 00000050 00 00 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00 |
Первые 20 байт – это заголовок пакета. По смещению 0x0C находятся IP-адреса источника и получателя – DF DF 01 0A (223.223.1.10) и DF DF 01 03 (223.223.1.3). Поле данных IP-пакета обнулено.
Теперь изменим условие задачи – будем перехватывать и блокировать пакеты, исходящие с локального хоста. Для этого переключим ловушку в точку LOCAL_OUT. В структуре our_ops в поле hooknum занесем значение NF_IP_LOCAL_OUT, т.е. структура примет вид:
static struct nf_hook_ops our_ops = { {NULL,NULL}, our_hook, PF_INET, NF_IP_LOCAL_OUT, NF_IP_PRI_FILTER-1 };
Перекомпилируем и загружаем модуль, запускаем пользовательский процесс и удаленному хосту отправляем три ICMP-пакета:
ping -c3 223.223.1.10
Результат работы команды ping:
PING 223.223.1.10 (223.223.1.10): 56 data bytes Indev - <NULL> ping: sendto: Operation not permitted ping: wrote 223.223.1.10 64 chars, ret=-1 64 bytes from 223.223.1.10: icmp_seq=1 ttl=225 time=0,9 ms 64 bytes from 223.223.1.10: icmp_seq=1 ttl=225 time=0,4 ms --- 223.223.1.10 ping statistics --- 3 packets transmitted, 2 packet received, 33% packet loss |
Первый пакет в сеть не попал – был заблокирован модулем. Следующие два прошли беспрепятственно.
Таким образом, при помощи NETFILTER мы получили возможность воздействовать на процесс прохождения пакетов по стеку IPv4-протокола, перехватывать их, блокировать, изменять их содержание. Это может оказаться полезным, если вы решите разработать собственный модуль фильтрации сетевого трафика, учета статистики трафика, шифрования IP-пакетов и т.д.
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|