From ad6e71ffef415bf985b1386ad215846af9437dcc Mon Sep 17 00:00:00 2001 From: RickeyRen Date: Tue, 18 Aug 2026 10:09:12 +0800 Subject: [PATCH] fix: probe alternate WARP endpoints --- package.json | 6 +- src/modules/warp/warp-endpoint-policy.ts | 26 +++++++ src/modules/warp/warp.service.ts | 65 ++++++++++++++++- test/warp-endpoint-policy.test.cjs | 93 ++++++++++++++++++++++++ test/warp-status.test.mjs | 3 - 5 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 src/modules/warp/warp-endpoint-policy.ts create mode 100644 test/warp-endpoint-policy.test.cjs diff --git a/package.json b/package.json index 61b9311..c20bd9f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ }, "scripts": { "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "format": "prettier --write package.json \"src/**/*.ts\" \"test/**/*.{ts,mjs,cjs}\"", + "format:check": "prettier --check package.json \"src/**/*.ts\" \"test/**/*.{ts,mjs,cjs}\"", + "test": "node --require ts-node/register --test test/*.test.*", "start": "nest start", "start:dev": "NODE_ENV=development nest start --watch", "start:debug": "NODE_ENV=development nest start --debug --watch", @@ -102,4 +104,4 @@ "tsconfig-paths": "^4.2.0", "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/src/modules/warp/warp-endpoint-policy.ts b/src/modules/warp/warp-endpoint-policy.ts new file mode 100644 index 0000000..aca5d5b --- /dev/null +++ b/src/modules/warp/warp-endpoint-policy.ts @@ -0,0 +1,26 @@ +export const WARP_ENDPOINT_CANDIDATES = [ + '162.159.192.1:2408', + '162.159.192.1:500', + '162.159.192.1:1701', + '162.159.192.1:4500', +] as const; + +type TWarpTraceState = null | { warp: string }; + +export function getWarpEndpointCandidates(configuredEndpoint: string | null): string[] { + if ( + !configuredEndpoint || + !WARP_ENDPOINT_CANDIDATES.some((endpoint) => endpoint === configuredEndpoint) + ) { + return [...WARP_ENDPOINT_CANDIDATES]; + } + + return [ + configuredEndpoint, + ...WARP_ENDPOINT_CANDIDATES.filter((endpoint) => endpoint !== configuredEndpoint), + ]; +} + +export function hasDualStackWarpTrace(ipv4: TWarpTraceState, ipv6: TWarpTraceState): boolean { + return ipv4?.warp === 'on' && ipv6?.warp === 'on'; +} diff --git a/src/modules/warp/warp.service.ts b/src/modules/warp/warp.service.ts index 34e3ee2..916e913 100644 --- a/src/modules/warp/warp.service.ts +++ b/src/modules/warp/warp.service.ts @@ -5,6 +5,11 @@ import type { THostConnectivity, TWarpOperation, TWarpStatus } from '@libs/contr import { Injectable, Logger } from '@nestjs/common'; +import { + getWarpEndpointCandidates, + hasDualStackWarpTrace, + WARP_ENDPOINT_CANDIDATES, +} from './warp-endpoint-policy'; import { TWarpCommandResult } from './warp.types'; const WARP_INTERFACE = 'warp'; @@ -12,7 +17,7 @@ const WARP_CONFIG_PATH = '/etc/wireguard/warp.conf'; const WARP_TOOL_PATH = '/usr/local/bin/wgcf'; const WARP_TRACE_URL = 'https://www.cloudflare.com/cdn-cgi/trace'; const WGCF_RELEASES_API_URL = 'https://api.github.com/repos/ViRb3/wgcf/releases/latest'; -const WARP_ENDPOINT = '162.159.192.1:2408'; +const WARP_DEFAULT_ENDPOINT = WARP_ENDPOINT_CANDIDATES[0]; const WARP_IPV6_ROUTE_METRIC = 4242; const WARP_IPV6_ROUTE_POST_UP = `PostUp = ip -6 route replace ::/0 dev %i metric ${WARP_IPV6_ROUTE_METRIC}`; const WARP_IPV6_ROUTE_PRE_DOWN = `PreDown = ip -6 route del ::/0 dev %i metric ${WARP_IPV6_ROUTE_METRIC} 2>/dev/null || true`; @@ -67,7 +72,7 @@ test -s wgcf-profile.conf echo "[warp] normalizing WireGuard profile" sed -i -E '/^DNS =/d' wgcf-profile.conf -sed -i -E 's#^Endpoint = .*$#Endpoint = ${WARP_ENDPOINT}#' wgcf-profile.conf +sed -i -E 's#^Endpoint = .*$#Endpoint = ${WARP_DEFAULT_ENDPOINT}#' wgcf-profile.conf grep -q '^Table = off$' wgcf-profile.conf || sed -i '/^MTU =/a Table = off' wgcf-profile.conf grep -q '^PersistentKeepalive = ' wgcf-profile.conf \ || sed -i '/^Endpoint =/a PersistentKeepalive = 25' wgcf-profile.conf @@ -213,6 +218,7 @@ export class WarpService { await this.execFixed('/usr/bin/wg-quick', ['up', WARP_INTERFACE], 20_000, { onOutput: (line) => this.appendOperationLog(line), }); + await this.ensureReachableWarpEndpoint(); this.finishOperation('WARP enabled'); return await this.getStatus(); } catch (error) { @@ -335,7 +341,9 @@ export class WarpService { if (!this.hasWarpConfig()) return; const original = readFileSync(WARP_CONFIG_PATH, 'utf8'); - let updated = original.replace(/^Endpoint = .*$/m, `Endpoint = ${WARP_ENDPOINT}`); + const configuredEndpoint = this.getConfiguredWarpEndpoint(original); + const preferredEndpoint = getWarpEndpointCandidates(configuredEndpoint)[0]; + let updated = original.replace(/^Endpoint = .*$/m, `Endpoint = ${preferredEndpoint}`); if (!/^Table = off$/m.test(updated)) { updated = updated.replace(/^MTU = .*$/m, (line) => `${line}\nTable = off`); @@ -384,6 +392,38 @@ export class WarpService { } } + private async ensureReachableWarpEndpoint(): Promise { + const configuredEndpoint = this.getConfiguredWarpEndpoint(); + const peer = ( + await this.execFixed('/usr/bin/wg', ['show', WARP_INTERFACE, 'peers'], 5_000) + ).stdout + .split('\n') + .map((line) => line.trim()) + .find(Boolean); + + if (!peer) { + throw new Error('WARP peer is missing'); + } + + for (const endpoint of getWarpEndpointCandidates(configuredEndpoint)) { + this.appendOperationLog(`Testing WARP endpoint ${endpoint}`); + await this.execFixed( + '/usr/bin/wg', + ['set', WARP_INTERFACE, 'peer', peer, 'endpoint', endpoint], + 5_000, + ); + + const [ipv4, ipv6] = await Promise.all([this.getTrace('4'), this.getTrace('6')]); + if (!hasDualStackWarpTrace(ipv4, ipv6)) continue; + + this.persistWarpEndpoint(endpoint); + this.appendOperationLog(`Using WARP endpoint ${endpoint}`); + return; + } + + throw new Error('No WARP endpoint provided working IPv4 and IPv6 traces'); + } + private async installIfMissingOrSingleStack(): Promise { if (this.hasWarpConfig() && this.hasDualStackConfig()) { this.appendOperationLog('Existing WARP profile is already dual-stack'); @@ -446,7 +486,24 @@ export class WarpService { if (config === null && !this.hasWarpConfig()) return false; const warpConfig = config ?? readFileSync(WARP_CONFIG_PATH, 'utf8'); - return warpConfig.includes(`Endpoint = ${WARP_ENDPOINT}`); + const configuredEndpoint = this.getConfiguredWarpEndpoint(warpConfig); + return getWarpEndpointCandidates(configuredEndpoint)[0] === configuredEndpoint; + } + + private getConfiguredWarpEndpoint(config: string | null = null): string | null { + if (config === null && !this.hasWarpConfig()) return null; + + const warpConfig = config ?? readFileSync(WARP_CONFIG_PATH, 'utf8'); + return warpConfig.match(/^Endpoint = (.+)$/m)?.[1].trim() ?? null; + } + + private persistWarpEndpoint(endpoint: string): void { + const original = readFileSync(WARP_CONFIG_PATH, 'utf8'); + const updated = original.replace(/^Endpoint = .*$/m, `Endpoint = ${endpoint}`); + + if (updated !== original) { + writeFileSync(WARP_CONFIG_PATH, updated, { mode: 0o600 }); + } } private hasDeprecatedIpv6EndpointRouteConfig(config: string | null = null): boolean { diff --git a/test/warp-endpoint-policy.test.cjs b/test/warp-endpoint-policy.test.cjs new file mode 100644 index 0000000..057472b --- /dev/null +++ b/test/warp-endpoint-policy.test.cjs @@ -0,0 +1,93 @@ +const assert = require('node:assert/strict'); +const { describe, it } = require('node:test'); + +let endpointPolicy; + +try { + endpointPolicy = require('../src/modules/warp/warp-endpoint-policy.ts'); +} catch (error) { + assert.fail(`WARP endpoint policy is not implemented: ${error.message}`); +} + +const { getWarpEndpointCandidates, hasDualStackWarpTrace } = endpointPolicy; +const { WarpService } = require('../src/modules/warp/warp.service.ts'); + +const createServiceHarness = (tracesByEndpoint) => { + const service = new WarpService(); + const attemptedEndpoints = []; + const persistedEndpoints = []; + let activeEndpoint = null; + + service.getConfiguredWarpEndpoint = () => '162.159.192.1:2408'; + service.appendOperationLog = () => {}; + service.execFixed = async (_command, args) => { + if (args[0] === 'show') { + return { stdout: 'public-peer-key\n', stderr: '' }; + } + + activeEndpoint = args.at(-1); + attemptedEndpoints.push(activeEndpoint); + return { stdout: '', stderr: '' }; + }; + service.getTrace = async (ipVersion) => tracesByEndpoint[activeEndpoint]?.[ipVersion] ?? null; + service.persistWarpEndpoint = (endpoint) => persistedEndpoints.push(endpoint); + + return { attemptedEndpoints, persistedEndpoints, service }; +}; + +describe('WARP endpoint policy', () => { + it('keeps a supported configured endpoint first and falls back to alternate UDP ports', () => { + assert.deepEqual(getWarpEndpointCandidates('162.159.192.1:500'), [ + '162.159.192.1:500', + '162.159.192.1:2408', + '162.159.192.1:1701', + '162.159.192.1:4500', + ]); + }); + + it('uses the default candidate order for an unsupported endpoint', () => { + assert.deepEqual(getWarpEndpointCandidates('engage.cloudflareclient.com:2408'), [ + '162.159.192.1:2408', + '162.159.192.1:500', + '162.159.192.1:1701', + '162.159.192.1:4500', + ]); + }); + + it('accepts an endpoint only when both WARP traces are on', () => { + assert.equal(hasDualStackWarpTrace({ warp: 'on' }, { warp: 'on' }), true); + assert.equal(hasDualStackWarpTrace({ warp: 'on' }, null), false); + assert.equal(hasDualStackWarpTrace({ warp: 'on' }, { warp: 'off' }), false); + }); + + it('retries a failed default endpoint and persists the first dual-stack endpoint', async () => { + const harness = createServiceHarness({ + '162.159.192.1:2408': { 4: { warp: 'on' }, 6: null }, + '162.159.192.1:500': { 4: { warp: 'on' }, 6: { warp: 'on' } }, + }); + + await harness.service.ensureReachableWarpEndpoint(); + + assert.deepEqual(harness.attemptedEndpoints, ['162.159.192.1:2408', '162.159.192.1:500']); + assert.deepEqual(harness.persistedEndpoints, ['162.159.192.1:500']); + }); + + it('does not persist an endpoint when every candidate fails dual-stack verification', async () => { + const harness = createServiceHarness( + Object.fromEntries( + getWarpEndpointCandidates(null).map((endpoint) => [ + endpoint, + { 4: { warp: 'on' }, 6: { warp: 'off' } }, + ]), + ), + ); + + await assert.rejects( + harness.service.ensureReachableWarpEndpoint(), + /No WARP endpoint provided working IPv4 and IPv6 traces/, + ); + + assert.deepEqual(harness.attemptedEndpoints, getWarpEndpointCandidates(null)); + assert.deepEqual(harness.persistedEndpoints, []); + }); +}); diff --git a/test/warp-status.test.mjs b/test/warp-status.test.mjs index 8674bb4..cd306aa 100644 --- a/test/warp-status.test.mjs +++ b/test/warp-status.test.mjs @@ -70,9 +70,6 @@ describe('WARP contract shape', () => { assert.match(service, /wgcf generate/); assert.doesNotMatch(service, /s#\^\(Address = \[\^,\]\+\),\.\*#\\1#/); assert.match(service, /WGCF_RELEASES_API_URL/); - assert.match(service, /WARP_ENDPOINT = '162\.159\.192\.1:2408'/); - assert.match(service, /Endpoint = \$\{WARP_ENDPOINT\}/); - assert.doesNotMatch(service, /WARP_ENDPOINT = 'engage\.cloudflareclient\.com:2408'/); assert.doesNotMatch(service, /WARP_IPV6_ENDPOINT_ROUTE_POST_UP/); assert.match(service, /getTrace\('4'\)/); assert.match(service, /getTrace\('6'\)/);