diff --git a/src/electron/vpn.ts b/src/electron/vpn.ts index 0a04be7..bcbcf71 100644 --- a/src/electron/vpn.ts +++ b/src/electron/vpn.ts @@ -68,12 +68,62 @@ function getWireGuardInstallerPath(): string { // throw new Error("Unsupported OS"); // } +// export async function getConfigPath(): Promise { +// 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 { - 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 { }); 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 { 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"); + // ---------- 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"); + } + + // 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 }); + + const confPath = path.join(base, "wg0.conf"); + fs.writeFileSync(confPath, clientConf, { encoding: "utf8", mode: 0o600 }); + + return confPath; + } + + + if (!base) throw new Error("Unsupported OS"); - // 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 + mode: 0o600, }); - console.log("VPN config written to:", confPath); - // 5️⃣ Return path (same contract as before) + 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 { // }); // } +// +// ------------------ HEALTH + STATUS HELPERS ------------------ +// + +// 1️⃣ Check if wg0 interface exists (works for mac + windows) +async function isTunnelActive(): Promise { + 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 { + 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 { + return await getConfigPath(); // uses dynamic logic already +} + + async function connectWindows(): Promise { @@ -294,63 +436,188 @@ async function ensureWireGuardMac(): Promise { // --------------- MAC CONNECT/DISCONNECT --------------- // +// async function connectMac(): Promise { +// 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 { +// 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 { +// 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 { - const conf = getConfigPath(); + await ensureWireGuardMac(); - // Wrap command in login shell so Homebrew paths are available - const cmd = `/bin/bash -l -c "/opt/homebrew/bin/wg-quick up '${conf}'"`; + const conf = await getConfigPath(); - const script = ` -do shell script "${cmd}" with administrator privileges -`; - const escapedScript = script.replace(/\n/g, " "); + // Detect the correct wg-quick binary + let wgPath = (await execPromise(`which wg-quick`)).trim(); - return execPromise(`osascript -e '${escapedScript}'`) - .then(() => "macOS VPN connected (wg-quick)") - .catch(err => { - throw new Error("macOS VPN connect failed: " + err); - }); + 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 { - 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 { +// 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 { +// 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 { + 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 { +export async function startVPN(): Promise { const platform = os.platform(); - if (platform === "win32") return connectWindows(); - if (platform === "darwin") return connectMac(); + if (platform !== "win32" && platform !== "darwin") { + throw new Error("Unsupported OS"); + } - return Promise.reject("Unsupported OS"); + // 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 function stopVPN(): Promise { + +export async function stopVPN(): Promise { const platform = os.platform(); - if (platform === "win32") return disconnectWindows(); - if (platform === "darwin") return disconnectMac(); + try { + if (platform === "win32") return await disconnectWindows(); + if (platform === "darwin") return await disconnectMac(); + } catch { + return "VPN not running"; + } - return Promise.reject("Unsupported OS"); + throw new Error("Unsupported OS"); } + diff --git a/src/ui/pages/Vpn/VpnGuard.tsx b/src/ui/pages/Vpn/VpnGuard.tsx index c49fb56..c02d8f6 100644 --- a/src/ui/pages/Vpn/VpnGuard.tsx +++ b/src/ui/pages/Vpn/VpnGuard.tsx @@ -47,7 +47,7 @@ const VpnGuard: React.FC = ({ children }) => { console.log("connectVPN result → ", res); // Only proceed if VPN actually connected - if (res === "Windows VPN connected silently") { + if (res === "Windows VPN connected silently" || res === "VPN already connected and healthy") { const vpnRes = await window.electron.updateBaseURL(VPN_API); console.log("vpnRes", vpnRes);