Брандмауэр Часть 2
Часть 2
Владимир Мешков
В первой части статьи мы начали изучение одного из вариантов построения
брандмауэра, рассмотрели структуру его основной составляющей – модуля ядра, а
также получили навыки внесения изменений в ядро операционной системы Linux. В
этой части мы рассмотрим две оставшиеся составные брандмауэра – процесс-демон и
программу инициализации и запуска процесса-демона.
Задача программы инициализации и запуска процесса демона – принять исходные
данные (правила фильтрации) и запустить на выполнение процесс-демон, передав
ему эти правила. В нашем примере правилами фильтрации является IP-адрес хоста,
чьи пакеты мы будем блокировать.
Процесс-демон после активизации передает модулю
ядра правила фильтрации и в дальнейшем занимается ведением log-файла, в котором
фиксируется время запуска/останова демона и попытки доступа с запрещенного
адреса.
Теперь давайте детально рассмотрим каждую
составляющую.
Программа инициализации и
запуска процесса-демона
Нижеприведенный программный код разместим в файле sfc.c. Здесь будет
находиться главная функция main().
Рассмотрение программы начнем с определения
заголовочных файлов и переменных.
Нам понадобятся следующие header-файлы:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "sfc.h"
В файле sfc.h определено имя файла, в котором
хранится идентификатор процесса-демона (PID-файл). Файл имеет следующее
содержание:
#define PID "daemon.pid"
Идентификатор процесса-демона определим как
глобальную переменную:
static pid_t pid;
А теперь распишем главную функцию main().
int main (int argc, char *argv[])
{
void usage():
Это прототип функции для обработки неправильного
ввода параметров. Данная функция имеет следующий вид:
void usage()
{
fprintf(stderr,"\nUsage:
daemon [ start / stop ]\n\n");
return;
}
Программа при запуске принимает
один параметр, определяющий режим ее работы:
n start – запустить процесс-демон на выполнение;
n stop – завершить выполнение процесса-демона.
Для работы нам понадобятся
переменные:
n int pid_file – дескриптор файла для хранения
идентификатора демона;
n struct stat s – структура для хранения
атрибутов файла.
Проверяем правильность ввода входных параметров:
if(argc!=2) {
usage();
return (-1);
}
Если входной параметр указан, определяем, какой
режим работы задан. Их, как мы уже сказали, два.
Режим запуска процесса-демона на
выполнение
if(!(strcmp(argv[1],"start")))
{
Во избежание повторного запуска проверяем наличие
в текущем каталоге PID-файла. Если файл присутствует, то демон уже запущен, о
чем пользователь получает уведомление:
if(stat(PID,&s)==0) {
fprintf(stderr,"\nDaemon
is allready running !\n\n");
return (-1);
}
Инициализируем демон:
init_daemon();
Функция инициализации будет определена ниже.
Демон запустим как дочерний процесс.
pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid==0) {
Отсоединяемся от терминала:
setsid();
Стартуем демон:
start_daemon();
exit(1);
}
Родительский процесс создает PID-файл и
записывает в него идентификатор процесса-демона:
pid_file=open(PID,O_CREAT|O_TRUNC|O_RDWR,0644);
if(pid_file < 0) {
perror(PID);
return (-1);
}
if(write(pid_file,(char
*)&pid,sizeof(pid_t)) < 0) {
perror(PID);
return (-1);
}
close(pid_file);
}
Режим остановки выполнения
процесса-демона
if(!(strcmp(argv[1],"stop")))
{
Для остановки процесса-демона необходимо получить
значение его идентификатора.
Это значение извлекаем из PID-файла:
pid_file=open(PID,O_RDONLY);
if(pid_file<0) {
perror(PID);
return (-1);
}
if(read(pid_file,(char
*)&pid,sizeof(pid_t)) < 0) {
perror(PID);
return (-1);
}
close(pid_file);
PID-файл нам больше не нужен,
удаляем его:
if(unlink(PID) < 0) {
perror(PID);
return (-1);
}
Теперь останавливаем процесс-демон, послав ему
сигнал SIGINT:
kill(pid,SIGINT);
}
На этом функция main() завершается:
return (0);
}
Процесс-демон
Весь код, отвечающий за запуск, функционирование и остановку
процесса-демона, разместим в файле sf_daemon.c. По сути, этот файл будет
представлять собой набор функций.
Заголовочные файлы и переменные
Вначале, как всегда, определимся с заголовочными файлами и переменными. Нам
понадобятся:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <time.h>
#include <signal.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/in.h>
#include "sf_daemon.h"
Файл sf_daemon.h имеет следующее содержание:
#include <linux/types.h>
#include <linux/ip.h>
Имя log-файла:
#define LOG "/var/log/daemon"
struct data_log {
__u32 addr;
int action;
int ready;
};
В этом файле определено имя
log-файла и структура data_log, в которой хранятся данные для заполнения
log-файла. Назначение полей структуры следующее:
n __u32 – IP-адрес хоста (в сетевом формате), от
которого поступил пакет;
n int action – выполняемое действие (1 –
разрешить прохождение пакета, 0 – отбросить пакет);
n int ready – флаг готовности данных в устройстве
для считывания.
Поскольку наш демон работает с двумя файлами
(файл устройства /dev/firewall и log-файл), то необходимо определить две
переменные для хранения дескрипторов этих файлов:
int fddev=0; - дескриптор
файла устройства;
int f; - дескриптор
log-файла.
Функции
Первая функция, которую мы рассмотрим, останавливает выполнение процесса-демона.
Вот что она из себя представляет:
void stop_daemon()
{
close(fddev);
stop_log(f);
exit(0);
}
Функция закрывает устройство, завершает ведение
log-файла и осуществляет выход из программы.
Следующую функцию можно назвать центральной
частью процесса-демона. Эта функция осуществляет непосредственный обмен данными
с модулем ядра и заполняет log-файл. Главной особенностью данной функции
является выполнение в бесконечном цикле, который прерывается только при
поступлении сигнала SIGINT.
void packet_loop(void)
{
Структура для информационного обмена с модулем:
struct data_log data;
Размер блока данных, считанного из модуля:
int count;
Запускаем цикл:
for (;;) {
Считываем из модуля данные, в случае ошибки
завершаем выполнение:
count=read(fddev,(char
*)&data,sizeof(struct data_log));
if(count<0) stop_daemon();
Если установлен флаг готовности данных для
считывания и поступил пакет с запрещенного адреса, фиксируем это событие в
log-файле:
if(data.ready==1) {
if(data.action==0)
{
if(fill_log(f,data.action,data.addr)
< 0)
stop_daemon();
}
}
}
}
Заполнением log-файла ведает функция fill_log, к
ней мы еще вернемся.
Теперь подошла очередь функции инициализации.
Напомню, что ее задача – передать модулю ядра правила фильтрации (т.е.
IP-адрес).
void init_daemon()
{
int err;
struct iphdr ip_pack;
В структуре ip_pack, в поле saddr (адрес
источника), будет находится запрещенный IP-адрес.
Обнулим эту структуру:
memset(&ip_pack,0,sizeof(struct
iphdr));
и заполним поле адреса источника:
ip_pack.saddr=inet_addr("192.168.1.10");
Подготовим к работе log-файл. Если log-файл
отсутствует, создаем его:
f=open(LOG,O_CREAT|O_APPEND|O_RDWR,0644);
if(f<0) {
perror(«open log»);
exit(0);
}
Теперь передадим модулю правила фильтрации.
Открываем устройство в режиме чтения/записи:
fddev=open("/dev/firewall",O_RDWR);
if(fddev<0) {
perror("firewall");
exit(0);
}
Записываем в него структуру ip_pack:
err=write(fddev,&ip_pack,sizeof(struct
iphdr));
if(err<0) {
perror("firewall");
stop_daemon();
}
Итак, IP-пакеты, поступившие с хоста с адресом
192.168.1.10, будут заблокированы на входе нашей системы.
Выходим из функции:
return;
}
Если вам не понравилось, что IP-адрес введен
непосредственно в исходный текст, то можете усовершенствовать код, считывая
адрес из файла или из командной строки.
Теперь рассмотрим функцию, которая осуществляет
запуск демона на выполнение.
void start_daemon()
{
Демон должен реагировать только на один сигнал –
SIGINT. При получении этого сигнала демон завершает выполнение. Все остальные
сигналы необходимо заблокировать.
Определим переменные:
sigset_t mask;
static struct sigaction act;
Создадим полный набор сигналов, исключив из него
SIGINT:
sigfillset(&mask);
sigdelset(&mask,SIGINT);
Блокируем все сигналы:
sigprocmask(SIG_SETMASK,&mask,NULL);
Определяем новый обработчик для SIGINT:
act.sa_handler=stop_daemon;
sigaction(SIGINT,&act,NULL);
А теперь стартуем:
start_log(f);
packet_loop();
exit(1);
}
LOG-файл
Нам осталось рассмотреть функции для ведения log-файла. Их три:
n start_log – запись о начале выполнения
процесса-демона;
n fill_log – запись информации о блокировании
IP-пакета;
n stop_log – запись об остановке выполнения
процесса-демона.
Каждая из этих функций фиксирует текущее время
возникновения того или иного события.
Все три функции разместим в файле sf_log.c.
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <sys/socket.h>
#include <linux/in.h>
#define BSIZE 80
Все функции принимают в качестве аргумента
дескриптор log-файла, в который будет осуществляться запись информации. В
случае удачного завершения операции все функции возвращают 0, в случае ошибки
-1.
Функция start_log
int start_log(int f)
{
char buf[BSIZE];
time_t start_t;
Обнулим буфер и получим текущее время:
bzero(buf,BSIZE);
time(&start_t);
Формируем буфер и записываем его в log-файл:
sprintf(buf,"Daemon started
at %s", ctime(&start_t));
if(write(f,buf,strlen(buf))
< 0) return (-1);
return (0);
}
Функция stop_log
Функции start_log и stop_log практически не отличаются друг от друга, кроме
имен переменных, поэтому привожу код без комментариев:
int stop_log(int f)
{
char buf[BSIZE];
time_t stop_t;
bzero(buf,BSIZE);
time(&stop_t);
sprintf(buf,»Daemon stoped
at %s», ctime(&stop_t));
if(write(f,buf,strlen(buf))
< 0) return (-1);
close(f);
return (0);
}
Функция fill_log
Эта функция, кроме дескриптора log-файла,
принимает IP-адрес пакета, который был заблокирован (u_long addr), и
идентификатор выполненного действия (int action). Функция очень простая, и
необходимости в комментариях я не вижу.
int fill_log(int f, int action,
u_long addr)
{
char buf[BSIZE];
time_t fill_t;
bzero(buf,BSIZE);
time(&fill_t);
if(action==0) {
sprintf(buf,"Packet
from %s was rejected at %s",inet_ntoa(addr), ctime(&fill_t));
if (write(f,buf,strlen(buf))
< 0)
return (-1);
return (0);
}
}
Makefile
Для сборки исполняемого модуля создадим Makefile следующего содержания:
CC = gcc
name = daemon
DAEMON = sfc.o sf_daemon.o sf_log.o
$(name): $(DAEMON)
$(CC) -g -o $(name)
$(DAEMON)
sfc.o: sfc.c
$(CC) -c sfc.c
sf_daemon.o: sf_daemon.c
$(CC) -c sf_daemon.c
sf_log.o: sf_log.c
$(CC) -c sf_log.c
clean:
rm -f *.o
Здесь все должно быть вам знакомо. Ключ «-g» при
успешной сборке можно будет заменить на «-s».
Запуск и остановка выполнения
процесса-демона
После сборки в текущем каталоге появится исполняемый файл daemon. Для его
запуска наберите команду:
./daemon start
Перед запуском процесса-демона необходимо
загрузить модуль ядра.
После запуска демона в текущем каталоге появится
файл daemon.pid. Не удаляйте этот файл! В нем хранится идентификатор
процесса-демона для возможности его корректной остановки. Для остановки
выполнения процесса-демона введите команду:
./daemon stop
Файл daemon.pid автоматически удаляется.
Информация о времени запуска и останова
процесса-демона, а также о заблокированных пакетах будет зафиксирована в файле
/var/log/daemon.
При подготовке статьи были использованы исходные
тексты и документация брандмауэра SINUS (http://www.ifi.unizh.ch).
Все права зарезервированы. Этот материал принадлежит или лицензирован компании PLARANA INC.
Только для частного использования. Любое распространение запрещено без письменного разрешения PLARANA INC
|