Очередной привет из прошлого, на этот раз из 2007 года 🙂 Но работает без какого-либо пригляда по сей день!
Дан почтовый сервер на Postfix под FreeBSD, как и любой его собрат постоянно борющийся со спамом, но т.к. большинство пользователей являются внешними, есть ещё одна специфичная проблема: украденные вирусами у пользователей пароли используются для легитимного с точки зрения сервера отправки спама. Для предотвращения таких ситуаций было написано несколько скриптов выполняющих следующие функции:
— Сбор и сохранение в MySQL информации о входящих SMTP соединениях на основании парзинга логов Postfix
— WEB-интерфейс для удобного просмотра текущей ситуации и зачатками статистики
— Уведомление на email в случае превышения лимита соединений с одного IP-адреса
— Автоматическая блокировка IP-адреса при превышении лимита через pf
Почему был избран такой путь, а не использованы более-менее стандартные инструменты наподобие
1. Сбор информации
Таблица MySQL:
CREATE TABLE `log` ( `date` date default NULL, `time` time default NULL, `srcip` varchar(15) default NULL, `dstip` varchar(15) default NULL, `srcaddr` varchar(50) default '', `dstaddr` varchar(50) default NULL ) ENGINE=MyISAM DEFAULT CHARSET=latin1; |
А где индексы? Сейчас я бы добавил 🙂
Скрипт-парзер:
Выполняется в 05 и 35 минут каждого часа:
5,35 * * * * /opt/mstat_postfix/mstat_postfix.pl |
#!/usr/bin/perl use DBI; use Time::localtime; use Fcntl qw(:DEFAULT :flock O_RDWR O_CREAT); use Date::Manip; test_unique(); $logfile = '/var/log/maillog'; $ltime_file = '/opt/mstat_postfix/ltime'; $database = 'mstat_postfix'; $dbuser = 'DBUSER'; $dbpassword = 'DBPASSWORD'; $tm = localtime; $year = sprintf("%04d",$tm->year+1900); $dbh = DBI->connect("DBI:mysql:${database}", $dbuser, $dbpassword) or die $DBI::errstr; my %rec, $ltime; open(FLTIME, $ltime_file); $ltime = <FLTIME>; close(FLTIME); open(MAIL, $logfile); while ($line = <MAIL>) { my ($month, $day, $time, $hostname, $servicename, $id, $message) = split /\s+/, $line, 7; if ($id =~ /([a-z0-9]+)\:/i) { $id = $1; $rec{$id} = {} unless ($rec{$id}); if ($message =~ 'removed') { $rec{$id}->{'removed'}++; } else { while ($message =~ /(client|size|from|to)=(\S+?)(\s|,)/g) { if ($1 eq 'client') { $rec{$id}->{'month'} = sprintf "%s", $month; $rec{$id}->{'day'} = sprintf "%d", $day; $rec{$id}->{'time_sql'} = sprintf "%s", $time; $cur_date = $year.'-'.$rec{$id}->{'month'}.'-'.$rec{$id}->{'day'}.' '.$rec{$id}->{'time_sql'}; $date = ParseDate($cur_date); $date_sql = substr($date,0,4).'-'.substr($date,4,2).'-'.substr($date,6,2); $rec{$id}->{'date_sql'} = $date_sql; $rec{$id}->{'time'} = UnixDate($date,"%s"); } $rec{$id}->{$1} = $2; } } } } close(MAIL); foreach my $id (sort { $rec{$a}->{'time'} cmp $rec{$b}->{'time'} } keys %rec) { $rec{$id}->{'client'} =~ s/(.+)\[(\d+\.\d+\.\d+\.\d+)\]/$2/; $rec{$id}->{'from'} =~ s/<(.+)>/$1/; $rec{$id}->{'to'} =~ s/<(.+)>/$1/; $rec{$id}->{'from'} =~ s/'/#/g; $rec{$id}->{'to'} =~ s/"/#/g;; if ( $rec{$id}->{'removed'} && $rec{$id}->{'client'} ne '127.0.0.1' ) { next if ($rec{$id}->{'time'} <= $ltime); $date_sql = substr($date,0,4).'-'.substr($date,4,2).'-'.substr($date,6,2); $sql = "INSERT INTO `log` (`date`,`time`,`srcip`,`dstip`,`srcaddr`,`dstaddr`) VALUES (\'$rec{$id}->{'date_sql'}\',\'$rec{$id}->{'time_sql'}\',\'$rec{$id}->{'client'}\',\'0.0.0.0\',\'$rec{$id}->{'from'}\',\'$rec{$id}->{'to'}\')"; my $sth = $dbh->prepare( $sql ) or die $DBI::errstr; $sth->execute or die $DBI::errstr; } $ltime_new = $rec{$id}->{'time'}; } open(FLTIME, "> $ltime_file"); print FLTIME $ltime_new; close(FLTIME); $dbh->disconnect; exit(); sub test_unique(){ my @a=split /\//,$0; my $lockfile="/var/run/".$a[$#a]."\.lock-2"; if (-e $lockfile){ if (sysopen(FH, $lockfile, O_WRONLY) && flock(FH, LOCK_EX|LOCK_NB)){ return 0; } else { print "Script $a[$#a] alredy started!!!\n"; exit(); } } sysopen(FH, $lockfile, O_WRONLY|O_CREAT) && flock(FH, LOCK_EX|LOCK_NB) || die $!; return 0; } |
2. WEB-интерфейс
Отдельно не выкладываю, т.к. большой и не интересный, можно скачать внизу вместе со всем остальным.
3. Уведомления и автоматизация
Выполняется в 15 и 45 минут каждого часа:
15,45 * * * * /opt/mstat_postfix/mail.pl |
#!/usr/bin/perl use Net::SMTP; use DBI; use Time::localtime; use Fcntl qw(:DEFAULT :flock O_RDWR O_CREAT); $database = 'mstat_postfix'; $dbuser = 'DBUSER'; $dbpassword = 'DBPASSWORD'; $dbh = DBI->connect("DBI:mysql:${database}", $dbuser, $dbpassword) or die $DBI::errstr; $tm = localtime; $cur_hour = sprintf("%02d",$tm->hour); $cur_date = sprintf("%04d-%02d-%02d",$tm->year+1900,($tm->mon)+1,$tm->mday); $ALARM = 0; $UPDATEpf = 0; $sql = "SELECT hour(`time`) as `ht`, count(`srcip`) as `cs`, `srcip` FROM `log` WHERE ((`date` = \'$cur_date\') AND (`time` LIKE \'$cur_hour%\')) GROUP BY `ht`,`srcip` ORDER BY `cs` DESC;"; my $sth = $dbh->prepare( $sql ) or die $DBI::errstr; $sth->execute or die $DBI::errstr; my ($ht,$cs,$srcip); $rv = $sth->bind_columns(\($ht,$cs,$srcip)); while ($sth->fetch) { if ($cs >= 100){$ALARM = 1;}; if ($cs >= 150){ system("echo $srcip >> /etc/pf.mailblock"); $UPDATEpf = 1; }; }; if ($UPDATEpf eq 1){ system("/sbin/pfctl -f /etc/pf.conf"); }; if ($ALARM eq 1){ $mailfrom = 'from@domain.ru'; $smtppass = 'PASSWORD'; $mailto = 'to_admin@domain.ru'; $smtp = Net::SMTP->new('smtp.domain.ru'); $smtp->auth($mailfrom, $smtppass); $smtp->mail($mailfrom); $smtp->to($mailto); $smtp->data(); $smtp->datasend("From: $mailfrom\n"); $smtp->datasend("To: $mailto\n"); $smtp->datasend("Subject: Posibly SPAM activity on HOSTING server!\n"); $smtp->datasend("\n"); $smtp->datasend("Detected posibly SPAM activity on HOSTING server!\n"); $smtp->datasend("Please visit http://mailstat.domain.ru/mstat/mstat.pl to check.\n"); $smtp->dataend(); $smtp->quit; }; $rc = $dbh->disconnect; exit(); |
Схема работы очень простая: считаем количество соединений с одного IP в текущем часу и если больше лимита (100) отправляем предупреждение на почту, а если больше второго лимита (150), то и пишем IP нарушителя в файл /etc/pf.mailblock, а после обновляем pf.
И в завершении пример конфигурации pf:
########## # Macros # ########## # Interface ext_if = "bge1" ext_ip = "IP1/25" int_if = "bge0" int_ip = "IP2/32" # My services for all tcp_svc = "ftp-data:ftp smtp domain http pop3 https 2000:2100 1055" udp_svc = "domain 1055" # My services for NOT all n_tcp_svc = "ftp-data:ftp smtp domain http pop3 https 2000:2100 953" n_udp_svc = "domain 953" block_out_ports = "{ 8080, 6666, 6667, 6668, 6669, 3264 }" our_ips = "{ IP1, IP2, 192.168.1.1, 192.168.1.2 }" # Tables table <hack> persist file "/etc/pf.block" table <mailblock> persist file "/etc/pf.mailblock" ################### # Runtime Options # ################### set skip on lo0 set state-policy if-bound set optimization conservative set ruleset-optimization basic ######### # Begin # ######### scrub in all ################## # Default policy # ################## block in log on $ext_if pass out quick on $ext_if inet proto tcp from any to $our_ips port $block_out_ports block out quick on $ext_if inet proto tcp from any to any port $block_out_ports pass out keep state ############################ # Blocking spoofed packets # ############################ antispoof quick for $ext_if block in quick from urpf-failed ################### # Incoming policy # ################### # Block hack block in quick from <hack> to any block in quick from <mailblock> to any # ICMP pass in on $ext_if inet proto icmp icmp-type echoreq code 0 keep state # SSH pass in on $ext_if inet proto tcp from any to $ext_if port {ssh} flags S/SA keep state (max-src-conn 5, max-src-conn-rate 3/300, overload <ssh_block> flush) block on $ext_if inet proto tcp from <ssh_block> to $ext_if port {ssh} probability 65% # Services pass in on $ext_if inet proto udp from any to $ext_if port {$udp_svc} keep state pass in on $ext_if inet proto tcp from any to $ext_if port {$tcp_svc} flags S/SA keep state pass in on $int_if inet proto udp from any to $int_if port {$n_udp_svc} keep state pass in on $int_if inet proto tcp from any to $int_if port {$n_tcp_svc} flags S/SA keep state |
Скачать архив: mailstat.zip
Оставить комментарий или два