magos/ARCHITECTURE.md
2026-04-24 17:44:32 +03:00

33 KiB
Raw Blame History

Архитектура Magos

Обзор системы

Magos - это система управления WAF (Web Application Firewall) контейнерами на базе Nginx/Angie, которая:

  • Автоматически разворачивает и управляет Docker-контейнерами с Nginx
  • Настраивает балансировщик Angie для распределения трафика
  • Интегрируется с API ServicePipe для получения конфигурации
  • Поддерживает graceful shutdown при масштабировании и миграции
  • Обеспечивает изоляцию клиентов через отдельные контейнеры
  • Поддерживает два WAF-вендора: PTAF (Docker-контейнеры) и SW/SolidWall (нативный процесс)

Ключевые особенности:

  • Многоклиентовая архитектура - каждый клиент имеет свои контейнеры
  • Поддержка двух вендоров - PTAF (Docker) и SolidWall (нативный nginx)
  • Динамическое управление портами - автоматическое выделение портов из пулов с учётом всех состояний сокетов
  • Кэширование данных - минимизация обращений к API
  • Failover БД - автоматическое переключение между primary/secondary
  • Graceful draining - безопасное удаление контейнеров без потери трафика
  • Умный lock-файл - автоматическая очистка при падении процесса

Компоненты системы

1. Основные файлы программы

main.go                # Точка входа, основной цикл обработки
config.go              # Управление конфигурацией
db.go                  # Работа с PostgreSQL
api.go                 # Интеграция с ServicePipe API
types.go               # Структуры данных
utils.go               # Утилиты и вспомогательные функции

2. Управление контейнерами

containers_part1.go    # Создание и настройка контейнеров
containers_part2.go    # Управление жизненным циклом контейнеров
drain.go               # Graceful shutdown и миграция

3. Генерация конфигураций

nginx.go               # Генерация конфигов Nginx и Angie
ports.go               # Управление портами
ptaf_processor.go      # Обработка PTAF-клиентов
sw_processor.go        # Обработка SW/SolidWall-клиентов

Поддерживаемые WAF-вендоры

PTAF (Positive Technologies Application Firewall)

Схема проксирования:

Клиент → Angie (хост) → Docker-контейнер (PTAF nginx) → Origin
  • Контейнеры поднимаются через docker-compose
  • Angie — фронтенд-балансировщик, конфиги в /etc/angie/http.d/angie-ptaf-{clientTitle}.conf
  • Nginx внутри контейнера — WAF-агент, конфиги в /home/install/conf/ptaf-nginx/
  • Порты выделяются автоматически из диапазонов 18000-20999 (HTTP) и 21000-23999 (HTTPS)

SW / SolidWall

Схема проксирования:

Клиент → solidwall-nginx (WAF, публичный) → solidwall-nginx (внутренний) → Origin
  • Работает без Docker, напрямую на хосте
  • Двойное проксирование через внутренние порты 8000 (HTTP) и 9000 (HTTPS)
  • Конфиги: /etc/solidwall-nginx/sites-available/solidwall-{clientTitle}-{l7ResourceID}.conf
  • Симлинки в sites-enabled/
  • Утилита не трогает: nginx.conf, директорию ssl/, default.conf

Архитектура данных

База данных PostgreSQL

Таблица instances_new

Связь hostname → instance (waf_instance)

hostname    | instance
------------|------------
waf-host-01 | instance-a
waf-host-01 | instance-b
waf-host-02 | instance-c

Таблица client_info

Основная информация о клиентах

Поля:
- client_title           # Уникальный идентификатор клиента
- containers_count       # Количество контейнеров (0 = drain всех)
- ptaf_config            # Строка подключения к PTAF-серверу (CONNECTION_STRING)
- fluent_bit_port        # Порт для FluentBit логирования
- waf_instance           # Instance для привязки к хосту
- docker_image           # Docker-образ для контейнеров клиента (переопределяет глобальный)
- docker_image_download  # URL для скачивания образа
- debug                  # Режим отладки (bool, влияет на лог-уровень)
- shm_size               # Размер /dev/shm в GB (дефолт: 1)
- ptaf_fallback_code     # Код ответа ptaf_fallback: число или 'pass' (дефолт: 418)
- sp_antiddos            # Использовать антиддос сети в set_real_ip (дефолт: true)
- real_ip_networks       # Кастомные сети для set_real_ip если sp_antiddos = false (inet[])

Важно: waf_instance определяет на каком хосте должен работать клиент

containers_count = 0: все контейнеры клиента помечаются для drain, Angie-конфиг удаляется

Таблица apps_settings

Настройки каждого L7 ресурса (домена)

Основные поля:
- l7resourceid                      # ID ресурса из ServicePipe API
- client_title                      # Связь с client_info
- waf_enabled                       # Включен ли WAF
- waf_vendor                        # 'ptaf' или 'sw'
- mode                              # auto/manual/disabled

Порты:
- custom_input_http_ports[]         # HTTP порты для прослушивания (Angie/SW)
- custom_input_https_ports[]        # HTTPS порты для прослушивания (Angie/SW)
- custom_output_http_ports[]        # HTTP порты для проксирования к origin
- custom_output_https_ports[]       # HTTPS порты для проксирования к origin

Кастомизация Nginx (PTAF):
- upstream_nginx_custom             # Кастомные upstream директивы
- server_nginx_custom               # Кастомный server блок / SERVER_BLOCK
- location_nginx_custom             # Кастомные location директивы
- server_directives_nginx_custom    # Дополнительные server директивы

Кастомизация Angie:
- upstream_angie_custom             # Кастомные upstream директивы
- server_angie_custom               # Кастомный server блок / SERVER_BLOCK
- location_angie_custom             # Кастомные location директивы
- server_directives_angie_custom    # Дополнительные server директивы
- custom_angie_ssl                  # Полная замена SSL директив
- custom_sw_nginx_ssl               # Кастомные SSL директивы для SW

Специальные настройки:
- sni                               # 0 = keepalive on, 1 = keepalive off
- ssl_enabled                       # true/false (по умолчанию true)
- max_fails                         # Попыток до пометки сервера недоступным (дефолт 5)
- fail_timeout                      # Время восстановления сервера (дефолт 15с)
- balancing_method                  # Метод балансировки: least_conn, ip_hash, random и др.
- proxy_next_upstream_codes         # HTTP-коды для proxy_next_upstream (дефолт: 500,502,503,504)

Таблица sp_info (кэш API данных)

Кэширует данные из ServicePipe API

Поля:
- sid              # l7resourceid
- domain_name      # Основной домен
- aliases          # JSON массив алиасов
- origins          # JSON массив origin серверов
- updated_at       # Timestamp обновления

TTL кэша: 10 минут

Таблица manual_info

Ручное управление (альтернатива API)

Поля:
- sid              # l7resourceid
- domain_name      # Основной домен
- aliases          # JSON массив алиасов
- origins          # JSON массив origin серверов

Примечание: Данные из manual_info не имеют TTL и всегда актуальны

Таблица ips

Настройки IP сетей

Поля:
- id (always = 1)
- antibot_networks[]    # Массив CIDR для antibot
- waf_networks[]        # Массив CIDR для WAF (фильтруются из origins)

Процесс работы

1. Инициализация

main()  initConfig()  connectToDB()

Шаги:

  1. Создание lock-файла /var/lock/ptaf-main.lock с PID (умная проверка — автоматически удаляет если процесс уже не существует)
  2. Подключение к БД (primary → secondary fallback)
  3. Загрузка IP сетей из таблицы ips
  4. Получение hostname текущего сервера
  5. Определение instances для этого хоста
  6. Для PTAF-хостов: проверка/скачивание Docker-образов (по одному на клиента)

2. Основной цикл обработки

┌─────────────────────────────────────────┐
│  Глобальная проверка drain-контейнеров  │
│  (processAllDrainingContainersGlobally) │
└─────────────────────────────────────────┘
            ↓
┌─────────────────────────────────────────┐
│  Для каждого instance на этом хосте     │
└─────────────────────────────────────────┘
            ↓
┌─────────────────────────────────────────┐
│  Получить всех клиентов для instance    │
│  (getClientInfoByInstance)              │
└─────────────────────────────────────────┘
            ↓
┌─────────────────────────────────────────┐
│  Определить vendor (ptaf/sw)            │
│  → processPTAFClient / processSWClient  │
└─────────────────────────────────────────┘
            ↓
┌─────────────────────────────────────────┐
│  Финальная проверка drain Angie-конфигов│
│  (processDrainingAngieConfigs)          │
└─────────────────────────────────────────┘

3. Обработка PTAF-клиента

1. Проверка принадлежности хоста к WAF instance
   ├─ Не принадлежит → handleClientMigration (drain всех контейнеров + Angie конфига)
   └─ Принадлежит → продолжаем

2. containers_count = 0?
   └─ Да → drain всех контейнеров + Angie конфига, выход

3. cleanStaleDrainMarkers — очистка устаревших маркеров

4. fetchPTAFResourcesData — получение данных ресурсов из API/кеша

5. resolvePortsForClient — загрузка/выделение портов
   ├─ .ports.json существует → переиспользуем порты
   ├─ Scale up → сохраняем старые порты, добавляем новые
   └─ Нет файла → выделяем все заново

6. generateAngieConfigsWithoutReload — генерация Angie конфига

7. setupContainers — генерация конфигов и управление контейнерами
   ├─ Запущен недавно, порты не слушают → пересоздаём
   ├─ Compose изменился → пересоздаём
   ├─ Конфиг изменился → nginx -s reload
   └─ Новый → запускаем (с ожиданием освобождения портов)

8. Проверка портов → reload Angie если нужно

9. processDrainingContainers — drain лишних контейнеров

4. Обработка SW-клиента

1. Проверка принадлежности хоста к WAF instance
   └─ Не принадлежит → пропускаем (миграция не реализована для SW)

2. Создание директорий: sites-available, sites-enabled, /var/log/solidwall

3. Для каждого ресурса:
   - Генерация конфига solidwall-{clientTitle}-{l7ResourceID}.conf
   - Создание симлинка в sites-enabled

4. solidwall-nginx -t + solidwall-nginx -s reload (если конфиги изменились)

5. Сбор данных о ресурсах

Для каждого ресурса определяется режим работы через поле mode:

Режим auto (по умолчанию)

1. Проверка кэша sp_info (TTL 10 мин)
   ├─ Данные свежие → использовать
   └─ Данные устарели → API запрос
2. API запросы (с retry × 3):
   ├─ GET /l7/resource/{id}/global → domain_name
   ├─ GET /l7/alias/global?l7ResourceId={id} → aliases
   └─ GET /l7/origin/global?l7ResourceId={id} → origins
3. Фильтрация origins (удалить WAF IP)
4. Сохранение в sp_info

Режим manual

1. Чтение из таблицы manual_info
2. Фильтрация origins (удалить WAF IP)
3. API и кэш игнорируются

Режим disabled

Ресурс полностью пропускается

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

Диапазоны портов

const (
    HTTPPortStart  = 18000  // HTTP: 18000-20999 (3000 портов)
    HTTPPortEnd    = 20999
    HTTPSPortStart = 21000  // HTTPS: 21000-23999 (3000 портов)
    HTTPSPortEnd   = 23999
)

Важно: На хостах необходимо зарезервировать этот диапазон чтобы ядро не использовало его для исходящих ephemeral-соединений:

sysctl -w net.ipv4.ip_local_reserved_ports=18000-23999

Алгоритм выделения портов

┌────────────────────────────────────────────────┐
│ 1. Получить занятые порты (ss -tuanl)           │
│    Включает: LISTEN, ESTAB, TIME-WAIT           │
└────────────────────────────────────────────────┘
            ↓
┌────────────────────────────────────────────────┐
│ 2. Загрузить сохраненные порты                 │
│    из .ports.json (если есть)                  │
│    ├─ Scale up → сохранить старые, добавить    │
│    └─ Нет файла → выделить все заново          │
└────────────────────────────────────────────────┘
            ↓
┌────────────────────────────────────────────────┐
│ 3. Для каждого ресурса × контейнера:           │
│    - Определить нужные порты                   │
│    - Выделить свободные Docker порты           │
└────────────────────────────────────────────────┘
            ↓
┌────────────────────────────────────────────────┐
│ 4. Сохранить в .ports.json                     │
└────────────────────────────────────────────────┘

Структура портов

type PortMapping struct {
    HTTPPorts  map[int]int  // map[customPort]dockerPort
    HTTPSPorts map[int]int  // map[customPort]dockerPort
}

Docker-образ

Образ задаётся на двух уровнях с приоритетом клиента над глобальным:

Уровень Источник
Глобальный DOCKER_IMAGE в /etc/magos/magos.env
Клиентский docker_image в таблице client_info

Если образ отсутствует локально — скачивается tar-архив по URL из docker_image_download (или DOCKER_IMAGE_URL) и загружается через docker load. Одинаковые образы проверяются только один раз за запуск.


Режим отладки (debug)

Поле debug в client_info. При debug = true:

docker-compose.yml:

- PTAF_DEBUG=True
- ERROR_LOG_LEVEL=debug

nginx.conf:

env PTAF_LOG_LEVEL=DEBUG;
env DEBUG=true;
error_log stderr debug;

Angie server-блоки:

client_body_buffer_size 1m;

Важно: включение debug режима вызывает пересоздание контейнера.


Graceful Shutdown и миграция

Типы drain операций

1. Масштабирование вниз (Scale Down)

Timeout: 90 секунд

Триггер: containers_count уменьшен в БД
Действие: лишние контейнеры помечаются маркером /tmp/ptaf-drain-ptaf_{client}_{num}
Через 90с: docker-compose down + удаление файлов

2. Отключение клиента (containers_count = 0)

Timeout: 90 секунд

Триггер: containers_count = 0
Действие: все контейнеры + Angie-конфиг помечаются для drain
Через 90с: удаление контейнеров и конфига Angie

3. Миграция между хостами

Timeout: 3600 секунд (1 час)

Триггер: waf_instance изменился в БД
Действие: контейнеры и Angie-конфиг помечаются маркером :migration
Через 1ч: проверка актуальности → удаление если клиент не вернулся

Формат маркеров

/tmp/ptaf-drain-ptaf_{clientTitle}_{001}   — контейнер (содержит timestamp или timestamp:migration)
/tmp/ptaf-angie-drain-{clientTitle}        — Angie конфиг (аналогичный формат)

Кастомизация конфигов через SERVER_BLOCK

Формат для полей server_nginx_custom и server_angie_custom:

[SERVER_BLOCK:443:example.com:www.example.com]
proxy_ssl_certificate /etc/ssl/certs/example.crt;
proxy_ssl_certificate_key /etc/ssl/private/example.key;
proxy_ssl_server_name on;
[/SERVER_BLOCK]

Синтаксис: [SERVER_BLOCK:{порт}:{server_name}:{alias1}:{alias2}...]

  • Поддерживает произвольное количество алиасов
  • Несколько SERVER_BLOCK в одном поле — поддерживается
  • Для Angie: SERVER_BLOCK создаёт отдельный server-блок с проксированием на контейнеры
  • Для nginx: SERVER_BLOCK создаёт отдельный server-блок с проксированием на origin

Метод балансировки

Поле balancing_method в apps_settings. Записывается без точки с запятой:

UPDATE apps_settings SET balancing_method = 'least_conn' WHERE l7resourceid = 12345;

Допустимые значения: least_conn, ip_hash, random, random two least_conn и др.

Применяется первой строкой в upstream-блоке для Angie, nginx и SW.


proxy_next_upstream

Поле proxy_next_upstream_codes в apps_settings. Записывается через запятую без http_ префикса:

-- Убрать 500 из списка (бэкенд вернул 500, Angie не должен делать retry)
UPDATE apps_settings SET proxy_next_upstream_codes = '502,503,504' WHERE l7resourceid = 12345;

Дефолт: 500,502,503,504. Итоговая строка в конфиге:

proxy_next_upstream error timeout invalid_header http_502 http_503 http_504;

Запуск контейнеров

Ожидание освобождения портов

Перед запуском контейнера утилита проверяет занятость портов через ss -tuanl (все состояния включая TIME-WAIT и ESTAB). Порт считается занятым если:

  • Есть LISTEN сокет
  • Есть ESTAB от docker-proxy (старый контейнер ещё не остановился)

После остановки контейнера при пересоздании:

  1. Ожидаем закрытия ESTAB соединений Angie (до 30 сек)
  2. Ожидаем полного освобождения портов (до 180 сек)
  3. Запуск нового контейнера (до 3 попыток с паузой 3 сек)

Сохранение портов

.ports.json сохраняется только после успешного запуска контейнера:

  • При пересоздании контейнера (recreateContainer) — сохраняется если recreateContainer вернул true
  • При первом запуске нового контейнера — сохраняется если контейнер успешно запустился
  • При неизменённом compose — сохраняется сразу (порты не менялись)

Если контейнер не запустился — .ports.json остаётся старым, и при следующем запуске magos порты будут перевыделены заново.

Проверка после запуска

Если контейнер запущен менее 5 минут и порты не слушают — автоматическое пересоздание.


Именование контейнеров

Имя Docker-контейнера формируется динамически на основе hostname хоста:

PTAF-TB-docker-AZ3-HW-node01 → {clientTitle}_AZ3_hn01_a001
PTAF-TB-docker-AZ1-node04    → {clientTitle}_AZ1_n04_a001

Правила парсинга hostname:

  • AZ{N} — номер зоны доступности
  • Наличие HW в hostname → префикс hn (hardware node), отсутствие → n
  • node{NN} — номер ноды
  • a{001} — порядковый номер агента

Fallback: если hostname не соответствует формату — используется старый формат ptaf_{clientTitle}_{001}.

Важно: директории на диске (/home/install/ptaf/{clientTitle}/{clientTitle}-ptaf-agent{001}/) не меняются — только container_name и hostname в Docker.


Производительность

DefaultWorkerProcesses   = 4       // Nginx worker processes
DefaultWorkerConnections = 64000   // Соединений на worker
worker_rlimit_nofile     = 1048576 // Лимит файловых дескрипторов

Timeouts

ContainerDrainTimeout  = 90        // Секунд для scale down / отключения
MigrationDrainTimeout  = 3600      // Секунд для миграции (1 час)
APITimeout             = 120       // Секунд для API запросов
CacheTTL               = 600       // Секунд (10 минут) для sp_info
MaxLockAge             = 15 мин    // После этого lock считается устаревшим

Буферы (Angie и nginx)

proxy_buffers 8 64k;
proxy_buffer_size 64k;
proxy_busy_buffers_size 128k;
large_client_header_buffers 4 128k;

shm_size

По умолчанию 1gb. Управляется через поле shm_size в client_info.


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

Фильтрация IP адресов

WAF Networks - исключаются из origins (предотвращение routing loops). Всегда добавляются в set_real_ip_from в Angie.

Antibot Networks - используются для set_real_ip_from в Angie по умолчанию (sp_antiddos = true).

Custom Real IP Networks - если у клиента sp_antiddos = false и заполнено поле real_ip_networks в client_info — используются вместо Antibot Networks в set_real_ip_from. Применяется для клиентов без антиддоса перед Angie.

Внутри контейнера nginx всегда использует set_real_ip_from 172.16.0.0/12 (Docker bridge сеть Angie).

SSL/TLS

Дефолтные настройки (Angie):

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

Кастомизация через custom_angie_ssl — полная замена дефолтов для Angie Кастомизация через custom_sw_nginx_ssl — полная замена дефолтов для SW

mTLS

Автоматическая замена путей:

/etc/ssl/certs/     → /opt/ptaf/conf/main/
/etc/ssl/private/   → /opt/ptaf/conf/main/

Применяется во всех custom директивах, содержащих proxy_ssl_certificate

Lock-файл

При старте утилита:

  1. Читает PID из существующего lock-файла
  2. Если процесс не существует → удаляет устаревший lock и продолжает
  3. Если процесс работает > 15 минут → предупреждение и подсказка как убить
  4. При завершении → lock удаляется через defer

Логирование

Структура логов nginx

/var/log/ptaf_nginx/{client-title}/ptaf-agent001/
├── {sid}_{domain}_access.log
└── {sid}_{domain}_error.log

Формат access лога (JSON)

{
  "SID": 12345,
  "server_name": "example.com",
  "request_id": "abc123",
  "app_name": "example.com",
  "proxyed_to": "192.168.1.1:443",
  "upstream_status": "200",
  "response_status_code": 200,
  "request_time": 0.056
}

Поле request_id берётся из заголовка X-Request-ID.

Уровни логирования программы

✓  — Успешная операция
○  — Информация (без изменений)
⚠  — Предупреждение
→  — Действие/переход
└─ — Детали операции
❌ — Критическая ошибка
  — Информационное сообщение

Мониторинг и логирование

FluentBit интеграция

Активируется через поле fluent_bit_port в client_info:

environment:
  - EVENT_SENDER_ENABLED=1
  - EVENT_SENDER_HOST=172.17.0.1
  - EVENT_SENDER_PORT={fluent_bit_port}
  - EVENT_SENDER_TIMEOUT=3

Troubleshooting

Проблема: Контейнер не запускается

Проверить:

  1. docker logs ptaf_{client}_{num}
  2. Наличие конфигов в /home/install/conf/ptaf-nginx/
  3. Занятость портов: ss -tuanl | grep {port}
  4. Права на /var/log/ptaf_nginx/ (должны быть 0777)
  5. Зомби docker-proxy процессы: ps aux | grep docker-proxy | grep -v grep

Проблема: Порты конфликтуют

Действия:

  1. Проверить ss -tuanl | grep :{port} — все состояния сокетов
  2. Проверить cat /proc/net/tcp | grep -i "$(printf '%04X' {port})" — таблица ядра
  3. Убедиться что ip_local_reserved_ports=18000-23999 установлен на хосте
  4. Удалить .ports.json — порты будут перевыделены при следующем запуске

Проблема: Lock-файл не удалился

Действия:

  1. Magos автоматически удалит lock если процесс уже не существует
  2. Если процесс завис: kill {PID} && rm /var/lock/ptaf-main.lock
  3. PID записан в lock-файле: cat /var/lock/ptaf-main.lock

Проблема: Клиент не мигрирует

Проверить:

  1. waf_instance в client_info
  2. Записи в instances_new
  3. Наличие маркеров /tmp/ptaf-drain-*
  4. Логи программы на обоих хостах

Проблема: SW nginx не перезагружается

Проверить:

  1. solidwall-nginx -t
  2. Наличие симлинков в sites-enabled/
  3. Синтаксис в sites-available/*.conf

Расширение системы

Добавление нового клиента (PTAF)

INSERT INTO client_info (client_title, containers_count, waf_instance)
VALUES ('new-client', 2, 'instance-a');

INSERT INTO apps_settings (l7resourceid, client_title, waf_vendor, waf_enabled, mode)
VALUES (12345, 'new-client', 'ptaf', true, 'auto');

Добавление нового клиента (SW)

INSERT INTO client_info (client_title, containers_count, waf_instance)
VALUES ('new-sw-client', 0, 'sw-instance-a');  -- containers_count игнорируется для SW

INSERT INTO apps_settings (l7resourceid, client_title, waf_vendor, waf_enabled, mode)
VALUES (12346, 'new-sw-client', 'sw', true, 'auto');

Включение debug-режима для клиента

UPDATE client_info SET debug = true WHERE client_title = 'CLIENT001';

Задать индивидуальный Docker-образ для клиента

UPDATE client_info
SET
    docker_image = 'ptaf-core-nginx-agent:release-4.3.1.431839-debian-bullseye-nginx-1.28.0',
    docker_image_download = 'https://example.com/image.tar'
WHERE client_title = 'CLIENT001';

Увеличить shared memory для контейнера

UPDATE client_info SET shm_size = 8 WHERE client_title = 'CLIENT001';

Настроить балансировку

UPDATE apps_settings SET balancing_method = 'least_conn' WHERE l7resourceid = 12345;

Настроить ptaf_fallback

-- Пропускать трафик если WAF недоступен
UPDATE client_info SET ptaf_fallback_code = 'pass' WHERE client_title = 'CLIENT001';

-- Вернуть 503 вместо 418
UPDATE client_info SET ptaf_fallback_code = '503' WHERE client_title = 'CLIENT001';

Отключить антиддос для клиента (кастомные real_ip сети)

UPDATE client_info
SET sp_antiddos = false,
    real_ip_networks = '{10.0.0.0/8, 192.168.1.0/24}'
WHERE client_title = 'CLIENT001';