diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0a3f43a..707b032 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.0.0, Agent 2.0.0, HAProxy 3.4, Ubuntu 24.04" + placeholder: "Panel 2.0.1, Agent 2.0.1, HAProxy 3.4, Ubuntu 24.04" validations: required: true - type: textarea diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa2d1b..9d8ba5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2.0.1 — Толерантность на одном DNS-пуле + +- Исправлено: маршрут с одним сервером-доменом в режиме DNS-пула терял «Толерантность» и алгоритм «Меньше соединений» при сохранении — панель сворачивала его в старый формат одиночного маршрута, где эти настройки не хранятся. Теперь такой маршрут остаётся пулом серверов, и толерантность сохраняется и применяется агентом. Маршруты без толерантности и с закреплением по хешу IP рендерятся как раньше, байт в байт. +- Агент для этого не меняется; номер 2.0.1 у Panel и Node Agent общий. + ## 2.0.0 — Готовые образы и расширенная балансировка Panel и Node Agent — 2.0.0, HAProxy renderer v21. Первый стабильный релиз после 1.0.8; в него вошли все изменения предварительных сборок 1.1.x. Заметки к релизу с инструкцией по обновлению: [docs/releases/2.0.0.md](docs/releases/2.0.0.md). diff --git a/README.md b/README.md index f9d530d..c3fd6b0 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ 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.0.0` — на сервере ничего не собирается, миграции БД применяет сам образ; +5. выполнит `docker compose pull && docker compose up -d` с образом `ghcr.io/nodeflow-dev/nodeflow-panel:2.0.1` — на сервере ничего не собирается, миграции БД применяет сам образ; 6. опубликует подписанные релизы Node Agent (amd64 и arm64) и сохранит реквизиты входа в `~/nodeflow-credentials.txt` (права `0600`). Файрвол установщик не меняет. Для автоматизации без вопросов передайте переменные `NODEFLOW_DOMAIN`, `NODEFLOW_AUTH_MODE=cookie|none` и при необходимости `NODEFLOW_VERSION=2.0.0` (по умолчанию — последний релиз): @@ -90,7 +90,7 @@ sudo NODE_AGENT_TOKEN=<токен> bash install-node.sh Команды выполняются от root (`sudo -i`): каталог `/opt/nodeflow` закрыт для остальных пользователей. ```bash -v=2.0.0 +v=2.0.1 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: @@ -119,7 +119,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 отвечает версией 2.0.0. При ошибке возвращаются прежние `compose.yaml` и `.env`; дамп БД остаётся (миграции автоматически не откатываются). -Затем в **Настройки → Node Agent** обновите ноды до 2.0.0 — updater заменит Agent атомарно и откатит его при неудачном запуске. Самообновление не меняет systemd-юнит, поэтому на нодах, установленных версиями 1.0.x, один раз добавьте права на настройку pipe-буферов ядра (без этого splice остаётся на 256 КиБ) — или переустановите Agent из меню ноды: +Затем в **Настройки → Node Agent** обновите ноды до 2.0.1 — updater заменит Agent атомарно и откатит его при неудачном запуске. Самообновление не меняет systemd-юнит, поэтому на нодах, установленных версиями 1.0.x, один раз добавьте права на настройку pipe-буферов ядра (без этого splice остаётся на 256 КиБ) — или переустановите Agent из меню ноды: ```bash sudo install -d /etc/systemd/system/nodeflow-node-agent.service.d diff --git a/cmd/node-agent/main.go b/cmd/node-agent/main.go index 554d110..74cbbf8 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.0.0" +var version = "2.0.1" 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 b7da1ea..b11e762 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.0.0" +var panelVersion = "2.0.1" func main() { if len(os.Args) > 1 { diff --git a/compose.release.yaml b/compose.release.yaml index 1c3a882..cbe4d38 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.0.0} + image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.0.1} restart: "no" command: ["migrate"] depends_on: @@ -79,7 +79,7 @@ services: <<: *hardening panel-api: - image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.0.0} + image: ghcr.io/nodeflow-dev/nodeflow-panel:${NODEFLOW_VERSION:-2.0.1} restart: unless-stopped depends_on: migrate: diff --git a/docs/install/README-NODE-AGENT.txt b/docs/install/README-NODE-AGENT.txt index a728718..67d8983 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.0.0-linux-amd64 - nodeflow-node-agent-2.0.0-linux-arm64 + nodeflow-node-agent-2.0.1-linux-amd64 + nodeflow-node-agent-2.0.1-linux-arm64 install.sh публикует их в Panel сам. Для ручной загрузки скачайте файл и сверьте SHA-256 с digest, который GitHub показывает для asset'а: - v=2.0.0 f=nodeflow-node-agent-2.0.0-linux-amd64 + v=2.0.1 f=nodeflow-node-agent-2.0.1-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.0.0, ОС linux, архитектура amd64 или arm64 — по файлу. Выберите +Версия 2.0.1, ОС linux, архитектура amd64 или arm64 — по файлу. Выберите бинарник и нажмите «Загрузить и подписать». Затем назначьте релиз на странице нужной ноды. diff --git a/docs/install/index.html b/docs/install/index.html index d3a0db8..2356888 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.0.0-linux-amd64 и nodeflow-node-agent-2.0.0-linux-arm64 лежат отдельными assets GitHub Release. install.sh скачивает оба, сверяет SHA-256 с digest, который GitHub публикует для asset'а, и публикует их в Panel.
+
Бинарники Agent в комплект не входят. nodeflow-node-agent-2.0.1-linux-amd64 и nodeflow-node-agent-2.0.1-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.0.0-linux-amd64 (или -arm64) из assets GitHub Release.
  2. Сверьте sha256sum файла с digest asset'а на странице релиза (или в https://api.github.com/repos/NodeFlow-dev/nodeflow/releases/tags/v2.0.0, поле digest).
  3. Откройте Настройки → Node Agent (подписанные релизы).
  4. Версия: 2.0.0.
  5. ОС: linux.
  6. Архитектура: amd64 или arm64 — по файлу.
  7. Выберите файл и нажмите «Загрузить и подписать».
+
  1. Скачайте nodeflow-node-agent-2.0.1-linux-amd64 (или -arm64) из assets GitHub Release.
  2. Сверьте sha256sum файла с digest asset'а на странице релиза (или в https://api.github.com/repos/NodeFlow-dev/nodeflow/releases/tags/v2.0.0, поле digest).
  3. Откройте Настройки → Node Agent (подписанные релизы).
  4. Версия: 2.0.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.0.0». Для нод, установленных версиями 1.0.x, см. раздел «Обновление с 1.0.x» в заметках к релизу 2.0.0 (однократный systemd drop-in для pipe-буферов ядра).

+

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

8

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

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

diff --git a/docs/releases/2.0.1.md b/docs/releases/2.0.1.md new file mode 100644 index 0000000..38dff43 --- /dev/null +++ b/docs/releases/2.0.1.md @@ -0,0 +1,16 @@ +# NodeFlow 2.0.1 + +Panel **2.0.1** · Node Agent **2.0.1** · HAProxy renderer **v21** + +Исправление к 2.0.0. + +## Исправлено + +- **«Толерантность» пропадала после сохранения** на маршрутах, где единственный сервер — домен в режиме DNS-пула (например, `co-de…` со всеми его A/AAAA-адресами), а первый сервер выбирается «Меньше соединений». Панель сохраняла такой маршрут в старом формате одиночного маршрута, в котором нет полей алгоритма и толерантности, и при следующем открытии поле было пустым. Теперь маршрут остаётся пулом серверов: толерантность сохраняется, а агент каждые 5 с выравнивает веса адресов пула. +- Маршруты без толерантности и с закреплением по хешу IP клиента не меняются: конфигурация HAProxy для них рендерится байт в байт как в 2.0.0. + +## Обновление + +Повторно запустите `install.sh` или обновите образ до `ghcr.io/nodeflow-dev/nodeflow-panel:2.0.1`. Миграций нет. Node Agent обновлять не обязательно — 2.0.0 уже умеет толерантность; 2.0.1 можно назначить в «Настройки → Node Agent» для единой версии. + +После обновления откройте затронутые маршруты, снова задайте толерантность и сохраните. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f8c3015..a5d35da 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "nodeflow-frontend", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nodeflow-frontend", - "version": "2.0.0", + "version": "2.0.1", "dependencies": { "@mantine/core": "^8.3.10", "@mantine/hooks": "^8.3.10", diff --git a/frontend/package.json b/frontend/package.json index eff11f6..782d69a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "nodeflow-frontend", "private": true, - "version": "2.0.0", + "version": "2.0.1", "type": "module", "scripts": { "dev": "vite", diff --git a/internal/panel/route_single_pool_tolerance_test.go b/internal/panel/route_single_pool_tolerance_test.go new file mode 100644 index 0000000..6a78497 --- /dev/null +++ b/internal/panel/route_single_pool_tolerance_test.go @@ -0,0 +1,68 @@ +package panel + +import ( + "strings" + "testing" +) + +// A single DNS-pool server with source_table + leastconn tolerance must keep +// the tolerance (it used to collapse to the legacy single-target route and +// silently drop balance_algorithm and the tolerance). +func TestValidateRouteSingleDNSPoolKeepsLeastConnTolerance(t *testing.T) { + port, enabled, check := 443, true, true + tolerance := 0.2 + in := routeInput{ + Name: "de", ListenerIP: "*", ListenerPort: &port, MatchMode: "sni", + SNIs: []string{"co-de.example.com"}, ProxyProtocol: "v2", QuotaAction: "observe", + Enabled: &enabled, HealthCheck: &check, BalanceMode: "pool", + StickyMode: StickyModeSourceTable, BalanceAlgorithm: BalanceAlgorithmLeastConn, + BalanceTolerance: &tolerance, + Servers: []routeServerInput{{Name: "de", TargetType: "tcp", Host: "co-de.example.com", Port: 443, DNSPool: true}}, + } + spec, err := validateRoute(in, true) + if err != nil { + t.Fatal(err) + } + if len(spec.Servers) != 1 || !spec.Servers[0].DNSPool { + t.Fatalf("servers = %+v, want one explicit DNS-pool server", spec.Servers) + } + if spec.BalanceAlgorithm != BalanceAlgorithmLeastConn || spec.LeastPingTolerance != 0.2 { + t.Fatalf("algorithm=%q tolerance=%v, want leastconn 0.2", spec.BalanceAlgorithm, spec.LeastPingTolerance) + } + if !specNeedsLeastConnAgent(spec) { + t.Fatal("tolerance route must require the leastconn Agent controller") + } + + // Without a tolerance the same input stays the byte-identical legacy route. + in.BalanceTolerance = nil + in.BalanceAlgorithm = "" + spec, err = validateRoute(in, true) + if err != nil { + t.Fatal(err) + } + if len(spec.Servers) != 0 || !spec.DNSPool { + t.Fatalf("plain single DNS pool must stay legacy: servers=%d dns_pool=%v", len(spec.Servers), spec.DNSPool) + } + + // sticky source ignores algorithm and tolerance: stays legacy too. + in.StickyMode, in.BalanceTolerance = StickyModeSource, &tolerance + spec, err = validateRoute(in, true) + if err != nil { + t.Fatal(err) + } + if len(spec.Servers) != 0 { + t.Fatalf("source-hash single DNS pool must stay legacy, got %d servers", len(spec.Servers)) + } +} + +func TestSingleDNSPoolToleranceRendersAgentManagedTemplate(t *testing.T) { + _, got := renderPayload(t, `{"name":"de","match_mode":"fallback","balance_mode":"pool","balance_algorithm":"leastconn","sticky_mode":"source_table","balance_tolerance":0.2, + "servers":[{"name":"de","host":"co-de.example.com","port":443,"dns_pool":true}]}`) + backend := backendSection(got.Config) + if !strings.Contains(backend, "algo=leastconn tolerance=0.20") { + t.Fatalf("missing Agent tolerance annotation:\n%s", backend) + } + if !strings.Contains(backend, "server-template ") || !strings.Contains(backend, "stick on src") { + t.Fatalf("want DNS-pool server-template with source_table stickiness:\n%s", backend) + } +} diff --git a/internal/panel/route_validation.go b/internal/panel/route_validation.go index 64d9f14..266b37b 100644 --- a/internal/panel/route_validation.go +++ b/internal/panel/route_validation.go @@ -253,7 +253,8 @@ func validateRoute(in routeInput, requireEnabled bool) (RouteSpec, error) { } in.TargetType = primary.TargetType in.TargetHost, in.TargetPort, in.UnixSocketPath = primary.Host, primary.Port, primary.UnixSocketPath - if len(servers) == 1 && primary.PreferredIP == "" && primary.Weight == 0 && primary.Cost == 0 && len(primary.IPWeights) == 0 { + if len(servers) == 1 && primary.PreferredIP == "" && primary.Weight == 0 && primary.Cost == 0 && len(primary.IPWeights) == 0 && + !(primary.DNSPool && poolDistributionNeedsServers(in)) { // A single server without preferred_ip, weight, cost or // ip_weights is exactly the legacy single-target route. Store it // that way so the backend keeps the canonical nf_srv_ server @@ -261,6 +262,10 @@ func validateRoute(in routeInput, requireEnabled bool) (RouteSpec, error) { // stays byte-identical to routes created before servers[] // existed. routes has no weight/cost/ip_weights columns of its // own, so a server that sets any of them must stay explicit. + // A single DNS pool balances between its addresses: when the + // client asks for a tolerance or an algorithm the legacy path + // cannot express, it stays a servers[] route so the setting is + // kept instead of being silently dropped. in.DNSPool = primary.DNSPool servers = nil } @@ -567,6 +572,45 @@ const ( maxSlowstartSeconds = 600 ) +// poolDistributionNeedsServers reports whether the requested distribution +// of a single-server route can only be expressed on the servers[] path: +// a leastconn tolerance, leastping, random, static-rr or an algorithm other +// than the one the legacy single-target render implies. With sticky_mode +// source the algorithm and tolerance are inert, so nothing is lost there. +func poolDistributionNeedsServers(in routeInput) bool { + sticky := strings.ToLower(strings.TrimSpace(in.StickyMode)) + if sticky == StickyModeSource || sticky == "" { + // '' on a DNS pool resolves to source (see resolvePoolDistribution): + // the source hash ignores algorithm and tolerance. + return false + } + tolerance := 0.0 + if in.LeastPingTolerance != nil { + tolerance = *in.LeastPingTolerance + } + if in.BalanceTolerance != nil { + tolerance = *in.BalanceTolerance + } + if tolerance > 0 || in.LeastPingToleranceMS > 0 { + return true + } + algorithm := strings.ToLower(strings.TrimSpace(in.BalanceAlgorithm)) + switch sticky { + case StickyModeLeastConn: + sticky, algorithm = StickyModeNone, BalanceAlgorithmLeastConn + case StickyModeRoundRobin: + sticky, algorithm = StickyModeNone, BalanceAlgorithmRoundRobin + } + if algorithm == "" { + return false + } + legacyDefault := BalanceAlgorithmRoundRobin + if sticky == StickyModeSourceTable { + legacyDefault = BalanceAlgorithmLeastConn + } + return algorithm != legacyDefault +} + func validBalanceAlgorithm(value string) bool { switch value { case "", BalanceAlgorithmRoundRobin, BalanceAlgorithmStaticRR, BalanceAlgorithmRandom, BalanceAlgorithmLeastConn, BalanceAlgorithmLeastPing: diff --git a/nodeflow.env.example b/nodeflow.env.example index ed6d6a9..443e690 100644 --- a/nodeflow.env.example +++ b/nodeflow.env.example @@ -4,7 +4,7 @@ # placeholders (openssl rand -hex 24 / -hex 32). # Panel image tag (ghcr.io/nodeflow-dev/nodeflow-panel:). -NODEFLOW_VERSION=2.0.0 +NODEFLOW_VERSION=2.0.1 POSTGRES_DB=nodeflow POSTGRES_USER=nodeflow diff --git a/scripts/test-install.sh b/scripts/test-install.sh index 4796ca1..3098b9a 100755 --- a/scripts/test-install.sh +++ b/scripts/test-install.sh @@ -237,7 +237,7 @@ make_release() { version=$1 kit=NodeFlow-Panel-$version-Agent-$version-install-kit rm -rf "$rel" "$work/kit-src" && mkdir -p "$rel" "$work/kit-src/$kit" - sed "s/NODEFLOW_VERSION:-2\.0\.0/NODEFLOW_VERSION:-$version/" \ + sed "s/NODEFLOW_VERSION:-[0-9][0-9.]*}/NODEFLOW_VERSION:-$version}/" \ "$root/compose.release.yaml" > "$expected_compose" if [ "${LAYOUT:-}" = legacy ]; then mkdir -p "$work/kit-src/$kit/01-PANEL"