fix: simplify mac nordvpn tailscale coordination
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user