Внешние правила доступа в Postfix на примере front-end к GLD

в 7:47, , рубрики: perl, postfix, системное администрирование, метки: ,

Так получилось, что в организации, где я работаю, в качестве почтового сервера используется 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 транспорта, имеют следующий вид:

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=$defaultActionnn";
        }
        else
        {
            # Отклоняем письмо
            print STDOUT "action=DEFER_IF_PERMIT Service temporary unavailablenn";
        }
        $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=$defaultActionnn";
        }
        else
        {
            print STDOUT passToGLD($dump);
        }
        
        $dump = '';
    }
    else
    {
        $dump .= $_;
    }
}

$dbh->disconnect();

#####################################################################
#
#  Subs
#
#####################################################################

sub passToGLD
{
    my $dump = shift();
    $dump .= "nn";
    
    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 года говорит о том, что проект не совсем жив. Интересно кто-нибудь кроме меня им пользуется?

В общем жду комментариев. И да простят меня нелюбители длинных текстов! Я и сам такой.

Автор: ischerbin


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js