A small Bash daemon for monitoring the ICMP availability of a pool of servers with notifications in Telegram
  • Shell 97.7%
  • Dockerfile 2.3%
Find a file
2026-11-10 17:07:00 +04:00
docker-compose.yml chore: init 2026-11-10 17:07:00 +04:00
Dockerfile chore: init 2026-11-10 17:07:00 +04:00
hosts chore: init 2026-11-10 17:07:00 +04:00
pingmon.env chore: init 2026-11-10 17:07:00 +04:00
pingmon.service chore: init 2026-11-10 17:07:00 +04:00
pingmon.sh chore: init 2026-11-10 17:07:00 +04:00
README.md chore: init 2026-11-10 17:07:00 +04:00

pingmon

pingmon — небольшой Bash-демон для мониторинга ICMP-доступности пула серверов с уведомлениями в Telegram.

Поддерживаются два режима:

  • одиночный монитор — один экземпляр опрашивает весь пул;
  • реплицированный мониторинг — несколько экземпляров независимо опрашивают пул, но обычные уведомления отправляет только активная реплика.

Проект не требует БД, внешнего координатора или общего сетевого хранилища. Состояние каждого экземпляра хранится локально.

Возможности

  • параллельный ping всех хостов;
  • гистерезис переходов UP/DOWN для подавления кратковременных сбоев;
  • одно сообщение на все переходы, подтверждённые за цикл;
  • сообщение о восстановлении с длительностью простоя;
  • повтор отправки, если Telegram или все каналы доставки недоступны;
  • перебор SOCKS5-прокси и прямого подключения;
  • атомарное сохранение состояния через tmp + rename;
  • перечитывание пула хостов без перезапуска;
  • репликация со статическим приоритетом;
  • снапшот текущих DOWN при перехвате роли;
  • запуск через systemd или Docker Compose.

Как это работает

Состояние хоста

Для каждого хоста хранятся два независимых значения:

  • state — подтверждённое состояние доступности: UP или DOWN;
  • notified — последнее состояние, отправка которого была подтверждена успешным завершением curl.

Если state != notified, активный монитор пытается отправить уведомление. После успешной отправки notified приравнивается к state.

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

Переходы подтверждаются последовательными проверками:

UP → DOWN   после FAIL_THRESHOLD неудачных проверок подряд
DOWN → UP   после OK_THRESHOLD успешных проверок подряд

Одна проверка выполняет ping -c PING_COUNT. Хост считается доступным, если получен хотя бы один ответ. Началом простоя считается первая неудачная проверка текущего сбоя, а не момент подтверждения DOWN.

Если хост успел упасть и восстановиться, пока Telegram был недоступен, устаревшие DOWN и UP не отправляются: уведомления описывают актуальное состояние, а не всю пропущенную историю.

Репликация

MONITORS задаёт порядок приоритета, а SELF — имя локального экземпляра:

MONITORS="mon-a mon-b"
SELF="mon-b"

Каждая реплика:

  1. Самостоятельно пингует весь пул.
  2. Хранит собственный state-файл.
  3. Проверяет, есть ли доступный монитор выше по приоритету.
  4. Отправляет обычные события только в активной роли.

Пассивная реплика продолжает наблюдение, но при изменении state молча устанавливает notified=state. Поэтому при перехвате она уже имеет актуальную картину и не отправляет накопившуюся историю.

При переходе в активную роль реплика отправляет один снапшот:

  • список всех подтверждённых DOWN;
  • либо сообщение, что подтверждённых недоступных хостов нет.

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

State-файлы реплик не нужно синхронизировать. Общий state-файл добавил бы отдельную сетевую зависимость и не устранил бы network partition.

Требования

Для systemd-установки:

  • Linux;
  • Bash 5 или новее;
  • curl;
  • ping из iputils;
  • ca-certificates;
  • systemd.

На Debian/Ubuntu зависимости устанавливаются командой:

sudo apt update
sudo apt install -y bash curl iputils-ping ca-certificates

Для Docker-установки:

  • Docker Engine с Compose plugin;
  • Linux-хост: в Compose используется network_mode: host.

Дополнительно нужны:

  • токен Telegram-бота;
  • ID чата, канала или группы;
  • ICMP-доступность наблюдаемых хостов;
  • HTTPS-доступ к api.telegram.org напрямую или через один из настроенных прокси.

Состав проекта

Файл Назначение
pingmon.sh Основной демон
pingmon.env Пример параметров и Telegram-конфигурации
hosts Пример пула наблюдаемых хостов
pingmon.service systemd unit
Dockerfile Образ на Alpine с Bash, curl и iputils
docker-compose.yml Запуск контейнера с host networking и постоянным состоянием
README.md Документация

Настройка

pingmon.env

Не используйте приведённые примеры токена, адресов и ID чата как рабочие значения. Не публикуйте заполненный pingmon.env в Git.

Переменная По умолчанию Описание
TG_TOKEN нет Токен Telegram-бота; обязательна
TG_CHAT_ID нет ID чата для уведомлений; обязательна
INTERVAL 30 Период опроса в секундах
PING_COUNT 3 Число ICMP-пакетов на одну проверку
PING_WAIT 2 Ожидание ответа на пакет в секундах
FAIL_THRESHOLD 3 Неудачных проверок подряд до DOWN
OK_THRESHOLD 2 Успешных проверок подряд до UP
TG_PROXIES direct Каналы доставки через пробел, перебираются слева направо
MONITORS пусто Имена мониторов по убыванию приоритета; пусто — одиночный режим
SELF пусто Имя локального монитора; при репликации должно входить в MONITORS
HOSTS_FILE /etc/pingmon/hosts Путь к пулу хостов
STATE_FILE /var/lib/pingmon/state Путь к локальному state-файлу

Числовые параметры должны быть положительными целыми числами.

Пример без прокси:

TG_PROXIES=direct

Пример с локальным и резервным SOCKS5-прокси:

TG_PROXIES="socks5h://10.0.0.5:1080 socks5h://10.0.0.6:1080 direct"

direct означает прямое подключение с игнорированием proxy-переменных окружения. Рекомендуется socks5h://, а не socks5://: в первом случае DNS-имя api.telegram.org разрешается на стороне прокси.

Для каждого канала curl использует --connect-timeout 5 и --max-time 15.

hosts

Формат строки:

<имя> <IP-адрес-или-DNS-имя>

Пример:

# name          address
mon-a           10.0.0.5
mon-b           10.0.0.6
gw              192.168.1.1
db-01           10.0.0.11
vpn-edge        vpn.example.com

Правила:

  • имя может содержать только A–Z, a–z, 0–9, ., _ и -;
  • пустые строки и строки, начинающиеся с #, игнорируются;
  • имена должны быть уникальными;
  • в реплицированном режиме каждое имя из MONITORS обязательно присутствует в hosts;
  • файл перечитывается каждый цикл, поэтому изменение пула не требует перезапуска;
  • при дублировании имени или пустом новом пуле сохраняется предыдущий рабочий пул;
  • если монитор отсутствует в новом пуле, цикл пропускается и записывается ошибка в журнал.

Одиночный режим

В pingmon.env:

MONITORS=
SELF=
TG_PROXIES=direct

В hosts укажите только наблюдаемые хосты. Примеры mon-a и mon-b можно удалить.

Одиночный режим не отправляет снапшот при запуске. Сообщения формируются только при подтверждённых переходах UP/DOWN.

Режим двух реплик

Пример логической схемы:

                    ┌────────────────┐
                    │   Telegram     │
                    └───────▲────────┘
                            │
              ┌─────────────┴─────────────┐
              │                           │
       ┌──────┴──────┐             ┌──────┴──────┐
       │   mon-a     │             │   mon-b     │
       │ приоритет 1 │             │ приоритет 2 │
       └──────┬──────┘             └──────┬──────┘
              │                           │
              └────────── общий пул ──────┘

На обеих нодах hosts и порядок MONITORS должны быть одинаковыми:

MONITORS="mon-a mon-b"

На mon-a:

SELF=mon-a
TG_PROXIES="socks5h://10.0.0.5:1080 socks5h://10.0.0.6:1080 direct"

На mon-b:

SELF=mon-b
TG_PROXIES="socks5h://10.0.0.6:1080 socks5h://10.0.0.5:1080 direct"

Локальный прокси указан первым, прокси соседа — вторым. Если прокси не используются, оставьте TG_PROXIES=direct.

Нормальное поведение:

  1. mon-a активен, mon-b пассивен.
  2. После подтверждённого DOWN узла mon-a узел mon-b перехватывает роль и отправляет снапшот.
  3. После подтверждённого возвращения mon-a узел mon-b уступает роль.
  4. В реплицированном режиме реплика стартует пассивной; если она сразу становится активной, отправляется снапшот. Поэтому перезапуск текущей активной реплики может дать повторный снапшот.

Установка через systemd

systemd является предпочтительным вариантом для этой утилиты: мониторинг не зависит от Docker daemon, а Bash, curl и iputils не создают сложного окружения, ради которого обязательно нужен контейнер.

Сначала отредактируйте pingmon.env и hosts, затем из каталога проекта выполните:

sudo install -m755 pingmon.sh /usr/local/bin/pingmon.sh
sudo install -d -m755 /etc/pingmon
sudo install -m600 pingmon.env /etc/pingmon/pingmon.env
sudo install -m644 hosts /etc/pingmon/hosts
sudo install -m644 pingmon.service /etc/systemd/system/pingmon.service
sudo systemctl daemon-reload
sudo systemctl enable --now pingmon

Права на файлы различаются намеренно:

  • pingmon.env содержит токен и должен быть 0600;
  • hosts не содержит секретов и должен быть доступен процессу с DynamicUser=yes, поэтому устанавливается с 0644.

Проверка:

systemctl status pingmon --no-pager
journalctl -u pingmon -f

Unit-файл включает:

  • Restart=always и RestartSec=5;
  • DynamicUser=yes;
  • StateDirectory=pingmon;
  • AmbientCapabilities=CAP_NET_RAW;
  • NoNewPrivileges=yes;
  • ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes.

CAP_NET_RAW выдаётся явно, поскольку NoNewPrivileges=yes отключает получение file capabilities исполняемым файлом ping.

После изменения pingmon.env или pingmon.sh требуется перезапуск:

sudo systemctl restart pingmon

После изменения только hosts перезапуск не нужен.

Установка через Docker Compose

Отредактируйте pingmon.env и hosts, затем выполните:

docker compose up -d --build
docker compose ps
docker compose logs -f pingmon

Compose-конфигурация использует:

  • network_mode: host — проверки выполняются с точки зрения сети хоста, без Docker NAT;
  • init: true — корректную обработку сигналов и завершение дочерних процессов;
  • volume pingmon-state — сохранение состояния между пересозданиями контейнера;
  • TZ=Europe/Samara — часовой пояс для времени в сообщениях; при необходимости измените значение.

Перезапуск после изменения env-файла:

docker compose up -d --force-recreate

После изменения только hosts пересоздание не требуется: файл смонтирован read-only и перечитывается каждый цикл.

Остановка без удаления состояния:

docker compose down

Не используйте docker compose down -v, если нужно сохранить состояние: флаг -v удаляет volume.

При репликации на каждом сервере должен быть собственный pingmon.env с правильным SELF. Именованные volumes разных серверов не синхронизируются — это ожидаемое поведение.

Эксплуатация

Журналы systemd

journalctl -u pingmon --since today
journalctl -u pingmon -f

Журналы Docker

docker compose logs --since 24h pingmon
docker compose logs -f pingmon

Основные сообщения:

Сообщение Значение
started: ... Демон запущен с указанными параметрами
<host>: DOWN Подтверждён переход в DOWN
<host>: UP Подтверждён переход в UP
became active Реплика перехватила отправку
became passive Реплика уступила отправку приоритетному монитору
telegram send failed ... Обычные события не отправлены; будет повтор
telegram takeover snapshot failed ... Снапшот не отправлен; будет перестроен и повторён
monitor ... is missing ... Ошибка конфигурации пула; цикл пропущен
stopping Получен SIGTERM или SIGINT

State-файл

По умолчанию состояние находится в /var/lib/pingmon/state. Каждая строка имеет формат:

<name> <state> <notified> <changed_epoch> <down_at_epoch>

Файл записывается атомарно. Он предназначен для диагностики, а не для редактирования во время работы демона.

Поведение при отказах

Ситуация Поведение
Наблюдаемый хост перестал отвечать После гистерезиса активная реплика отправляет DOWN
Хост восстановился После гистерезиса отправляется UP с длительностью простоя
Telegram временно недоступен Событие остаётся pending и повторяется
Первый прокси недоступен Перебираются следующие записи TG_PROXIES
Приоритетный монитор перестал отвечать на ICMP Нижестоящая реплика перехватывает роль и отправляет снапшот
Приоритетный монитор вернулся Нижестоящая реплика становится пассивной
Процесс pingmon умер, но его хост отвечает на ping Другая реплика не перехватывает роль: она проверяет ICMP, а не heartbeat процесса
Реплики не видят друг друга, но обе видят Telegram Возможны сообщения от обеих; префикс [SELF] помогает распознать partition

Гарантии и ограничения

  • Семантика доставки — at-least-once, не exactly-once. При таймауте запрос мог быть выполнен Telegram, но ответ мог потеряться. Повтор через тот же или другой прокси способен создать дубль.
  • ICMP-доступность не равна работоспособности сервиса. Хост может отвечать на ping при неработающем приложении. Для проверки приложений потребуется отдельный TCP/HTTP healthcheck.
  • Репликация не является распределённым консенсусом. Нет leases, quorum или защиты от split-brain.
  • Отказ процесса монитора не обнаруживается, если хост продолжает отвечать на ping. Restart=always и restart policy Docker защищают от части таких отказов, но не заменяют heartbeat.
  • При первом запуске новые хосты считаются UP. Первый DOWN возникает только после FAIL_THRESHOLD неудачных проверок.
  • Все переходы цикла объединяются в одно сообщение. Это уменьшает риск rate limit, но текущая версия не разбивает сообщение на части: при очень большом количестве событий можно превысить лимит длины Telegram.
  • Отказ единственного канала доставки не компенсируется репликацией. Если все реплики используют один и тот же прокси, его отказ заблокирует уведомления всех экземпляров.
  • Отказ самого одиночного монитора остаётся невидимым для него самого. Для такого сценария нужен внешний dead man's switch или вторая реплика.

Безопасность

  • Храните заполненный pingmon.env с правами 0600 и не добавляйте его в систему контроля версий.
  • Токен передаётся curl через stdin-конфиг (-K -), а не через аргументы командной строки, поэтому не отображается в argv процесса.
  • Если URL прокси содержит логин и пароль, они также становятся частью конфигурации окружения; защищайте env-файл соответствующим образом.
  • hosts не должен содержать секреты: в systemd-установке он намеренно доступен непривилегированному процессу.

Минимальная проверка перед установкой

Проверка синтаксиса:

bash -n pingmon.sh

Проверка конфигурации Compose без запуска:

docker compose config --quiet

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