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
|