Change real_ip

This commit is contained in:
Magnus Root 2026-04-24 17:44:32 +03:00
parent f2998502db
commit 707407a433
11 changed files with 259 additions and 82 deletions

View file

@ -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';
```

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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}}

12
db.go
View file

@ -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") {

View file

@ -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)

View file

@ -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 {

View file

@ -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 /)

View file

@ -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

View file

@ -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 ответов

View file

@ -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)
}