diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9f77eec..cfe3c02 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -11,7 +11,7 @@ body: attributes: label: Версии description: Panel, Node Agent, HAProxy и ОС ноды. - placeholder: "Panel 2.2.0, Agent 2.2.0, HAProxy 3.4, Ubuntu 24.04" + placeholder: "Panel 2.3.0, Agent 2.3.0, HAProxy 3.4, Ubuntu 24.04" validations: required: true - type: textarea diff --git a/CHANGELOG.md b/CHANGELOG.md index e524d01..bb8100c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2.3.0 — Английский интерфейс и стабильные коды ошибок API + +Panel и Node Agent — 2.3.0 (код Agent не менялся, изменён только bootstrap-скрипт на стороне Panel — обновление Agent необязательно), HAProxy renderer v21, миграций нет. Заметки к релизу: [docs/releases/2.3.0.md](docs/releases/2.3.0.md). Спасибо @ANHELL404 (#21, #23). + +- **Английский интерфейс** — локализации ru/en, переключатель языка в боковой панели и на странице входа; справка HAProxy на английском. +- **README на английском** — [README.en.md](README.en.md), заметки [2.2.0](docs/releases/2.2.0.en.md) и [2.3.0](docs/releases/2.3.0.en.md) на английском. +- **Стабильные коды в API** — `error_code`, `message_key`/`message_params` для копирования маршрутов, применения шаблонов и сообщений линтера HAProxy, `note_key` (`built_from_routes`); русское `message` сохранено для совместимости. Параметр `copy_suffix` для имени копии. +- **Исправлено:** bootstrap-скрипт Agent устанавливает бинарник в `/usr/local/bin` (#21, #23, @ANHELL404). + ## 2.2.0 — Закрепление в пулах с резервными серверами, копирование и шаблоны маршрутов Panel и Node Agent — 2.2.0 (код Agent не менялся), HAProxy renderer v21, миграции 000056 (`route_templates`) и 000057 (прежнее распределение старых failover-маршрутов). Заметки к релизу: [docs/releases/2.2.0.md](docs/releases/2.2.0.md). Спасибо @fesevu (#19, #20). diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..e750aea --- /dev/null +++ b/README.en.md @@ -0,0 +1,212 @@ +**English** | [Русский](README.md) + +

+ NodeFlow — HAProxy Infrastructure Control +

+ +

+ Install · + Upgrade · + Screenshots · + What's new in 2.3.0 · + Issues · + Support +

+ +NodeFlow is a control panel for a fleet of HAProxy nodes doing TCP and SNI routing. The Panel (Go API + React UI + PostgreSQL) stores routes and immutable configuration revisions, while a Node Agent on each node connects to the Panel over mTLS on its own, validates and applies the HAProxy config, reports metrics and updates itself from signed releases. No manual editing of `haproxy.cfg` and no inbound access to nodes is needed for management. + +![Nodes overview](docs/images/nodes-overview.png) + +## Features + +- **SNI routing and fallback.** Shared listeners per port, multiple SNI names per route, a "Fallback" route for everything that did not match; IP, domain and Unix socket backends. +- **Multiple servers and DNS pools.** Up to 16 servers per route in "Pool" and "Primary + backups" modes; a domain can act as a DNS pool (all A/AAAA addresses, unreachable ones excluded), with separate weights for pool IPs. +- **Load balancing.** `roundrobin`, `static-rr`, `random`, `leastconn` and `leastping` ("Lowest latency"). Weights per server and per DNS pool IP, plus a cost (latency multiplier) for `leastping`; tolerance: servers with similar load or latency share clients by weight, and the Agent sets weights on the fly through the runtime API, without a reload. +- **Client stickiness.** IP hash (consistent or map-based, IPv6 grouped by prefix) or a table with TTL that survives reloads; the "Auto" size is derived from the node's RAM. +- **PROXY protocol.** Accepted from trusted IPs, subnets and domains (the Agent resolves them and updates the ACL without a reload), or "from everyone"; health checks with a PROXY header. +- **Rate limiting.** Upload/download limits per client IP — via HAProxy filters or in the kernel via nftables (`shaper_mode=kernel`), without losing splice. +- **Performance.** Zero-copy splice in both directions; the Agent raises `fs.pipe-max-size`, and the Panel renders 1 MiB buffers instead of 256 KiB. +- **Quotas and traffic accounting.** Monthly traffic for nodes, backends and routes; per-route quotas. +- **Safe apply.** Desired/actual revisions, a full `haproxy -c` check before the config is replaced, graceful reload with rollback. +- **`haproxy.cfg` editor.** Node page → "HAProxy configuration": Monaco with highlighting and autocompletion, directive hints on hover, live validation (unknown directives and options, stray characters, ports, backend references), a diff before applying and revision history. A `haproxy -c` error from the node is shown as text with links to lines; one button returns to the config generated from routes. +- **Node HAProxy settings.** `maxconn`, `nbthread` and the `connect`/`client`/`server` timeouts live in the node settings and apply to all routes via a reload without dropping connections; connection logs can be turned off there too. +- **Prometheus `/metrics`** in the Panel: node and route state, connections, traffic, revision sync (bearer token, CIDR allowlist). +- **Signed Agent updates.** Ed25519 + SHA-256, a monotonic sequence, atomic activation and automatic rollback. +- **mTLS.** A separate client certificate per node, rotation and staged renewal; the Agent's local API is reachable from loopback only. +- **UFW.** Listener port rules are built from the applied revision; other rules are left untouched. + +## Architecture + +![NodeFlow architecture](docs/brand/architecture.svg) + +The Agent opens the connection to the Panel itself: it receives revisions and commands and sends heartbeats with metrics. SSH is needed once — for the initial Agent installation. + +## Requirements + +- **Panel:** Ubuntu 22.04+/Debian 12+ (amd64 or arm64), 2 vCPU, 2 GB RAM, 15 GB disk, a domain with an A/AAAA record pointing to the server, open `80`, `443` and `4200/tcp`. The installer sets up Docker Engine with Compose and Caddy itself. +- **Nodes:** Linux amd64 or arm64 with systemd and HAProxy 3.x. Automatic installation from the Panel supports Ubuntu 24.04 (noble) and 26.04 (resolute) — the Panel installs HAProxy 3.4 from the official HAProxy Performance repository and does not downgrade a newer installed HAProxy — and Debian: if HAProxy is not present, the distribution's `haproxy` package is installed (on Debian 12 that is 2.6, so install HAProxy 3.x beforehand). nftables is needed for in-kernel rate limiting. SSH access as root or as a user with sudo, by password or key. + +## Quick install + +### Panel + +On a clean server: + +```bash +curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/install.sh | sudo sh +``` + +The installer will: + +1. ask for the domain and check that its DNS points to this server; +2. offer an access mode: Caddy cookie gate (an activation link sets a secure cookie) or none — the Panel admin token is required either way; +3. install Docker and **Caddy** — Caddy obtains a Let's Encrypt certificate itself and proxies HTTPS to the Panel, which listens only on `127.0.0.1:8080`; +4. download the install kit of the latest release, verify it against the SHA-256 that GitHub publishes for every release asset, take `compose.release.yaml` from it, and generate `.env`, the CA, the mTLS certificate and the update signing key in `/opt/nodeflow`; +5. run `docker compose pull && docker compose up -d` with the `ghcr.io/nodeflow-dev/nodeflow-panel:2.3.0` image — nothing is built on the server, and the image applies database migrations itself; +6. publish signed Node Agent releases (amd64 and arm64) and save the login credentials to `~/nodeflow-credentials.txt` (mode `0600`). + +The installer does not change the firewall. For unattended automation, pass `NODEFLOW_DOMAIN`, `NODEFLOW_AUTH_MODE=cookie|none` and, if needed, `NODEFLOW_VERSION=2.3.0` (defaults to the latest release): + +```bash +curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/install.sh \ + | sudo NODEFLOW_DOMAIN=panel.example.com NODEFLOW_AUTH_MODE=cookie sh +``` + +The installer is also included in the install kit (`NodeFlow-Panel--Agent--install-kit.tar.gz` in the release assets): unpack it and run `sudo ./install.sh` — then `compose.release.yaml` is taken from the kit, and only the Agent binaries for publishing to the Panel are downloaded from GitHub. + +The release assets are exactly three files: the install kit (installers, `compose.release.yaml`, a `.env` template, PKI and node scripts, the Agent systemd unit, instructions) and the `nodeflow-node-agent--linux-amd64` / `-linux-arm64` binaries. The Panel ships only as the `ghcr.io/nodeflow-dev/nodeflow-panel` image. There is no separate `SHA256SUMS`: the installers check every downloaded file against the SHA-256 that GitHub publishes for the asset (the `digest` field in the release API) and abort on a mismatch or a missing digest. + +### Nodes + +In the Panel: **Nodes → Add node** → IP, SSH port, user and password or key → verify the host fingerprint → **"Install Node Agent"**. The Panel connects over SSH once and installs the signed Agent, the updater and the node's mTLS certificate; from then on the node is managed only through the outbound mTLS channel on `4200/tcp`. + +For recovery without the Panel there is `install-node.sh`: it downloads the Agent for the node's architecture from the latest release (or from `NODEFLOW_VERSION`) and verifies it against the asset's SHA-256 from the GitHub API; the systemd unit is embedded in the script. It requires `curl`, `jq` or `python3`, and an installed HAProxy: + +```bash +curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/scripts/install-node.sh -o install-node.sh +sudo NODE_AGENT_TOKEN= bash install-node.sh +``` + +The same script is in the install kit: `scripts/install-node.sh`. + +### Manual installation with Docker Compose + +Run the commands as root (`sudo -i`): the `/opt/nodeflow` directory is closed to other users. + +```bash +v=2.3.0 +kit=NodeFlow-Panel-$v-Agent-$v-install-kit +curl -fsSLO "https://github.com/NodeFlow-dev/nodeflow/releases/download/v$v/$kit.tar.gz" +# compare with the asset digest on the release page or in the API: +curl -fsSL "https://api.github.com/repos/NodeFlow-dev/nodeflow/releases/tags/v$v" \ + | jq -r --arg n "$kit.tar.gz" '.assets[] | select(.name == $n) | .digest' +sha256sum "$kit.tar.gz" +tar -xzf "$kit.tar.gz" && cd "$kit" && sha256sum -c SHA256SUMS +install -d -m 0750 /opt/nodeflow +install -m 0644 compose.release.yaml /opt/nodeflow/compose.yaml +install -m 0600 nodeflow.env.example /opt/nodeflow/.env # fill in secrets and domain +install -d /opt/nodeflow/scripts && install -m 0755 scripts/init-*.sh /opt/nodeflow/scripts/ +/opt/nodeflow/scripts/init-mtls-pki.sh panel.example.com /opt/nodeflow # CA, mTLS, signing key +cd /opt/nodeflow && docker compose pull && docker compose up -d +curl -fsS http://127.0.0.1:8080/healthz +``` + +The Panel needs an HTTPS reverse proxy in front of it: ready-made Caddy and Nginx examples are in [`docs/install/reverse-proxy`](docs/install/reverse-proxy). Port `4200` is not proxied: nodes connect to it directly. + +## Upgrading from 1.0.x + +Run the same installer again on the Panel server: + +```bash +curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/install.sh | sudo sh +``` + +When it finds `/opt/nodeflow/.env`, it acts as an upgrader: it makes a `pg_dump` and a configuration archive in `/var/backups/nodeflow/`, keeps `.env`, `tls/`, `pki/` and the Caddy snippet `/etc/caddy/conf.d/nodeflow-panel.caddy`, moves the installation from a source build to the ghcr.io image, runs migrations and checks that the Panel responds with the new version. On failure the previous `compose.yaml` and `.env` are restored; the database dump is kept (migrations are not rolled back automatically). + +Then, in **Settings → Node Agent**, update the nodes to 2.3.0 — the updater replaces the Agent atomically and rolls it back if it fails to start. Self-update does not change the systemd unit, so on nodes installed by 1.0.x versions add permission to tune kernel pipe buffers once (without it splice stays at 256 KiB) — or reinstall the Agent from the node menu: + +```bash +sudo install -d /etc/systemd/system/nodeflow-node-agent.service.d +printf '[Service]\nReadWritePaths=-/etc/sysctl.d -/proc/sys/fs/pipe-max-size -/proc/sys/fs/pipe-user-pages-soft\n' \ + | sudo tee /etc/systemd/system/nodeflow-node-agent.service.d/20-kernel-pipes.conf +sudo systemctl daemon-reload && sudo systemctl restart nodeflow-node-agent +``` + +Details, behaviour changes and the list of migrations are in the [2.0.0 release notes](docs/releases/2.0.0.md) (Russian). + +## Screenshots + +### Node card + +![Node card](docs/images/node-detail.png) + +### Route editor with HAProxy preview + +![Route editor](docs/images/route-editor.png) + +### haproxy.cfg editor with hints and validation + +![haproxy.cfg editor](docs/images/haproxy-editor.png) + +### Node HAProxy settings + +![Node HAProxy settings](docs/images/node-settings-haproxy.png) + +### Traffic and top routes + +![Traffic](docs/images/traffic-overview.png) + +### Settings: Node Agent releases, Panel and Prometheus + +![Settings](docs/images/settings.png) + +## Building from source + +Requires Go 1.25+ and Node.js 22+. + +```bash +git clone https://github.com/NodeFlow-dev/nodeflow.git && cd nodeflow +(cd frontend && npm ci && npm run build) +./scripts/build-panel.sh # panel-api with embedded UI +go build -trimpath -o node-agent ./cmd/node-agent +go build -trimpath -o node-updater ./cmd/node-updater +go test ./... +``` + +Local setup: `cp .env.example .env`, fill in the secrets and run `docker compose up -d --build` — the root `compose.yaml` builds the image from `Dockerfile.panel`. PostgreSQL integration tests are enabled with the `NODEFLOW_TEST_DATABASE_URL` variable. + +## Documentation + +- Release notes: [2.3.0](docs/releases/2.3.0.en.md), [2.2.0](docs/releases/2.2.0.en.md); in Russian: [2.1.0](docs/releases/2.1.0.md), [2.0.1](docs/releases/2.0.1.md), [2.0.0](docs/releases/2.0.0.md), [CHANGELOG](CHANGELOG.md) +- [Panel API](docs/api-contract.md) (Russian) +- [Node HAProxy settings and config editor](docs/features/haproxy-configuration.md) (Russian) +- [Architecture](docs/architecture.md) (Russian) +- [Initial node installation](docs/bootstrap-flow.md) (Russian) +- [Agent self-update](docs/node-self-update.md) (Russian) +- [Manual installation](docs/install/README-PANEL.txt) (Russian) + +## Security + +- The Node Agent is not exposed to the internet: its local API accepts only loopback traffic, and management goes over the outbound mTLS channel. +- Each node gets its own client certificate. +- Agent releases are verified by Ed25519 signature, SHA-256, platform and version sequence. +- By default the Panel listens only on `127.0.0.1`; exposing HTTP externally requires an explicit `ALLOW_INSECURE_HTTP=true`. +- Privileged actions are written to the audit log. + +Report vulnerabilities as described in [SECURITY.md](SECURITY.md), not through public Issues. + +## Feedback + +Bug reports, ideas and installation questions go to [Issues](../../issues). Include the Panel/Agent versions, the node's OS and HAProxy version, steps to reproduce and anonymized logs — without tokens, keys, certificates or client IP addresses. Pull request rules are in [CONTRIBUTING.md](CONTRIBUTING.md). + +## Support the project + +Support matters a lot for keeping NodeFlow updates coming. + +[Tribute](https://web.tribute.tg/d/Pat) + +Tron: `TNUe93tFxeHj4avBY8s3NWzjaiyZfPWD9T` + +## License + +© 2026 NodeFlow. All rights reserved. The source code is available for review, for self-hosting by NodeFlow users and for contributing to this repository. Redistribution, including of modified versions, and commercial use of the code require written permission from the copyright holder. Full text in [LICENSE](LICENSE). diff --git a/README.md b/README.md index a038b65..3994940 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[English](README.en.md) | **Русский** +

NodeFlow — HAProxy Infrastructure Control

@@ -6,7 +8,7 @@ Установка · Обновление · Скриншоты · - Что нового в 2.2.0 · + Что нового в 2.3.0 · Issues · Поддержка

@@ -60,10 +62,10 @@ curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/install. 2. предложит режим доступа: Caddy cookie-gate (ссылка активации ставит защищённую cookie) или без него — токен администратора Panel нужен в любом случае; 3. установит Docker и **Caddy** — Caddy сам получит сертификат Let's Encrypt и будет проксировать HTTPS на Panel, которая слушает только `127.0.0.1:8080`; 4. скачает install kit последнего релиза, проверит его по SHA-256, который GitHub публикует для каждого asset релиза, возьмёт из него `compose.release.yaml`, сгенерирует `.env`, CA, сертификат mTLS и ключ подписи обновлений в `/opt/nodeflow`; -5. выполнит `docker compose pull && docker compose up -d` с образом `ghcr.io/nodeflow-dev/nodeflow-panel:2.2.0` — на сервере ничего не собирается, миграции БД применяет сам образ; +5. выполнит `docker compose pull && docker compose up -d` с образом `ghcr.io/nodeflow-dev/nodeflow-panel:2.3.0` — на сервере ничего не собирается, миграции БД применяет сам образ; 6. опубликует подписанные релизы Node Agent (amd64 и arm64) и сохранит реквизиты входа в `~/nodeflow-credentials.txt` (права `0600`). -Файрвол установщик не меняет. Для автоматизации без вопросов передайте переменные `NODEFLOW_DOMAIN`, `NODEFLOW_AUTH_MODE=cookie|none` и при необходимости `NODEFLOW_VERSION=2.2.0` (по умолчанию — последний релиз): +Файрвол установщик не меняет. Для автоматизации без вопросов передайте переменные `NODEFLOW_DOMAIN`, `NODEFLOW_AUTH_MODE=cookie|none` и при необходимости `NODEFLOW_VERSION=2.3.0` (по умолчанию — последний релиз): ```bash curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/install.sh \ @@ -92,7 +94,7 @@ sudo NODE_AGENT_TOKEN=<токен> bash install-node.sh Команды выполняются от root (`sudo -i`): каталог `/opt/nodeflow` закрыт для остальных пользователей. ```bash -v=2.2.0 +v=2.3.0 kit=NodeFlow-Panel-$v-Agent-$v-install-kit curl -fsSLO "https://github.com/NodeFlow-dev/nodeflow/releases/download/v$v/$kit.tar.gz" # сверьте с digest asset'а на странице релиза или в API: @@ -121,7 +123,7 @@ curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/install. Найдя `/opt/nodeflow/.env`, он работает как апгрейдер: делает `pg_dump` и архив конфигурации в `/var/backups/nodeflow/`, сохраняет `.env`, `tls/`, `pki/` и Caddy-сниппет `/etc/caddy/conf.d/nodeflow-panel.caddy`, переводит установку со сборки из исходников на образ ghcr.io, запускает миграции и проверяет, что Panel отвечает новой версией. При ошибке возвращаются прежние `compose.yaml` и `.env`; дамп БД остаётся (миграции автоматически не откатываются). -Затем в **Настройки → Node Agent** обновите ноды до 2.2.0 — updater заменит Agent атомарно и откатит его при неудачном запуске. Самообновление не меняет systemd-юнит, поэтому на нодах, установленных версиями 1.0.x, один раз добавьте права на настройку pipe-буферов ядра (без этого splice остаётся на 256 КиБ) — или переустановите Agent из меню ноды: +Затем в **Настройки → Node Agent** обновите ноды до 2.3.0 — updater заменит Agent атомарно и откатит его при неудачном запуске. Самообновление не меняет systemd-юнит, поэтому на нодах, установленных версиями 1.0.x, один раз добавьте права на настройку pipe-буферов ядра (без этого splice остаётся на 256 КиБ) — или переустановите Agent из меню ноды: ```bash sudo install -d /etc/systemd/system/nodeflow-node-agent.service.d @@ -175,7 +177,7 @@ go test ./... ## Документация -- Заметки к релизам [2.2.0](docs/releases/2.2.0.md), [2.1.0](docs/releases/2.1.0.md), [2.0.1](docs/releases/2.0.1.md) и [2.0.0](docs/releases/2.0.0.md), [CHANGELOG](CHANGELOG.md) +- Заметки к релизам [2.3.0](docs/releases/2.3.0.md), [2.2.0](docs/releases/2.2.0.md), [2.1.0](docs/releases/2.1.0.md), [2.0.1](docs/releases/2.0.1.md) и [2.0.0](docs/releases/2.0.0.md), [CHANGELOG](CHANGELOG.md) - [API Panel](docs/api-contract.md) - [Настройки HAProxy ноды и редактор конфига](docs/features/haproxy-configuration.md) - [Архитектура](docs/architecture.md) diff --git a/cmd/node-agent/main.go b/cmd/node-agent/main.go index 3d69c1d..21adfb1 100644 --- a/cmd/node-agent/main.go +++ b/cmd/node-agent/main.go @@ -14,7 +14,7 @@ import ( "github.com/nodeflow/nodeflow/internal/agent" ) -var version = "2.2.0" +var version = "2.3.0" func main() { showVersion := flag.Bool("version", false, "print Node Agent version and exit") diff --git a/cmd/panel-api/main.go b/cmd/panel-api/main.go index 18e31e8..63681b1 100644 --- a/cmd/panel-api/main.go +++ b/cmd/panel-api/main.go @@ -18,7 +18,7 @@ import ( // panelVersion is the published Panel release version. Release builds may // replace it with -ldflags "-X main.panelVersion=...". -var panelVersion = "2.2.0" +var panelVersion = "2.3.0" func main() { if len(os.Args) > 1 { diff --git a/compose.release.yaml b/compose.release.yaml index 28dac25..b51a015 100644 --- a/compose.release.yaml +++ b/compose.release.yaml @@ -30,7 +30,7 @@ services: <<: *hardening migrate: - image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.2.0} + image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.3.0} restart: "no" command: ["migrate"] depends_on: @@ -79,7 +79,7 @@ services: <<: *hardening panel-api: - image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.2.0} + image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.3.0} restart: unless-stopped depends_on: migrate: diff --git a/docs/brand/nodeflow-hero.svg b/docs/brand/nodeflow-hero.svg index 7476609..2b1aa20 100644 --- a/docs/brand/nodeflow-hero.svg +++ b/docs/brand/nodeflow-hero.svg @@ -18,7 +18,7 @@ Control the flow.Keep the control. Ноды, TCP/SNI-маршруты, трафик и безопасные ревизии HAProxy —в одной прозрачной control plane. - v2.2.0HAProxyDocker Compose + v2.3.0HAProxyDocker Compose Paneldesired state Node AgentmTLS heartbeat diff --git a/docs/install/README-NODE-AGENT.txt b/docs/install/README-NODE-AGENT.txt index b2b04e7..8b7d728 100644 --- a/docs/install/README-NODE-AGENT.txt +++ b/docs/install/README-NODE-AGENT.txt @@ -2,13 +2,13 @@ NODEFLOW NODE AGENT — SIGNED RELEASE UPLOAD =========================================== Бинарники Agent — отдельные assets GitHub Release: - nodeflow-node-agent-2.2.0-linux-amd64 - nodeflow-node-agent-2.2.0-linux-arm64 + nodeflow-node-agent-2.3.0-linux-amd64 + nodeflow-node-agent-2.3.0-linux-arm64 install.sh публикует их в Panel сам. Для ручной загрузки скачайте файл и сверьте SHA-256 с digest, который GitHub показывает для asset'а: - v=2.2.0 f=nodeflow-node-agent-2.2.0-linux-amd64 + v=2.3.0 f=nodeflow-node-agent-2.3.0-linux-amd64 curl -fsSLO https://github.com/NodeFlow-dev/nodeflow/releases/download/v$v/$f curl -fsSL https://api.github.com/repos/NodeFlow-dev/nodeflow/releases/tags/v$v \ | jq -r --arg n "$f" '.assets[] | select(.name == $n) | .digest' @@ -17,7 +17,7 @@ install.sh публикует их в Panel сам. Для ручной загр В Panel откройте: Настройки → Node Agent (подписанные релизы) -Версия 2.2.0, ОС linux, архитектура amd64 или arm64 — по файлу. Выберите +Версия 2.3.0, ОС linux, архитектура amd64 или arm64 — по файлу. Выберите бинарник и нажмите «Загрузить и подписать». Затем назначьте релиз на странице нужной ноды. diff --git a/docs/install/index.html b/docs/install/index.html index 6823e2a..6747a42 100644 --- a/docs/install/index.html +++ b/docs/install/index.html @@ -34,7 +34,7 @@

Как установить Panel и подключить первую HAPro
Корень комплектаinstall.sh

Установщик и апгрейдер Panel, compose.release.yaml для готового образа ghcr.io/nodeflow-dev/nodeflow-panel, шаблон nodeflow.env.example, SHA256SUMS файлов комплекта.

scripts/ · configs/ · docs/scripts/install-node.sh

Скрипты PKI (init-mtls-pki.sh, init-update-signing-key.sh), ручная установка Agent и подготовка UFW ноды, systemd-юнит Agent, эта инструкция и примеры reverse proxy в docs/install/reverse-proxy/.

-
Бинарники Agent в комплект не входят. nodeflow-node-agent-2.2.0-linux-amd64 и nodeflow-node-agent-2.2.0-linux-arm64 лежат отдельными assets GitHub Release. install.sh скачивает оба, сверяет SHA-256 с digest, который GitHub публикует для asset'а, и публикует их в Panel.
+
Бинарники Agent в комплект не входят. nodeflow-node-agent-2.3.0-linux-amd64 и nodeflow-node-agent-2.3.0-linux-arm64 лежат отдельными assets GitHub Release. install.sh скачивает оба, сверяет SHA-256 с digest, который GitHub публикует для asset'а, и публикует их в Panel.
1

Подготовьте сервер Panel

Нужен домен, публичный IP и чистый Ubuntu или Debian.

@@ -158,7 +158,7 @@

Как установить Panel и подключить первую HAPro
5

Опубликуйте Node Agent

Без совместимого подписанного релиза Panel не начнёт установку ноды.

Автоматический установщик публикует Agent сам. Вручную:

-
  1. Скачайте nodeflow-node-agent-2.2.0-linux-amd64 (или -arm64) из assets GitHub Release.
  2. Сверьте sha256sum файла с digest asset'а на странице релиза (или в https://api.github.com/repos/NodeFlow-dev/nodeflow/releases/tags/v2.2.0, поле digest).
  3. Откройте Настройки → Node Agent (подписанные релизы).
  4. Версия: 2.2.0.
  5. ОС: linux.
  6. Архитектура: amd64 или arm64 — по файлу.
  7. Выберите файл и нажмите «Загрузить и подписать».
+
  1. Скачайте nodeflow-node-agent-2.3.0-linux-amd64 (или -arm64) из assets GitHub Release.
  2. Сверьте sha256sum файла с digest asset'а на странице релиза (или в https://api.github.com/repos/NodeFlow-dev/nodeflow/releases/tags/v2.3.0, поле digest).
  3. Откройте Настройки → Node Agent (подписанные релизы).
  4. Версия: 2.3.0.
  5. ОС: linux.
  6. Архитектура: amd64 или arm64 — по файлу.
  7. Выберите файл и нажмите «Загрузить и подписать».
Panel подписывает бинарник собственным Ed25519-ключом. Нода проверяет подпись, SHA-256 и порядок релиза; при неудачном запуске updater возвращает прежний Agent.
@@ -177,7 +177,7 @@

Как установить Panel и подключить первую HAPro
3. Проверить обновление
cd /opt/nodeflow
 sudo docker compose ps
 curl -fsS http://127.0.0.1:8080/healthz
-

После обновления Panel обновите Agent на нодах: «Настройки → Node Agent» → «Обновить до 2.2.0». Для нод, установленных версиями 1.0.x, см. раздел «Обновление с 1.0.x» в заметках к релизу 2.0.0 (однократный systemd drop-in для pipe-буферов ядра).

+

После обновления Panel обновите Agent на нодах: «Настройки → Node Agent» → «Обновить до 2.3.0». Для нод, установленных версиями 1.0.x, см. раздел «Обновление с 1.0.x» в заметках к релизу 2.0.0 (однократный systemd drop-in для pipe-буферов ядра).

8

Проверьте результат

Четыре короткие проверки после установки или обновления.

diff --git a/docs/releases/2.2.0.en.md b/docs/releases/2.2.0.en.md new file mode 100644 index 0000000..7fb99cd --- /dev/null +++ b/docs/releases/2.2.0.en.md @@ -0,0 +1,39 @@ +# NodeFlow 2.2.0 + +Panel **2.2.0** · Node Agent **2.2.0** · HAProxy renderer **v21** + +Client stickiness in DNS pools with backup servers, copying routes between nodes, and a route template library. Two migrations (000056, 000057). The Agent is unchanged. Thanks to @fesevu (#19, #20). + +## New + +- **Client stickiness and `leastconn` in pools with backup servers (#19).** A DNS pool with a backup server now supports client stickiness (sticky) and `leastconn` balancing. A new failover route is created without stickiness by default. + - Limitations: in backup-server mode, `leastping`/`leastconn` with a tolerance greater than 0 are rejected; `leastping` combined with `source` stickiness is rejected too. + - A client stuck to a backup server stays on it until the stickiness entry expires, even after the primary server is back. +- **Copying routes between nodes (#20).** A route can be copied to another node. The copy is created as a disabled draft — check the ports and addresses and enable it manually. +- **Route template library (#20).** A route can be saved as a template and used to create new routes on any node. Templates store `schema_version`. Migration 000056 (`route_templates`). + +## Fixes + +- **Rendering of existing failover routes does not change.** Migration 000057 keeps the previous balancing for routes with the legacy `source_table`: such routes are republished once with an identical HAProxy config. Re-saving an old failover route in the UI also keeps its balancing. +- **Audit:** route copying (`route.copy`) and template actions (`route_template.*`) are written to the audit log. +- Clear error messages and names for copies/templates; the copy and template dialogs follow the panel's style. + +## Upgrading from 2.1.0 + +1. **Panel.** Run the installer again: + + ```bash + curl -fsSL https://raw.githubusercontent.com/NodeFlow-dev/nodeflow/main/install.sh | sudo sh + ``` + + Manually: set `NODEFLOW_VERSION=2.2.0` in `/opt/nodeflow/.env` and run `docker compose pull && docker compose up -d`. The image applies migrations 000056 and 000057 itself. +2. **Node Agent — optional.** The Agent code did not change in 2.2.0; everything works with Agent 2.1.x. To keep versions uniform: "Settings → Node Agent" → "Update to 2.2.0". + +Upgrading from 1.0.x? Read the [2.0.0 release notes](2.0.0.md) (Russian) first: they cover a one-time systemd drop-in for kernel pipe buffers. + +## Release files + +- `NodeFlow-Panel-2.2.0-Agent-2.2.0-install-kit.tar.gz` — install kit. +- `nodeflow-node-agent-2.2.0-linux-amd64`, `nodeflow-node-agent-2.2.0-linux-arm64` — Node Agent binaries. + +The Panel ships as the `ghcr.io/nodeflow-dev/nodeflow-panel:2.2.0` image (linux/amd64 and linux/arm64). The SHA-256 of each file is the `digest` field in the release API; `install.sh` and `install-node.sh` verify it themselves. diff --git a/docs/releases/2.3.0.en.md b/docs/releases/2.3.0.en.md new file mode 100644 index 0000000..2ffe05a --- /dev/null +++ b/docs/releases/2.3.0.en.md @@ -0,0 +1,21 @@ +# NodeFlow 2.3.0 + +Panel **2.3.0** · Node Agent **2.3.0** · HAProxy renderer **v21** + +English UI with a language switch, an English README and stable error codes in the API. No migrations. Thanks to @ANHELL404 (#21, #23). + +## New + +- **English UI.** The panel is now available in English (ru/en locales). Switch the language in the sidebar or on the login page; the HAProxy directive help is available in English too. +- **English README** — [README.en.md](../../README.en.md) and English release notes for 2.2.0 and 2.3.0. +- **Stable codes in the API.** Route copy errors, template apply errors and HAProxy lint messages now carry `error_code` and `message_key` with `message_params`; the built-from-routes note carries `note_key` (`built_from_routes`). The Russian `message` field is kept for compatibility. +- **`copy_suffix`** — route copy parameter that sets the suffix of the copy name. + +## Fixes + +- The Agent bootstrap script installs the binary into `/usr/local/bin` (#21, #23). Thanks to @ANHELL404. + +## Upgrading + +- No migrations. +- Agent code is unchanged — only the bootstrap script served by the Panel changed. Upgrading the Agent on existing nodes is optional; new installs get the fixed script right away. diff --git a/docs/releases/2.3.0.md b/docs/releases/2.3.0.md new file mode 100644 index 0000000..cdcb5bb --- /dev/null +++ b/docs/releases/2.3.0.md @@ -0,0 +1,21 @@ +# NodeFlow 2.3.0 + +Panel **2.3.0** · Node Agent **2.3.0** · HAProxy renderer **v21** + +Английский интерфейс с переключателем языка, README на английском и стабильные коды ошибок в API. Миграций нет. Спасибо @ANHELL404 (#21, #23). + +## Новое + +- **Английский интерфейс.** Панель переведена на английский (локализации ru/en). Язык переключается в боковой панели и на странице входа; справка по директивам HAProxy тоже доступна на английском. +- **README на английском** — [README.en.md](../../README.en.md) и английские заметки к релизам 2.2.0 и 2.3.0. +- **Стабильные коды в API.** Ошибки копирования маршрутов, применения шаблонов и сообщения линтера HAProxy теперь содержат `error_code` и `message_key` с `message_params`; заметка о сборке из маршрутов — `note_key` (`built_from_routes`). Поле `message` на русском сохранено для совместимости. +- **`copy_suffix`** — параметр копирования маршрута, задающий суффикс имени копии. + +## Исправления + +- Bootstrap-скрипт установки Agent кладёт бинарник в `/usr/local/bin` (#21, #23). Спасибо @ANHELL404. + +## Обновление + +- Миграций нет. +- Код Agent не менялся — изменён только bootstrap-скрипт, который отдаёт Panel. Обновлять Agent на существующих нодах необязательно; новые установки сразу получают исправленный скрипт. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 803e3af..2a8c04a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "nodeflow-frontend", - "version": "2.2.0", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nodeflow-frontend", - "version": "2.2.0", + "version": "2.3.0", "dependencies": { "@mantine/core": "^8.3.10", "@mantine/hooks": "^8.3.10", diff --git a/frontend/package.json b/frontend/package.json index 8d52542..14f68a5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,14 +1,14 @@ { "name": "nodeflow-frontend", "private": true, - "version": "2.2.0", + "version": "2.3.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "typecheck": "tsc -b --pretty false", "preview": "vite preview", - "test": "node --experimental-strip-types --test tests/*.test.ts" + "test": "node --experimental-strip-types --import ./tests/setup.ts --test tests/*.test.ts" }, "dependencies": { "@mantine/core": "^8.3.10", diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index adcf310..a63c28a 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -2,6 +2,7 @@ import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { lazy, Suspense } from 'react'; import { AppFrame } from '../components/AppFrame'; import { demoSuffix } from '../lib/navigation'; +import { t } from '../i18n'; const NodesOverviewPage = lazy(() => import('../pages/NodesOverviewPage').then((module) => ({ default: module.NodesOverviewPage }))); const NodeDetailPage = lazy(() => import('../pages/NodeDetailPage').then((module) => ({ default: module.NodeDetailPage }))); @@ -17,14 +18,14 @@ export function App() { }> } /> - }>} /> - }>} /> - }>} /> - }>} /> - }>} /> - }>} /> - }>} /> - }>} /> + }>} /> + }>} /> + }>} /> + }>} /> + }>} /> + }>} /> + }>} /> + }>} /> } /> diff --git a/frontend/src/components/AppFrame.tsx b/frontend/src/components/AppFrame.tsx index 7c8e4d2..9654416 100644 --- a/frontend/src/components/AppFrame.tsx +++ b/frontend/src/components/AppFrame.tsx @@ -5,13 +5,15 @@ import { useEffect, useRef, useState } from 'react'; import { NavLink, Outlet, useLocation } from 'react-router-dom'; import { api } from '../lib/api'; import { preloadAppRoute, preloadInternalLink } from '../lib/navigation'; +import { LanguageMenu } from './LanguageMenu'; import { LoginPanel } from './LoginPanel'; import { NodeFlowLogo } from './NodeFlowLogo'; +import { t } from '../i18n'; const navigation = [ - { to: '/nodes', label: 'Ноды', icon: IconStack2 }, - { to: '/traffic', label: 'Трафик', icon: IconChartLine }, - { to: '/settings', label: 'Настройки', icon: IconSettings }, + { to: '/nodes', label: t('app.nodes'), icon: IconStack2 }, + { to: '/traffic', label: t('app.traffic'), icon: IconChartLine }, + { to: '/settings', label: t('app.settings'), icon: IconSettings }, ]; export function AppFrame() { @@ -74,7 +76,7 @@ export function AppFrame() { }; if (authState === 'checking') { - return
; + return
; } if (authState === 'unauthenticated') { @@ -97,10 +99,10 @@ export function AppFrame() { > - + - +
{navigation.map(({ to, label, icon: Icon }) => { @@ -120,13 +122,14 @@ export function AppFrame() { ); })} +
diff --git a/frontend/src/components/LanguageMenu.tsx b/frontend/src/components/LanguageMenu.tsx new file mode 100644 index 0000000..5e3cbb3 --- /dev/null +++ b/frontend/src/components/LanguageMenu.tsx @@ -0,0 +1,37 @@ +import { Menu, SegmentedControl } from '@mantine/core'; +import { IconLanguage } from '@tabler/icons-react'; +import { getLang, LANGS, setLang, t, type Lang } from '../i18n'; + +const labels: Record = { ru: t('app.languageRu'), en: t('app.languageEn') }; + +/** Sidebar language row above the account block, styled like a nav item. */ +export function LanguageMenu() { + const current = getLang(); + return ( + + + + + + {t('app.language')} + {LANGS.map((lang) => ( + setLang(lang)} rightSection={lang === current ? '✓' : null}> + {labels[lang]} + + ))} + + + ); +} + +/** Compact RU / EN switch for screens without the sidebar (sign-in). */ +export function LanguageSwitch() { + return ( + setLang(value as Lang)} aria-label={t('app.language')} + data={LANGS.map((lang) => ({ value: lang, label: lang.toUpperCase() }))} /> + ); +} diff --git a/frontend/src/components/LoginPanel.tsx b/frontend/src/components/LoginPanel.tsx index d5a8a64..288595e 100644 --- a/frontend/src/components/LoginPanel.tsx +++ b/frontend/src/components/LoginPanel.tsx @@ -2,7 +2,9 @@ import { Alert, Button, PasswordInput } from '@mantine/core'; import { useState, type FormEvent } from 'react'; import { IconAlertCircle, IconLock } from '@tabler/icons-react'; import { api } from '../lib/api'; +import { LanguageSwitch } from './LanguageMenu'; import { NodeFlowLogo } from './NodeFlowLogo'; +import { t } from '../i18n'; export function LoginPanel({ onSuccess }: { onSuccess: () => void }) { const [token, setToken] = useState(''); @@ -15,17 +17,17 @@ export function LoginPanel({ onSuccess }: { onSuccess: () => void }) { await api('/auth/login', { method: 'POST', body: JSON.stringify({ token: token.trim() }) }); setToken(''); onSuccess(); } catch (reason) { - setError(reason instanceof Error ? reason.message : 'Не удалось войти'); + setError(reason instanceof Error ? reason.message : t('app.signInFailed')); } finally { setPending(false); } }; return (
- -

Вход в панель

Используйте административный токен. После входа он заменяется защищённой browser-сессией.

+
+

{t('app.signInToThePanel')}

{t('app.useTheAdminTokenAfter')}

{error && }>{error}} - setToken(event.currentTarget.value)} leftSection={} required autoFocus /> - + setToken(event.currentTarget.value)} leftSection={} required autoFocus /> +
); diff --git a/frontend/src/components/NodeFlowLogo.tsx b/frontend/src/components/NodeFlowLogo.tsx index f5a3d71..723079f 100644 --- a/frontend/src/components/NodeFlowLogo.tsx +++ b/frontend/src/components/NodeFlowLogo.tsx @@ -1,6 +1,7 @@ import { Link, useLocation } from 'react-router-dom'; import { useEffect, useState } from 'react'; import { demoSuffix } from '../lib/navigation'; +import { t } from '../i18n'; let cachedPanelVersion = ''; let panelVersionRequest: Promise | null = null; @@ -49,7 +50,7 @@ export function NodeFlowLogo() { }, []); return ( - + NodeFlow {panelVersion && ( - + v{panelVersion} )} diff --git a/frontend/src/components/PageHeader.tsx b/frontend/src/components/PageHeader.tsx index 94a497a..e12da22 100644 --- a/frontend/src/components/PageHeader.tsx +++ b/frontend/src/components/PageHeader.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from 'react'; +import { t } from '../i18n'; interface PageHeaderProps { title: ReactNode; @@ -26,7 +27,7 @@ export function PageHeader({ return (
- {breadcrumb && } + {breadcrumb && }
{backAction &&
{backAction}
} {icon && } diff --git a/frontend/src/components/StateView.tsx b/frontend/src/components/StateView.tsx index 478fbd8..d537438 100644 --- a/frontend/src/components/StateView.tsx +++ b/frontend/src/components/StateView.tsx @@ -1,6 +1,7 @@ import { Button } from '@mantine/core'; import { IconAlertTriangle, IconDatabaseOff } from '@tabler/icons-react'; import type { ReactNode } from 'react'; +import { t } from '../i18n'; interface StateViewProps { title: string; @@ -21,5 +22,5 @@ export function StateView({ title, description, action, tone = 'empty' }: StateV } export function RetryButton({ onClick }: { onClick: () => void }) { - return ; + return ; } diff --git a/frontend/src/features/haproxy-editor/haproxyDocs.en.ts b/frontend/src/features/haproxy-editor/haproxyDocs.en.ts new file mode 100644 index 0000000..5f60e88 --- /dev/null +++ b/frontend/src/features/haproxy-editor/haproxyDocs.en.ts @@ -0,0 +1,508 @@ +// English hover documentation for haproxy.cfg: Russian source text (haproxyDocs.ts) → English. +// Directive names and syntax are kept verbatim; only prose and are translated. +// Pure data: no runtime imports. + +export const DOCS_EN: Record = { + "Имя сгенерировано NodeFlow; на него завязаны метрики и квоты. **Не переименовывать.**": "Name generated by NodeFlow; metrics and quotas depend on it. **Do not rename.**", + "Параметры процесса HAProxy: лимиты, потоки, логи, stats-сокет, пользователь. Секция одна на весь конфиг.": "HAProxy process settings: limits, threads, logs, stats socket, user. One section per config.", + "Значения по умолчанию для всех следующих frontend/backend/listen: режим, таймауты, опции логирования.": "Defaults for all following frontend/backend/listen sections: mode, timeouts, logging options.", + "⚠ Если добавить **второй defaults**, то он применяется только к секциям ниже него.": "⚠ If you add a **second defaults**, it applies only to the sections below it.", + "Точка входа: слушает порты (bind), принимает соединения и выбирает backend по правилам.": "Entry point: listens on ports (bind), accepts connections and picks a backend by rules.", + "Группа серверов назначения, алгоритм балансировки и проверки здоровья.": "Group of destination servers, load-balancing algorithm and health checks.", + "Frontend и backend в одной секции: bind и server рядом. Удобно для простых TCP-прокси.": "Frontend and backend in one section: bind and server side by side. Handy for simple TCP proxies.", + "Набор DNS-серверов, через которые HAProxy перерезолвит имена серверов во время работы.": "Set of DNS servers HAProxy uses to re-resolve server names at runtime.", + "Синхронизация stick-table между процессами HAProxy (в NodeFlow — между старым и новым процессом при reload).": "Stick-table synchronization between HAProxy processes (in NodeFlow — between the old and new process on reload).", + "Список пользователей и групп для HTTP basic-auth.": "List of users and groups for HTTP basic auth.", + "Кеш HTTP-ответов в памяти. Для TCP-маршрутов не используется.": "In-memory HTTP response cache. Not used for TCP routes.", + "Внешняя программа, которую запускает и перезапускает master-процесс HAProxy.": "External program started and restarted by the HAProxy master process.", + "Кольцевой буфер в памяти для логов и трассировок.": "In-memory ring buffer for logs and traces.", + "Набор пользовательских страниц ошибок (errorfile) для HTTP-режима.": "Set of custom error pages (errorfile) for HTTP mode.", + "Хранилище сертификатов и ключей, на которые ссылаются bind-строки.": "Store of certificates and keys referenced by bind lines.", + "SMTP-серверы для почтовых уведомлений об изменении состояния серверов.": "SMTP servers for email alerts about server state changes.", + "Приём и пересылка syslog-сообщений через HAProxy.": "Receives and forwards syslog messages through HAProxy.", + "Условный блок конфигурации: строки до .endif читаются, только если условие истинно (проверяется при загрузке конфига).": "Conditional config block: lines up to .endif are read only if the condition is true (evaluated when the config loads).", + "⚠ Если не закрыть блок **.endif**, то HAProxy не загрузит конфиг.": "⚠ If the block is not closed with **.endif**, HAProxy will not load the config.", + "Альтернативная ветка условного блока .if.": "Alternative branch of an .if block.", + "Ветка «иначе» условного блока .if.": "“Else” branch of an .if block.", + "Закрывает условный блок .if.": "Closes an .if block.", + "Выводит сообщение уровня notice при загрузке конфига.": "Prints a notice-level message when the config loads.", + "Выводит предупреждение при загрузке конфига.": "Prints a warning when the config loads.", + "Выводит ошибку и прерывает загрузку конфига.": "Prints an error and aborts loading the config.", + "Выводит диагностическое сообщение (только с ключом -dD).": "Prints a diagnostic message (only with -dD).", + "Максимум одновременных соединений: в global — на весь процесс, во frontend — на этот вход. Лишние соединения ждут в очереди ядра.": "Maximum concurrent connections: in global — for the whole process, in a frontend — for this entry point. Extra connections wait in the kernel queue.", + "⚠ Если поставить **слишком мало**, то новые клиенты будут ждать или отваливаться по таймауту.": "⚠ If set **too low**, new clients will wait or drop on timeout.", + "Число рабочих потоков HAProxy. По умолчанию — по числу доступных CPU.": "Number of HAProxy worker threads. Defaults to the number of available CPUs.", + "⚠ Если указать **больше ядер**, чем есть, то потоки конкурируют и задержка растёт.": "⚠ If you set **more than the cores** available, threads compete and latency grows.", + "Куда отправлять логи: syslog-адрес или сокет, facility и уровень. «log global» в прокси берёт настройки из global.": "Where to send logs: syslog address or socket, facility and level. “log global” in a proxy takes the settings from global.", + "⚠ Если писать лог **каждого соединения** на маленький диск, то syslog быстро заполнит место.": "⚠ If you log **every connection** to a small disk, syslog will fill it up quickly.", + "Использовать те же log-цели, что заданы в секции global.": "Use the same log targets as defined in the global section.", + "Шаблон строки лога соединения с переменными вида %ci, %ft, %B.": "Connection log line format with variables such as %ci, %ft, %B.", + "UNIX-сокет Runtime API: статистика, включение/выключение серверов, веса, таблицы. NodeFlow Agent читает через него метрики и управляет квотами.": "Runtime API UNIX socket: statistics, enabling/disabling servers, weights, tables. NodeFlow Agent reads metrics and enforces quotas through it.", + "⚠ Если **удалить или переименовать** /run/haproxy/admin.sock, то метрики, квоты и мягкий reload в NodeFlow перестанут работать.": "⚠ If you **remove or rename** /run/haproxy/admin.sock, metrics, quotas and graceful reload in NodeFlow stop working.", + "Сколько ждать команду на stats-сокете, прежде чем закрыть соединение.": "How long to wait for a command on the stats socket before closing the connection.", + "Пользователь, от имени которого работает HAProxy после старта.": "User HAProxy runs as after startup.", + "Группа, от имени которой работает HAProxy после старта.": "Group HAProxy runs as after startup.", + "Файл, в который записывается PID процесса.": "File the process PID is written to.", + "Запуск в фоне. Под systemd обычно не нужен: сервис использует master-worker режим.": "Run in the background. Usually not needed under systemd: the service uses master-worker mode.", + "Режим master-worker: мастер-процесс перезапускает воркеры при reload без потери сокетов.": "Master-worker mode: the master process restarts workers on reload without losing sockets.", + "Переводит процесс в chroot-каталог после старта.": "Moves the process into a chroot directory after startup.", + "⚠ Если включить **chroot**, то пути к файлам (acl -f, errorfile) должны быть доступны внутри каталога.": "⚠ If **chroot** is enabled, file paths (acl -f, errorfile) must be reachable inside the directory.", + "Сколько старый процесс может дожидаться завершения соединений после reload, прежде чем закрыть их принудительно.": "How long the old process may wait for connections to finish after a reload before closing them forcibly.", + "⚠ Если значение **не задано**, то старые процессы с долгими TCP-сессиями живут неограниченно и держат память.": "⚠ If **not set**, old processes with long TCP sessions live indefinitely and hold memory.", + "Список шифров TLS ≤1.2 по умолчанию для bind с ssl.": "Default TLS ≤1.2 cipher list for bind lines with ssl.", + "Список шифров TLS 1.3 по умолчанию для bind с ssl.": "Default TLS 1.3 cipher suites for bind lines with ssl.", + "Опции TLS по умолчанию для bind: минимальная версия, отключение tickets и т.п.": "Default TLS options for bind: minimum version, disabling tickets, etc.", + "Список шифров по умолчанию для TLS-соединений к серверам.": "Default cipher list for TLS connections to servers.", + "Опции TLS по умолчанию для соединений к серверам.": "Default TLS options for connections to servers.", + "Размер pipe-буфера ядра для splice. NodeFlow подбирает его по объёму памяти ноды.": "Kernel pipe buffer size for splice. NodeFlow sizes it from the node memory.", + "⚠ Если задать **слишком большой** размер на ноде с малой памятью, то под нагрузкой не хватит RAM.": "⚠ If set **too large** on a low-memory node, RAM will run out under load.", + "Размер буфера на соединение. Влияет на память и на максимальный размер заголовков.": "Per-connection buffer size. Affects memory and the maximum header size.", + "⚠ Если **увеличить** bufsize, то память на maxconn соединений растёт пропорционально.": "⚠ If you **increase** bufsize, memory for maxconn connections grows proportionally.", + "Резерв буфера под перезапись HTTP-заголовков.": "Buffer reserve for rewriting HTTP headers.", + "Размер DH-параметров для TLS.": "DH parameter size for TLS.", + "Имя этого процесса в секции peers. NodeFlow использует его для переноса stick-table между процессами при reload.": "Name of this process in the peers section. NodeFlow uses it to carry stick-tables across processes on reload.", + "Отключает SO_REUSEPORT на слушающих сокетах.": "Disables SO_REUSEPORT on listening sockets.", + "Привязка потоков HAProxy к ядрам CPU.": "Pins HAProxy threads to CPU cores.", + "Лимит открытых файлов процесса. По умолчанию вычисляется из maxconn.": "Process open-files limit. Computed from maxconn by default.", + "Разрешает экспериментальные директивы.": "Allows experimental directives.", + "Режим прокси: tcp — прозрачная пересылка байтов (NodeFlow), http — разбор HTTP.": "Proxy mode: tcp — transparent byte forwarding (NodeFlow), http — HTTP parsing.", + "⚠ Если frontend и backend в **разных режимах**, то HAProxy откажется загружать конфиг.": "⚠ If the frontend and backend use **different modes**, HAProxy refuses to load the config.", + "Сколько ждать установления TCP-соединения с сервером.": "How long to wait for the TCP connection to the server to be established.", + "⚠ Если значение **слишком большое**, то клиент долго ждёт при недоступном сервере вместо перехода на другой.": "⚠ If the value is **too large**, the client waits long on an unavailable server instead of moving to another.", + "Максимальная пауза без данных со стороны клиента. После неё соединение закрывается.": "Maximum inactivity on the client side. The connection is closed after it.", + "⚠ Если поставить **слишком мало** для VPN/прокси-трафика, то простаивающие сессии будут рваться.": "⚠ If set **too low** for VPN/proxy traffic, idle sessions will be cut.", + "Максимальная пауза без данных со стороны сервера.": "Maximum inactivity on the server side.", + "⚠ Если значение **меньше timeout client**, то длинные сессии обрываются со стороны сервера.": "⚠ If the value is **less than timeout client**, long sessions are cut from the server side.", + "Таймаут бездействия для установившегося туннеля (WebSocket, CONNECT, TCP). Заменяет client/server после установления.": "Inactivity timeout for an established tunnel (WebSocket, CONNECT, TCP). Replaces client/server once established.", + "Дополнительный таймаут на чтение ответа health-check после соединения.": "Additional timeout for reading the health-check response after connecting.", + "Сколько соединение может ждать в очереди, когда у серверов исчерпан maxconn.": "How long a connection may wait in the queue when servers have reached maxconn.", + "Таймаут бездействия клиента после полузакрытия соединения (FIN).": "Client inactivity timeout after a half-closed connection (FIN).", + "Таймаут бездействия сервера после полузакрытия соединения (FIN).": "Server inactivity timeout after a half-closed connection (FIN).", + "Время на получение полного HTTP-запроса (защита от slowloris).": "Time allowed to receive a complete HTTP request (slowloris protection).", + "Сколько ждать следующий HTTP-запрос в keep-alive соединении.": "How long to wait for the next HTTP request on a keep-alive connection.", + "Интервал периодического перерезолва имён в секции resolvers.": "Interval for periodic re-resolution of names in the resolvers section.", + "Сколько ждать ответа DNS-сервера перед повторным запросом.": "How long to wait for a DNS server reply before retrying.", + "Расширенный формат лога для TCP: адреса, время, байты, состояние завершения.": "Extended TCP log format: addresses, timings, bytes, termination state.", + "Расширенный формат лога для HTTP.": "Extended HTTP log format.", + "Не логировать соединения без данных (сканеры портов, проверки балансировщиков).": "Do not log connections without data (port scanners, load balancer probes).", + "Пересылка данных клиент→сервер через splice ядра без копирования в user space. Экономит CPU.": "Forwards client→server data with kernel splice, without copying into user space. Saves CPU.", + "Пересылка данных сервер→клиент через splice ядра без копирования в user space.": "Forwards server→client data with kernel splice, without copying into user space.", + "HAProxy сам решает, когда использовать splice.": "HAProxy decides on its own when to use splice.", + "При неудачном подключении разрешает повтор на другой сервер, даже если клиент «прилип» к исходному.": "On a failed connection allows retrying on another server, even if the client is “stuck” to the original one.", + "Health-check через TCP-сценарий (tcp-check connect/send/expect). Без правил — просто проверка соединения.": "Health check via a TCP script (tcp-check connect/send/expect). Without rules — just a connection check.", + "Health-check HTTP-запросом; сервер считается живым при ответе 2xx/3xx.": "Health check via an HTTP request; the server is considered alive on a 2xx/3xx response.", + "Когда все основные серверы недоступны — балансировать между всеми backup-серверами, а не только первым.": "When all primary servers are down, balance across all backup servers instead of only the first one.", + "Включить TCP keepalive к клиенту: ядро будет замечать «мёртвые» клиентские соединения.": "Enable TCP keepalive to the client: the kernel will detect “dead” client connections.", + "Включить TCP keepalive к серверу.": "Enable TCP keepalive to the server.", + "TCP keepalive в обе стороны.": "TCP keepalive in both directions.", + "Добавлять заголовок X-Forwarded-For (только HTTP-режим).": "Add the X-Forwarded-For header (HTTP mode only).", + "Снимать запрос из очереди, если клиент уже закрыл соединение.": "Drop a request from the queue if the client has already closed the connection.", + "Логировать смену результатов health-check.": "Log health-check result changes.", + "Отключает опцию, унаследованную из defaults.": "Disables an option inherited from defaults.", + "Через сколько простоя ядро начинает слать keepalive-пробы клиенту.": "Idle time after which the kernel starts sending keepalive probes to the client.", + "Интервал между keepalive-пробами клиенту.": "Interval between keepalive probes to the client.", + "Сколько неотвеченных keepalive-проб до закрытия клиентского соединения.": "Unanswered keepalive probes before the client connection is closed.", + "Через сколько простоя ядро начинает слать keepalive-пробы серверу.": "Idle time after which the kernel starts sending keepalive probes to the server.", + "Интервал между keepalive-пробами серверу.": "Interval between keepalive probes to the server.", + "Сколько неотвеченных keepalive-проб до закрытия серверного соединения.": "Unanswered keepalive probes before the server connection is closed.", + "Сколько раз повторять неудачное подключение к серверу.": "How many times to retry a failed connection to a server.", + "Алгоритм выбора сервера в backend.": "Server selection algorithm in the backend.", + "⚠ Если сменить алгоритм **с source/hash**, то клиенты потеряют привязку к серверам.": "⚠ If you switch the algorithm **from source/hash**, clients lose their server stickiness.", + "Как хеш (balance source/hash) отображается на серверы: map-based — быстро, consistent — минимальные перестановки при изменениях.": "How the hash (balance source/hash) maps to servers: map-based — fast, consistent — minimal reshuffling on changes.", + "Для consistent-хеша: сервер не получит больше указанного процента от средней нагрузки.": "For a consistent hash: a server gets no more than the given percentage of the average load.", + "Адрес и порт, на которых слушает frontend. Можно указать несколько bind.": "Address and port the frontend listens on. Several bind lines are allowed.", + "bind [<адрес>]:<порт>[,…] [опции]": "bind [
]:[,…] [options]", + "⚠ Если порт **уже занят** другим процессом, то HAProxy не запустится после reload.": "⚠ If the port is **already in use** by another process, HAProxy will not start after reload.", + "Сколько ждать данные клиента для проверки content-правил (например, SNI из TLS ClientHello).": "How long to wait for client data to evaluate content rules (e.g. SNI from the TLS ClientHello).", + "⚠ Если убрать **inspect-delay**, то SNI-правила не успеют увидеть ClientHello и трафик уйдёт в default_backend.": "⚠ If you remove **inspect-delay**, SNI rules will not see the ClientHello in time and traffic goes to default_backend.", + "Правило над содержимым соединения после inspect-delay: accept, reject, установка лимитов и т.п.": "Rule on connection content after inspect-delay: accept, reject, set limits, etc.", + "Правило сразу после accept(), до чтения данных: принять, отклонить, ожидать PROXY-заголовок.": "Rule right after accept(), before reading data: accept, reject, expect a PROXY header.", + "Правило после handshake (PROXY/TLS), до чтения данных.": "Rule after the handshake (PROXY/TLS), before reading data.", + "Правило над данными от сервера.": "Rule on data coming from the server.", + "Шаг TCP health-check: открыть соединение.": "TCP health-check step: open a connection.", + "Шаг TCP health-check: отправить строку.": "TCP health-check step: send a string.", + "Шаг TCP health-check: ожидаемый ответ.": "TCP health-check step: expected response.", + "Отправить соединение в указанный backend, если условие истинно. Правила проверяются по порядку.": "Send the connection to the given backend if the condition is true. Rules are evaluated in order.", + "⚠ Если backend **не существует**, то HAProxy не загрузит конфиг.": "⚠ If the backend **does not exist**, HAProxy will not load the config.", + "Backend для соединений, не подошедших ни под одно use_backend.": "Backend for connections that match no use_backend rule.", + "Именованное условие: выборка (src, req.ssl_sni…) плюс значения для сравнения. Используется в if/unless.": "Named condition: a fetch (src, req.ssl_sni…) plus values to compare. Used in if/unless.", + "acl <имя> <выборка> [флаги] <значения>": "acl [flags] ", + "Сервер назначения в backend: имя, адрес:порт и опции проверок, веса, PROXY protocol.": "Destination server in a backend: name, address:port and options for checks, weights, PROXY protocol.", + "server <имя> <адрес>[:<порт>] [опции]": "server
[:] [options]", + "⚠ Если **переименовать** сервер nf_srv_*, то NodeFlow потеряет его метрики и квоты.": "⚠ If you **rename** an nf_srv_* server, NodeFlow loses its metrics and quotas.", + "Создаёт N серверов из одного DNS-имени: каждый слот получает один из адресов записи.": "Creates N servers from one DNS name: each slot gets one of the record addresses.", + "server-template <префикс> <кол-во> :<порт> [опции]": "server-template : [options]", + "Опции по умолчанию для всех server в секции.": "Default options for all server lines in the section.", + "default-server [опции]": "default-server [options]", + "Таблица в памяти для привязки клиентов к серверам или счётчиков (скорость, соединения).": "In-memory table for binding clients to servers or for counters (rate, connections).", + "⚠ Если **size** мал для числа клиентов, то старые записи вытесняются и привязка теряется.": "⚠ If **size** is too small for the number of clients, old entries are evicted and stickiness is lost.", + "Запомнить в stick-table, на какой сервер ушёл клиент, и отправлять его туда же.": "Remember in the stick-table which server a client went to and keep sending it there.", + "Искать клиента в stick-table без записи.": "Look up the client in the stick-table without storing.", + "Записывать клиента в stick-table без поиска.": "Store the client in the stick-table without looking up.", + "Фильтр ограничения скорости входящего (upload) трафика. Лимит применяется через tcp-request content set-bandwidth-limit.": "Rate-limit filter for incoming (upload) traffic. The limit is applied with tcp-request content set-bandwidth-limit.", + "Фильтр ограничения скорости исходящего (download) трафика.": "Rate-limit filter for outgoing (download) traffic.", + "Подключает фильтр потока данных (bwlim-in/out, compression, trace…).": "Attaches a data stream filter (bwlim-in/out, compression, trace…).", + "filter <тип> [параметры]": "filter [parameters]", + "Файл ответа для HTTP-кода ошибки.": "Response file for an HTTP error code.", + "Правило обработки HTTP-запроса (только mode http).": "HTTP request processing rule (mode http only).", + "Правило обработки HTTP-ответа (только mode http).": "HTTP response processing rule (mode http only).", + "Шаг HTTP health-check.": "HTTP health-check step.", + "Нагрузка на backend, при которой серверы используют полный maxconn (для динамического minconn).": "Backend load at which servers use their full maxconn (for dynamic minconn).", + "Длина очереди ядра для ещё не принятых соединений.": "Kernel queue length for not yet accepted connections.", + "Ограничение новых сессий в секунду на frontend.": "Limit of new sessions per second on the frontend.", + "Текстовое описание секции (видно в статистике).": "Text description of the section (shown in statistics).", + "Секция загружается, но не работает.": "The section is loaded but inactive.", + "Явно включает секцию (по умолчанию).": "Explicitly enables the section (default).", + "URI для внешних health-проверок самого HAProxy.": "URI for external health checks of HAProxy itself.", + "Исходный адрес для соединений к серверам.": "Source address for connections to servers.", + "DNS-сервер для этой секции resolvers.": "DNS server for this resolvers section.", + "Сколько раз повторять DNS-запрос, прежде чем считать резолв неудачным.": "How many times to retry a DNS query before treating resolution as failed.", + "Сколько хранить последний результат DNS для заданного статуса ответа (valid, nx, timeout…).": "How long to keep the last DNS result for a given response status (valid, nx, timeout…).", + "⚠ Если **hold valid** слишком велик, то смена IP у бэкенда подхватится с задержкой.": "⚠ If **hold valid** is too long, a backend IP change is picked up late.", + "Взять nameserver-ы из /etc/resolv.conf.": "Take nameservers from /etc/resolv.conf.", + "Максимальный размер DNS-ответа по UDP (EDNS0).": "Maximum DNS response size over UDP (EDNS0).", + "Участник синхронизации stick-table.": "Stick-table synchronization peer.", + "Включает health-check сервера. Упавший сервер исключается из балансировки.": "Enables server health checks. A failed server is removed from load balancing.", + "⚠ Если **check** не указан, то HAProxy шлёт трафик на сервер, даже если он недоступен.": "⚠ If **check** is not set, HAProxy sends traffic to the server even when it is down.", + "Интервал между health-check.": "Interval between health checks.", + "inter <время> # по умолчанию 2s": "inter