diff --git a/scripts/prepare.mjs b/scripts/prepare.mjs index 633af1a..f0caacc 100644 --- a/scripts/prepare.mjs +++ b/scripts/prepare.mjs @@ -322,32 +322,7 @@ const resolveSysproxy = () => file: 'sysproxy.exe', downloadURL: `https://github.com/mihomo-party-org/sysproxy/releases/download/${arch}/sysproxy.exe` }) -const resolveRunner = () => - resolveResource({ - file: 'mihomo-party-run.exe', - downloadURL: `https://github.com/mihomo-party-org/mihomo-party-run/releases/download/${arch}/mihomo-party-run.exe` - }) -const resolveMonitor = async () => { - const tempDir = path.join(TEMP_DIR, 'TrafficMonitor') - const tempZip = path.join(tempDir, `${arch}.zip`) - if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, { recursive: true }) - } - await downloadFile( - `https://github.com/mihomo-party-org/mihomo-party-run/releases/download/monitor/${arch}.zip`, - tempZip - ) - const zip = new AdmZip(tempZip) - const resDir = path.join(cwd, 'extra', 'files') - const targetPath = path.join(resDir, 'TrafficMonitor') - if (fs.existsSync(targetPath)) { - fs.rmSync(targetPath, { recursive: true }) - } - zip.extractAllTo(targetPath, true) - - console.log(`[INFO]: TrafficMonitor finished`) -} const resolve7zip = () => resolveResource({ @@ -438,18 +413,7 @@ const tasks = [ retry: 5, winOnly: true }, - { - name: 'runner', - func: resolveRunner, - retry: 5, - winOnly: true - }, - { - name: 'monitor', - func: resolveMonitor, - retry: 5, - winOnly: true - }, + { name: 'substore', func: resolveSubstore, diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 5b54e1c..8683f56 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -290,16 +290,53 @@ async function checkProfile(): Promise { } } -export async function manualGrantCorePermition(): Promise { +/** + * 检查TUN模式所需的权限 + * @returns Promise 是否有足够权限 + */ +export async function checkTunPermissions(): Promise { + const { core = 'mihomo' } = await getAppConfig() + const corePath = mihomoCorePath(core) + + try { + if (process.platform === 'win32') { + const execPromise = promisify(exec) + try { + await execPromise('net session') + return true + } catch { + return false + } + } + + 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 + } + } catch { + return false + } + + return false +} + +/** + * 为TUN模式获取必要的权限 + */ +export async function grantTunPermissions(): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) const execPromise = promisify(exec) const execFilePromise = promisify(execFile) + if (process.platform === 'darwin') { const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}` const command = `do shell script "${shell}" with administrator privileges` await execPromise(`osascript -e '${command}'`) } + if (process.platform === 'linux') { await execFilePromise('pkexec', [ 'bash', @@ -307,6 +344,10 @@ export async function manualGrantCorePermition(): Promise { `chown root:root "${corePath}" && chmod +sx "${corePath}"` ]) } + + if (process.platform === 'win32') { + throw new Error('Windows platform requires running as administrator') + } } export async function getDefaultDevice(): Promise { diff --git a/src/main/index.ts b/src/main/index.ts index 9b27aa8..e578a91 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -10,15 +10,12 @@ import { createTray, hideDockIcon, showDockIcon } from './resolve/tray' import { init } from './utils/init' import { join } from 'path' import { initShortcut } from './resolve/shortcut' -import { execSync, spawn, exec } from 'child_process' -import { createElevateTask } from './sys/misc' +import { spawn, exec } from 'child_process' import { promisify } from 'util' import { stat } from 'fs/promises' import { initProfileUpdater } from './core/profileUpdater' -import { existsSync, writeFileSync } from 'fs' -import { exePath, taskDir } from './utils/dirs' -import path from 'path' -import iconv from 'iconv-lite' +import { existsSync } from 'fs' +import { exePath } from './utils/dirs' import { startMonitor } from './resolve/trafficMonitor' import { showFloatingWindow } from './resolve/floatingWindow' import { initI18n } from '../shared/i18n' @@ -69,37 +66,6 @@ async function fixUserDataPermissions(): Promise { let quitTimeout: NodeJS.Timeout | null = null export let mainWindow: BrowserWindow | null = null -// Windows 管理员权限检查(仅在生产模式下) -if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin')) { - try { - createElevateTask() - } catch (createError) { - try { - if (process.argv.slice(1).length > 0) { - writeFileSync(path.join(taskDir(), 'param.txt'), process.argv.slice(1).join(' ')) - } else { - writeFileSync(path.join(taskDir(), 'param.txt'), 'empty') - } - if (!existsSync(path.join(taskDir(), 'mihomo-party-run.exe'))) { - throw new Error('mihomo-party-run.exe not found') - } else { - execSync('%SystemRoot%\\System32\\schtasks.exe /run /tn mihomo-party-run') - } - } catch (e) { - let createErrorStr = `${createError}` - let eStr = `${e}` - try { - createErrorStr = iconv.decode((createError as { stderr: Buffer }).stderr, 'gbk') - eStr = iconv.decode((e as { stderr: Buffer }).stderr, 'gbk') - } catch { - // ignore - } - showSafeErrorBox('common.error.adminRequired', `${createErrorStr}\n${eStr}`) - } finally { - app.exit() - } - } -} async function initApp(): Promise { await fixUserDataPermissions() diff --git a/src/main/resolve/tray.ts b/src/main/resolve/tray.ts index f9410d9..62263e6 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 } from '../core/manager' +import { quitWithoutCore, restartCore, checkTunPermissions, grantTunPermissions } from '../core/manager' import { floatingWindow, triggerFloatingWindow } from './floatingWindow' import { t } from 'i18next' @@ -178,6 +178,23 @@ 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 + } + } + } catch (error) { + console.warn('Permission check failed:', error) + } + await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) } else { await patchControledMihomoConfig({ tun: { enable } }) diff --git a/src/main/sys/autoRun.ts b/src/main/sys/autoRun.ts index ab9bd24..c0edb6e 100644 --- a/src/main/sys/autoRun.ts +++ b/src/main/sys/autoRun.ts @@ -1,4 +1,5 @@ -import { exePath, homeDir, taskDir } from '../utils/dirs' +import { exePath, homeDir } from '../utils/dirs' +import { tmpdir } from 'os' import { mkdir, readFile, rm, writeFile } from 'fs/promises' import { exec } from 'child_process' import { existsSync } from 'fs' @@ -19,7 +20,7 @@ function getTaskXml(): string { InteractiveToken - HighestAvailable + LeastPrivilege @@ -43,8 +44,7 @@ function getTaskXml(): string { - "${path.join(taskDir(), `mihomo-party-run.exe`)}" - "${exePath()}" + "${exePath()}" @@ -81,7 +81,7 @@ export async function checkAutoRun(): Promise { export async function enableAutoRun(): Promise { if (process.platform === 'win32') { const execPromise = promisify(exec) - const taskFilePath = path.join(taskDir(), `${appName}.xml`) + const taskFilePath = path.join(tmpdir(), `${appName}.xml`) await writeFile(taskFilePath, Buffer.from(`\ufeff${getTaskXml()}`, 'utf-16le')) await execPromise( `%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f` diff --git a/src/main/sys/misc.ts b/src/main/sys/misc.ts index e8931a1..d3a9338 100644 --- a/src/main/sys/misc.ts +++ b/src/main/sys/misc.ts @@ -1,4 +1,4 @@ -import { exec, execFile, execSync, spawn } from 'child_process' +import { exec, execFile, spawn } from 'child_process' import { app, dialog, nativeTheme, shell } from 'electron' import { readFile } from 'fs/promises' import path from 'path' @@ -9,11 +9,8 @@ import { mihomoCorePath, overridePath, profilePath, - resourcesDir, - resourcesFilesDir, - taskDir + resourcesDir } from '../utils/dirs' -import { copyFileSync, writeFileSync } from 'fs' export function getFilePath(ext: string[]): string[] | undefined { return dialog.showOpenDialogSync({ @@ -68,56 +65,7 @@ export function setNativeTheme(theme: 'system' | 'light' | 'dark'): void { nativeTheme.themeSource = theme } -function getElevateTaskXml(): string { - return ` - - - - - InteractiveToken - HighestAvailable - - - - Parallel - false - false - false - false - false - - false - false - - true - true - false - false - false - PT0S - 3 - - - - "${path.join(taskDir(), `mihomo-party-run.exe`)}" - "${exePath()}" - - - -` -} -export function createElevateTask(): void { - const taskFilePath = path.join(taskDir(), `mihomo-party-run.xml`) - writeFileSync(taskFilePath, Buffer.from(`\ufeff${getElevateTaskXml()}`, 'utf-16le')) - copyFileSync( - path.join(resourcesFilesDir(), 'mihomo-party-run.exe'), - path.join(taskDir(), 'mihomo-party-run.exe') - ) - execSync( - `%SystemRoot%\\System32\\schtasks.exe /create /tn "mihomo-party-run" /xml "${taskFilePath}" /f` - ) -} export function resetAppConfig(): void { if (process.platform === 'win32') { diff --git a/src/main/sys/sysproxy.ts b/src/main/sys/sysproxy.ts index ea098cd..f76af23 100644 --- a/src/main/sys/sysproxy.ts +++ b/src/main/sys/sysproxy.ts @@ -167,7 +167,7 @@ async function requestSocketRecreation(): Promise { const { promisify } = require('util') const execPromise = promisify(exec) - // Use osascript with administrator privileges (same pattern as manualGrantCorePermition) + // Use osascript with administrator privileges (same pattern as grantTunPermissions) const shell = `pkill -USR1 -f party.mihomo.helper` const command = `do shell script "${shell}" with administrator privileges` await execPromise(`osascript -e '${command}'`) diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index ce1c7ec..a3a76cc 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -56,7 +56,12 @@ import { subStoreFrontendPort, subStorePort } from '../resolve/server' -import { manualGrantCorePermition, quitWithoutCore, restartCore } from '../core/manager' +import { + quitWithoutCore, + restartCore, + checkTunPermissions, + grantTunPermissions +} from '../core/manager' import { triggerSysProxy } from '../sys/sysproxy' import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater' import { @@ -185,7 +190,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('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)()) + ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)()) ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext)) ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath)) ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr)) diff --git a/src/renderer/src/components/sider/tun-switcher.tsx b/src/renderer/src/components/sider/tun-switcher.tsx index 20a5986..145b83a 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 } from '@renderer/utils/ipc' +import { restartCore, checkTunPermissions, grantTunPermissions } from '@renderer/utils/ipc' import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import React from 'react' @@ -38,6 +38,26 @@ 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) + return + } + } else { + return + } + } + } catch (error) { + console.warn('Permission check failed:', error) + } + await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) } else { await patchControledMihomoConfig({ tun: { enable } }) diff --git a/src/renderer/src/locales/en-US.json b/src/renderer/src/locales/en-US.json index afe79b2..8464a83 100644 --- a/src/renderer/src/locales/en-US.json +++ b/src/renderer/src/locales/en-US.json @@ -316,6 +316,8 @@ "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.failed": "Permission authorization failed", "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 c114f5f..3bd7f24 100644 --- a/src/renderer/src/locales/zh-CN.json +++ b/src/renderer/src/locales/zh-CN.json @@ -316,6 +316,8 @@ "tun.notifications.coreAuthSuccess": "内核授权成功", "tun.notifications.firewallResetSuccess": "防火墙重设成功", "tun.error.tunPermissionDenied": "虚拟网卡启动失败,请尝试手动授予内核权限", + "tun.permissions.required": "启用TUN模式需要管理员权限,是否现在授权?", + "tun.permissions.failed": "权限授权失败", "dns.title": "DNS 设置", "dns.enable": "启用 DNS", "dns.enhancedMode.title": "域名映射模式", diff --git a/src/renderer/src/pages/tun.tsx b/src/renderer/src/pages/tun.tsx index 4d1c1d3..9e036a6 100644 --- a/src/renderer/src/pages/tun.tsx +++ b/src/renderer/src/pages/tun.tsx @@ -3,7 +3,7 @@ import BasePage from '@renderer/components/base/base-page' import SettingCard from '@renderer/components/base/base-setting-card' import SettingItem from '@renderer/components/base/base-setting-item' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' -import { manualGrantCorePermition, restartCore, setupFirewall } from '@renderer/utils/ipc' +import { grantTunPermissions, restartCore, setupFirewall } from '@renderer/utils/ipc' import { platform } from '@renderer/utils/init' import React, { Key, useState } from 'react' import { useAppConfig } from '@renderer/hooks/use-app-config' @@ -129,7 +129,7 @@ const Tun: React.FC = () => { color="primary" onPress={async () => { try { - await manualGrantCorePermition() + await grantTunPermissions() new Notification(t('tun.notifications.coreAuthSuccess')) await restartCore() } catch (e) { diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 8900eee..51bab96 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -227,8 +227,14 @@ export async function triggerSysProxy(enable: boolean): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable)) } -export async function manualGrantCorePermition(): Promise { - return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition')) + + +export async function checkTunPermissions(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkTunPermissions')) +} + +export async function grantTunPermissions(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('grantTunPermissions')) } export async function getFilePath(ext: string[]): Promise {