Compare commits

...

8 Commits

5 changed files with 791 additions and 418 deletions

774
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
"build": "tsc && vite build",
"preview": "vite preview",
"transpile:electron": "tsc --project src/electron/tsconfig.json",
"dist:mac": "npm run transpile:electron && npm run build && electron-builder --mac --x64 --arm64",
"dist:mac": "npm run transpile:electron && npm run build && electron-builder --mac --universal",
"dist:win": "npm run transpile:electron && npm run build && electron-builder --win --x64",
"dist:linux": "npm run transpile:electron && npm run build && electron-builder --linux --x64",
"test:e2e": "playwright test",
@ -82,7 +82,10 @@
],
"mac": {
"target": [
"dmg"
{
"target": "dmg",
"arch": "universal"
}
],
"icon": "build/icon.icns",
"extendInfo": {
@ -126,13 +129,13 @@
"@vitejs/plugin-react": "^4.3.1",
"cross-env": "^7.0.3",
"daisyui": "^5.4.3",
"electron": "^32.1.2",
"electron": "^39.2.5",
"electron-builder": "^25.0.5",
"electron-rebuild": "^3.2.9",
"npm-run-all": "^4.1.5",
"rcedit": "^5.0.2",
"typescript": "^5.6.2",
"vite": "^5.4.7",
"vitest": "^2.1.1"
"vite": "^7.2.6",
"vitest": "^4.0.15"
}
}

View File

@ -11,7 +11,7 @@ export function ipcMainHandle<Key extends keyof EventPayloadMapping>(
handler: () => EventPayloadMapping[Key]
) {
ipcMain.handle(key, (event) => {
validateEventFrame(event.senderFrame);
validateEventFrame(event.senderFrame!);
return handler();
});
}
@ -21,7 +21,7 @@ export function ipcMainOn<Key extends keyof EventPayloadMapping>(
handler: (payload: EventPayloadMapping[Key]) => void
) {
ipcMain.on(key, (event, payload) => {
validateEventFrame(event.senderFrame);
validateEventFrame(event.senderFrame!);
return handler(payload);
});
}
@ -34,11 +34,16 @@ export function ipcWebContentsSend<Key extends keyof EventPayloadMapping>(
webContents.send(key, payload);
}
export function validateEventFrame(frame: WebFrameMain) {
if (isDev() && new URL(frame.url).host === 'localhost:5123') {
export function validateEventFrame(frame: WebFrameMain | null): asserts frame is WebFrameMain {
if (!frame) throw new Error('Missing frame');
const url = frame.url;
if (isDev() && new URL(url).host === 'localhost:5123') {
return;
}
if (frame.url !== pathToFileURL(getUIPath()).toString()) {
if (url !== pathToFileURL(getUIPath()).toString()) {
throw new Error('Malicious event');
}
}

View File

@ -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");
}
// 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<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();
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<string> {
export async function stopVPN(): Promise<string> {
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");
}

View File

@ -47,7 +47,7 @@ const VpnGuard: React.FC<Props> = ({ 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);