Skip to content

Multi-node, payment registry, flexible registration — release 1.0.0 - #30

Merged
AppsGanin merged 74 commits into
mainfrom
feat/multinode
Jul 15, 2026
Merged

Multi-node, payment registry, flexible registration — release 1.0.0#30
AppsGanin merged 74 commits into
mainfrom
feat/multinode

Conversation

@AppsGanin

@AppsGanin AppsGanin commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Мультинода

Панель управляет удалёнными нодами (агент rospanel node, установка по SSH из UI, self-update, авто-broadcast panel-URL). Пер-серверная конфигурация: протоколы, routing/DNS, egress-бэкенды (proxy-lanes, WARP, Opera), REALITY-донор и ключи, Geo с авто-обновлением, просмотр логов ноды, health. Мульти-нод share-links / clash / singbox / subscription. Миграция 0026_multinode.

Платёжные провайдеры (реестр)

Захардкоженные колонки YooKassa/CryptoBot → реестр: один провайдер = один файл (Descriptor + Client), настройки/вебхук/поллинг — generic. Креды в таблице payment_providers (шифрование at-rest, миграция 0027). Новые: PayPalych, RioPay, RollyPay, SeverPay, Platega, PayPear, AuraPay (карты/СБП, ₽) и Heleket (крипта, счёт в ₽). Провайдеры без надёжной подписи вебхука подтверждаются ре-фетчем статуса. Кастомное название кнопки и выбор метода на провайдера.

Гибкая саморегистрация

Режимы вместо тумблера: закрыта / открыта / модерация / по коду (миграции 0028, 0029). Модерация = заявка (аккаунт не создаётся, доступа к боту нет до одобрения); одобрение/отклонение кнопками в админ-боте или на вкладке «Заявки». Атомарный claim от гонок, восстановление заявки при сбое. Telegram ID в карточке юзера. Дефолт для свежих установок — закрыта.

AppsGanin added 30 commits July 14, 2026 00:10
… timer stop, decoy on internal errors

Addresses adversarial review findings: deleted node now reliably revoked instead of
serving with live credentials; wake-registry no longer leaks channels; long-poll timer
stopped on early wake; pre-auth internal errors serve decoy to preserve stealth.
…yRaw

internal/nodeagent: join, long-poll sync loop with backoff, config apply (ACME/hop/
connguard/decoy), stats poll with inflight/pending idempotent batching. cmd/rospanel/
node.go: install/run/set-panel/status/uninstall + systemd unit. Cert paths use
sentinels the agent substitutes with its own absolute paths.
…ss nodes

sub.ShareLinksAll / ClashYAMLMulti / SingBoxJSONMulti iterate [local + each enabled,
connected node]; each node's links carry its name via Settings.NodeLabel. Manager
.NodeLinkSettings provides per-node settings clones; subscription handler spans them.
Single-server output is byte-identical (golden test).
…th section

Sync response gains Update (agent runs updater.Apply + restarts) and panel_url
broadcast when a node reaches a stale host (auto-heals a panel move; node set-panel is
the manual fallback). Panel/API endpoints POST /nodes/{id}/update + /nodes/update-all.
Health report gains a nodes section (offline / version-skew warnings).
internal/provision: SSH into a target (password or key), run the install command,
stream the log; host key fingerprint via trust-on-first-use, creds never stored.
POST /api/nodes/{id}/provision streams the install over SSE. NodesPanel add-node
dialog gains an 'Install over SSH' tab with a live log. Adds golang.org/x/crypto/ssh.
- provision: serialize SSE writes (stdout+stderr goroutines) — data race
- agent: back off on repeated config-apply failure (version skew no longer storms)
- nodes: enforce unique node names + dedup subscription labels (dup names broke Clash/sing-box)
- panel-URL broadcast: only a bare-domain :443 host (no IP/IPv6/port/localhost bricking) + agent validates the URL
- --insecure now persists and applies to the sync client, not just join
- ensureDecoy: swap handler on a live listener (no masquerade gap / relisten race)
- honest SSH TOFU comment; install.sh --join arg guard
…real cert

Add certLoop to the node agent (mirrors the panel's tlsLoop): retries ACME fast while
on a self-signed fallback, slow once a CA cert is in place, and reloads Xray when the
cert changes so the real Let's Encrypt cert is served the moment it's issued. The next
sync auto-reports the new fingerprint so the panel drops the pin from the node's links.
Cert-file writes serialized (certMu) so applyState and certLoop don't clash.
The SSH-provision endpoint is a POST (needs the creds in the body) and is audited, so
its ResponseWriter is wrapped by auditStatus, which forwards Flush only via Unwrap. The
direct type assertion in sseStart missed it → 'стриминг не поддерживается'. sseStart now
walks the Unwrap chain (regression test included).
…f-signed

A panel on a bare IP / self-signed cert can't be TLS-verified by a joining node, so
the join failed with x509 and the node never connected. nodeInstallCommand now appends
--insecure when the panel isn't on a CA-valid cert (copy-paste + SSH-provision + API);
a real-domain panel omits it. The node persists the flag and uses it for sync too.
… over SSH

Shown on any offline/never-connected remote node; re-runs the SSH install with a fresh
token and streams the log, which also surfaces why the node didn't connect.
…url|bash

The install command downloaded install.sh from main + the release binary — neither has
node support until the feature is merged/released, so the SSH install put a full PANEL
on the target instead of a node. Now the panel uploads its own (gzipped) binary over the
existing SSH connection and runs 'rospanel node install' with it: exact same build, no
GitHub dependency, works for unreleased builds. The node installer also stops a
conflicting rospanel PANEL service on the target (a box can't be both).
…polish

- xray.VersionMatchesPinned tolerates the 'v' prefix (xray reports 26.6.27, pinned is
  v26.6.27) — no more spurious ⚠ on every node
- NodeRoutingDialog: per-node routing (block/direct rules only — lanes/WARP/Opera don't
  apply on nodes) + DNS, each inherit-or-override; POST /api/nodes/{id}/routing
- NodeView carries the routing/dns overrides so the editor prefills
- remove 'Сбросить протоколы' button; fix install-modal wording (node already listed)
On a node everything egresses direct already (no proxy lanes to bypass), so the
'Напрямую' domain/IP rules were no-ops — leave only the block rules. Renamed the
control to 'Блокировки и DNS'.
…the fallback

A browser hitting a node's :443 negotiates HTTP/2 (Xray offers ALPN h2), and Xray
forwards it prior-knowledge over the plaintext fallback to the agent's decoy. The decoy
http.Server lacked UnencryptedHTTP2, so it couldn't parse the h2 frames →
ERR_HTTP2_PROTOCOL_ERROR and no masquerade. Enable HTTP/1.1 + h2c like the panel's own
server.
AppsGanin added 28 commits July 14, 2026 17:18
New geo_refresh_hours setting (0 = never): the panel re-downloads stale geo on a
timer and reloads Xray, and pushes the cadence to nodes (NodeMeta.GeoRefreshHours)
so each node's agent auto-refreshes its own geo on the same schedule and restarts
Xray to pick it up. Cadence is edited in the master card's Geo tab (Никогда / раз в
день / 3 дня / неделю); enabling it refreshes immediately if geo is already stale.
Migration 0029; GET /api/geo now returns {files, refresh_hours}; POST /api/geo/cadence.
…nodes)

REALITY keys were already per-node; this makes the DONOR (masquerade SNI) per-server
too and adds a regenerate-keys action on each server card.
- nodes.reality_dest (migration 0030): the node's own donor, empty ⇒ inherit the
  panel's. nodeSettings uses it when set. NodeView exposes each server's donor +
  public key / short id / gRPC service (private key stays hidden).
- SetNodeReality / SetMasterReality: validate the donor (master also live-probes a
  changed donor while REALITY is on), persist it, and optionally regenerate the
  server's REALITY keypair; nodes wake, the master reloads Xray.
- Endpoints POST /api/nodes/master-reality and /api/nodes/{id}/reality.
- UI: a REALITY section on the master and node cards — donor SNIs, the keys, and a
  'перегенерировать ключи' button (confirmed; it invalidates that server's links).
… Подключения

- Node Geo tab: the agent auto-refreshes geo on the fleet cadence; a 'Обновить
  сейчас' button flags an immediate refresh (RequestNodeGeoRefresh → sync response
  RefreshGeo → agent geo.Refresh + Xray restart). New POST /api/nodes/{id}/geo-refresh.
- ConnectionsPanel: removed the REALITY donor (SNI), key display and regenerate
  button — REALITY donor + keys are per-server on the «Сервера» cards now. The port
  and anti-replay (transport) stay.
Groundwork for full per-node connections. nodes.connections_config (migration 0031)
holds a node's own transport (WS path, ports, hop range/interval, REALITY port +
anti-replay, uTLS fingerprints, connection names, anti-DPI) as a JSON blob; empty ⇒
inherit the master's. nodeSettings applies it, and NodeMeta now derives hop/hysteria
ports from the node's effective settings (ns) rather than the master's.

- buildConnectionsStatus(set) factored out of ConnectionsInfo so a node reuses it.
- NodeConnectionsInfo / ApplyNodeConnections (protocols + REALITY donor/keys +
  transport, syntactic validation; the node's own xray -test is the backstop).
- store: SetNodeConnections, SetNodeProtocols; scan the blob.
- Endpoints GET/POST /api/nodes/{id}/connections. Inert: no frontend calls them yet
  and an unconfigured node behaves exactly as before.
…чения tab

Every server (master + each node) now edits ALL its connection settings — protocols
on/off, names, uTLS fingerprints, ports, port-hopping, WS path, REALITY
(donor/keys/regen/port/anti-replay) and anti-DPI — in a «Подключения» tab on its card.

- ConnectionsEditor: the connections UI refactored into a controlled, scope-aware
  component (injected load/save; inline save instead of a SaveBar so it fits the card
  modal). Master → global connections (restarts the panel Xray); node → its own blob
  (applied by the node, no panel restart).
- Card «Основное» slimmed to name/label + decoy; the separate Протоколы and REALITY
  sections (and their endpoints' UI) folded into Подключения.
- Global Подключения settings tab removed; ConnectionsPanel.tsx deleted.
…ence, node domain like master

- ConnectionsEditor + TLSPanel: flat bordered blocks (bg-gray-50/60) instead of the
  heavy shadowed/dimmed Cards, so they match the modal's other sections.
- Server dialogs: the footer Save shows only on tabs it persists (Основное/Роутинг);
  the Подключения/Geo/Домен tabs have their own save, so no more double 'Сохранить'.
- Nodes report their cert issuer + expiry (agent reads its cert); the node Домен tab
  now shows «выдан <issuer> · ещё N дн.» like the master's.
- Per-node geo cadence: each node has its OWN geo auto-refresh schedule (migration
  0033), edited in its Geo tab; NodeMeta pushes the node's own cadence. Master keeps
  its own. Migration 0032 adds the node cert columns.
- Fixed stale text (geo desc, restore.tsx 'Настройки → Домен' → the master card).
…el/GeoSection)

- Per-node ACME: each node can pick its own CA provider (Let's Encrypt / ZeroSSL) +
  e-mail; the panel fetches ZeroSSL EAB and pushes provider/email/EAB via NodeMeta
  (migration 0034). Empty ⇒ inherit the panel's. GET/POST /api/nodes/{id}/tls +
  NodeTLSStatus/SetNodeACME (builds cert info from the node's report).
- TLSPanel is now scope-aware (load/save injected, redirectOnSuccess flag): the node
  Домен tab reuses the exact master UI (address + cert issuer/days + change form with
  provider choice), re-issuing the node's cert without a panel redirect.
- Nodes report their geo file status every sync; GET /api/nodes/{id}/geo. The node
  Geo tab now reuses GeoSection (file sizes/dates + cadence + refresh), like the master.
- Connections editor: save/cancel bar always visible (disabled until dirty).
Every tab (Основное/Роутинг/DNS) now saves on its own and stays open,
matching Подключения/Geo/Домен, with a note that unsaved edits won't
apply. Removes the shared footer Save and the save-then-close behaviour.
- New POST /api/nodes/{id}/dns (SetNodeDNS) so a node's DNS saves on its
  own; the routing endpoint no longer carries DNS and preserves it.
- TabSaveBar gains 'Отменить' (revert to last-saved) + dirty-gated Save,
  matching ConnectionsEditor; note clarifies each section saves separately.
- useServerRouting tracks a baseline for dirty/revert/commit; DnsEditor
  re-syncs when its value is reset externally.
Backend:
- agent: persist report-id watermark in state.json and resume it on
  restart — a restarted node no longer has its traffic silently dropped
  by the panel's forward-only watermark (was: accounting/quota broke
  after the first self-update/reboot). Panel: don't ack a report the DB
  failed to record, so the node resends instead of losing it.
- nodeSettings: null the master-only proxy-mode inbound + credentials so
  they never leak into a node's config.
- IngestNodeSync: reject a disabled node's traffic/device/status ingest
  up front (its token still authenticates only to be told to stop).
- NodeLinkSettings: skip a self-signed node with no reported cert hash
  (unpinnable link fails silently); disambiguate a node whose name
  collides with the master's config label.
- ConsumeJoinToken: atomic conditional UPDATE (re-asserts the token
  hash) so a concurrent consume / racing regen can't double-mint or
  clobber a revoke.
- provision: issue the install token WITHOUT revoking the node's
  permanent token, so a failed SSH re-provision can't down a live node.
- node API: read deadline on join/sync bodies (slow-loris hardening).
- /v1 node mutations now land in the admin audit trail.
- regen-join: 404 instead of a raw sql 500.
- updateNode preserves protocols when omitted (no revert of a Подключения edit).
- agent: prune stale per-user counters; self-update honours the agent ctx.

Frontend:
- node Основное save no longer echoes (stale) protocol flags — omits them.
- SSH install reuses the created node id on retry (no orphan nodes).
- master routing tab falls back to the node's own routing if the live
  fetch fails (a save can't wipe real rules).
- DNS dirty-tracking seeded canonically; geo cadence rolls back on error;
  SSH dialogs non-dismissible mid-install; SSE flushes a trailing frame.

Tests: report-id persistence across restart, proxy-mode non-leak,
IssueJoinToken keeps the permanent token.
…nique node name

- TestRoutingPropagatesMasterAndNode: master routing lands in the master
  config, a node's own routing lands in the exact JSON the panel pushes
  (NodeDesiredState), neither inherits the other's rule.
- migration 0035: partial unique index on lower(name) WHERE deleted_at=0
  as a DB backstop to the app-level NodeNameTaken check (a raced create/
  rename could otherwise duplicate a name and collide Clash/sing-box tags).
  Store returns ErrNodeNameTaken → manager maps to a validation error.
  Name is reusable after a node is deleted (partial index over live rows).
- Servers page is now a single divided list of compact 2-line rows
  (status dot + name + state chip + host, then inline meta), replacing
  the tall per-server cards. Denser, scales to a larger fleet.
- 'Обновить все ноды' shows whenever there are remote nodes (was hidden
  unless one was behind); a ⚠ marks that an update is available.
…on dark

- Master name+decoy don't touch the Xray config, so saving them must not
  go through useXrayApply (which blocks polling for an Xray restart that
  never happens → ~2min hang). Use a plain save with its own busy state,
  matching the node dialog. Routing/DNS keep the apply-wait (they reload Xray).
- Dropdown menu: border-gray-300 + shadow-xl so it reads as a distinct
  panel when overlaying a same-coloured card surface on the dark theme.
…lts weekly

- Fold 0026–0036 (nodes table, master_label, per-node protocols/reality/
  connections/cert-info/geo/acme, unique-name index, geo weekly default)
  into one 0026_multinode.sql for a clean feature migration. Verified the
  resulting schema is column-identical to the incremental one.
- Geo auto-refresh defaults to weekly (168h): baked into the schema and set
  explicitly in CreateNode (defaultGeoRefreshHours) so new nodes are weekly
  even on installs whose nodes table predates the default.
- Node/install log surfaces use theme-aware bg-gray-50 + text-danger/warning/
  success (was bg-gray-900, which the ramp flips light on the dark theme).
Replace the hardcoded YooKassa/CryptoBot columns with a provider registry: each
provider is one self-contained file (Descriptor + Client) and the settings API,
webhook route and polling fallback are generic — adding a provider needs no UI
change. Credentials move to a payment_providers table, encrypted at rest.

New: PayPalych, RioPay, RollyPay, SeverPay, Platega, PayPear, AuraPay (cards/SBP,
RUB) and Heleket (crypto, RUB invoice). Providers whose webhook signature is
absent or fragile are confirmed by re-fetching status over the authenticated API
instead of trusting the callback body. Per-provider custom pay-button name and
payment-method selection (Platega/PayPear/AuraPay default to the provider's own
method picker). Includes shared model/settings/api groundwork.
Replace the on/off registration toggle with modes: closed, open, moderation and
invite-code (Settings -> Telegram). Moderation records a pending request — no user
is created and the bot grants no access — that an admin approves or rejects via
admin-bot buttons or the new "Заявки" tab (shown only in moderation mode); the
account is created, plan applied and chat linked only on approval. Invite mode
gates signup behind a code. The link-by-code flow from the user card is unchanged
and works in every mode. The user card's Telegram section now shows the linked
Telegram ID.
Review follow-ups on the moderation flow:
- Atomic ClaimRegistrationRequest gates approval/rejection so a double-click or two
  admins (bot + panel) can't create the account twice or send contradictory pings.
- Approval restores the request (and removes any orphaned account) if user creation
  or chat-linking fails after the claim, instead of losing the signup.
- Guard against a duplicate when the chat got linked (via a panel code) while the
  request was pending.
- A re-tap in moderation short-circuits before the global rate limit, so it can't be
  burned to block other signups.
- Drop the dead admin-alert fallback (it could never fire); the panel queue is the
  authoritative surface. Reject an empty stored invite code. Rename the notifier's
  reqID param (was misleadingly userID).
Flip the tg_user_reg_enabled default (0005) from 1 to 0 so a brand-new database
seeds registration mode 'off' (via the 0028 backfill) — enabling the user bot no
longer opens signups by accident. The migration runner keys on file name, so this
only affects databases that never ran 0005; existing installs keep their mode.
Covered by TestFreshInstallRegistrationClosed.
Release-As: 1.0.0
@AppsGanin
AppsGanin merged commit 6aa1b45 into main Jul 15, 2026
4 checks passed
@AppsGanin
AppsGanin deleted the feat/multinode branch July 15, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant