Как стать автором
Обновить

Postfix — amavisd-new без localhost или почтовый сервер по новому

Время на прочтение4 мин
Количество просмотров9.6K
Есть масса инструкций, как поднять почтовый сервер на связке postfix — amavisd-new — dovecot. И подавляющее большинство из них повторяют друг друга почти дословно, включая ошибки и неточности.

Мне кажется скучным бездумно нажимать кнопки, поэтому я решил оптимизировать стандартную конфигурацию: что если построить взаимодействие postfix и amavisd-new не через localhost, а на unix socket?

Как оказалось, всё не так просто, но я это сделал! Инструкция и патч под катом.

Честно сказать, я вообще не люблю взаимодействие через localhost в пределах одной машины. Если требуется организовать обмен данными между двумя приложениями, то гораздо правильнее, безопаснее и менее ресурсоёмко сделать это через unix socket на файловой системе. Тем более, что так можно организовать защиту (средствами прав на файловой системе) даже там, где она отсутствует на уровне приложения или протокола.

Итак, путь почты в обсуждаемой связке выгладит так:



Выходит, нам нужно организовать две стыковки: при передаче на фильтрацию и при возврате обратно в MTA. Так как сокет создаёт слушатель, то в первом случае это будет amavisd, а во втором — postfix.

Начнём со второго, ибо это проще и лучше описано. Для того, чтобы postfix создал слушающий сокет, нужно просто во второй колонке master.cf (колонка type) указать unix, а не inet. В этом случае первая колонка определяет путь и имя файла сокета.

Так как процессы postfix работают в chroot (это можно отключить для конкретного процесса, но не стоит), то папку нужно создать внутри домашней директории postfix: /var/spool/postfix. В ней будут оба сокета:

mkdir /var/spool/postfix/amavis
chown amavis:postfix /var/spool/postfix/amavis
chmod 770 /var/spool/postfix/amavis

Ну и конфигурация postfix:

amavis/postfix-in unix  y                -               y               -               -               smtpd
        -o smtpd_client_restrictions=$local_clients_only
        -o smtpd_helo_restrictions=
        -o smtpd_sender_restrictions=
        -o smtpd_recipient_restrictions=
        -o smtpd_milters=

Конкретные опции зависят от вашей настройки, это мой вариант.

Тут есть две беды:

  1. Путь будет относительно /var/spool/postfix/private, на которую навешаны очень строгие права.
  2. Не уверен, что во всех дистрибутивах так, но в Ubuntu точно. Права на папку лучше не трогать (там лежат сокеты всех сервисов postfix), лучше просто создать symlink.
  3. Помимо сокета, postfix создаёт и pid-файл для процесса, имя которого генерируется автоматически по маске $type.$name. Где type будет равен unix, а name берётся из первой колонки master.cf. Получается unux.amavis/postfix-in т.е. файл в подпапке. Сам он её не создаст и упадёт с ошибкой.

Итак, подставляем костыли:

cd /var/spool/postfix/private
ln -s ../amavis .
mkdir /var/spool/postfix/pid/unix.amavis

Не очень красиво, но и не деструктивно для штатной структуры папок пакета.

Перезапускаем postfix и убеждаемся, что файл сокета появился в папке amavis, а pid-файл в pid/unix.amavis. К сожалению, права на сокет — 666, но права на папку, которую создали ранее, защитят файл от лишних глаз.

Проверить работу можно командой:

netcat -U /var/spool/postfix/amavis/postfix-in
220 mail.example.ru ESMTP Postfix

Отлично, с этим справились. Теперь к amavisd.

Сначала настроим обратный путь почты через unix socket, принадлежащий postfix. Это работает из коробки:

$forward_method = 'smtp:/var/spool/postfix/amavis/postfix-in';
$notify_method = \$forward_method;

Ну а теперь самое сложное — настроить сокеты в amavisd. Решение можно найти в интернете, но там предлагается использовать единственный сокет, задаваемый параметром $unix_socketname. Мне же хотелось, чтобы и собственный протокол amavisd-new (AM.PDP) и приём почты происходили через сокеты.

Файл с конфигурацией по умолчанию содержит упоминание дерективы @listen_sockets, но никакого описания для неё нет. Зато оно есть в release notes, даже с примерами! Правда и там сокет всего один, но что мешает попробовать?

ОК, а как задать протокол для сокета (который указывается в policy bank)? Во всех примерах пишут просто SOCK. По аналогии с inet-сокетами (там можно указать хост-порт) я предположил, что нужно указывать полный путь к файлу сокета. Вот что получилось:

$unix_socketname = undef;
$inet_socket_bind = undef;
$inet_socket_port = undef;
@listen_sockets = ('/var/lib/amavis/amavisd.sock', '/var/spool/postfix/amavis/amavis-in.sock');
$unix_socket_mode = 0660;
%interface_policy = (
    '/var/lib/amavis/amavisd.sock' => 'AM.PDP-SOCK',
    '/var/spool/postfix/amavis/amavis-in.sock' => 'LMTP-SOCK'
);
$policy_bank{'LMTP-SOCK'} = {
    protocol => 'LMTP'
};
$policy_bank{'AM.PDP-SOCK'} = {
  protocol => 'AM.PDP',
  auth_required_release => 0, # don't require secret-id for release
};

Перезапускаю, проверяю — действительно, создались оба сокета! Победа? Не совсем, при попытке подключиться к сокету, ничего не происходит, а в лог записывается ошибка, что протокол для него не определён. Выходит, policy bank к ним не применяются.

Как же так? Пришлось идти в код.

Этот поход принёс две новости — как водится, хорошую и плохую. Хорошая состоит в том, что предположение относительно %interface_policy было верным:

# load policy banks according to my socket (destination),
# then check for allowed access from the peer (client/source)
#
sub access_is_allowed($;$$$$) {
  my($unix_socket_path, $src_addr, $src_port, $dst_addr, $dst_port) = @_;
  my(@bank_names);
  if (defined $unix_socket_path) {
    push(@bank_names, $interface_policy{"SOCK"});
    push(@bank_names, $interface_policy{$unix_socket_path});
  } elsif (defined $dst_addr && defined $dst_port) {
    $dst_addr = '['.lc($dst_addr).']' if $dst_addr =~ /:[0-9a-f]*:/i;  # IPv6?
    push(@bank_names, $interface_policy{$dst_port});
    push(@bank_names, $interface_policy{"$dst_addr:$dst_port"});
  }
  load_policy_bank($_) for @bank_names;

Плохая — что $unix_socket_path приходит в эту функцию пустым. Заполняется он следующим образом:

my $is_ux = $sock && $sock->UNIVERSAL::can('NS_proto') &&
            $sock->NS_proto eq 'UNIX';

И оба свойства там пустые.

Изучение документации подсказало такой вариант:

my $unix_socket_path = $sock->hostpath();

И это заработало! Готовый .patch можно сказать здесь.

Остался последний штрих. Так как amavisd создаёт свой сокет с правами только на себя, а доступ для остальных мы запретили (что верно), то нужно добавить postfix в группу amavis, чтобы он смог писать в сокет:

gpasswd -a postfix amavis

Готово.

P.S.: Патч и описание проблемы я отправил Mark Martinec по почте так как никакого упоминания про багтракер на сайте не нашёл. Ответ до сих пор не получил, но особо на него и не рассчитываю — проект выглядит заброшенным (последний релиз больше двух лет назад).
Теги:
Хабы:
Всего голосов 7: ↑6 и ↓1+5
Комментарии3

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань