Регулярно натыкаюсь на статьи про прикручивание к почтовикам антиспамовских систем (например spamassassin и подобных). Каждый раз, смотря на эти связки и кучу проблем которые они приносят, «пожимаю плечами» и искренно не понимаю зачем все это? Спам можно очень эффективно рубить непосредственно силами почтовика, без привлечения сторонних программ, некоторые из которых еще и требуют длительного обучения (насколько я знаю, но могу и ошибаться — не использую я их).
Метод отлова спама, который я опишу в этой статье, дает эффективность, примерно 97%. Он опробован на 10-ке серверов, и работает уже как минимум лет семь.
Все примеры конфигурации буду приводить для почтового сервера exim, собранного с поддержкой mysql. Но перевести их на тот-же postfix не составит особого труда.
Пользователи храняться в БД. Дабы не загромождать статью структура БД в приложенном файле (ссылка). Структура взята от ранних версий postfixadmin. Дабы было удобно админить пользователей.
Для начала инициализируем несколько переменных, которые будут использоваться в наших проверках почты. Названия переменных достаточно «говорящие».
domainlist local_domains = ${lookup mysql{SELECT `domain` FROM `domain` WHERE `domain`='${domain}' AND `active`='1'}}
domainlist relay_to_domains = ${lookup mysql{SELECT `domain` FROM `domain` WHERE `domain`='${domain}' AND `active`='1'}}
Указываем почтовику правила по которым будет проверяться почта
acl_smtp_rcpt = acl_check_rcpt
acl_smtp_data = acl_check_data
Заголовки и текст письма непосредственно.
Ну а теперь начнем проверять нашу почту. Начинаем с заголовков.
acl_check_rcpt:
# принимать сообщения которые пришли с локалхоста, не по TCP/IP
accept hosts = :
# Запрещаем письма содержащие в локальной части символы @; %; !; /; |.
deny message = "Incorrect symbol in address"
domains = +local_domains
local_parts = ^[.] : ^.*[@%!/|]
# Проверяем недопустимые символы для нелокальных получателей:
deny message = "Incorrect symbol in address"
domains = !+local_domains
local_parts = ^[./|] : ^.*[@%!] : ^.*/\.\./
# Принимаем почту для постмастеров локальных доменов без проверки отправителя
accept local_parts = postmaster
domains = +local_domains
# Запрещщаем, если невозможно проверить отправителя (отсутствует в списке локальных пользователей)
require verify = sender
# Запрещщаем тех, кто не обменивается приветственными сообщениями (HELO/EHLO)
deny message = "HELO/EHLO require by SMTP RFC"
condition = ${if eq{$sender_helo_name}{}{yes}{no}}
# Принимаем сообщения от тех, кто аутентифицировался
accept authenticated = *
# Отрубаем тех, кто подставляет свой IP в HELO
deny message = "Your IP in HELO - access denied!"
# все хосты кроме тех, что в relay_from_hosts
hosts = * : !+relay_from_hosts
condition = ${if eq{$sender_helo_name}{$sender_host_address}
{true}{false}}
# Рубаем хосты типа *adsl*; *dialup*; *pool*;....
deny message = "your hostname is bad (adsl, poll, ppp & etc)."
condition = ${if match{$sender_host_name}
{adsl|dialup|pool|peer|dhcp}
{yes}{no}}
# Рубаем тех, кто в блэк-листах.
deny message = "you in blacklist - $dnslist_domain"
hosts = !+relay_from_hosts
dnslists = dul.dnsbl.sorbs.net :
sbl-xbl.spamhaus.org
# Начинаем подсчет очков спама для письма. Вес каждого параметра подбирался экспериментально.
warn set acl_m0 = 0
logwrite = "ACL m0 set default as $acl_m0 for
host=$sender_host_name [$sender_host_address] with
HELO=$sender_helo_name (domain in e-mail = $sender_address_domain)"
# наличие в теме слова Vigra, ранее было очень актуально, сейчас данное правило уже устарело
# 200 очков
warn condition = ${if match{$h_subject}
{viagra}
{yes}{no}}
set acl_m0 = ${eval:$acl_m0+200}
logwrite = "STAGE0: ACL m0 set = $acl_m0 for
host=$sender_host_name [$sender_host_address] with
HELO=$sender_helo_name - VIAGRA!!!!"
# Проверяем соответствие HELO и обратной записи DNS - на правильном почтовике они должны совпадать
# 30 очков
warn condition = ${if !eq{$sender_helo_name}{$sender_host_name}{yes}{no}}
hosts = !+relay_from_hosts : *
set acl_m0 = ${eval:$acl_m0+30}
logwrite = "STAGE1: ACL m0 set = $acl_m0 for
host=$sender_host_name [$sender_host_address] with
HELO=$sender_helo_name - reverse zone not match with HELO"
# Проверяем наличие обратной зоны для хоста
# 30 очков
warn condition = ${if eq{$host_lookup_failed}{1}{yes}{no}}
hosts = !+relay_from_hosts : *
set acl_m0 = ${eval:$acl_m0+30}
logwrite = "STAGE2: ACL m0 set = $acl_m0 for
host=$sender_host_name [$sender_host_address] with
HELO=$sender_helo_name - no reverse zone for host"
# Проверяем число точек и дефисов в доменном имени. Если больше 4-х уже такой домен вызывает подозрение
# 40 очков
warn condition = ${if match{$sender_host_name}
{N((?>w+[.|-]){4,})N}{yes}{no}}
hosts = !+relay_from_hosts : *
set acl_m0 = ${eval:$acl_m0+40}
logwrite = "STAGE3: ACL m0 set = $acl_m0 for
host=$sender_host_name [$sender_host_address] with
HELO=$sender_helo_name - more dots or defice in name"
# Проверяем общую длину адреса отправителя. Больше 30 символов вызывает подозрение
# 10 очков
warn condition = ${if >{${strlen:$sender_address}}{30}{yes}{no}}
hosts = !+relay_from_hosts : *
set acl_m0 = ${eval:$acl_m0+10}
logwrite = STAGE4: ACL m0 set = $acl_m0 for
host=$sender_host_name [$sender_host_address] with HELO=$sender_helo_name
- many big sender address [$sender_address]
# Очки за диалап в имени хоста. В данном правиле используется файлик со списком масок. Честно, за давностью лет, не помню где его взял
# Его Вы можете найти в архиве с БД, ссылка на которую приведена в начале статьи
# 60 очков
warn condition = ${lookup{$sender_host_name}
wildlsearch{/usr/local/etc/exim/dialup_hosts}
{yes}{no}}
hosts = !+relay_from_hosts : *
set acl_m0 = ${eval:$acl_m0+60}
logwrite = "STAGE5: ACL m0 set = $acl_m0 for
host=$sender_host_name [$sender_host_address] with
host=$sender_helo_name - dialup, ppp & etc..."
# Очки за диалап в HELO
# 60 очков
warn condition = ${lookup{$sender_helo_name}
wildlsearch{/usr/local/etc/exim/dialup_hosts}
{yes}{no}}
hosts = !+relay_from_hosts : *
set acl_m0 = ${eval:$acl_m0+60}
logwrite = "STAGE6: ACL m0 set = $acl_m0 for
host=$sender_host_name [$sender_host_address] with
HELO=$sender_helo_name - dialup, ppp & etc..."
# Проверяем есть ли зона первого уровня из HELO
# 150 очков
warn condition = ${if !eq{${lookup mysql{SELECT 1 FROM
`list_top_level_domains` WHERE `zone` =
LCASE(CONCAT('.', SUBSTRING_INDEX(
'${quote_mysql:$sender_helo_name}',
'.', -1)))}}}{1}{yes}{no}}
hosts = !+relay_from_hosts : *
set acl_m0 = ${eval:$acl_m0+150}
logwrite = non-existent domain in HELO -
'$sender_helo_name' setting acl_m0 = $acl_m0
warn set acl_m2 = 0
# Проверяем, не было ли письма на этот адрес от наших пользователей.
# Тут же заполняем whitelist если письмо от нашего локального отправителя
warn condition = ${if eq{${lookup mysql{SELECT 1 FROM `sended_list`
WHERE `user_to` =
LCASE('${quote_mysql:$sender_address}')
AND `user_from`
= LCASE('${quote_mysql:$local_part@$domain}')
AND `last_mail_timestamp` < `last_mail_timestamp`
+ (60*24*60*60) LIMIT 1}}}{1}{yes}{no}}
condition = ${lookup mysql{INSERT IGNORE INTO `domain_whitelist`
(`domainname`, `domain_ip`, `added_timestamp`,
`last_mail_timestamp`, `mail_count`) VALUES
(LCASE('${quote_mysql:$sender_address_domain}'),
'${quote_mysql:$sender_host_address}',
UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '1')
ON DUPLICATE KEY UPDATE
`last_mail_timestamp` = UNIX_TIMESTAMP(),
`mail_count` = `mail_count` + 1}}
hosts = !+relay_from_hosts : *
set acl_m2 = 1
logwrite = STAGE7: $sender_address ==> $local_part@$domain;
setting acl_m2 = $acl_m2; WHITELIST for this addresses
# Проверка наличия домена в whitelist
warn condition = ${if eq{${lookup mysql{SELECT 1
FROM `domain_whitelist`
WHERE `domain_ip` =
'${quote_mysql:$sender_host_address}'
LIMIT 1}}}{1}{yes}{no}}
hosts = !+relay_from_hosts : *
set acl_m2 = 1
logwrite = STAGE8: $sender_address ==> $local_part@$domain;
setting acl_m2 = $acl_m2; WHITELIST for ALL domains
# Сбрасываем переменную с очками спама если домен в whitelist или наши пользователи общались с данным респондентом
warn condition = ${if eq{$acl_m2}{1}{yes}{no}}
logwrite = Resetting acl_m0 $acl_m0 --> 0, host in whitelist
($sender_address ==> $local_part@$domain)
set acl_m0 = 0
# Проверка получателя в локальных доменах. Если не проходит, то проверяется следующий ACL
accept domains = +local_domains
endpass
message = "In my mailserver not stored this user"
verify = recipient
# Проверяем получателя в релейных доменах
accept domains = +relay_to_domains
endpass
message = "main server not know relay to this address"
verify = recipient
# Разрешаем почту от доменов в списке relay_from_hosts
accept hosts = +relay_from_hosts
# Если неподошло ни одно правило - явно ищут открытый релей
deny message = "This is not open relay"
Теперь перейдем к проверке тела письма.
acl_check_data:
deny message = contains $found_extension file (blacklisted).
demime = com:vbs:bat:pif:scr:exe
deny message = This message contains a MIME error ($demime_reason)
demime = *
condition = ${if >{$demime_errorlevel}{2}{1}{0}}
deny message = This message contains NUL characters
log_message = NUL characters!
condition = ${if >{$body_zerocount}{0}{1}{0}}
deny message = Incorrect headers syntax
hosts = !+relay_from_hosts:*
!verify = header_syntax
# Здесь можем отклонить почту по подсчитанным ранее очкам спама. При сумме очков более 99, считаем, что это спам.
# Но, как показала практика, после раскоментирования следующих строчек, юзеры начинают очень нервничать и
# переживать - а работает ли вообще у нас почта, и где любимый спам по утрам :) А потому далее мы используем эти
# очки немного по другому
#deny message = Possible SPAM message
# log_message = Possible SPAM message
# condition = ${if >{$acl_m0}{99}{yes}{no}}
# Пропускаем остальное
accept
В exim есть механизм system filter. Вот туда и добавляем
if $acl_m0 matches ^\d+
then
logwrite "FILTER: debug - digit in variable acl_m0 = $acl_m0 (after first if)"
if $acl_m0 is above 99
then
headers add "X-Spam-Description: if spam count >= 100 - this is spam"
headers add "X-Spam-Count: $acl_m0"
headers add "Old-Subject: $h_subject:"
headers remove "Subject"
headers add "Subject: (*** SPAM ***) $h_old-subject:"
headers add "X-Spam: YES"
logwrite "EXIM FILTER: Spam count = $acl_m0 ; Added SPAM header"
endif
endif
Т.е. мы в начало темы письма вставляем сточку "(*** SPAM ***)", по которой клиенты пользователей уже отсортировуют спам.
Как видите набор правил не велик, но позволяет эффективно отфильтровывать спам, при этом не ставя никаких дополнительных систем. Конечно есть вероятность ложных срабатываний, но после первого же письма от нашего пользователя к тому, которого случайно включили в спам, и он попадает в whitelist.
Повторюсь, по моим наблюдениям, такие достаточно простые правила не пропускают где-то 97% спама.
Автор: BAV_Lug