ВЛАДИМИР МЕШКОВ
Брандмауэр
Часть 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," Usage: daemon [ start / stop ] ");
return;
}
Программа при запуске принимает один параметр, определяющий режим ее работы:
- start – запустить процесс-демон на выполнение;
- stop – завершить выполнение процесса-демона.
Для работы нам понадобятся переменные:
- int pid_file – дескриптор файла для хранения идентификатора демона;
- struct stat s – структура для хранения атрибутов файла.
Проверяем правильность ввода входных параметров:
if(argc!=2) {
usage();
return (-1);
}
Если входной параметр указан, определяем, какой режим работы задан. Их, как мы уже сказали, два.
Режим запуска процесса-демона на выполнение
if(!(strcmp(argv[1],"start"))) {
Во избежание повторного запуска проверяем наличие в текущем каталоге PID-файла. Если файл присутствует, то демон уже запущен, о чем пользователь получает уведомление:
if(stat(PID,&s)==0) {
fprintf(stderr," Daemon is allready running ! ");
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-файла. Назначение полей структуры следующее:
- __u32 – IP-адрес хоста (в сетевом формате), от которого поступил пакет;
- int action – выполняемое действие (1 – разрешить прохождение пакета, 0 – отбросить пакет);
- 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-файла. Их три:
- start_log – запись о начале выполнения процесса-демона;
- fill_log – запись информации о блокировании IP-пакета;
- 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).