Всем хорош Zabbix, за исключением маленьких таймаутов для встроенных и внешних проверок. В файле конфигурации сервера указан максимальный таймаут 30 секунд, чего совершенно недостаточно, например, для пинга с использованием 10 000 пакетов, даже если мы установим интервал в 100 мс. Как мы знаем, по российским нормативам потери пакетов в сетях связи не должны превышать один пакет на тысячу. Особо требовательные заказчики трактуют эту цифру по-своему: всё что больше или равно 0,05% можно округлить до тех самых 0,1% или одного пакета на тысячу. Поэтому я решил для самых критичных узлов, особенно при поступлении заявки, использовать внешнюю программу (назовём её losshd), которая будет в несколько потоков измерять потери пакетов и записывать результаты в базу, а для Zabbix использовать простую утилиту (назовём её getloss), которая быстро вытащит из базы необходимое значение. Распишем алгоритм работы нашей утилиты: По запросу getloss -hlocalhost --dbname=zabbix --dbuser=zabbix --dbpass=mypassword      --address=192.168.0.1 утилита проверяет, есть ли в таблице для демона losshd адрес 192.168.0.1. Если да – достаёт из базы значение поля loss и выводит его в стандартный вывод. Если нет – добавляет его в таблицу и выводит значение 0 (будем считать, что потерь нет, пока их наличие не доказано); Далее мы обновляем в таблице счётчик last_read. А вот алгоритм работы демона losshd: Считываем из базы все ip-адреса, требующие проверки; Создаём отдельные потоки для отправки пакетов ICMP Echo Request для каждого адреса (далее - сендеры); Создаём один общий поток для ловли ответов – ICMP Echo Reply (далее - ресивер); Ожидаем завершения работы всех потоков; Считаем процент потерь для каждого IP; Записываем обновления в базу; После этого мы удаляем из таблицы все строки, у которых last_read последний раз обновлялся более недели назад. Так мы снизим нагрузку на демон losshd, убирая невостребованные проверки; Начинаем всё с начала. Итак, приступим к реализации. Я не буду размещать здесь весь код, иначе статья получится слишком объёмной. Можете посмотреть его в моём гитхабе: https://github.com/Lordgprs/losshd. Сначала создаём таблицу для внешней проверки. Для простоты сделаем её в  той же базе, с которой работает Zabbix server. Я использую PostgreSQL: Наш код на C++ будет состоять из шести файлов: options.h, options.cpp, getloss.h, getloss.cpp, losshd.h, losshd.cpp. Я приведу выборочно объявления и определения классов и методов, чтобы не перегружать статью.  Пишем базовый класс для работы с параметрами командной строки: Здесь удалены все конструкторы по умолчанию, указываем только свой конструктор от параметров argc и argv (впрочем, тоже пустой). Перечень опций и их значения мы планируем использовать в классах-потомках, поэтому делаем их protected.  Пишем класс распознавания опций для утилиты getloss: Конструктор и метод, который проверяет аргументы: Класс для работы с СУБД: В функции main() мы создаём объекты для парсинга опций и соединения с СУБД и получаем цифру с потерями из базы согласно вышеописанному алгоритму: Выводим полученное значение в стандартный вывод, чтобы сервер Zabbix смог считать его оттуда. Перевод строки здесь оставлен для отладки - необходимости в нём нет. Теперь займёмся сердцем нашей проверки: демоном losshd. Описание структур Ipv4 и Icmp, а также основные методы, такие как подсчёт контрольной суммы, практически без изменений взяты из документации к Boost.Asio:  Создадим классы IcmpSender и IcmpReceiver для соответственно отправки и получения icmp-пакетов: Пройдёмся по некоторым полям класса IcmpSender: destination_ - IP, который будем пинговать; socket_ - сокет Asio для отправки ICMP-пакетов; sequence_number_ - номер пакета ICMP; count_, interval_, size_ - соответственно, количество отправляемых пакетов, интервал между отправками и размер пакета; mtx_ и condition_variable_ - ссылки на мьютекс и переменную состояния для синхронизации потоков. В классе IcmpReceiver у нас будут следующие поля: Сокет socket_ и буфер reply_buffer_ для получения ответа ICMP; senders_count_ - ссылка на переменную, содержащую количество активных потоков-сендеров. Когда это поле будет равно нулю, мы прекратим ловить ответы; ping_results_ - результаты нашего пинга (количество принятых пакетов). В качестве ключа используем IP-адрес, переведённый в 32-битное число; Так же как в классе-сендере, здесь мы пользуемся ссылками на общий мьютекс и переменную состояния; kReceiveTimerFrequency - этой константой мы задаём количество секунд, через которое таймер будет проверять, остались ли активные потоки-сендеры. Рассмотрим процесс отправки пакетов: Здесь всё просто: заполняем объект данными и отправляем его в сокет с использованием перегруженного оператора сдвига. Между итерациями просто засыпаем на заданное полем interval_ количество миллисекунд. Теперь рассмотрим процесс ловли ответов. Здесь уже всё посложнее: Привязываем сокет к методу HandleReceive, который и будет обрабатывать входящие пакеты. Инициализируем таймер, который проверяет, живы ли потоки-сендеры (CheckIfSendersExists). Мы готовы принимать пакеты - сообщаем это остальным потокам используя метод notify_all() переменной состояния. Когда же потоки-сендеры прекратят свою работу, останавливаем таймер и сокет: Пока же отправка идёт, ловим пакеты: Увеличиваем количество принятых пакетов для IP, с которого пришёл ответ, в контейнере ping_results_. Итак, мы разобрались как отправлять и принимать пакеты. Напишем класс-планировщик, который будет отвечать за инициализацию потоков и запись результата: Конструктору класса Scheduler мы передаём только ссылку на опции командной строки. Методы CreateReceiver и CreateSender созданы для увеличения читабельности кода - они возвращают лямбда-выражение, передаваемое в конструктор потока std::thread. У нашего планировщика следующие приватные поля: options_ - набор опций командной строки; conn_ - соединение с СУБД; txn_ - обработка запросов к базе данных; address_list_ - перечень адресов, которые мы будем пинговать; ping_results_ - уже знакомый нам контейнер с результатами пинга; condition_, mtx_ - синхронизация потоков. Получаем перечень адресов, которые будем пинговать, попутно удаляя из таблицы адреса, статистику по которым не снимали более недели: Запускаем потоки: После завершения их работы складываем результаты в базу данных. Отдельно хочу рассмотреть поток-сендер: Все потоки-сендеры не начинают отправку пакетов, пока не получат уведомление от потока-ресивера с использованием метода notify_all(). После завершения сбора статистики необходимо подготовиться к новой итерации. Для этого заново получаем актуальный перечень адресов для мониторинга и очищаем контейнер с результатами: Ну и определим наконец функцию main(): Реализуем работу демона классическим для UNIX способом: используя системный вызов fork(). Собираем и запускаем наш проект. Я буду это делать на Ubuntu 22.04 LTS с использованием CMake и conan:   Мы получили в каталоге build два исполняемых файла: getloss и losshd. Запускаем losshd: Копируем getloss в каталог /usr/local/bin. В каталоге с внешними проверками zabbix создаём bash-скрипт getloss.sh: Создаём новый пустой шаблон. Я назвал его ICMP Packet Loss HD. В шаблоне создаём новый элемент данных: Добавляем шаблон узлам сети, которым требуется точное измерение качества связи. Ждём и наслаждаемся такими графиками: Сервисом уже можно пользоваться, но необходимо всё же доделать некоторые вещи: Реализовать логирование в произвольный поток вместо std::cout, а возможно и в базу; Добавить обработку исключений библиотеки libpqxx (обрыв соединения с СУБД и т.д.). Поскольку у меня есть нехватка качественного код-ревью, буду рад любой критике, которая сделает мой код лучше. Всем спасибо за внимание, надеюсь и вам помогла моя заметка.  Сетевой инженер / программист