diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 8683f56..590fb5b 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -350,6 +350,60 @@ export async function grantTunPermissions(): Promise { } } +// Windows 管理员权限 + +export async function checkAdminPrivileges(): Promise { + if (process.platform !== 'win32') { + return true + } + + try { + const execPromise = promisify(exec) + await execPromise('net session') + return true + } catch { + return false + } +} + +export async function requestAdminPrivileges(): Promise { + if (process.platform !== 'win32') { + throw new Error('This function is only available on Windows') + } + + const execPromise = promisify(exec) + const exePath = process.execPath + const args = process.argv.slice(1) + + try { + const escapedExePath = exePath.replace(/'/g, "''") + const escapedArgs = args.map(arg => `'${arg.replace(/'/g, "''")}'`).join(', ') + + let command: string + if (args.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) + + await execPromise(command, { windowsHide: true }) + + await new Promise(resolve => setTimeout(resolve, 1000)) + + 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}`) + } +} + +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`) diff --git a/src/main/resolve/tray.ts b/src/main/resolve/tray.ts index 62263e6..be1740a 100644 --- a/src/main/resolve/tray.ts +++ b/src/main/resolve/tray.ts @@ -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 } from '../core/manager' +import { quitWithoutCore, restartCore, checkTunPermissions, grantTunPermissions, checkAdminPrivileges, requestAdminPrivileges } from '../core/manager' import { floatingWindow, triggerFloatingWindow } from './floatingWindow' import { t } from 'i18next' @@ -178,21 +178,42 @@ export const buildContextMenu = async (): Promise => { const enable = item.checked try { if (enable) { - // 检查TUN权限 - try { - const hasPermissions = await checkTunPermissions() - if (!hasPermissions) { - try { - await grantTunPermissions() - } catch (error) { - console.error('Failed to grant TUN permissions:', error) - item.checked = false - ipcMain.emit('updateTrayMenu') - return + // 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) + } + } + + if (process.platform !== 'win32') { + try { + const hasPermissions = await checkTunPermissions() + if (!hasPermissions) { + try { + await grantTunPermissions() + } catch (error) { + console.error('Failed to grant TUN permissions:', error) + item.checked = false + ipcMain.emit('updateTrayMenu') + return + } + } + } catch (error) { + console.warn('Permission check failed:', error) } - } catch (error) { - console.warn('Permission check failed:', error) } await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index a3a76cc..15b5c95 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -60,7 +60,10 @@ import { quitWithoutCore, restartCore, checkTunPermissions, - grantTunPermissions + grantTunPermissions, + manualGrantCorePermition, + checkAdminPrivileges, + requestAdminPrivileges } from '../core/manager' import { triggerSysProxy } from '../sys/sysproxy' import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater' @@ -190,6 +193,9 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('restartCore', ipcErrorWrapper(restartCore)) ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached)) 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('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)()) ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)()) diff --git a/src/renderer/src/components/sider/tun-switcher.tsx b/src/renderer/src/components/sider/tun-switcher.tsx index 145b83a..88b6d54 100644 --- a/src/renderer/src/components/sider/tun-switcher.tsx +++ b/src/renderer/src/components/sider/tun-switcher.tsx @@ -3,7 +3,7 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c import BorderSwitch from '@renderer/components/base/border-swtich' import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb' import { useLocation, useNavigate } from 'react-router-dom' -import { restartCore, checkTunPermissions, grantTunPermissions } from '@renderer/utils/ipc' +import { restartCore } from '@renderer/utils/ipc' import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import React from 'react' @@ -38,24 +38,40 @@ const TunSwitcher: React.FC = (props) => { const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null const onChange = async (enable: boolean): Promise => { if (enable) { - // 检查TUN权限 - try { - const hasPermissions = await checkTunPermissions() - if (!hasPermissions) { - const confirmed = confirm(t('tun.permissions.required')) - if (confirmed) { - try { - await grantTunPermissions() - } catch (error) { - alert(t('tun.permissions.failed') + ': ' + error) + // Windows下检查管理员权限 + if (window.electron.process.platform === 'win32') { + try { + const isAdmin = await window.electron.ipcRenderer.invoke('checkAdminPrivileges') + if (!isAdmin) { + const confirmed = confirm(t('tun.permissions.required')) + if (confirmed) { + try { + const notification = new Notification(t('tun.permissions.requesting')) + + await window.electron.ipcRenderer.invoke('requestAdminPrivileges') + notification.close() + return + } catch (error) { + console.error('Failed to request admin privileges:', error) + alert(t('tun.permissions.failed') + ': ' + error) + return + } + } else { return } - } else { - 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 } }) diff --git a/src/renderer/src/locales/en-US.json b/src/renderer/src/locales/en-US.json index 8464a83..87023a9 100644 --- a/src/renderer/src/locales/en-US.json +++ b/src/renderer/src/locales/en-US.json @@ -318,6 +318,8 @@ "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.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...", "dns.title": "DNS Settings", "dns.enable": "Enable DNS", "dns.enhancedMode.title": "Domain Mapping Mode", diff --git a/src/renderer/src/locales/zh-CN.json b/src/renderer/src/locales/zh-CN.json index 3bd7f24..594a35b 100644 --- a/src/renderer/src/locales/zh-CN.json +++ b/src/renderer/src/locales/zh-CN.json @@ -318,6 +318,8 @@ "tun.error.tunPermissionDenied": "虚拟网卡启动失败,请尝试手动授予内核权限", "tun.permissions.required": "启用TUN模式需要管理员权限,是否现在授权?", "tun.permissions.failed": "权限授权失败", + "tun.permissions.windowsRestart": "Windows下需要以管理员身份重新启动应用才能使用TUN模式", + "tun.permissions.requesting": "正在请求管理员权限,请在UAC对话框中点击'是'...", "dns.title": "DNS 设置", "dns.enable": "启用 DNS", "dns.enhancedMode.title": "域名映射模式", diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 51bab96..8a46c72 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -237,6 +237,10 @@ export async function grantTunPermissions(): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('grantTunPermissions')) } +export async function manualGrantCorePermition(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition')) +} + export async function getFilePath(ext: string[]): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext)) }