fix: simplify mac nordvpn tailscale coordination

This commit is contained in:
Stefano Fiorini
2026-03-12 01:55:46 -05:00
parent 916d8bf95a
commit 09b1c1e37a
4 changed files with 161 additions and 12 deletions

View File

@@ -14,6 +14,7 @@ const WG_STATE_DIR = path.join(STATE_DIR, "wireguard");
const WG_CONFIG_PATH = path.join(WG_STATE_DIR, `${MAC_WG_INTERFACE}.conf`);
const AUTH_CACHE_PATH = path.join(STATE_DIR, "auth.json");
const LAST_CONNECTION_PATH = path.join(STATE_DIR, "last-connection.json");
const MAC_TAILSCALE_STATE_PATH = path.join(STATE_DIR, "tailscale.json");
const OPENCLAW_NORDVPN_CREDENTIALS_DIR = path.join(
os.homedir(),
".openclaw",
@@ -35,6 +36,7 @@ const MAC_WG_HELPER_PATH = path.join(
);
const CLIENT_IPV4 = "10.5.0.2";
const DNS_FALLBACK_RESOLVERS = ["1.1.1.1", "8.8.8.8"];
const NORDVPN_MAC_DNS_SERVERS = ["103.86.96.100", "103.86.99.100"];
function printJson(payload, exitCode = 0, errorStream = false) {
const body = `${JSON.stringify(payload, null, 2)}\n`;
@@ -139,6 +141,18 @@ function commandExists(name) {
// Continue.
}
}
const fallbackDirs = ["/usr/sbin", "/usr/bin", "/bin", "/usr/local/bin", "/opt/homebrew/bin"];
for (const entry of fallbackDirs) {
const full = path.join(entry, name);
try {
fs.accessSync(full, fs.constants.X_OK);
return full;
} catch {
// Continue.
}
}
return "";
}
@@ -238,6 +252,72 @@ function detectMacWireguardActiveFromIfconfig(ifconfigOutput) {
return false;
}
function buildMacTailscaleState(tailscaleWasActive) {
return { tailscaleWasActive: Boolean(tailscaleWasActive) };
}
function getMacTailscalePath(deps = {}) {
const commandExistsFn = deps.commandExists || commandExists;
const fileExistsFn = deps.fileExists || fileExists;
const discovered = commandExistsFn("tailscale");
if (discovered) return discovered;
const fallbacks = ["/opt/homebrew/bin/tailscale", "/Applications/Tailscale.app/Contents/MacOS/Tailscale"];
for (const fallback of fallbacks) {
if (fileExistsFn(fallback)) return fallback;
}
return "tailscale";
}
function isMacTailscaleActive(status) {
return Boolean(status && status.BackendState === "Running");
}
async function getMacTailscaleStatus() {
const tailscale = getMacTailscalePath();
const result = await runExec(tailscale, ["status", "--json"]);
if (!result.ok) {
throw new Error((result.stderr || result.stdout || result.error).trim() || "tailscale status --json failed");
}
const json = JSON.parse(result.stdout || "{}");
return {
active: isMacTailscaleActive(json),
raw: json,
};
}
async function stopMacTailscaleIfActive() {
const status = await getMacTailscaleStatus();
if (!status.active) {
writeJsonFile(MAC_TAILSCALE_STATE_PATH, buildMacTailscaleState(false));
return { tailscaleWasActive: false };
}
const tailscale = getMacTailscalePath();
const result = await runExec(tailscale, ["down"]);
if (!result.ok) {
throw new Error((result.stderr || result.stdout || result.error).trim() || "tailscale down failed");
}
writeJsonFile(MAC_TAILSCALE_STATE_PATH, buildMacTailscaleState(true));
return { tailscaleWasActive: true };
}
async function resumeMacTailscaleIfNeeded() {
const state = readJsonFile(MAC_TAILSCALE_STATE_PATH);
if (!state || !state.tailscaleWasActive) {
return { restored: false };
}
const tailscale = getMacTailscalePath();
const result = await runExec(tailscale, ["up"]);
if (!result.ok) {
throw new Error((result.stderr || result.stdout || result.error).trim() || "tailscale up failed");
}
try {
fs.unlinkSync(MAC_TAILSCALE_STATE_PATH);
} catch {
// Ignore cleanup errors.
}
return { restored: true };
}
async function resolveHostnameWithFallback(hostname, options = {}) {
const resolvers = options.resolvers || DNS_FALLBACK_RESOLVERS;
const resolveWithResolver =
@@ -574,11 +654,14 @@ async function installNordvpn(installProbe) {
}
if (installProbe.platform === "linux") {
const command =
"TMP=$(mktemp) && " +
"if command -v curl >/dev/null 2>&1; then curl -fsSL https://downloads.nordcdn.com/apps/linux/install.sh -o \"$TMP\"; " +
"elif command -v wget >/dev/null 2>&1; then wget -qO \"$TMP\" https://downloads.nordcdn.com/apps/linux/install.sh; " +
"else echo 'curl or wget required' >&2; exit 1; fi && sh \"$TMP\" && rm -f \"$TMP\"";
const command = [
'TMPFILE="$(mktemp)"',
'if command -v curl >/dev/null 2>&1; then curl -fsSL https://downloads.nordcdn.com/apps/linux/install.sh -o "${TMPFILE}"; ' +
'elif command -v wget >/dev/null 2>&1; then wget -qO "${TMPFILE}" https://downloads.nordcdn.com/apps/linux/install.sh; ' +
"else echo 'curl or wget required' >&2; exit 1; fi",
'sh "${TMPFILE}"',
'rm -f "${TMPFILE}"',
].join(" && ");
const ok = await runShell(command);
if (!ok) {
throw new Error("NordVPN Linux installer failed");
@@ -812,6 +895,7 @@ function buildWireguardConfig(server, privateKey) {
"[Interface]",
`PrivateKey = ${privateKey}`,
`Address = ${addresses.join(", ")}`,
`DNS = ${NORDVPN_MAC_DNS_SERVERS.join(", ")}`,
"",
"[Peer]",
`PublicKey = ${publicKey}`,
@@ -931,6 +1015,10 @@ async function connectViaMacWireguard(installProbe, target) {
const config = buildWireguardConfig(selectedServer, credentials.nordlynx_private_key);
ensureDir(WG_STATE_DIR);
writeTextFile(WG_CONFIG_PATH, config, 0o600);
let tailscaleStopped = false;
const tailscaleState = await stopMacTailscaleIfActive();
tailscaleStopped = tailscaleState.tailscaleWasActive;
const down = await runSudoWireguard(installProbe, "down");
if (!down.ok) {
@@ -942,6 +1030,9 @@ async function connectViaMacWireguard(installProbe, target) {
const up = await runSudoWireguard(installProbe, "up");
if (!up.ok) {
if (tailscaleStopped) {
await resumeMacTailscaleIfNeeded();
}
throw new Error((up.stderr || up.stdout || up.error).trim() || "wg-quick up failed");
}
@@ -976,6 +1067,7 @@ async function connectViaMacWireguard(installProbe, target) {
city: targetMeta.city ? targetMeta.city.name : getServerCityName(selectedServer),
},
interfaceName: MAC_WG_INTERFACE,
tailscaleStopped,
raw: up.stdout.trim() || up.stderr.trim(),
};
}
@@ -1000,13 +1092,25 @@ async function disconnectNordvpn(installProbe) {
if (installProbe.platform === "darwin" && installProbe.wireguard && installProbe.wireguard.dependenciesReady) {
if (!installProbe.wireguard.active) {
return { backend: "wireguard", changed: false, message: "No active macOS WireGuard NordVPN connection found." };
const tailscale = await resumeMacTailscaleIfNeeded();
return {
backend: "wireguard",
changed: false,
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");
}
return { backend: "wireguard", changed: true, message: "Disconnected the macOS NordLynx/WireGuard session." };
const tailscale = await resumeMacTailscaleIfNeeded();
return {
backend: "wireguard",
changed: true,
tailscaleRestored: tailscale.restored,
message: "Disconnected the macOS NordLynx/WireGuard session.",
};
}
if (installProbe.platform === "darwin" && installProbe.appInstalled) {