import { ChildProcess, exec, execFile, spawn } from 'child_process' import { dataDir, coreLogPath, mihomoCoreDir, mihomoCorePath, mihomoProfileWorkDir, mihomoTestDir, mihomoWorkConfigPath, mihomoWorkDir } from '../utils/dirs' import { generateProfile } from './factory' import { getAppConfig, getControledMihomoConfig, getProfileConfig, patchAppConfig, patchControledMihomoConfig, manageSmartOverride } from '../config' import { app, ipcMain, net } from 'electron' import { startMihomoTraffic, startMihomoConnections, startMihomoLogs, startMihomoMemory, stopMihomoConnections, stopMihomoTraffic, stopMihomoLogs, stopMihomoMemory, patchMihomoConfig, getAxios } from './mihomoApi' import chokidar from 'chokidar' import { readFile, rm, writeFile } from 'fs/promises' import { promisify } from 'util' import { mainWindow } from '..' import path from 'path' import os from 'os' import { createWriteStream, existsSync } from 'fs' import { uploadRuntimeConfig } from '../resolve/gistApi' import { startMonitor } from '../resolve/trafficMonitor' import { safeShowErrorBox } from '../utils/init' import i18next from '../../shared/i18n' import { managerLogger } from '../utils/logger' // 内核名称白名单 const ALLOWED_CORES = ['mihomo', 'mihomo-alpha', 'mihomo-smart'] as const type AllowedCore = (typeof ALLOWED_CORES)[number] function isValidCoreName(core: string): core is AllowedCore { return ALLOWED_CORES.includes(core as AllowedCore) } // 路径检查 function validateCorePath(corePath: string): void { if (corePath.includes('..')) { throw new Error('Invalid core path: directory traversal detected') } const dangerousChars = /[;&|`$(){}[\]<>'"\\]/ if (dangerousChars.test(path.basename(corePath))) { throw new Error('Invalid core path: contains dangerous characters') } const normalizedPath = path.normalize(path.resolve(corePath)) const expectedDir = path.normalize(path.resolve(mihomoCoreDir())) if (!normalizedPath.startsWith(expectedDir + path.sep) && normalizedPath !== expectedDir) { throw new Error('Invalid core path: not in expected directory') } } function shellEscape(arg: string): string { return "'" + arg.replace(/'/g, "'\\''") + "'" } chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => { try { await stopCore(true) await startCore() } catch (e) { safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) } }) // 动态生成 IPC 路径 export const getMihomoIpcPath = (): string => { if (process.platform === 'win32') { const isAdmin = getSessionAdminStatus() const sessionId = process.env.SESSIONNAME || process.env.USERNAME || 'default' const processId = process.pid if (isAdmin) { return `\\\\.\\pipe\\MihomoParty\\mihomo-admin-${sessionId}-${processId}` } else { return `\\\\.\\pipe\\MihomoParty\\mihomo-user-${sessionId}-${processId}` } } const uid = process.getuid?.() || 'unknown' const processId = process.pid return `/tmp/mihomo-party-${uid}-${processId}.sock` } const ctlParam = process.platform === 'win32' ? '-ext-ctl-pipe' : '-ext-ctl-unix' let setPublicDNSTimer: NodeJS.Timeout | null = null let recoverDNSTimer: NodeJS.Timeout | null = null let child: ChildProcess let retry = 10 let isRestarting = false export async function startCore(detached = false): Promise[]> { const { core = 'mihomo', autoSetDNS = true, diffWorkDir = false, mihomoCpuPriority = 'PRIORITY_NORMAL' } = await getAppConfig() const { 'log-level': logLevel } = await getControledMihomoConfig() if (existsSync(path.join(dataDir(), 'core.pid'))) { const pid = parseInt(await readFile(path.join(dataDir(), 'core.pid'), 'utf-8')) try { process.kill(pid, 'SIGINT') } catch { // ignore } finally { await rm(path.join(dataDir(), 'core.pid')) } } const { current } = await getProfileConfig(true) const { tun } = await getControledMihomoConfig() const corePath = mihomoCorePath(core) // 管理 Smart 内核覆写配置 await manageSmartOverride() await generateProfile() await checkProfile() await stopCore() await cleanupSocketFile() if (tun?.enable && autoSetDNS) { try { await setPublicDNS() } catch (error) { await managerLogger.error('set dns failed', error) } } // 获取动态 IPC 路径 const dynamicIpcPath = getMihomoIpcPath() await managerLogger.info(`Using IPC path: ${dynamicIpcPath}`) if (process.platform === 'win32') { await validateWindowsPipeAccess(dynamicIpcPath) } // 内核日志输出到独立的 core-日期.log 文件 const stdout = createWriteStream(coreLogPath(), { flags: 'a' }) const stderr = createWriteStream(coreLogPath(), { flags: 'a' }) child = spawn( corePath, ['-d', diffWorkDir ? mihomoProfileWorkDir(current) : mihomoWorkDir(), ctlParam, dynamicIpcPath], { detached: detached, stdio: detached ? 'ignore' : undefined } ) if (process.platform === 'win32' && child.pid) { os.setPriority(child.pid, os.constants.priority[mihomoCpuPriority]) } if (detached) { await managerLogger.info( `Core process detached successfully on ${process.platform}, PID: ${child.pid}` ) child.unref() return new Promise((resolve) => { resolve([new Promise(() => {})]) }) } child.on('close', async (code, signal) => { await managerLogger.info(`Core closed, code: ${code}, signal: ${signal}`) if (isRestarting) { await managerLogger.info('Core closed during restart, skipping auto-restart') return } if (retry) { await managerLogger.info('Try Restart Core') retry-- await restartCore() } else { await stopCore() } }) child.stdout?.pipe(stdout) child.stderr?.pipe(stderr) return new Promise((resolve, reject) => { child.stdout?.on('data', async (data) => { const str = data.toString() if (str.includes('configure tun interface: operation not permitted')) { patchControledMihomoConfig({ tun: { enable: false } }) mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') reject(i18next.t('tun.error.tunPermissionDenied')) } if ( (process.platform !== 'win32' && str.includes('External controller unix listen error')) || (process.platform === 'win32' && str.includes('External controller pipe listen error')) ) { await managerLogger.error('External controller listen error detected:', str) if (process.platform === 'win32') { await managerLogger.info('Attempting Windows pipe cleanup and retry...') try { await cleanupWindowsNamedPipes() await new Promise((resolve) => setTimeout(resolve, 2000)) } catch (cleanupError) { await managerLogger.error('Pipe cleanup failed:', cleanupError) } } reject(i18next.t('mihomo.error.externalControllerListenError')) } if ( (process.platform !== 'win32' && str.includes('RESTful API unix listening at')) || (process.platform === 'win32' && str.includes('RESTful API pipe listening at')) ) { resolve([ new Promise((resolve) => { child.stdout?.on('data', async (data) => { if ( data.toString().toLowerCase().includes('start initial compatible provider default') ) { try { mainWindow?.webContents.send('groupsUpdated') mainWindow?.webContents.send('rulesUpdated') await uploadRuntimeConfig() } catch { // ignore } await patchMihomoConfig({ 'log-level': logLevel }) resolve() } }) }) ]) await waitForCoreReady() // 强制刷新 axios 实例以使用新的管道路径 await getAxios(true) await startMihomoTraffic() await startMihomoConnections() await startMihomoLogs() await startMihomoMemory() retry = 10 } }) }) } export async function stopCore(force = false): Promise { try { if (!force) { await recoverDNS() } } catch (error) { await managerLogger.error('recover dns failed', error) } if (child) { child.removeAllListeners() child.kill('SIGINT') } stopMihomoTraffic() stopMihomoConnections() stopMihomoLogs() stopMihomoMemory() // 强制刷新 axios try { await getAxios(true) } catch (error) { await managerLogger.warn('Failed to refresh axios instance:', error) } // 清理 Socket 文件 await cleanupSocketFile() } async function cleanupSocketFile(): Promise { if (process.platform === 'win32') { await cleanupWindowsNamedPipes() } else { await cleanupUnixSockets() } } // Windows 命名管道清理 async function cleanupWindowsNamedPipes(): Promise { try { const execPromise = promisify(exec) try { const { stdout } = await execPromise( `powershell -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Process | Where-Object {$_.ProcessName -like '*mihomo*'} | Select-Object Id,ProcessName | ConvertTo-Json"`, { encoding: 'utf8' } ) if (stdout.trim()) { await managerLogger.info(`Found potential pipe-blocking processes: ${stdout}`) try { const processes = JSON.parse(stdout) const processArray = Array.isArray(processes) ? processes : [processes] for (const proc of processArray) { const pid = proc.Id if (pid && pid !== process.pid) { try { // 先检查进程是否存在 process.kill(pid, 0) process.kill(pid, 'SIGTERM') await managerLogger.info(`Terminated process ${pid} to free pipe`) } catch (error: unknown) { if ((error as { code?: string })?.code !== 'ESRCH') { await managerLogger.warn(`Failed to terminate process ${pid}:`, error) } } } } } catch (parseError) { await managerLogger.warn('Failed to parse process list JSON:', parseError) // 回退到文本解析 const lines = stdout.split('\n').filter((line) => line.includes('mihomo')) for (const line of lines) { const match = line.match(/(\d+)/) if (match) { const pid = parseInt(match[1]) if (pid !== process.pid) { try { process.kill(pid, 0) process.kill(pid, 'SIGTERM') await managerLogger.info(`Terminated process ${pid} to free pipe`) } catch (error: unknown) { if ((error as { code?: string })?.code !== 'ESRCH') { await managerLogger.warn(`Failed to terminate process ${pid}:`, error) } } } } } } } } catch (error) { await managerLogger.warn('Failed to check mihomo processes:', error) } await new Promise((resolve) => setTimeout(resolve, 1000)) } catch (error) { await managerLogger.error('Windows named pipe cleanup failed:', error) } } // Unix Socket 清理 async function cleanupUnixSockets(): Promise { try { const socketPaths = [ '/tmp/mihomo-party.sock', '/tmp/mihomo-party-admin.sock', `/tmp/mihomo-party-${process.getuid?.() || 'user'}.sock` ] for (const socketPath of socketPaths) { try { if (existsSync(socketPath)) { await rm(socketPath) await managerLogger.info(`Cleaned up socket file: ${socketPath}`) } } catch (error) { await managerLogger.warn(`Failed to cleanup socket file ${socketPath}:`, error) } } } catch (error) { await managerLogger.error('Unix socket cleanup failed:', error) } } // Windows 命名管道访问验证 async function validateWindowsPipeAccess(pipePath: string): Promise { try { await managerLogger.info(`Validating pipe access for: ${pipePath}`) await managerLogger.info(`Pipe validation completed for: ${pipePath}`) } catch (error) { await managerLogger.error('Windows pipe validation failed:', error) } } export async function restartCore(): Promise { // 防止并发重启 if (isRestarting) { await managerLogger.info('Core restart already in progress, skipping duplicate request') return } isRestarting = true try { await startCore() } catch (e) { await managerLogger.error('restart core failed', e) throw e } finally { isRestarting = false } } export async function keepCoreAlive(): Promise { try { await startCore(true) if (child && child.pid) { await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString()) } } catch (e) { safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) } } export async function quitWithoutCore(): Promise { await managerLogger.info(`Starting lightweight mode on platform: ${process.platform}`) try { await startCore(true) if (child && child.pid) { await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString()) await managerLogger.info(`Core started in lightweight mode with PID: ${child.pid}`) } } catch (e) { await managerLogger.error('Failed to start core in lightweight mode:', e) safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) } await startMonitor(true) await managerLogger.info('Exiting main process, core will continue running in background') app.exit() } async function checkProfile(): Promise { const { core = 'mihomo', diffWorkDir = false } = await getAppConfig() const { current } = await getProfileConfig() const corePath = mihomoCorePath(core) const execFilePromise = promisify(execFile) try { await execFilePromise(corePath, [ '-t', '-f', diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'), '-d', mihomoTestDir() ]) } catch (error) { await managerLogger.error('Profile check failed', error) if (error instanceof Error && 'stdout' in error) { const { stdout, stderr } = error as { stdout: string; stderr?: string } await managerLogger.info('Profile check stdout', stdout) await managerLogger.info('Profile check stderr', stderr) const errorLines = stdout .split('\n') .filter((line) => line.includes('level=error') || line.includes('error')) .map((line) => { if (line.includes('level=error')) { return line.split('level=error')[1]?.trim() || line } return line.trim() }) .filter((line) => line.length > 0) if (errorLines.length === 0) { const allLines = stdout.split('\n').filter((line) => line.trim().length > 0) throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${allLines.join('\n')}`) } else { throw new Error( `${i18next.t('mihomo.error.profileCheckFailed')}:\n${errorLines.join('\n')}` ) } } else { throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}: ${error}`) } } } export async function checkTunPermissions(): Promise { return checkMihomoCorePermissions() } export async function grantTunPermissions(): Promise { const { core = 'mihomo' } = await getAppConfig() // 验证内核名称 if (!isValidCoreName(core)) { throw new Error(`Invalid core name: ${core}. Allowed values: ${ALLOWED_CORES.join(', ')}`) } const corePath = mihomoCorePath(core) // 验证路径 validateCorePath(corePath) const execFilePromise = promisify(execFile) if (process.platform === 'darwin') { const escapedPath = shellEscape(corePath) const script = `do shell script "chown root:admin ${escapedPath} && chmod +sx ${escapedPath}" with administrator privileges` await execFilePromise('osascript', ['-e', script]) } if (process.platform === 'linux') { await execFilePromise('pkexec', ['chown', 'root:root', corePath]) await execFilePromise('pkexec', ['chmod', '+sx', corePath]) } if (process.platform === 'win32') { throw new Error('Windows platform requires running as administrator') } } // 在应用启动时检测一次权限 let sessionAdminStatus: boolean | null = null export async function initAdminStatus(): Promise { if (process.platform === 'win32' && sessionAdminStatus === null) { sessionAdminStatus = await checkAdminPrivileges().catch(() => false) } } export function getSessionAdminStatus(): boolean { if (process.platform !== 'win32') { return true } return sessionAdminStatus ?? false } // 等待内核完全启动并创建管道 async function waitForCoreReady(): Promise { const maxRetries = 30 const retryInterval = 500 for (let i = 0; i < maxRetries; i++) { try { const axios = await getAxios(true) await axios.get('/') await managerLogger.info(`Core ready after ${i + 1} attempts (${(i + 1) * retryInterval}ms)`) return } catch (error) { if (i === 0) { await managerLogger.info('Waiting for core to be ready...') } if (i === maxRetries - 1) { await managerLogger.warn(`Core not ready after ${maxRetries} attempts, proceeding anyway`) return } await new Promise((resolve) => setTimeout(resolve, retryInterval)) } } } export async function checkAdminPrivileges(): Promise { if (process.platform !== 'win32') { return true } const execPromise = promisify(exec) try { // fltmc 检测管理员权限 await execPromise('chcp 65001 >nul 2>&1 && fltmc', { encoding: 'utf8' }) await managerLogger.info('Admin privileges confirmed via fltmc') return true } catch (fltmcError: unknown) { const errorCode = (fltmcError as { code?: number })?.code || 0 await managerLogger.debug(`fltmc failed with code ${errorCode}, trying net session as fallback`) try { // net session 备用 await execPromise('chcp 65001 >nul 2>&1 && net session', { encoding: 'utf8' }) await managerLogger.info('Admin privileges confirmed via net session') return true } catch (netSessionError: unknown) { const netErrorCode = (netSessionError as { code?: number })?.code || 0 await managerLogger.debug( `Both fltmc and net session failed, no admin privileges. Error codes: fltmc=${errorCode}, net=${netErrorCode}` ) return false } } } // TUN 权限确认框 export async function showTunPermissionDialog(): Promise { const { dialog } = await import('electron') const i18next = await import('i18next') await managerLogger.info('Preparing TUN permission dialog...') await managerLogger.info(`i18next available: ${typeof i18next.t === 'function'}`) const title = i18next.t('tun.permissions.title') || '需要管理员权限' const message = i18next.t('tun.permissions.message') || '启用 TUN 模式需要管理员权限,是否现在重启应用获取权限?' const confirmText = i18next.t('common.confirm') || '确认' const cancelText = i18next.t('common.cancel') || '取消' await managerLogger.info( `Dialog texts - Title: "${title}", Message: "${message}", Confirm: "${confirmText}", Cancel: "${cancelText}"` ) const choice = dialog.showMessageBoxSync({ type: 'warning', title: title, message: message, buttons: [confirmText, cancelText], defaultId: 0, cancelId: 1 }) await managerLogger.info(`TUN permission dialog choice: ${choice}`) return choice === 0 } // 错误显示框 export async function showErrorDialog(title: string, message: string): Promise { const { dialog } = await import('electron') const i18next = await import('i18next') const okText = i18next.t('common.confirm') || '确认' dialog.showMessageBoxSync({ type: 'error', title: title, message: message, buttons: [okText], defaultId: 0 }) } export async function restartAsAdmin(forTun: boolean = true): Promise { if (process.platform !== 'win32') { throw new Error('This function is only available on Windows') } const exePath = process.execPath const args = process.argv.slice(1) const restartArgs = forTun ? [...args, '--admin-restart-for-tun'] : args try { // 处理路径和参数的引号 const escapedExePath = exePath.replace(/'/g, "''") const argsString = restartArgs.map((arg) => arg.replace(/'/g, "''")).join("', '") let command: string if (restartArgs.length > 0) { command = `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -ArgumentList '${argsString}' -Verb RunAs"` } else { command = `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"` } await managerLogger.info('Restarting as administrator with command', command) // 执行 PowerShell 命令 exec(command, { windowsHide: true }, async (error, _stdout, stderr) => { if (error) { await managerLogger.error('PowerShell execution error', error) await managerLogger.error('stderr', stderr) } else { await managerLogger.info('PowerShell command executed successfully') } }) const { app } = await import('electron') app.quit() } catch (error) { await managerLogger.error('Failed to restart as administrator', error) throw new Error(`Failed to restart as administrator: ${error}`) } } export async function checkMihomoCorePermissions(): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) try { if (process.platform === 'win32') { // Windows 权限检查 return await checkAdminPrivileges() } if (process.platform === 'darwin' || process.platform === 'linux') { const { stat } = await import('fs/promises') const stats = await stat(corePath) return (stats.mode & 0o4000) !== 0 && stats.uid === 0 } } catch { return false } return false } // 检测高权限内核 export async function checkHighPrivilegeCore(): Promise { try { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) await managerLogger.info(`Checking high privilege core: ${corePath}`) if (process.platform === 'win32') { const { existsSync } = await import('fs') if (!existsSync(corePath)) { await managerLogger.info('Core file does not exist') return false } const hasHighPrivilegeProcess = await checkHighPrivilegeMihomoProcess() if (hasHighPrivilegeProcess) { await managerLogger.info('Found high privilege mihomo process running') return true } const isAdmin = await checkAdminPrivileges() await managerLogger.info(`Current process admin privileges: ${isAdmin}`) return isAdmin } if (process.platform === 'darwin' || process.platform === 'linux') { await managerLogger.info('Non-Windows platform, skipping high privilege core check') return false } } catch (error) { await managerLogger.error('Failed to check high privilege core', error) return false } return false } async function checkHighPrivilegeMihomoProcess(): Promise { try { if (process.platform === 'win32') { const execPromise = promisify(exec) const mihomoExecutables = ['mihomo.exe', 'mihomo-alpha.exe', 'mihomo-smart.exe'] for (const executable of mihomoExecutables) { try { const { stdout } = await execPromise( `chcp 65001 >nul 2>&1 && tasklist /FI "IMAGENAME eq ${executable}" /FO CSV`, { encoding: 'utf8' } ) const lines = stdout.split('\n').filter((line) => line.includes(executable)) if (lines.length > 0) { await managerLogger.info(`Found ${lines.length} ${executable} processes running`) for (const line of lines) { const parts = line.split(',') if (parts.length >= 2) { const pid = parts[1].replace(/"/g, '').trim() try { const { stdout: processInfo } = await execPromise( `powershell -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Process -Id ${pid} | Select-Object Name,Id,Path,CommandLine | ConvertTo-Json"`, { encoding: 'utf8' } ) const processJson = JSON.parse(processInfo) await managerLogger.info(`Process ${pid} info: ${processInfo.substring(0, 200)}`) if (processJson.Name.includes('mihomo') && processJson.Path === null) { return true } } catch (error) { await managerLogger.info( `Cannot get info for process ${pid}, might be high privilege` ) } } } } } catch (error) { await managerLogger.error(`Failed to check ${executable} processes`, error) } } } if (process.platform === 'darwin' || process.platform === 'linux') { const execPromise = promisify(exec) try { const mihomoExecutables = ['mihomo', 'mihomo-alpha', 'mihomo-smart'] let foundProcesses = false for (const executable of mihomoExecutables) { try { const { stdout } = await execPromise(`ps aux | grep ${executable} | grep -v grep`) const lines = stdout .split('\n') .filter((line) => line.trim() && line.includes(executable)) if (lines.length > 0) { foundProcesses = true await managerLogger.info(`Found ${lines.length} ${executable} processes running`) for (const line of lines) { const parts = line.trim().split(/\s+/) if (parts.length >= 1) { const user = parts[0] await managerLogger.info(`${executable} process running as user: ${user}`) if (user === 'root') { return true } } } } } catch (error) { // ignore } } if (!foundProcesses) { await managerLogger.info('No mihomo processes found running') } } catch (error) { await managerLogger.error('Failed to check mihomo processes on Unix', error) } } } catch (error) { await managerLogger.error('Failed to check high privilege mihomo process', error) } return false } // TUN 模式获取权限 export async function requestTunPermissions(): Promise { if (process.platform === 'win32') { await restartAsAdmin() } else { const hasPermissions = await checkMihomoCorePermissions() if (!hasPermissions) { await grantTunPermissions() } } } export async function checkAdminRestartForTun(): Promise { if (process.argv.includes('--admin-restart-for-tun')) { await managerLogger.info('Detected admin restart for TUN mode, auto-enabling TUN...') try { if (process.platform === 'win32') { const hasAdminPrivileges = await checkAdminPrivileges() if (hasAdminPrivileges) { await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } }) const { checkAutoRun, enableAutoRun } = await import('../sys/autoRun') const autoRunEnabled = await checkAutoRun() if (autoRunEnabled) { await enableAutoRun() } await restartCore() await managerLogger.info('TUN mode auto-enabled after admin restart') const { mainWindow } = await import('../index') mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') } else { await managerLogger.warn('Admin restart detected but no admin privileges found') } } } catch (error) { await managerLogger.error('Failed to auto-enable TUN after admin restart', error) } } else { // 检查 TUN 配置与权限的匹配,但不自动开启 TUN await validateTunPermissionsOnStartup() } } export async function validateTunPermissionsOnStartup(): Promise { try { const { tun } = await getControledMihomoConfig() if (!tun?.enable) { return } const hasPermissions = await checkMihomoCorePermissions() if (!hasPermissions) { await managerLogger.warn( 'TUN is enabled but insufficient permissions detected, prompting user...' ) const confirmed = await showTunPermissionDialog() if (confirmed) { await restartAsAdmin() return } await managerLogger.warn('User declined admin restart, auto-disabling TUN...') await patchControledMihomoConfig({ tun: { enable: false } }) const { mainWindow } = await import('../index') mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') await managerLogger.info('TUN auto-disabled due to insufficient permissions') } else { await managerLogger.info('TUN permissions validated successfully') } } catch (error) { await managerLogger.error('Failed to validate TUN permissions on startup', error) } } export async function manualGrantCorePermition(): Promise { return grantTunPermissions() } export async function getDefaultDevice(): Promise { const execPromise = promisify(exec) const { stdout: deviceOut } = await execPromise(`route -n get default`) let device = deviceOut.split('\n').find((s) => s.includes('interface:')) device = device?.trim().split(' ').slice(1).join(' ') if (!device) throw new Error('Get device failed') return device } async function getDefaultService(): Promise { const execPromise = promisify(exec) const device = await getDefaultDevice() const { stdout: order } = await execPromise(`networksetup -listnetworkserviceorder`) const block = order.split('\n\n').find((s) => s.includes(`Device: ${device}`)) if (!block) throw new Error('Get networkservice failed') for (const line of block.split('\n')) { if (line.match(/^\(\d+\).*/)) { return line.trim().split(' ').slice(1).join(' ') } } throw new Error('Get service failed') } async function getOriginDNS(): Promise { const execPromise = promisify(exec) const service = await getDefaultService() const { stdout: dns } = await execPromise(`networksetup -getdnsservers "${service}"`) if (dns.startsWith("There aren't any DNS Servers set on")) { await patchAppConfig({ originDNS: 'Empty' }) } else { await patchAppConfig({ originDNS: dns.trim().replace(/\n/g, ' ') }) } } async function setDNS(dns: string): Promise { const service = await getDefaultService() const execPromise = promisify(exec) await execPromise(`networksetup -setdnsservers "${service}" ${dns}`) } async function setPublicDNS(): Promise { if (process.platform !== 'darwin') return if (net.isOnline()) { const { originDNS } = await getAppConfig() if (!originDNS) { await getOriginDNS() await setDNS('223.5.5.5') } } else { if (setPublicDNSTimer) clearTimeout(setPublicDNSTimer) setPublicDNSTimer = setTimeout(() => setPublicDNS(), 5000) } } async function recoverDNS(): Promise { if (process.platform !== 'darwin') return if (net.isOnline()) { const { originDNS } = await getAppConfig() if (originDNS) { await setDNS(originDNS) await patchAppConfig({ originDNS: undefined }) } } else { if (recoverDNSTimer) clearTimeout(recoverDNSTimer) recoverDNSTimer = setTimeout(() => recoverDNS(), 5000) } }