diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 2b9c581..43398dd 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -417,6 +417,125 @@ export async function checkMihomoCorePermissions(): Promise { 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') { + const { stat, existsSync } = await import('fs') + const { promisify } = await import('util') + const statAsync = promisify(stat) + + if (!existsSync(corePath)) { + await managerLogger.info('Core file does not exist') + return false + } + + const stats = await statAsync(corePath) + const hasSetuid = (stats.mode & 0o4000) !== 0 + const isOwnedByRoot = stats.uid === 0 + + await managerLogger.info(`Core file stats - setuid: ${hasSetuid}, owned by root: ${isOwnedByRoot}, mode: ${stats.mode.toString(8)}`) + + return hasSetuid && isOwnedByRoot + } + } 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) + + try { + const { stdout } = await execPromise('tasklist /FI "IMAGENAME eq mihomo.exe" /FO CSV') + const lines = stdout.split('\n').filter(line => line.includes('mihomo.exe')) + + if (lines.length > 0) { + await managerLogger.info(`Found ${lines.length} mihomo 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(`wmic process where "ProcessId=${pid}" get Name,ProcessId,ExecutablePath,CommandLine /format:csv`) + await managerLogger.info(`Process ${pid} info: ${processInfo.substring(0, 200)}`) + + if (processInfo.includes('mihomo')) { + 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 mihomo processes', error) + } + } + + if (process.platform === 'darwin' || process.platform === 'linux') { + const execPromise = promisify(exec) + + try { + const { stdout } = await execPromise('ps aux | grep mihomo | grep -v grep') + const lines = stdout.split('\n').filter(line => line.trim() && line.includes('mihomo')) + + if (lines.length > 0) { + await managerLogger.info(`Found ${lines.length} mihomo processes running`) + + for (const line of lines) { + const parts = line.trim().split(/\s+/) + if (parts.length >= 1) { + const user = parts[0] + await managerLogger.info(`Mihomo process running as user: ${user}`) + + if (user === 'root') { + return true + } + } + } + } + } 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') { diff --git a/src/main/index.ts b/src/main/index.ts index ce921e6..080a55a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,11 +3,11 @@ import { registerIpcMainHandlers } from './utils/ipc' import windowStateKeeper from 'electron-window-state' import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron' import { addProfileItem, getAppConfig, patchAppConfig } from './config' -import { quitWithoutCore, startCore, stopCore, checkAdminRestartForTun } from './core/manager' +import { quitWithoutCore, startCore, stopCore, checkAdminRestartForTun, checkHighPrivilegeCore, restartAsAdmin } from './core/manager' import { triggerSysProxy } from './sys/sysproxy' import icon from '../../resources/icon.png?asset' import { createTray, hideDockIcon, showDockIcon } from './resolve/tray' -import { init } from './utils/init' +import { init, initBasic } from './utils/init' import { join } from 'path' import { initShortcut } from './resolve/shortcut' import { spawn, exec } from 'child_process' @@ -113,7 +113,62 @@ if (process.platform === 'win32' && !exePath().startsWith('C')) { app.commandLine.appendSwitch('in-process-gpu') } -const initPromise = init() +// 内核检测 +async function checkHighPrivilegeCoreEarly(): Promise { + try { + await initBasic() + + // 应用管理员权限运行,跳过检测 + if (process.platform === 'win32') { + const { checkAdminPrivileges } = await import('./core/manager') + const isCurrentAppAdmin = await checkAdminPrivileges() + + if (isCurrentAppAdmin) { + console.log('Current app is running as administrator, skipping privilege check') + return + } + } else if (process.platform === 'darwin' || process.platform === 'linux') { + if (process.getuid && process.getuid() === 0) { + console.log('Current app is running as root, skipping privilege check') + return + } + } + + const hasHighPrivilegeCore = await checkHighPrivilegeCore() + if (hasHighPrivilegeCore) { + try { + const appConfig = await getAppConfig() + const language = appConfig.language || (app.getLocale().startsWith('zh') ? 'zh-CN' : 'en-US') + await initI18n({ lng: language }) + } catch { + await initI18n({ lng: 'zh-CN' }) + } + + const choice = dialog.showMessageBoxSync({ + type: 'warning', + title: i18next.t('core.highPrivilege.title'), + message: i18next.t('core.highPrivilege.message'), + buttons: [i18next.t('common.confirm'), i18next.t('common.cancel')], + defaultId: 0, + cancelId: 1 + }) + + if (choice === 0) { + try { + await restartAsAdmin() + process.exit(0) + } catch (error) { + showSafeErrorBox('common.error.adminRequired', `${error}`) + process.exit(1) + } + } else { + process.exit(0) + } + } + } catch (e) { + console.error('Failed to check high privilege core:', e) + } +} app.on('second-instance', async (_event, commandline) => { showMainWindow() @@ -154,9 +209,10 @@ app.whenReady().then(async () => { // Set app user model id for windows electronApp.setAppUserModelId('party.mihomo.app') + await checkHighPrivilegeCoreEarly() + try { - // 首先等待初始化完成,确保配置文件和目录都已创建 - await initPromise + await init() const appConfig = await getAppConfig() // 如果配置中没有语言设置,则使用系统语言 @@ -170,6 +226,7 @@ app.whenReady().then(async () => { showSafeErrorBox('common.error.initFailed', `${e}`) app.quit() } + try { const [startPromise] = await startCore() startPromise.then(async () => { diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index 262ddd9..d8a0162 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -318,12 +318,17 @@ function initDeeplink(): void { } } -export async function init(): Promise { +// 基础初始化 +export async function initBasic(): Promise { await initDirs() await initConfig() await migration() await initFiles() await cleanup() +} + +export async function init(): Promise { + await initBasic() await startSubStoreFrontendServer() await startSubStoreBackendServer() const { sysProxy } = await getAppConfig() diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 55a0e41..2fdee17 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -65,7 +65,8 @@ import { checkAdminPrivileges, restartAsAdmin, checkMihomoCorePermissions, - requestTunPermissions + requestTunPermissions, + checkHighPrivilegeCore } from '../core/manager' import { triggerSysProxy } from '../sys/sysproxy' import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater' @@ -200,6 +201,7 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('restartAsAdmin', () => ipcErrorWrapper(restartAsAdmin)()) ipcMain.handle('checkMihomoCorePermissions', () => ipcErrorWrapper(checkMihomoCorePermissions)()) ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)()) + ipcMain.handle('checkHighPrivilegeCore', () => ipcErrorWrapper(checkHighPrivilegeCore)()) ipcMain.handle('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)()) ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)()) diff --git a/src/renderer/src/locales/en-US.json b/src/renderer/src/locales/en-US.json index d1564ef..ed016a3 100644 --- a/src/renderer/src/locales/en-US.json +++ b/src/renderer/src/locales/en-US.json @@ -36,6 +36,8 @@ "common.error.shortcutRegistrationFailedWithError": "Failed to register shortcut: {{error}}", "common.error.adminRequired": "Please run with administrator privileges for first launch", "common.error.initFailed": "Application initialization failed", + "core.highPrivilege.title": "High Privilege Core Detected", + "core.highPrivilege.message": "A high privilege core (administrator privileges or setuid bit) has been detected. For security reasons, it is recommended to restart the application with administrator privileges. Restart now?", "common.updater.versionReady": "v{{version}} Version Ready", "common.updater.goToDownload": "Go to Download", "common.updater.update": "Update", diff --git a/src/renderer/src/locales/zh-CN.json b/src/renderer/src/locales/zh-CN.json index e3513ba..2a101d0 100644 --- a/src/renderer/src/locales/zh-CN.json +++ b/src/renderer/src/locales/zh-CN.json @@ -36,6 +36,8 @@ "common.error.shortcutRegistrationFailedWithError": "快捷键注册失败:{{error}}", "common.error.adminRequired": "首次启动请以管理员权限运行", "common.error.initFailed": "应用初始化失败", + "core.highPrivilege.title": "检测到高权限内核", + "core.highPrivilege.message": "检测到当前内核具有高权限(管理员权限或 setuid 位),为了安全起见,建议以管理员权限重启应用。是否现在重启?", "common.updater.versionReady": "v{{version}} 版本就绪", "common.updater.goToDownload": "前往下载", "common.updater.update": "更新", diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 8a46c72..316ecae 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -241,6 +241,18 @@ export async function manualGrantCorePermition(): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition')) } +export async function checkHighPrivilegeCore(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkHighPrivilegeCore')) +} + +export async function checkAdminPrivileges(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkAdminPrivileges')) +} + +export async function restartAsAdmin(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('restartAsAdmin')) +} + export async function getFilePath(ext: string[]): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext)) }