auspex/ARCHITECTURE.md
2026-06-09 17:12:18 +03:00

23 KiB
Executable file
Raw Permalink Blame History

Auspex — Architecture

Обзор

Auspex — монолитное CLI-приложение на Go. Все модули живут в одном пакете main, конфигурация глобальная, каналы доставки алертов shared. Такой подход выбран осознанно — утилита запускается по cron, живёт секунды, сложная изоляция не нужна.

┌─────────────────────────────────────────────────────┐
│                      auspex                         │
│                                                     │
│  main.go ──► runByFlag() / showMenu()               │
│                    │                                │
│         ┌──────────┼──────────────┐                 │
│         ▼          ▼              ▼                 │
│   certcheck    portcheck      logcheck   ...        │
│         │          │              │                 │
│         └──────────┴──────────────┘                 │
│                    │                                │
│              Alert Pipeline                         │
│         ┌──────────┼──────────────┐                 │
│         ▼          ▼              ▼                 │
│     telegram    email        mattermost             │
└─────────────────────────────────────────────────────┘

Файлы и их роли

main.go — точка входа

Отвечает за два режима запуска:

CLI-флаги (runByFlag): прямой вызов нужного модуля по флагу (-c, -p, -l, -sv, -d, -m, -ss, -w).

Интерактивное меню (showMenu): ASCII-баннер, numbered menu, bufio.Reader для ввода. Поддерживает как цифры (1-8), так и текстовые алиасы (-c, c).

auspex [флаг] [аргументы]
       │
       ├── -ss  → runPortsView()
       ├── -c   → runCertCheck()
       ├── -p   → runPortCheck()
       ├── -m   → runMatchCheck(clientFilter)
       ├── -l   → runLogCheck()
       ├── -sv  → runServiceCheck()
       ├── -d   → runDockerCheck()
       ├── -w   → runWebCheck(sid)
       ├── -v   → версия
       └── -h   → usage

config.go — конфигурация

Все настройки — глобальные переменные вида cfgXxx. Загрузка из /etc/auspex/auspex.env при старте через loadConfig()applyConfig().

Формат .env:

KEY=VALUE        # кавычки обрезаются автоматически
# комментарии игнорируются

Если файл не найден — используются значения по умолчанию, вывод предупреждения в stdout. Программа продолжает работу.

Группы переменных:

  • Telegram (токен, chat ID, прокси)
  • Mattermost (URL, токен бота, channel ID)
  • Email/SMTP (хост, порт, логин, пароль, from, to)
  • PostgreSQL (user, password, dbname, port, primary host, secondary host)
  • Пути (SSL dir, nginx glob, log dir, state files)
  • Пороги (cert days, log stale minutes)
  • Параметры проверки (timeout, retries, workers)
  • Список служб для проверки

Вспомогательные функции:

  • isTelegramConfigured() — проверяет наличие токена и chat ID
  • isEmailConfigured() — проверяет host, from, to
  • isMattermostConfigured() — проверяет URL, токен, channel ID
  • printConfigStatus() — выводит статус всех каналов при запуске

telegram.go — доставка в Telegram

Функции отправки:

  • telegramSendHTML(message) — HTML-форматирование (теги <b>, <code>, <i>)
  • telegramSendPlainText(message) — plain text без форматирования (используется в portcheck)

Цепочка прокси (telegramHTTPClients):

Возвращает срез []*http.Client в порядке приоритета:

1. HTTP прокси  (если задан TELEGRAM_HTTP_PROXY)
2. SOCKS5       (если задан TELEGRAM_SOCKS5_PROXY)
3. Прямое       (всегда, как последний fallback)

telegramDoPost и telegramDoPostForm перебирают клиентов по очереди. При ошибке одного — переходят к следующему с логом предупреждения. Успешный вариант логируется.

SOCKS5: реализован через golang.org/x/net/proxy. Логин/пароль задаются отдельными переменными (не в URL) — это исключает проблемы со спецсимволами вроде ! и $ в паролях.

HTTP прокси: реализован через стандартный http.Transport{Proxy: http.ProxyURL(...)}.


email.go — доставка по Email

emailSendHTML(subject, message) — отправка HTML-письма через SMTP.

Особенности реализации:

  • Telegram-теги (<b>, <code>, <i>) являются валидным HTML и отображаются в письме без конвертации
  • \n заменяются на <br> для корректного отображения переносов строк
  • Письмо оборачивается в HTML-шаблон с CSS стилями (emailBuildHTML)
  • Поддерживается AUTH LOGIN (тип авторизации который использует Microsoft Exchange и многие корпоративные серверы) через кастомный emailLoginAuth, реализующий интерфейс smtp.Auth
  • Стандартный smtp.PlainAuth намеренно не используется — он несовместим с рядом серверов
  • Порт 587 + STARTTLS работает автоматически через smtp.SendMail
  • EMAIL_TO поддерживает несколько адресов через запятую

Диагностика при неполной конфигурации:

  • Не задан HOST/FROM/TO → лог [Email] Не настроен — пропускаем
  • Не задан USER или PASSWORD → предупреждение, попытка без авторизации (для relay-серверов)
  • Ошибка SMTP → лог с кодом ответа и телом ошибки

mattermost.go — доставка в Mattermost

mattermostSend(message) — отправка через Bot API.

Endpoint: POST /api/v4/posts с заголовком Authorization: Bearer <token>.

Успешный ответ: 201 Created (не 200, как в большинстве API).

mmHTMLToMarkdown(html) — конвертер форматирования:

HTML (Telegram) Markdown (Mattermost)
<b>текст</b> **текст**
<i>текст</i> екст_
<code>текст</code> `текст`
<pre>текст</pre> ```текст```
━━━━━ ---
&lt; &gt; &amp; < > &
Прочие теги удаляются

certcheck.go — проверка SSL сертификатов

Точка входа: runCertCheck()

Алгоритм:

  1. ccFindCerts(cfgSSLDir) — рекурсивный обход директории, фильтрация по расширениям (.crt, .pem, .cer, .cert, .der, .p7b, .p7c, .p7s, .csr)
  2. Параллельный парсинг через пул горутин (cfgMaxWorkers)
  3. ccParseCert(path) — определение формата (PEM/DER), парсинг через crypto/x509, вычисление daysLeft
  4. ccCheckAndAlert(cert) — сравнение с порогами и отправка алерта

Уровни алертов:

Условие Emoji Уровень
daysLeft < 0 💀💀💀 Сертификат истёк
daysLeft <= CERT_CRITICAL_DAYS 🚨🚨🚨 Критично
daysLeft <= CERT_WARNING_DAYS ⚠️⚠️ Предупреждение
daysLeft <= CERT_INFO_DAYS Информация
Иначе Алерт не нужен

portcheck.go — проверка доступности портов

Точка входа: runPortCheck()

Алгоритм:

  1. Подключение к PostgreSQL (primary → secondary failover)
  2. Загрузка карты check_ports из таблицы apps_settings — какие конфиги проверять
  3. pcParseNginxConfig(file) — парсинг upstream { server ip:port; } блоков regex'ом
  4. Дедупликация endpoint'ов по всем файлам
  5. Параллельная TCP-проверка (net.DialTimeout) с повторами (cfgMaxRetries)
  6. pcCompareStates — сравнение с предыдущим state-файлом
  7. Отправка алерта только при изменениях

State-файл (/tmp/port_checker_state.json):

{
  "timestamp": "2026-04-14 13:10:10",
  "status": {
    "91.206.126.83:443": {"endpoint": "...", "available": true, "config_file": "Client_12345"}
  }
}

Имена конфигов строятся из имени файла без расширения: Client_12345.confClient_12345.


logcheck.go — проверка свежести логов

Точка входа: runLogCheck()

Алгоритм (4 шага через PostgreSQL):

  1. lcGetInstancesByHostname — инстансы для текущего хоста (instances_new)
  2. lcGetClientsByInstances — клиенты для этих инстансов (client_info)
  3. lcGetAppsByClients — приложения с check_write_logs = true (apps_settings)
  4. lcCheckLogFile — поиск файла по паттерну {LOG_DIR}/*/*/{l7resourceid}*_access.log, проверка ModTime

Файл считается устаревшим если time.Since(ModTime) > cfgLogStaleMinutes.

Уровни в алерте по времени с последней записи:

Минут Emoji
61120 ⚠️
121240 🔥
> 240 💀

matchcheck.go — проверка совпадений Angie ↔ Docker

Точка входа: runMatchCheck(clientFilter)

Проверяет соответствие между портами в конфигах Angie и запущенными Docker контейнерами. Поддерживает фильтрацию по имени клиента.

Алгоритм:

  1. Парсинг конфигов Angie из cfgAngieConfDir — извлечение портов на которых слушает Angie
  2. docker ps — список запущенных контейнеров с их портами
  3. Сопоставление: для каждого клиента проверяется есть ли соответствующий контейнер и совпадают ли порты
  4. Алерт при расхождениях

servicecheck.go — проверка служб systemd

Точка входа: runServiceCheck()

Алгоритм:

  1. Параллельная проверка всех служб из cfgServicesToCheck через горутины
  2. scCheckService(name)systemctl is-active <name> + systemctl show <name> --property=SubState,Description
  3. Загрузка предыдущего state-файла
  4. scCompareStates — сравнение текущих статусов с предыдущими
  5. Алерт только при изменениях

State-файл (/tmp/auspex_service_state.json):

{
  "timestamp": "2026-04-14 13:00:00",
  "services": {
    "angie": "active",
    "docker": "failed"
  }
}

Типы изменений в алерте:

Изменение Emoji
active → любой другой
любой → active (восстановление)
failed 💀
inactive 🔴
activating/deactivating 🟡

dockercheck.go — проверка Docker контейнеров

Точка входа: runDockerCheck()

Алгоритм:

  1. dcCheckDockerAvailable()docker info для проверки доступности демона
  2. dcGetContainers()docker ps -a --format '{{.ID}}|{{.Names}}|{{.Status}}|{{.Image}}'
  3. dcGetHealth(id)docker inspect --format '{{if .State.Health}}...{{end}}' для каждого контейнера отдельно (.Health недоступен в docker ps в ряде сборок)
  4. Нормализация статуса: "Up 2 hours""running", "Exited (1) 3 min ago""exited"
  5. dcCompareStates — сравнение с предыдущим state-файлом
  6. Алерт только при изменениях

Типы событий:

Kind Условие Emoji
appeared Новый контейнер (не при первом запуске) 🆕
disappeared Контейнер исчез из docker ps -a 👻
status_changed → не running Упал/завис 🔴 💀 🔄
status_changed → running Восстановился
health_changed → unhealthy Нездоров 🤒
health_changed → healthy Оздоровился

State-файл (/tmp/auspex_docker_state.json):

{
  "timestamp": "2026-04-14 13:00:00",
  "containers": {
    "portainer": {"status": "running", "health": "none"},
    "zabbix-agent-7.0": {"status": "running", "health": "none"}
  }
}


webcheck.go — проверка доступности сайтов

Точка входа: runWebCheck(sidArg string)

Проверяет HTTP-доступность доменов WAF-клиента через браузер lynx. Использование lynx позволяет обойти антибот-системы которые блокируют стандартные HTTP-клиенты (Go, Python, curl).

Аргумент: один или несколько SID через запятую:

auspex -w 14285
auspex -w 14285,11071,9823

Алгоритм (один SID):

  1. Проверяет наличие lynx через exec.LookPath — если нет, понятная ошибка с командой установки
  2. Подключается к PostgreSQL (primary → secondary failover) — один раз для всех SID
  3. wcGetDomains — запрашивает domain_name и aliases из sp_info WHERE sid = $1, затем client_title из apps_settings WHERE l7resourceid = $1
  4. Парсит aliases из JSONB-массива ([]string)
  5. Очищает домены через wcCleanDomain — убирает markdown-ссылки вида [текст](url), протоколы, слеши
  6. Для каждого домена вызывает wcCheckDomainwcPrintResult
  7. Выводит итоговую статистику через wcPrintSummary
  8. Сохраняет CSV-файл

Алгоритм (несколько SID):

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

Несколько SID (3). Сохранить результаты:
  1) В один файл (с разделителями по SID)
  2) В отдельные файлы для каждого SID
  • Вариант 1 — один файл webcheck_{sid1}_{sid2}_{date}.csv с секциями:
=== SID: 14285 | Название клиента ===
domain,protocol,http_code,available,checked_at
...

=== SID: 11071 | Другой клиент ===
...
  • Вариант 2 — отдельный файл для каждого SID как обычно

БД и lynx инициализируются один раз независимо от количества SID.

wcGetDomains — два запроса к БД:

-- 1. Домены клиента
SELECT domain_name, aliases FROM sp_info WHERE sid = $1

-- 2. Название клиента (fallback: sid если не найдено)
SELECT client_title FROM apps_settings WHERE l7resourceid = $1 LIMIT 1

wcCheckDomain:

  • Wildcard домены (*.example.com) — пропускаются немедленно с кодом wildcard
  • Пробует https:// → при DNS-ошибке возвращает dns_error без попытки http
  • Пробует http:// как fallback если https не дал HTTP-кода
  • Если оба протокола не дали результата → unavailable

wcLynxGet(url) — ядро проверки:

lynx -head -dump -connect_timeout=10 -read_timeout=10 <url>
  • -head — HTTP HEAD запрос (только заголовки, без загрузки тела)
  • -dump — неинтерактивный режим, вывод в stdout
  • Парсит HTTP-код регулярным выражением HTTP/[\d.]+ (\d{3})
  • При редиректах берёт последний код — он актуальный
  • DNS-ошибка определяется по строкам Unable to locate remote host и Can't access startfile в выводе lynx

Коды результата:

HTTPCode Значение
200, 301, 302... HTTP-код ответа сервера
dns_error Домен не резолвится
wildcard Wildcard домен, пропущен
error Оба протокола недоступны

wcIsSuccessCode — считает успешными коды 2xx и 3xx.

CSV-файл сохраняется в /var/log/auspex/ с UTF-8 BOM для корректного открытия в Excel:

  • Один SID: webcheck_{sid}_{date}.csv
  • Несколько SID (раздельно): webcheck_{sid}_{date}.csv для каждого
  • Несколько SID (объединённо): webcheck_{sid1}_{sid2}_..._{date}.csv

Просмотр CSV в терминале:

column -t -s',' /var/log/auspex/webcheck_14285_2026-04-29.csv

portsview.go — TUI просмотр портов

Точка входа: runPortsView()

Интерактивный TUI на базе Bubble Tea. Показывает занятые порты на хосте (ss -tlnp / netstat). Поддерживает фильтрацию, навигацию клавишами.


Pipeline доставки алертов

Все модули с алертами следуют единому паттерну:

// 1. Telegram
if err := telegramSendHTML(message); err != nil {
    log.Printf("❌ Ошибка отправки в Telegram: %v", err)
} else {
    log.Println("✅ Алерт успешно отправлен в Telegram")
}

// 2. Email
if err := emailSendHTML("Тема", message); err != nil {
    log.Printf("❌ Ошибка отправки Email: %v", err)
} else {
    log.Println("✅ Алерт успешно отправлен на Email")
}

// 3. Mattermost
if err := mattermostSend(message); err != nil {
    log.Printf("❌ Ошибка отправки в Mattermost: %v", err)
} else {
    log.Println("✅ Алерт успешно отправлен в Mattermost")
}

Все три вызова независимы — ошибка одного не влияет на остальные.


Паттерн state-файлов

Модули -p, -sv, -d используют одинаковый механизм для предотвращения спама при запуске по cron:

Запуск N:   Загрузить prev_state → Получить curr_state → Compare → Alert if changed → Save curr_state
Запуск N+1: Загрузить prev_state → Получить curr_state → Compare → ...

Первый запуск (state-файл отсутствует): состояние сохраняется, алерты не отправляются. Исключение: в dockercheck появление нового контейнера (dcKindAppeared) тоже не алертится при первом запуске — флаг isFirstRun передаётся в dcCompareStates.


Параллелизм

Модуль Механизм
certcheck Пул горутин (cfgMaxWorkers) через канал задач
servicecheck Горутина на каждую службу + sync.WaitGroup
dockercheck Последовательно (inspect на каждый контейнер отдельно)
portcheck Горутина на каждый endpoint

Работа с PostgreSQL

Модули portcheck и logcheck используют PostgreSQL с failover:

primary (cfgPrimaryDBHost) → ping OK? → использовать
                           → ping fail → secondary (cfgSecondaryDBHost)

Каждый модуль реализует свой pcConnectDB() / lcConnectDB() / wcConnectDB() — функции идентичны по логике, префиксы разные для избежания коллизий имён в пакете main.

webcheck также использует PostgreSQL для получения доменов из таблицы sp_info.


Соглашения по именованию

Все идентификаторы в каждом модуле имеют уникальный префикс — это позволяет всем файлам жить в одном пакете main без конфликтов:

Модуль Префикс
certcheck cc
portcheck pc
logcheck lc
matchcheck mc
servicecheck sc
dockercheck dc
email email
mattermost mm
webcheck wc
config cfg