воскресенье, 30 июня 2019 г.

Exim и Dovecot quota-status

5 лет назад я написал заметку Exim и Dovecot без SQL, в конце которой имелся такой вот абзац:
Для проверки квот на этапе RCPT сеанса ESMTP можно было бы воспользоваться не только самописным скриптом, но и сервисом проверки квот quota-status, который появился в Dovecot 2.2 (в Debian Wheezy поставляется Dovecot версии 2.1.7), благо Exim позволяет отправлять запросы в юникс-сокеты и читать из них ответ.
С момента написания той заметки я по-прежнему пользуюсь тем почтовым сервером, настройка которого была описана в статье. Сервер пережил несколько обновлений операционной системы, и Dovecot сейчас обновлён до версии 2.2.27.

Не так давно некто Slavko в комментариях к той заметке спросил, удалось ли мне прикрутить quota-satus к Exim. Я попробовал и у меня получилось. В ходе дальнейшей переписки в комментариях найденное решение было улучшено, а также была подтверждена его пригодность для промышленной эксплуатации на серверах с большим потоком входящих писем.

Объясню вкратце, зачем нужен quota-status и в чём сложности его интеграции с Exim.

Когда Dovecot и Exim настраиваются с использованием базы данных SQL, имеется возможность хранить информацию о текущем использовании квот почтового ящика в базе данных. Эту информацию можно использовать в почтовом сервере Exim на этапе приёма писем, чтобы не принимать к доставке письма на переполненные почтовые ящики.

При настройке связки Dovecot и Exim без использования базы данных SQL такой простой способ проверки квот пропадает. Exim не может проверить квоту почтового ящика получателя и принимает письмо к доставке. Если места в ящике нет, Dovecot не принимает письмо от Exim и Exim вынужден слать отправителю письма ответное письмо с сообщением об ошибке - рикошет.

В Dovecot имеется плагин quota-status, который реализует сервис для проверки превышения квоты получателем письма. Небольшая проблема заключается в том, что этот сервис рассчитан на работу с Postfix, т.к. сервис реализует протокол, используемый Postfix.

Однако, в Exim - это не простой почтовый сервер. Т.к. он обладает богатыми возможностями по фильтрации писем, его можно называть своего рода фреймворком для реализации SMTP-серверов. В частности, Exim позволяет передавать запросы в TCP- и Unix-сокеты и читать ответы из них. Этим можно воспользоваться для того, чтобы попытаться воспользоваться сервисом quota-status, который предоставляется Dovecot.

Настройка Dovecot

В файл /etc/dovecot/conf.d/90-quota.conf вписываем:
plugin {
  quota_status_success = OK 
  quota_status_nouser = NOUSER
  quota_status_overquota = OVER
}

service quota-status {
  executable = quota-status -p postfix
  
  unix_listener exim-quota-status {
    mode = 0660
    user = Debian-exim
    group = Debian-exim
  }
  
  client_limit = 1
}
Осталось перезапустить Dovecot:
# systemctl restart dovecot.service
Проверить работу сервиса можно при помощи утилиты socat. В примере ниже проиллюстрирована проверка статуса двух почтовых ящиков:
# socat STDIO UNIX:/var/run/dovecot/exim-quota-status 
recipient=xxx@stupin.su

action=OK

recipient=yyy@stupin.su

action=OVER
Как видно, с почтовым ящиком xxx@stupin.su всё в порядке, а вот у почтового ящика yyy@stupin.su квота превышена.

Настройка Exim

В файл конфигурации /etc/exim4/exim4.conf перед ACL грейлистинга вставляем такую проверку:
defer message = 422 Mailbox $local_part@$domain is over quota
         domains = +local_domains
         condition = ${if eq{${extract{action}\
                                      {${readsocket{/var/run/dovecot/exim-quota-status}\
                                                   {size=$message_size\nrecipient=$local_part@$domain\n\n}\
                                                   {5s}\
                                                   { }\
                                                   {action=FAIL}}}}}\
                            {OVER}\
                            {yes}\
                            {no}}
Указанное выше условие отправляет в сокет /var/run/dovecot/exim-quota-status три строки. В первой строке указан размер письма, во второй - его получатель, третья строка - пустая. Пустая строка сигнализирует о завершении запроса. Дальше в течение 5 секунд ожидается ответ. Если ответ поступил, то все переводы строк в ответе заменяются на пробелы. Если ответ не поступил, то вместо ответа дальше будет использоваться строка "action=FAIL". В результате должна получиться строка, в которой содержится ассоциативный массив, в котором разделителями элементов являются пробелы, а разделителями ключей и значений - знаки "равно", вот такая:
key1=value1 key2=value2 key3=value3
Выражение extract извлекает из этого словаря значение ключа action. Если значение равно OVER, то считается, что ящик переполнен и отправителю сообщается, что он должен отложить письмо в очередь, т.к. в данный момент почтовый ящик одного из получателей переполнен.

Осталось перезапустить Exim:
# systemctl restart exim4.service
Письма на переполненный ящик отбиваются с таким сообщением в журнале:
2019-06-11 22:48:31 H=forward100p.mail.yandex.net [77.88.28.100] X=TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256 CV=no F= temporarily rejected RCPT : 422 Mailbox yyy@stupin.su is over quota

Пригодность решения к промышленному применению

У Slavko возникли опасения, что Exim будет закрывать подключение к Dovecot по истечение 5 секунд даже в тех случаях, когда Dovecot уже ответил на запрос. Такая задержка может привести к проблемам при приёме большого потока входящих писем.

Судя по описанию readsocket на странице Chapter 11 - String expansions, Exim пишет запрос и сразу закрывает ту половинку сокета, которая используется для передачи данных в направлении Dovecot. После этого он читает ответ из оставшейся половинки сокета, в которую Dovecot пишет ответ для Exim. Чтобы Exim не закрывал свою половинку сокета, через которую отправляет данные в Dovecot, нужно после таймаута явным образом указать опцию shutdown=no, вот так: {5s:shutdown=no}. Поведение по умолчанию в нашем случае как раз подходящее, поэтому эту опцию писать не нужно.

Если посмотреть со стороны Dovecot, то он может отреагировать на поведение Exim одним из двух способов:
  1. либо сразу обнаружить закрытие половинки сокета и закрыть вторую половинку, не отправляя ответ,
  2. либо сначала отправить ответ, а потом закрыть свою половинку сокета.
Во-первых, я попробовал сымитировать ситуацию при помощи printf и socat:
# printf "recipient=yyy@stupin.su\n\n" | socat STDIO UNIX:/var/run/dovecot/exim-quota-status 
action=OVER
Как видно, ответ пришёл.

Во-вторых, я попробовал оттрассировать процесс dovecot/quota-status -p postfix при помощи strace и увидел, что сразу после ответа клиентское подключение через Unix-сокет закрывается. "Невооружённым взглядом" заметно, что таймаута в 5 секунд нет, т.к. вся проверка отрабатывает меньше чем за секунду.

Так что за образование очередей из писем, ожидающих проверки квоты ящиков адресатов можно не беспокоиться.

Отклонение адресата вместо отказа от письма

Я пробовал указывать выражение discard, чтобы отбрасывать только тех получателей, ящики которых переполнены. Но если вместо defer написать discard, то в журналах почтового сервера появляются ошибки следующего вида:
2019-06-11 22:35:32 configured error code starts with incorrect digit (expected 2) in "422 Mailbox yyy@stupin.su is over quota"
2019-06-11 22:35:32 H=forward104j.mail.yandex.net [5.45.198.247] F= RCPT : discarded by RCPT ACL: 422 Mailbox ууу@stupin.su is over quota
Exim считает, что его неправильно настроили, т.к. ответ должен начинаться с цифры 2.

Описанная конфигурация тестировалось на Debian Stretch. Готов выслушать замечания или дополнения к описанной конфигурации.

Использованные материалы

воскресенье, 23 июня 2019 г.

Контроль использования пулов подключений в PgBouncer при помощи Zabbix

Я уже рассказывал о PgBouncer'е в одной из своих прошлых заметок: Проксирование запросов к PostgreSQL через PgBouncer. PgBouncer - это прокси-сервер для СУБД PostgreSQL, который устанавливает постоянные подключения к серверу PostgreSQL и распределяет по этим постоянным подключениям поступающие подключения от клиентов.

Для уменьшения потребления оперативной памяти нужно стремиться к тому, чтобы пул подключений к серверу PostgreSQL было как можно меньше, т.к. каждое подключение к серверу порождает запуск ещё одного процесса для его обслуживания. С другой стороны, если чрезмерно уменьшить количество подключений к серверу PostgreSQL, то приложения могут успешно устанавливать подключения к PgBouncer'у, но долгое время ожидать освобождения подключения к серверу PostgreSQL, прежде чем смогут выполнить запрос.

Подключения к самому PgBouncer'у обычно обходятся очень дёшево, поэтому нет особого смысла уменьшать их количество, но тем не менее, может оказаться полезно отслеживать и их, т.к. это может помочь в диагностике внутренних проблем в приложении.

Для контроля входящих подключений к PgBouncer'у и использования пула подключений PgBouncer'а к серверу PostgreSQL я решил воспользоваться Zabbix.

Настройка пользователя

Для съёма статистики будем использовать пользователя mon с паролем mon. Впишем в файл /etc/pgbouncer/userlist.txt нового пользователя:
"mon" "mon"
Теперь нужно разрешить этому пользователю просматривать статистику. Для этого откроем файл /etc/pgbouncer/pgbouncer.ini, найдём опцию stats_users. По умолчанию она закомментирована. В таком случае нужно её раскомментировать и прописать одного-единственного пользователя mon. Если же она уже раскомментирована, тогда нужно добавить пользователя mon через запятую к списку имеющихся:
stats_users = mon
Осталось перезагрузить pgbouncer, чтобы новый пользователь получил доступ к статистике PgBouncer'а:
# systemctl reload pgbouncer.service

Скрипт для Zabbix-агента

Скрипт для обнаружения пулов подключений написан на языке командной строки, использует только утилиту psql для подключения к PgBouncer'у и язык awk для разбора результатов запросов:
#!/bin/sh

MON_USER=mon
MON_PASSWORD=mon
PGBOUNCER_PORT=6432

COMMAND=$1
USER=$2
DB=$3

if [ "x$COMMAND" = "xdiscover" ]
then
    env PGPASSWORD=$MON_PASSWORD psql -p $PGBOUNCER_PORT -U $MON_USER pgbouncer -t -c 'show pools;' \
      | awk -F'|' 'BEGIN { printf "{\"data\":["; n=0; }
                   /\|/ { if (n != 0)
                            printf ",";
                          gsub(" ", "", $1);
                          gsub(" ", "", $2);
                          printf "{\"{#PGBOUNCER_DATABASE}\": \"" $1 "\", \"{#PGBOUNCER_USER}\": \"" $2 "\"}";
                          n++; }
                   END { printf "]}"; }'
else
  env PGPASSWORD=$MON_PASSWORD psql -p $PGBOUNCER_PORT -U $MON_USER pgbouncer -t -c 'show pools;' \
    | awk -F'|' \
          -v DB=$DB \
          -v USER=$USER \
          -v COMMAND=$COMMAND '/\|/ { gsub(" ", "", $1);
                                      gsub(" ", "", $2);
                                      if (($1 == DB) && ($2 == USER))
                                      {
                                        if (COMMAND == "cl_active")
                                        {
                                          gsub(" ", "", $3);
                                          print $3;
                                        }
                                        else if (COMMAND == "cl_waiting")
                                        {
                                          gsub(" ", "", $4);
                                          print $4;
                                        }
                                        else if (COMMAND == "cl_total")
                                        {
                                          print $3 + $4;
                                        }
                                        else if (COMMAND == "sv_active")
                                        {
                                          gsub(" ", "", $5);
                                          print $5;
                                        }
                                        else if (COMMAND == "sv_idle")
                                        {
                                          gsub(" ", "", $6);
                                          print $6;
                                        }
                                        else if (COMMAND == "sv_used")
                                        {
                                          gsub(" ", "", $7);
                                          print $7;
                                        }
                                        else if (COMMAND == "sv_tested")
                                        {
                                          gsub(" ", "", $8);
                                          print $8;
                                        }
                                        else if (COMMAND == "sv_login")
                                        {
                                          gsub(" ", "", $9);
                                          print $9;
                                        }
                                        else if (COMMAND == "sv_total")
                                        {
                                          print $5 + $6 + $7 + $8 + $9;
                                        }
                                        else if (COMMAND == "maxwait")
                                        {
                                          gsub(" ", "", $10);
                                          print $10;
                                        }
                                      }
                                    }'
fi
Я поместил его в файле /etc/zabbix/pgbouncer.sh и выставил права доступа, позволяющие редактировать его только пользователю root, а читать и выполнять - пользователями из группы zabbix:
# chown root:zabbix /etc/zabbix/pgbouncer.sh
# chmod u=rwx,g=rx,o= /etc/zabbix/pgbouncer.sh
Как можно понять из указанных выше команд, у остальных пользователей доступа к файлу нет. Сделано это на случай, если нужно будет поменять пароль пользователя mon на более безопасный, чтобы никто не мог увидеть этот пароль, посмотрев тело скрипта.

Настройка Zabbix-агента

Для того, чтобы использовать данные скрипта из Zabbix'а, нужно добавить в конфигурацию Zabbix-агента /etc/zabbix/zabbix_agentd.conf на том компьютере, где установлен PgBouncer, следующие строчки:
UserParameter=pgbouncer.pool.discover,/etc/zabbix/pgbouncer.sh discover
UserParameter=pgbouncer.pool.cl_active[*],/etc/zabbix/pgbouncer.sh cl_active $1 $2
UserParameter=pgbouncer.pool.cl_waiting[*],/etc/zabbix/pgbouncer.sh cl_waiting $1 $2
UserParameter=pgbouncer.pool.cl_total[*],/etc/zabbix/pgbouncer.sh cl_total $1 $2
UserParameter=pgbouncer.pool.sv_active[*],/etc/zabbix/pgbouncer.sh sv_active $1 $2
UserParameter=pgbouncer.pool.sv_idle[*],/etc/zabbix/pgbouncer.sh sv_idle $1 $2
UserParameter=pgbouncer.pool.sv_used[*],/etc/zabbix/pgbouncer.sh sv_used $1 $2
UserParameter=pgbouncer.pool.sv_tested[*],/etc/zabbix/pgbouncer.sh sv_tested $1 $2
UserParameter=pgbouncer.pool.sv_login[*],/etc/zabbix/pgbouncer.sh sv_login $1 $2
UserParameter=pgbouncer.pool.sv_total[*],/etc/zabbix/pgbouncer.sh sv_total $1 $2
UserParameter=pgbouncer.pool.maxwait[*],/etc/zabbix/pgbouncer.sh maxwait $1 $2
После изменения конфигурации нужно перезапустить Zabbix-агента:
# systemctl restart zabbix-agent.service

Шаблоны Zabbix

Я подготовил два шаблона для Zabbix 3.4:
В обоих шаблонах имеется элемент данных для низкоуровневого обнаружения пулов подключений. Обнаружение происходит раз в час:

В шаблонах настроены прототипы элементов данных, позволяющих контролировать количество подключений от клиентов, количество подключений к серверу, а также время ожидания клиента в очереди до получения доступа к одному из свободных подключений к серверу:

Имеются также прототипы графиков, позволяющих следить за клиентскими подключениями, подключениями к серверу и временем ожидания в очереди:

Примеры графиков

Количество клиентских подключений:

Количество подключений к серверу:

Время ожидания клиента в очереди в ожидании свободного подключения к серверу:

воскресенье, 16 июня 2019 г.

Контроль количества процессов приложений uWSGI через Zabbix

Задумался как-то о том, чтобы подобрать подходящее количество рабочих процессов для каждого из приложений, работающих под управлением uwsgi. Если рабочих процессов не хватает, то приложение работает не в полную силу и часть поступающих запросов встаёт в очередь до освобождения рабочего процесса. Если же процессов слишком много, то часть из них окажутся неиспользуемыми и будут просто занимать свободную оперативную память. Чтобы задать подходящее количество рабочих процессов, нужно понять, сколько обычно их используется в действительности.

В uwsgi новых версий есть множество разных способов контролировать его работу, но в имеющейся у меня версии 1.2.3 из репозитория Debian Wheezy поддерживается не так-то много вариантов для его контроля. Я остановился на варианте с механизмом Stats Server.

Настройка сокет-файлов

Чтобы включить Stats Server для каждого из приложений, нужно добавить соответствующую опцию в конфигурацию каждого из приложений в файлах /etc/uwsgi/apps-available/. Однако, чтобы не редактировать много файлов конфигурации, можно изменить файл с настройками по умолчанию. Копируем из файла /usr/share/uwsgi/conf/default.ini настройки в файл /etc/uwsgi/uwsgi.ini и добавляем опцию:
stats = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/stats
В результате у меня получился файл с таким содержимым:
[uwsgi]
autoload = true
master = true
workers = 2
no-orphans = true
pidfile = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/pid
socket = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/socket
stats = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/stats
chmod-socket = 660
log-date = true
uid = www-data
gid = www-data
Теперь пропишем использование этого файла с настройками по умолчанию в файле /etc/default/uwsgi, прописав в него соответствующую настройку:
INHERITED_CONFIG=/etc/uwsgi/uwsgi.ini
Теперь нужно перезапустить все приложения:
# /etc/init.d/uwsgi restart
В каталогах приложений /var/run/uwsgi/app/<приложение>/ должны появиться файлы stats.

Скрипт обнаружения и опроса

К сожалению, права доступа к этому файлу точно такие же, как у файла сокета и поменять их через файл конфигурации приложения нельзя, поэтому для доступа к файлу статистики от имени пользователя zabbix придётся либо воспользоваться sudo, либо включить пользователя zabbix в группу www-data. Я воспользовался sudo. Однако, прежде чем перейти к настройкам sudo и Zabbix-агента, нужно написать скрипт, с помощью которого Zabbix-агент будет извлекать необходимые данные из сокетов статистики.

У меня получился скрипт на языке Python со следующим содержимым:
#!/usr/bin/python

import sys, os, socket, json

def discover():
    data = []
    for app in os.listdir('/run/uwsgi/app/'):
        data.append({'{#UWSGI_APP}': app})
    data = {'data': data}
    return json.dumps(data)

def read_stats(unix_socket):
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    try:
        s.connect(unix_socket)
    except socket.error:
        return None

    data = ''
    while True:
        buf = s.recv(4096)
        if len(buf) < 1:
            break
        data += buf
    s.close()

    data = data.decode('UTF-8')
    return json.loads(data)

def analyze_stats(data):
    listen_queue = data['listen_queue']
    listen_queue_errors = data['listen_queue_errors']
    
    total = 0
    idle = 0
    busy = 0
    for worker in data['workers']:
        total += 1
        if worker['status'] == 'idle':
            idle += 1
        elif worker['status'] == 'busy':
            busy += 1

    pidle = idle * 100.0 / total
    pbusy = busy * 100.0 / total

    return listen_queue, listen_queue_errors, total, idle, busy, pidle, pbusy

if __name__ == '__main__':
    if len(sys.argv) == 2:
        if sys.argv[1] == 'discover':
            print discover()
    elif len(sys.argv) == 3:
        app = sys.argv[2]
        data = read_stats('/run/uwsgi/app/%s/stats' % app)
        if data is None:
            print 'No stats'
            sys.exit()

        listen_queue, listen_queue_errors, total, idle, busy, pidle, pbusy = analyze_stats(data)
        if sys.argv[1] == 'listen_queue':
            print listen_queue
        elif sys.argv[1] == 'listen_queue_errors':
            print listen_queue_errors
        elif sys.argv[1] == 'total':
            print total
        elif sys.argv[1] == 'idle':
            print idle
        elif sys.argv[1] == 'busy':
            print busy
        elif sys.argv[1] == 'pidle':
            print pidle
        elif sys.argv[1] == 'pbusy':
            print pbusy
Я поместил этот скрипт в файл /etc/zabbix/uwsgi.py. Не забудьте поменять права доступа к файлу:
# chown root:root /etc/zabbix/uwsgi.py
# chmod ugo=rx /etc/zabbix/uwsgi.py

Настройка sudo и Zabbix-агента

Теперь дадим пользователю zabbix права на запуск этого скрипта от имени пользователя root. Запускаем visudo и добавляем в файл конфигурации две строчки:
Defaults:zabbix !requiretty
zabbix ALL=(www-data:ALL) NOPASSWD:/etc/zabbix/uwsgi.py *
Отредактируем файл конфигурации Zabbix-агента /etc/zabbix/zabbix_agentd.conf:
UserParameter=uwsgi.discover,/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py discover
UserParameter=uwsgi.total[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py total $1
UserParameter=uwsgi.idle[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py idle $1
UserParameter=uwsgi.busy[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py busy $1
UserParameter=uwsgi.pidle[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py pidle $1
UserParameter=uwsgi.pbusy[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py pbusy $1
UserParameter=uwsgi.listen_queue[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py listen_queue $1
UserParameter=uwsgi.listen_queue_errors[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py listen_queue_errors $1
И перезапустим Zabbix-агента, чтобы новые настройки вступили в силу:
# /etc/init.d/zabbix-agentd restart

Шаблоны Zabbix

Я подготовил два шаблона для Zabbix 3.4:
В обоих шаблонах имеется элемент данных для низкоуровневого обнаружения, который находит все имеющиеся сокет-файлы для статистики приложений. Обнаружение происходит раз в час:

Есть прототипы элементов данных, с помощью которых контролируется: длина очереди, количество ошибок в очереди, общее количество процессов приложения, количество занятых процессов, количество свободных процессов, процент занятых процессов и процент свободных процессов. Значения этих данных для каждого из приложений снимаются раз в минуту:

Пример графика

Приведу пример графика с реального сервера:

Из графиков видно, что приложения maps и bottle иногда используют 100% рабочих процессов, а вот приложение ncc постоянно использует не более 30% рабочих процессов.

воскресенье, 9 июня 2019 г.

Исправление ручного закрытия проблем в Zabbix 3.4

В Zabbix версии 3.2 появилась возможность закрывать проблемы вручную, которая бывает очень полезной, если невозможно сформулировать выражение, при котором проблема должна закрываться автоматически. Например, это может оказаться полезным в триггерах, которые срабатывают при появлении определённой строчки в каком-либо журнале, или если информация о проблеме поступает в виде трапа в элемент данных типа Zabbix-траппер или SNMP trap, особенно если сообщения о восстановлении не предусмотрены или потерялись.

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

Для ручного закрытия проблемы нужно найти проблему и перейти по ссылке в колонке подтверждения. Например, ниже показана страница просмотра проблем с одним из таких триггеров:

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

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

Итак, думаю что вам уже стало понятно, что лучше дать возможность всем закрывать видимые ими проблемы вручную, чем давать им доступ на редактирование настроек узлов. Для этого пересоберём пакет для Debian, внеся в исходные тексты веб-интерфейса соответствующие исправления.

Добавим официальные репозитории с файлами для сборки пакетов в файл /etc/apt/sources.list:
deb-src http://repo.zabbix.com/zabbix/3.4/debian stretch main
Установим ключ репозитория Zabbix и обновим список доступных пакетов:
# wget http://repo.zabbix.com/zabbix-official-repo.key -O - | apt-key add -
# apt-get update
Установим необходимые для сборки пакетов Zabbix зависимости и скачаем файлы для сборки пакетов:
# apt-get build-dep zabbix
# apt-get source zabbix
Ставим пакеты с инструментами, которые пригодятся нам для пересборки пакета:
# apt-get install dpkg-dev devscripts fakeroot
Переходим в каталог zabbix-3.4.12-1+stretch, в который распаковались исходные тексты пакета, скачиваем и накладываем заплатку:
# cd zabbix-3.4.12-1+stretch
# wget http://stupin.su/files/zabbix3_4_12_manual_close_event_acknowledge.diff
# patch -Np0 < zabbix3_4_12_manual_close_event_acknowledge.diff
Текст самой заплатки:
Description: Fixed manual closing problems for users without rights to edit trigger
Fixed manual closing problems for users without rights to edit trigger

Author: Vladimir Stupin <vladimir@stupin.su>
Last-Update: <2019-03-01>

--- zabbix-3.4.12-1+wheezy.orig/frontends/php/app/controllers/CControllerAcknowledgeEdit.php
+++ zabbix-3.4.12-1+wheezy/frontends/php/app/controllers/CControllerAcknowledgeEdit.php
@@ -132,7 +132,6 @@ class CControllerAcknowledgeEdit extends
                        'output' => [],
                        'triggerids' => $triggerids,
                        'filter' => ['manual_close' => ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED],
-                       'editable' => true,
                        'preservekeys' => true
                ]);
--- zabbix-3.4.12-1+wheezy.orig/app/controllers/CControllerAcknowledgeCreate.php        2019-03-01 16:15:28.931300319 +0500
+++ zabbix-3.4.12-1+wheezy/app/controllers/CControllerAcknowledgeCreate.php     2019-03-01 16:15:14.211550047 +0500
@@ -87,7 +87,6 @@
                                'output' => [],
                                'triggerids' => $triggerids,
                                'filter' => ['manual_close' => ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED],
-                               'editable' => true,
                                'preservekeys' => true
                        ]);
--- zabbix-3.4.12-1+wheezy.orig/include/classes/api/services/CEvent.php 2019-03-01 16:18:17.636438283 +0500
+++ zabbix-3.4.12-1+wheezy/include/classes/api/services/CEvent.php      2019-03-01 16:18:24.272325613 +0500
@@ -860,7 +860,6 @@
                        'eventids' => $eventids,
                        'source' => EVENT_SOURCE_TRIGGERS,
                        'object' => EVENT_OBJECT_TRIGGER,
-                       'editable' => true
                ]);
 
                if ($events_count != count($events)) {
Если всё прошло успешно, то можно внести комментарии к изменениям. Запускаем команду для редактирования журнала изменений:
# dch -i
И приводим последнюю запись к подобному виду:
zabbix (1:3.4.12-1+stretch-p2) UNRELEASED; urgency=low

  * Non-maintainer upload.
  * Fixed manual closing problems for users without rights to edit trigger

 -- Vladimir Stupin <vladimir@stupin.su>  Thu, 07 Mar 2019 11:04:14 +0500
Теперь можно собрать двоичные пакеты и исправленный пакет с исходными текстами:
# dpkg-buildpackage -us -uc -rfakeroot
В каталоге выше появятся собранные пакеты, которые можно установить при помощи dpkg. Нас прежде всего интересует пересобранный пакет с веб-интерфейсом Zabbix:
# dpkg -i zabbix-frontend-php_3.4.12-1+stretch-p2_all.deb

воскресенье, 2 июня 2019 г.

Пересборка libiksemel для решения проблемы JABBER tls handshake failed в Zabbix

После обновления операционной системы Debian с Wheezy до Stretch перестала работать отправка сообщений через Jabber. При обновлении пакет libiksemel3 обновился с версии 1.2-4 до версии 1.4-3. В журнале сервера Zabbix /var/log/zabbix/zabbix_server.log обнаружились строчки вида:
1769:20190422:083154.224 JABBER: [monitoring@jabber.server.ru/xxx] tls handshake failed
В интернете можно найти массу примеров того, как люди справляются с подобными проблемами:
Как видно, большинство статей ограничивается обходными решениями. Лишь в третьей статье сделана попытка справиться с источником проблем. Проблема эта, правда, была другая и её решение тоже не было корректным: вместо увеличения таймаута автор статьи по-сути просто отключил проверку таймаута.

Поскольку раньше отправка сообщений в Jabber у меня работала без нареканий, то я сразу заподозрил, что в библиотеке libiksemel, при помощи которой Zabbix отправляет сообщения в Jabber, повысились требования к безопасности используемых протоколов и шифров. Для начала я решил узнать, какие протоколы и шифры поддерживает Jabber-сервер.

Jabber-сервер по умолчанию принимает подключения от клиентов на TCP-порт 5222. Через этот порт принимаются как подключения без шифрования, так и с шифрованием. При подключении клиент узнаёт о возможностях сервера и может согласовать с ним шифрование данных, если сервер такую возможность позволяет. К сожалению, я не знаю, как проверить список поддерживаемых протоколов и шифров, подключившись на этот порт.

Однако, для совместимости со старыми клиентами Jabber-сервер обычно принимает подключения также и на порт 5223, на котором сразу после подключения нужно согласовать шифрование, а затем через зашифрованное соединение уже обмениваться данными по протоколу XMPP.

libiksemel для установки защищённых подключений использует библиотеку GNU TLS. Попробуем воспользоваться утилитой командной строки gnutls-cli-debug, использующей эту библиотеку, для проверки возможностей Jabber-сервера. Для этого установим пакет gnutls-bin:
# apt-get install gnutls-bin
Вызываем утилиту для получения списка возможностей Jabber-сервера:
$ gnutls-cli-debug -p 5223 jabber.server.ru
Утилита выводит следующую информацию:
Warning: getservbyport(5223) failed. Using port number as service.
GnuTLS debug client 3.5.8
Checking jabber.server.ru:5223
                             for SSL 3.0 (RFC6101) support... yes
                        whether we need to disable TLS 1.2... no
                        whether we need to disable TLS 1.1... no
                        whether we need to disable TLS 1.0... no
                        whether %NO_EXTENSIONS is required... no
                               whether %COMPAT is required... no
                             for TLS 1.0 (RFC2246) support... yes
                             for TLS 1.1 (RFC4346) support... no
                                  fallback from TLS 1.1 to... TLS 1.0
                             for TLS 1.2 (RFC5246) support... no
                                  fallback from TLS 1.6 to... failed (server requires fallback dance)
              for inappropriate fallback (RFC7507) support... yes
                               for certificate chain order... sorted
                  for safe renegotiation (RFC5746) support... yes
                     for Safe renegotiation support (SCSV)... yes
                    for encrypt-then-MAC (RFC7366) support... no
                   for ext master secret (RFC7627) support... no
                           for heartbeat (RFC6520) support... no
                       for version rollback bug in RSA PMS... no
                  for version rollback bug in Client Hello... no
            whether the server ignores the RSA PMS version... no
whether small records (512 bytes) are tolerated on handshake... yes
    whether cipher suites not in SSL 3.0 spec are accepted... yes
whether a bogus TLS record version in the client hello is accepted... yes
         whether the server understands TLS closure alerts... no
            whether the server supports session resumption... no
                      for anonymous authentication support... no
                      for ephemeral Diffie-Hellman support... no
                   for ephemeral EC Diffie-Hellman support... no
                             for curve SECP256r1 (RFC4492)... no
                             for curve SECP384r1 (RFC4492)... no
                             for curve SECP521r1 (RFC4492)... no
           for curve X25519 (draft-ietf-tls-rfc4492bis-07)... no
                  for AES-128-GCM cipher (RFC5288) support... no
                  for AES-128-CCM cipher (RFC6655) support... no
                for AES-128-CCM-8 cipher (RFC6655) support... no
                  for AES-128-CBC cipher (RFC3268) support... yes
             for CAMELLIA-128-GCM cipher (RFC6367) support... no
             for CAMELLIA-128-CBC cipher (RFC5932) support... yes
                     for 3DES-CBC cipher (RFC2246) support... yes
                  for ARCFOUR 128 cipher (RFC2246) support... yes
            for CHACHA20-POLY1305 cipher (RFC7905) support... no
                                       for MD5 MAC support... yes
                                      for SHA1 MAC support... yes
                                    for SHA256 MAC support... no
                              for ZLIB compression support... no
                     for max record size (RFC6066) support... no
                for OCSP status response (RFC6066) support... no
              for OpenPGP authentication (RFC6091) support... no
Как видно, сервер поддерживает только протоколы SSL 3.0 и TLS 1.0, а протоколы TLS 1.1 и TLS 1.2 не поддерживаются. Не понял я только, откуда в выводе утилиты взялся TLS 1.6.

Теперь распакуем пакет с исходными текстами, чтобы проверить, какие настройки используются:
# apt-get source libiksemel3
В файле src/stream.c имеется строчка:
const char *priority_string = "SECURE256:+SECURE192:-VERS-TLS-ALL:+VERS-TLS1.2";
Видно, что Jabber-сервер поддерживает только протоколы SSL3 и TLS1.0, а libiksemel требует, чтобы сервер поддерживал как минимум TLS1.2. Безопасность - это, конечно, хорошо, но, как мне кажется, не стоит бежать впереди паровоза и выставлять настройки более жёсткие, чем это принято в библиотеке GNU TLS по умолчанию.

Прежде чем менять строку приоритетов и пересобирать пакет, можно протестировать новую строку приоритетов при помощи такой команды:
$ gnutls-cli --priority 'NORMAL:-VERS-SSL3.0' -p 5223 jabber.server.ru
Если подключение устанавливается успешно с указанной строкой приоритетов, можно продолжать.

Я поменял строку приоритетов следующим образом:
const char *priority_string = "NORMAL:-VERS-SSL3.0";
Вы можете оставить там только слово NORMAL, т.к. я добавил запрет использовать протокол SSL3.0 для того, чтобы принудить клиента использовать протокол TLS1.0, поддерживаемый используемым мной сервером Jabber. Подробнее о формате строки приоритетов можно почитать, например, здесь: 7.9 Priority strings

Подтверждаем изменения, формируя из них заплату:
# dpkg-source --commit
В качестве имени заплаты я указал строчку downgrade_gnutls_priority_string_for_jabber.server.ru

У меня получился такая заплатка:
Description: Downgrade GNU TLS priority string for jabber.server.ru
 Downgrade GNU TLS priority string for jabber.server.ru
Author: Vladimir Stupin <vladimir@stupin.su>
Last-Update: 2019-04-22

--- libiksemel-1.4.orig/src/stream.c
+++ libiksemel-1.4/src/stream.c
@@ -63,7 +63,7 @@ tls_pull (iksparser *prs, char *buffer,
 static int
 handshake (struct stream_data *data)
 {
-       const char *priority_string = "SECURE256:+SECURE192:-VERS-TLS-ALL:+VERS-TLS1.2";
+       const char *priority_string = "NORMAL:-VERS-SSL3.0";
        int ret;

        if (gnutls_global_init () != 0)
Запускаем команду для обновления журнала изменений пакета:
# dch -i
Добавляем такую запись:
libiksemel (1.4-3+b1u1) UNRELEASED; urgency=medium

  * Downgrade GNU TLS priority string for jabber.server.ru

 -- Vladimir Stupin <vladimir@stupin.ru>  Mon, 22 Apr 2019 10:29:11 +0300
Осталось собрать пакеты с исходными текстами и двоичные пакеты:
# dpkg-buildpackage -us -uc -rfakeroot
Можно установить libiksemel из собранных пакетов, перезапустить сервер Zabbix и проверить отправку сообщений в Jabber. У меня всё заработало.