From a02158dcfc0129ae90b5501dd7f6e39284f86843 Mon Sep 17 00:00:00 2001 From: Human and Agent dVPN <271368948+Sentinel-Autonomybuilder@users.noreply.github.com> Date: Mon, 27 Apr 2026 01:32:12 -0700 Subject: [PATCH] fix(v2ray): race-free config cleanup, replace fixed 2s setTimeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The V2Ray config file at /sentinel-v2ray/config.json contains the session UUID. Old code deleted it via a fixed 2s setTimeout right after spawn(). Two problems: 1. Race against next loop iteration. cfgPath is the SAME path every attempt (config.json, not config..json). If outbound A's V2Ray fails fast and the loop advances to outbound B which overwrites cfgPath in <2s, the STALE 2s timer from A then deletes B's running config. 2. Pending timer holds the event loop open if the process exits before the timer fires. Fix: delete the moment V2Ray has demonstrably loaded the config (SOCKS port is up) OR when the process exits, whichever first. A 5s unref'd safety-net timer handles the edge case where neither happens before abort. Idempotent guard (cfgDeleted) prevents double-unlink warnings. On Windows, V2Ray may still have the file open when unlink runs — the existing try/catch swallows EBUSY, and the exit handler picks it up once the proc dies. No regression vs. the old timer there. --- connection/tunnel.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/connection/tunnel.js b/connection/tunnel.js index 37bb98d..dd5f892 100644 --- a/connection/tunnel.js +++ b/connection/tunnel.js @@ -560,12 +560,28 @@ async function setupV2Ray({ remoteUrl, serverHost, sessionId, privKey, v2rayExeP } }); } - // Delete config after V2Ray reads it (contains UUID credentials) - setTimeout(() => { try { unlinkSync(cfgPath); } catch {} }, 2000); + // Delete config after V2Ray reads it (contains UUID credentials). + // Race-safe approach: delete the moment we know V2Ray has loaded config + // (port is up OR process exited), and a deadline-bound unref'd safety net. + // The previous fixed 2s timer raced the loop iterating to the next outbound, + // which overwrites cfgPath — the stale timer would then delete the NEW config. + let cfgDeleted = false; + const deleteCfg = () => { + if (cfgDeleted) return; + cfgDeleted = true; + try { unlinkSync(cfgPath); } catch {} // best-effort: V2Ray may have file open on Windows + }; + proc.once('exit', deleteCfg); + const cfgSafetyTimer = setTimeout(deleteCfg, 5000); + cfgSafetyTimer.unref(); // Wait for SOCKS5 port to accept connections instead of fixed sleep. // V2Ray binding is async — fixed 6s sleep causes false failures on slow starts. const ready = await waitForPort(socksPort, timeouts.v2rayReady); + // Once the SOCKS port accepts connections, V2Ray has fully parsed the config — + // safe to delete (Windows: file is still open by V2Ray, unlink will return EBUSY, + // try/catch swallows; falls back to exit-handler or process orphan-cleanup). + if (ready) deleteCfg(); if (!ready || proc.exitCode !== null) { progress(onProgress, logFn, 'tunnel', ` ${ob.tag}: v2ray ${proc.exitCode !== null ? `exited (code ${proc.exitCode})` : 'SOCKS5 port not ready'}, skipping`); proc.kill();