Журнал Системный Администратор, Август 2003

Журнал Системный Администратор

Август 2003

Цена: $4.5 US

  Подписаться

Зарегистриванные пользователи, пожалуйста следуйте этой ссылке

Версия для печати Вернуться к оглавлению

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 определена в заголовочном файле <linux/netfilter.h>.

Рассмотрим ее:

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;

};

 

Основные поля структуры:

n  nf_hookfn *hook – ловушка, т.е. функция, которая будет вызвана для обработки (анализа) пакета. Именно эта функция решает, что сделать с пакетом – отбросить его или принять.

n  int pf – протокол. Для IPv4 это значение равно PF_INET.

n  int hooknum – точка подключения ловушки.

n  int priority – приоритет. К одной точке может быть подключено несколько ловушек. Чтобы установить порядок их вызова, вводится приоритет. Ловушка с самым низким приоритетом первой обработает пакет.

Прототип функции-ловушки также определен в файле <linux/netfilter.h> и выглядит следующим образом:

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 *));

 

Аргументы функции:

n  unsigned int hooknum – точка подключения ловушки (определяется в структуре nf_hook_ops).

n  struct sk_buff **skb – двойной указатель на структуру sk_buff. Данная структура содержит полную информацию о сетевом пакете. Определена в файле <linux/skbuff.h>.

n  const struct net_device *in, *out – информация о входном и выходном интерфейсе.

Перечень возвращаемых функцией значений перечислен в файле <linux/netfilter.h>.

Пример использования NETFILTER

Рассмотрим на простом примере, как использовать NETFILTER. Разработаем модуль ядра, выполняющий следующие действия:

n  перехват и блокирование IP-пакета, адресованного локальному хосту;

n  передачу перехваченного 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;

 

Эта структура будет передана пользовательскому процессу. Назначение полей структуры:

n  __u16 iph_len – длина заголовка IP-пакета;

n  __u32 pkt_len – длина IP-пакета;

n  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\n",(char *)indev);

    printk("Outdev - %s\n",(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,

};

 

Рассмотрим эти функции.

n  Функция открытия устройства:

 

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 и сбрасываем флаг готовности данных.

n  Функция чтения из устройства:

 

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-пакета.

n  Функция закрытия устройства:

 

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.

n  Функции инициализации и выгрузки модуля выполняют стандартную процедуру регистрации и снятия регистрации устройства в системе:

 

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("\nЖдем пакет ... ");

Обнуляем структуры:

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("пакет получен.\n\n");

printf("Длина пакета\t-\t%d\n",data.len);

printf("Длина IP-заголовка\t-\t%d\n",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("\nSource IP\t-\t%s\n",inet_ntoa(ip.saddr));

    printf("Destin. IP\t-\t%s\n",inet_ntoa(ip.daddr));

    printf("Протокол\t-\t%d\n",ip.protocol);

    printf("Длина заголовка\t-\t%d\n",ip.ihl<<2);

    printf("Длина пакета\t-\t%d\n\n",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

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>

Outdev - eth0

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-пакетов и т. д.




Все права зарезервированы. Этот материал принадлежит или лицензирован компании PLARANA INC. Только для частного использования. Любое распространение запрещено без письменного разрешения PLARANA INC
Версия для печати Вернуться к оглавлению
oread