Skip to content

Add WARP control endpoints for nodes#40

Open
RickeyRen wants to merge 13 commits into
remnawave:mainfrom
RickeyRen:codex/warp-node-control-upstream
Open

Add WARP control endpoints for nodes#40
RickeyRen wants to merge 13 commits into
remnawave:mainfrom
RickeyRen:codex/warp-node-control-upstream

Conversation

@RickeyRen

@RickeyRen RickeyRen commented Jun 3, 2026

Copy link
Copy Markdown

Summary

  • add node-side WARP status plus install, enable, disable, and uninstall lifecycle controls
  • report host IPv4/IPv6 separately from WARP IPv4/IPv6 so WARP routes do not pollute host connectivity
  • keep WARP transport on a stable IPv4 endpoint while enabling dual-stack WARP trace checks
  • run wgcf update after registration so fresh one-click installs reliably handshake

Verification

  • node --test test/warp-status.test.mjs
  • npm run build
  • npm run lint
  • Built and pushed ghcr.io/rickeyren/remnawave-node:warp-node-control@sha256:957eea53bfbdcb8446867ebe0e4a1054a8d4acc96b2aecada5426702be7f178e
  • Deployed the image to 4 production test nodes via docker pull/recreate, without remote compilation
  • Verified WARP IPv4 and IPv6 warp=on on HK, GB, Bandwagon, and RackNerd
  • Verified host IPv4/IPv6 probes stay warp=off; Bandwagon correctly reports no native host IPv6
  • From the panel, fully uninstalled Bandwagon WARP, then installed and enabled it from zero; final WARP IPv4/IPv6 traces are on

@SpilexX

SpilexX commented Jun 3, 2026

Copy link
Copy Markdown

Are you serious? Maybe you'll also add an API for updating apt packages, and for a server reboot? Is Ansible a joke to you?

@RickeyRen RickeyRen force-pushed the codex/warp-node-control-upstream branch from 3f93582 to fca1b18 Compare June 3, 2026 18:01
@RickeyRen

Copy link
Copy Markdown
Author

Production smoke test update (2026-06-03):

  • Reproduced a missing-WARP state on RackNerd by backing up/removing /etc/wireguard/warp.conf and deleting the warp interface.
  • Triggered the real Remnawave backend API against the node (POST /api/nodes/:uuid/actions/warp/disable then POST /api/nodes/:uuid/actions/warp/enable). Both calls returned HTTP 200.
  • Verified on the node host that warp came back UP, wg show warp latest-handshakes reported a fresh handshake, and /etc/wireguard/warp.conf contains Table = off plus PersistentKeepalive = 25.
  • Confirmed no leftover wgcf, installer shell, curl, or temporary node processes remained after the run.

This validates the non-interactive node-side installer path used by this PR through the full backend action flow.

@RickeyRen RickeyRen marked this pull request as ready for review June 4, 2026 01:27
@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds five WARP lifecycle endpoints (status, install, enable, disable, uninstall) to the node, implemented as a new WarpModule with a 873-line WarpService that manages wgcf registration, WireGuard interface control, IPv6 route injection, and separate host/WARP connectivity probes.

  • WarpService runs Cloudflare trace probes via curl --interface warp to report WARP IPv4/IPv6 status separately from host connectivity, and exposes operation progress logs for each lifecycle action.
  • Both xray.service.ts (startXray) and stats.service.ts now include getStatus() and getHostConnectivity() calls, adding network round-trips to paths that were previously fast.
  • docker-compose-prod.yml gains a /etc/wireguard host bind-mount; the Dockerfile gains wireguard-tools, iproute2, openresolv, bash, and curl.

Confidence Score: 3/5

The feature itself works as described, but two structural issues in the core service should be addressed before this merges to avoid hard-to-diagnose production failures.

The withOperation wrapper has no guard against concurrent invocations — two simultaneous panel requests could run conflicting WireGuard commands and leave this.operation in a mixed state. More immediately, getStatus() awaits the IPv4 and IPv6 Cloudflare traces sequentially rather than in parallel, giving a worst-case 16-second stall that now sits directly inside the startXray critical path; on a node where WARP is installed but the Cloudflare endpoint is temporarily unreachable, every xray restart will time out for 16 seconds before proceeding.

src/modules/warp/warp.service.ts needs the concurrency guard and parallel trace fix; src/modules/xray-core/xray.service.ts inherits the latency from the sequential probes.

Important Files Changed

Filename Overview
src/modules/warp/warp.service.ts New 873-line service implementing WARP lifecycle; has no concurrency guard on operations and runs Cloudflare trace probes sequentially (up to 16s worst-case latency per getStatus call).
src/modules/xray-core/xray.service.ts Adds warpService.getStatus() and getHostConnectivity() to the startXray critical path; both can stall for seconds when WARP probes time out.
src/modules/stats/stats.service.ts Adds WARP status and host connectivity probes to the stats collection call; same latency concern as xray, but stats polling is less latency-sensitive.
libs/contract/models/node-system.schema.ts Adds WarpStatusSchema, HostConnectivitySchema, and WarpOperationSchema to the contract; well-typed with nullable fields and an enum for warp state.
src/modules/warp/warp.controller.ts Thin controller mapping five WARP routes to WarpService; correctly guarded by JwtDefaultGuard and HttpExceptionFilter.
Dockerfile Adds bash, curl, iproute2, openresolv, and wireguard-tools to the Alpine image; all required dependencies for the WARP feature.
docker-compose-prod.yml Binds /etc/wireguard from host into container; necessary for WARP config persistence, consistent with existing NET_ADMIN capability.

Sequence Diagram

sequenceDiagram
    participant Panel
    participant WarpController
    participant WarpService
    participant Shell as Shell (wgcf/wg-quick/ip)
    participant CF as Cloudflare Trace

    Panel->>WarpController: POST /warp/install
    WarpController->>WarpService: install()
    WarpService->>WarpService: withOperation('installing')
    WarpService->>Shell: bash WARP_INSTALL_SCRIPT
    WarpService->>WarpService: normalizeWarpConfig()
    WarpService->>CF: curl -4 --interface warp /cdn-cgi/trace
    WarpService->>CF: curl -6 --interface warp /cdn-cgi/trace
    WarpService-->>Panel: TWarpStatus

    Panel->>WarpController: POST /warp/enable
    WarpController->>WarpService: enable()
    WarpService->>Shell: wg-quick up warp
    WarpService-->>Panel: TWarpStatus

    Panel->>WarpController: POST /warp/disable
    WarpController->>WarpService: disable()
    WarpService->>Shell: wg-quick down warp
    WarpService-->>Panel: TWarpStatus

    Panel->>WarpController: POST /warp/uninstall
    WarpController->>WarpService: uninstall()
    WarpService->>Shell: stopRunningInterface()
    WarpService-->>Panel: TWarpStatus

    Panel->>WarpController: GET /stats
    WarpController->>WarpService: getStatus() + getHostConnectivity()
    WarpService->>CF: curl probes (WARP + host interfaces)
    WarpService-->>Panel: NodeSystemStats
Loading

Reviews (1): Last reviewed commit: "fix: stabilize dual-stack warp install" | Re-trigger Greptile

Comment on lines +265 to +288
public async uninstall(): Promise<TWarpStatus> {
if (process.platform !== 'linux') {
return this.getStatus('WARP is supported only on Linux nodes');
}

return this.withOperation('uninstalling', 'Preparing WARP uninstall', async () => {
try {
if (await this.isInterfaceRunning()) {
this.appendOperationLog('Stopping WARP before uninstall');
await this.stopRunningInterface();
}

this.removeFileIfExists(WARP_CONFIG_PATH);
this.removeFileIfExists(WARP_TOOL_PATH);
this.finishOperation('WARP uninstalled');
return await this.getStatus();
} catch (error) {
const message = this.toSafeError(error);
this.logger.warn(`Failed to uninstall WARP: ${message}`);
this.failOperation(message);
return this.getStatus(message);
}
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No concurrency guard on lifecycle operations

withOperation immediately overwrites this.operation without checking whether a previous operation is still in-flight. Two concurrent HTTP POST requests — e.g., POST /warp/install and POST /warp/enable arriving simultaneously — will both enter the method, interleave their mutations to this.operation, and may both invoke installIfMissingOrSingleStack (running wgcf register/wg-quick up in parallel), producing corrupted WireGuard state. A simple early-return guard checking this.operation.state !== 'idle' in each public method would prevent this.

Comment on lines +131 to +133
const hasWireGuardHandshake = running ? await this.hasWireGuardHandshake() : false;
const traceIpv4 = running ? await this.getTrace('4') : null;
const traceIpv6 = running ? await this.getTrace('6') : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Sequential Cloudflare trace probes block the xray start path

getTrace('4') and getTrace('6') are sequential await calls, each with an 8-second execFixed timeout. When WARP is installed and running but the Cloudflare endpoint is slow or unreachable, getStatus() alone can stall for up to 16 seconds. This is then called unconditionally inside startXray(), which means every xray restart now inherits that latency. Running the two traces in parallel would cut the worst case in half.

Suggested change
const hasWireGuardHandshake = running ? await this.hasWireGuardHandshake() : false;
const traceIpv4 = running ? await this.getTrace('4') : null;
const traceIpv6 = running ? await this.getTrace('6') : null;
const hasWireGuardHandshake = running ? await this.hasWireGuardHandshake() : false;
const [traceIpv4, traceIpv6] = running
? await Promise.all([this.getTrace('4'), this.getTrace('6')])
: [null, null];

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

2 participants