Walld - управление Инет-трафиком
 
Исходный код: walld-0.4-nix.tar.gz (2003-03-22 16:23:52/1818/212)

Вступление

Всем привет! Спешу всем сообщить, что я бросил пить кофе. Теперь только чай. Кстати, это повлекло за собой определенные последствия. Например, я увидел необходимость написания некой системы, которая позволяла бы администратору игрового салона открывать и закрывать доступ в Интернет. Если кто не знает, объясню – выражение 'администратор игрового салона' не имеет ничего общего с продвинутыми системными или сетевыми администраторами. Скорее всего, это противоположности. Это значит, что снаружи все должно быть не просто, а очень просто. Тут нажал, там нажал и 'Ok'. Не переживайте, простота снаружи вовсе не гарантирует простоты изнутри. Так что работы предостаточно. Нам придется наконец-то познакомиться с живым компьютерным демоном – в конце-концов мы выясним что это за птица. Кроме того нас ожидает некоторое углубление в системные тонкости, а точнее мы научимся использовать некоторые сигналы и изучим реакцию на них. И еще несколько вкусностей у нас впереди, но это – секрет. Итак, за дело.

Задача

На самом деле все не только в бросании питья кофе. Получил я давеча реальный приказ от главнокомандующего – типа, нужно сделать такую фичу, что бы человек пришел в салон, заплатил бабки и ему открыли доступ в Интернет. Время кончилось – доступ прекращается. Почесав в затылке, через 1,5 дня я вроде сделал что-то. Сейчас смотрю, на то что получилось и кое что мне не совсем нравится. Ну, домашнее задание оставим на потом, а сейчас немного файрвольных тонкостей.

Значит, шлюз наш, трудяга, является дверью в Интернет. На шлюзе Linux Mandrake который собран с поддержкой ipchains – встроенный в ядро файрвол. В принципе, если у вас другой файрвол, не спешите огорчаться – нужно будет исправить лишь те участки кода, в которых происходит добавление и удаление файрвольных правил. То есть нужно что бы те правила, которые вы забиваете ручками, автоматически добавлялись и удалялись с помощью нашей программы. Наш демон при запуске будет читать конфигурационный файл, в котором прописаны адреса хостов, которым нужен доступ по времени. Если при чтении конфига выясняется, что нужно кому-то открыть доступ, демон составляет файрвольное правило, и добавляет его. Как только отведенное время закачивается, демон удаляет правило из цепочки. И так далее, и в том же духе. Хочу сразу заметить, что для правильной работы необходимо настроить файрвол таким образом, что бы по умолчанию пакеты с контролируемых хостов отвергались. У меня пул адресов для игрового салона составляет верхнюю половину fake-подсети (класса С естесно). По умолчанию установлена REJECT-политика для цепочки правил forward.

Вторая часть программы – интерфейс, позволяющий непродвинутому юзеру управлять файрволом. Скрипт просто переписывает конфигурационный файл. В принципе, ничего сложного. В последний момент я дописал анализатор лог-файла, в результате чего была добавлена последняя колонка. В ней показывается суммарное время доступа в часах и минутах. В мыслях еще и создание полноценного анализатора, что бы создавал отчеты по типу webalizer-а, но это, не обессудьте, когда будет не лень.

Демон

Ну что, погнали наши городских. Да не переживайте так. Это даже не демон, а так – подмастерье. Всего то строк двести.
#!/usr/bin/perl –w $pid = fork; exit if pid; die "Couldn't fork!" unless defined($pid); use POSIX; die "Can't start new session: $!" unless POSIX::setsid(); 
Вот так мы создаем демона. В статье Процессы и IPC мы уже рассматривали работу fork. Поэтому сильно на этом внимание заострять не будем. В той же статье я упоминал, что родительский процесс не завершится, пока есть хотя бы один порожденный с общими потоками ввода-вывода. Функция setsid() выполняет отсоединение дочернего потока: она закрывает все общие дескрипторы. Поэтому командная строка нам возвращается сразу после вызова этой функции. Все – демон работает, но завершается естественным путем, так как больше никакого кода не выполняется. Для того, что бы демон оставался в памяти, мы будем использовать самую простую схему – выполнять работу в цикле. Но цикл не должен быть бесконечным, а то будет похоже на работу Windows 8)
$do_work = 1; $lastr = 0; $log_file = ""; $pid_file = ""; %conf = (); $day = (localtime)[3]; $SIG{INT} = $SIG{TERM} = \&UsKill; $SIG{HUP} = \&Recon; while ($do_work){ } sub UsKill{ 	$do_work = undef; } sub Recon{ 	$lastr = 0; } 
Ясно – что дело темное! Тут много непонятных переменных, объявленных по неизвестно каким причинам. Не смотрите на $lastr, $logfile, $pid_file, %conf и $day. Эти переменные мы будем использовать в программе дальше. Для нас здесь интересно то, что мы делаем с хэшем %SIG и переменной $do_work. Как известно, %SIG содержит ссылки на обработчики сигналов. Что такое сигнал? Например, при нажатии ^C во время работы программы, система посылает ей (программе) сигнал INT. Что бы обработать этот сигнал, мы должны переопределить обработчик. По умолчанию, INT, TERM и некоторые другие сигналы безоговорочно завершают работу программы. Согласитесь, это некультурное поведение для демона. А сигнал HUP мы просто обязаны привязать к перечитыванию конфигурационных файлов. Хочется же почувствовать себя немного ближе к таким мастерам, как разработчики Апача и прочих монстров 8)

Дальше, программа организует цикл, условием выполнения которого является инициализированное значение в $do_work. Как только нам придет INT или TERM, мы вывалимся из цикла, сделаем что-нибудь нужное после цикла и завершим работу. Как вам такая идея?

Конфигурационный файл у меня лежит в том же каталоге, что и демон. Поэтому, для чтения добавляем в цикл сразу после открытия следующие строки:

if (-M 'walld.conf' != $lastr){ 	$lastr = -M 'walld.conf'; 	Log('Reloading…'); 	ReloadConf(); } 
Думаю, что объяснять назначение функции Log() не стоит. Функция ReloadConf() выполняет загрузку конфигурационного файла. Теперь, глянем на условие (мне кажется, я неплохо придумал 8). –M 'walld.conf' возвращает время изменения файла в днях. В начале мы инициализировали переменную $lastr значением 0. Естественно, что эти значение не совпадут. Таким образом, инициируется начальная загрузка файла. Мало того, в дальнейшем мы найдем еще одно применение подобного механизма загрузки.

Пишем дальше:

	my ($m,$h,$d) = (localtime)[1,2,3]; 	if ($d != $day){ 		ResetAll(); 		$day = $d; 		Log("Change of date"); 	}else{ 		my @keys = keys(%conf); 		my $save = 0; 		foreach my $ip (@keys){ 			if ($conf{$ip}->{status} eq 'opened' && 				$conf{$ip}->{stopat} =~ /(\d+):(\d+)/{ 				my ($ch,$cm,$end) = ($1 + 0,$2 + 0,0); 				$end = 1 if ($h == $ch && $m >= $cm); 				$end = 1 if ($h > $ch); 				if ($end == 1){ 					Stop($ip); 					$save = 1; 				} 			} 		} 		if ($save == 1){ 			SaveConfig(); 			$lastr = -M 'walld.conf'; 		} 	} 	sleep(30); 
Вот и вся административная работа. Сначала мы получаем минуты, часы и номер дня для последующего сравнения. При каждой смене дня мы сбрасываем все установленные правила, для того, что бы не спутать время прошлого и последующих дней. Можно добавить сюда вызов функции, которая будет анализировать лог-файл за прошедший день. У меня так и сделано, только код функции до сих пор не написан.

Если смена дня не произошла, тогда мы начинаем проверять все доступные адреса на предмет остановки доступа. Нам неизвестна кое-какая деталь, а точнее мы ничего не знаем о хэше %conf. Для простоты, я вынес хэш %conf в отдельный файл – это и есть конфигурационный файл. Каждый элемент хэша – это ссылка на анонимный хэш из трех элементов. В общем, описание одного хоста выглядит так:

$conf{'192.168.1.128'} = { 	name => 'gamer1', 	status = > 'closed', 	stopat => '11:30' }; 
Думаю, тут ничего непонятного нет. Такую структуру может подправить и не совсем опытный администратор (который имеет место быть под моим началом). Кроме того, в конфигурационном файле на всяки случай описаны переменные $log_file, $pid_file и хэш %conf. Именно по этому в самом начале мы лишний раз объявляем переменные с этими именами – что бы Perl не ругался во время запуска.

Так вот, вернемся к нашим овечкам. Переменная $save – это флаг, сигнализирующий о необходимости записать изменения в конфигурационный файл. Ведь не будем же мы его переписывать через каждые 30 секунд (как указано ниже). Далее мы перебираем ключи хэша %conf. Ключи представляют собой IP-адреса хостов, которые нужно контролировать. В условии мы проверяем, разрешен ли доступ с указанного адреса и при этом сразу выполняем разбор указанного времени. Если время указано в неправильном формате, то хост игнорируется. Что ж, такая у него се-ля-ви. После разбора времени мы определяем необходимость закрытия доступа. Устанавливаем флажок $end, если нужно. Если мы остановим доступ хотя бы для одного хоста, то конфиг надо будет переписать, о чем мы и сигнализируем в переменной $save. Далее, ждем 30 секунд и повторяем все сначала. Почему 30? А потому, что у нас точность работы равна одной минуте. В общем, если вам не нравится, можете вообще не ждать – но это будет пустой тратой процессорного времени.

Ах да, еще один момент. После сохранения конфигурационного файла мы устанавливаем значение $lastr, что бы лишний раз не перечитывать конфиг.

Теперь давайте посмотрим, что же у нас после цикла:

ResetAll(); unlink($pid_file); Log("Shutdown daemon"); # ------------------------------------------- 
Как оговаривалось выше, ResetAll() закрывает доступ для всех, что бы не лазили, пока демон спит. Удаляем файл, в котором был записан идентификатор процесса (его создает функция ReloadConf()).

Теперь приступим к расмотрению всяких разных функций, которые мы вызывали из рабочего цикла. Прежде всего, это ReloadConf():

sub ReloadConf{ 	die "Configuration file not found!" unless –r 'walld.conf'; 	do 'walld.conf'; 	Log("Configuration was loaded"); 	my $rules = GetRules(); 	unless (defined($rules)){ 		Log("Fatal: ipchains –L forward –n failed"); 		die; 	} 	if (open(PID,"> $pid_file")){ 		flock(PID,2); 		print PID $$; 		flock(PID,8); 		close(PID); 	}else{ 		Log("Fatal: can't open pid-file"); 		die; 	} 	my @ws = sort(keys(%conf)); 	foreach my $ip (@ws){ 		if ($conf{$ip}->{status} eq 'opened'){ 			unless (SearchRule($rules,$ip)){ 				system("ipchains -A forward -s $ip -j MASQ"); 				Log("To open for $ip"); 			}else{ 				Log("$ip already opened"); 			} 		}else{ 			if (SearchRule($rules,$ip)){ 				system("ipchains -D forward -s $ip -j MASQ"); 				Log("To not open for $ip"); 			}else{ 				Log("$ip already closed"); 			} 		} 	} } 
Обидно, если мы не находим walld.conf. Почему мы подключаем конфигурационный файл с помощью do? А потому что, я посмотрю, как вы вторично будете подключать его с помощью require. Для демона это не подходит. Дело в том, что require заставляет Perl помечать модуль файл как уже загруженный. С одной стороны это хорошо - если у вас куча модулей и во всех есть подключение одного и того же модуля с помощью require, в итоге этот модуль будет загружен лишь раз. Но именно поэтому для перечитывания конфига require нам и не подходит. Вот это первое, что мне не нравится в программе. Просто не хотелось возиться с конфигом, вот и пришлось воспользоваться do.

Далее, в переменную $rules мы записываем ссылку на массив правил для цепочки forward нашего файрвола. Если мы не смогли получить список правил, значит, что-то не так. По этому, с обидой вываливаемся на die. Работу функции GetRules() мы расмотрим, но чуть позже, так как она не играет ключевой роли.

После всей этой дребедени, мы пытаемся записать идентификатор процесса демона в pid-файл. Так просто, на всякий случай. Но если не выходит, опять же сильно обижаемся.

Следующий цикл разберите сами, там нет ничего сложного. Правило добавляется или удаляется, в соответствии со значением status конфигурации хоста. При чем, удаление выполняется, только если правило существует, а добавление - если такого правила не сущестувует. Единственно, замечу про функцию SearchRule() - она возвращает истину, если искомое правило уже определено. Ее мы то же рассмотрим, и то же немного позже. Если вы используете другой файрвол, то эта функция одна из тех, которые нужно изменять в соответствии со спецификой вашего софта. Где менять я думаю понятно. Во всех вызовах system. Кстати, кто заметит здесь недочет (или вернее неэффективный код) тот молодец. Я, может быть, укажу на это в конце статьи, если не забуду.

Далее, код функции ResetAll():

sub ResetAll{ 	Log("To close access for all"); 	my $rules = GetRules(); 	unless (defined($rules)){ 		Log("fatal: ipchains -L forward -n failed"); 		die; 	} 	my @ws = sort(keys(%conf)); 	foreach my $ip (@ws){ 		if ($config{$ip}->{status} eq 'opened'){ 			if (SearchRule($rules,$ip)){ 				system("ipchains -D forward -s $ip -j MASQ"); 				Log("To not open for $ip"); 			}else{ 				Log("$ip already closed"); 			} 		} 	} } 
Работа этой функции похожа на работу ReloadConf() в том месте, где для хостов применяются правила. Только здесь - это удаление правил из цепочки. Если вы используете другое файрвольный софт, то эту функцию то же придется менять - конкретно в вызове system.

И напоследок, самая простая функция Stop():

sub Stop{ 	my ($ip) = @_; 	return unless defined($ip); 	my $rules = GetRules(); 	unless (defined($rules)){ 		Log("Fatal: ipchains -L forward -n failed"); 		die; 	} 	if (SearchRule($rules,$ip)){ 		system("ipchains -D forward -s $ip -j MASQ"); 		Log("$ip was denied access"); 		$conf{$ip}->{status} = 'closed'; 	} } 
Эта функция удаляет правило для конкретного хоста.

Теперь функция перезаписи конфигурационного файла:

sub SaveConfig{ 	unless (open(CONF,">walld.conf")){ 		Log("Cannot open config file"); 		die; 	} 	flock(CONF,2); 	print CONF "\$log_file = '$log_file'; \$pid_file = '$pid_file'; "; 	foreach my $key (sort(keys(%conf))){ 		print CONF "\$conf{'$key'} = { 	name => '$conf{$key}->{name}', 	status => '$conf{$key}->{status}', 	stopat => '$conf{$key}->{stopat}' }; "; 	} print CONF "1;"; flock(CONF,8); close(CONF); } 
Эта функция переписывает конфигурационный файл в соответствии с содержимым %conf и еще нескольких переменных.

Функция Log() у меня выглядит следующим образом:

sub Log{ 	return unless defined($_[0]); 	return unless open(LOG,">> $log_file"); 	flock(LOG,2); 	print LOG scalar(localtime),": ",$_[0],"\n"; 	flock(LOG,8); 	close(LOG); } 
Не совсем профессионально, конечно, но это не ключевой момент программы, поэтому можете переписать эту функцию как вам угодно. Хочу только заметить, что скрипт, который генерирует форму для управления демоном, анализирует лог-файл с целью подсчета общего использованного времени, так что, если формат файла будет отличаться от моего, вам придется многое изменить в скрипте.

У нас осталось всего две функции GetRules() и SearchRule(). начнем расмотрение с первой из них:

sub GetRules{ 	my (@rules); 	return undef unless open(READ,"ipchains -L forward -n |"); 	while (<READ>){ 		chomp; 		push(@rules,$_); 	} close(READ); 	return \@rules; } 
Эта функция получает список правил для цепочки правил forward. Если вы используете другой файрвол, код этой функции нужно будет изменить в вызове system.

И, наконец, последняя функция SearchRule():

sub SearchRule{ 	my ($rules,$ip) = @_; 	return undef unless defined($rules); 	return undef unless defined($ip); 	for (my $i = 0; $i <= $#{$rules}; $i ++){ 		if (${$rules}[$i] =~ /MASQ(.+?)$ip\s+0\.0\.0\.0\/0/){ 			return $i; 		} 	} 	return undef; } 
Формат информации, возвращенной функцией GetRules() непосредственно зависит от типа программного обеспечения. Возможно, вам придется исправить регулярное выражение, которое выполняет опознание правила.

Ну, вот и все, код демона завершен. Осталось только проверить его работу. Создаем конфигурационный файл walld.conf и описываем там пару хостов. Не забываем указать путь к pid- и log-файлам. демон запускается так:

[root@avalon inet]# ./walld 
После этого выполните
[root@avalon inet]# ps -awx | more 
и найдите процесс 'perl -w ./walld'. Запомните его идентификатор (или можно подсмотреть в pid-файле). Перечитать конфиг можно послав демону сигнал HUP. Например, если pid демона 4418
[root@avalon inet]# kill -HUP 4418 
Или можно изменить конфигурационный файл. В следующей итерации цикла, демон определит, что изменилось время изменения конфигурационного файла и перечитает конфиг. После всего этого можно просмотреть лог-файл. Ну и напоследок, убиваем демона следующим образом:
[root@avalon inet]# kill 4418 
После этого в лог-файле появится запись Sutdown daemon, что будет сигнализировать о корректном завершении процесса.

Так как в своей работе мы используем ipchains - программу, доступную только привелегированным пользователям, соответственно и демон должен запускаться с правами привелегированного пользователя. Отсутствие связей с внешним миром довольно высоко поднимает планку безопасности программы. Так как немаловажное значение имеет конфигурационный файл, то нужно принять соответствующие меры для его защиты. Если до этого файла доберется злоумышленник, то он сможет открыть доступ любому хосту изнутри локальной подсети по любым протоколам. Если вы допускаете возможность использования демона злоумышленниками, советую ужесточить файрвольные правила, генерируемые нашей программой - указать протокол и интерфейс. Обязательно прикройте файрволом входящие Интернет-соединения.

 
Автор: Whirlwind
 
Оригинал статьи: http://woweb.ru/publ/58-1-0-430