|
|
|
|
@ -68,12 +68,62 @@ function getWireGuardInstallerPath(): string {
|
|
|
|
|
// throw new Error("Unsupported OS");
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// export async function getConfigPath(): Promise<string> {
|
|
|
|
|
// if (process.platform !== "win32") {
|
|
|
|
|
// throw new Error("Only Windows supported for dynamic WireGuard config");
|
|
|
|
|
// }
|
|
|
|
|
// console.log("Calling http://35.95.209.161/api/v1/vpn/connect")
|
|
|
|
|
// // 1️⃣ Call API to get dynamic config
|
|
|
|
|
// let response = null;
|
|
|
|
|
// try {
|
|
|
|
|
// response = await axios.post(
|
|
|
|
|
// "http://35.95.209.161/api/v1/vpn/connect",
|
|
|
|
|
// {},
|
|
|
|
|
// { timeout: 15000 }
|
|
|
|
|
// );
|
|
|
|
|
// } catch (err: any) {
|
|
|
|
|
// console.error("VPN API error:", {
|
|
|
|
|
// message: err.message,
|
|
|
|
|
// code: err.code,
|
|
|
|
|
// response: err.response?.data,
|
|
|
|
|
// });
|
|
|
|
|
// throw err;
|
|
|
|
|
// }
|
|
|
|
|
// console.log("Called http://35.95.209.161/api/v1/vpn/connect", JSON.stringify(response.data));
|
|
|
|
|
|
|
|
|
|
// if (!response.data?.success || !response.data.clientConf) {
|
|
|
|
|
// throw new Error("Invalid VPN config response");
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// const clientConf: string = response.data.clientConf;
|
|
|
|
|
|
|
|
|
|
// // 2️⃣ Decide base directory (same idea as your existing logic)
|
|
|
|
|
// const base = isDev()
|
|
|
|
|
// ? path.join(process.cwd(), "resources", "wireguard", "windows")
|
|
|
|
|
// : path.join(process.resourcesPath, "wireguard", "windows");
|
|
|
|
|
|
|
|
|
|
// // Ensure directory exists
|
|
|
|
|
// fs.mkdirSync(base, { recursive: true });
|
|
|
|
|
|
|
|
|
|
// // 3️⃣ Create temp / runtime conf file
|
|
|
|
|
// // IMPORTANT: name must stay stable (WireGuard service name depends on it)
|
|
|
|
|
// const confPath = path.join(base, "wg0.conf");
|
|
|
|
|
|
|
|
|
|
// // 4️⃣ Write config to file
|
|
|
|
|
// fs.writeFileSync(confPath, clientConf, {
|
|
|
|
|
// encoding: "utf-8",
|
|
|
|
|
// mode: 0o600, // secure permissions
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// console.log("VPN config written to:", confPath);
|
|
|
|
|
// // 5️⃣ Return path (same contract as before)
|
|
|
|
|
// return confPath;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
export async function getConfigPath(): Promise<string> {
|
|
|
|
|
if (process.platform !== "win32") {
|
|
|
|
|
throw new Error("Only Windows supported for dynamic WireGuard config");
|
|
|
|
|
}
|
|
|
|
|
console.log("Calling http://35.95.209.161/api/v1/vpn/connect")
|
|
|
|
|
// 1️⃣ Call API to get dynamic config
|
|
|
|
|
console.log("Calling API: http://35.95.209.161/api/v1/vpn/connect");
|
|
|
|
|
|
|
|
|
|
// Fetch dynamic config from server (works for both Windows & macOS)
|
|
|
|
|
let response = null;
|
|
|
|
|
try {
|
|
|
|
|
response = await axios.post(
|
|
|
|
|
@ -89,7 +139,6 @@ export async function getConfigPath(): Promise<string> {
|
|
|
|
|
});
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
console.log("Called http://35.95.209.161/api/v1/vpn/connect", JSON.stringify(response.data));
|
|
|
|
|
|
|
|
|
|
if (!response.data?.success || !response.data.clientConf) {
|
|
|
|
|
throw new Error("Invalid VPN config response");
|
|
|
|
|
@ -97,29 +146,48 @@ export async function getConfigPath(): Promise<string> {
|
|
|
|
|
|
|
|
|
|
const clientConf: string = response.data.clientConf;
|
|
|
|
|
|
|
|
|
|
// 2️⃣ Decide base directory (same idea as your existing logic)
|
|
|
|
|
const base = isDev()
|
|
|
|
|
// ---------- PLATFORM SPECIFIC OUTPUT DIRECTORY ----------
|
|
|
|
|
let base = "";
|
|
|
|
|
|
|
|
|
|
if (process.platform === "win32") {
|
|
|
|
|
base = isDev()
|
|
|
|
|
? path.join(process.cwd(), "resources", "wireguard", "windows")
|
|
|
|
|
: path.join(process.resourcesPath, "wireguard", "windows");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure directory exists
|
|
|
|
|
// if (process.platform === "darwin") {
|
|
|
|
|
// base = isDev()
|
|
|
|
|
// ? path.join(process.cwd(), "resources", "wireguard", "macos")
|
|
|
|
|
// : path.join(process.resourcesPath, "wireguard", "macos");
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
if (process.platform === "darwin") {
|
|
|
|
|
const base = "/usr/local/etc/wireguard";
|
|
|
|
|
fs.mkdirSync(base, { recursive: true });
|
|
|
|
|
|
|
|
|
|
// 3️⃣ Create temp / runtime conf file
|
|
|
|
|
// IMPORTANT: name must stay stable (WireGuard service name depends on it)
|
|
|
|
|
const confPath = path.join(base, "wg0.conf");
|
|
|
|
|
fs.writeFileSync(confPath, clientConf, { encoding: "utf8", mode: 0o600 });
|
|
|
|
|
|
|
|
|
|
// 4️⃣ Write config to file
|
|
|
|
|
fs.writeFileSync(confPath, clientConf, {
|
|
|
|
|
encoding: "utf-8",
|
|
|
|
|
mode: 0o600, // secure permissions
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log("VPN config written to:", confPath);
|
|
|
|
|
// 5️⃣ Return path (same contract as before)
|
|
|
|
|
return confPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!base) throw new Error("Unsupported OS");
|
|
|
|
|
|
|
|
|
|
fs.mkdirSync(base, { recursive: true });
|
|
|
|
|
|
|
|
|
|
const confPath = path.join(base, "wg0.conf");
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(confPath, clientConf, {
|
|
|
|
|
encoding: "utf-8",
|
|
|
|
|
mode: 0o600,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log("Dynamic VPN config saved:", confPath);
|
|
|
|
|
return confPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Detect installed WireGuard.exe on Windows
|
|
|
|
|
function findWireGuardCLI(): string | null {
|
|
|
|
|
const possiblePaths = [
|
|
|
|
|
@ -196,6 +264,80 @@ async function ensureWireGuardInstalled(): Promise<string> {
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// ------------------ HEALTH + STATUS HELPERS ------------------
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
// 1️⃣ Check if wg0 interface exists (works for mac + windows)
|
|
|
|
|
async function isTunnelActive(): Promise<boolean> {
|
|
|
|
|
console.log("🔍 Checking if VPN tunnel is active...");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// ---------------------- macOS ----------------------
|
|
|
|
|
if (process.platform === "darwin") {
|
|
|
|
|
console.log("🖥 Platform: macOS — detecting WireGuard via utun interface...");
|
|
|
|
|
|
|
|
|
|
const netOutput = await execPromise(`ifconfig`);
|
|
|
|
|
console.log("📡 ifconfig output length:", netOutput.length);
|
|
|
|
|
|
|
|
|
|
const utunMatch = netOutput.match(/utun\d+/);
|
|
|
|
|
console.log("🔎 utun interface detected:", utunMatch);
|
|
|
|
|
|
|
|
|
|
if (utunMatch) {
|
|
|
|
|
console.log("✅ VPN tunnel ACTIVE (utun interface exists)");
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
console.log("❌ VPN tunnel NOT active (no utun interface found)");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------- Windows ----------------------
|
|
|
|
|
if (process.platform === "win32") {
|
|
|
|
|
console.log("🖥 Platform: Windows — detecting WireGuard via wg show...");
|
|
|
|
|
|
|
|
|
|
const wgOutput = await execPromise(`wg show 2>&1`);
|
|
|
|
|
console.log("📡 wg show output length:", wgOutput.length);
|
|
|
|
|
|
|
|
|
|
const interfaceFound = wgOutput.includes("interface: wg0");
|
|
|
|
|
console.log("🔎 wg0 interface detected:", interfaceFound);
|
|
|
|
|
|
|
|
|
|
if (interfaceFound) {
|
|
|
|
|
console.log("✅ VPN tunnel ACTIVE (wg0 found)");
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
console.log("❌ VPN tunnel NOT active (wg0 not found)");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------- Unsupported OS ----------------------
|
|
|
|
|
console.log("⚠️ Unsupported OS platform — cannot detect VPN state");
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.log("❌ isTunnelActive() failed:", err);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 2️⃣ Check endpoint health
|
|
|
|
|
async function isTunnelHealthy(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.get("http://10.0.0.1/health", { timeout: 2000 });
|
|
|
|
|
return !!response.data?.success;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3️⃣ Download config only if needed
|
|
|
|
|
async function ensureConfig(platform: string): Promise<string> {
|
|
|
|
|
return await getConfigPath(); // uses dynamic logic already
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function connectWindows(): Promise<string> {
|
|
|
|
|
|
|
|
|
|
@ -248,37 +390,234 @@ async function disconnectWindows(): Promise<string> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// ------------------ MACOS CONNECT/DISCONNECT ------------------
|
|
|
|
|
//
|
|
|
|
|
async function ensureHomebrew(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await execPromise("brew --version");
|
|
|
|
|
return; // brew already installed
|
|
|
|
|
} catch (_) {
|
|
|
|
|
console.log("Homebrew missing. Installing...");
|
|
|
|
|
|
|
|
|
|
function connectMac(): Promise<string> {
|
|
|
|
|
const conf = getConfigPath();
|
|
|
|
|
return execPromise(`sudo wg-quick up "${conf}"`).then(() => "macOS VPN connected");
|
|
|
|
|
await execPromise(
|
|
|
|
|
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Update shell env — macOS uses zprofile for system shells
|
|
|
|
|
await execPromise(`echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function disconnectMac(): Promise<string> {
|
|
|
|
|
return execPromise(`sudo wg-quick down wg0`).then(() => "macOS VPN disconnected");
|
|
|
|
|
async function ensureWireGuardMac(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await execPromise("which wg");
|
|
|
|
|
return; // already installed
|
|
|
|
|
} catch (_) {
|
|
|
|
|
console.log("Installing Homebrew + wireguard-tools...");
|
|
|
|
|
|
|
|
|
|
// Install Homebrew if missing
|
|
|
|
|
try {
|
|
|
|
|
await execPromise("brew --version");
|
|
|
|
|
} catch {
|
|
|
|
|
console.log("Homebrew missing. Installing...");
|
|
|
|
|
await execPromise(
|
|
|
|
|
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Load Homebrew env for next commands
|
|
|
|
|
await execPromise(`echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile`);
|
|
|
|
|
await execPromise(`eval "$(/opt/homebrew/bin/brew shellenv)"`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Install CLI WireGuard tools
|
|
|
|
|
await execPromise("brew install wireguard-tools");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// --------------- MAC CONNECT/DISCONNECT ---------------
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
// async function connectMac(): Promise<string> {
|
|
|
|
|
// const conf = getConfigPath();
|
|
|
|
|
|
|
|
|
|
// // Wrap command in login shell so Homebrew paths are available
|
|
|
|
|
// const cmd = `/bin/bash -l -c "/opt/homebrew/bin/wg-quick up '${conf}'"`;
|
|
|
|
|
|
|
|
|
|
// const script = `
|
|
|
|
|
// do shell script "${cmd}" with administrator privileges
|
|
|
|
|
// `;
|
|
|
|
|
// const escapedScript = script.replace(/\n/g, " ");
|
|
|
|
|
|
|
|
|
|
// return execPromise(`osascript -e '${escapedScript}'`)
|
|
|
|
|
// .then(() => "macOS VPN connected (wg-quick)")
|
|
|
|
|
// .catch(err => {
|
|
|
|
|
// throw new Error("macOS VPN connect failed: " + err);
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// async function connectMac(): Promise<string> {
|
|
|
|
|
// try {
|
|
|
|
|
// const conf = await getConfigPath();
|
|
|
|
|
// console.log("Configuration Path:", conf); // Debug the path
|
|
|
|
|
|
|
|
|
|
// const cmd = `/bin/bash -l -c "/opt/homebrew/bin/wg-quick up '${conf}'"`;
|
|
|
|
|
|
|
|
|
|
// const script = `
|
|
|
|
|
// do shell script "${cmd}" with administrator privileges
|
|
|
|
|
// `;
|
|
|
|
|
// const escapedScript = script.replace(/\n/g, " ");
|
|
|
|
|
|
|
|
|
|
// await execPromise(`osascript -e '${escapedScript}'`);
|
|
|
|
|
|
|
|
|
|
// return "macOS VPN connected (wg-quick)";
|
|
|
|
|
// } catch (err) {
|
|
|
|
|
// console.error("macOS VPN connect failed:", err);
|
|
|
|
|
// throw new Error("macOS VPN connect failed: " + err);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// async function connectMac(): Promise<string> {
|
|
|
|
|
// await ensureWireGuardMac();
|
|
|
|
|
|
|
|
|
|
// const conf = await getConfigPath();
|
|
|
|
|
|
|
|
|
|
// const cmd = `/bin/bash -c "sudo /opt/homebrew/bin/wg-quick up '${conf}'"`;
|
|
|
|
|
|
|
|
|
|
// try {
|
|
|
|
|
// await execPromise(cmd);
|
|
|
|
|
// return "macOS VPN connected";
|
|
|
|
|
// } catch (err: any) {
|
|
|
|
|
// throw new Error("macOS VPN connect failed: " + err.message);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
async function connectMac(): Promise<string> {
|
|
|
|
|
await ensureWireGuardMac();
|
|
|
|
|
|
|
|
|
|
const conf = await getConfigPath();
|
|
|
|
|
|
|
|
|
|
// Detect the correct wg-quick binary
|
|
|
|
|
let wgPath = (await execPromise(`which wg-quick`)).trim();
|
|
|
|
|
|
|
|
|
|
const cmd = `do shell script "${wgPath} up '${conf}'" with administrator privileges`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await execPromise(`osascript -e '${cmd}'`);
|
|
|
|
|
console.log("macOS wg-quick result:", result);
|
|
|
|
|
return "macOS VPN connected";
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error("macOS VPN connect failed:", err);
|
|
|
|
|
throw new Error("macOS VPN connect failed: " + err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// function disconnectMac(): Promise<string> {
|
|
|
|
|
// const cmd = `/bin/bash -l -c "/opt/homebrew/bin/wg-quick down wg0"`;
|
|
|
|
|
|
|
|
|
|
// const script = `
|
|
|
|
|
// do shell script "${cmd}" with administrator privileges
|
|
|
|
|
// `;
|
|
|
|
|
// const escapedScript = script.replace(/\n/g, " ");
|
|
|
|
|
|
|
|
|
|
// return execPromise(`osascript -e '${escapedScript}'`)
|
|
|
|
|
// .then(() => "macOS VPN disconnected")
|
|
|
|
|
// .catch(err => {
|
|
|
|
|
// throw new Error("macOS VPN disconnect failed: " + err);
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// function disconnectMac(): Promise<string> {
|
|
|
|
|
// const cmd = `/bin/bash -c "sudo /opt/homebrew/bin/wg-quick down wg0"`;
|
|
|
|
|
|
|
|
|
|
// return execPromise(cmd)
|
|
|
|
|
// .then(() => "macOS VPN disconnected")
|
|
|
|
|
// .catch(err => {
|
|
|
|
|
// throw new Error("macOS VPN disconnect failed: " + err.message);
|
|
|
|
|
// });
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function disconnectMac(): Promise<string> {
|
|
|
|
|
let wgPath = (await execPromise(`which wg-quick`)).trim();
|
|
|
|
|
|
|
|
|
|
const cmd = `do shell script "${wgPath} down wg0" with administrator privileges`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await execPromise(`osascript -e '${cmd}'`);
|
|
|
|
|
return "macOS VPN disconnected";
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
throw new Error("macOS VPN disconnect failed: " + err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// ------------------ PUBLIC API ------------------
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
export function startVPN(): Promise<string> {
|
|
|
|
|
export async function startVPN(): Promise<string> {
|
|
|
|
|
const platform = os.platform();
|
|
|
|
|
|
|
|
|
|
if (platform === "win32") return connectWindows();
|
|
|
|
|
if (platform === "darwin") return connectMac();
|
|
|
|
|
|
|
|
|
|
return Promise.reject("Unsupported OS");
|
|
|
|
|
if (platform !== "win32" && platform !== "darwin") {
|
|
|
|
|
throw new Error("Unsupported OS");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function stopVPN(): Promise<string> {
|
|
|
|
|
// STEP 1: tunnel active?
|
|
|
|
|
console.log("Start VPN Called for pltf :", platform)
|
|
|
|
|
const active = await isTunnelActive();
|
|
|
|
|
console.log("VPN Active Status :", active)
|
|
|
|
|
|
|
|
|
|
if (active) {
|
|
|
|
|
console.log("VPN already active — checking health...");
|
|
|
|
|
|
|
|
|
|
// STEP 2: healthy?
|
|
|
|
|
if (await isTunnelHealthy()) {
|
|
|
|
|
console.log("VPN healthy — skip reconnect");
|
|
|
|
|
return "VPN already connected and healthy";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log("VPN active but unhealthy — reconnecting...");
|
|
|
|
|
await stopVPN(); // gracefully disconnect
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// STEP 3: ensure config before connect
|
|
|
|
|
await ensureConfig(platform);
|
|
|
|
|
|
|
|
|
|
// STEP 4: connect
|
|
|
|
|
if (platform === "win32") {
|
|
|
|
|
return connectWindows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (platform === "darwin") {
|
|
|
|
|
return connectMac();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// STEP 5: unreachable fallback to satisfy TS
|
|
|
|
|
throw new Error("Unexpected platform state");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function stopVPN(): Promise<string> {
|
|
|
|
|
const platform = os.platform();
|
|
|
|
|
|
|
|
|
|
if (platform === "win32") return disconnectWindows();
|
|
|
|
|
if (platform === "darwin") return disconnectMac();
|
|
|
|
|
|
|
|
|
|
return Promise.reject("Unsupported OS");
|
|
|
|
|
try {
|
|
|
|
|
if (platform === "win32") return await disconnectWindows();
|
|
|
|
|
if (platform === "darwin") return await disconnectMac();
|
|
|
|
|
} catch {
|
|
|
|
|
return "VPN not running";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Error("Unsupported OS");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|