Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,33 @@ RUN apk add --no-cache curl \
&& rm -f /tmp/asn-prefixes-lmdb.tar.gz


# ----- veil core -----------------------------------------------------
# Pulls the platform-matching `veil` static binary from the upstream
# release artefact set (linux/amd64 + arm64). The image multiplexes
# on TARGETARCH so a `docker buildx build --platform linux/arm64,...`
# bundle picks the right artefact without per-arch conditionals.
FROM alpine:3.21 AS veil

ARG VEIL_CORE_VERSION=v0.1.0-alpha.1
ARG VEIL_REPO=redstone-md/veil
ARG TARGETARCH

RUN apk add --no-cache curl ca-certificates \
&& case "${TARGETARCH}" in \
amd64) VEIL_ARCH=amd64 ;; \
arm64) VEIL_ARCH=arm64 ;; \
*) echo "unsupported TARGETARCH=${TARGETARCH}" && exit 1 ;; \
esac \
&& curl -fsSL "https://github.com/${VEIL_REPO}/releases/download/${VEIL_CORE_VERSION}/veil-linux-${VEIL_ARCH}" \
-o /usr/local/bin/veil \
&& chmod +x /usr/local/bin/veil \
&& /usr/local/bin/veil --version


FROM node:24.15-alpine

LABEL org.opencontainers.image.title="Remnawave Node"
LABEL org.opencontainers.image.description="Remnawave Node with built-in XRay Core"
LABEL org.opencontainers.image.description="Remnawave Node with built-in XRay Core and Veil"
LABEL org.opencontainers.image.url="https://github.com/remnawave/node"
LABEL org.opencontainers.image.source="https://github.com/remnawave/node"
LABEL org.opencontainers.image.vendor="Remnawave"
Expand All @@ -45,24 +68,33 @@ COPY --from=xray /usr/local/share/xray/geoip.dat /usr/local/share/xray/geoip.dat
COPY --from=xray /usr/local/share/xray/geosite.dat /usr/local/share/xray/geosite.dat
COPY --from=xray /usr/local/share/asn /usr/local/share/asn

# Bundle Veil binary from the dedicated stage above.
COPY --from=veil /usr/local/bin/veil /usr/local/bin/veil

COPY supervisord.conf /etc/supervisord.conf
COPY docker-entrypoint.sh /usr/local/bin/

RUN apk add --no-cache supervisor libnftnl libmnl \
&& mkdir -p /var/log/supervisor \
&& mkdir -p /var/log/supervisor /etc/veil /var/lib/veil \
&& chmod +x /usr/local/bin/docker-entrypoint.sh /opt/app/dist/cli.js \
&& ln -s /usr/local/bin/xray /usr/local/bin/rw-core \
&& ln -s /opt/app/dist/cli.js /usr/local/bin/cli \
&& printf '#!/bin/sh\ntail -n +1 -f /var/log/supervisor/xray.out.log\n' > /usr/local/bin/xlogs \
&& printf '#!/bin/sh\ntail -n +1 -f /var/log/supervisor/xray.err.log\n' > /usr/local/bin/xerrors \
&& chmod +x /usr/local/bin/xlogs /usr/local/bin/xerrors
&& printf '#!/bin/sh\ntail -n +1 -f /var/log/supervisor/veil.out.log\n' > /usr/local/bin/vlogs \
&& printf '#!/bin/sh\ntail -n +1 -f /var/log/supervisor/veil.err.log\n' > /usr/local/bin/verrors \
&& chmod +x /usr/local/bin/xlogs /usr/local/bin/xerrors /usr/local/bin/vlogs /usr/local/bin/verrors

ENV NODE_ENV=production
ENV NODE_OPTIONS="--max-http-header-size=65536"
ENV UV_THREADPOOL_SIZE=24

ENV XRAY_JSON_STRICT=true
# Surfaced to VeilService via process.env. Bumped automatically by
# the release pipeline when --build-arg VEIL_CORE_VERSION changes.
ENV VEIL_CORE_VERSION=${VEIL_CORE_VERSION}
ENV VEIL_BINARY_PATH=/usr/local/bin/veil

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

CMD ["node", "dist/main.js"]
CMD ["node", "dist/main.js"]
1 change: 1 addition & 0 deletions libs/contract/api/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './handler';
export * from './plugin';
export * from './stats';
export * from './veil';
export * from './xray';
7 changes: 7 additions & 0 deletions libs/contract/api/controllers/veil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const VEIL_CONTROLLER = 'veil' as const;

export const VEIL_ROUTES = {
START: 'start',
STOP: 'stop',
NODE_HEALTH_CHECK: 'healthcheck',
} as const;
5 changes: 5 additions & 0 deletions libs/contract/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export const REST_API = {
STOP: `${ROOT}/${CONTROLLERS.XRAY_CONTROLLER}/${CONTROLLERS.XRAY_ROUTES.STOP}`,
NODE_HEALTH_CHECK: `${ROOT}/${CONTROLLERS.XRAY_CONTROLLER}/${CONTROLLERS.XRAY_ROUTES.NODE_HEALTH_CHECK}`,
},
VEIL: {
START: `${ROOT}/${CONTROLLERS.VEIL_CONTROLLER}/${CONTROLLERS.VEIL_ROUTES.START}`,
STOP: `${ROOT}/${CONTROLLERS.VEIL_CONTROLLER}/${CONTROLLERS.VEIL_ROUTES.STOP}`,
NODE_HEALTH_CHECK: `${ROOT}/${CONTROLLERS.VEIL_CONTROLLER}/${CONTROLLERS.VEIL_ROUTES.NODE_HEALTH_CHECK}`,
},
STATS: {
GET_USER_ONLINE_STATUS: `${ROOT}/${CONTROLLERS.STATS_CONTROLLER}/${CONTROLLERS.STATS_ROUTES.GET_USER_ONLINE_STATUS}`,
GET_USERS_STATS: `${ROOT}/${CONTROLLERS.STATS_CONTROLLER}/${CONTROLLERS.STATS_ROUTES.GET_USERS_STATS}`,
Expand Down
1 change: 1 addition & 0 deletions libs/contract/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './handler';
export * from './plugin';
export * from './stats';
export * from './veil';
export * from './xray';
24 changes: 24 additions & 0 deletions libs/contract/commands/veil/get-node-health-check.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from 'zod';

import { REST_API } from '../../api';

export namespace GetNodeHealthCheckVeilCommand {
export const url = REST_API.VEIL.NODE_HEALTH_CHECK;

export const RequestSchema = z.object({}).strict();
export type Request = z.infer<typeof RequestSchema>;

export const ResponseSchema = z.object({
response: z.object({
/** Node-process self-reports as alive. */
isNodeOnline: z.boolean(),
/** veil daemon is running and its admin API responds. */
isVeilOnline: z.boolean(),
/** Reported by `veil --version`; null when the binary is missing. */
veilVersion: z.string().nullable(),
/** This Remnawave Node package's version. */
nodeVersion: z.string(),
}),
});
export type Response = z.infer<typeof ResponseSchema>;
}
3 changes: 3 additions & 0 deletions libs/contract/commands/veil/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './get-node-health-check.command';
export * from './start.command';
export * from './stop.command';
55 changes: 55 additions & 0 deletions libs/contract/commands/veil/start.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { z } from 'zod';

import { NodeSystemSchema } from '../../models';
import { REST_API } from '../../api';

export namespace StartVeilCommand {
export const url = REST_API.VEIL.START;

/**
* StartVeilCommand drives the local veil-server process via
* supervisord. The panel pushes the full server.yaml as a string
* (so future schema bumps don't require a node-side rebuild) plus
* the bind address of the embedded admin API the panel will poll
* for health.
*/
export const RequestSchema = z.object({
internals: z.object({
forceRestart: z.boolean().default(false),
/**
* SHA-256 of the full server.yaml the panel intends to
* push. The node uses it to short-circuit a restart when
* the running config matches.
*/
configHash: z.string(),
}),
/**
* server.yaml verbatim. Validated by the veil binary at start
* time; we do not re-parse it here because the YAML schema
* lives in the veil core repo and would create a cross-
* project version coupling.
*/
serverConfig: z.string(),
/**
* Optional admin API address (host:port) to expose to the
* panel. Defaults to 127.0.0.1:9090 when omitted.
*/
adminAddr: z.string().optional(),
});

export type Request = z.infer<typeof RequestSchema>;

export const ResponseSchema = z.object({
response: z.object({
isStarted: z.boolean(),
version: z.string().nullable(),
error: z.string().nullable(),
nodeInformation: z.object({
version: z.string().nullable(),
}),
system: NodeSystemSchema,
}),
});

export type Response = z.infer<typeof ResponseSchema>;
}
17 changes: 17 additions & 0 deletions libs/contract/commands/veil/stop.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from 'zod';

import { REST_API } from '../../api';

export namespace StopVeilCommand {
export const url = REST_API.VEIL.STOP;

export const RequestSchema = z.object({}).strict();
export type Request = z.infer<typeof RequestSchema>;

export const ResponseSchema = z.object({
response: z.object({
isStopped: z.boolean(),
}),
});
export type Response = z.infer<typeof ResponseSchema>;
}
2 changes: 2 additions & 0 deletions src/modules/remnawave-node.modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NetworkStatsModule } from './network-stats/network-stats.module';
import { AsnLmdbModule } from './asn-lmdb/asn-lmdb.module';
import { HandlerModule } from './handler/handler.module';
import { PluginModule } from './_plugin/plugin.module';
import { VeilModule } from './veil-core/veil.module';
import { XrayModule } from './xray-core/xray.module';
import { StatsModule } from './stats/stats.module';

Expand All @@ -13,6 +14,7 @@ import { StatsModule } from './stats/stats.module';
NetworkStatsModule,
PluginModule,
StatsModule,
VeilModule,
XrayModule,
HandlerModule,
],
Expand Down
3 changes: 3 additions & 0 deletions src/modules/veil-core/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { StopVeilHandler } from './stop-veil';

export const COMMANDS = [StopVeilHandler];
2 changes: 2 additions & 0 deletions src/modules/veil-core/commands/stop-veil/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './stop-veil.command';
export * from './stop-veil.handler';
13 changes: 13 additions & 0 deletions src/modules/veil-core/commands/stop-veil/stop-veil.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Command } from '@nestjs/cqrs';

import { ICommandResponse } from '@common/types/command-response.type';

import { StopVeilResponseModel } from '../../models';

export class StopVeilCommand extends Command<ICommandResponse<StopVeilResponseModel>> {
constructor(
public readonly args: { withOnlineCheck?: boolean } = {},
) {
super();
}
}
20 changes: 20 additions & 0 deletions src/modules/veil-core/commands/stop-veil/stop-veil.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

import { ICommandResponse } from '@common/types/command-response.type';

import { StopVeilResponseModel } from '../../models';
import { VeilService } from '../../veil.service';
import { StopVeilCommand } from './stop-veil.command';

@CommandHandler(StopVeilCommand)
export class StopVeilHandler
implements ICommandHandler<StopVeilCommand, ICommandResponse<StopVeilResponseModel>>
{
constructor(private readonly veilService: VeilService) {}

async execute(command: StopVeilCommand): Promise<ICommandResponse<StopVeilResponseModel>> {
return this.veilService.stopVeil({
withOnlineCheck: command.args.withOnlineCheck ?? false,
});
}
}
7 changes: 7 additions & 0 deletions src/modules/veil-core/dtos/get-node-health-check.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createZodDto } from 'nestjs-zod';

import { GetNodeHealthCheckVeilCommand } from '@libs/contracts/commands';

export class GetNodeHealthCheckVeilResponseDto extends createZodDto(
GetNodeHealthCheckVeilCommand.ResponseSchema,
) {}
3 changes: 3 additions & 0 deletions src/modules/veil-core/dtos/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './get-node-health-check.dto';
export * from './start-veil.dto';
export * from './stop-veil.dto';
6 changes: 6 additions & 0 deletions src/modules/veil-core/dtos/start-veil.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createZodDto } from 'nestjs-zod';

import { StartVeilCommand } from '@libs/contracts/commands';

export class StartVeilRequestDto extends createZodDto(StartVeilCommand.RequestSchema) {}
export class StartVeilResponseDto extends createZodDto(StartVeilCommand.ResponseSchema) {}
5 changes: 5 additions & 0 deletions src/modules/veil-core/dtos/stop-veil.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createZodDto } from 'nestjs-zod';

import { StopVeilCommand } from '@libs/contracts/commands';

export class StopVeilResponseDto extends createZodDto(StopVeilCommand.ResponseSchema) {}
22 changes: 22 additions & 0 deletions src/modules/veil-core/models/get-node-health-check.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { GetNodeHealthCheckVeilCommand } from '@libs/contracts/commands';

export class GetNodeHealthCheckVeilResponseModel
implements GetNodeHealthCheckVeilCommand.Response['response']
{
public isNodeOnline: boolean;
public isVeilOnline: boolean;
public veilVersion: null | string;
public nodeVersion: string;

constructor(
isNodeOnline: boolean,
isVeilOnline: boolean,
veilVersion: null | string,
nodeVersion: string,
) {
this.isNodeOnline = isNodeOnline;
this.isVeilOnline = isVeilOnline;
this.veilVersion = veilVersion;
this.nodeVersion = nodeVersion;
}
}
3 changes: 3 additions & 0 deletions src/modules/veil-core/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './get-node-health-check.model';
export * from './start-veil.response.model';
export * from './stop-veil.response.model';
27 changes: 27 additions & 0 deletions src/modules/veil-core/models/start-veil.response.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { TNodeSystem } from '@libs/contracts/models';

interface INodeInformation {
version: string | null;
}

export class StartVeilResponseModel {
public isStarted: boolean;
public version: null | string;
public error: null | string;
public nodeInformation: INodeInformation;
public system: TNodeSystem;

constructor(
isStarted: boolean,
version: null | string,
error: null | string,
nodeInformation: INodeInformation,
system: TNodeSystem,
) {
this.isStarted = isStarted;
this.version = version;
this.error = error;
this.nodeInformation = nodeInformation;
this.system = system;
}
}
9 changes: 9 additions & 0 deletions src/modules/veil-core/models/stop-veil.response.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StopVeilCommand } from '@libs/contracts/commands';

export class StopVeilResponseModel implements StopVeilCommand.Response['response'] {
public isStopped: boolean;

constructor(isStopped: boolean) {
this.isStopped = isStopped;
}
}
Loading