Так получилось, что в организации, где я работаю, в качестве почтового сервера используется Postfix. В связке с ним используются средства для фильтрации спама и вирусов Spamassassin, Amavisd-new и ClamAV. В дополнение ко всему этому реализован greylisting с использованием GLD. Последний прост в настройке «легок» в работе, но из этого вытекает один недостаток — недостаточно гибок. Дабы побороть это я обратил взор на интересную фичу Postfix — Postfix SMTP Access Policy Delegation. Информации на великом и могучем по этой теме мало. Кому интересно как добавить свои проверки до передачи данных к GLD или как реализовать свои «внешние» правила в Postfix с использованием любимых или просто привычных языков и средств — прошу под кат.
Началось все с того, что часть пользователей все настойчивей стала жаловаться на строгость антиспам фильтра. Требовали отключить все проверки аргументируя это тем, что они готовы получать весь спам лишь бы не терять ни единого письма. Наше дело админское, надо так надо. И вот тут выяснилось, что в amavisd-new это реализуется просто, а вот в GLD такой возможности нет. Там есть whitelist, но он основан на данных отправителя, я так до конца не разобрался работает ли он с адресами получателя потому как мне все равно хотелось держать один whitelist в одном месте а не заниматься поддержкой двух сразу. Да и задачи могут меняться и на будущее мне все равно была нужна некая прослойка между Postfix и GLD чтобы я мог реализовать в ней все что мне захочется.
Памятуя, как работает GLD (через сетевой сокет, слушает порт 2525), я изучил механизм check_policy_service и вот что получается. Postfix передает данные SMTP сессии по указанному в директиве адресу. Если писать свой демон, слушающий сокет, не хочется, у Postfix есть средство — демон spawn, который работает наподобие inetd. Он сам слушает нужные сокеты и передает все что получает на указанный транспорт.
Данные имеют формат name=value по одной на строку, окончание пакета данных обозначается пустой строкой. Ответ должен состоять из одной строки вида action=value за которой также должна следовать пустая строка. action может принимать как стандартные для Postfix-овских списков OK или REJECT, так и DUNNO что значит продолжать проверку другими фильтрами, этот работу закончил или DEFER_IF_PERMIT Some text... который приведет к отклонению письма с кодом 450 и ответом Some text…
Согласно официальной документации, данные, поступающие на STDIN транспорта, имеют следующий вид:
Итак, вооружаемся инструментом. Лично мне привычней Perl. Если Вы любите и умеете читать чужие perl сорцы можете закрывать вкладку с этой статьей и переходить к изучению greylist.pl из состава примеров, поставляемых с Postfix. Тем более он, как и положено грамотному примеру, написан понятно, с отступами и комментариями. Мы же переходим к настройке Postfix.
Делается все по заветами оф. документации. Сначала правим master.cf добавляя в его конец наш новый транспорт:
Это просто описание, для того, чтобы задействовать этот транспорт пишем в main.cf следующее:
Обратите внимание, reject_unauth_destination должно стоять ДО вашего check_policy_service.
Сначала, я приведу каркас скрипта, на его основе Вы, дочитавший до сюда, сможете сделать все, что Вам потребуется.
Теперь для тех, кого заинтересовала тема не в общем, а именно мой случай (посредничество между Postfix и GLD или ему подобными) я привожу полный текст получившегося (и реально работающего) у меня скрипта:
Только в процессе написания статьи я начал задумываться об отказе от GLD и развитии скрипта как его заместителя. Хоть GLD и написан на Си, но все таки использует MySQL для хранения данных, так что выигрыш в производительности не так уж и велик. Ну и последнее обновление в мае 2006 года говорит о том, что проект не совсем жив. Интересно кто-нибудь кроме меня им пользуется?
В общем жду комментариев. И да простят меня нелюбители длинных текстов! Я и сам такой.
Вводная
Началось все с того, что часть пользователей все настойчивей стала жаловаться на строгость антиспам фильтра. Требовали отключить все проверки аргументируя это тем, что они готовы получать весь спам лишь бы не терять ни единого письма. Наше дело админское, надо так надо. И вот тут выяснилось, что в amavisd-new это реализуется просто, а вот в GLD такой возможности нет. Там есть whitelist, но он основан на данных отправителя, я так до конца не разобрался работает ли он с адресами получателя потому как мне все равно хотелось держать один whitelist в одном месте а не заниматься поддержкой двух сразу. Да и задачи могут меняться и на будущее мне все равно была нужна некая прослойка между Postfix и GLD чтобы я мог реализовать в ней все что мне захочется.
Теория
Памятуя, как работает GLD (через сетевой сокет, слушает порт 2525), я изучил механизм check_policy_service и вот что получается. Postfix передает данные SMTP сессии по указанному в директиве адресу. Если писать свой демон, слушающий сокет, не хочется, у Postfix есть средство — демон spawn, который работает наподобие inetd. Он сам слушает нужные сокеты и передает все что получает на указанный транспорт.
Данные имеют формат name=value по одной на строку, окончание пакета данных обозначается пустой строкой. Ответ должен состоять из одной строки вида action=value за которой также должна следовать пустая строка. action может принимать как стандартные для Postfix-овских списков OK или REJECT, так и DUNNO что значит продолжать проверку другими фильтрами, этот работу закончил или DEFER_IF_PERMIT Some text... который приведет к отклонению письма с кодом 450 и ответом Some text…
Согласно официальной документации, данные, поступающие на STDIN транспорта, имеют следующий вид:
Postfix version 2.1 and later: request=smtpd_access_policy protocol_state=RCPT protocol_name=SMTP helo_name=some.domain.tld queue_id=8045F2AB23 sender=foo@bar.tld recipient=bar@foo.tld recipient_count=0 client_address=1.2.3.4 client_name=another.domain.tld reverse_client_name=another.domain.tld instance=123.456.7 Postfix version 2.2 and later: sasl_method=plain sasl_username=you sasl_sender= size=12345 ccert_subject=solaris9.porcupine.org ccert_issuer=Wietse+20Venema ccert_fingerprint=C2:9D:F4:87:71:73:73:D9:18:E7:C2:F3:C1:DA:6E:04 Postfix version 2.3 and later: encryption_protocol=TLSv1/SSLv3 encryption_cipher=DHE-RSA-AES256-SHA encryption_keysize=256 etrn_domain= Postfix version 2.5 and later: stress= Postfix version 2.9 and later: ccert_pubkey_fingerprint=68:B3:29:DA:98:93:E3:40:99:C7:D8:AD:5C:B9:C9:40 [empty line]
К делу
Итак, вооружаемся инструментом. Лично мне привычней Perl. Если Вы любите и умеете читать чужие perl сорцы можете закрывать вкладку с этой статьей и переходить к изучению greylist.pl из состава примеров, поставляемых с Postfix. Тем более он, как и положено грамотному примеру, написан понятно, с отступами и комментариями. Мы же переходим к настройке Postfix.
Postfix
Делается все по заветами оф. документации. Сначала правим master.cf добавляя в его конец наш новый транспорт:
# Greylist policy daemon filter gld unix - n n - 0 spawn user=nobody argv=/home/bender/scripts/gld.pl
Это просто описание, для того, чтобы задействовать этот транспорт пишем в main.cf следующее:
smtpd_recipient_restrictions = ... reject_unauth_destination, check_policy_service unix:private/gld gld_time_limit = 3600
Обратите внимание, reject_unauth_destination должно стоять ДО вашего check_policy_service.
Perl
Сначала, я приведу каркас скрипта, на его основе Вы, дочитавший до сюда, сможете сделать все, что Вам потребуется.
#!/usr/bin/perl $dump = ''; $defaultAction = 'DUNNO'; # Отключаем буферизацию вывода. select((select(STDOUT), $| = 1)[0]); ##################################################################### # Основной цикл ##################################################################### while (<STDIN>) { if ($_ eq "\n") { # Данные получены, обрабатываем if (meetSomeReq($dump)) { # Обработка закончена, возвращаем DUNNO print STDOUT "action=$defaultAction\n\n"; } else { # Отклоняем письмо print STDOUT "action=DEFER_IF_PERMIT Service temporary unavailable\n\n"; } $dump = ''; } else { # Сохраняем поступающие данные $dump .= $_; } } ##################################################################### # Функции ##################################################################### sub meetSomeReq { my $dump = shift(); my $line = ''; my %param = (); my $result = 1; # Конвертируем данные, полученные от Postfix в хэш foreach $line (split(/\n/, $dump)) { chomp($line); my ($key, $val) = split(/=/, $line); $param{$key} = $val; } # Делаем что-то и меняем на основе этого значение $result при необходимости return $result; }
Результат
Теперь для тех, кого заинтересовала тема не в общем, а именно мой случай (посредничество между Postfix и GLD или ему подобными) я привожу полный текст получившегося (и реально работающего) у меня скрипта:
#!/usr/bin/perl use IO::Socket; use DBI; my $dbh = DBI->connect("DBI:mysql:host=localhost;database=amavisd", "amavisadmin", "amavisadminpw") or die "Couldn't connect to server !$ \n"; $dump = ''; $defaultAction = 'DUNNO'; # Unbuffer standard output. select((select(STDOUT), $| = 1)[0]); ##################################################################### # Main loop ##################################################################### while (<STDIN>) { if ($_ eq "\n") { if (inWhiteList($dump)) { print STDOUT "action=$defaultAction\n\n"; } else { print STDOUT passToGLD($dump); } $dump = ''; } else { $dump .= $_; } } $dbh->disconnect(); ##################################################################### # # Subs # ##################################################################### sub passToGLD { my $dump = shift(); $dump .= "\n\n"; my $sock = new IO::Socket::INET( PeerAddr => '127.0.0.1', PeerPort => '2525', Proto => 'tcp', ); die "Could not create socket: $!\n" unless $sock; print $sock $dump; $resp = <$sock>; close($sock); return $resp."\n"; } sub inWhiteList { my $dump = shift(); my $line = ''; my %param = (); my $result = 1; my $maxSize = 65536; # # Convert text dump to hash # foreach $line (split(/\n/, $dump)) { chomp($line); my ($key, $val) = split(/=/, $line); $param{$key} = $val; } # # Check user's policy # if ($param{'size'} < $maxSize) # Pass large mails without check { my ($user, $domain) = split(/@/, $param{'recipient'}); my $qry = "SELECT count(email) FROM users WHERE policy_id='3' AND (email=? OR email=?)"; my $sth = $dbh->prepare($qry); $sth->execute($param{'recipient'}, '@'.$domain); my @row = $sth->fetchrow_array(); $sth->finish(); $result = $row[0]; } return $result; }
Заключение
Только в процессе написания статьи я начал задумываться об отказе от GLD и развитии скрипта как его заместителя. Хоть GLD и написан на Си, но все таки использует MySQL для хранения данных, так что выигрыш в производительности не так уж и велик. Ну и последнее обновление в мае 2006 года говорит о том, что проект не совсем жив. Интересно кто-нибудь кроме меня им пользуется?
В общем жду комментариев. И да простят меня нелюбители длинных текстов! Я и сам такой.
