From 647828aa78e217bf36872ba548b82818feb0f02c Mon Sep 17 00:00:00 2001 From: Stefano Fiorini Date: Thu, 12 Mar 2026 02:22:50 -0500 Subject: [PATCH] fix: force mac nordvpn disconnect teardown --- .../nordvpn-client/scripts/nordvpn-client.js | 29 +++++++++++++-- .../scripts/nordvpn-client.test.js | 37 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/skills/nordvpn-client/scripts/nordvpn-client.js b/skills/nordvpn-client/scripts/nordvpn-client.js index f515cd2..33ae311 100644 --- a/skills/nordvpn-client/scripts/nordvpn-client.js +++ b/skills/nordvpn-client/scripts/nordvpn-client.js @@ -365,6 +365,12 @@ function cleanupMacWireguardState(paths = {}) { return { cleaned }; } +function shouldAttemptMacWireguardDisconnect(wireguardState) { + if (!wireguardState) return false; + if (wireguardState.active) return true; + return Boolean(wireguardState.configPath || wireguardState.endpoint || wireguardState.lastConnection); +} + function fetchJson(url, headers = {}) { return new Promise((resolve) => { const targetUrl = new URL(url); @@ -1107,20 +1113,35 @@ async function disconnectNordvpn(installProbe) { } if (installProbe.platform === "darwin" && installProbe.wireguard && installProbe.wireguard.dependenciesReady) { - if (!installProbe.wireguard.active) { - const cleaned = cleanupMacWireguardState(); + if (!shouldAttemptMacWireguardDisconnect(installProbe.wireguard)) { const tailscale = await resumeMacTailscaleIfNeeded(); return { backend: "wireguard", changed: false, - stateCleaned: cleaned.cleaned, tailscaleRestored: tailscale.restored, message: "No active macOS WireGuard NordVPN connection found.", }; } const down = await runSudoWireguard(installProbe, "down"); if (!down.ok) { - throw new Error((down.stderr || down.stdout || down.error).trim() || "wg-quick down failed"); + const message = (down.stderr || down.stdout || down.error).trim(); + const normalized = message.toLowerCase(); + if ( + normalized.includes("is not a known interface") || + normalized.includes("unable to access interface") || + normalized.includes("not found") + ) { + const cleaned = cleanupMacWireguardState(); + const tailscale = await resumeMacTailscaleIfNeeded(); + return { + backend: "wireguard", + changed: false, + stateCleaned: cleaned.cleaned, + tailscaleRestored: tailscale.restored, + message: "No active macOS WireGuard NordVPN connection found.", + }; + } + throw new Error(message || "wg-quick down failed"); } const cleaned = cleanupMacWireguardState(); const tailscale = await resumeMacTailscaleIfNeeded(); diff --git a/skills/nordvpn-client/scripts/nordvpn-client.test.js b/skills/nordvpn-client/scripts/nordvpn-client.test.js index b5a09e4..0fd608f 100644 --- a/skills/nordvpn-client/scripts/nordvpn-client.test.js +++ b/skills/nordvpn-client/scripts/nordvpn-client.test.js @@ -21,6 +21,8 @@ module.exports = { typeof getMacTailscalePath === "function" ? getMacTailscalePath : undefined, isMacTailscaleActive: typeof isMacTailscaleActive === "function" ? isMacTailscaleActive : undefined, + shouldAttemptMacWireguardDisconnect: + typeof shouldAttemptMacWireguardDisconnect === "function" ? shouldAttemptMacWireguardDisconnect : undefined, detectMacWireguardActiveFromIfconfig: typeof detectMacWireguardActiveFromIfconfig === "function" ? detectMacWireguardActiveFromIfconfig : undefined, resolveHostnameWithFallback: @@ -130,6 +132,41 @@ test("cleanupMacWireguardState removes stale config and last-connection files", assert.equal(fs.existsSync(lastConnectionPath), false); }); +test("shouldAttemptMacWireguardDisconnect does not trust active=false when residual state exists", () => { + const { shouldAttemptMacWireguardDisconnect } = loadInternals(); + assert.equal(typeof shouldAttemptMacWireguardDisconnect, "function"); + + assert.equal( + shouldAttemptMacWireguardDisconnect({ + active: false, + configPath: "/Users/stefano/.nordvpn-client/wireguard/nordvpnctl.conf", + endpoint: null, + lastConnection: null, + }), + true + ); + + assert.equal( + shouldAttemptMacWireguardDisconnect({ + active: false, + configPath: null, + endpoint: null, + lastConnection: { country: "Italy" }, + }), + true + ); + + assert.equal( + shouldAttemptMacWireguardDisconnect({ + active: false, + configPath: null, + endpoint: null, + lastConnection: null, + }), + false + ); +}); + test("isMacTailscaleActive treats Running backend as active", () => { const { isMacTailscaleActive } = loadInternals(); assert.equal(typeof isMacTailscaleActive, "function");