From 5d8db3d25df35bba3dae7fd4677d3c6e427f4147 Mon Sep 17 00:00:00 2001 From: Human and Agent dVPN <271368948+Sentinel-Autonomybuilder@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:20:26 -0400 Subject: [PATCH] fix(vpn): clean up temp config files on disconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WireGuard and V2Ray config files were created in system temp directories but never deleted. WireGuard configs contain the private key in plaintext — a security risk. V2Ray configs contain node addresses and UUIDs. Now both classes track their config file path and clean up on disconnect. WireGuard configs are overwritten with zeros before deletion to scrub the private key. File permissions set to 0o600 (owner-only) on creation. Fixes #40 --- src/vpn/v2ray/index.ts | 28 +++++++++++++++++++-- src/vpn/wireguard/index.ts | 51 ++++++++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/vpn/v2ray/index.ts b/src/vpn/v2ray/index.ts index 21dc8df..d192c7e 100644 --- a/src/vpn/v2ray/index.ts +++ b/src/vpn/v2ray/index.ts @@ -104,9 +104,11 @@ export class V2Ray { config: V2RayConf; uuid: string; child: null | ChildProcessWithoutNullStreams; + configPath: string | null; constructor() { this.child = null; + this.configPath = null; this.uuid = randomUUID(); // https://github.com/sentinel-official/sentinel-go-sdk/blob/development/v2ray/client.json.tmpl this.config = { @@ -252,6 +254,7 @@ export class V2Ray { output = path.join(tempDirectory, "v2ray_" + randomBytes(8).toString('hex') + ".json"); } fs.writeFileSync(output, JSON.stringify(this.config, null, 4)); + this.configPath = output; return output; } @@ -284,8 +287,29 @@ export class V2Ray { * @returns `true` if the signal was sent successfully, `false` if no process is running. */ public disconnect(): boolean { - if (this.child) return this.child.kill('SIGINT'); - return false; + const killed = this.child ? this.child.kill('SIGINT') : false; + this.cleanup(); + return killed; + } + + /** + * Removes config files from disk. + * + * @param configFile - Optional path. Falls back to the last written config. + */ + public cleanup(configFile?: string): void { + const target = configFile || this.configPath; + if (!target) return; + try { + fs.unlinkSync(target); + const dir = path.dirname(target); + if (dir.includes('sentinel-js-sdk')) { + fs.rmdirSync(dir); + } + } catch { + // Best-effort cleanup + } + if (target === this.configPath) this.configPath = null; } /** diff --git a/src/vpn/wireguard/index.ts b/src/vpn/wireguard/index.ts index 809ae78..19a17fa 100644 --- a/src/vpn/wireguard/index.ts +++ b/src/vpn/wireguard/index.ts @@ -52,10 +52,12 @@ export class Wireguard { publicKey: string privateKey: string + configPath: string | null constructor() { this.interface = null; this.peer = null; + this.configPath = null; const keys = this.genKeys(); this.publicKey = keys.pub @@ -163,6 +165,8 @@ export class Wireguard { if (this.peer.presharedKey) config += "PresharedKey = " + this.peer.presharedKey + "\n" fs.writeFileSync(output, config); + this.configPath = output; + try { fs.chmodSync(output, 0o600); } catch {} return output } return null @@ -253,14 +257,47 @@ export class Wireguard { * * @param configFile - Path to the `.conf` file used when connecting. */ - public disconnect(configFile: string) { - const child = spawn("wg-quick", ["down", configFile]) - child.stdout.setEncoding('utf8'); - child.stdout.on('data', function (data) { console.log('stdout: ' + data.trim()); }); + public disconnect(configFile: string): Promise { + return new Promise((resolve, reject) => { + const child = spawn("wg-quick", ["down", configFile]); + let stderr = ''; + + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (data) => { stderr += data; }); + child.on('error', (err) => reject(new Error(`Failed to start wg-quick: ${err.message}`))); + child.on('close', (code) => { + // Clean up config file (contains private key in plaintext) + this.cleanup(configFile); + if (code === 0) resolve(); + else reject(new Error(`wg-quick down failed (exit code ${code}): ${stderr.trim()}`)); + }); + }); + } - child.stderr.setEncoding('utf8'); - child.stderr.on('data', function (data) { console.log('stderr: ' + data.trim()); }); - child.on('close', (code) => console.log(`wg-quick down exited with code ${code}.`)); + /** + * Removes config files from disk. Overwrites with zeros before deletion + * to scrub the private key from the filesystem. + * + * @param configFile - Path to the config file to clean up. + */ + public cleanup(configFile?: string): void { + const target = configFile || this.configPath; + if (!target) return; + try { + // Overwrite with zeros to scrub private key before unlinking + const size = fs.statSync(target).size; + fs.writeFileSync(target, Buffer.alloc(size, 0)); + fs.unlinkSync(target); + // Try to remove parent temp directory if empty + const dir = path.dirname(target); + if (dir.includes('sentinel-js-sdk')) { + fs.rmdirSync(dir); + } + } catch { + // Best-effort cleanup + } + if (target === this.configPath) this.configPath = null; } /**