diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7ffefe4..73eacba 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -114,6 +114,8 @@ waf-host-02 | instance-c - 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` определяет на каком хосте должен работать клиент @@ -516,13 +518,43 @@ proxy_next_upstream error timeout invalid_header http_502 http_503 http_504; 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. + +--- + + ### Производительность @@ -561,9 +593,13 @@ large_client_header_buffers 4 128k; ### Фильтрация IP адресов -**WAF Networks** - исключаются из origins (предотвращение routing loops) +**WAF Networks** - исключаются из origins (предотвращение routing loops). Всегда добавляются в `set_real_ip_from` в Angie. -**Antibot Networks** - используются для идентификации antibot трафика +**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 @@ -753,8 +789,17 @@ UPDATE apps_settings SET balancing_method = 'least_conn' WHERE l7resourceid = 12 ```sql -- Пропускать трафик если WAF недоступен -UPDATE client_info SET ptaf_fallback_code = 'pass' WHERE client_uuid = 12345; +UPDATE client_info SET ptaf_fallback_code = 'pass' WHERE client_title = 'CLIENT001'; -- Вернуть 503 вместо 418 -UPDATE client_info SET ptaf_fallback_code = '503' WHERE client_uuid = 12345; +UPDATE client_info SET ptaf_fallback_code = '503' WHERE client_title = 'CLIENT001'; +``` + +### Отключить антиддос для клиента (кастомные real_ip сети) + +```sql +UPDATE client_info +SET sp_antiddos = false, + real_ip_networks = '{10.0.0.0/8, 192.168.1.0/24}' +WHERE client_title = 'CLIENT001'; ``` diff --git a/DATABASE_SCHEMA.md b/DATABASE_SCHEMA.md index e11f76d..3d19815 100644 --- a/DATABASE_SCHEMA.md +++ b/DATABASE_SCHEMA.md @@ -98,7 +98,9 @@ WHERE hostname = $1 AND instance = $2; | `docker_image_download` | TEXT | URL для скачивания tar-архива образа | `https://...` | | `debug` | BOOLEAN | Режим отладки | `false` | | `shm_size` | INTEGER | Размер /dev/shm в GB | `1` | -| `ptaf_fallback_code` | TEXT | Код ответа ptaf_fallback | `'418'` | +| `ptaf_fallback_code` | TEXT | Код ответа ptaf_fallback: число или 'pass' | `418` | +| `sp_antiddos` | BOOLEAN | Использовать антиддос сети в set_real_ip | `true` | +| `real_ip_networks` | INET[] | Кастомные сети для set_real_ip (если sp_antiddos=false) | `{10.0.0.0/8}` | ### Используемые запросы @@ -114,7 +116,10 @@ SELECT docker_image, docker_image_download, debug, - shm_size + shm_size, + ptaf_fallback_code, + sp_antiddos, + real_ip_networks FROM client_info WHERE waf_instance = $1; ``` @@ -133,7 +138,10 @@ SELECT docker_image, docker_image_download, debug, - shm_size + shm_size, + ptaf_fallback_code, + sp_antiddos, + real_ip_networks FROM client_info WHERE client_title = $1; ``` @@ -154,6 +162,15 @@ ALTER TABLE client_info ALTER TABLE client_info ADD COLUMN IF NOT EXISTS shm_size INTEGER DEFAULT 1; + +ALTER TABLE client_info + ADD COLUMN IF NOT EXISTS ptaf_fallback_code TEXT DEFAULT '418'; + +ALTER TABLE client_info + ADD COLUMN IF NOT EXISTS sp_antiddos BOOLEAN DEFAULT TRUE; + +ALTER TABLE client_info + ADD COLUMN IF NOT EXISTS real_ip_networks INET[] DEFAULT NULL; ``` ### Ключевые поля @@ -180,6 +197,28 @@ ALTER TABLE client_info Размер разделяемой памяти `/dev/shm` для Docker-контейнера в гигабайтах. Результат в docker-compose: `shm_size: '2gb'`. Дефолт: 1. +#### ptaf_fallback_code + +Код HTTP ответа который PTAF возвращает когда WAF-модуль недоступен. Записывается в блок `http {}` файла `nginx.conf` — применяется глобально для всех ресурсов контейнера. + +```sql +UPDATE client_info SET ptaf_fallback_code = 'pass' WHERE client_title = 'CLIENT001'; +UPDATE client_info SET ptaf_fallback_code = '503' WHERE client_title = 'CLIENT001'; +``` + +#### sp_antiddos / real_ip_networks + +`sp_antiddos = true` (дефолт) — в Angie используются антиддос сети из таблицы `ips` для `set_real_ip_from`. + +`sp_antiddos = false` + непустой `real_ip_networks` — вместо антиддос сетей используются кастомные сети из `real_ip_networks`. WAF сети из `ips` при этом всегда присутствуют. + +```sql +UPDATE client_info +SET sp_antiddos = false, + real_ip_networks = '{10.0.0.0/8, 192.168.1.0/24}' +WHERE client_title = 'CLIENT001'; +``` + --- ## Таблица: apps_settings @@ -248,8 +287,7 @@ SELECT max_fails, fail_timeout, balancing_method, - proxy_next_upstream_codes, - ptaf_fallback_code + proxy_next_upstream_codes FROM apps_settings WHERE client_title = $1 ORDER BY l7resourceid ASC; @@ -268,9 +306,6 @@ ALTER TABLE apps_settings ALTER TABLE apps_settings ADD COLUMN IF NOT EXISTS proxy_next_upstream_codes TEXT DEFAULT '500,502,503,504'; - -ALTER TABLE client_info - ADD COLUMN IF NOT EXISTS ptaf_fallback_code TEXT DEFAULT '418'; ``` ### Кастомные поля (подробно) @@ -310,21 +345,6 @@ WHERE l7resourceid = 12345; proxy_next_upstream error timeout invalid_header http_502 http_503 http_504; ``` -#### ptaf_fallback_code - -Код HTTP ответа который PTAF возвращает когда WAF-модуль недоступен. - -```sql --- Пропускать трафик если WAF недоступен -UPDATE client_info SET ptaf_fallback_code = 'pass' WHERE client_uuid = 12345; - --- Вернуть 503 вместо 418 -UPDATE client_info SET ptaf_fallback_code = '503' WHERE client_uuid = 12345; - --- Вернуть дефолт (418) -UPDATE client_info SET ptaf_fallback_code = NULL WHERE client_uuid = 12345; -``` - #### upstream_angie_custom / upstream_nginx_custom **Режим:** REPLACE (полная замена keepalive директив) @@ -563,6 +583,12 @@ SELECT client_title FROM client_info WHERE debug = true; -- Клиенты с индивидуальным образом SELECT client_title, docker_image FROM client_info WHERE docker_image IS NOT NULL; +-- Клиенты без антиддоса +SELECT client_title, real_ip_networks FROM client_info WHERE sp_antiddos = false; + +-- Клиенты с кастомным ptaf_fallback +SELECT client_title, ptaf_fallback_code FROM client_info WHERE ptaf_fallback_code != '418'; + -- Ресурсы с кастомной балансировкой SELECT client_title, l7resourceid, balancing_method FROM apps_settings diff --git a/containers_part1.go b/containers_part1.go index d2142d3..82870fe 100644 --- a/containers_part1.go +++ b/containers_part1.go @@ -10,7 +10,7 @@ import ( "time" ) -func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, resources []ResourceData, resourcePortMap map[int][]PortMapping) error { +func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, resources []ResourceData, resourcePortMap map[int][]PortMapping, hostname string) error { log.Printf("Настройка контейнеров для %s", clientTitle) baseDir := filepath.Join("/home/install/ptaf", clientTitle) containerChanges := make(map[int]bool) @@ -19,11 +19,13 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, for containerNum := 1; containerNum <= clientInfo.ContainersCount; containerNum++ { containerName := fmt.Sprintf("%s-ptaf-agent%03d", clientTitle, containerNum) containerDir := filepath.Join(baseDir, containerName) + containerFullName := buildContainerFullName(clientTitle, containerNum, hostname) log.Printf(" Обработка контейнера: %s", containerName) + confDDir := filepath.Join("/home/install/conf/ptaf-nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum), "conf.d") dirs := []string{ containerDir, - filepath.Join("/home/install/conf/ptaf-nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum), "conf.d"), + confDDir, } for _, dir := range dirs { existed := false @@ -37,6 +39,10 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, log.Printf(" ✓ Создана директория: %s", dir) } } + // conf.d должна быть доступна пользователю ptaf внутри контейнера + if err := os.Chmod(confDDir, 0777); err != nil { + return fmt.Errorf("ошибка установки прав для conf.d %s: %w", confDDir, err) + } logDir := filepath.Join("/var/log/ptaf_nginx", clientTitle, fmt.Sprintf("ptaf-agent%03d", containerNum)) logDirExisted := false @@ -151,7 +157,7 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, for idx, pm := range containerPorts { log.Printf(" PortMapping[%d]: HTTP=%v, HTTPS=%v", idx, pm.HTTPPorts, pm.HTTPSPorts) } - changed, err = generateDockerCompose(composeFile, config, clientTitle, clientInfo, containerNum, resources, containerPorts) + changed, err = generateDockerCompose(composeFile, config, clientTitle, clientInfo, containerNum, containerFullName, resources, containerPorts) if err != nil { return fmt.Errorf("ошибка генерации docker-compose.yml: %w", err) } @@ -159,8 +165,12 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, log.Printf(" ✓ docker-compose.yml изменен: %s", composeFile) composeChanges[containerNum] = true } - if err := saveContainerPorts(containerDir, containerNum, resources, containerPorts); err != nil { - log.Printf(" ⚠ Ошибка сохранения портов: %v", err) + // Сохраняем порты только если compose не изменился (нет пересоздания) + // Если compose изменился — сохраним после успешного запуска контейнера + if !composeChanges[containerNum] { + if err := saveContainerPorts(containerDir, containerNum, resources, containerPorts); err != nil { + log.Printf(" ⚠ Ошибка сохранения портов: %v", err) + } } } @@ -169,12 +179,27 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, containerName := fmt.Sprintf("%s-ptaf-agent%03d", clientTitle, containerNum) containerDir := filepath.Join(baseDir, containerName) composeFile := filepath.Join(containerDir, "docker-compose.yml") - containerFullName := fmt.Sprintf("ptaf_%s_%03d", clientTitle, containerNum) + containerFullName := buildContainerFullName(clientTitle, containerNum, hostname) containerExists, containerRunning := checkContainerStatus(containerFullName) + // Собираем порты контейнера для сохранения после успешного запуска + containerPorts := []PortMapping{} + for _, res := range resources { + ports, ok := resourcePortMap[res.L7ResourceID] + if !ok || containerNum-1 >= len(ports) { + continue + } + containerPorts = append(containerPorts, ports[containerNum-1]) + } + if containerExists { if composeChanges[containerNum] { - recreateContainer(containerFullName, composeFile, containerDir, clientTitle, resources, resourcePortMap, containerNum) + success := recreateContainer(containerFullName, composeFile, containerDir, clientTitle, resources, resourcePortMap, containerNum) + if success { + if err := saveContainerPorts(containerDir, containerNum, resources, containerPorts); err != nil { + log.Printf(" ⚠ Ошибка сохранения портов: %v", err) + } + } continue } if !containerRunning { @@ -312,6 +337,9 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, log.Printf(" ⚠ Контейнер %s не запустился после %d попыток. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, maxRetries, containerDir) continue } + if err := saveContainerPorts(containerDir, containerNum, resources, containerPorts); err != nil { + log.Printf(" ⚠ Ошибка сохранения портов: %v", err) + } } } diff --git a/containers_part2.go b/containers_part2.go index 007e61c..cd98b7b 100644 --- a/containers_part2.go +++ b/containers_part2.go @@ -71,7 +71,7 @@ func isPortBusyByAnyProcess(port int) bool { return false } -func recreateContainer(containerFullName, composeFile, containerDir, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping, containerNum int) { +func recreateContainer(containerFullName, composeFile, containerDir, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping, containerNum int) bool { log.Printf(" docker-compose.yml изменился, пересоздаём контейнер %s...", containerFullName) portsToFree := make([]int, 0) for _, res := range resources { @@ -110,7 +110,7 @@ func recreateContainer(containerFullName, composeFile, containerDir, clientTitle log.Printf(" → Контейнер не остановился, принудительное удаление...") if err := forceRemoveContainer(containerFullName); err != nil { log.Printf(" ⚠ Не удалось принудительно удалить контейнер: %v", err) - return + return false } } } @@ -165,7 +165,7 @@ func recreateContainer(containerFullName, composeFile, containerDir, clientTitle if !allPortsFree { log.Printf(" ⚠ Порты не освободились за 180 секунд!") log.Printf(" → Проверьте вручную: sudo lsof -i :%d", portsToFree[0]) - return + return false } } time.Sleep(2 * time.Second) @@ -179,18 +179,19 @@ func recreateContainer(containerFullName, composeFile, containerDir, clientTitle log.Printf(" ⚠ Порт всё ещё занят после ожидания освобождения!") log.Printf(" → Проверьте: docker ps -a | grep %s", clientTitle) log.Printf(" → Найдите процесс: sudo lsof -i | grep LISTEN | grep 127.0.0.1") - return + return false } time.Sleep(5 * time.Second) _, isRunning := checkContainerStatus(containerFullName) if !isRunning { log.Printf(" ⚠ Контейнер %s не запустился. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, containerDir) - return + return false } log.Printf(" ✓ Контейнер %s пересоздан с новыми портами", containerFullName) + return true } -func startContainer(containerFullName, composeFile, containerDir string) { +func startContainer(containerFullName, composeFile, containerDir string) bool { log.Printf(" Контейнер %s остановлен, запускаем...", containerFullName) cmd := exec.Command("docker-compose", "-f", composeFile, "up", "-d") cmd.Dir = containerDir @@ -200,9 +201,10 @@ func startContainer(containerFullName, composeFile, containerDir string) { _, isRunning := checkContainerStatus(containerFullName) if !isRunning { log.Printf(" ⚠ Контейнер %s не запустился. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, containerDir) - return + return false } log.Printf(" ✓ Контейнер %s запущен", containerFullName) + return true } // getContainerStartedAt возвращает время запуска контейнера @@ -646,10 +648,11 @@ func generateMimeTypes(filePath string) (bool, error) { } // generateDockerCompose генерирует docker-compose.yml файл -func generateDockerCompose(filePath string, config Config, clientTitle string, clientInfo *ClientInfo, containerNum int, resources []ResourceData, portMappings []PortMapping) (bool, error) { +func generateDockerCompose(filePath string, config Config, clientTitle string, clientInfo *ClientInfo, containerNum int, containerFullName string, resources []ResourceData, portMappings []PortMapping) (bool, error) { type ComposeData struct { ClientTitle string ContainerNum int + ContainerFullName string DockerImage string PTAFConfig string WorkerProcesses int @@ -714,6 +717,7 @@ func generateDockerCompose(filePath string, config Config, clientTitle string, c data := ComposeData{ ClientTitle: clientTitle, ContainerNum: containerNum, + ContainerFullName: containerFullName, DockerImage: dockerImage, PTAFConfig: "", WorkerProcesses: config.WorkerProcesses, @@ -739,8 +743,8 @@ func generateDockerCompose(filePath string, config Config, clientTitle string, c image: '{{.DockerImage}}' restart: unless-stopped shm_size: '{{.ShmSize}}gb' - container_name: ptaf_{{.ClientTitle}}_{{printf "%03d" .ContainerNum}} - hostname: ptaf_{{.ClientTitle}}_{{printf "%03d" .ContainerNum}} + container_name: {{.ContainerFullName}} + hostname: {{.ContainerFullName}} environment: - CONNECTION_STRING={{.PTAFConfig}} - WORKER_PROCESSES={{.WorkerProcesses}} diff --git a/db.go b/db.go index 8eff52b..6cfc33f 100644 --- a/db.go +++ b/db.go @@ -6,7 +6,7 @@ import ( "log" "strings" - _ "github.com/lib/pq" + "github.com/lib/pq" ) // connectToDB подключается к базе данных PostgreSQL @@ -98,7 +98,8 @@ func checkHostBelongsToInstance(db *sql.DB, hostname, instance string) (bool, er func getClientInfoByInstance(db *sql.DB, instance string) ([]ClientInfo, error) { query := ` SELECT containers_count, ptaf_config, client_title, fluent_bit_port, waf_instance, - docker_image, docker_image_download, debug, shm_size, ptaf_fallback_code + docker_image, docker_image_download, debug, shm_size, ptaf_fallback_code, + sp_antiddos, real_ip_networks FROM client_info WHERE waf_instance = $1 ` @@ -127,6 +128,8 @@ func getClientInfoByInstance(db *sql.DB, instance string) ([]ClientInfo, error) &ci.Debug, &ci.ShmSize, &ci.PtafFallbackCode, + &ci.SpAntiddos, + pq.Array(&ci.RealIPNetworks), ) if err != nil { return nil, fmt.Errorf("ошибка сканирования строки: %w", err) @@ -212,7 +215,8 @@ func getAppsSettingsByClientTitle(db *sql.DB, clientTitle string) ([]AppsSetting func getClientInfoByClientTitle(db *sql.DB, clientTitle string) (*ClientInfo, error) { query := ` SELECT containers_count, ptaf_config, client_title, fluent_bit_port, waf_instance, - docker_image, docker_image_download, debug, shm_size, ptaf_fallback_code + docker_image, docker_image_download, debug, shm_size, ptaf_fallback_code, + sp_antiddos, real_ip_networks FROM client_info WHERE client_title = $1 ` @@ -230,6 +234,8 @@ func getClientInfoByClientTitle(db *sql.DB, clientTitle string) (*ClientInfo, er &clientInfo.Debug, &clientInfo.ShmSize, &clientInfo.PtafFallbackCode, + &clientInfo.SpAntiddos, + pq.Array(&clientInfo.RealIPNetworks), ) if err != nil { if strings.Contains(err.Error(), "does not exist") { diff --git a/drain.go b/drain.go index 8cd17d9..d4699a4 100644 --- a/drain.go +++ b/drain.go @@ -30,12 +30,22 @@ func getActualContainerCount(clientTitle string) int { } // Считаем по drain-маркерам в /tmp (контейнеры могли быть уже удалены с диска) - for num := max + 1; ; num++ { - markerFile := fmt.Sprintf("/tmp/ptaf-drain-ptaf_%s_%03d", clientTitle, num) - if _, err := os.Stat(markerFile); err != nil { - break + // Ищем маркеры по паттерну /tmp/ptaf-drain-{clientTitle}_* независимо от формата имени + matches, _ := filepath.Glob(fmt.Sprintf("/tmp/ptaf-drain-%s_*", clientTitle)) + for _, match := range matches { + // Извлекаем номер агента из конца имени файла (формат: ..._a001 или ..._001) + base := filepath.Base(match) + base = strings.TrimPrefix(base, "ptaf-drain-") + // Убираем префикс clientTitle_ + rest := strings.TrimPrefix(base, clientTitle+"_") + // Ищем номер в конце: _a001 или _001 + parts := strings.Split(rest, "_") + lastPart := parts[len(parts)-1] + lastPart = strings.TrimPrefix(lastPart, "a") + var num int + if _, err := fmt.Sscanf(lastPart, "%d", &num); err == nil && num > max { + max = num } - max = num } return max @@ -161,7 +171,8 @@ func processAllDrainingContainersGlobally(db *sql.DB, hostname string) { hasExistingMarkers := false actualCount := getActualContainerCount(clientTitle) for containerNum := 1; containerNum <= actualCount; containerNum++ { - if _, err := os.Stat(fmt.Sprintf("/tmp/ptaf-drain-ptaf_%s_%03d", clientTitle, containerNum)); err == nil { + containerFullName := buildContainerFullName(clientTitle, containerNum, hostname) + if _, err := os.Stat(fmt.Sprintf("/tmp/ptaf-drain-%s", containerFullName)); err == nil { hasExistingMarkers = true break } @@ -169,7 +180,7 @@ func processAllDrainingContainersGlobally(db *sql.DB, hostname string) { if !hasExistingMarkers { log.Printf(" → Помечаю контейнеры для миграции") for containerNum := 1; containerNum <= actualCount; containerNum++ { - containerFullName := fmt.Sprintf("ptaf_%s_%03d", clientTitle, containerNum) + containerFullName := buildContainerFullName(clientTitle, containerNum, hostname) exists, _ := checkContainerStatus(containerFullName) if exists { if err := markContainerForDrain(containerFullName, true); err != nil { @@ -210,12 +221,10 @@ func processAllDrainingContainersGlobally(db *sql.DB, hostname string) { func checkAndProcessExistingDrainMarkers(clientTitle string, containersCount int, processedCount *int) { hasAnyDrainMarkers := false - actualCount := getActualContainerCount(clientTitle) - for containerNum := 1; containerNum <= actualCount; containerNum++ { - if _, err := os.Stat(fmt.Sprintf("/tmp/ptaf-drain-ptaf_%s_%03d", clientTitle, containerNum)); err == nil { - hasAnyDrainMarkers = true - break - } + // Ищем маркеры по паттерну независимо от формата имени контейнера + matches, _ := filepath.Glob(fmt.Sprintf("/tmp/ptaf-drain-%s_*", clientTitle)) + if len(matches) > 0 { + hasAnyDrainMarkers = true } if !hasAnyDrainMarkers { if _, err := os.Stat(fmt.Sprintf("/tmp/ptaf-angie-drain-%s", clientTitle)); err == nil { @@ -244,7 +253,7 @@ func processDrainingContainers(clientTitle string, maxContainerNum int) { actualCount := getActualContainerCount(clientTitle) for containerNum := maxContainerNum + 1; containerNum <= actualCount; containerNum++ { containerName := fmt.Sprintf("%s-ptaf-agent%03d", clientTitle, containerNum) - containerFullName := fmt.Sprintf("ptaf_%s_%03d", clientTitle, containerNum) + containerFullName := buildContainerFullName(clientTitle, containerNum, hostname) containerDir := filepath.Join(baseDir, containerName) composeFile := filepath.Join(containerDir, "docker-compose.yml") exists, _ := checkContainerStatus(containerFullName) diff --git a/main.go b/main.go index 7142a58..ae8fdc3 100644 --- a/main.go +++ b/main.go @@ -345,7 +345,7 @@ func handleClientMigration(clientInfo ClientInfo, hostname string) { actualCount := getActualContainerCount(clientInfo.ClientTitle) for containerNum := 1; containerNum <= actualCount; containerNum++ { - containerFullName := fmt.Sprintf("ptaf_%s_%03d", clientInfo.ClientTitle, containerNum) + containerFullName := buildContainerFullName(clientInfo.ClientTitle, containerNum, hostname) exists, _ := checkContainerStatus(containerFullName) if exists { log.Printf(" → Помечаю контейнер %s для drain (миграция, таймаут %d сек)", @@ -373,14 +373,14 @@ func handleClientMigration(clientInfo ClientInfo, hostname string) { } // cleanStaleDrainMarkers очищает устаревшие drain-маркеры если клиент вернулся на хост -func cleanStaleDrainMarkers(clientInfo ClientInfo) { +func cleanStaleDrainMarkers(clientInfo ClientInfo, hostname string) { log.Printf(" → Проверка и очистка устаревших маркеров drain для %s", clientInfo.ClientTitle) cleanedContainers := 0 cleanedAngieMarker := false actualCount := getActualContainerCount(clientInfo.ClientTitle) for containerNum := 1; containerNum <= actualCount; containerNum++ { - containerFullName := fmt.Sprintf("ptaf_%s_%03d", clientInfo.ClientTitle, containerNum) + containerFullName := buildContainerFullName(clientInfo.ClientTitle, containerNum, hostname) markerFile := fmt.Sprintf("/tmp/ptaf-drain-%s", containerFullName) if _, err := os.Stat(markerFile); err == nil { diff --git a/nginx.go b/nginx.go index 4c6ea2f..76a9309 100644 --- a/nginx.go +++ b/nginx.go @@ -10,7 +10,7 @@ import ( ) // generateAngieConfigsWithoutReload генерирует конфиги Angie БЕЗ автоматического reload -func generateAngieConfigsWithoutReload(config Config, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping, debug bool) (bool, error) { +func generateAngieConfigsWithoutReload(config Config, clientTitle string, clientInfo ClientInfo, resources []ResourceData, resourcePortMap map[int][]PortMapping, debug bool) (bool, error) { log.Printf("Генерация конфигов Angie для %s", clientTitle) configDir := "/etc/angie/http.d" @@ -47,7 +47,7 @@ func generateAngieConfigsWithoutReload(config Config, clientTitle string, resour return false, fmt.Errorf("не найдены порты для l7ResourceID %d", res.L7ResourceID) } - err := writeAngieResourceConfig(f, config, res, containerPorts, debug) + err := writeAngieResourceConfig(f, config, clientInfo, res, containerPorts, debug) if err != nil { os.Remove(tmpFile) return false, fmt.Errorf("ошибка генерации конфига для l7ResourceID %d: %w", res.L7ResourceID, err) @@ -93,8 +93,8 @@ func generateAngieConfigsWithoutReload(config Config, clientTitle string, resour } // generateAngieConfigs генерирует конфигурационные файлы Angie с автоматическим reload -func generateAngieConfigs(config Config, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping, debug bool) error { - changed, err := generateAngieConfigsWithoutReload(config, clientTitle, resources, resourcePortMap, debug) +func generateAngieConfigs(config Config, clientTitle string, clientInfo ClientInfo, resources []ResourceData, resourcePortMap map[int][]PortMapping, debug bool) error { + changed, err := generateAngieConfigsWithoutReload(config, clientTitle, clientInfo, resources, resourcePortMap, debug) if err != nil { return err } @@ -107,7 +107,7 @@ func generateAngieConfigs(config Config, clientTitle string, resources []Resourc } // writeAngieResourceConfig записывает конфигурацию для одного ресурса в Angie -func writeAngieResourceConfig(f *os.File, config Config, res ResourceData, containerPorts []PortMapping, debug bool) error { +func writeAngieResourceConfig(f *os.File, config Config, clientInfo ClientInfo, res ResourceData, containerPorts []PortMapping, debug bool) error { allDomains := []string{res.ServerName} allDomains = append(allDomains, res.Aliases...) processedDomains := processDomainsWithWWW(allDomains) @@ -271,7 +271,7 @@ func writeAngieResourceConfig(f *os.File, config Config, res ResourceData, conta f.WriteString(fmt.Sprintf(" access_log /var/log/angie/%d_%s_access.log waf;\n", res.L7ResourceID, res.ServerName)) f.WriteString(fmt.Sprintf(" error_log /var/log/angie/%d_%s_error.log error;\n\n", res.L7ResourceID, res.ServerName)) - writeNetworkSettings(f, config) + writeNetworkSettings(f, config, clientInfo) writeStandardServerSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesAngieCustom), debug, getProxyNextUpstream(res.AppsSettings)) if hasCustomLocation { @@ -324,7 +324,7 @@ func writeAngieResourceConfig(f *os.File, config Config, res ResourceData, conta writeAngieSSLSettings(f, res.AppsSettings.CustomAngieSSL) } - writeNetworkSettings(f, config) + writeNetworkSettings(f, config, clientInfo) writeStandardServerSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesAngieCustom), debug, getProxyNextUpstream(res.AppsSettings)) if hasCustomLocation { @@ -357,7 +357,7 @@ func writeAngieResourceConfig(f *os.File, config Config, res ResourceData, conta } log.Printf(" └─ Angie Server блок: порт %d, server_name %s%s", block.Port, block.ServerName, aliasInfo) - generateAngieCustomServerBlock(f, config, res, block, containerPorts, customLocationStr, debug) + generateAngieCustomServerBlock(f, config, clientInfo, res, block, containerPorts, customLocationStr, debug) } } @@ -365,14 +365,22 @@ func writeAngieResourceConfig(f *os.File, config Config, res ResourceData, conta } // writeNetworkSettings записывает WAF и Antibot networks для Angie -func writeNetworkSettings(f *os.File, config Config) { +func writeNetworkSettings(f *os.File, config Config, clientInfo ClientInfo) { f.WriteString(" # waf networks\n") for _, net := range config.WAFNetworks { f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net)) } - f.WriteString(" # antibot networks\n") - for _, net := range config.AntibotNetworks { - f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net)) + if !clientInfo.SpAntiddos && len(clientInfo.RealIPNetworks) > 0 { + // Кастомные сети вместо антиддос сетей + f.WriteString(" # custom real_ip networks\n") + for _, net := range clientInfo.RealIPNetworks { + f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net)) + } + } else { + f.WriteString(" # antibot networks\n") + for _, net := range config.AntibotNetworks { + f.WriteString(fmt.Sprintf(" set_real_ip_from %s;\n", net)) + } } f.WriteString("\n real_ip_header X-Forwarded-For;\n") f.WriteString(" real_ip_recursive on;\n\n") @@ -500,7 +508,7 @@ func writeNginxCustomOrDefaultDirectives(f *os.File, customDirectives string) { } // generateAngieCustomServerBlock генерирует кастомный server блок для Angie -func generateAngieCustomServerBlock(f *os.File, config Config, res ResourceData, block CustomServerBlock, containerPorts []PortMapping, customLocationStr string, debug bool) { +func generateAngieCustomServerBlock(f *os.File, config Config, clientInfo ClientInfo, res ResourceData, block CustomServerBlock, containerPorts []PortMapping, customLocationStr string, debug bool) { var upstreamName string if block.IsHTTPS { upstreamName = fmt.Sprintf("secure%d_%d_%s", res.L7ResourceID, block.Port, res.ServerName) @@ -538,7 +546,7 @@ func generateAngieCustomServerBlock(f *os.File, config Config, res ResourceData, f.WriteString(fmt.Sprintf(" error_log /var/log/angie/%d_%s_port%d_error.log error;\n\n", res.L7ResourceID, sanitizeServerName(block.ServerName), block.Port)) - writeNetworkSettings(f, config) + writeNetworkSettings(f, config, clientInfo) writeStandardServerSettings(f, getCustomServerDirectives(res.AppsSettings.ServerDirectivesAngieCustom), debug, getProxyNextUpstream(res.AppsSettings)) // Кастомные location блоки из location_angie_custom (перед location /) diff --git a/ptaf_processor.go b/ptaf_processor.go index e287729..1759418 100644 --- a/ptaf_processor.go +++ b/ptaf_processor.go @@ -72,7 +72,7 @@ func processPTAFClient( } // Автоочистка устаревших маркеров drain - cleanStaleDrainMarkers(clientInfo) + cleanStaleDrainMarkers(clientInfo, hostname) // Получение данных ресурсов resourcesData := fetchPTAFResourcesData(db, config, appsSettingsList) @@ -95,14 +95,14 @@ func processPTAFClient( // ════════════════════════════════════════════════════════════ // 1. Генерация конфигурации Angie БЕЗ автоматического reload - angieChanged, err := generateAngieConfigsWithoutReload(config, clientInfo.ClientTitle, resourcesData, resourcePortMap, clientInfo.Debug) + angieChanged, err := generateAngieConfigsWithoutReload(config, clientInfo.ClientTitle, clientInfo, resourcesData, resourcePortMap, clientInfo.Debug) if err != nil { log.Printf("Ошибка генерации конфигов Angie для %s: %v", clientInfo.ClientTitle, err) return false } // 2. Создание директорий и конфигов для контейнеров (пересоздание если нужно) - err = setupContainers(config, clientInfo.ClientTitle, &clientInfo, resourcesData, resourcePortMap) + err = setupContainers(config, clientInfo.ClientTitle, &clientInfo, resourcesData, resourcePortMap, hostname) if err != nil { log.Printf("Ошибка настройки контейнеров для %s: %v", clientInfo.ClientTitle, err) return false diff --git a/types.go b/types.go index 8e3a9a4..e537489 100644 --- a/types.go +++ b/types.go @@ -53,6 +53,8 @@ type ClientInfo struct { Debug bool // Режим отладки (влияет на лог-уровень и переменные окружения) ShmSize int // Размер /dev/shm в GB (дефолт 1) PtafFallbackCode sql.NullString // Код ответа ptaf_fallback: число или 'pass' (дефолт: 418) + SpAntiddos bool // Использовать антиддос сети для set_real_ip (дефолт true) + RealIPNetworks []string // Кастомные сети для set_real_ip если SpAntiddos = false } // Структуры для API ответов diff --git a/utils.go b/utils.go index 5e34d79..e9fc850 100644 --- a/utils.go +++ b/utils.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "database/sql" "encoding/hex" + "fmt" "io" "log" "os" @@ -335,3 +336,51 @@ func writeCustomLocationBlock(w stringWriter, customLocationStr string, correctU } w.WriteString("\n") } + +// parseHostnameForContainer парсит hostname хоста и возвращает префикс для имени контейнера. +// Примеры: +// +// PTAF-TB-docker-AZ3-HW-node01 → AZ3_hn01 +// PTAF-TB-docker-AZ1-node04 → AZ1_n04 +func parseHostnameForContainer(hostname string) string { + // Извлекаем AZ номер + azPart := "" + nodePart := "" + isHW := false + + parts := strings.Split(hostname, "-") + for i, part := range parts { + if strings.HasPrefix(strings.ToUpper(part), "AZ") { + azPart = strings.ToUpper(part) // например AZ3 + } + if strings.ToUpper(part) == "HW" { + isHW = true + } + if strings.HasPrefix(strings.ToLower(part), "node") && i == len(parts)-1 { + // Берём только цифры после "node" + nodeNum := strings.TrimPrefix(strings.ToLower(part), "node") + if isHW { + nodePart = "hn" + nodeNum + } else { + nodePart = "n" + nodeNum + } + } + } + + if azPart == "" || nodePart == "" { + return "" + } + return azPart + "_" + nodePart +} + +// buildContainerFullName формирует полное имя Docker-контейнера. +// Если hostname парсится корректно: {clientTitle}_{AZx}_{hn/n}{NN}_a{001} +// Иначе fallback на старый формат: ptaf_{clientTitle}_{001} +func buildContainerFullName(clientTitle string, containerNum int, hostname string) string { + prefix := parseHostnameForContainer(hostname) + if prefix == "" { + return fmt.Sprintf("ptaf_%s_%03d", clientTitle, containerNum) + } + // prefix = "AZ3_hn01", итог: "PSB_CFA_AZ3_hn01_a001" + return fmt.Sprintf("%s_%s_a%03d", clientTitle, prefix, containerNum) +}