Замучавшись бороться с lex+yacc решил перевести конфигурацию rspamd в xml формат. Минусы старой системы довольно прозаичны: lex при переключении внутренних состояний парсера (lex states) не умеет при yyrestart'е переключаться в INITIAL state, что приводит к невозможности перечитывания конфига "на лету". Кроме этого, сами по себе lex+yacc предоставляют слишком много возможностей для генерации грамматик, что само по себе неплохо, но я ловлю себя на мысле, что bind like конфиг-файл зачастую не очень очевиден для пользователя, яркий пример, когда переменные rspamd на самом деле являются не переменными в полном понимании этого слова, а подстановками текста. Также такой конфиг крайне сложно парсить чем-то, отличным от оригинальной lex/yacc грамматики. Моя же идея была в расширении интерфейса управления кластера rspamd, давая возможность конфигурации машин в кластере более-менее атоматически. Выбор лежал между ini-like форматом и xml (yaml и json тоже рассматривались, но никаких существенных преимуществ, кроме уменьшения размера конфига, я не нашел), но у ini нет понятия уровней вложенности, а это мне было нужно для описания файлов статистики внутри classifier'а. Конечным решением системы, которая бы предоставляла компромисс между удобством ручного написания сложных правил и возможностью настройки параметров автоматически (через web интерфейс или же shell script), я выбрал lua + xml. То есть, логика правил описывается в lua, используя все возможности этого языка, включая, например, переменные, являющиеся функциями, а включаются эти правила, а также назначаются веса, описываются рабочие процессы в xml. Такое решение, на мой взгляд, позволяет отделить код правил от собственно процесса настройки системы. Поддержку старого формата я оставил, и теперь rspamd умеет конвертировать старый формат в xml (конвертировать правила в lua он, к сожалению, не умеет, но умеет представлять их в виде xml). Сразу же видимый профит - возможность "мягкого" рестарта с перечитыванием конфига. В будущем планируется введение динамических правил, которые можно было бы загружать в кластер через контроллер, не выполняя рестарта. Также анализ правил, заимствованных из spamassassin'а показал, что все это лучше делать через отдельные статистические файлы, которые после обучения поставлять вместе с rspamd. Ну и напоследок, если у кого-то вдруг появилось желание заменить SA или другую систему спам фильтрации на rspamd, но в rspamd не хватает какой-то функциональности, то я был бы рад выслушать подобные замечания, равно как и другие идеи по развитию проекта.
Tuesday, April 27, 2010
Thursday, February 4, 2010
Небольшой обзор возможностей rspamd
Так как до сих пор у меня не появилось идей, как рассказать легко и понятно о том, зачем и как использовать rspamd, я написал краткий обзор rspamd: фичи, установка, настройка и обучение. Надеюсь, он будет полезен тем, кто хочет использовать rspamd или тем, кто даже не знает о его существовании. Обзор тут: http://cebka.pp.ru/why-rspamd.html.
Thursday, October 1, 2009
ICQ транспорт и kqueue
При настройке icq транспорта (разумеется, имеется в виду py-icqt, так как остальные существующие сейчас проекты либо мертвы, как jit, либо не работают) на FreeBSD возникло странное желание использовать kqueue reactor, так как очевидно, что традиционные select/poll - не лучший выбор при сколько-нибудь большом числе persistent коннекций. Итак, вначале необходимо поставить devel/py-kqueue. Далее, если мы пропишем в конфигурационном файле
<reactor>kqueue</reactor>
то, скорее всего pyicqt начнет кушать 100% CPU. Если посмотреть ktrace, то можно будет увидеть следующее:
25821 python2.5 RET kevent -1 errno 9 Bad file descriptor
25821 python2.5 CALL gettimeofday(0xbfbfd8c8,0)
25821 python2.5 RET gettimeofday 0
25821 python2.5 CALL gettimeofday(0xbfbfd5b8,0)
25821 python2.5 RET gettimeofday 0
25821 python2.5 CALL write(0x2,0x28e70014,0x2ab)
25821 python2.5 GIO fd 2 wrote 683 bytes
"Traceback (most recent call last):
File "/usr/local/lib/jabber/pyicq/PyICQt.py", line 16, in <module>
main.main()
File "/usr/local/lib/jabber/pyicq/src/main.py", line 473, in main
reactor.run()
File "/usr/local/lib/python2.5/site-packages/twisted/internet/base.py", line 1128, in run
self.mainLoop()
--- <exception caught here> ---
File "/usr/local/lib/python2.5/site-packages/twisted/internet/base.py", line 1140, in mainLoop
self.doIteration(t)
File "/usr/local/lib/python2.5/site-packages/twisted/internet/kqreactor.py", line 189, in doKEvent
l = self._kq.kevent([], len(self._selectables), timeout)
exceptions.OSError: [Errno 9] Bad file descriptor
"
Вспоминаем неприятную особенность kqueue: при форке не наследуюся kqueue дескрипторы. То есть, необходимо выполнить демонизацию самостоятельно. Вторая проблема - неверный расчет тайм-аутов. Фикс ее:
Очевидно, что для получения наносекунд, нужно умножить миллисекунды на миллион, а не на сто тысяч.
Вторая проблема - корявая демонизация. Я решил ее исправлением стартового скрипта следующим образом:
То есть, демонизация происходит при помощи /usr/sbin/daemon. Проверка трейса работы pyicqt показала, что после этих действий для работы он использует kevent, причем, использует правильно. Хотя наличие select'ов все равно смущает, но для socket IO он все же использует kqueue.
<reactor>kqueue</reactor>
то, скорее всего pyicqt начнет кушать 100% CPU. Если посмотреть ktrace, то можно будет увидеть следующее:
25821 python2.5 RET kevent -1 errno 9 Bad file descriptor
25821 python2.5 CALL gettimeofday(0xbfbfd8c8,0)
25821 python2.5 RET gettimeofday 0
25821 python2.5 CALL gettimeofday(0xbfbfd5b8,0)
25821 python2.5 RET gettimeofday 0
25821 python2.5 CALL write(0x2,0x28e70014,0x2ab)
25821 python2.5 GIO fd 2 wrote 683 bytes
"Traceback (most recent call last):
File "/usr/local/lib/jabber/pyicq/PyICQt.py", line 16, in <module>
main.main()
File "/usr/local/lib/jabber/pyicq/src/main.py", line 473, in main
reactor.run()
File "/usr/local/lib/python2.5/site-packages/twisted/internet/base.py", line 1128, in run
self.mainLoop()
--- <exception caught here> ---
File "/usr/local/lib/python2.5/site-packages/twisted/internet/base.py", line 1140, in mainLoop
self.doIteration(t)
File "/usr/local/lib/python2.5/site-packages/twisted/internet/kqreactor.py", line 189, in doKEvent
l = self._kq.kevent([], len(self._selectables), timeout)
exceptions.OSError: [Errno 9] Bad file descriptor
"
Вспоминаем неприятную особенность kqueue: при форке не наследуюся kqueue дескрипторы. То есть, необходимо выполнить демонизацию самостоятельно. Вторая проблема - неверный расчет тайм-аутов. Фикс ее:
--- kqsyscallmodule.c.bak 2009-10-02 18:24:37.000000000 +0400
+++ kqsyscallmodule.c 2009-10-02 18:26:37.000000000 +0400
@@ -141,7 +141,7 @@
}
statichere PyTypeObject KQEvent_Type = {
- PyObject_HEAD_INIT(NULL)
+ PyObject_HEAD_INIT(&PyType_Type)
0, // ob_size
"KQEvent", // tp_name
sizeof(KQEventObject), // tp_basicsize
@@ -295,14 +295,14 @@
/* Build timespec for timeout */
totimespec.tv_sec = timeout / 1000;
- totimespec.tv_nsec = (timeout % 1000) * 100000;
+ totimespec.tv_nsec = (timeout % 1000) * 1000000;
// printf("timespec: sec=%d nsec=%d\n", totimespec.tv_sec, totimespec.tv_nsec);
/* Make the call */
-
+ Py_BEGIN_ALLOW_THREADS
gotNumEvents = kevent (self->fd, changelist, haveNumEvents, triggered, wantNumEvents, &totimespec);
-
+ Py_END_ALLOW_THREADS
/* Don't need the input event list anymore, so get rid of it */
free (changelist);
@@ -365,7 +365,7 @@
statichere PyTypeObject KQueue_Type = {
/* The ob_type field must be initialized in the module init function
* to be portable to Windows without using C++. */
- PyObject_HEAD_INIT(NULL)
+ PyObject_HEAD_INIT(&PyType_Type)
0, /*ob_size*/
"KQueue", /*tp_name*/
sizeof(KQueueObject), /*tp_basicsize*/
Очевидно, что для получения наносекунд, нужно умножить миллисекунды на миллион, а не на сто тысяч.
Вторая проблема - корявая демонизация. Я решил ее исправлением стартового скрипта следующим образом:
--- /usr/local/etc/rc.d/jabber-pyicq-transport.orig 2009-10-02 18:57:06.000000000 +0400
+++ /usr/local/etc/rc.d/jabber-pyicq-transport 2009-10-02 19:29:00.000000000 +0400
@@ -24,8 +24,9 @@
: ${jabber_pyicq_user="ejabberd"}
pidfile="${jabber_pyicq_piddir}/PyICQt.pid"
-command_interpreter="/usr/local/bin/python2.5"
-command="${jabber_pyicq_dir}/PyICQt.py"
-command_args="-b -o pid=${pidfile}"
+python="/usr/local/bin/python2.5"
+procname="${python}"
+command="/usr/sbin/daemon"
+command_args="${python} ${jabber_pyicq_dir}/PyICQt.py -o pid=${pidfile}"
run_rc_command "$1"
То есть, демонизация происходит при помощи /usr/sbin/daemon. Проверка трейса работы pyicqt показала, что после этих действий для работы он использует kevent, причем, использует правильно. Хотя наличие select'ов все равно смущает, но для socket IO он все же использует kqueue.
Monday, August 31, 2009
Windows resolver
Знаете, что сделает resolver винды, если его попросить отрезолвить url c base10 закодированным ip http://9715522259? Правильно, резолвер винды сможет даже 9 миллиардов превратить в ip адрес, тупо взяв младшие 32 бита от результата преобразования. Поэтому для парсинга url'ей, рассылаемых спамерами, которые содержат base10 encoded ip, надо преобразовывать его в uintmax_t, и брать младшие 32 бита.
Monday, April 13, 2009
Rspamd
Бета версия rspamd доступна для тестирования. Для сборки требуется cmake и gmime2.2. Сейчас rspamd работает примерно на порядок быстрее, чем spamassassin, но для окончательного релиза необходимо еще много тестирования. Буду признателен за любую информацию об использовании rspamd, а также о багах, в нем найденных. Rspamd доступен тут: http://cebka.pp.ru/trac
Friday, February 20, 2009
CMake + libperl
Начну с того, что встроенный модуль CMake никуда не годится:
Естественно, пользоваться им нельзя. Кроме того, у перла до 5.8.9 есть очень гадкая особенность - DynaLoader.a, который есть в ldflags'ах. Если собирается приложение с -fPIC, то порядок линковки объектов важен, и DynaLoader имеет свойство ломать сборку. Я решил эту проблему копированием DynaLoader.a за угол с добавлением префикса lib. Выглядит это так:
Этот метод работает как при наличии DynaLoader.a, так и при его отсутствии. Достаточно в настройках таргета добавить следующее:
SET(PERL_POSSIBLE_INCLUDE_PATHS
/usr/lib/perl/5.8.3/CORE
/usr/lib/perl/5.8.2/CORE
/usr/lib/perl/5.8.1/CORE
/usr/lib/perl/5.8.0/CORE
/usr/lib/perl/5.8/CORE
)
Естественно, пользоваться им нельзя. Кроме того, у перла до 5.8.9 есть очень гадкая особенность - DynaLoader.a, который есть в ldflags'ах. Если собирается приложение с -fPIC, то порядок линковки объектов важен, и DynaLoader имеет свойство ломать сборку. Я решил эту проблему копированием DynaLoader.a за угол с добавлением префикса lib. Выглядит это так:
# Find perl libraries and cflags
EXECUTE_PROCESS(COMMAND ${PERL_EXECUTABLE} -MExtUtils::Embed -e ccopts OUTPUT_VARIABLE PERL_CFLAGS)
EXECUTE_PROCESS(COMMAND ${PERL_EXECUTABLE} -MExtUtils::Embed -e ldopts OUTPUT_VARIABLE PERL_LDFLAGS)
STRING(REGEX REPLACE "[\r\n]" " " PERL_CFLAGS ${PERL_CFLAGS})
STRING(REGEX REPLACE " +$" "" PERL_CFLAGS ${PERL_CFLAGS})
STRING(REGEX REPLACE "[\r\n]" " " PERL_LDFLAGS ${PERL_LDFLAGS})
STRING(REGEX REPLACE " +$" "" PERL_LDFLAGS ${PERL_LDFLAGS})
# Handle DynaLoader
STRING(REGEX MATCH "/[^ ]*/DynaLoader.a" PERL_DYNALOADER ${PERL_LDFLAGS})
STRING(REGEX REPLACE "/[^ ]*/DynaLoader.a " "" PERL_LDFLAGS ${PERL_LDFLAGS})
IF(PERL_DYNALOADER)
EXECUTE_PROCESS(COMMAND ${CMAKE_COMMAND} -E copy ${PERL_DYNALOADER} ${project_BINARY_DIR}/compat/libdynaloader.a)
LINK_DIRECTORIES(${rspamd_BINARY_DIR}/compat/)
ENDIF(PERL_DYNALOADER)
Этот метод работает как при наличии DynaLoader.a, так и при его отсутствии. Достаточно в настройках таргета добавить следующее:
IF(PERL_DYNALOADER)
TARGET_LINK_LIBRARIES(target dynaloader)
ENDIF(PERL_DYNALOADER)
Wednesday, February 18, 2009
Эффективная почтовая система на базе nginx+postfix+rmilter
Довольно часто возникает необходимость сделать высокоэффективную почтовую систему с возможностью масштабированияв плане увеличения mx'ов. Для этого можно воспользоваться такой связкой: на входе стоит nginx с policy, которая выполняет асинхронный резолвинг адресов и фильтрацию по rbl+regexp. На многих потоках почты, это снижает процент почты, доходящей до MTA, примерно в 2-3 раза. При этом policy выполняет балансировку между различными backend'ами на основании весов. MTA использует rmiter для выполнения различных проверок: clamav, spamd, ratelimits, greylisting, regexps. При этом, все данные greylisting'а и лимитов сохраняются в memcached, что обеспечивает очень быструю работу и возможность скалирования системы.
Итак, вначале скачиваем nginx версии 0.6.11: http://sysoev.ru/nginx/nginx-0.6.11.tar.gz Более свежие версии, к сожалению, работать не будут. Далее скачиваем libevent и применяем мой патч для поддержки TXT записей в резолвере:
libevent_txt.patch
Почему этой возможности до сих пор нет в libevent, мне не очень понятно.
После установки патченной версии libevent можно устанавливать nginx и policy.
Вначале патчим исходники nginx патчем для поддержки policy (также фиксится работа pipelining'а и включается режим работы без аутентификации). Патч лежит тут:
http://cebka.pp.ru/hg/hgwebdir.cgi/nginx-smtp-policy/file/ab33c3b8f1f1/patch-src_mail
После этого собираем nginx с поддержкой модуля mail.
Далее можно собрать policy. Скачиваем policy отсюда: http://cebka.pp.ru/hg/hgwebdir.cgi/nginx-smtp-policy/archive/tip.tar.gz
Собирается просто командой make+make install. При этом ставятся файлы nginx-smtp-policy и nginx-policy-watchdog с соответствующими стартовыми скриптами в /usr/local/etc/rc.d
Настройка policy предельно проста: в каждой строчке содержится переменная и ее значение. Строчки с # в начале считаются комментариями. Пример используемого мной конфига:
# nameserver - definition of nameserver (maybe multiply)
# special value resolv.conf is used to parse /etc/resolv.conf file
nameserver 195.19.37.129
# logfile - full path to logfile
# Default: /var/log/policy.log
logfile /var/log/policy/policy.log
# pidfile - full path to pidfile
# Default: /var/run/policy.pid
pidfile /var/log/policy/policy.pid
# backend_host - address of host that are backends for nginx (postfix address)
# Example host1:weight1,host2:weight2
# Default: 127.0.0.1
backends 127.0.0.1:1
# backend_port - port of backend
# Default: 25
backend_port 25
# listen - address of socket to listen on, maybe tcp or unix:
# tcp: listen tcp:host:port
# unix: listen unix:/path/to/sock
# Default: tcp:localhost:7070
listen tcp:localhost:7070
# loglevel - numeric value of logging verbocity
# 0 - only errors are logged
# 1 - warnings and errors are logged
# 2 - warnings, notices and errors are logged
# 3 - everything including debug messages is logged
# Default: 0
loglevel 2
# helo_regexp_file - specify path to helo regexp file and error code and error message string
# separated by ':' (maybe multiply)
# Example: helo_regexp_file /usr/local/etc/nginx-policy/helo.re:554 5.7.1:Bad address
# Default:
helo_regexp_file /usr/local/etc/postfix/maps/policy_helo_regexp:554 5.7.1:Helo rejected
# hostname_regexp_file - specify path to host regexp file and error code and error message string
# separated by ':' (maybe multiply)
# Example: hostname_regexp_file /usr/local/etc/nginx-policy/host.re:554 5.7.1:Bad address
# Default:
hostname_regexp_file /usr/local/etc/postfix/maps/policy_reverse_regexp:554 5.7.1:We do not accept mail form this dynamic pool
check_rbl xbl.spamhaus.org
check_rbl insecure-bl.rambler.ru
Файлы, которые я использую для этих регэкспов, можно найти в http://cebka.pp.ru/stuff/
Postfix ставится стандартным образом. Отдельно могу указать следующие вещи:
если мы на одном хосте пускаем policy и postfix, то в main.cf указываем
inet_interfaces = localhost
также для корректной работы rmilter нужно указать следующие опции:
milter_protocol = 4
smtpd_milters = unix:/var/run/rmilter/rmilter.sock
milter_default_action = accept
milter_mail_macros = i {auth_type} {auth_authen} {auth_author} {mail_addr} {client_addr} {client_name}
для работы XCLIENT указываем, с каких хостов он разрешен (это хост, где работает policy)
smtpd_authorized_xclient_hosts = localhost
Далее настроим nginx:
mail {
server_name host.ru;
auth_http localhost:7070/nginxauth.cgi;
smtp_auth none;
#smtp_capabilities "SIZE 28311552" PIPELINING 8BITMIME;
smtp_capabilities "SIZE 28311552" 8BITMIME;
xclient on;
smtp_helo_required on;
smtp_banner "mxi.icn.bmstu.ru ESMTP nginx\n\nSystem Info: This is a mail server at host.ru\n\n\n\nEmail contact: <abuse@host.ru>\n\n";
proxy_pass_error_message on;
server {
#listen 3525;
listen ip1:25;
listen ip2:25;
protocol smtp;
timeout  
; 300s;
}
}
Rmilter для данной системы можно поставить из портов FreeBSD - mail/rmilter
Настройка rmilter детально описано в конфиге и странице rmilter (8). Memcached для работы мильтра можно использовать любой. Для общения с memcached лучше использовать tcp. Работа в режиме udp пока нестабильна из-за кучи ошибок в реализации udp в мемкешеде.
Работа этой связки проверялась в FreeBSD 6, FreeBSD 7 и FreeBSD 8-CURRENT. Если кому-то удастся завести эту связку на других системах, шлите патчи :) Также в http://cebka.pp.ru/stuff лежат примеры конфигов и используемые нами "белые" списки.
Итак, вначале скачиваем nginx версии 0.6.11: http://sysoev.ru/nginx/nginx-0.6.11.tar.gz Более свежие версии, к сожалению, работать не будут. Далее скачиваем libevent и применяем мой патч для поддержки TXT записей в резолвере:
libevent_txt.patch
Почему этой возможности до сих пор нет в libevent, мне не очень понятно.
После установки патченной версии libevent можно устанавливать nginx и policy.
Вначале патчим исходники nginx патчем для поддержки policy (также фиксится работа pipelining'а и включается режим работы без аутентификации). Патч лежит тут:
http://cebka.pp.ru/hg/hgwebdir.cgi/nginx-smtp-policy/file/ab33c3b8f1f1/patch-src_mail
После этого собираем nginx с поддержкой модуля mail.
Далее можно собрать policy. Скачиваем policy отсюда: http://cebka.pp.ru/hg/hgwebdir.cgi/nginx-smtp-policy/archive/tip.tar.gz
Собирается просто командой make+make install. При этом ставятся файлы nginx-smtp-policy и nginx-policy-watchdog с соответствующими стартовыми скриптами в /usr/local/etc/rc.d
Настройка policy предельно проста: в каждой строчке содержится переменная и ее значение. Строчки с # в начале считаются комментариями. Пример используемого мной конфига:
# nameserver - definition of nameserver (maybe multiply)
# special value resolv.conf is used to parse /etc/resolv.conf file
nameserver 195.19.37.129
# logfile - full path to logfile
# Default: /var/log/policy.log
logfile /var/log/policy/policy.log
# pidfile - full path to pidfile
# Default: /var/run/policy.pid
pidfile /var/log/policy/policy.pid
# backend_host - address of host that are backends for nginx (postfix address)
# Example host1:weight1,host2:weight2
# Default: 127.0.0.1
backends 127.0.0.1:1
# backend_port - port of backend
# Default: 25
backend_port 25
# listen - address of socket to listen on, maybe tcp or unix:
# tcp: listen tcp:host:port
# unix: listen unix:/path/to/sock
# Default: tcp:localhost:7070
listen tcp:localhost:7070
# loglevel - numeric value of logging verbocity
# 0 - only errors are logged
# 1 - warnings and errors are logged
# 2 - warnings, notices and errors are logged
# 3 - everything including debug messages is logged
# Default: 0
loglevel 2
# helo_regexp_file - specify path to helo regexp file and error code and error message string
# separated by ':' (maybe multiply)
# Example: helo_regexp_file /usr/local/etc/nginx-policy/helo.re:554 5.7.1:Bad address
# Default:
helo_regexp_file /usr/local/etc/postfix/maps/policy_helo_regexp:554 5.7.1:Helo rejected
# hostname_regexp_file - specify path to host regexp file and error code and error message string
# separated by ':' (maybe multiply)
# Example: hostname_regexp_file /usr/local/etc/nginx-policy/host.re:554 5.7.1:Bad address
# Default:
hostname_regexp_file /usr/local/etc/postfix/maps/policy_reverse_regexp:554 5.7.1:We do not accept mail form this dynamic pool
check_rbl xbl.spamhaus.org
check_rbl insecure-bl.rambler.ru
Файлы, которые я использую для этих регэкспов, можно найти в http://cebka.pp.ru/stuff/
Postfix ставится стандартным образом. Отдельно могу указать следующие вещи:
если мы на одном хосте пускаем policy и postfix, то в main.cf указываем
inet_interfaces = localhost
также для корректной работы rmilter нужно указать следующие опции:
milter_protocol = 4
smtpd_milters = unix:/var/run/rmilter/rmilter.sock
milter_default_action = accept
milter_mail_macros = i {auth_type} {auth_authen} {auth_author} {mail_addr} {client_addr} {client_name}
для работы XCLIENT указываем, с каких хостов он разрешен (это хост, где работает policy)
smtpd_authorized_xclient_hosts = localhost
Далее настроим nginx:
mail {
server_name host.ru;
auth_http localhost:7070/nginxauth.cgi;
smtp_auth none;
#smtp_capabilities "SIZE 28311552" PIPELINING 8BITMIME;
smtp_capabilities "SIZE 28311552" 8BITMIME;
xclient on;
smtp_helo_required on;
smtp_banner "mxi.icn.bmstu.ru ESMTP nginx\n\nSystem Info: This is a mail server at host.ru\n\n\n\nEmail contact: <abuse@host.ru>\n\n";
proxy_pass_error_message on;
server {
#listen 3525;
listen ip1:25;
listen ip2:25;
protocol smtp;
timeout  
; 300s;
}
}
Rmilter для данной системы можно поставить из портов FreeBSD - mail/rmilter
Настройка rmilter детально описано в конфиге и странице rmilter (8). Memcached для работы мильтра можно использовать любой. Для общения с memcached лучше использовать tcp. Работа в режиме udp пока нестабильна из-за кучи ошибок в реализации udp в мемкешеде.
Работа этой связки проверялась в FreeBSD 6, FreeBSD 7 и FreeBSD 8-CURRENT. Если кому-то удастся завести эту связку на других системах, шлите патчи :) Также в http://cebka.pp.ru/stuff лежат примеры конфигов и используемые нами "белые" списки.
Subscribe to:
Posts (Atom)