fix: privilege check and elevation restart logic

This commit is contained in:
ezequielnick 2025-08-06 21:41:30 +08:00
parent e27ddbd16e
commit 6b93a59616
7 changed files with 136 additions and 73 deletions

View File

@ -18,7 +18,7 @@ import {
patchControledMihomoConfig,
manageSmartOverride
} from '../config'
import { app, dialog, ipcMain, net } from 'electron'
import { app, ipcMain, net } from 'electron'
import {
startMihomoTraffic,
startMihomoConnections,
@ -290,10 +290,6 @@ async function checkProfile(): Promise<void> {
}
}
/**
* TUN模式所需的权限
* @returns Promise<boolean>
*/
export async function checkTunPermissions(): Promise<boolean> {
const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core)
@ -310,7 +306,6 @@ export async function checkTunPermissions(): Promise<boolean> {
}
if (process.platform === 'darwin' || process.platform === 'linux') {
// Unix系统检查核心文件是否有setuid权限
const { stat } = await import('fs/promises')
const stats = await stat(corePath)
return (stats.mode & 0o4000) !== 0 && stats.uid === 0
@ -322,9 +317,6 @@ export async function checkTunPermissions(): Promise<boolean> {
return false
}
/**
* TUN模式获取必要的权限
*/
export async function grantTunPermissions(): Promise<void> {
const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core)
@ -350,8 +342,6 @@ export async function grantTunPermissions(): Promise<void> {
}
}
// Windows 管理员权限
export async function checkAdminPrivileges(): Promise<boolean> {
if (process.platform !== 'win32') {
return true
@ -366,7 +356,7 @@ export async function checkAdminPrivileges(): Promise<boolean> {
}
}
export async function requestAdminPrivileges(): Promise<void> {
export async function restartAsAdmin(): Promise<void> {
if (process.platform !== 'win32') {
throw new Error('This function is only available on Windows')
}
@ -376,27 +366,101 @@ export async function requestAdminPrivileges(): Promise<void> {
const args = process.argv.slice(1)
try {
const restartArgs = [...args, '--admin-restart-for-tun']
const escapedExePath = exePath.replace(/'/g, "''")
const escapedArgs = args.map(arg => `'${arg.replace(/'/g, "''")}'`).join(', ')
const escapedArgs = restartArgs.map(arg => `'${arg.replace(/'/g, "''")}'`).join(', ')
let command: string
if (args.length > 0) {
if (restartArgs.length > 0) {
command = `powershell -WindowStyle Hidden -Command "Start-Process -FilePath '${escapedExePath}' -ArgumentList ${escapedArgs} -Verb RunAs"`
} else {
command = `powershell -WindowStyle Hidden -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
}
console.log('Requesting admin privileges with command:', command)
console.log('Restarting as administrator with command:', command)
await execPromise(command, { windowsHide: true })
await new Promise(resolve => setTimeout(resolve, 1000))
await new Promise(resolve => setTimeout(resolve, 1500))
const { app } = await import('electron')
app.quit()
} catch (error) {
console.error('Admin privilege request failed:', error)
throw new Error(`Failed to request admin privileges: ${error}`)
console.error('Failed to restart as administrator:', error)
throw new Error(`Failed to restart as administrator: ${error}`)
}
}
export async function checkMihomoCorePermissions(): Promise<boolean> {
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
}
// TUN模式获取权限
export async function requestTunPermissions(): Promise<void> {
if (process.platform === 'win32') {
await restartAsAdmin()
} else {
const hasPermissions = await checkMihomoCorePermissions()
if (!hasPermissions) {
await grantTunPermissions()
}
}
}
export async function checkAdminRestartForTun(): Promise<void> {
if (process.argv.includes('--admin-restart-for-tun')) {
console.log('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 } })
await restartCore()
console.log('TUN mode auto-enabled after admin restart')
const { mainWindow } = await import('../index')
mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu')
} else {
console.warn('Admin restart detected but no admin privileges found')
}
}
} catch (error) {
console.error('Failed to auto-enable TUN after admin restart:', error)
}
} else if (process.platform === 'win32') {
try {
const hasAdminPrivileges = await checkAdminPrivileges()
const { tun } = await getControledMihomoConfig()
if (hasAdminPrivileges && !tun?.enable) {
console.log('Running with admin privileges but TUN is disabled')
const { mainWindow } = await import('../index')
mainWindow?.webContents.send('adminPrivilegesDetected', { tunEnabled: false })
}
} catch (error) {
console.error('Failed to check admin privileges on startup:', error)
}
}
}

View File

@ -3,7 +3,7 @@ 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 } from './core/manager'
import { quitWithoutCore, startCore, stopCore, checkAdminRestartForTun } from './core/manager'
import { triggerSysProxy } from './sys/sysproxy'
import icon from '../../resources/icon.png?asset'
import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
@ -173,6 +173,8 @@ app.whenReady().then(async () => {
const [startPromise] = await startCore()
startPromise.then(async () => {
await initProfileUpdater()
// 上次是否为了开启 TUN 而重启
await checkAdminRestartForTun()
})
} catch (e) {
showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`)

View File

@ -19,7 +19,7 @@ import { mainWindow, showMainWindow, triggerMainWindow } from '..'
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
import { triggerSysProxy } from '../sys/sysproxy'
import { quitWithoutCore, restartCore, checkTunPermissions, grantTunPermissions, checkAdminPrivileges, requestAdminPrivileges } from '../core/manager'
import { quitWithoutCore, restartCore, checkMihomoCorePermissions, requestTunPermissions, restartAsAdmin } from '../core/manager'
import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
import { t } from 'i18next'
@ -178,42 +178,33 @@ export const buildContextMenu = async (): Promise<Menu> => {
const enable = item.checked
try {
if (enable) {
// Windows 管理员权限
if (process.platform === 'win32') {
try {
const isAdmin = await checkAdminPrivileges()
if (!isAdmin) {
try {
await requestAdminPrivileges()
return
} catch (error) {
console.error('Failed to request admin privileges:', error)
item.checked = false
ipcMain.emit('updateTrayMenu')
return
}
}
} catch (error) {
console.warn('Admin check failed:', error)
}
}
// 检查权限
try {
const hasPermissions = await checkMihomoCorePermissions()
if (process.platform !== 'win32') {
try {
const hasPermissions = await checkTunPermissions()
if (!hasPermissions) {
if (!hasPermissions) {
if (process.platform === 'win32') {
try {
await grantTunPermissions()
await restartAsAdmin()
} catch (error) {
console.error('Failed to grant TUN permissions:', error)
console.error('Failed to restart as admin from tray:', error)
item.checked = false
ipcMain.emit('updateTrayMenu')
return
}
} else {
try {
await requestTunPermissions()
} catch (error) {
console.error('Failed to grant TUN permissions from tray:', error)
item.checked = false
ipcMain.emit('updateTrayMenu')
return
}
}
} catch (error) {
console.warn('Permission check failed:', error)
}
} catch (error) {
console.warn('Permission check failed in tray:', error)
}
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })

View File

@ -63,7 +63,9 @@ import {
grantTunPermissions,
manualGrantCorePermition,
checkAdminPrivileges,
requestAdminPrivileges
restartAsAdmin,
checkMihomoCorePermissions,
requestTunPermissions
} from '../core/manager'
import { triggerSysProxy } from '../sys/sysproxy'
import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater'
@ -195,7 +197,9 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
ipcMain.handle('manualGrantCorePermition', () => ipcErrorWrapper(manualGrantCorePermition)())
ipcMain.handle('checkAdminPrivileges', () => ipcErrorWrapper(checkAdminPrivileges)())
ipcMain.handle('requestAdminPrivileges', () => ipcErrorWrapper(requestAdminPrivileges)())
ipcMain.handle('restartAsAdmin', () => ipcErrorWrapper(restartAsAdmin)())
ipcMain.handle('checkMihomoCorePermissions', () => ipcErrorWrapper(checkMihomoCorePermissions)())
ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)())
ipcMain.handle('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)())
ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)())

View File

@ -38,40 +38,40 @@ const TunSwitcher: React.FC<Props> = (props) => {
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const onChange = async (enable: boolean): Promise<void> => {
if (enable) {
// Windows下检查管理员权限
if (window.electron.process.platform === 'win32') {
try {
const isAdmin = await window.electron.ipcRenderer.invoke('checkAdminPrivileges')
if (!isAdmin) {
try {
// 检查内核权限
const hasPermissions = await window.electron.ipcRenderer.invoke('checkMihomoCorePermissions')
if (!hasPermissions) {
if (window.electron.process.platform === 'win32') {
const confirmed = confirm(t('tun.permissions.required'))
if (confirmed) {
try {
const notification = new Notification(t('tun.permissions.requesting'))
await window.electron.ipcRenderer.invoke('requestAdminPrivileges')
const notification = new Notification(t('tun.permissions.restarting'))
await window.electron.ipcRenderer.invoke('restartAsAdmin')
notification.close()
return
} catch (error) {
console.error('Failed to request admin privileges:', error)
console.error('Failed to restart as admin:', error)
alert(t('tun.permissions.failed') + ': ' + error)
return
}
} else {
return
}
} else {
// macOS/Linux下尝试自动获取权限
try {
await window.electron.ipcRenderer.invoke('requestTunPermissions')
} catch (error) {
console.warn('Permission grant failed:', error)
alert(t('tun.permissions.failed') + ': ' + error)
return
}
}
} catch (error) {
console.warn('Admin check failed:', error)
}
}
// macOS/Linux 获取权限
if (window.electron.process.platform !== 'win32') {
try {
await window.electron.ipcRenderer.invoke('manualGrantCorePermition')
} catch (error) {
console.warn('Permission grant failed:', error)
}
} catch (error) {
console.warn('Permission check failed:', error)
}
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })

View File

@ -316,10 +316,11 @@
"tun.notifications.coreAuthSuccess": "Core Authorization Successful",
"tun.notifications.firewallResetSuccess": "Firewall Reset Successful",
"tun.error.tunPermissionDenied": "TUN interface start failed, please try to manually grant core permissions",
"tun.permissions.required": "TUN mode requires administrator privileges. Grant permissions now?",
"tun.permissions.required": "TUN mode requires administrator privileges. Restart the application now to get permissions?",
"tun.permissions.failed": "Permission authorization failed",
"tun.permissions.windowsRestart": "On Windows, you need to restart the application as administrator to use TUN mode",
"tun.permissions.requesting": "Requesting administrator privileges, please click 'Yes' in the UAC dialog...",
"tun.permissions.restarting": "Restarting application with administrator privileges, please click 'Yes' in the UAC dialog...",
"dns.title": "DNS Settings",
"dns.enable": "Enable DNS",
"dns.enhancedMode.title": "Domain Mapping Mode",

View File

@ -316,10 +316,11 @@
"tun.notifications.coreAuthSuccess": "内核授权成功",
"tun.notifications.firewallResetSuccess": "防火墙重设成功",
"tun.error.tunPermissionDenied": "虚拟网卡启动失败,请尝试手动授予内核权限",
"tun.permissions.required": "启用TUN模式需要管理员权限是否现在授权",
"tun.permissions.required": "启用TUN模式需要管理员权限是否现在重启应用获取权限",
"tun.permissions.failed": "权限授权失败",
"tun.permissions.windowsRestart": "Windows下需要以管理员身份重新启动应用才能使用TUN模式",
"tun.permissions.requesting": "正在请求管理员权限请在UAC对话框中点击'是'...",
"tun.permissions.restarting": "正在以管理员权限重启应用请在UAC对话框中点击'是'...",
"dns.title": "DNS 设置",
"dns.enable": "启用 DNS",
"dns.enhancedMode.title": "域名映射模式",