Sendmail from-to firewall HOWTO

Данный мануал рассматривает реализацию системы фильтрации почты в программе sendmail. Большое преимущество этого метода (по крайней мере, мне оно кажется немаловажным) -- это то, что все изменения претерпевает лишь файл конфигурации sendmail. Никаких левых патчей, тем более бинарников -- несколько строчек в sendmail.mc, пересборка sendmail.cf и -- вуаля. И никаких мейлеров писать не надо, все уже написано до нас.

Для начала поставим задачу. Вот приходит, допустим, пользователь к почтовому админу (то есть к вам), и говорит: меня, говорит, сильно утомляют эти чертовы спамеры. При том, говорит, что почта мне нужна всего с двух-трех адресов со всего земного шара, а я каждый день получаю по три десятка писем, и все равно удаляю их, не читая. Можно ли что-нибудь с этим сделать? Ха, скажете вы, да легко. Сейчас, заправим пару строчек в access-файл, и будешь ты весь в шоколаде. Нет, останавливает он вас (продвинутый такой пользователь, админ, наверное, бывший), так делать нельзя. Наш манагер по рекламе (уж не знаю, есть ли такая должность, и чем этот человек занимается) получает почту с этих адресов, и она ему порой бывает нужна. Нельзя ли как-нибудь по-другому? Ну отчего же, можно, менее уверенно говорите вы. В cyrus-imapd реализован протокол sieve, так вроде он что-то такое делать умеет, впрочем, я не уверен... Ну, sieve так sieve, соглашается он. Только учти, что это письмо придет целиком, а там -- пол-мегабайта мусора. Умножай на двадцать-тридцать, а потом -- на количество сотрудников. Получишь бесполезный выход трафика.

То есть, надо организовать этакий фильтр на стадии 'rcpt to:', когда все необходимые входные параметры, а именно: почтовый адрес отправителя, почтовый адрес получателя, IP-адрес отправителя -- уже известны. Прибегнем к опыту IP-firewall'ов. Там (в самом простом случае) на пропуск пакетов во-первых, ставится политика (разрешить пропуск пакета, если не найден его точный шаблон в правилах firewall'а, или запретить его), во-вторых прописываются правила: от такого-то тому-то -- разрешить, тому-то -- запретить.

Начинаем думать. Надо организовать такую функцию, которая требует на вход два или три параметра -- адрес отправителя и/или IP-адрес машины отправителя и адрес получателя, и один выходной -- разрешить или запретить. В sendmail'е есть так называемый map, который вообще-то нам бы подошел, но на вход он берет только один параметр, не больше и не меньше. Попытаемся это обойти следующим образом:

spammer@badcompany.com!vav@mail.snz.ru REJECT

Надеюсь, идея уже понятна? Мы просто об"единяем адрес отправителя и адрес получателя таким образом, чтобы они образовывали один параметр только разделяем их знаком '!'.

На выходе, соответственно, тоже один параметр, уже бинарый: запретить (REJECT) или пропустить (ACCEPT). Не наляпайте пробелов между адресами, ибо пробел -- это разделитель RHS и LHS.

Теперь -- вариации на тему:

POLICY!vav@mail.snz.ruREJECT
spammer@baddomain.ru!vav@mail.snz.ruREJECT
friend@goodomain.net!vav@mail.snz.ruACCEPT
@snakespit.com!vav@mail.snz.ruREJECT
spammer@!vav@mail.snz.ruREJECT
@frienddom.com!vav@mail.snz.ruACCEPT
[192.168.0.1]!vav@mail.snz.ruACCEPT
[192.168.2]!vav@mail.snz.ruREJECT
exchange.spamer.ru!vav@mail.snz.ruREJECT
zdes-horoshih-lyudej.net!vav@mail.snz.ruREJECT
В этом примере подопытным респондентом является vav@mail.snz.ru.

Если вы опытны достаточно, чтобы писать и понимать правила подстановки, то здесь открывается широкое поле для вашей фантазии. Все, что я привел здесь -- лишь то, что понадобилось мне в моей битве с пользователями. Таким образом можно об"единить не два, и не три параметра, а все, которые доступны на текущий момент исполнения ловушки. Но мне кажется, что это ненужное усложнение.

Приоритет правил: а что делать, если пришло письмо от friend@spamer.ru, а файл firewall содержит следующие строки:

POLICY!vav@mail.snz.ruREJECT
@spamer.ru!vav@mail.snz.ruREJECT
friend@!vav@mail.snz.ruACCEPT
?

Это уже зависит от порядка разборов правил sendmail'а. В нижеследующем примере, который я здесь привожу, сначала ищется точное попадание, потом имя пользователя, потом домен, потом IP-адрес, потом проверяет доменное имя хоста. Поэтому это письмо пройдет нормально.

Итак, идея осознана, осталось ее только впрограммировать. Пишем в хвост sendmail.mc:


LOCAL_CONFIG

# Это будет у нас база данных firewall'ьных правил.

Kfirewall hash -o /etc/mail/sendmail-firewall

# Local_check_rcpt -- локальное расширение для ловушки 'rcpt to:'

# После исполнение команды 'rcpt to:' срабатывает ловушка check_rcpt,

# в которой стоит вызов правила Local_check_rcpt

# Hint: не смотрите эти правила lynx'ом.

# Смотрите это links'ом или чем-нибудь графическим,

# жалательно с широким экраном, чтобы правила не переносились

# И НЕ ЗАБЫВАЕМ, ЧТО ЛЕВАЯ ЧАСТЬ ПРАВИЛА ДОЛЖНА ОТДЕЛЯТЬСЯ

# ОТ ПРАВОЙ ТАБУЛЯЦИЕЙ.

SLocal_check_rcpt

# Канонизируем адрес получателя, чтобы привести его

# к виду user@domain

R$* $: $>3 $1
# Запоминая адрес получателя, канонизируем адрес посылателя

R$* < @ $* . > $: $1@$2 $| $>3 $&f
# Проверяем точное попадание
R$* $| $* < @ $*. > $: $1 $| < $(firewall $2@$3!$1 $: $) >
# Разбираем полученное
R$* $| <ACCEPT> $@ $1
R$* $| <REJECT> $#error $@ 5.2.5 $: "Direct prohibition"
# Точное попадание не найдено -- двигаемся дальше
R$* $| <> $: $1 $| $>3 $&f
# Попытаемся проверить на имя пользователя и на домен.

# Делаем это сразу одним правилом, чтобы не мудрить.
R$* $| $* < @ $* . > $: $1 $| < $(firewall $2@!$1 $: NO $) > < $(firewall @$3!$1 $: NO $) >
# Сначала просматриваем, было ли разрешено/запрещено имя пользователя
R$* $| <ACCEPT> <$-> $@ $1
R$* $| <REJECT> <$-> $#error $@ 5.2.6 $: "Name prohibition"
# Потом проверяем доменную часть
R$* $| <$-> <ACCEPT> $@ $1
R$* $| <$-> <REJECT> $#error $@ 5.2.7 $: "Domain rejection"
# Упоминание об имени пользователя и домене не найдено.

# Следующая проверка -- подставляем IP-адрес -- client_addr
R$* $| <$-> <$-> $: $1 $| $&{client_addr} $| <>
# Ищем сначала рекурсивно...
R$* $| $+.$- $| <> $1 $| $2 $| < $(firewall [$2.$3]!$1 $: $) >
# ..., а так как последнее число в IP-адресе рекурсивно

# отыскать не получится, то обрабатываем этот случай отдельной

# строкой.
R$* $| $-.$- $| <> $1 $| $2 $| < $(firewall [$2]!$1 $: $) >
# Разбираем найденное.
R$* $| $* $| <ACCEPT> $@ $1
R$* $| $* $| <REJECT> $#error $@ 5.2.8 $: "Bad ip"
# Ищем по имени хоста -- подставляем макрос client_name
R$* $| $* $| <> $: $1 $| $&{client_name} $| <>
# По аналогии с IP-адресами -- ищем рекурсивно,

# только в IP-адресах с каждой итерацией последнюю лексему,

# а здесь -- первую
R$* $| $-.$+ $| <> $1 $| $3 $| <$(firewall $2.$3!$1 $: $)>
# Точно так же обрабатываем последнюю часть доменного имени хоста.

# (net, ru, com, edu, etc...)
R$* $| $-.$- $| <> $: $1 $| $2 $| <$(firewall $3!$1 $: $)>
R$* $| $* $| <ACCEPT> $@ $1
R$* $| $* $| <REJECT> $#error $@ 5.2.8 $: "Bad hostname"
# Если все прошли -- нет ничего, то смотрим на политику

# Если политика не прописана -- значит, ACCEPT
R$* $| $* $| <> $: $1 $| < $(firewall POLICY!$1 $: ACCEPT $) >
R$* $| <ACCEPT> $@ $1
R$* $| <REJECT> $#error $@ 5.2.5 $: "Default policy works"

После всего этого исполняет две команды:

пересобираем файл конфигурации sendmail.cf

[root@mail mail]# m4 sendmail.mc > sendmail.cf

и заставляем sendmail перечитать свой файл конфигурации.

[root@mail mail]# kill -HUP `head -n1 /var/run/sendmail.pid`

Будучи под Linux'ом, я предпочитаю для перезапуска sendmail'а пользоваться командой

[root@mail mail]# /etc/rc.d/init.d/sendmail restart

Для проверок почаще пользуйтесь командой sendmail -bt:

[vav@mail mail]$ sendmail -bt
ADDRESS TEST MODE (ruleset 3 NOT automatically invoked)
Enter <ruleset> <address>
>.Dfspammer@mail.ru
>.D{client_addr}194.67.57.105
>Local_check_rcpt vav@mail.snz.ru
Local_check_rcpt input: vav @ mail . snz . ru
Local_check_rcpt returns: $# error $@ 5 . 2 . 5 $: "Exact prohibition"
>

Небольшое замечание: такую подробную диагностику причин не принятия почты я бы рекомендовал только на период отладки системы. Потому как всякая дополнительная информация будет давать спамерам намек, как ее (систему) обмануть. В общем, вместо всяких там "Bad user name" или "Default policy rejects your mail" говорите просто: "You can't send a letter to this user. Go away."

Собственно, на этом месте можно было бы поставить точку. Но присутствует в этой схеме некая незавершенность. Что, если пользователь поставил себе политику отвергать, разрешил доступ паре-тройке корреспондентов и забыл про это все. И тут, вместе со спамом приходит нужное письмо, а мы ему -- "go away". Нехорошо получится. Побороть в принципе это можно, только поставив политику принимать. В общем, если ставится политика отвергать, то неплохо бы обрабатывать логи и посылать пользователю каждый день письмо, в котором содержится список адресов отвегнутых корреспондентов. Еще лучше было бы, если бы sendmail слал бы куда-нибудь какой-нибудь сигнал, чтобы все это ловить на лету, но как это сделать средствами sendmail'а, я не знаю.

Вот, собственно, и все. Если у вас есть какие-нибудь вопросы, то можете послать их мне. Если вопрос интересный, я его с удовольствием почитаю. Отвечать не обещаю.

Вронский Владимир, ноябрь 2001 года.