Backend counterpart to the matching feat(veil-core) PR on
remnawave/node. Lets operators register a node whose proxy daemon
is Veil (https://github.com/redstone-md/veil) instead of Xray; the
queue processors that drive node lifecycle dispatch on a new
Nodes.core enum.
What ships
==========
* Prisma migration 20260512000000_add_node_core
- new enum node_core { XRAY, VEIL }
- new column Nodes.core (NOT NULL DEFAULT 'XRAY', indexed)
Existing rows default to XRAY → migration is a no-op for every
existing fleet. Direct INSERTs (admin scripts, seed) keep
working without specifying the column.
* NodesEntity.core: 'XRAY' | 'VEIL' alongside the existing fields.
* AxiosService gains three counterparts to startXray/stopXray/
getNodeHealth:
- startVeil(data, url, port)
- stopVeil(url, port)
- getVeilNodeHealth(url, port)
Same zstd-compressed POST shape as startXray, same TResult<>
return surface, same ERRORS map.
* StartNodeProcessor branches at node.core: XRAY path is unchanged;
VEIL path probes /node/veil/healthcheck, generates server.yaml,
pushes via /node/veil/start, updates the node row + emits the
same NODE.CONNECTED / NODE.DISCONNECTED events the Xray path does.
* StopNodeProcessor branches the same way (stopVeil vs stopXray).
* Inline shim src/common/veil-contract/veil-commands.ts holds the
Zod schemas for the three commands until @remnawave/node-contract
is published with the matching namespaces (the node-side PR is
also Draft today). Once node-contract@>=2.8.0 ships, the shim is
deleted and the imports collapse to a single line.
Deliberately scoped out of this PR
==================================
* server.yaml generator is a deliberate stub. buildVeilServerConfig
emits a known-good three-transport skeleton on canonical ports so
end-to-end connectivity is verifiable; mapping the operator's
existing inbounds + Reality decoy origins / mimicry profiles /
decoy traffic into the Veil schema lands in a follow-up after
the upstream Veil schema stabilises post-audit.
* Per-user CRUD on VEIL-cored nodes. Veil exposes /api/users on
the admin port; the existing handler/* processors hit Xray's
gRPC user-add path which doesn't apply. A VeilUserSyncProcessor
comes in the same follow-up that fleshes out the YAML generator.
* Frontend (remnawave/frontend) — separate repo, separate PR, no
changes here.
Backwards compatibility
=======================
Pure addition. All existing Nodes default core=XRAY → every
processor branch falls through to the unchanged Xray path. Nodes
the operator hasn't migrated to VEIL see zero behavioural change.
Removing this column or dropping the new axios methods has no
runtime effect on a fleet that hasn't onboarded a VEIL node.
Marked Draft because:
1. The node-side counterpart (remnawave/node feat(veil-core))
is also Draft, and node-contract isn't published yet.
2. Veil itself is pre-alpha (v0.1.0-alpha.1) and pending its
first external audit; we don't want operators landing on
this without that context.
Summary
Backend counterpart to remnawave/node#38 (also Draft). Lets operators register a node whose proxy daemon is Veil instead of Xray. The queue processors that drive node lifecycle dispatch on a new
Nodes.coreenum (XRAY|VEIL); existing fleets keep their behaviour unchanged because every existing row defaults tocore='XRAY'.What ships
Schema
Migration
20260512000000_add_node_core/migration.sql— additive, NOT NULL DEFAULT 'XRAY' + index on the column for fast per-node lookups.Axios facade (
src/common/axios/axios.service.ts) — three counterparts to the existingstartXray/stopXray/getNodeHealth:startVeil(data, url, port)— POST/node/veil/startwith the same zstd-compressed body shape Xray usesstopVeil(url, port)— GET/node/veil/stopgetVeilNodeHealth(url, port)— GET/node/veil/healthcheckSame
TResult<>return surface, sameERRORS.NODE_ERROR_WITH_MSGmapping, same JWT-boundaxiosInstancedefaults — no new HTTP plumbing.Processors
StartNodeProcessor— branches atnode.core. The Xray happy-path is byte-identical to today; the VEIL branch goes through a new privatestartVeilNode()that probes/node/veil/healthcheck, generatesserver.yaml, pushes via/node/veil/start, updates the node row + emits the sameNODE.CONNECTED/NODE.DISCONNECTEDevents as the Xray path.StopNodeProcessor— branches the same way (stopVeilvsstopXray).Inline contract shim (
src/common/veil-contract/veil-commands.ts) — Zod schemas for the three Veil commands. Mirrors what@remnawave/node-contractwill expose once the node-side PR lands a new release. The shim is documented as throwaway: a single comment block in the file lists the imports to swap once the package is on npm.Deliberately scoped out of this PR
server.yamlgenerator that maps the operator's existing inbounds + Reality decoy origins / mimicry profiles / decoy traffic into Veil's schemabuildVeilServerConfigships a known-good three-transport skeleton on canonical ports so end-to-end connectivity is verifiable; richer mapping lands in a follow-up./api/userson its admin port; the existinghandler/*processors use Xray's gRPC user-add path. AVeilUserSyncProcessorlands in the same follow-up that fleshes out the YAML generator.remnawave/frontend, separate PR./node/stats/*is already core-agnostic (system metrics, not Xray-specific) so no change needed in this PR.Backwards compat
Pure addition. Every existing
Nodesrow migrates tocore='XRAY'. Each branch in the touched processors falls through to the unchanged Xray path for those rows. An operator who never registers a VEIL node sees zero behavioural change. Rolling this out to a fleet that hasn't onboarded VEIL is a no-op deploy.Test plan
The repo's existing test infra is integration-shaped (Postgres + Redis + node mocks); rather than spinning up a half-stub I exercised this against a live setup:
npx prisma migrate deployagainst a fresh Postgres — migration applies cleanly,SELECT core FROM nodesreturnsXRAYfor pre-migration rows.core='VEIL'(manual UPDATE for now until the frontend PR lands the dropdown).StartNodeProcessorpicks the VEIL branch, probes/node/veil/healthcheck, posts/node/veil/startwith the stubserver.yaml, node flips toisConnected=true.StopNodeProcessorcallsstopVeil, supervisord on the node side stops theveilprogram, node row clears.startXraypath (regression check).File map
Total: 7 files changed, +369 / -3.
Notes for reviewers
@modules/@common/@libsimport groups,pRetry+AxiosErrorfailure surface, same logger / event-emitter shapes the Xray processor uses.zodand the SHA-256 helper usesnode:crypto, both already in the dep tree.@remnawave/node-contractships the real namespaces, the veil-contract shim folder gets deleted in a one-line PR (just swap the import).