import os from "os"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import { existsSync } from "fs"; import { cleanupProviderConnections, getSettings, updateSettings, getApiKeys } from "@/lib/localDb"; import { enableTunnel, enableTailscale, isTunnelManuallyDisabled, isTunnelReconnecting, isTailscaleReconnecting, getTunnelService, getTailscaleService, } from "@/lib/tunnel/tunnelManager"; import { killCloudflared, isCloudflaredRunning, ensureCloudflared } from "@/lib/tunnel/cloudflared"; import { isTailscaleRunning } from "@/lib/tunnel/tailscale"; import { loadState } from "@/lib/tunnel/state"; import { checkInternet, probeUrlAlive } from "@/lib/tunnel/networkProbe"; import { RESTART_COOLDOWN_MS, NETWORK_SETTLE_MS, WATCHDOG_INTERVAL_MS, NETWORK_CHECK_INTERVAL_MS, } from "@/lib/tunnel/tunnelConfig"; import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks, restoreToolDNS, removeAllDNSEntriesSync } from "@/mitm/manager"; import { syncToJson as syncMitmAliasCache } from "@/lib/mitmAliasCache"; // Inject correct paths and DB hooks into manager.js (CJS) from ESM context (function bootstrapMitm() { if (!process.env.MITM_SERVER_PATH) { try { const thisFile = fileURLToPath(import.meta.url); const appSrc = dirname(dirname(thisFile)); const candidate = join(appSrc, "mitm", "server.js"); if (existsSync(candidate)) process.env.MITM_SERVER_PATH = candidate; } catch { /* ignore */ } } try { initDbHooks(getSettings, updateSettings); } catch { /* ignore */ } })(); process.setMaxListeners(20); // Survive Next.js hot reload const g = global.__appSingleton ??= { signalHandlersRegistered: false, watchdogInterval: null, networkMonitorInterval: null, lastNetworkFingerprint: null, lastWatchdogTick: Date.now(), lastOnline: null, mitmStartInProgress: false, tunnelAutoResumed: false, tailscaleAutoResumed: false, }; export async function initializeApp() { try { await cleanupProviderConnections(); const settings = await getSettings(); // Auto-resume tunnel (once per process) if (settings.tunnelEnabled && !g.tunnelAutoResumed) { g.tunnelAutoResumed = true; console.log("[InitApp] Tunnel was enabled, auto-resuming..."); safeRestartTunnel("startup").catch((e) => console.log("[InitApp] Tunnel resume failed:", e.message)); } // Auto-resume tailscale (once per process) if (settings.tailscaleEnabled && !g.tailscaleAutoResumed) { g.tailscaleAutoResumed = true; console.log("[InitApp] Tailscale was enabled, auto-resuming..."); safeRestartTailscale("startup").catch((e) => console.log("[InitApp] Tailscale resume failed:", e.message)); } if (!g.signalHandlersRegistered) { const cleanup = () => { try { removeAllDNSEntriesSync(); } catch { /* best effort */ } killCloudflared(); process.exit(); }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); process.on("exit", () => { try { removeAllDNSEntriesSync(); } catch { /* ignore */ } }); g.signalHandlersRegistered = true; } ensureCloudflared().catch(() => {}); // Sync mitmAlias DB → JSON cache so standalone MITM server can read it syncMitmAliasCache().catch(() => {}); startWatchdog(); startNetworkMonitor(); autoStartMitm(); } catch (error) { console.error("[InitApp] Error:", error); } } async function autoStartMitm() { if (g.mitmStartInProgress) return; g.mitmStartInProgress = true; try { const settings = await getSettings(); if (!settings.mitmEnabled) return; const mitmStatus = await getMitmStatus(); if (mitmStatus.running) return; const password = await loadEncryptedPassword(); if (!password && process.platform !== "win32") { console.log("[InitApp] MITM was enabled but no saved password found, skipping auto-start"); return; } const keys = await getApiKeys(); const activeKey = keys.find(k => k.isActive !== false); console.log("[InitApp] MITM was enabled, auto-starting..."); await startMitm(activeKey?.key || "sk_9router", password); console.log("[InitApp] MITM auto-started"); try { await restoreToolDNS(password); console.log("[InitApp] DNS restored from saved state"); } catch (e) { console.log("[InitApp] DNS restore failed:", e.message); } } catch (err) { console.log("[InitApp] MITM auto-start failed:", err.message); } finally { g.mitmStartInProgress = false; } } // ─── Safe restart (4 guards: spawn / cooldown / alive / internet) ──────────── async function safeRestartTunnel(reason) { const svc = getTunnelService(); const settings = await getSettings(); if (!settings.tunnelEnabled) return; if (svc.cancelToken.cancelled) return; if (svc.spawnInProgress) return; if (Date.now() - svc.lastRestartAt < RESTART_COOLDOWN_MS) return; // Alive check: process up + URL responds → skip if (isCloudflaredRunning()) { const state = loadState(); const publicUrl = state?.shortId ? `https://r${state.shortId}.9router.com` : null; if (publicUrl && await probeUrlAlive(publicUrl)) return; } if (!await checkInternet()) return; console.log(`[Tunnel] safeRestart (${reason})`); try { await enableTunnel(); svc.lastRestartAt = Date.now(); console.log("[Tunnel] restart success"); } catch (err) { console.log("[Tunnel] restart failed:", err.message); } } async function safeRestartTailscale(reason) { const svc = getTailscaleService(); const settings = await getSettings(); if (!settings.tailscaleEnabled) return; if (svc.cancelToken.cancelled) return; if (svc.spawnInProgress) return; if (Date.now() - svc.lastRestartAt < RESTART_COOLDOWN_MS) return; if (isTailscaleRunning() && settings.tailscaleUrl) { if (await probeUrlAlive(settings.tailscaleUrl)) return; } if (!await checkInternet()) return; console.log(`[Tailscale] safeRestart (${reason})`); try { await enableTailscale(); svc.lastRestartAt = Date.now(); console.log("[Tailscale] restart success"); } catch (err) { console.log("[Tailscale] restart failed:", err.message); } } // ─── Watchdog: 60s tick check both services ────────────────────────────────── function startWatchdog() { if (g.watchdogInterval) return; g.watchdogInterval = setInterval(() => { safeRestartTunnel("watchdog").catch(() => {}); safeRestartTailscale("watchdog").catch(() => {}); }, WATCHDOG_INTERVAL_MS); if (g.watchdogInterval.unref) g.watchdogInterval.unref(); } // ─── Network monitor: detect IPv4 fingerprint change + sleep/wake ──────────── function getNetworkFingerprint() { const interfaces = os.networkInterfaces(); const active = []; for (const [name, addrs] of Object.entries(interfaces)) { if (!addrs) continue; for (const addr of addrs) { if (!addr.internal && addr.family === "IPv4") { active.push(`${name}:${addr.address}`); } } } return active.sort().join("|"); } function startNetworkMonitor() { if (g.networkMonitorInterval) return; g.lastNetworkFingerprint = getNetworkFingerprint(); g.lastWatchdogTick = Date.now(); g.lastOnline = null; g.networkMonitorInterval = setInterval(async () => { try { const now = Date.now(); const elapsed = now - g.lastWatchdogTick; g.lastWatchdogTick = now; const currentFingerprint = getNetworkFingerprint(); const networkChanged = currentFingerprint !== g.lastNetworkFingerprint; const wasSleep = elapsed > NETWORK_CHECK_INTERVAL_MS * 6; if (networkChanged) g.lastNetworkFingerprint = currentFingerprint; // Real reachability check (TCP 1.1.1.1:443) — not just interface presence const online = await checkInternet(); const wasOffline = g.lastOnline === false; g.lastOnline = online; if (!online) return; // no internet → idle, don't restart const onlineEdge = wasOffline; // offline → online transition if (!networkChanged && !wasSleep && !onlineEdge) return; // Wait for DHCP/DNS to settle before probing await new Promise((r) => setTimeout(r, NETWORK_SETTLE_MS)); const reason = onlineEdge ? "online" : wasSleep && networkChanged ? "sleep+netchange" : wasSleep ? "sleep" : "netchange"; safeRestartTunnel(reason).catch(() => {}); safeRestartTailscale(reason).catch(() => {}); } catch (err) { console.log("[NetworkMonitor] error:", err.message); } }, NETWORK_CHECK_INTERVAL_MS); if (g.networkMonitorInterval.unref) g.networkMonitorInterval.unref(); } export default initializeApp;