0

Изменение белого списка по умолчанию в policyd

Используя iRedMail, столкнулся с необходимостью внести некоторые домены в белый список, чтобы по ним не проходила проверка greylist.
В случае с iRedMail за это отвечает сервис policyd, который хранит записи в mysql.

В двух словах про механизм greylist

В общих чертах механизм работает следующим образом. Почтовая система получает сообщение и проверяет отправителя, получателя и хост отправителя. В случае если в системе есть запись, что письма с такими параметрами уже приходили, письмо доставляется, если нет, то удаленному серверу отвечает примерно следующее: «Извини, сейчас твое письмо не могу принять, попробуй еще раз позже.», при этом отмечаем, что такой-то пытался доставить сообщение. Если попытаетсся это сделать, например, через 40 мин, значит очень хочет его доставить, и мы его пропустим. После этого терпеливо выжидаем данное время, и если удаленный сервер повторяет попытку, то запоминаем, что у данного отправителя все хорошо, и в след раз его сообщение примем. Именно так и поступит в большинстве случаев удаленный почтовый сервер. Спам рассылка же, как правило, ждать и повторять не будет, однако на практике все больше и больше спама прорывается, но речь не об этом. Далее речь пойдет про policyd, как реализацию данного механизма.
Если интересны подробности

Как это мешает пользователям

Очевидно, что это задержки в доставке писем. Кроме того, ситуацию усугубляет следующее.
Допустим пришло письмо

Nov 26 09:08:13 mail cbpolicyd[3451]: module=Greylisting, action=defer, host=94.100.179.58, helo=smtp3.mail.ru, from=, to=, reason=greylisted
Nov 26 09:08:13 mail postfix/smtpd[21794]: NOQUEUE: reject: RCPT from smtp3.mail.ru[94.100.179.58]: 451 4.7.1 <>: Recipient address rejected: Greylisting in effect, please come back later; from=<> to=<> proto=ESMTP helo=<smtp3.mail.ru>

После этого это же сообщение приходит совсем с другого сервера:

Nov 26 09:11:09 mail cbpolicyd[3451]: module=Greylisting, action=defer, host=94.100.181.184, helo=fallback1.mail.ru, from=, to=, reason=greylisted
Nov 26 09:11:09 mail postfix/smtpd[22176]: NOQUEUE: reject: RCPT from fallback1.mail.ru[94.100.181.184]: 451 4.7.1 <>: Recipient address rejected: Greylisting in effect, please come back later; from=<> to=<> proto=ESMTP helo=<fallback1.mail.ru>

И проверка greylist начинается с начала. Не очень приятно.

Как обновить whitelist

Для начала мне надо было понять какие домены нужно добавить.
я посмотрел так:

$ cat /var/log/maillog* | grep "rejected: Greylisting in effect" | awk '{print $24}' | awk -F@ '{print $2}' | sed 's/>//g' | sort | uniq -c | sort -n -k1

 105 ya.ru
 121 inbox.ru
 125 bounce.linkedin.com
 170 bk.ru
 945 yandex.ru
1262 mail.ru

Затем определиться как их обрабатывать.
Для получения списка актуальных сетей для yandex.ru, mail.ru и т.д. логично было получать их SPF записи и добавлять их в таблицу policyd. Я придерживался этого плана.
В результате у меня появился следующий скрипт:

#!/usr/bin/python
# coding: utf-8

import dns.resolver
import MySQLdb

# mysql settings
DBHost = 'localhost'
DBUser = 'cluebringer'
DBPassword = 'password'
DBName = 'cluebringer'


domains = ['yandex.ru','mail.ru','gmail.com']

def get_networks(domain,ips=[],orig_domain=''):

  def name2ip(name):
    ans = dns.resolver.query(name, 'A')
    out = []
    for a in ans:
      out.append("ip4:"+str(a))
    return out

  out = ips
  answers = dns.resolver.query(domain, 'TXT')
  answer = answers.response.to_text()
  if answer.find('v=spf1') != -1:
    tmp = answer[answer.find('v=spf1'):]
    tmp = tmp[:tmp.find('"\n')]
    tmp = tmp.replace('" "', "")
    records = tmp.split(" ")
    if orig_domain == '':
      orig_domain = domain
    for rec in records:
      if rec.find('ip4') != -1:
        out.append(rec)
      else:
        if rec.find('include') != -1:
          domain = rec[rec.find(':')+1:]
          get_networks(domain,out,orig_domain)
        if rec.find('redirect') != -1:
          domain = rec[rec.find('=')+1:]
          get_networks(domain,out,orig_domain)
        if rec.find('mx') != -1:
          q = dns.resolver.query(orig_domain, 'MX')
          for i in q:
            i = i.to_text()[i.to_text().find(" ")+1:]
            for j in name2ip(i):
              out.append(j)
    return out
  else:
    return False


def wl_insert(dbhost,dbuser,dbpwd,dbname,ip_array,comment):
  table = 'greylisting_whitelist'
  db = MySQLdb.connect(dbhost,dbuser,dbpwd,dbname)
  cur = db.cursor()
  values = ''
  for i in ip_array:
    i = i.replace('ip4:','')
    values = values + '("SenderIP:'+i+'","'+comment+'",0),'
  values = values[:-1] + ';'
  cur.execute("insert ignore into `"+table+"` (Source,Comment,Disabled) VALUES "+ values)
  db.commit()
  db.close()

# MAIN

for i in domains:
  networks = get_networks(i)
  wl_insert(DBHost,DBUser,DBPassword,DBName,networks,i)

Хочу отметить, что скрипт не проверяет исключительные ситуации и в процессе работы ругается на дублирующие записи, однако работает:

whitelist-spf-update-v2.py:66: Warning: Duplicate entry 'SenderIP:216.239.32.0/19' for key 'Source'
  cur.execute("insert ignore into `"+table+"` (Source,Comment,Disabled) VALUES "+ values)

Не забывайте про бекапы, т.к. скрипт точно обрабытывает не все записи, которые могут встретиться в TXT записи зоны домена.
Буду признателен, если у кого-нибудь хватит времени и сил допилить его до адекватного состояния.

Alexey Egorychev

FreeBSD and Linux sysadmin. Know many systems like mailsystems, DB, WWW stack. Automation with salt, ansible. Monitoring with nagios, zabbix.