воскресенье, 29 декабря 2019 г.

Решение проблемы с SSL/TLS в Sylpheed

На рабочем компьютере решил обновить Debian с релиза Stretch до Buster и столкнулся с проблемой: при попытке проверить почту Sylpheed на POP3-сервере вылетает окно с ошибкой:

Если попытаться запустить Sylpheed через окно терминала, можно увидеть чёть больше подробностей:
$ sylpheed 

(sylpheed:7387): LibSylph-WARNING **: 15:42:26.003: SSL_connect() failed with error 1, ret = -1 (error:1425F102:SSL routines:ssl_choose_client_version:unsupported protocol)


(sylpheed:7387): LibSylph-WARNING **: 15:42:26.003: can't start TLS session.


(sylpheed:7387): LibSylph-WARNING **: 15:42:26.004: [15:42:26] Сбой сокета.
Как видно, POP3-сервер не поддерживает требуемую версию протокола SSL/TLS.

В прошлом мне уже приходилось разбираться с подобной проблемой при отправке уведомлений из Zabbix на сервер Jabber: Пересборка libiksemel для решения проблемы JABBER tls handshake failed в Zabbix. В этот раз я попробовал пойти тем же путём, установил утилиту командной строки gnutls-cli-debug и воспользоваться ей для проверки возможностей Jabber-сервера.

Устанавливаем пакет gnutls-bin:
# apt-get install gnutls-bin
Вызываем утилиту для получения списка возможностей Jabber-сервера:
$ gnutls-cli-debug -p 995 mail.server.ru
Утилита выводит следующую информацию:
GnuTLS debug client 3.6.7
Checking mail.server.ru:995
                        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
                             for TLS 1.3 (RFC8446) support... no
|<1>| FFDHE groups advertised, but server didn't support it; falling back to server's choice
                       TLS1.2 neg fallback from TLS 1.6 to... TLS1.0
                                     for HTTPS server name... unknown
                               for certificate chain order... sorted
                  for safe renegotiation (RFC5746) support... 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... yes
            whether the server supports session resumption... no
                      for anonymous authentication support... no
|<1>| FFDHE groups advertised, but server didn't support it; falling back to server's choice
                      for ephemeral Diffie-Hellman support... yes
|<1>| FFDHE groups advertised, but server didn't support it; falling back to server's choice
                        for RFC7919 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 (RFC8422)... no
                      for AES-GCM cipher (RFC5288) support... no
                      for AES-CCM cipher (RFC6655) support... no
                    for AES-CCM-8 cipher (RFC6655) support... no
                      for AES-CBC cipher (RFC3268) support... yes
                 for CAMELLIA-GCM cipher (RFC6367) support... no
                 for CAMELLIA-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 max record size (RFC6066) support... no
                for OCSP status response (RFC6066) support... no
Сервер POP3 не поддерживает новейшие версии протокола TLS1.2 и TLS1.1, но поддерживает TLS1.0. Видимо почтовый клиент пытается использовать более безопасную версию протокола и не соглашается на TLS1.0.

Решение проблемы несколько затянулось, т.к. первоначально я пошёл по ложному следу. Попробовал удалить сначала пакет openssl, а потом libgnutls30. Пакет sylpheed попадал в список удаляемых пакетов лишь во втором случае. На самом деле sylpheed зависел не от библиотеки libgnutls30, а от библиотеки libssl1.1. Для изменения настроек OpenSSL, которые используются по умолчанию, оказалось достаточно поправить файл /etc/ssl/openssl.cnf.

В файле были прописаны такие настройки:
[system_default_sect]
MinProtocol = TLSv1.2
CipherString = DEFAULT@SECLEVEL=2
Для того, чтобы sylpheed успешно подключился к почтовому серверу, оказалось достаточно поменять минимальный требуемый протокол, вот так:
[system_default_sect]
MinProtocol = TLSv1.0
CipherString = DEFAULT@SECLEVEL=2

воскресенье, 22 декабря 2019 г.

Использование quilt для подготовки заплат

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

Кроме того, накладывать заплатки приходится не только на сам Zabbix, но и на связанные с ним библиотеки:
Правки приходится делать в разных пакетах, не только связанных непосредственно с Zabbix, из-за чего я даже завёл репозиторий для доработанных пакетов. В заметке Создание своего репозитория Debian при помощи aptly можно найти ещё несколько примеров доработанных пакетов, доработке некоторых из которых были посвящены отдельные заметки.

Количество специфичных заплаток для Zabbix, с которыми приходится работать, со временем только увеличивается. Если aptly помогает упорядочить работу с большим количеством нестандартных пакетов, то quilt помогает упорядочить работу с большим количеством заплаток одного и того же пакета.

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

Ниже кратко описаны основные команды quilt, которые могут пригодиться для управления заплатками.

Создаём новую заплатку:
$ quilt new permit-edit-maintenances
Добавляем в заплатку файлу, которые собираемся менять:
$ quilt add frontends/php/maintenance.php
$ quilt add frontends/php/include/classes/api/services/CMaintenance.php
Посмотреть список файлов, содержимое которых будет отслеживаться в заплате, можно при помощи команды:
$ quilt files
Чтобы изменения в файлах не отслеживались в заплате, можно воспользоваться такой командой:
$ quilt remove config.guess config.sub database/mysql/create.sql database/postgresql/create.sql database/sqlite3/create.sql
Редактируем файлы:
$ vim frontends/php/maintenance.php
$ vim frontends/php/include/classes/api/services/CMaintenance.php
Посмотреть получившуюся заплатку можно при помощи следующей команды:
$ quilt diff
Сохранить получившуюся заплатку в каталог debian/patches можно при помощи следующей команды:
$ quilt refresh
Список всех заплаток можно посмотреть при помощи команды:
$ quilt series
Текущая редактируемая заплата в выведенном списке будет подсвечена.

При необходимости редактировать не последнюю заплатку, можно перемещаться по списку заплат. Для перемещения по списку на предыдущую заплату можно воспользоваться командой:
$ quilt pop
Для применения текущей заплаты и для перехода к следующей по списку можно воспользоваться командой:
$ quilt push
Импорт заплатки из внешнего источинка осуществяется следующим образом:
$ quilt import zabbix3_4_12_permit_edit_maintenances.patch
Сразу после импорта заплатку нужно применить:
$ quilt push
Не стоит продолжать импорт заплаток без применения, т.к. применяться они будут в обратном порядке - послденяя импортированная будет первой применённой. Это сбивает с толку и может вызывать проблемы, если заплатки зависят друг от друга.

воскресенье, 15 декабря 2019 г.

OpenSMTPd как локальный SMTP-ретранслятор

В прошлом я писал заметки Postfix как локальный SMTP-ретранслятор и Postfix как локальный SMTP-ретранслятор во FreeBSD, в которых описывал настройку Postfix для пересылки писем администратору системы.

К списку самых распространённых полноформатных SMTP-серверов для Unix, в который входят Sendmail, Exim, Postfix и, с некоторой натяжкой, qmail, присоединился ещё один - OpenSMTPd. Этот сервер был разработан в рамках проекта OpenBSD и по архитектуре напоминает Postfix и qmail: он тоже состоит из нескольких отдельных взаимодействующих друг с другом процессов.

OpenSMTPd появился в репозиториях Debian и я решил попробовать настроить его в качестве замены Postfix из указанных выше статей. Первым делом установим OpenSMTPd из пакетов:
# apt-get install opensmtpd
Приводим файл конфигурации /etc/smtpd.conf к следующему виду:
listen on lo

table aliases file:/etc/aliases
table secrets file:/etc/secrets

accept from local for local virtual <aliases> deliver to mbox
accept from local for any relay via tls+auth://user@mail.server.net:25 auth  as "user@server.net"
В файл /etc/aliases прописываем переадресации для получателей писем:
postmaster: root
root: admin@domain.tld
В файл /etc/secrets прописываем пароли для учётных записей:
user password
Имя user должно совпадать с указанным в URL релея tls+auth://user@mail.server.net:25.

Поменяем права доступа к созданным файлам:
# chown root:root /etc/aliases /etc/secrets /etc/smtpd.conf
# chmod u=rw,go=r /etc/aliases /etc/smtpd.conf
# chmod u=rw,go= /etc/secrets
Проверить правильность файла конфигурации можно при помощи следующей команды:
# smtpd -n
Запускаем почтовый сервер:
# systemctl restart opensmtpd
При редактировании файлов, указанных в опциях table, нужно сообщить об изменении демону, чтобы он перечитал содержимое таблиц. Например, после редактирования таблицы aliases нужно воспользоваться такой командой:
# smtpctl update table aliases
К сожалению, пока что в OpenSMTPd нельзя менять тему писем таким же образом, как это было описано в заметке Смена темы письма в Postfix. У Postfix простой файл конфигурации, но большое количество настроек, взаимодействующих между собой неочевидным образом. В этом плане OpenSMTPd выглядит достойной альтернативой, т.к. сочетает простоту и наглядность настройки с безопасной архитектурой.

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

воскресенье, 8 декабря 2019 г.

Резервное копирование файлов и баз данных через SSH

Когда-то давно на работе задумались о резервном копировании серверов. У меня к тому времени уже был наработанный подход к резервному копированию, которым пользуются многие системные администраторы Unix.

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

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

Управление архивом

Начнём с того, что нужно организовать резервные копии в хранилище таким образом, чтобы можно было легко найти нужную резервную копию и удалять устаревшие резервные копии. Файлы будем именовать по схеме YYYYMMDD_filename. При добавлении новой резервной копии filename нужно будет удостовериться, что новый файл не пуст. Если с файлом всё в порядке, то можно снабдить его префиксом YYYYMMDD_, а затем найти и удалить устаревшие резервные копии этого файла. Для выполнения этих функций в скрипте предусмотрены переменная KEEP_DAYS и функция с не самым удачным названием clear_old, которая берёт на себя описанные задачи:
KEEP_DAYS=14

clear_old()
{
# $1 - backup filename

# Если файл с указанным именем не существует
if [ ! -f "$1" ]
then
  return 0
fi

size=`du --bytes "$1" | cut -f1`

# Если размер меньше 512 байт
if [ $size -lt 512 ]
then
  # Удаляем сам файл резервной, а старые резервные копии не трогаем
  rm $1
  return 1
else
  # Переименовываем новый файл резервной копии
  mv $1 `date "+%Y%m%d"`_$1
  
  # Удаляем предыдущие файлы старше KEEP_DAYS дней
  find . -name \*$1 -mtime +$KEEP_DAYS -delete
  return 0
fi
}
Чтобы добавить файл в архив, нужно вызвать функцию clear_old и передать ей имя добавляемого в архив файла.

Резервное копирование локальных файлов

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

В случае с FreeBSD функция резервного копирования файлов выглядела следующим образом:
BACKUP_USER=rbackup
BACKUP_GROUP=rbackup

freebsd_files_local()
{
# $1 - backup filename

touch "$1"
chown $BACKUP_USER:$BACKUP_GROUP "$1"
chmod 0600 "$1"

tar -cjf - -T- --exclude '*.sql.gz' --exclude 'etc/zabbix/xbackup/*' <<END 2>/dev/null > "$1"
/etc/
/usr/local/etc/
/root/
/usr/home/
/usr/local/www/
END

clear_old "$1"
if [ $? -ne 0 ]
then
  echo "Backing up local system to file $1 failed"
fi
}
Функция принимает один аргумент - имя файла, в котором нужно сохранить резервную копию файлов. Создаваемый файл архива будет иметь формат .tbz (или .tar.bz2).

Перед созданием архива сначала создаётся пустой файл, владельцем которого становится пользователь, указанный в переменной BACKUP_USER, группой-владельцем становится группа, указанная в переменной BACKUP_GROUP, права доступа к файлу выставляются таким образом, что читать и писать его может только пользователь, указанный в переменной BACKUP_USER. Это делается для того, чтобы предотвратить чтение файла резервной копии посторонними пользователями в процессе его создания.

В архив помещаются файлы из каталогов /etc/, /usr/local/etc/, /usr/local/www/, /root/ и /usr/home/, кроме файлов с расширением .sql.gz и файлов из каталога /etc/zabbix/xbackup/. По-сути, в архив сохраняются файлы конфигурации, файлы из домашних каталогов пользователей и файлы веб-приложений, за исключением файлов с резервными копиями баз данных.

После создания архива для файла вызывается функция clear_old, которая переименовывает файл, снабжая его имя префиксом YYYYMMDD_ с текущей датой, и удаляет устаревшие экземпляры этого файла.

В переменных BACKUP_USER и BACKUP_GROUP выше указаны пользователь и группа с именем rbackup. Чтобы создать их, можно воспользоваться такими командами:
# pw add group rbackup
# pw add user rbackup -g rbackup -c "User for backup purposes" -d /usr/home/rbackup -m
В случае с Linux функция резервного копирования файлов была такой:
linux_files_local()
{
# $1 - backup filename

touch "$1"
chown $BACKUP_USER:$BACKUP_GROUP "$1"
chmod 0600 "$1"

tar -cjf - --files-from=- --exclude 'home/*/.pycharm_helpers/*' --exclude 'root/.cpan/*' --exclude 'root/.cache/*' --exclude 'home/*/.cache/*' --exclude 'usr/local/lib/*' <<END 2>/dev/null > "$1"
/etc/
/root/
/home/
/usr/local/
/usr/lib/zabbix/
/usr/share/mapnik/
/var/www/
/var/lib/dokuwiki/
END

clear_old "$1"
if [ $? -ne 0 ]
then
  echo "Backing up local system to file $1 failed"
fi
}
В целом эта функция не отличается от функции для FreeBSD. Резервному копированию подвергаются каталог с файлами конфигурации, домашние каталоги пользователей, каталог веб-приложений, а также каталог /usr/local/, и каталоги с файлами Zabbix и DokuWiki. Из резервного копирования исключаются файлы, создаваемые средой разработки PyCharm (она умеет работать по SSH), каталоги с кэшированными данными, модулями Perl.

Аналогично FreeBSD, в Linux нужно создать пользователя и группу rbackup, который будут использоваться в качестве владельца резервных копий:
# groupadd rbackup
# useradd -c "User for backup purposes" -d /home/rbackup -m -g rbackup rbackup

Резервное копирование файлов с удалённых систем

Резервное копирование удалённых файловых систем работает аналогично, с той лишь разницей, что команды резервного копирования запускаются по SSH, а их стандартный вывод сохраняется в файл в локальной файловой системе.
SSH_PRIVKEY=/root/.ssh/id_rsa

freebsd_files()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - backup filename

touch "$3"
chown $BACKUP_USER:$BACKUP_GROUP "$3"
chmod 0600 "$3"

ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "/usr/local/bin/sudo /usr/bin/tar -cjf - -T- --exclude '*.sql.gz' --exclude 'etc/zabbix/xbackup/*'" <<END 2>/dev/null > "$3"
/etc/
/usr/local/etc/
/root/
/usr/home/
/usr/local/www/
END
  
clear_old "$3"
if [ $? -ne 0 ]
then
  echo "Backing up remote system $1:$2 to file $3 failed"
fi
}
Функции передаются три аргумента:
  1. IP-адрес или доменное имя удалённой системы, резервную копию файлов с которой нужно снять,
  2. порт SSH-сервера на этой системе (было время, когда использовались системы с SSH-сервером на нестандартном порту),
  3. имя файла создаваемого архива.
Файлы, подлежащие резервному копированию, без особых проблем сможет прочитать только пользователь root. Однако, в целях безопасности, не хотелось бы разрешать удалённый доступ по SSH для пользователя root, пусть аутентификация и производится с использованием ключей. Не хочется также разрешать скрипту резервного копирования выполнять по SSH какие угодно команды. Для того, чтобы скрипт резервного копирования не смог повредить систему, случайно - из-за ошибки администратора, или специально - если доступ к скрипту получил злоумышленник, резервное копирование производится с использованием учётной записи из переменной BACKUP_USER и sudo.

Для копирования публичного SSH-ключа в домашний каталог пользователя из переменной BACKUP_USER на FreeBSD я пользовался такими командами, которые просто копировал в терминал при настройке нового сервера или виртуальной машины:
# chown rbackup:rbackup /usr/home/rbackup
# mkdir /home/rbackup/.ssh
# chown rbackup:rbackup /usr/home/rbackup/.ssh
# cat <<END > /usr/home/rbackup/.ssh/authorized_keys
ТУТ ПУБЛИЧНЫЙ SSH-КЛЮЧ
END
# chown rbackup:rbackup /usr/home/rbackup/.ssh/authorized_keys
Для того, чтобы разрешить пользователю rbackup запускать через sudo команду tar для резервного копирования файлов, я использовал запускал visudo и вставлял такие настройки:
Defaults:rbackup !requiretty
rbackup    ALL=(root:ALL) NOPASSWD:/usr/bin/tar -cjf - -T- *
Для Linux аналогичная функция выглядит следующим образом:
linux_files()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - backup filename

touch "$3"
chown $BACKUP_USER:$BACKUP_GROUP "$3"
chmod 0600 "$3"

ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "/usr/bin/sudo /bin/tar -cjf - --files-from=- --exclude 'home/*/.pycharm_helpers/*' --exclude 'root/.cpan/*' --exclude 'root/.cache/*' --exclude 'home/*/.cache/*' --exclude 'usr/local/lib/*'" 2>/dev/null <<END > "$3"
/etc/
/root/
/home/
/usr/local/
/usr/lib/zabbix/
/usr/share/mapnik/
/var/www/
/var/lib/dokuwiki/
END

clear_old "$3"
if [ $? -ne 0 ]
then
  echo "Backing up remote system $1:$2 to file $3 failed"
fi
}
Для настройки публичных SSH-ключей использовались аналогичные команды:
# mkdir /home/rbackup/.ssh
# chown rbackup:rbackup /home/rbackup/.ssh
# cat <<END > /home/rbackup/.ssh/authorized_keys
ТУТ ПУБЛИЧНЫЙ SSH-КЛЮЧ  
END
# chown rbackup:rbackup /home/rbackup/.ssh/authorized_keys
Для настройки прав в sudo использовались такие строчки:
Defaults:rbackup !requiretty
rbackup ALL=(root:ALL) NOPASSWD:/bin/tar -cjf - --files-from=- *

Резервное копирование удалённой базы данных MySQL

MySQL является сетевым сервером, поэтому резервные копии баз данных можно снимать по сети, не прибегая к помощи SSH. Однако, для того, чтобы не заниматься настройкой фильтрации пакетов, а также не гонять по сети лишний объём данных в открытом виде, резервное копирование баз данных было решено выполнять тоже через SSH.
PASSWORD=тут-пароль

generic_mysql()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - database name
# $4 - backup filename

touch "$4"
chown $BACKUP_USER:$BACKUP_GROUP "$4"
chmod 0600 "$4"
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "mysqldump --single-transaction -u$BACKUP_USER -p$PASSWORD $3 | bzip2" 2>/dev/null > "$4"
  
clear_old "$4"
if [ $? -ne 0 ]
then
  echo "Backing up $3 database from system $1:$2 to file $4 failed"
fi
}
Функция принимает 4 аргумента:
  1. IP-адрес или доменное имя удалённой системы, резервную копию файлов с которой нужно снять,
  2. порт SSH-сервера на этой системе (было время, когда использовались системы с SSH-сервером на нестандартном порту),
  3. имя базы данных, резервную копию которой нужно снять,
  4. имя файла создаваемого архива.
Функция вызывает mysqldump для получения резервной копии требуемой базы данных, а для сжатия данных перед отправкой через сеть используется компрессор bzip2. Соответственно, резервная копия, которая будет получена при помощи этой функции, имеет формат .sql.bz2.

Для снятия резервной копии используется пользователь с тем же именем, который является владельцем архивов и используется для подключения к удалённым системам по SSH. Его имя настроено в переменной BACKUP_USER. А для аутентификации этого пользователя используется пароль, указанный в переменной PASSWORD. Понятно, что этот пользователь должен создан и ему должны быть предоставлены права доступа к указанным базам данных. Для этого можно воспользоваться такими запросами:
CREATE USER 'rbackup'@'localhost' IDENTIFIED BY 'тут-пароль';
FLUSH PRIVILEGES;
GRANT SHOW DATABASES, SELECT, LOCK TABLES, RELOAD ON *.* TO 'rbackup'@'localhost';
FLUSH PRIVILEGES;
Т.к. в функции не используется каких-то специфичных путей к файлам и нет необходимости указывать полный путь к команде, выполняемой через sudo, то эта функция пригодна для использования и с FreeBSD и с Linux.

Резервное копирование удалённой базы данных PostgreSQL

Функция резервного копирования базы данных PostgreSQL в целом аналогична функции для резервного копирования базы данных MySQL. Для снятия резервной копии используется утилита pg_dump:
generic_pgsql()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - database name
# $4 - backup filename

touch "$4"
chown $BACKUP_USER:$BACKUP_GROUP "$4"
chmod 0600 "$4"
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "env PGPASSWORD=$PASSWORD pg_dump -U$BACKUP_USER $3 | bzip2" > "$4" 2>/dev/null

clear_old "$4"
if [ $? -ne 0 ]
then
  echo "Backing up $3 database from system $1:$2 to file $4 failed"
fi
}
Для снятия резервной копии нужно создать пользователя, указанного в переменной BACKUP_USER, и предоставить ему права доступа ко всем базам данных, резервные копии которых будет необходимо снимать. Войдя в систему под пользователем postgres, создаём пользователя для резервного копирования и вводим его пароль в процессе его создания:
$ createuser -D -R -I -S -P rbackup
Теперь нужно подключиться к каждой из баз данных при помощи команды psql -d база-данных и выполнить следующие запросы:
GRANT SELECT ON ALL TABLES IN SCHEMA public TO rbackup;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO rbackup;

Группировка действий по резервному копированию

Для снятия всех необходимых резервных копий с одного сервера можно создавать функции следующего вида:
backup_server()
{
linux_files 'server.domain.tld' 22 'server.tbz'

generic_mysql 'server.domain.tld' 22 'mysql' 'server_mysql.sql.bz2'
generic_mysql 'server.domain.tld' 22 'base' 'server_base.sql.bz2'

generic_pgsql 'server.domain.tld' 22 'database' 'server_database.sql.bz2'
}

Выполнение резервного копирования

Теперь в скрипте имеются функции для снятия резервных копий каждого сервера. Осталось только создать каталог для хранения резервных копий и выполнить резервное копирование. Для этого создадим каталог и выставим права доступа к нему:
# mkdir /backups
# chown rbackup:rbackup /backups
# chmod u=rwx,g=rx,o= /backups
Теперь впишем в скрипт команды перехода в каталог с резервными копиями, на всякий случай поменяем маску создаваемых файлов и последовательно вызовем функции резервного копирования всех требуемых серверов:
cd /backups
umask 0077

backup_server1
backup_server2

Резервное копирование резервных копий

Не стоит складывать все яйца в одну корзину. На случай, если с резервными копиями в основном месте хранения что-нибудь случится, можно выполнить резервное копирование резервных копий на другой сервер. Для этого на другом сервере создаётся аналогичный каталог, в который при помощи rsync синхронизируются изменения из каталога на основном сервере:
/usr/bin/rsync -a --delete-after -e "ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=22 -i $SSH_PRIVKEY" /backups/ $BACKUP_USER@backup-server.domain.tld.:/backups/
Один процесс rsync запускается локально, один процесс rsync запускается удалённо по SSH. Оба процесса обмениваются друг с другом информацией через SSH. Взаимодействуя друг с другом, они копируют недостающие файлы, обновляют фрагменты изменившихся файлов, удаляют файлы, ставшие не нужными.

Отладочная информация

Для того, чтобы иметь представление о времени начала и завершения тех или иных этапов резервного копирования, можно добавлять в функции или в тело скрипта отладочные сообщения с отметками времени. Например, в свой скрипт я вставил отладочные сообщения, фиксирующие моменты начала и завершения обновления основного архива и моменты начала и завершения синхронизации резервного архива:
echo "BACKUP FINISHED: "`date "+%Y-%m-%d %H:%M:%S"`
# тут резервное копирование серверов
echo "BACKUP STARTED: "`date "+%Y-%m-%d %H:%M:%S"`
  
echo "RSYNC STARTED: "`date "+%Y-%m-%d %H:%M:%S"`
# тут вызов rsync
echo "RSYNC FINISHED: "`date "+%Y-%m-%d %H:%M:%S"`

Заключение

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

воскресенье, 1 декабря 2019 г.

Скрипты управления списком IP-адресов в iptables/ipset и ipfw/table

Года 4 назад на работе перевёл всех Zabbix-агентов в активный режим, т.к. этот режим должен быть эффективнее чем опрос обычных пассивных Zabbix-агентов. Для снятия данных с обычных Zabbix-агентов сервер Zabbix сам устанавливает подключение к Zabbix-агенту, запрашивает у него необходимые метрики, после чего отключается. Для этого сервер Zabbix используют процессы poller, каждый из которых бывает занят не только во время активных действий, но и во время ожидания данных от Zabbix-агента. Если же Zabbix-агент работает в активном режиме, то сервер Zabbix не предпринимает никаких активных действий, а ждёт действий со стороны агента. Активный Zabbix-агент подключается к серверу Zabbix, запрашивает у него список метрик, за которыми нужно наблюдать, и периодичность их контроля. После этого Zabbix-агент самостоятельно собирает данные с необходимой периодичностью и отправляет их на сервер Zabbix. В этом случае сервер Zabbix использует процессы trapper, которые работают только во время приёма уже готовых данных. На самом деле на фоне общей нагрузки снижение использования ресурсов оказалось совсем незаметным, но речь сейчас не об этом.

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

Чтобы автоматизировать процесс добавления IP-адресов в сетевой фильтр на сервере Zabbix, а также максимально снизить возможность отправки поддельных данных с любого свободного IP-адреса, решил написать скрипт, который будет извлекать из базы данных Zabbix список IP-адресов интерфейсов из тех сетевых узлов, на которых есть элементы данных, имеющие тип "Zabbix-агент (активный)".

Для Linux с его iptables и ipset получился такой скрипт под названием ipset_auto.sh, который можно поместить в планировщик задач cron:
#!/bin/sh

AWK="/usr/bin/awk"
SORT="/usr/bin/sort"
UNIQ="/usr/bin/uniq"
IPSET="/sbin/ipset"
XARGS="/usr/bin/xargs"

update()
{
  SET="$1"
  NEED_IPS="$2"

  CURRENT_IPS=`$IPSET list $SET | $AWK '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ { print $0; }'`

  DIFF_IPS=`(echo "$NEED_IPS" ; echo -n "$CURRENT_IPS") | $SORT | $UNIQ -u`
  ADD_IPS=`(echo "$NEED_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`
  DEL_IPS=`(echo "$CURRENT_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`

  if [ -n "$ADD_IPS" ]
  then
    echo "--- $SET add ---"
    echo "$ADD_IPS"
    echo "$ADD_IPS" | $XARGS -n1 $IPSET add $SET
  fi

  if [ -n "$DEL_IPS" ]
  then
    echo "--- $SET del ---"
    echo "$DEL_IPS"
    echo "$DEL_IPS" | $XARGS -n1 $IPSET del $SET
  fi
}

# ZABBIX

MYSQL=`$AWK '/^DBUser=/ { split($0, a, "=");
                          user = a[2]; }

             /^DBPassword=/ { split($0, a, "=");
                              password = a[2]; }

             /^DBName=/ { split($0, a, "=");
                          db = a[2]; }

             /^DBHost=/ { split($0, a, "=");
                          host = a[2]; }

             END { if (user && password && host && db) 
                     print "/usr/bin/mysql --connect-timeout=5 -u" user " -p" password " -h" host " " db;
                   else if (user && password && db)
                     print "/usr/bin/mysql --connect-timeout=5 -u" user " -p" password " " db; }' /etc/zabbix/zabbix_server.conf`

if [ -z "$MYSQL" ]
then
  echo "MYSQL not defined"
  exit
fi

NEED_IPS=`$MYSQL -N <<END 2>/dev/null
SELECT DISTINCT interface.ip
FROM items
JOIN hosts ON hosts.hostid = items.hostid
  AND hosts.status = 0
  AND hosts.proxy_hostid IS NULL
JOIN interface ON interface.hostid = items.hostid
  AND interface.type = 1
  AND interface.ip <> '127.0.0.1'
WHERE items.type = 7
  AND items.status = 0;
END
`
ERROR=$?
if [ $ERROR -ne 0 ]
then
  echo "Failed to execute SQL-query"
  exit
fi

update "zabbix_auto" "$NEED_IPS"
Для подключения к базе данных (в данном случае это MySQL, но переделка под другие СУБД тривиальна) скрипт использует настройки из файла конфигурации /etc/zabbix/zabbix_server.conf. Список требуемых IP-адресов в переменной NEED_IPS формируется SQL-запросом, который можно переработать под свои нужды. Например, у меня в скрипте есть ещё пара SQL-запросов, управляющих списками IP-адресов в множествах tftp_auto и ciu_auto. В последней строке скрипта функция update обновляет множество zabbix_auto так, чтобы в нём были только IP-адреса из переменной NEED_IPS.

Для создания множества IP-адресов zabbix_auto в ipset можно воспользоваться командой:
# ipset create zabbix_auto hash:ip
Для создания правила в iptables, которое разрешит всем IP-адресам из множества zabbix_auto взаимодействовать с сервером Zabbix, можно воспользоваться командой:
# iptables -A INPUT -p tcp -m set --match-set zabbix_auto src -m tcp --dport 10051 -j ACCEPT
Аналогичный скрипт для ipfw/table называется ipfw_auto.sh и выглядит следующим образом:
#!/bin/sh

AWK="/usr/bin/awk"
SED="/usr/bin/sed"
SORT="/usr/bin/sort"
UNIQ="/usr/bin/uniq"
XARGS="/usr/bin/xargs"

update()
{
  TABLE="$1"
  NEED_IPS="$2"

  IPFW=`$AWK -v TABLE="$TABLE" '{ split($0, a, "=");
                                  if (a[1] == TABLE)
                                  {
                                    table = a[2];
                                    print "/sbin/ipfw table " a[2];
                                  }
                                }' /etc/firewall.conf`

  if [ -z "$IPFW" ]
  then
    echo "IPFW not defined"
    exit
  fi

  CURRENT_IPS=`$IPFW list | $SED -e 's/\/32 0$//'`

  DIFF_IPS=`(echo "$NEED_IPS" ; echo -n "$CURRENT_IPS") | $SORT | $UNIQ -u`
  ADD_IPS=`(echo "$NEED_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`
  DEL_IPS=`(echo "$CURRENT_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`

  if [ -n "$ADD_IPS" ]
  then
    echo "--- $TABLE add ---"
    echo "$ADD_IPS"
    echo "$ADD_IPS" | $XARGS -n1 $IPFW add
  fi

  if [ -n "$DEL_IPS" ]
  then
    echo "--- $TABLE del ---"
    echo "$DEL_IPS"
    echo "$DEL_IPS" | $XARGS -n1 $IPFW delete
  fi
}

MYSQL=`$AWK '/^DBUser=/ { split($0, a, "=");
                          user = a[2]; }

             /^DBPassword=/ { split($0, a, "=");
                              password = a[2]; }

             /^DBName=/ { split($0, a, "=");
                          db = a[2]; }

             /^DBHost=/ { split($0, a, "=");
                          host = a[2]; }

             END { if (user && password && host && db) 
                     print "/usr/local/bin/mysql --connect-timeout=5 -u" user " -p" password " -h" host " " db;
                   else if (user && password && db)
                     print "/usr/local/bin/mysql --connect-timeout=5 -u" user " -p" password " " db; }' /usr/local/etc/zabbix34/zabbix_server.conf`

if [ -z "$MYSQL" ]
then
  echo "MYSQL not defined"
  exit
fi

# ZABBIX

NEED_IPS=`$MYSQL -N <<END 2>/dev/null
SELECT DISTINCT interface.ip
FROM items
JOIN hosts ON hosts.hostid = items.hostid
  AND hosts.status = 0
  AND hosts.proxy_hostid IS NULL
JOIN interface ON interface.hostid = items.hostid
  AND interface.type = 1
  AND interface.ip <> '127.0.0.1'
WHERE items.type = 7
  AND items.status = 0;
END
`
ERROR=$?
if [ $ERROR -ne 0 ]
then
  echo "Failed to execute SQL-query"
  exit
fi

update "table_zabbix_auto" "$NEED_IPS"
Особенность этого скрипта заключается в том, что в ipfw таблицы не имеют имён, а нумеруются. Номер таблицы выясняется через файл /etc/firewall.conf, в котором переменной с именем таблицы присваивается соответствующий номер. Например, для таблицы table_ssh номер задаётся следующим образом:
table_ssh=100

Подробнее о настройке ipfw/table можно прочитать в одной из моих прошлых заметок: Настройка ipfw во FreeBSD.

Активные Zabbix-агенты и база данных Zabbix приведены для примера, а вообще эти скрипты можно приспособить для любых других целей. Можно скачивать список IP-адресов с веб-страницы (главное, чтобы её не подменили и чтобы она не оказалась внезапно пустой), можно воспользоваться в каком-нибудь самодельном биллинге для открытия доступа пользователям, прошедшим авторизацию и закрытия доступа пользователям, превысившим лимит. Можно сочетать одно с другим.

FreeBSD на работе постепенно заменяем на Debian, поэтому скрипт ipfw_auto.sh скоро станет мне не нужным. Что касается Debian, то netfilter/iptables в Debian Buster уже заменён на nftables/nft. Пока что утилита iptables никуда не делась и умеет работать с nftables, но в будущем скрипт ipset_auto.sh тоже утратит актуальность и потребует переработки. Оба скрипта, однако, пока что могут пригодиться кому-нибудь ещё, поэтому решил поделиться ими.

воскресенье, 24 ноября 2019 г.

Добавление в репозиторий aptly пакетов новой архитектуры

Ещё одна микрозаметка. Не так давно я уже писал об aplty - утилите для управления репозиториями пакетов Debian: Создание своего репозитория Debian при помощи aptly.

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

Удаляем существующую публикацию репозитория с именем stretch в каталоге repo:
$ aptly publish drop stretch repo
Removing /home/stupin/.aptly/public/repo/dists/stretch...
Cleaning up prefix "repo" components main...

Published repository has been removed successfully.
Публикуем репозиторий на том же месте снова, указывая явным образом список публикуемых архитектур пакетов:
$ aptly publish repo -architectures="amd64,armhf,sources" -skip-signing=true stretch repo
Loading packages...
Generating metadata files and linking package files...
Finalizing metadata files...

Local repo stretch has been successfully published.
Please setup your webserver to serve directory '/home/stupin/.aptly/public' with autoindexing.
Now you can add following line to apt sources:
  deb http://your-server/repo/ stretch main
Don't forget to add your GPG key to apt with apt-key.

You can also use `aptly serve` to publish your repositories over HTTP quickly.
При дальнейших обновлениях уже опубликованного репозитория все архитектуры, указанные при его первичной публикации, будут обрабатываться обычным образом:
$ aptly publish update -skip-signing=true stretch repo
Loading packages...
Generating metadata files and linking package files...
Finalizing metadata files...
Cleaning up prefix "repo" components main...

Publish for local repo repo/stretch [amd64, armhf, sources] publishes {main: [stretch]} has been successfully updated.
Посмотреть список архитектур пакетов в уже опубликованных репозиториях можно следующим образом:
$ aptly publish list
Published repositories:
  * repo/buster [amd64, source] publishes {main: [buster]}
  * repo/stretch [amd64, armhf, sources] publishes {main: [stretch]}
  * repo/wheezy [amd64, source] publishes {main: [wheezy]}

воскресенье, 17 ноября 2019 г.

Проверка действительности SSL-сертификата в urllib2

Эта очень короткая заметка представляет собой дополнение к одной из моих прошлых заметок: Использование urllib2 в Python. После обновления Debian с релиза Wheezy до релиза Stretch библиотека urllib2 при обращении к HTTPS-ресурсам стала проверять действительность сертификата. Т.к. в моём случае на этих ресурсах использовался сертификат, подписанный удостоверяющим центром локальной сети, то обращения к таким ресурсам приводили к ошибкам: библиотека urllib2 в подобных случаях исторгала из своих недр исключение ssl.SSLError.

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

Первым делом нужно установить в систему стандартные сертификаты удостоверяющих центров. Сделать это можно путём установки пакета ca-certificates:
# apt-get install ca-certificates
Дополнительные сертификаты удостоверяющих центров, которым следует доверять, можно положить в каталог /usr/local/share/ca-certificates, после чего обновить файл /etc/ssl/certs/ca-certificates.crt при помощи незамысловатой команды:
# update-ca-certificates
Теперь можно указать файл /etc/ssl/certs/ca-certificates.crt библиотеке urllib2. Сделать это можно, добавив в список обработчиков запросов дополнительный обработчик, который занимается проверкой действительности SSL-сертификатов. В прошлой статье уже имеются примеры использования сразу нескольких разных обработчиков. Если воспользоваться тем же подходом, то получится примерно так:
handlers = []

# Добавляем обработчик HTTPS-запросов
context = ssl.create_default_context()
context.load_verify_locations('/etc/ssl/certs/ca-certificates.crt')
handler = urllib2.HTTPSHandler(context=context)
handlers.append(handler)

opener = urllib2.build_opener(*handlers)

воскресенье, 10 ноября 2019 г.

Контроль работоспособности DHCP-сервера в Zabbix

Как-то раз на работе потребовалось поставить на контроль DHCP-сервер. Сделать это нужно было, как обычно, срочно. Пока я делал, заказчики постоянно "держали руку на пульсе", интересовались результатами и поторапливали. Пытался воспользоваться различными утилитами командной строки, но в итоге наиболее пригодным вариантом оказалось написание собственного скрипта на python с использованием библиотеки ScaPy, благо в интернете можно найти примеры подобных скриптов. Собирались испытать скрипт на одном DHCP-сервере, а потом поставить на контроль ещё несколько десятков.

Года через два наткнулся на этот забытый скрипт. Раз уж он никому не нужен, то можно попытаться довести его до ума и поделиться им. Забрал скрипт домой и стал экспериментировать на виртуалках. Пока доводил скрипт до ума, на работе снова возникла необходимость поставить на контроль один DHCP-сервер, никак не связанный с теми, для которых скрипт первоначально писался.

Принципы работы DHCP

Для начала ознакомимся немого с принципами работы DHCP.

Выдача настроек по DHCP происходит в следующей последовательности:
  1. клиент, желающий получить настройки, посылает широковещательный запрос DHCPDISCOVERY, при помощи которого пытается обнаружить доступные DHCP-серверы,
  2. сервер откликается на запрос DHCPDISCOVERY и отвечает пакетом DHCPOFFER, в котором предлагает клиенту использовать определённый IP-адрес,
  3. клиент отправляет серверу запрос DHCPREQUEST, в котором просит закрепить за ним указанный IP-адрес,
  4. сервер отвечает клиенту пакетом DHCPACK, в котором подтверждает, что IP-адрес закреплён за клиентом. Если сервер по каким-то причинам не может закрепить за клиентом указанный им IP-адрес, то отвечает пакетом DHCPNAK.
Для продления аренды ранее выданного сервером IP-адреса клиент использует запросы DHCPREQUEST, указывая в запросе ранее выданный ему IP-адрес.

Также протоколом предусмотрены пакеты DHCPDECLINE и DHCPINFO.

При помощи DHCPDECLINE клиент может сообщить серверу, что предложенный им IP-адрес уже используется.

Если клиенту не нужен IP-адрес от DHCP-сервера, то вместо запроса DHCPREQUEST клиент может отправить запрос DHCPINFORM для запроса дополнительных сетевых параметров. Так же как и в ответ на запрос DHCPREQUEST, на запрос DHCPINFORM сервер отвечает пакетом DHCPACK.

Если клиент хочет вернуть IP-адрес DHCP-серверу, он может отправить серверу запрос DHCPRELEASE. Если сервер получит такой запрос, он помечает IP-адрес как свободный. Протоколом не предусмотрен ответ на запросы DHCPRELEASE, поэтому невозможно убедиться в том, что сервер получил и обработал запрос.

Описание скрипта dhcp.py

А теперь перейдём к делу. Для контроля работоспособности DHCP-сервера был подготовлен скрипт dhcp.py, принимающий следующие аргументы:
  • имя сетевого интерфейса, за которым находится контролируемый DHCP-сервер,
  • MAC-адрес клиента, который будет использоваться в запросах к DHCP-серверу. По умолчанию - MAC-адрес сетевого интерфейса,
  • IP-адрес, который ожидается получить от DHCP-сервера. По умолчанию клиент будет принимать любой IP-адрес,
  • IP-адреса DHCP-серверов, ответ от которых ожидается получить. Если на пути между клиентом и DHCP-сервером имеются DHCP-релеи, то тут нужно указывать настоящие IP-адреса серверов, а не IP-адреса релеев. Можно указать несколько IP-адресов, разделив их двоеточиями. По умолчанию принимаются ответы от любых DHCP-серверов,
  • время ожидания ответа, по умолчанию составляет 2 секунды.
Обязательно для указания только имя интерфейса. Все остальные настройки не обязательны и могут быть опущены.

Алгоритм работы скрипта следующий:
  • через указанный интерфейс отправляется запрос DHCPDISCOVERY с указанным MAC-адресом или MAC-адресом интерфейса,
  • в течение таймаута принимаются пакеты DHCPOFFER,
  • если пакетов DHCPOFFER не поступило, выполнение скрипта прерывается,
  • если указан ожидаемый IP-адрес, то проверяется, что в DHCPOFFER предложен именно этот IP-адрес. Если DHCP-сервер предложил другой IP-адрес, то выполнение скрипта прерывается,
  • если указан список IP-адресов DHCP-серверов, то проверяется, что в DHCPOFFER указан IP-адрес одного из ожидаемых DHCP-серверов. Если получен ответ от другого DHCP-сервера, то выполнение скрипта прерывается,
  • отправляется запрос DHCPREQUEST с просьбой выдать в аренду предложенный IP-адрес,
  • в течение таймаута принимаются пакеты DHCPACK,
  • если пакетов DHCPACK не поступило, выполнение скрипта прерывается,
  • отправляется запрос DHCPRELEASE на возврат арендованного IP-адреса.
Коды ошибок скрипта, которые он выдаёт на стандартный вывод:
  1. указанный сетевой интерфейс не существует,
  2. неправильный MAC-адрес,
  3. нет ответа DHCPOFFER,
  4. неправильный пакет DHCPOFFER,
  5. получен ответ от нежелательного DHCP-сервера,
  6. предложен нежелательный IP-адрес,
  7. нет ответа DHCPACK,
  8. неправильный пакет DHCPACK.
Для работы скрипту необходимы привилегии пользователя root, но Zabbix работает под простым пользователем, поэтому придётся воспользоваться sudo. Если использовать скрипт dhcp.py как скрипт внешнего опроса, то для его запуска через sudo потребуется создать ещё один скрипт. Но такое решение не кажется мне красивым. Я решил вызывать скрипт dhcp.py через Zabbix-агента, для чего понадобится настроить в конфигурации агента UserParameter, где найдётся место и для sudo.

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

Сначала установим пакет sudo, если он ещё не установлен:
# apt-get install sudo
И выдадим пользователю zabbix, от имени которого работает Zabbix-агент, права запускать скрипт dhcp.py с правами пользователя root:
Defaults:zabbix !requiretty
zabbix ALL=(ALL) NOPASSWD:/etc/zabbix/dhcp.py *
Теперь положим скрипт dhcp.py в каталог /etc/zabbix и выдадим права запускать его:
# chmod +x /etc/zabbix/dhcp.py
В конфигурации Zabbix-агента в файле /etc/zabbix/zabbix_agentd.conf добавим строчку:
UserParameter=dhcp[*],/usr/bin/sudo /etc/zabbix/dhcp.py "$1" "$2" "$3" "$4" "$5"
После изменения конфигурации Zabbix-агента, его нужно перезапустить:
# systemctl restart zabbix-agent

Шаблоны для Zabbix

Для того, чтобы в последних данных можно было наглядно видеть результат последней проверки, я подготовил файл для преобразования значений Valuemap_DHCP.xml. В файле созданы следующие преобразования значений:

Также я подготовил два варианта шаблона:
  • Template_App_DHCP.xml - шаблон для использования с Zabbix-агентом, работающим в обычном, пассивном режиме,
  • Template_App_DHCP_Active.xml - шаблон для использования с Zabbix-агентом, работающим в активном режиме.
В шаблоне имеются макросы, при помощи которых можно настроить элемент данных, который контролирует состояние DHCP-сервера. Указано только одно значение по умолчанию для имени интерфейса - eth0, т.к. этот аргумент обязателен для указания.

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

К этому элементу данных присоединено 8 триггеров, два из которых имеют высокую важность, т.к. свидетельствуют о неработоспособности DHCP-сервера или о появлении чужого DHCP-сервера. Остальные триггеры имеют уровень важности "Предупреждение":

Пример использования

Для примера я настроил контроль исправности DHCP-сервера на своём домашнем компьютере. В виртуальную машину с Zabbix проброшен интерфейс ens7, смотрящий в локальную сеть с DHCP-сервером. На интерфейсе отсутствуют сетевые настройки, используется он только для контроля исправности DHCP-сервера. На уровне сетевого узла созданы макросы, переопределяющие значения по умолчанию, определённые в шаблоне:

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

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

воскресенье, 13 октября 2019 г.

Контроль в Zabbix параметров SMART дисков, подключенных к аппаратному RAID-массиву

Эта заметка продолжает идеи предыдущих заметок, описывающих контроль аппаратных RAID-массивов и параметров S.M.A.R.T. жёстких дисков:
На этот раз мы поставим на контроль средствами Zabbix не только аппаратные RAID-массивы на основе контроллеров LSI MegaRAID SAS, но и параметры S.M.A.R.T. жёстких дисков, подключенных к этому RAID-контроллеру. В отличие от прошлой заметки по S.M.A.R.T., где использовался общий для всех дисков порог по количеству перемещённых секторов и секторов, ожидающих перемещения, на этот раз в шаблоне будет предусмотрена индивидуальная настройка порогов для каждого из дисков.

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

Для контроля состояния аппаратного RAID-массива нам понадобится утилита megacli. Установить утилиту megacli в Debian можно из неофициального репозитория HwRAID. Например, чтобы подключить репозиторий в Debian Stretch, нужно добавить в файл /etc/apt/sources.list такую строчку:
deb http://hwraid.le-vert.net/debian stretch main
Установим в систему GPG-ключ для проверки подлинности репозитория при помощи команды:
# wget -O - https://hwraid.le-vert.net/debian/hwraid.le-vert.net.gpg.key | apt-key add -
Теперь можно обновить список пакетов, доступных для установки из репозиториев:
# apt-get update
И установить утилиту mecacli для управления RAID-контроллером:
# apt-get install megacli
Утилита smartctl имеется в официальных репозиториях Debian, установить её можно при помощи следующей команды:
# apt-get install smartmontools
Также для получения списка SAS-дисков (они же - Serial Attached SCSI) нам понадобится также утилита lsscsi, которую можно установить из одноимённого пакета. Пакет lsscsi тоже имеется в официальных репозиториях, установим его:
# apt-get install lsscsi
Теперь пользователю zabbix, от имени которого работает Zabbix-агент, дать права вызывать утилиты megacli и smartctl. Для этого воспользуемся утилитой sudo. Если она ещё не установлена, то установить её можно при помощи команды:
# apt-get install sudo
Запускаем visudo для редактирования прав доступа через sudo:
# visudo
Добавим следующую строчку, чтобы Zabbix-агент мог вызывать утилиту sudo в неинтерактивном режиме:
Defaults:zabbix !requiretty
Добавляем права запускать megacli и smartctl:
zabbix ALL=(ALL) NOPASSWD: /usr/sbin/megacli -LDInfo -Lall -aALL, \
                           /usr/sbin/megacli -AdpBbuCmd -GetBbuStatus -aALL, \
                           /usr/sbin/megacli -PdList -aALL
                           /usr/sbin/smartctl -d megaraid\,* -i *, \
                           /usr/sbin/smartctl -d megaraid\,* -H *, \
                           /usr/sbin/smartctl -d megaraid\,* -A *
Создаём скрипт /etc/zabbix/megacli.sh:
#!/bin/sh

(/usr/bin/lsscsi 2>&1 ; /usr/bin/sudo /usr/sbin/megacli -PdList -aALL 2>&1) \
  | /usr/bin/awk -F: 'BEGIN { print "{\"data\": [";
                              n = 0; }
  
                      /^\[/ { split($0, cols, / +/);
                              hctl = cols[1];
                              device = cols[6];

                              gsub(/\[/, "", hctl);
                              gsub(/\]/, "", hctl);
                              split(hctl, cols, /:/);
                              array = cols[1];

                              array_device[array] = device; }

                      /^Adapter #/ { split($0, cols, /\#/);
                                     array = cols[2]; }

                      $1 ~ /^Slot Number$/ { slot = $2; }

                      $1 ~ /^Device Id$/ { id = $2;
                                           if (n > 0)
                                             printf ",\n";
                                           printf "{\"{#DEVICE}\": \"%s\", ", array_device[array];
                                           printf "\"{#ARRAY}\": \"%d\", ", array;
                                           printf "\"{#SLOT}\": \"%d\", ", slot;
                                           printf "\"{#ID}\": \"%d\"}", id;
                                           n += 1; }

                      END { printf "\n]}"; }'
Выставялем права доступа к скрипту:
# chown root:root /etc/zabbix/megacli.sh
# chmod u=rwx,go=rx /etc/zabbix/megacli.sh
Добавляем в файл /etc/zabbix/zabbix_agentd.conf следующие строчки:
UserParameter=raid.status,/usr/bin/sudo /usr/sbin/megacli -LDInfo -Lall -aALL | /usr/bin/awk 'BEGIN { s = 1; } /^State.*:.*(No.*ptimal|Degraded)$/ { s = 0; } END { print s; }'
UserParameter=raid.no_battery,/usr/bin/sudo /usr/sbin/megacli -AdpBbuCmd -GetBbuStatus -aALL | /bin/grep -cE '(Battery Pack Missing.*es|Battery State.*Missing|The required hardware component is not present)'
UserParameter=raid.battery,/usr/bin/sudo /usr/sbin/megacli -AdpBbuCmd -GetBbuStatus -aALL | /usr/bin/awk 'BEGIN { s = 1; } /^Battery State: *Not? (Optimal|Operational)/ { s = 0; } /^Battery State: *(Learning|Charging|Discharging)/ { s = 2; } END { print s; }'

UserParameter=raid.smart.list,/etc/zabbix/megacli.sh
UserParameter=raid.smart.model[*],/usr/bin/sudo /usr/sbin/smartctl -d megaraid,$2 -i $1 2>&1 | /usr/bin/awk -F: '$$1 ~ /^Vendor$/ { gsub(/(^ +| +$)/, "", $$2); model = $$2; } $$1 ~ /^Product$/ { gsub(/(^ +| +$)/, "", $$2); print model " " $$2; }'
UserParameter=raid.smart.serial[*],/usr/bin/sudo /usr/sbin/smartctl -d megaraid,$2 -i $1 2>&1 | /usr/bin/awk -F: '$$1 ~ /^Serial number$/ { gsub(/(^ +| +$)/, "", $$2); print $$2; }'
UserParameter=raid.smart.health[*],/usr/bin/sudo /usr/sbin/smartctl -d megaraid,$2 -H $1 2>&1 | /usr/bin/awk 'BEGIN { h = 0; } / (OK|PASSED)$/ { h = 1; } END { print h; }'
UserParameter=raid.smart.reallocated[*],/usr/bin/sudo /usr/sbin/smartctl -d megaraid,$2 -A $1 2>&1 | /usr/bin/awk -F: '$$1 ~ /^Elements in grown defect list$/ { print $$2; }'
UserParameter=raid.smart.temperature[*],/usr/bin/sudo /usr/sbin/smartctl -d megaraid,$2 -A $1 2>&1 | /usr/bin/awk -F: '$$1 ~ /^Current Drive Temperature$/ { gsub(/ C/, "", $$2); print $$2; }'
Осталось перезапустить Zabbix-агента:
# systemctl restart zabbix-agent

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

Загружаем модуль ядра mfip, который позволяет считывать параметры S.M.A.R.T. сквозь RAID-контроллер:
# kldload mfip
Прописываем его в автозагрузку в файле /boot/loader.conf:
mfip_load="YES"
Как и в случае с Linux, во FreeBSD нам понадобятся утилиты megacli, smartctl и sudo. К счастью, все их можно установить из портов systuils/megacli, sysutils/sudo и sysutils/smartmontools:
# cd /usr/port/sysutils/megacli
# make install
# cd /usr/port/sysutils/smartmontools
# make install
# cd /usr/ports/sysutils/sudo
# make install
Также для получения списка дисков нам потребуется утилита, аналогичная lsscsi в Linux. Во FreeBSD для этого воспользуемся утилитой camcontrol, которая имеется в базовой системе.

Запускаем visudo для редактирования прав доступа через sudo:
# visudo
Добавим следующую строчку, чтобы Zabbix-агент мог вызывать утилиту sudo в неинтерактивном режиме:
Defaults:zabbix !requiretty
Добавляем права запускать camcontrol и smartctl:
%zabbix     ALL=(ALL) NOPASSWD:/usr/local/sbin/MegaCli -PdList -aALL, \
                               /usr/local/sbin/MegaCli -LDInfo -Lall -aALL, \
                               /usr/local/sbin/MegaCli -AdpBbuCmd -GetBbuStatus -aALL, \
                               /sbin/camcontrol rescan all, \
                               /sbin/camcontrol devlist, \
                               /usr/local/sbin/smartctl -i *, \
                               /usr/local/sbin/smartctl -H *, \
                               /usr/local/sbin/smartctl -A *
Создаём скрипт /usr/local/etc/zabbix34/megacli.sh:
#!/bin/sh

/usr/local/bin/sudo /sbin/camcontrol rescan all >/dev/null 2>/dev/null

(/usr/local/bin/sudo /sbin/camcontrol devlist 2>&1 ; /usr/local/bin/sudo /usr/local/sbin/MegaCli -PdList -aALL 2>&1) \
  | /usr/bin/awk -F: 'BEGIN { print "{\"data\": [";
                              n = 0; }

                      /scbus/ { match($0, /scbus[0-9]+ /);
                                array = substr($0, RSTART + 5, RLENGTH - 6);

                                match($0, /target [0-9]+ /);
                                id = substr($0, RSTART + 7, RLENGTH - 8);

                                match($0, /\(.+[0-9]+\)/);
                                device_with_id = substr($0, RSTART + 1, RLENGTH - 2);

                                match(device_with_id, /[0-9]+/);
                                device = "/dev/" substr(device_with_id, 1, RSTART - 1);
                                device_id = substr(device_with_id, RSTART, RLENGTH);

                                array_device[array "_" id] = device;
                                array_device_id[array "_" id] = device_id; }

                      /^Adapter #/ { split($0, cols, /\#/);
                                     array = cols[2]; }

                      $1 ~ /^Slot Number$/ { slot = $2; }

                      $1 ~ /^Device Id$/ { id = $2 + 0;
                                           if (n > 0)
                                             printf ",\n";
                                           printf "{\"{#DEVICE}\": \"%s\", ", array_device[array "_" id];
                                           printf "\"{#ARRAY}\": \"%d\", ", array;
                                           printf "\"{#SLOT}\": \"%d\", ", slot;
                                           printf "\"{#ID}\": \"%d\"}", array_device_id[array "_" id];
                                           n += 1; }

                      END { printf "\n]}"; }'
Добавляем в файл /usr/local/etc/zabbix34/zabbix_agentd.conf следующие строчки:
UserParameter=raid.status,/usr/local/bin/sudo /usr/local/sbin/MegaCli -LDInfo -Lall -aALL 2>&1 | /usr/bin/awk 'BEGIN { s = 1; } /^State.*:.*(No.*ptimal|Degraded)$/ { s = 0; } END { print s; }'
UserParameter=raid.no_battery,/usr/local/bin/sudo /usr/local/sbin/MegaCli -AdpBbuCmd -GetBbuStatus -aALL 2>&1 | /usr/bin/grep -cE '(Battery Pack Missing.*es|Battery State.*issing|The required hardware component is not present)'
UserParameter=raid.battery,/usr/local/bin/sudo /usr/local/sbin/MegaCli -AdpBbuCmd -GetBbuStatus -aALL 2>&1 | /usr/bin/awk 'BEGIN { s = 1; } /^Battery State: *Not? (Optimal|Operational)/ { s = 0; } /^Battery State: *(Learning|Charging|Discharging)/ { s = 2; } END { print s; }'
  
UserParameter=raid.smart.list,/usr/local/etc/zabbix34/megacli.sh
UserParameter=raid.smart.model[*],/usr/local/bin/sudo /usr/local/sbin/smartctl -i $1$2 2>&1 | /usr/bin/awk -F: '$$1 ~ /^Vendor$/ { gsub(/(^ +| +$)/, "", $$2); model = $$2; } $$1 ~ /^Product$/ { gsub(/(^ +| +$)/, "", $$2); print model " " $$2; }'
UserParameter=raid.smart.serial[*],/usr/local/bin/sudo /usr/local/sbin/smartctl -i $1$2 2>&1 | /usr/bin/awk -F: '$$1 ~ /^Serial number$/ { gsub(/(^ +| +$)/, "", $$2); print $$2; }'
UserParameter=raid.smart.health[*],/usr/local/bin/sudo /usr/local/sbin/smartctl -H $1$2 2>&1 | /usr/bin/awk 'BEGIN { h = 0; } / (OK|PASSED)$/ { h = 1; } END { print h; }'
UserParameter=raid.smart.reallocated[*],/usr/local/bin/sudo /usr/local/sbin/smartctl -A $1$2 2>&1 | /usr/bin/awk -F: '$$1 ~ /^Elements in grown defect list$/ { print $$2; }'
UserParameter=raid.smart.temperature[*],/usr/local/bin/sudo /usr/local/sbin/smartctl -A $1$2 2>&1 | /usr/bin/awk -F: '$$1 ~ /^Current Drive Temperature$/ { gsub(/ C/, "", $$2); print $$2; }'
Выставялем права доступа к скрипту:
# chown root:wheel /usr/local/etc/zabbix34/megacli.sh
# chmod u=rwx,go=rx /usr/local/etc/zabbix34/megacli.sh
Перезапускаем Zabbix-агента:
# /usr/local/etc/rc.d/zabbix_agentd restart

Шаблоны для Zabbix

Я подготовил два шаблона для контроля состояния RAID-контроллера и параметров S.M.A.R.T. жёстких дисков, которые к нему подключены:
В шаблоне есть три элемента данных. Один контролирует целостность RAID-массивов, второй - наличие батарей в контроллерах, третий - состояние каждой из батарей:

Каждому из упомянутых элементов данных соответствует один триггер и ещё один триггер срабатывает, если батарея контроллера заряжается или разряжается:


Для обнаружения жёстких дисков, подключенных к RAID-контроллеру, создано правило низкоуровневого обнаружения:

В правиле имеется 5 прототипов элементов данных для контроля параметров S.M.A.R.T. каждого жёсткого диска. Раз в час снимаются модель диска и его серийный номер, раз в 10 минут снимаются данные об исправности диска, его температуре и количестве перемещённых секторов:

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

Для настройки порога срабатывания триггера по количеству перемещённых секторов в шаблоне имеется макрос {$SMART_REALLOCATED_LIMIT}, значение которого по умолчанию равно нулю:

Как можно заметить, в выражениях триггеров этот макрос используется в виде {$SMART_REALLOCATED_LIMIT:"{#ARRAY}/{#SLOT}"}. Макросы такого вида описаны в Руководстве по Zabbix, 7 Настройка, 10 Макросы, 2 Пользовательские макросы, Контекст пользовательских макросов.

При срабатывании триггера вида "RAID-массив 0, слот 2: Количество перемещённых секторов 13 > 0" можно переопределить значение макроса для конкретного диска. Чтобы погасить этот триггер, на уровне узла можно определить макрос {$SMART_REALLOCATED_LIMIT:0/2} со значением 13. Порог срабатывания триггеров на других жёстких дисках останется прежним - будет использоваться значение по умолчанию, взятое из шаблона.

воскресенье, 6 октября 2019 г.

Установка Minecraft в Debian Buster

На официальном сайте minecraft можно скачать deb-пакет с программой для запуска игры Minecraft. Этот пакет, однако, не устанавливается в Debian Buster, т.к. требует в качестве зависимости установить openjdk-8-jre, а имеющийся в репозитории пакет openjdk-11-jre его не устраивает. К счастью, это легко исправить. Для этого нужно всего-лишь распаковать пакет, исправить список зависимостей, и запаковать обратно.

Распаковываем пакет в каталог Minecraft:
$ dpkg-deb -R Minecraft.deb Minecraft
Открываем в текстовом редакторе файл Minecraft/DEBIAN/control, находим строчку Depends и заменяем первую зависимость:
Depends: openjdk-11-jre, ...
Запаковываем файлы обратно в пакет:
$ dpkg-deb -b Minecraft minecraft-launcher_2.1.5965_all.deb
Получившийся пакет устанавливаем при помощи следующей команды:
# dpkg -i minecraft-launcher_2.1.5965_all.deb
Пакет не установится, ссылаясь на то, что для его работы не хватает нескольких пакетов. Автоматическая установка недостающих пакетов при помощи команды # apt-get install -f не срабатывает - удаляется сам пакет с программой для запуска Minecraft.

Устанавливаем необходимые зависимости вручную:
# apt-get install ca-certificates openjdk-11-jre gconf-service libpango1.0-0 libcurl4
Возможно также понадобится установить пакет libglfw3:
# apt-get install libglfw3
Теперь пробуем установить программу снова:
# dpkg -i minecraft-launcher_2.1.5965_all.deb

На этот раз всё проходит успешно. Можно запускать игру:

Экран программы для запуска игры:

Меню игры:

Игра:

воскресенье, 29 сентября 2019 г.

Установка и настройка драйверов nvidia для X-сервера

Заменил в компьютере одну подержанную устаревшую материнскую плату на другую подержанную, устаревшую чуть менее. На прежней был встроенный видеоконтроллер от Intel, на новой - от Nvidia. Не задумываясь поставил свободный драйвер nouveau, однако столкнулся с проблемами: 1. изображение на экране изредка моргало, как будто кто-то периодически вставляет 25 кадр, 2. компьютер спонтанно зависал, предварительно заливая экран белым цветом в чёрные горизонтальные прерывистые полоски. Стал грешить на видеоконтроллер и его драйвер. Пытался заставить работать драйвер nvidia где-то с полчаса. Попытки увенчались успехом. Пока помнил последовательность настройки, записал всё в блокнот. Теперь из блокнота выбрасываю записи в блог, чтобы в следующий раз не ходить по всем граблям снова.

Для начала узнаем, видеконтроллер какого из производителей установлен в системе:
$ lspci | fgrep VGA
00:0d.0 VGA compatible controller: NVIDIA Corporation C61 [GeForce 7025 / nForce 630a] (rev a2)
В нашем случае это встроенный видеоконтроллер NVidia. Чтобы определить, какой драйвер следует использовать с этим видеоконтроллером, установим утилиту nvidia-detect и запустим её:
# apt-get install nvidia-detect
# nvidia-detect
Утилита должна сообщить имя пакета с драйвером видеоконтроллера. В моём случае это был пакет nvidia-legacy-304xx-driver. Установим этот пакет:
# apt-get install --install-recommends nvidia-legacy-304xx-driver
После установки пакета с драйвером будет собран модуль ядра, который нужно подгрузить. Сделать это можно при помощи следующей команды:
# modprobe nvidia-legacy-304xx
Вписываем модуль ядра nvidia-legacy-304xx в файл /etc/modules, чтобы при перезагрузке системы или включении компьютера этот модуль загружался автоматически.

Также нужно установить поддержку этого драйвера со стороны X-сервера. Для этого ставим пакет с именем xserver-xorg-video-nvidia-legacy-304xx:
# apt-get install xserver-xorg-video-nvidia-legacy-304xx
Свободный драйвер nouveau в X-сервере имеет более высокий приоритет, чем фирменный драйвер от NVidia. Поэтому, чтобы X-сервер использовал именно фирменный драйвер, нужно удалить из системы пакет, отвечающий за поддержку nouveau со стороны X-сервера:
# apt-get remove xserver-xorg-video-nouveau
Осталось перезапустить дисплейный менеджер, чтобы он запустил X-сервер, а X-сервер начал использовать новые драйверы. Я пользуюсь дисплейным менеджером lightdm, поэтому в моём случае это можно сделать следующими командами:
# systemctl stop lightdm
# systemctl start lightdm
По окончании настройки пакет nvidia-detect можно удалить, т.к. вероятность, что он вновь понадобится, довольно мала.
# apt-get purge nvidia-detect
P.S. Кстати, замена драйвера помогла решить проблему.

воскресенье, 22 сентября 2019 г.

Настройка BGP в Quagga

Понадобилось на работе настроить динамическую маршрутизацию по протоколу BGP, чтобы сэкономить время на настройке статических маршрутов. Поэкспериментировал для начала на виртуальных машинах на домашнем компьютере, подготовил для себя шпаргалку. На работе настраивал свои серверы в паре с сетевым администратором, который со своей стороны настраивал аппаратные маршрутизаторы. (Данила, если ты это читаешь, то передаю тебе привет!) Когда работа была выполнена, дополнил шпаргалку несколькими полезными разделами.

Через пару месяцев понадобилось вернуться к настройке BGP по ещё двум поводам. Во-первых, на одной из виртуальных машин в целях резервирования анонсы нужно было принимать сразу от двух соседних маршрутизаторов. Во-вторых, понадобилось настроить сервер, который сам в целях резервирования должен быть доступен через два соседних маршрутизатора. Конфигурацию этого сервера проверил и дополнил другой сетевой администратор. (Андрей, тебе тоже привет!) В результате добавил в шпаргалку ещё несколько пунктов.

По обыкновению выбрасываю отработанный материал в мусоркублог.

Установка пакетов

Для установки демона маршрутизации Quagga с поддержкой одного лишь протокола BGP достаточно установить только один пакет:
# apt-get install quagga-bgpd
Вместе с этим пакетом будет установлен также пакет quagga-core, в котором находится демон zebra. Демон zebra отвечает за добавление маршрутов, полученных от демонов динамической маршрутизации, в системную таблицу маршрутизации.

Настройка демона zebra

Демон zebra выполняет роль прослойки между операционной системой и демонами динамической маршрутизации. Демоны динамической маршрутизации выступают в роли клиентов демона zebra и пользуются его API.

Первоначальная настройка демона zebra проста, нужно создать файл конфигурации /etc/quagga/zebra.conf с содержимым следующего вида:
hostname <hostname>
password <password>
enable password <secret_password>
После этого можно включать автозапуск демона при загрузке системы и запустить сам демон маршрутизации:
# systemctl enable zebra.service
# systemctl start zebra.service
Дальнейшее управление демоном, в том числе его конфигурирование, можно осуществлять при помощи команды telnet:
$ telnet 127.0.0.1 2601
Через telent доступен интерфейс командной строки, похожий на интерфейс командной строки устройств Cisco. После изменения настроек не забывайте сохранять их при помощи команды write.

Я настраивал демона на двух тестовых виртуальных машинах с именами bgp1 и bgp2.

Настройка демона bgpd

Демон bgpd реализует поддержку протокола динамической маршрутизации BGP.

Для первоначальной настройки демона bgpd нужно создать файл /etc/quagga/bgpd.conf.

На первой виртуальной машине содержимое файла конфигурации было примерно таким:
hostname bgp1
password <password>
enable password <secret_password>
!
router bgp 64500
 bgp router-id 169.254.252.6
 network 169.254.253.0/24
 network 169.254.254.0/24
 neighbor 169.254.252.7 remote-as 64500
Эта виртуальная машина лишь анонсирует две сети на вторую виртуальную машину. Номера автономных систем на обеих виртуальных машинах одинаковы и взяты из диапазона 64496-64511 для примеров и документации. Поскольку используются одинаковые номера автономных систем, то в нашем случае будет использоваться вариант протокола iBGP - внутренний протокол пограничных шлюзов.

На второй виртуальной машине содержимое файла конфигурации было примерно следующим:
hostname bgp2
password <password>
enable password <secret_password>
!
router bgp 64500
 bgp router-id 169.254.252.7
 neighbor 169.254.252.6 remote-as 64500
Эта виртуальная машина не анонсировала своих сетей, зато принимала анонсы от первой.

После первоначальной настройки можно на обеих виртуальных машинах включить автозапуск демонов bgpd при загрузке системы и запустить их:
# systemctl enable bgpd.service
# systemctl start bgpd.service
Подобно демону zebra, управление и настройку демона bgpd можно осуществлять при помощи команды telnet:
$ telnet 127.0.0.1 2605
Точно так же через telent доступен интерфейс командной строки, похожий на интерфейс командной строки устройств Cisco. После изменения настроек не забывайте сохранять их при помощи команды write.

Белый список маршрутов

Рассмотрим конфигурирование демона bgpd через telnet. Для примера настроим фильтрацию маршрутов на второй виртуальной машине. Для начала подключаемся через telnet:
$ telnet 127.0.0.1 2605
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.

Hello, this is Quagga (version 1.1.1).
Copyright 1996-2005 Kunihiro Ishiguro, et al.


User Access Verification

Password:
Вводим пароль, который был указан в опции password.

Далее повышаем собственные привилегии при помощи команды enable. В ответ на запрос команды enable вводим пароль, который был указан в опции enable password.
bgp2> enable
Password: 
bgp2#
Далее переходим в режим настройки при помощи команды configure terminal:
bgp2# configure terminal 
bgp2(config)#
Настроим сначала список префиксов, который назовём PREFIX-LIST-FROM-BGP1:
bgp2(config)# ip prefix-list PREFIX-LIST-FROM-BGP1 seq 1 permit 169.254.254.0/24
bgp2(config)# ip prefix-list PREFIX-LIST-FROM-BGP1 seq 2 permit 169.254.253.0/24
bgp2(config)#
Теперь создадим на основе этого списка префиксов маршрутную карту с именем MAP-FROM-BGP1:
bgp2(config) # route-map MAP-FROM-BGP1 permit 10
bgp2(config-route-map)#
Добавим в маршрутную карту список префиксов PREFIX-LIST-FROM-BGP1:
bgp2(config-route-map)# match ip address prefix-list PREFIX-LIST-FROM-BGP1
bgp2(config-route-map)#
И покинем диалог настройки маршрутной карты:
bgp2(config-route-map)# exit
bgp2(config)#
Осталось немного. Переходим в режим настройки протокола динамической маршрутизации bgp:
bgp2(config)# router bgp 64500
bgp2(config-router)#
Добавим созданную маршрутную карту для фильтрации маршрутов, получаемых от соседа:
bgp2(config-router)# neighbor 169.254.252.6 route-map MAP-FROM-BGP1 in
bgp2(config-router)#
Покидаем режим настройки bgp:
bgp2(config-router)# exit
bgp2(config)#
Сохраняем изменения конфигурации на диск:
bgp2(config)# write
Configuration saved to /etc/quagga/bgpd.conf
bgp2(config)#
Осталось выйти из режима конфигурирования:
bgp2(config)# exit
bgp2#
В процессе настройки из любого режима можно проверять правильность каждой введённой команды, просматривая текущий файл конфигурации при помощи команды show running-config:
bgp2(config)# show running-config                          

Current configuration:
!
hostname bgp2
password <password>
enable password <secret_password>
!
router bgp 64500
 bgp router-id 169.254.252.7
 neighbor 169.254.252.6 remote-as 64500
 neighbor 169.254.252.6 route-map MAP-FROM-BGP1 in
!
 address-family ipv6
 exit-address-family
 exit
!
ip prefix-list PREFIX-LIST-FROM-BGP1 seq 1 permit 169.254.254.0/24
ip prefix-list PREFIX-LIST-FROM-BGP1 seq 2 permit 169.254.253.0/24
!
route-map MAP-FROM-BGP1 permit 10
 match ip address prefix-list PREFIX-LIST-FROM-BGP1
!
line vty
!
end
Чтобы посмотреть текущую таблицу маршрутов, можно воспользоваться командой show ip bgp:
bgp2# show ip bgp 
BGP table version is 0, local router ID is 169.254.252.7
Status codes: s suppressed, d damped, h history, * valid, > best, = multipath,
              i internal, r RIB-failure, S Stale, R Removed
Origin codes: i - IGP, e - EGP, ? - incomplete

   Network          Next Hop            Metric LocPrf Weight Path
*>i169.254.253.0/24 169.254.252.6            0     100       0 i
*>i169.254.254.0/24 169.254.252.6            0     100       0 i

Displayed  2 out of 2 total prefixes
Если нужно применить новый или изменённый фильтр к уже принятым маршрутам, можно воспользоваться командой clear ip bgp такого вида:
bgp2# clear ip bgp 169.254.252.6 soft in
bgp2#
После этого все маршруты, не разрешённые маршрутной картой явным образом, должны пропасть из списка текущих маршрутов.

Чёрный список маршрутов

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

Не буду повторять предыдущий раздел, а расскажу кратко. Поскольку логика фильтрации обратная - нужно отбрасывать маршруты из списка, а не принимать их, то в маршрутной карте действие permit заменяется на deny:
bgp2(config)# route-map MAP-FROM-BGP1 deny 10
bgp2(config-route-map)# match ip address prefix-list PREFIX-LIST-FROM-BGP1
bgp2(config-route-map)# exit
bgp2(config)#
В списке префиксов действия permit не меняется. Чтобы отбросить анонсы сети 169.254.254.0/24, нужно поместить в список одну запись:
bgp2(config)# ip prefix-list PREFIX-LIST-FROM-BGP1 seq 1 permit 169.254.254.0/24
bgp2(config)#
Также может потребоваться не принимать анонсы маршрутов не только с точно совпадающим префиксом, но и анонсы маршрутов ко всем сетям, находящимся внутри указанной. Сделать это можно следующим образом:
bgp2(config)# ip prefix-list PREFIX-LIST-FROM-BGP1 seq 2 permit 169.254.253.0/24 le 32
bgp2(config)#
Выражение "le 32" означает, что условию удовлетворяют не только сети с префиксом 169.254.253.0/24, но и со всеми префиксами с длиной меньше 32 или равными 32. Например, если маршрутизатор BGP1 будет анонсировать маршруты к сетям 169.254.253.0/25 и 169.254.253.128/26, то оба анонса будут отброшены маршрутной картой.

Фильтрация исходящих анонсов

Сетевые администраторы рекомендуют создавать маршрутную карту не только для принимаемых маршрутов, но и для анонсируемых. Двойная защита позволяет застраховаться от непредусмотренных ошибок, допущенных при конфигурации одного из узлов.

Делается это аналогично фильтрации принимаемых маршрутов, с той лишь разницей, что ключевое слово in нужно заменить на out и прописать имя соответствующей маршрутной карты:
bgp2(config)# router bgp 64500
bgp2(config-router)# neighbor 169.254.252.6 route-map MAP-TO-BGP1 out
bgp2(config-router)# exit
bgp2(config)#
Если маршрутизатор должен только принимать анонсы, то такая маршрутная карта будет выглядеть следующим образом:
bgp2(config)# route-map MAP-TO-BGP1 deny 10
bgp2(config-router)# exit
bgp2(config)#

Применение политик без разрыва сеансов BGP

При изменении маршрутных карт и списков префиксов, чтобы новые политики фильтрации вступали в силу, нужно выполнять команду следующего вида:
bgp2# clear ip bgp 169.254.252.6 soft in
bgp2#
Эта команда разрывает сеанс BGP с соседним маршрутизатором и устанавливает его заново. При этом соседний маршрутизатор вновь анонсирует весь список маршрутов, который и подвергается фильтрации.

Чтобы не разрывать сеансы BGP только лишь для того, чтобы применить новые политики фильтрации маршрутов, сетевые администраторы рекомендуют включить для соседа режим мягкой переконфигурации:
bgp2(config)# router bgp 64500
bgp2(config-router)# neighbor 169.254.252.6 soft-reconfiguration inbound
bgp2(config-router)# exit
bgp2(config)#
В этом режиме сеанс BGP с соседним маршрутизатором не разрывается, а фильтрация применяется к уже принятому ранее списку маршрутов.

Описание соседей

Даже если в конфигурации BGP описан только один соседний маршрутизатор, лучше снабдить его описанием, чтобы в дальнейшем не приходилось ориентироваться только на IP-адреса и номера автономных систем. Сделать это можно следующим образом:
bgp2(config)# router bgp 64500
bgp2(config-router)# neighbor 169.254.252.6 description bgp1
bgp2(config-router)# exit
bgp2(config)#

Группировка настроек соседей

Рассмотрим случай, когда необходимо настроить соседство с двумя маршрутизаторами, настройки которых в целом должны быть идентичны. В примере ниже маршрутизатор с именем bgp3 и IP-адресом 169.254.252.10 соседствует с маршрутизаторами bgp1 и bgp2, имеющими IP-адреса 169.254.252.5 и 169.254.252.6 соответственно:
hostname bgp3
password <password>
enable password <secret_password>
!
router bgp 64500
 bgp router-id 169.254.252.10
 neighbor 169.254.252.5 remote-as 64500
 neighbor 169.254.252.5 route-map PREFIX-LIST-FROM-BGP1 in
 neighbor 169.254.252.5 soft-reconfiguration inbound
 neighbor 169.254.252.5 description bgp1.stupin.su
 neighbor 169.254.252.6 remote-as 64500
 neighbor 169.254.252.6 route-map PREFIX-LIST-FROM-BGP2 in
 neighbor 169.254.252.6 soft-reconfiguration inbound
 neighbor 169.254.252.6 description bgp2.stupin.su
!
ip prefix-list PREFIX-LIST-FROM-BGP1 seq 1 permit 169.254.254.0/24
ip prefix-list PREFIX-LIST-FROM-BGP1 seq 2 permit 169.254.253.0/24
!
ip prefix-list PREFIX-LIST-FROM-BGP2 seq 1 permit 169.254.254.0/24
ip prefix-list PREFIX-LIST-FROM-BGP2 seq 2 permit 169.254.253.0/24
!
route-map MAP-FROM-BGP1 permit 10
 match ip address prefix-list PREFIX-LIST-FROM-BGP1
!
route-map MAP-FROM-BGP2 permit 10
 match ip address prefix-list PREFIX-LIST-FROM-BGP2
Настройки соседей по замыслу должны быть идентичными, но из приведённой выше конфигурации это не очевидно. Если конфигурацию будет читать другой системный администратор, то ему во-первых придётся прочитать довольно много, во-вторых первоначальный замысел может оказаться для него не очевидным. Он может изменить настройки одного из соседей, но забыть поправить подобным образом настройки второго соседа.

Первое, что приходит в голову - это объединить списки префиксов и маршрутные карты:
hostname bgp3
password <password>
enable password <secret_password>
!
router bgp 64500
 bgp router-id 169.254.252.10
 neighbor 169.254.252.5 remote-as 64500
 neighbor 169.254.252.5 route-map PREFIX-LIST-FROM-BGP1_2 in
 neighbor 169.254.252.5 soft-reconfiguration inbound
 neighbor 169.254.252.5 description bgp1.stupin.su
 neighbor 169.254.252.6 remote-as 64500
 neighbor 169.254.252.6 route-map PREFIX-LIST-FROM-BGP1_2 in
 neighbor 169.254.252.6 soft-reconfiguration inbound
 neighbor 169.254.252.6 description bgp2.stupin.su
!
ip prefix-list PREFIX-LIST-FROM-BGP1_2 seq 1 permit 169.254.254.0/24
ip prefix-list PREFIX-LIST-FROM-BGP1_2 seq 2 permit 169.254.253.0/24
!
route-map MAP-FROM-BGP1_2 permit 10
 match ip address prefix-list PREFIX-LIST-FROM-BGP1_2
Конфигурация стала короче, замысел стал более понятным, но всё ещё есть возможность поменять настройки одного маршрутизатора, не меняя настройки второго. Настройки соседей можно сгруппировать:
hostname bgp3
password <password>
enable password <secret_password>
!
router bgp 64500
 bgp router-id 169.254.252.10
 neighbor NEIGHBORS-BGP1_2 peer-group
 neighbor NEIGHBORS-BGP1_2 remote-as 64500
 neighbor NEIGHBORS-BGP1_2 route-map PREFIX-LIST-FROM-BGP1_2 in
 neighbor NEIGHBORS-BGP1_2 soft-reconfiguration inbound
 neighbor 169.254.252.5 peer-group NEIGHBORS-BGP1_2
 neighbor 169.254.252.5 description bgp1.stupin.su
 neighbor 169.254.252.6 peer-group NEIGHBORS-BGP1_2
 neighbor 169.254.252.6 description bgp2.stupin.su
!
ip prefix-list PREFIX-LIST-FROM-BGP1_2 seq 1 permit 169.254.254.0/24
ip prefix-list PREFIX-LIST-FROM-BGP1_2 seq 2 permit 169.254.253.0/24
!
route-map MAP-FROM-BGP1_2 permit 10
 match ip address prefix-list PREFIX-LIST-FROM-BGP1_2
Теперь оба соседа состоят в одной группе и их настройки объединены, так что будут меняться только одновременно, если специально не отделить их друг от друга. Получившийся файл конфигурации стал короче и нагляднее.

Ограничение доступа к терминалу

Т.к. для настройки демонов zebra и bpgd используется протокол telnet, то я бы не стал делать его доступным через внешние интерфейсы сервера. Трафик в telnet не шифруется, а если кому-то нужно поправить настройки, то пусть лучше зайдёт сначала на сервер по SSH, а потом уже подключается к терминалу zebra или quagga локально. Если бы мне пришла в голову мысль сделать терминал доступным на внешних интерфейсах, то я открыл доступ в пакетном фильтре только для необходимых IP-адресов. Тем не менее, параноики исходят из справедливости принципа Мерфи, поэтому стремятся защититься даже от тех ситуаций, которые кажутся почти невероятными. Мне порекомендовали защитить доступ к терминалу управления списками управления доступом, вот так:
access-list TERMINAL permit 127.0.0.1/32
access-list TERMINAL deny any
!
line vty
 access-class TERMINAL

Анонсирование адреса Loopback-интерфейса

Если BGP настраивается на сервере для того, чтобы сервер был доступен через два маршрутизатора, то IP-адрес сервера, анонсируемый соседям по протоколу BGP, настраивается не на физическом интерфейсе, а на так называемом Loopback-интерфейсе.

В Debian я не нашёл способа создать отдельный петлевой интерфейс, поэтому анонсируемый IP-адрес можно добавить на один-единственный петлевой интерфейс lo в качестве дополнительного IP-адреса. Сделать это можно при помощи команды следующего вида:
# ip addr add 169.254.251.1/32 dev lo
Чтобы дополнительный IP-адрес назначался интерфейсу lo при перезагрузке сервера, нужно в файле /etc/network/interfaces прописать аналогичные настройки:
auto lo:1
iface lo:1 inet static
  address 169.254.251.1
  netmask 255.255.255.255
После этого нужно:
  1. настроить демона, который реализует необходимый сервис, так чтобы он принимал входящие подключения только на этот IP-адрес,
  2. разрешить соответствующий трафик в пакетном фильтре,
  3. добавить IP-адрес в конфигурацию демона bgpd.
При выполнении последнего пункта стоит учесть правила хорошего тона и создать отдельную маршрутную карту на исходящие анонсы, которая будет разрешать демону bgpd анонсировать соседям только этот IP-адрес. Например, если сервер должен принимать от маршрутизаторов только маршруты по умолчанию, а анонсировать в их сторону только Loopback-адрес 169.254.251.1, то конфигурация может выглядеть следующим образом:
hostname server
password <password>
enable password <secret_password>
!
router bgp 64500
 bgp router-id 169.254.251.1
 network 169.254.251.1/32
 neighbor NEIGHBORS-BGP1_2 peer-group
 neighbor NEIGHBORS-BGP1_2 remote-as 64500
 neighbor NEIGHBORS-BGP1_2 route-map PREFIX-LIST-FROM-BGP1_2 in
 neighbor NEIGHBORS-BGP1_2 route-map PREFIX-LIST-TO-BGP1_2 in
 neighbor NEIGHBORS-BGP1_2 soft-reconfiguration inbound
 neighbor 169.254.252.5 peer-group NEIGHBORS-BGP1_2
 neighbor 169.254.252.5 description bgp1.stupin.su
 neighbor 169.254.252.6 peer-group NEIGHBORS-BGP1_2
 neighbor 169.254.252.6 description bgp2.stupin.su
!
ip prefix-list PREFIX-LIST-FROM-BGP1_2 seq 1 permit 0.0.0.0/0
ip prefix-list PREFIX-LIST-TO-BGP1_2 seq 1 permit 169.254.251.1/32
!
route-map MAP-FROM-BGP1_2 permit 10
 match ip address prefix-list PREFIX-LIST-FROM-BGP1_2
!
route-map MAP-TO-BGP1_2 permit 10
 match ip address prefix-list PREFIX-LIST-TO-BGP1_2
На случай, если демон bgpd аварийно завершится, можно добавить в демоне zebra статический маршрут по умолчанию с низким приоритетом через одного из соседей:
ip route 0.0.0.0/0 169.254.252.5 250

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