воскресенье, 18 июня 2017 г.

Flash-плагин для Chromium и Iceweasel/Firefox в Debian

В 2012 году Adobe объявил о прекращении выхода новых версий Flash-плагина для Linux: 01.04.2012 14:29 Релиз Adobe Flash 11.2, последней версии с поддержкой Linux. Мне этот плагин не нужен, но вот люди, любящие играть во Flash-игры под Linux в социальных сетях, опечалились - многие игры отказываются работать в старых версиях плагина.

Надо сказать, что браузер Iceweasel/Firefox, являющийся далёким потомком браузера Netscape, предоставляет интерфейс для подключения к нему плагинов. Этот интерфейс называется NPAPI - Netscape Plugin Application Programming Interface - программный интерфейс подключаемых модулей Netscape.

У браузера Chromium, являющегося свободной версией браузера Chrome, разрабатываемого Google, имеется собственный интерфейс для подключения плагинов. В Google решили не использовать NPAPI, посчитав что в нём имеется масса проблем и нет нужных функций. Взамен Google предложили новый интерфейс - PPAPI - Pepper Plugin Application Program Interface - программный интерфейс подключаемых модулей Pepper.

Когда Adobe объявил, что новые версии Flash-плагина для Linux выпускаться не будут, это объявление не касалось Flash-плагина, который выпускался Google. Google продолжил поддержку выпуска Flash-плагина в варианте PPAPI для своего браузера Chrome. Этот плагин можно было использовать и в Linux.

И вот в прошлом году я прочитал такие две новости: 05.09.2016 16:52 Adobe возобновляет выпуск NPAPI-плагина с Flash Player для Linux, 20.12.2016 12:10 Релиз Adobe Flash 24 для Linux. "Неплохо" - сказал я и решил попробовать обновить Flash-плагин. На тот момент плагины без проблем обновились штатными средствами.

1.1. Штатная установка Flash-плагина в Chromium

Штатно Flash-плагин для Chromium устанавливается так:
# apt-get install pepperflashplugin-nonfree
# update-pepperflashplugin-nonfree --install

1.2. Штатная установка Flash-плагина в Iceweasel/Chromium

Для Iceweasel/Firefos штатно Flash-плагин устанавливается так:
# apt-get install flashplugin-nonfree
# update-flashplugin-nonfree --install

2.1. Проблемы при установке Flash-плагина в Chromium

Я бы и не стал писать эту небольшую заметку, если бы штатные средства продолжали бы работать и сейчас. Но, к сожалению, сейчас они работать перестали. Например, при попытке обновить Flash-плеер PPAPI для Chromium выдаются такие ошибки:
# update-pepperflashplugin-nonfree --install
--2017-03-28 22:28:55--  http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_57.0.2987.110-1_amd64.deb
Распознаётся dl.google.com (dl.google.com)… 209.85.233.190, 209.85.233.91, 209.85.233.136, ...
Подключение к dl.google.com (dl.google.com)|209.85.233.190|:80... соединение установлено.
HTTP-запрос отправлен. Ожидание ответа... 200 OK
Длина: 47312644 (45M) [application/x-debian-package]
Сохранение в: «/tmp/pepperflashplugin-nonfree.VjH7fdoXcK/google-chrome-stable_57.0.2987.110-1_amd64.deb»

     0K .......... .......... .......... .......... ..........  0%  483K 95s
------------------8<------------------8<------------------8<------------------
 46150K .......... .......... .......... .......... .......... 99% 5,47M 0s
 46200K ...                                                   100% 7160G=11s

2017-03-28 22:29:07 (4,01 MB/s) - «/tmp/pepperflashplugin-nonfree.VjH7fdoXcK/google-chrome-stable_57.0.2987.110-1_amd64.deb» сохранён [47312644/47312644]

mv: не удалось выполнить stat для «unpackchrome/opt/google/chrome/PepperFlash/libpepflashplayer.so»: Нет такого файла или каталога

2.2. Проблемы при установке Flash-плагина в Iceweasel/Firefox

При попытке обновить Flash-плагин для Iceweasel/Firefox происходит другая ошибка:
# update-flashplugin-nonfree --install
--2017-03-28 22:48:07--  https://fpdownload.adobe.com/get/flashplayer/pdc/24.0.0.186/flash_player_npapi_linux.x86_64.tar.gz
Распознаётся fpdownload.adobe.com (fpdownload.adobe.com)… 2.17.215.70
Подключение к fpdownload.adobe.com (fpdownload.adobe.com)|2.17.215.70|:443... соединение установлено.
HTTP-запрос отправлен. Ожидание ответа... 404 Not Found
2017-03-28 22:48:07 ОШИБКА 404: Not Found.

ERROR: wget failed to download https://fpdownload.adobe.com/get/flashplayer/pdc/24.0.0.186/flash_player_npapi_linux.x86_64.tar.gz
More information might be available at:
  http://wiki.debian.org/FlashPlayer

3.1. Решение проблемы с установкой Flash-плагина в Chromium

Чинить будем в том же порядке. Во-первых, чтобы починить Flash-плагин для Chromium, из браузера Chromium заходим на официальную страницу Adobe, переходим по ссылке внизу Продукты, справа в списке "Загрузить" переходим по ссылке Adobe Flash Player, в выпадающем списке выбираем формат ".tar.gz для Linux" и жмём на кнопку "Загрузить". Извлечём из скачанного архива с именем "flash_player_ppapi_linux.x86_64.tar.gz" интересующий нас файл динамической библиотеки и поместим его в нужное место:
# tar xzvf flash_player_ppapi_linux.x86_64.tar.gz libpepflashplayer.so
# mv libpepflashplayer.so /usr/lib/pepperflashplugin-nonfree/
# chown root:root /usr/lib/pepperflashplugin-nonfree/libpepflashplayer.so
# chmod u=rw,go=r /usr/lib/pepperflashplugin-nonfree/libpepflashplayer.so

3.2. Решение проблемы с установкой Flash-плагина в Iceweasel/Firefox

Теперь точно такие же действия можно проделать из браузера Iceweasel/Firefox. В результате должен скачаться архив с именем "flash_player_npapi_linux.x86_64.tar.gz". Поступаем с ним аналогично - извлекаем интересующий нас файл динамической библиотеки и помещаем его в нужное место. В случае Iceweasel кладём файл в каталог /usr/lib/flashplugin-nonfree/:
# tar xzvf flash_player_npapi_linux.x86_64.tar.gz libflashplayer.so
# mv libflashplayer.so /usr/lib/flashplugin-nonfree/libflashplayer.so
# chown root:root /usr/lib/flashplugin-nonfree/libflashplayer.so
# chmod u=rw,go=r /usr/lib/flashplugin-nonfree/libflashplayer.so
В случае Firefox кладём файл в каталог /usr/lib/mozillz/plugins/:
# tar xzvf flash_player_npapi_linux.x86_64.tar.gz libflashplayer.so
# mv libflashplayer.so /usr/lib/mozilla/plugins/libflashplayer.so
# chown root:root /usr/lib/mozilla/plugins/libflashplayer.so
# chmod u=rw,go=r /usr/lib/mozilla/plugins/libflashplayer.so

4. Использование PPAPI-плагина в Iceweasel/Firefox

Когда Adobe объявила о прекращении поддержки NPAPI-плагина для Linux, наш соотечественник Ринат Ибрагимов - программист из Казани, занялся разработкой специального адаптера интерфейсов, который бы позволил использовать PPAPI-плагин в браузерах, предоставляющих NPAPI-интерфейс. Проект называется FreshPlayer и устанавливается в браузер как NPAPI-плагин. Вместо использования git-репозитория и сборки адаптера из исходных текстов мы воспользуемся готовым репозиторием для Debian Jessie. Добавим в файл /etc/apt/sources.list такую строчку:
deb http://http.debian.net/debian jessie-backports main contrib
Теперь обновим списки пакетов, доступных для установки из репозиториев:
# apt-get update
И установим пакет с адаптером:
# apt-get install install browser-plugin-freshplayer-pepperflash

Заключение

Как правило, Flash-плагин без проблем работает в Chromium. А вот с Iceweasel/Firefox могут возникнуть сложности. Например, в Iceweasel плагин NPAPI у меня не заработал, зато заработал PPAPI-плагин через адаптер FreshPlayer. В Firefox заработали оба варианта - и плагин NPAPI и плагин PPAPI через адаптер FreshPlayer. Если не получается заставить работать первый попавшийся вариант - попробуйте другой: попробуйте сначала плагин NPAPI, затем попробуйте обновить Iceweasel до Firefox, а если это не поможет - вместо плагина NPAPI можно попробовать воспользоваться плагином PPAPI через FreshPlayer.

воскресенье, 11 июня 2017 г.

Настройка zswap в Debian

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

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

Для сжатия страниц памяти воспользуемся новым алгоритмом сжатия LZ4, который обладает более высокой производительностью по сравнению с предлагаемым по умолчанию алгоритмом сжатия LZO. Несмотря на то, что LZ4 сжимает чуть хуже, он меньше грузит процессор. Таким образом соблюдается более приемлемый баланс между экономией памяти и нагрузкой на процессор.

Модули ядра для поддержки LZ4

Для начала впишем в файл /etc/initramfs-tools/modules модули ядра, реализующие быстрый алгоритм сжатия LZ:
lz4
lz4_compress
И пересоберём образ загрузочной файловой системы, чтобы в него попали добавленные нами модули:
# update-initramfs -u -k all

Модуль ядра для сжатия страниц подкачки

Теперь прописываем автоматическое включение zswap при загрузке ядра в конфигурации загрузчика GRUB в файле /etc/default/grub. Для этого добавим ряд новых настроек в GRUB_CMDLINE_LINUX_DEFAULT:
zswap.enabled=1 zswap.compressor=lz4 zswap.max_pool_percent=80
Значение настроек:
  • zswap.enabled=1 - включает использование zswap,
  • zswap.compressor=lz4 - выбирает более быстрый алгоритм сжатия lz4 вместо алгоритма сжатия по умолчанию lzo,
  • zswap.max_pool_percent=80 - разрешает использовать до 80 процентов оперативной памяти для хранения сжатых данных.
После изменения конфигурации загрузчика обновим его автоматически генерируемые файлы с настройками:
# update-grub
Всё готово, теперь нужно перезагрузить систему. Очень редкое действие, но этом случае перезагрузка действительно необходимо, т.к. загрузить модули без перезагрузки мне лично не удалось:
# reboot

Проверка

После перезагрузки можно проверить, работает ли модуль и какой алгоритм сжатия используется:
$ dmesg | grep zswap:
[    0.830940] zswap: loading zswap
[    0.835122] zswap: using lz4 compressor

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

воскресенье, 4 июня 2017 г.

Статистика ввода-вывода diskstats в Linux через Zabbix

Как и во FreeBSD, для оценки производительности дисковой подсистемы Linux тоже можно воспользоваться утилитой iostat. Однако, в отличие от версии iostat из FreeBSD, в iostat из Linux нет опции, позволяющей выводить накопленные значения счётчиков. Чтобы получить полную статистику за интересующий нас интервал времени, можно воспользоваться файлом /proc/diskstats из специальной файловой системы /proc. Эта файловая система позволяет узнавать состояние различных подсистем ядра Linux и менять некоторые настройки.

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

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

Прежде чем что-то делать, ознакомимся с краткой документацией на этот файл по ссылке
https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats. Значения столбцов этого файла таковы:
  1. старший номер устройства (номер драйвера),
  2. младший номер устройства (порядковый номер устройства, управляемого этим драйвером),
  3. имя устройства в файловой системе /dev,
  4. количество успешно выполненных операций чтения,
  5. количество объединённых операций чтения,
  6. количество прочитанных секторов,
  7. время, потраченное на чтение, в миллисекундах,
  8. количество успешно выполненных операций записи,
  9. количество объединённых операций записи,
  10. количество записанных секторов,
  11. время, потраченное на запись, в миллисекундах,
  12. количество активных операций ввода-вывода (фактически - длина очереди транзакций),
  13. время, потраченное на выполнение операций ввода-вывода, в миллисекундах,
  14. взвешенное время выполнения операций ввода-вывода, в миллисекундах.
В этом файле имеется ссылка на ещё один документ - https://www.kernel.org/doc/Documentation/iostats.txt. В документе описываются те же самые колонки, но в нём пропущены первые три колонки, в которых указывается информация о самом диске. Применительно к столбцам файла /proc/diskstats там написано следующее:
  1. старший номер устройства (номер драйвера),
  2. младший номер устройства (порядковый номер устройства, управляемого этим драйвером),
  3. имя устройства в файловой системе /dev,
  4. количество выполненных операций чтения - общее количество успешно завершённых операций чтения,
  5. количество объединённых операций чтения - операции чтения соседних областей могут быть объединены друг с другом для повышения эффективности. Две операции чтения 4 килобайт можно объединить в одну операцию чтения 8 килобайт, прежде чем команда будет передана на диск. Такая операция будет поставлена в очередь и выполнена как одна операция. Это поле позволяет узнать, как часто происходит объединение операций,
  6. количество прочитанных секторов - общее количество успешно прочитанных секторов,
  7. время, потраченное на чтение, в миллисекундах - общее количество миллисекунд, потраченных всеми операциями чтения (замеренное между __make_request() и end_that_request_last()),
  8. количество выполненных операций записи - общее количество успешно завершённых операций записи,
  9. количество объединённых операций записи - см. описание поля 5,
  10. количество записанных секторов - общее количество успешно записанных секторов,
  11. время, потраченное на запись, в миллисекундах - общее количество миллисекунд, потраченных всеми операциями записи (замеренное между __make_request() и end_that_request_last()),
  12. количество активных операций ввода-вывода - единственное поле, которое сбрасывается в ноль. Увеличивается на единицу, когда запрос попадает в соответствующую структуру request_queue и уменьшается на единицу, когда этот запрос завершается,
  13. время, потраченное на выполнение операций ввода-вывода, в миллисекундах - это поле увеличивается, пока поле 12 отличается от нуля,
  14. взвешенное время выполнения операций ввода-вывода, в миллисекундах - это поле увеличивается с началом каждой операции ввода-вывода, завершением операции ввода-вывода, объединением операций ввода-вывода или при чтении этой статистики. Количество активных операций ввода-вывода (поле 12), помноженное на количество миллисекунд, потраченных на ввод-вывод с момента последнего обновления этого поля. Это поле позволяет легко оценить как время завершения ввода-вывода, так и время ожидания операции ввода-вывода в очереди.
Итак, что из этого удалось понять и что не удалось понять:
  • поле 12 не является накопительным и содержит текущее значение количества операций в очереди,
  • точный смысл поля 14 понять не удалось, будем считать что там находится некое "взвешенное" значение, по смыслу аналогичное полю 13,
  • размер сектора в полях 6 и 10 не известен.
На вопрос о размере сектора я нашёл очень подробный ответ How to get disk read/write bytes per second from /proc in programming on linux?, суть которого сводится к тому, что размер сектора в этом файле жёстко зафиксирован и всегда равен 512 байтам.

Теперь приступим к настройке Zabbix-агента. Для этого впишем в его файл конфигурации /etc/zabbix/zabbix_agentd.conf следующие строки:
UserParameter=diskstats.discovery,cat /proc/diskstats | awk 'BEGIN { printf "{\"data\":["; } { if (NR > 1) printf ","; printf "{\"{#DEVNAME}\":\"" $3 "\"}"; } END { printf "]}"; }'
UserParameter=diskstats.read.ops[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$4; }'
UserParameter=diskstats.read.merged[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$5; }'
UserParameter=diskstats.read.sectors[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$6; }'
UserParameter=diskstats.read.duration[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$7; }'
UserParameter=diskstats.write.ops[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$8; }'
UserParameter=diskstats.write.merged[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$9; }'
UserParameter=diskstats.write.sectors[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$10; }'
UserParameter=diskstats.write.duration[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$11; }'
UserParameter=diskstats.queue.length[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$12; }'
UserParameter=diskstats.busy.duration[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$13; }'
UserParameter=diskstats.transactions.duration[*],cat /proc/diskstats | awk '$$3 == "$1" { print $$14; }'
После внесения изменений в файл конфигурации Zabbix-агента, не забудьте его перезапустить:
# /etc/init.d/zabbix-agent restart
Я подготовил два варианта шаблона:
В шаблоне имеется правило низкоуровневого обнаружения, которое находит все дисковые устройства, статистику по которым выдаёт diskstats:

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

Для каждого найденного устройства создаются одиннадцать элементов данных, соответствующих колонкам производительности диска из файла diskstats:

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

Элемент данных с названием "Загрузка диска" показывает процент времени, в течение которого диск занимается обработкой хотя бы одной транзакции.

воскресенье, 28 мая 2017 г.

Статистика ввода-вывода iostat во FreeBSD через Zabbix

Для оценки производительности дисковой подсистемы FreeBSD можно воспользоваться утилитой iostat. Однако полную картину нагрузки на дисковую подсистему можно получить только собирая статистику за достаточно долгий срок. Как минимум это сутки, а лучше всего собрать статистику за неделю. По недельной статистике можно легко найти периоды максимальной нагрузки и связать их с периодически выполняющимися в системе задачами. Имея статистику за неделю, можно подобрать оптимальное время запуска процедуры резервного копирования. В этой статье речь пойдёт о том, как собирать статистику от утилиты iostat в Zabbix.

Для начала изучим документацию на саму команду iostat из состава FreeBSD. Простой вызов команды iostat отображает статистику дисковых устройств вместе со статистикой процессора и терминала. К счастью, у команды iostat имеется несколько опций, полезных для наших целей:
  • -d - отображать только статистику по дисковым устройствам,
  • -x - отобразить расширенную статистику,
  • -I - отображать счётчики нарастающим итогом, а не среднюю статистику за период времени.
Последняя опция - самая полезная. В интернете можно встретить шаблоны, в которых для сбора статистики команда iostat запускается на определённый интервал времени. У такого подхода есть как минимум два недостатка. Во-первых, в промежутках между запусками iostat статистика не собирается. Во-вторых, из-за того, что команда выполняется долго, используют промежуточный файл, в который периодически сохраняют собранную статистику, а потом извлекают из этого файла с помощью команд grep или awk. В общем, увидев в какой-нибудь очередной статье в интернете такой способ съёма статистики, я морщусь и перехожу к другой статье.

Команда iostat, вызванная со всеми этими опциями, выведет примерно такой результат:
# iostat -dxI
extended device statistics  
device     r/i   w/i    kr/i    kw/i wait svc_t  %b  
mfid0    6994332913.0 13128877288.0 544161035422.5 493573917948.0    0   5.1  18
Смысл столбцов такой:
  • device - имя файла устройства в файловой системе /dev,
  • r/i - количество операций чтения за период времени,
  • w/i - количество операций записи за период времени,
  • kr/i - прочитано килобайт за период времени,
  • kw/i - записано килобайт за период времени,
  • wait - длина очереди транзакций,
  • svc_t - средняя длительность транзакций в миллисекундах,
  • %b - процент времени, когда на устройстве была одна или более невыполненных транзакций.
Несмотря на то, что указана опция -I, предписывающая выводить накопленные значения счётчиков, числа в последних трёх колонках являются текущими средними значениями. Если судить по страницам руководства на официальном сайте, эти колонки стали показывать накопленные значения счётчиков только начиная с FreeBSD 10: FreeBSD 10.0-RELEASE iostat(8). У меня FreeBSD 10 пока нигде нет, поэтому отладить шаблон для соответствующих версий iostat пока возможности не было.

Добавим в файл конфигурации Zabbix-агента /usr/local/etc/zabbix24/zabbix_agentd.conf следующие строки:
UserParameter=iostat.discovery,iostat -dxI | awk 'BEGIN { printf "{\"data\":["; } { if (NR > 3) printf ","; if (NR > 2) printf "{\"{#DEVNAME}\":\"" $1 "\"}"; } END { printf "]}"; }'
UserParameter=iostat.read.ops[*],/usr/sbin/iostat -dxI | awk '$$1 == "$1" { print $$2; }'
UserParameter=iostat.write.ops[*],/usr/sbin/iostat -dxI | awk '$$1 == "$1" { print $$3; }'
UserParameter=iostat.read.bytes[*],/usr/sbin/iostat -dxI | awk '$$1 == "$1" { print 1024 * $$4; }'
UserParameter=iostat.write.bytes[*],/usr/sbin/iostat -dxI | awk '$$1 == "$1" { print 1024 * $$5; }'
UserParameter=iostat.queue.length[*],/usr/sbin/iostat -dxI | awk '$$1 == "$1" { print $$6; }'
UserParameter=iostat.transactions.duration[*],/usr/sbin/iostat -dxI | awk '$$1 == "$1" { print $$7; }'
UserParameter=iostat.busy.duration[*],/usr/sbin/iostat -dxI | awk '$$1 == "$1" { print $$8; }'
После внесения изменений в файл конфигурации Zabbix-агента, не забудьте его перезапустить:
# /usr/local/etc/rc.d/zabbix_agentd restart
Я подготовил два варианта шаблона:
В шаблоне имеется правило низкоуровневого обнаружения, которое находит все дисковые устройства, статистику по которым выдаёт iostat:

Для каждого найденного устройства создаётся семь элементов данных, соответствующих колонкам из вывода iostat:

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

Элемент данных с названием "Загрузка диска" показывает процент времени, в течение которого диск занимается обработкой хотя бы одной транзакции.

воскресенье, 21 мая 2017 г.

Контроль параметров S.M.A.R.T. жёстких дисков через Zabbix

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

Для контроля параметров S.M.A.R.T. на компьютере понадобится настроенный Zabbix-агент, а также установленные пакеты sudo и smartmontools.

Во-первых, при помощи команды visudo, разрешим пользователям из группы zabbix выполнять команды для контроля состояния диска от имени пользователя root:
%zabbix    ALL=(ALL) NOPASSWD:/usr/sbin/smartctl --scan, \
                              /usr/sbin/smartctl -i *, \
                              /usr/sbin/smartctl -H *, \
                              /usr/sbin/smartctl -A *
Во-вторых, добавим в файл конфигурации Zabbix-агента /etc/zabbix/zabbix_agentd.conf следующие "пользовательские параметры":
UserParameter=smartctl.list,/usr/bin/sudo /usr/sbin/smartctl --scan | awk 'BEGIN { printf "{\"data\": ["; } { if (NR != 1) printf ","; printf "{\"{#SMART}\": \"%s\"}", $1; } END { printf "]}"; }'
UserParameter=smartctl.model[*],/usr/bin/sudo /usr/sbin/smartctl -i $1 2>& | /usr/bin/awk -F: '$$1 ~ /^Device Model$/ { gsub(/(^ +| +$)/, "", $$2); print $$2; }'
UserParameter=smartctl.serial[*],/usr/bin/sudo /usr/sbin/smartctl -i $1 2>& | /usr/bin/awk -F: '$$1 ~ /^Serial Number$/ { gsub(/(^ +| +$)/, "", $$2); print $$2; }'
UserParameter=smartctl.health[*],/usr/bin/sudo /usr/sbin/smartctl -H $1 2>& | /usr/bin/awk 'BEGIN { h = 0; } / (OK|PASSED)$/ { h = 1; } END { print h; }'
UserParameter=smartctl.reallocated[*],/usr/bin/sudo /usr/sbin/smartctl -A $1 2>& | awk '/^  5 / { print $$10; }'
UserParameter=smartctl.pending[*],/usr/bin/sudo /usr/sbin/smartctl -A $1 2>& | awk '/^197 / { print $$10; }'
UserParameter=smartctl.temperature[*],/usr/bin/sudo /usr/sbin/smartctl -A $1 2>& | awk '/^194 / { print $$10; }'
После внесения изменений в конфигурацию Zabbix-агента, не забудьте его перезапустить:
# /etc/init.d/zabbix-agent restart
Я подготовил два шаблона для контроля параметров S.M.A.R.T.:
В обоих шаблонах имеется элемент данных для низкоуровневого обнаружения, который находит все имеющиеся в системе диски, поддерживающие S.M.A.R.T.:

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

Имеется три прототипа триггеров, который будут созданы для каждого обнаруженного жёсткого диска. Самый главный триггер срабатывает в том случае, когда S.M.A.R.T. явным образом сообщает о неисправности диска. Два других триггера срабатывают при превышении лимита неисправных секторов или секторов, ожидающих перемещения:

Лимиты для двух последних триггеров можно задать через соответствующие макросы - {$SMART_REALLOCATED_LIMIT} и {$SMART_PENDING_LIMIT}:

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

S.M.A.R.T. умеет отдавать массу другой информации о состоянии диска. Довольно подробный список параметров есть на уже упомянутой странице в Wikipedia: S.M.A.R.T. Не всегда S.M.A.R.T. успевает сообщить о неисправности жёсткого диска до того, как им и в самом деле становится невозможно пользоваться. Именно для этого я и добавил контроль двух выбранных мной параметров - количества перемещённых секторов и количества секторов, ожидающих перемещения. В случае роста этих счётчиков, можно заранее понять, что скоро S.M.A.R.T. может сообщить о неисправности диска. Иногда, правда, жёсткие диски продолжают исправно работать долгие годы, даже если на них уже есть десятки или даже сотни неисправных секторов. Поэтому вместо использования конкретных значений порогов срабатывания триггеров я и предусмотрел в шаблоне макросы, чтобы пороги можно было настраивать по месту. Лучше, всё же, не ждать появления десятков или сотен неисправных секторов, а менять диск заранее.

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

воскресенье, 14 мая 2017 г.

Контроль аппаратного RAID-контроллера LSI MegaRAID SAS во FreeBSD через Zabbix

В этой статье речь пойдёт о RAID-контроллерах FreeBSD, которые управляются драйвером mfi(4). На указанной странице руководства написано, что это драйвер контроллера LSI MegaRAID SAS. На самом же деле я некоторое время использовал описанную ниже схему для RAID-контроллера Intel RS2WC040. Об этом контроллере я ранее уже писал в двух других статьях:
Для поверки состояния RAID-контроллера нам понадобятся настроенный Zabbix-агент и пакет sudo.

При помощи команды visudo разрешим пользователям из группы zabbix выполнять от имени пользователя root команды для проверки состояния RAID-массивов и батареи RAID-контроллера:
%zabbix     ALL=(ALL) NOPASSWD:/usr/sbin/mfiutil show volumes, \
                               /usr/sbin/mfiutil show adapter, \
                               /usr/sbin/mfiutil show battery
Впишем в файл конфигурации Zabbix-агента /usr/local/etc/zabbix24/zabbix_agentd.conf соответствующие строки:
UserParameter=raid.mfiutil,sudo /usr/sbin/mfiutil show volumes | fgrep RAID | fgrep -vc OPTIMAL
UserParameter=raid.mfiutil.battery.present,sudo /usr/sbin/mfiutil show adapter | fgrep 'Battery Backup' | grep -vc present
UserParameter=raid.mfiutil.battery.status,sudo /usr/sbin/mfiutil show battery | fgrep Status | fgrep -vc normal
Первая команда возвращает количество неисправных RAID-массивов, вторая - количество контроллеров без установленной батареи, третья - количество батарей, не находящихся в статусе normal. То есть, если любое из значений отличается от нуля, то имеются проблемы.

После внесения изменений в конфигурацию Zabbix-агента, не забудьте его перезапустить:
# /etc/init.d/zabbix-agent restart
Я подготовил два шаблона для контроля состояния RAID-контроллера:
В шаблоне есть три элемента данных. Один контролирует целостность RAID-массивов, второй - наличие батарей в контроллерах, третий - состояние каждой из батарей:

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

Почему я оговорился о том, что использовал описанную схему только "некоторое время"? Потому что через некоторое время команда mfiutil переставала работать, выводя в ответ такие вот ошибки:
# mfiutil show volumes
mfiutil: Failed to get volume list: No such file or directory
# mfiutil show battery
mfiutil: Failed to get capacity info: No such file or directory
Это при том, что драйвер загружен в ядро и файлы устройства на месте:
# kldstat -v | grep mfi
  153 mfi/mfisyspd
  152 mfi/mfid
  151 pci/mfi
# ls /dev/mfi*
/dev/mfi0 /dev/mfid0 /dev/mfid0s1 /dev/mfid0s1a /dev/mfid0s1b
При каждом запуске команды mfiutil в журнале /var/log/messages появляются ошибки такого вида:
kernel: mfi0: IOCTL 0xc0404366 not handled
Возможно дело в том, что я использую не официальный драйвер, а с официальными драйверами, которые были добавлены в систему в последующих релизах, такой проблемы нет.

Есть сервер, где используется RAID-контроллер немного другой модели - Intel RS2BL040. Эта модель RAID-контроллера поддерживается официальным драйвером и на этом сервере многократные вызовы команды mfiutil не приводят к подобным ошибкам. Но в чём точно дело - в драйвере или в модели контроллера, я с уверенностью сказать не могу. Полагаю, что дело всё же в драйвере. В таком случае, скорее всего, описанная выше схема контроля не будет приводить к проблемам на системах, использующих официальный драйвер mfi.

После того, как я столкнулся с этой проблемой, вместо команды mfiutil я стал использовать команду megacli, собранную из порта sysutils/megacli. Утилита megacli работает безотказно. Правда, описывать контроль RAID-массива с её помощью я не стану - результат получился слишком неуклюжим.

воскресенье, 7 мая 2017 г.

Контроль программного RAID-массива FreeBSD через Zabbix

Совсем короткая статья про контроль программных RAID-массивов FreeBSD, созданных средствами gmirror. На этот раз кроме установленного и настроенного в системе Zabbix-агента не понадобится более никаких дополнительных пакетов.

Впишем в файл конфигурации Zabbix-агента /usr/local/etc/zabbix24/zabbix_agentd.conf всего одну строчку:
UserParameter=raid,/sbin/gmirror status -s 2>/dev/null | fgrep -vc COMPLETE
Команда gmirror status с ключом -s выводит состояние каждого диска, состоящего в каком-либо RAID-массиве, в одной строке. Команда fgrep -vc COMPLETE считает количество строчек, в которых нет статуса COMPLETE, который соответствует исправному состоянию диска в массиве. В итоге, если элемент данных raid равен нулю, то все RAID-массивы исправны.

После внесения изменений в конфигурацию Zabbix-агента, не забудьте его перезапустить:
# /etc/init.d/zabbix-agent restart
Я подготовил два шаблона для контроля параметров исправности RAID-массивов:
В шаблоне есть всего один элемент данных, контролирующий количество неисправных элементов RAID-массивов:

И всего один триггер, который срабатывает при наличии хотя бы одного неисправного элемента хотя бы одного RAID-массива:

По сути, оба шаблона полностью аналогичны шаблонам для контроля программных RAID-массивов Linux и отличаются от них только именами.

воскресенье, 30 апреля 2017 г.

Контроль программного RAID-массива Linux через Zabbix

Статья про контроль с помощью Zabbix программных RAID-массивов Linux, созданных средствами mdadm. На этот раз кроме установленного и настроенного в системе Zabbix-агента не понадобится более никаких дополнительных пакетов.

Первое решение, приходящее на ум - использовать программу mdadm. Но есть решение получше - использовать для этого файл /proc/mdstat. Для этого можно вписать в файл конфигурации Zabbix-агента /etc/zabbix/zabbix_agentd.conf всего одну строчку:
UserParameter=raid,egrep -c '_U|U_' /proc/mdstat
В этом файле состояние RAID-массивов отображается в виде символов между двумя квадратными скобками. Символ U соответствует исправному диску, символ подчёркивания соответствует неисправному диску. Команда egrep считает количество строчек, в которых символ U соседствует с символом подчёркивания. Предполагается, что в массиве есть как минимум два диска и один из них всегда исправен.

Но однажды мне понадобилось поставить на контроль систему, в которой были настроены массивы RAID-50 и сделано это было так: сначала из дисков собиралось несколько массивов RAID-5, а уже из этих RAID-массивов собирался массив RAID-0. Выглядит это вот так:
$ cat /proc/mdstat
Personalities : [linear] [multipath] [raid0] [raid1] [raid6] [raid5] [raid4]
[raid10]
md0 : active raid0 md3[2] md2[1] md1[0] md4[3]
93765249024 blocks super 1.2 512k chunks

md4 : active raid5 sdw1[0] sdab1[5] sdz1[3] sdx1[1] sdaa1[4] sdac1[7] sdy1[2]
23441312640 blocks super 1.2 level 5, 64k chunk, algorithm 2 [7/7] [UUUUUUU]
bitmap: 1/15 pages [4KB], 131072KB chunk

md2 : active raid5 sdk1[2] sdi1[0] sdl1[3] sdn1[5] sdj1[1] sdm1[4] sdo1[7]
23441312640 blocks super 1.2 level 5, 64k chunk, algorithm 2 [7/7] [UUUUUUU]
bitmap: 1/15 pages [4KB], 131072KB chunk

md3 : active raid5 sdq1[1] sds1[3] sdp1[0] sdt1[4] sdu1[5] sdv1[7] sdr1[2]
23441312640 blocks super 1.2 level 5, 64k chunk, algorithm 2 [7/7] [UUUUUUU]
bitmap: 2/15 pages [8KB], 131072KB chunk

md1 : active raid5 sdc1[1] sdb1[0] sde1[3] sdd1[2] sdf1[4] sdg1[5] sdh1[7]
23441312640 blocks super 1.2 level 5, 64k chunk, algorithm 2 [7/7] [UUUUUUU]
bitmap: 1/15 pages [4KB], 131072KB chunk

unused devices: <none>
В случае объединения дисков в режиме LINEAR можно наблюдать такую же картину, что и в случае RAID-0:
$ cat /proc/mdstat
Personalities : [linear]
md0 : active linear sdb[0] sde[3] sdd[2] sdc[1]
15628074304 blocks super 1.2 0k rounding

unused devices: <none>
По идее, хорошо бы контролировать деградацию массивов RAID-0 и LINEAR, но описанный выше способ контроля для них не применим.

Поиски способов контроля вывели на файловую систему /sys. На русском языке лучшим материалом на интересующую нас тему является статья Виктора Вислобокова: Недокументированные фишки программного RAID в Linux.

В итоге сформировался новый вариант строчки для файла конфигурации Zabbix-агента /etc/zabbix/zabbix_agentd.conf:
UserParameter=raid,fgrep -L in_sync /sys/block/md*/md/dev-*/state | wc -l
Этот вариант, правда, тоже может таить в себе неожиданности. У меня есть подозрения, что если в системе заготовлен запасной диск, который включён в один из RAID-массивов в таковом качестве, то в его файле состояния будет строчка spare. Если это действительно так, то это состояние нормальное и не должно приводить к срабатыванию триггера. Однако, системы с запасным диском мне не попадались и проверить свои подозрения мне было негде, поэтому я решил оставить всё как есть - все состояния отличные от in_sync будут считаться аварийными. Если мне попадётся в будущем такая система, то можно будет переделать контроль соответствующим образом, чтобы состояние spare не считалось аварийным.

После внесения изменений в конфигурацию Zabbix-агента, не забудьте его перезапустить:
# /etc/init.d/zabbix-agent restart
Я подготовил два шаблона для контроля параметров исправности RAID-массивов:
В шаблоне есть всего один элемент данных, контролирующий количество неисправных элементов RAID-массивов:

И всего один триггер, который срабатывает при наличии хотя бы одного неисправного элемента хотя бы одного RAID-массива:

воскресенье, 23 апреля 2017 г.

Контроль длины очереди отложенных писем Postfix через Zabbix

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

При помощи команды visudo разрешим пользователям из группы zabbix выполнять от имени пользователя root команду поиска файлов в очереди Postfix:
%zabbix    ALL=(ALL) NOPASSWD:/usr/bin/find /var/spool/postfix/deferred -type f
В файл конфигурации Zabbix-агента /etc/zabbix/zabbix_agentd.conf (или /usr/local/etc/zabbix24/zabbix_agentd.conf для случая FreeBSD с Zabbix-агентом версии 2.4) пропишем пользовательский параметр, который будет выполнять команду, подсчитывающую количество файлов, найденных в очереди отложенных писем Postfix:
UserParameter=postfix.deferred.length,sudo /usr/bin/find /var/spool/postfix/deferred -type f | wc -l
После внесения изменений в конфигурацию Zabbix-агента, не забудьте его перезапустить:
# /etc/init.d/zabbix-agent restart
В шаблоне есть:
  • один элемент данных с ключом postfix.deferred.length, с помощью которого раз в 5 минут контролируется количество писем в очереди отложенных,
  • один макрос {$POSTFIX_DEFERRED_LIMIT} со значением 200, который задаёт порог срабатывания триггера,
  • один триггер "Проблемы с отправкой почты", который срабатывает, если в очереди скопилось больше писем, чем указано в макросе.
Вот как выглядят соответствующие вкладки шаблона:



Вот наиболее интересный фрагмент графика с одного из серверов:

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

воскресенье, 16 апреля 2017 г.

Установка Debian с флешки по SSH

Когда возникает задача настроить сервер для другого города, часто задача решается одним из способов:
  1. Сервер отправляется в филиал полностью преднастроенным, так что местным системным администраторам остаётся только установить его в стойку и подключить к сети,
  2. Один из системных администраторов из головного офиса компании отправляется в командировку в филиал, где и настраивает сервер вместе с местными системными администраторами,
  3. Местные системные администраторы сами устанавливают на имеющееся оборудование операционную систему и настраивают на сервере сеть, после чего настройку сервера продолжают системные администраторы из головного офиса.
Последний способ позволяет обойтись без расходов на транспорт - не нужно платить за перевозку преднастроенного сервера, не нужно оплачивать командировочные расходы. Однако и у этого способа имеются свои недостатки - иногда на этапе установки операционной системы бывает нужно выполнить сложную разметку дисков или подгрузить дополнительные драйверы. В подобных случаях лучшим решением может быть использование устройства IP-KVM, если оно есть в филиале. Если же такого устройства в филиале нет, то сгодиться могут разнообразные суррогаты. Один из таких суррогатов и будет рассмотрен в этой статье. Мы подготовим образ флэш-карты с инсталлятором операционной системы Debian, консоль которого после загрузки будет доступна по SSH.

Чтобы подготовить загрузочный образ, необходима уже установленная система Debian. Установим пакет debian-installer, необходимые для его сборки зависимости и пакет fakeroot:
# apt-get source debian-installer
# apt-get build-dep debian-installer
# apt-get install fakeroot
В текущем каталоге будет создан каталог с исходными текстами пакета debian-installer. Перейдём в этот каталог:
# cd debian-installer-20130613+deb7u3/
Откроем в редакторе файл build/pkg-lists/base и добавим в него одну строчку:
#include "network-console"
Эта строчка добавит в инсталлятор дополнительные пакеты, которые сделают возможным доступ к загруженному инсталлятору по SSH.

Создадим файл preseed.cfg и пропишем в него ответы на первые вопросы инсталлятора:
# Выбираем локаль и раскладку клавиатуры
d-i localechooser/shortlist select RU
d-i keyboard-configuration/xkb-keymap select ru
# Задаём настройки сетевой карты
d-i netcfg/enable boolean true
d-i netcfg/choose_interface select eth0
d-i netcfg/disable_autoconfig boolean true
d-i netcfg/get_ipaddress string 192.168.1.16
d-i netcfg/get_netmask string 255.255.255.0
d-i netcfg/get_gateway string 192.168.1.1
d-i netcfg/get_nameservers string 192.168.1.2 192.168.1.3
d-i netcfg/confirm_static boolean true
# Задаём имя сервера и домен
d-i netcfg/get_hostname string server
d-i netcfg/get_domain string core.ufanet.ru
# Включаем доступ к инсталлятору по SSH с логином installer и паролем installer-password
d-i anna/choose_modules string network-console
d-i preseed/early_command string anna-install network-console
d-i network-console/password password installer-password
d-i network-console/password-again password installer-password
# Подключаем сетевой репозиторий для скачивания недостающих пакетов
d-i mirror/country string manual
d-i mirror/http/hostname string mirror.ufanet.ru
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
d-i mirror/suite string wheezy
d-i mirror/udeb/suite string wheezy
В этом файле можно сразу указать сетевые настройки будущего сервера, согласовав их с системными администраторами филилала. А можно договориться использовать одни и те же сетевые настройки только на период настройки новых серверов, после чего на готовом сервере менять настройки на постоянные.

Теперь отредактируем build/config/local и добавим в него полное имя файла preseed.cfg и кодовое имя релиза инсталлятора:
PRESEED=/root/preseed.cfg
USE_UDEBS_FROM=wheezy
Осталось только собрать образ загрузочного диска с инсталлятором. Для этого нужно перейти в каталог build и выполнить команду сборки образа:
# cd build
# fakeroot make rebuild_monolithic
Эта команда соберёт образ "small bootable CD image for network install" - "маленький загрузочный образ компакт-диска для сетевой установки". Такой образ содержит полный комплект пакетов самого инсталлятора, но устанавливаемые пакеты будет скачивать по сети.

Собранный образ можно найти в файле dest/netboot/mini.iso Остаётся только отправить этот образ системным администраторам филиала, например, по электронной почте. Дальше системные администраторы филиала должны будут записать этот образ на свободную флэшку и загрузить с неё будущий сервер. Дальнейший процесс разметки дисков, настройки учётных записей, настройки системных часов, часового пояса можно производить через SSH из головного офиса, зайдя по SSH с логином installer и указав пароль из файла preseed.cfg (в примере выше это пароль installer-password).

Полный список вариантов образов можно узнать, выполнив команду fakeroot make без указания конкретной цели сборки. Среди других вариантов для наших целей полезными могут оказаться:
  • rebuild_netboot - "tiny CD image that boots the netboot installer" - "крошечный образ компакт-диска, который загрузит установщик с сетевой загрузкой". Образ получится самым маленьким, т.к. недостающие пакеты инсталлятора будут загружаться по сети. Собранный образ можно найти в файле dest/monolithic/mini.iso,
  • rebuild_hd-media - "1 gb image (compressed) for USB memory stick" - "сжатый образ размером в 1 гигабайт для флэш-карт". Этот образ содержит в себе полный комплект пакетов инсталлятора и самые популярные устанавливаемые пакеты операционной системы. Собранный образ можно найти в файле dest/hd-media/boot.img.gz
При создании файла preseed.cfg можно ориентироваться на официальный пример файла wheezy/example-preseed.txt или брать значения опций из уже установленной где-либо системы. Для этого нужно установить в этой системе пакет debconf-utils:
# apt-get install debconf-utils
Вывести на экран значения настроек можно при помощи такой команды:
# debconf-get-selections --install
Вместо указания настроек mirror в файле preseed.cfg можно создать файл со списком репозиториев - build/sources.list.udeb.local. Например, вот такой:
deb http://mirror.ufanet.ru/debian wheezy main contrib non-free
deb http://mirror.ufanet.ru/debian wheezy-updates main contrib non-free
deb http://mirror.ufanet.ru/debian wheezy-proposed-updates main contrib non-free
deb http://mirror.ufanet.ru/debian-security wheezy/updates main contrib non-free
Использованные материалы:

воскресенье, 9 апреля 2017 г.

Решение проблемы с паразитным трафиком TCP ACK

Столкнулся с необычной проблемой. Имеется Zabbix-сервер, работающий под управлением FreeBSD, и Zabbix-прокси, работающий под управлением Linux. Между ними находится NAT-шлюз, работающий под управлением FreeBSD. В такой конфигурации Zabbix-сервер и Zabbix-прокси периодически начинают забрасывать друг друга огромным количеством пустых пакетов с флагом ACK:
09:59:13.868487 IP x.y.z.4.60272 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 3255861608], length 0
09:59:13.868496 IP x.y.z.8.10054 > x.y.z.4.60272: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.868498 IP x.y.z.4.53217 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 738884814], length 0
09:59:13.868504 IP x.y.z.8.10054 > x.y.z.4.53217: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.868990 IP x.y.z.4.60272 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 3255861608], length 0
09:59:13.868997 IP x.y.z.8.10054 > x.y.z.4.60272: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.868999 IP x.y.z.4.53217 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 738884814], length 0
09:59:13.869005 IP x.y.z.8.10054 > x.y.z.4.53217: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.869364 IP x.y.z.4.60272 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 3255861608], length 0
09:59:13.869372 IP x.y.z.8.10054 > x.y.z.4.60272: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.869374 IP x.y.z.4.53217 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 738884814], length 0
09:59:13.869380 IP x.y.z.8.10054 > x.y.z.4.53217: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.869863 IP x.y.z.4.60272 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 3255861608], length 0
09:59:13.869870 IP x.y.z.8.10054 > x.y.z.4.60272: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.869872 IP x.y.z.4.53217 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 738884814], length 0
09:59:13.869878 IP x.y.z.8.10054 > x.y.z.4.53217: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.870238 IP x.y.z.4.60272 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 3255861608], length 0
09:59:13.870245 IP x.y.z.8.10054 > x.y.z.4.60272: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.870247 IP x.y.z.4.53217 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 738884814], length 0
09:59:13.870253 IP x.y.z.8.10054 > x.y.z.4.53217: Flags [.], ack 4294967295, win 65535, length 0
Тут видно два подключения с портов 60272 и 53217 на порт 10054. Интервалы времени между ответными пакетами составляют единицы микросекунд. В результате образуется взаимный шторм в десятки тысяч пакетов в секунду. Самое интересное, что остановка Zabbix-прокси не приводит к пропаданию этого трафика - пакеты продолжают сыпаться.

Имеется аналогичная конфигурация, в которой Zabbix-сервер работает под управлением Linux. То есть два Linux'а разделены NAT-шлюзом под управлением FreeBSD. В этой конфигурации подобной проблемы не наблюдается.

Как я выяснил, Zabbix тут вообще не при чём. Я нашёл описание заплатки для Linux, в которой описана эта проблема: Merge branch 'tcp_ack_loops'. Проблема проявляется, когда промежуточный шлюз вносит изменения в параметры TCP-подключения этих двух узлов - меняет номер последовательности, номер ACK или отметку времени. Один из узлов, получив такой пакет, сообщает противоположной стороне о несовпадении параметров установленного подключения, отправляя ей пакет ACK. Другая сторона получает этот пакет ACK, а параметры в нём тоже не совпадают с параметрами установленного подключения. Обе стороны начинают забрасывать друг друга ACK-пакетами, в которых сообщается, что противная сторона отправила пакет с параметрами, отличающимися от параметров установленного соединения.

В новых версиях ядра Linux должна появиться переменная net.ipv4.tcp_invalid_ratelimit, описание которой можно найти здесь - /proc/sys/net/ipv4/* Variables. Приведу перевод описания этой переменной ядра:
tcp_invalid_ratelimit - ЦЕЛОЕ

Ограничивает максимальную скорость отправки дубликатов подтверждений в ответ на входящие пакеты TCP, которые соответствуют существующему подключению, но по одной из причин считаются неправильными:
  1. номер последовательности за пределами окна,
  2. номер подтверждения за пределами окна, или
  3. ошибка проверки PAWS (Protection Against Wrapped Sequence numbers - защита от завёрнутых номеров последовательности).
Может помочь смягчить простые DoS-атаки "цикл ACK", когда неисправный хост или злоумышленник между ними может переписать поля заголовка TCP таким образом, что это заставит каждую из сторон считать, что другая отправляет неисправные сегменты TCP, что заставит каждую из сторон отправлять непрекращающийся поток дубликатов подтверждений о неисправных сегментах.

Значение 0 отключит ограничение скорости дубликатов подтверждений в ответ на неисправные сегменты; в противном случае значение указывает минимальный интервал между отправкой дубликатов подтверждений в миллисекундах.

Значение по умолчанию: 500 миллисекунд.
Обновлять ядро Linux мне не хотелось, т.к. в конфигурации Linux-FreeBSD-Linux всё работало нормально и без этой заплатки. Я стал искать обходные решения.

На правильное направление меня вывела вот эта статья - FreeBSD Tuning and Optimization. Приведу перевод фрагмента, описывающего возможные проблемы от включения функции Syncookies:
Syncookies обладают как плюсами, так и минусами. Syncookies полезны если вы находитесь под DoS-атакой, поскольку с их помощью можно отличать настоящих клиентов от компьютеров злоумышленников. Но, поскольку опции TCP из начального SYN-пакета не сохраняются в Syncookies, опции TCP не применяются к подключению, препятствуя использованию функций масштабирования окна, отметок времени или точному определению размера MSS. Поскольку при получении ответного ACK устанавливаетcя подключение, злоумышленник может попробовать наводнить машину пакетами ACK, чтобы попытаться создать подключение.

Дополнительно для переполнения получателя настоящих SYN-куки злоумышленник может добавить к пакету данные. Тогда злоумышленник сможет отправлять данные сетевому демону FreeBSD, даже используя поддельный IP-адрес отправителя. Это приведёт к тому, что FreeBSD будет обрабатывать данные, которые не обрабатывались бы, если бы Syncookies были выключены. Несмотря на то, что Syncookies полезны во время DoS-атак, мы собираемся отключить синкуки.
Я попробовал выключить на Zabbix-сервере под управлением FreeBSD использование опции Syncookies:
# sysctl -w net.inet.tcp.syncookies=0
Затем на время заблокировал на время исходящий трафик на Zabbix-прокси, работающем под управлением Linux:
# iptables -A OUTPUT -d x.y.z.8 -p tcp -m tcp --dport 10054 -j DROP
Выждав полминуты, снова его разблокировал:
# iptables -D OUTPUT -d x.y.z.8 -p tcp -m tcp --dport 10054 -j DROP
В результате паразитный трафик пропал.

То есть, как я понял, изменения, вносимые в TCP-пакеты промежуточным шлюзом, осуществляющим трансляцию адресов, могут приводить к тому, что обе стороны будут слать друг другу ACK-пакеты с несовпадающими параметрами. Эти ACK-пакеты сервером FreeBSD будут восприниматься как пакеты, продолжающие процесс установки нового подключения - FreeBSD посчитает, что отправитель ACK-пакета ранее отправлял ей SYN-пакет и уже получил в ответ подтверждение в виде SYN-ACK-пакета. Таким образом FreeBSD создаст новое подключение. В дальнейшем обе стороны будут обмениваться друг с другом сообщениями о получении пакета, не соответствующего тем параметрам подключения, которые обе стороны согласовали друг с другом ранее. Не претендую на полное понимание ситуации и точность объяснения, но отключение Syncookies на стороне Zabbix-сервера под управлением FreeBSD помогло избавиться от проблемы.

Чтобы Syncookies отключались после перезагрузки системы, нужно внести соответствующие настройки в файл /etc/sysctl.conf:
net.inet.tcp.syncookies=0

воскресенье, 2 апреля 2017 г.

Решение проблемы с остановкой загрузки из-за RAID-контроллера Intel RS2WC040

Столкнулся с проблемой - один из серверов не загружался после аварийного отключения питания. К сожалению, снимки экрана в процессе загрузки не сохранились, поэтому я сейчас не могу сказать, что натолкнуло меня на мысль о том, что причина заключается в RAID-контроллере. Решил сравнить при помощи diff настройки контроллеров на проблемном сервере и на сервере, где такой проблемы не наблюдается. Для этого вывел настройки RAID-контроллеров при помощи такой команды:
# megacli -AdpAllInfo -aALL
Глаз зацепился только на одном различии, которое можно причислить собственно к настройкам контроллеров, а не к их свойствам:
< BIOS Error Handling            : Pause on Errors
---
> BIOS Error Handling            : Stop On Errors
Поискал в интернете и нашёл статью How to disable LSI MegaRAID SAS controller’s suspend boot on error “feature”

Посмотреть текущее значение этой настройки можно следующей командой:
# MegaCli -AdpBIOS -Dsply -aALL
На проблемном сервере эта команда выдавала следующее:
BIOS on Adapter 0 is Enabled.
    BIOS will Bypass error.
Auto select Boot on Adapter 0 is Disabled.

Exit Code: 0x00
На сервере без проблем эта команда выдавала следующее:
BIOS on Adapter 0 is Enabled.
    BIOS will Stop on error.
Auto select Boot on Adapter 0 is Disabled.

Exit Code: 0x00
Выставить значение Stop on error можно следующей командой:
# MegaCli -AdpBIOS -SOE -aALL
При последующих перезагрузках сервера больше такой проблемы не наблюдалось.

воскресенье, 26 марта 2017 г.

Таймаут DNS в OpenNTPd

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

Стал разбираться, что же не так. Как оказалось, загрузка остановилась на этапе запуска OpenNTPd. Заметил, что сервер не был подключен к сети. Воткнул сеть - сервер стал загружаться дальше. В интернете нашёл описание этой ошибки в OpenNTPd: net-misc/openntpd - failed dns results in extended startup delay when -s option in use

Проблема заключается в том, что у OpenNTPd предусмотрен таймаут для NTP-серверов, но не предусмотрен таймаут для DNS-серверов, поэтому демон продолжает в бесконечном цикле выполнять DNS-запросы, пытаясь узнать имена NTP-серверов.

Проблему можно решить одним из способов:
  • Убрать из настроек опцию -s,
  • Прописать в конфигурацию IP-адреса NTP-серверов, а не доменные имена,
  • Пропатчить пакет патчем из этого отчёта об ошибке.
Я решил тогда пропатчить пакет. Потом я много раз пользовался этим пакетом, но описать его сборку всё забывал. Вот сейчас решил всё-таки задокументировать эту процедуру, хотя в новых версиях Debian она станет больше не нужной, т.к. в новых версиях OpenNTPd ошибка уже исправлена.

Для начала отредактируем файл /etc/apt/sources.list и добавим строчки с репозиториями deb-src с исходными текстами:
deb http://mirror.ufanet.ru/debian/ wheezy main contrib non-free
deb http://mirror.ufanet.ru/debian/ wheezy-updates main contrib non-free
deb http://mirror.ufanet.ru/debian/ wheezy-proposed-updates main contrib non-free
deb http://mirror.ufanet.ru/debian-security wheezy/updates main contrib non-free

deb-src http://mirror.ufanet.ru/debian/ wheezy main contrib non-free
deb-src http://mirror.ufanet.ru/debian/ wheezy-updates main contrib non-free
deb-src http://mirror.ufanet.ru/debian/ wheezy-proposed-updates main contrib non-free
deb-src http://mirror.ufanet.ru/debian-security wheezy/updates main contrib non-free
Установим инструменты, необходимые для сборки пакета:
# apt-get install dpkg-dev devscripts fakeroot quilt
Установим пакет с исходными текстами openntpd:
# apt-get source openntpd
Скачаем подготовленный мной патч:
# wget http://stupin.su/files/openntpd-20080406p-dns-timeout.patch
Перейдём в каталог с исходными текстами пакета:
# cd openntpd-20080406p
Наложим скачанный патч:
# patch -Np0 < ../openntpd-20080406p-dns-timeout.patch
Установим пакеты, необходимые для сборки пакета openntpd:
# apt-get build-dep openntpd
Оформим изменения, сделанные в исходных текстах, в виде патча:
# dpkg-source --commit
В ответ на запрос имени заплатки введём dns-timeout.

Содержимое заголовка заплатки:
Description: DNS timeout added
 Added patch from https://bugs.gentoo.org/show_bug.cgi?id=493358
 .
 openntpd (20080406p-4) unstable; urgency=low
Author: Vladimir Stupin <vladimir@stupin.su>
Last-Update: <2017-02-09>

--- openntpd-20080406p.orig/ntpd.c
+++ openntpd-20080406p/ntpd.c
Теперь опишем сделанные изменения в журнале изменений:
# dch -i
Свежая запись в журнале будет выглядеть следующим образом:
openntpd (20080406p-4.1) UNRELEASED; urgency=low

  * DNS timeout added

 -- Vladimir Stupin <vladimir@stupin.su>  Thu, 09 Mar 2017 22:11:37 +0500
Соберём новый пакет с исходными текстами и двоичный пакет:
# dpkg-buildpackage -us -uc -rfakeroot
Теперь можно перейти на уровень выше и установить собранный пакет:
# cd ..
# dpkg -i openntpd_20080406p-4.1_amd64.deb
С новым пакетом пауза в процессе загрузки составляет 1 минуту 40 секунд, что вполне приемлемо, т.к. сервер в конце концов всё-таки загружается, а на фоне всего процесса загрузки эта дополнительная пауза почти не заметна.

воскресенье, 19 марта 2017 г.

Смена темы письма в Postfix

В одной из прошлых заметок Postfix как локальный SMTP-ретранслятор я описал настройки почтового сервера, выступающего в роли почтового клиента для другого сервера. Эту конфигурацию я использую для отправки служебных писем со своих серверов.

Часто письма с разных серверов приходят от одного и того же отправителя, с одной и той же темой. Если ни в теме письма, ни в его теле явным образом не упоминается сервер, с которого было отправлено письмо, то обычно приходится смотреть заголовки письма, чтобы определить, с какого из серверов оно на самом деле было отправлено.

Чтобы решить эту проблему и облегчить себе работу, я решил настроить все свои серверы так, чтобы Postfix добавлял в тему отправляемого письма имя сервера.

Сделать это просто. Для начала нужно установить пакет postfix-pcre:
# apt-get install postfix-pcre
Далее, добавим в файл /etc/postfix/main.cf одну строчку:
header_checks = pcre:/etc/postfix/rewrite_subject
Теперь нужно создать файл /etc/postfix/rewrite_subject и поместить в него правило, которое будет добавлять в тему письма дополнительный текст с именем сервера. Например, вот так:
/^Subject: (.*)$/ REPLACE Subject: $1 (from server.domain.tld)
Текст "/^Subject: (.*)$/" является регулярным выражением в стиле Perl. Это регулярное выражение будет совпадать со строкой заголовка, начинающейся с текста "Subject: ". Текст "(.*)" совпадает с любой темой письма и будет запомнен в переменной $1.

Действие "REPLACE" говорит о том, что нужно произвести замену текста.

Строка "Subject: $1 (from server.domain.tld)" содержит в текст, который заменит совпавшую строчку. При этом вместо переменной $1 будет подставлено её значение. Если имя переменной не отделено от остального текста пробельными символами, тогда её имя следует записывать в виде ${1}

Итак, осталось перезапустить почтовый сервер:
# /etc/init.d/postfix restart
Проверить, добавляется ли текст к теме письма можно следующим образом:
# mail -s test box@mailserver.tld
test
.
При этом в почтовом ящике должно появиться письмо с темой "test (from server.domain.tld)".

воскресенье, 12 марта 2017 г.

Использование urllib2 в Python

Как известно, Python поставляется с "батарейками" - в комплекте идёт масса различных модулей. Одним из таких модулей является модуль urllib2, позволяющий выполнять веб-запросы. К сожалению, этот модуль не блещет наглядностью, потому что для выполнения запроса зачастую требуется соединить между собой несколько объектов. Это может быть привычным по идеологии для программистов, пишущих на Java, но вот у большинства программистов, использующих скриптовые языки, этот подход напрочь отбивает всё желание пользоваться этим модулем. В результате я наблюдаю, например, что коллеги для выполнения веб-запросов часто пользуются сторонними, более дружелюбными модулями. Трое из них пользовались pycurl, а один - requests. Кстати, девиз модуля requests звучит как "Python HTTP для людей", что как бы намекает на то, что по мнению авторов модуля requests модуль urllib2 сделан для инопланетян. Пожалуй, в этом есть доля истины.

Не знаю почему, но я старался пользоваться родным модулем из Python. Возможно потому, что его использование позволяет обойтись без дополнительных зависимостей. Возможно также, что я пытался пользоваться родным модулем, потому что ранее в скриптах на Perl пользовался родным для него модулем LWP и у меня с ним не возникало никаких проблем. Я ожидал, что с родным модулем из Python тоже не должно возникнуть никаких проблем. Мне было трудно, но, к счастью, разбираться с модулем мне пришлось поэтапно, так что со временем в моих скриптах и программах набралось достаточное количество примеров на самые разные случаи с обработкой всех встречавшихся мне исключений. Я решил собрать все эти примеры в одном месте и поделиться ими в этой статье.

1. GET-запрос

Начнём с самого простого - с выполнения GET-запроса. Нужно просто скачать страницу.
import warnings, urllib, urllib2
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')

def http_get(str_url, dict_params, dict_headers, float_timeout):
    query = urllib.urlencode(dict_params)
    req = urllib2.Request(str_url + '?' + query, headers=dict_headers, timeout=float_timeout)
    
    opener = urllib2.build_opener()
    
    try:
        res = opener.open(req)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Здесь можно увидеть функцию http_get, которая принимает параметры:
  • str_url - строка с URL страницы, которую нужно скачать,
  • dict_params - словарь с параметрами и их значениями, из которых будет сформирована строка запроса. Эта строка запроса потом будет добавлена к строке URL после знака вопроса. Ключи словаря являются именами параметров, а значения - соответственно значениями,
  • dict_headers - словарь с дополнительными заголовками, которые будут добавлены в GET-запрос. Ключи словаря являются именами заголовков, а значения - их значениями,
  • float_timeout - число с плавающей запятой, задающее таймаут ожидания запроса.
Функция возвращает кортеж из двух значений: первое значение содержит код ошибки от веб-сервера, а второе значение содержит тело ответа. Если в строке URL содержится ошибка, не отвечает DNS-сервер, DNS-сервер сообщает об ошибке поиска имени или если соединиться с веб-сервером не удалось, будет возвращён кортеж с двумя значениями None. Если вам нужно различать ошибки разного рода, переделайте обработку ошибок под свои нужды.

Если вам нужно узнать заголовки из ответа, то имейте в виду, что объект res принадлежит классу httplib.HTTPResponse. У объектов этого класса есть методы getheader и getheaders. Метод getheader вернёт значение заголовка с указанным именем или значение по умолчанию. Метод getheaders вернёт список кортежей с именами заголовков и их значениями. Например, словарь из заголовков и их значений (исключая заголовки с несколькими значениями) можно было бы получить следующим образом:
headers = dict(res.getheaders())

2. POST-запрос enctype=application/x-www-form-urlencoded

Можно сказать, что это "обычный" POST-запрос. В таких POST-запросах применяется кодирование параметров, аналогичное тому, которое используется для кодирования параметров в GET-запросах. В случае с GET-запросами строка с параметрами добавляется после вопросительного знака к строке URL запрашиваемого ресурса. В случае с POST-запросом эта строка с параметрами помещается в тело запроса. В запросах такого рода нельзя передать на сервер файл.
import warnings, urllib, urllib2
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')

def http_post(str_url, dict_params, dict_headers, float_timeout):
    query = urllib.urlencode(dict_params)
    dict_headers['Content-type'] = 'application/x-www-form-urlencoded'
    req = urllib2.Request(str_url, data=query, headers=dict_headers, timeout=float_timeout)
    
    opener = urllib2.build_opener()
    
    try:
        res = opener.open(req)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Функция http_post по входным и выходным параметрам полностью аналогична функции http_get, только выполняет POST-запрос.

3. POST-запрос enctype=multipart/form-data

Если в запросе нужно отправить много данных, то они могут оказаться слишком большими, чтобы уместиться в одной строке. В таких случаях, например если нужно приложить файл, в веб-приложениях используются формы с типом "multipart/form-data". Для отправки подобных запросов переделаем предыдущую функцию, воспользовавшись модулем poster.
import warnings, urllib2
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')
from poster.encode import multipart_encode

def http_post_multipart(str_url, dict_params, dict_headers, float_timeout):
    body, headers = multipart_encode(dict_params)
    dict_headers.update(headers)
    req = urllib2.Request(str_url, data=body, headers=dict_headers, timeout=float_timeout)
    
    opener = urllib2.build_opener()
    
    try:
        res = opener.open(req)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Функция http_post_multipart по входным и выходным параметрам полностью аналогична функциям http_get и http_post, только выполняет POST-запрос, закодировав параметры способом, пригодным для передачи данных большого объёма, в том числе - файлов.

4. GET-запрос с аутентификацией

Продемонстрирую аутентификацию на примере с GET-запросом:
import warnings, urllib, urllib2
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')

def http_get_auth(str_url, dict_params, dict_headers, float_timeout, str_user, str_password):
    query = urllib.urlencode(dict_params)
    req = urllib2.Request(str_url + '?' + query, headers=dict_headers, timeout=float_timeout)
    
    passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
    passman.add_password(None, str_url, str_user, str_password)
    authhandler = urllib2.HTTPBasicAuthHandler(passman)
    
    opener = urllib2.build_opener(authhandler)
    
    try:
        res = opener.open(req)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Как видно, функция http_get_auth по выходным параметрам совпадает со всеми предыдущими функциями, но среди входных параметров появилось два новых:
  • str_user - строка, содержащая имя пользователя,
  • str_password - строка, содержащая пароль пользователя.
Внутри функции появились три новые строчки, выделенные жирным шрифтом. Эти строчки создают менеджер паролей и дополнительный обработчик ответа от веб-сервера. В четвёртой выделенной строчке создаётся объект, который будет выполнять запрос с использованием этого дополнительного обработчика. Задача обработчика простая - если при запросе без аутентификации будет возвращён код ошибки 401, то в запрос будет добавлен дополнительный заголовок, содержащий имя пользователя и его пароль, после чего запрос будет повторён.

Менеджер паролей может предоставлять обработчику разные пароли, в зависимости от области доступа (часто называемой realm'ом), которую сообщит веб-сервер или в зависимости от URL запрашиваемого ресурса.

5. GET-запрос с безусловной аутентификацией

Обработчик HTTPBasicAuthHandler устроен таким образом, что сначала всегда делает запрос без передачи логина и пароля. И только если получен ответ с кодом 401, уже пытается выполнить запрос с аутентификацией.

При попытке выполнить запрос к API, реализованному на основе библиотеки swagger, возникают некоторые трудности с аутентификацией. При обращении к API без аутентификации, API возвращает HTML-справку по использованию API с кодом ответа 403, поэтому дополнительных попыток получить страницу с аутентификацией не предпринимается.

Чтобы исправить эту ситуацию, я воспользовался ответом, подсмотренным здесь: does urllib2 support preemptive authentication authentication?
import warnings, urllib, urllib2, base64
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')

class PreemptiveBasicAuthHandler(urllib2.HTTPBasicAuthHandler):
    '''Preemptive basic auth.

    Instead of waiting for a 403 to then retry with the credentials,
    send the credentials if the url is handled by the password manager.
    Note: please use realm=None when calling add_password.'''
    def http_request(self, req):
        url = req.get_full_url()
        realm = None
        # this is very similar to the code from retry_http_basic_auth()
        # but returns a request object.
        user, pw = self.passwd.find_user_password(realm, url)
        if pw:
            raw = "%s:%s" % (user, pw)
            auth = 'Basic %s' % base64.b64encode(raw).strip()
            req.add_unredirected_header(self.auth_header, auth)
        return req

    https_request = http_request

def http_get_preemptive_auth(str_url, dict_params, dict_headers, float_timeout, str_user, str_password):
    query = urllib.urlencode(dict_params)
    req = urllib2.Request(str_url + '?' + query, headers=dict_headers, timeout=float_timeout)
    
    passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
    passman.add_password(None, str_url, str_user, str_password)
    authhandler = PreemptiveBasicAuthHandler(passman)
    
    opener = urllib2.build_opener(authhandler)
    
    try:
        res = opener.open(req)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Функция http_get_preemptive_auth по входным и выходным параметрам полностью аналогична функции http_get_auth, но в ней используется другой дополнительный обработчик - PreemptiveBasicAuthHandler, который отличается от обработчика HTTPBasicAuthHandler тем, что не выполняет запрос без аутентификации, ожидая получить ошибку 401, а сразу отправляет логин и пароль, соответствующие запрашиваемому URL.

6. GET-запрос через прокси

Смотрим пример:
import warnings, urllib, urllib2
warnings.filterwarnings('ignore', category=UserWarning, module='urllib2')

def http_get_proxy(str_url, dict_params, dict_headers, float_timeout, str_proxy):
    query = urllib.urlencode(dict_params)
    req = urllib2.Request(url + '?' + query, headers=dict_headers, timeout=float_timeout)
    
    proxyhandler = urllib2.ProxyHandler(str_proxy)

    opener = urllib2.build_opener(proxyhandler)
    
    try:
        res = opener.open(req)
    except urllib2.HTTPError as e:
        res = e
    except urllib2.URLError as e:
        return None, None
    except socket.timeout:
        return None, None
    except ssl.SSLError:
        return None, None
  
    return res.code, res.read()
Функция http_get_proxy по выходным параметрам полностью соответствует функции http_get, а среди входных параметров имеется один дополнительный:
  • str_proxy - строка с адресом веб-прокси. Имеет вид http://proxy.domain.tld:3128
В этом примере используется дополнительный обработчик запросов ProxyHandler, который позволяет отправлять запросы через веб-прокси. На самом деле, этому обработчику можно передать не строку с одним веб-прокси, а передать словарь, в котором для разных протоколов будут указаны разные прокси:
{
 'http': 'http://proxy-http.domain.tld:3128',
 'https': 'http://proxy-https.domain.tld:3128',
 'ftp': 'http://proxy-ftp.domain.tld:3128'
}

7. Использование нескольких обработчиков запросов одновременно

Когда в запросе нужно использовать несколько обработчиков одновременно, то можно растеряться, т.к. в примерах выше каждый раз используется только один обработчик для аутентификации или для прокси. Но использовать несколько обработчиков совсем не трудно. Это можно сделать, указав их через запятую, вот так:
opener = urllib2.build_opener(authhandler, proxyhandler)
Но и в этом случае можно растеряться, если список обработчиков нужно формировать динамически, так что каждый конкретный обработчик может отсутствовать. Тут тоже всё просто. Можно добавлять обработчики в массив, а затем использовать этот массив как список аргументов:
# В начале список обработчиков пуст
handlers = []

# Если нужна аутентификация, добавляем обработчик в список
if ...:
    ...
    authhandler = ...
    handlers.append(authhandler)

# Если запрос нужно выполнить через прокси, добавляем обработчик в список
if ...:
    ...
    proxyhandler = ...
    handlers.append(proxyhandler)

# Выполняем запрос, используя все обработчики из списка
opener = urllib2.build_opener(*handlers)
Если нужно пройти аутентификацию и на прокси и на веб-ресурсе, то для аутентификации на прокси можно использовать обработчик ProxyBasicAuthHandler, так что всего будет использоваться аж сразу три обработчика запросов.

Этих примеров достаточно для того, чтобы научиться пользоваться модулем urllib2 и понимать, в каком направлении надо копать, чтобы сделать что-то такое, что здесь не описано. Если у вас есть чем дополнить эту статью, прошу написать мне об этом в комментариях.

воскресенье, 5 марта 2017 г.

HKPK - расширение для фиксации публичного ключа HTTP в Apache, nginx и Lighttpd

Перевод: HTTP Public Key Pinning Extension HPKP for Apache, NGINX and Lighttpd
Автор: Реми ван Элст (Remy van Elst)

Содержание
  1. HPKP - расширение для фиксации публичного ключа HTTP
  2. Отпечаток SPKI - теория
  3. Что фиксировать
  4. Отпечаток SPKI
  5. Неполадки
  6. Настройка веб-сервера
    1. Apache
    2. Lighttpd
    3. nginx
  7. Отчёты
  8. Не фиксировать, только отчитываться
Фиксация публичного ключа означает, что цепочка сертификатов должна включать публичный ключ из разрешённого списка. Это даёт гарантии того, что только удостоверяющий центр из разрешённого списка может подписывать сертификаты для *.example.com, но не какой-то другой удостоверяющий центр, сертификат которого имеется в хранилище браузера. В этой статье объясняется теория и приводятся примеры настройки Apache, Lighttpd и nginx.

HPKP - Расширение для фиксации публичного ключа HTTP

Допустим, что имеется банк, который всегда использует сертификаты, выпущенные удостоверяющим центром компании A. При нынешней системе сертификации удостоверяющие центры компаний B, C и удостоверяющий центр АНБ могут создать сертификат этого банка, который будет приниматься, потому что эти компании также являются доверенными корневыми удостоверяющими центрами.

Если банк воспользуется расширением для фиксации публичного ключа - HPKP и зафиксирует первый промежуточный сертификат (от удостоверяющего центра компании A), браузеры не будут принимать сертификаты от удостоверяющих центров компаний B и C, даже если цепочка доверия верна. HPKP также позволяет браузеру отправлять в банк отчёты о попытках подмены, чтобы банк знал о проводящейся атаке.

Расширение для фиксации публичного ключа HTTP - Public Key Pinning Extension for HTTP (HPKP) - это стандарт пользовательских агентов HTTP, которые были разработаны начиная с 2011 года. Начало стандарту было положено в Google, когда фиксация была реализована в Chrome. Однако, впоследствии разработчики поняли, что ручное поддержание списка фиксированных сайтов не масштабируемо.

Вот краткий обзор возможностей HPKP:
  • HPKP задаётся на уровне HTTP с помощью заголовка Public-Key-Pins в ответах.
  • Политика срока хранения задаётся при помощи параметра max-age, который указывает длительность в секундах.
  • Заголовок Public-Key-Pins может использоваться только в успешном безопасном зашифрованном подключении.
  • При наличии нескольких заголовков обрабатывается только первый.
  • Фиксация может распространяться на поддомены при использовании параметра includeSubDomains.
  • При получении нового заголовка Public-Key-Pins, он заменяет ранее сохранённую фиксацию и метаданные.
  • Фиксация состоит из алгоритма хэширования и отпечатка Subject Public Key Info - информации публичного ключа субъекта.
В этой статье сначала рассматриваются принципы работы HPKP, а далее можно найти часть, где рассказывается, как получить необходимые отпечатки и настроить веб-сервер.

Отпечаток SPKI - теория

Как объясняет в своей статье Адам Лэнгли (Adam Langley), хэшируется публичный ключ, а не сертификат:
В общем, хэширование сертификатов - это очевидное решение, но не правильное. Проблема в том, что сертификаты удостоверяющего центра часто перевыпускаются: может быть несколько сертификатов с одним и тем же публичным ключом, именем в Subject и т.д., но с разными расширениями или сроками годности. Браузеры строят цепочки сертификатов из пула сертификатов снизу вверх, и альтернативная версия сертификата может заместить ожидаемую.

Например, у StartSSL имеется два корневых сертификата: один подписан SHA1, а другой - SHA256. Если нужно зафиксировать StartSSL как удостоверяющий центр, то какой из сертификатов следует использовать? Нужно использовать оба, но как узнать что есть второй корень, если об этом не сообщается?

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

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

Единственный тонкий момент заключается в том, что не стоит фиксировать кросс-сертифицированные корневые сертификаты. Например, корневой сертификат GoDaddy подписан сертификатом Valicert, так что старые клиенты, не знающие корневой сертификат GoDaddy всё-таки доверяют таким сертификатам. Однако, не стоит фиксировать сертификат Valicert, потому что более новые клиенты завершают цепочку сертификатом GoDaddy.

Также хэшируется SubjectPublicKeyInfo, но не двоичная последовательность публичного ключа. SPKI, наряду с самим публичным ключом, включает в себя тип публичного ключа и некоторые параметры. Это важно, потому что простое хэширование публичного ключа оставляет возможность для атаки из-за неправильной интерпретации. Рассмотрим публичный ключ Диффи-Хеллмана: если хэшируется только публичный ключ, а не SPKI полностью, то атакующий может воспользоваться тем же публичным ключом, но заставить клиента интерпретировать его как другую группу Диффи-Хеллмана. Таким же образом можно заставить браузер интерпретировать ключ RSA как ключ DSA и т.п.

Что фиксировать

Что нужно фиксировать? Не стоит фиксировать собственный публичный ключ. Ключ может смениться или оказаться скомпрометированным. Может понадобиться использовать несколько сертификатов, а ключ может смениться из-за слишком частой ротации сертификатов. Ключ может оказаться скомпрометированным, если веб-сервер взломают.

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

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

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

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

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

Это не проблема для HPKP, поскольку хэши берутся от SPKI публичного ключа, а не сертификата. Просроченная или отличающаяся цепочка удостоверяющего центра, подписавшего сертификат, в этом случае не имеет значения.

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

Отпечаток SPKI

Для получения отпечатка SPKI из сертификата можно воспользоваться командой OpenSSL, которая указана в черновике RFC:
openssl x509 -noout -in certificate.pem -pubkey | \
openssl asn1parse -noout -inform pem -out public.key;
openssl dgst -sha256 -binary public.key | openssl enc -base64
Результат:
klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=
Файл certificate.pem, подаваемый на вход команды - это первый сертификат в цепочке для этого веб-сайта. На момент написания - сервер удостоверяющего центра для безопасной проверки доменов COMODO RSA (COMODO RSA Domain Validation Secure Server CA), серийный номер - 2B:2E:6E:EA:D9:75:36:6C:14:8A:6E:DB:A3:7C:8C:07.

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

Неполадки

На момент написания этой статьи (январь 2015 года) HPKP поддерживает только один браузер (Chrome), и у него имеется важная проблема, которая заключается в том, что Chrome понимает директивы max-age и includeSubdomains из заголовков HSTS и HPKP как взаимно исключающие. Это означает, что если имеется HSTS и HPKP с разными политиками max-age или includeSubdomains, они будут перетасовываться. Обратитесь к описанию этой неполадки за более подробной информацией: HPKP принудительно использует includeSubDomains даже когда директива не указана. Благодарю Скотта Хелми (Scott Helme) из https://scotthelme.co.uk за исследования и за то, что сообщил о проблеме мне и в проект Chromium.

Настройка веб-сервера

Далее приведены инструкции по настройке трёх наиболее популярных веб-серверов. Поскольку HPKP - это просто заголовок HTTP, большинство веб-серверов позволяют задать его. Заголовок нужно настраивать на веб-сайте HTTPS.

Ниже указан пример фиксации сервера удостоверяющего центра для безопасной проверки доменов COMODO RSA (COMODO RSA Domain Validation Secure Server CA) и запасного удостоверяющего центра 2 Comodo PositiveSSL (Comodo PositiveSSL CA 2), со сроком годности в 30 дней, включая все поддомены.

Apache

Отредактируйте файл конфигурации Apache (например, /etc/apache2/sites-enabled/website.conf или /etc/apache2/httpd.conf) и добавьте следующие строки в секцию VirtualHost:
# Загрузка не обязательного модуля headers:
LoadModule headers_module modules/mod_headers.so

Header set Public-Key-Pins "pin-sha256=\"klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=\"; pin-sha256=\"633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q=\"; max-age=2592000; includeSubDomains"
Lighttpd

В случае Lighttpd всё очень просто. Добавим в файл конфигурации Lighttpd (например, в /etc/lighttpd/lighttpd.conf) следующие строки:
server.modules += ( "mod_setenv" )
$HTTP["scheme"] == "https" {
  setenv.add-response-header = ( "Public-Key-Pins" => "pin-sha256=\"klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=\"; pin-sha256=\"633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q=\"; max-age=2592000; includeSubDomains")
}
nginx

Настройка nginx даже ещё короче. Добавьте следующую строку в блок server, относящийся к настройке HTTPS:
add_header Public-Key-Pins 'pin-sha256="klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY="; pin-sha256="633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q="; max-age=2592000; includeSubDomains';
Отчёты

Отчёты HPKP позволяют пользовательским агентам отправлять вам отчёты о проблемах.

Если добавить к заголовку дополнительный параметр report-uri="http://example.org/hpkp-report" и настроить по этой ссылке прослушиватель, клиенты будут отправлять отчёты при возникновении проблем. Отчёт отправляет JSON-данные на адрес из report-uri в теле POST-запроса:
{
    "date-time": "2014-12-26T11:52:10Z",
    "hostname": "www.example.org",
    "port": 443,
    "effective-expiration-date": "2014-12-31T12:59:59",
    "include-subdomains": true,
    "served-certificate-chain": [
        "-----BEGINCERTIFICATE-----\nMIIAuyg[...]tqU0CkVDNx\n-----ENDCERTIFICATE-----"
    ],
    "validated-certificate-chain": [
        "-----BEGINCERTIFICATE-----\nEBDCCygAwIBA[...]PX4WecNx\n-----ENDCERTIFICATE-----"
    ],
    "known-pins": [
        "pin-sha256=\"dUezRu9zOECb901Md727xWltNsj0e6qzGk\"",
        "pin-sha256=\"E9CqVKB9+xZ9INDbd+2eRQozqbQ2yXLYc\""
    ]
}
Не фиксировать, только отчитываться

С помощью заголовка Public-Key-Pins-Report-Only можно настроить HPKP в режиме отправки отчётов без принуждения соблюдать фиксацию.

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