diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 81916554..79d25cec 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -7,13 +7,15 @@ import { mihomoWorkDir } from '../utils/dirs' import { generateProfile } from '../resolve/factory' -import { getAppConfig } from '../config' +import { getAppConfig, setAppConfig } from '../config' +import { safeStorage } from 'electron' import fs from 'fs' let child: ChildProcess export function startCore(): void { const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo') + grantCorePermition(corePath) generateProfile() checkProfile() stopCore() @@ -51,3 +53,26 @@ export function checkProfile(): void { const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo') execFileSync(corePath, ['-t', '-f', mihomoWorkConfigPath(), '-d', mihomoTestDir()]) } + +export function grantCorePermition(corePath: string): void { + if (getAppConfig().encryptedPassword && isEncryptionAvailable()) { + const password = safeStorage.decryptString(Buffer.from(getAppConfig().encryptedPassword ?? [])) + try { + if (process.platform === 'linux') { + execSync( + `echo "${password}" | sudo -S setcap cap_net_bind_service,cap_net_admin,cap_dac_override,cap_net_raw=+ep ${corePath}` + ) + } + if (process.platform === 'darwin') { + execSync(`echo "${password}" | sudo -S chown root:admin ${corePath}`) + execSync(`echo "${password}" | sudo -S chmod +sx ${corePath}`) + } + } catch (e) { + setAppConfig({ encryptedPassword: undefined }) + } + } +} + +export function isEncryptionAvailable(): boolean { + return safeStorage.isEncryptionAvailable() +} diff --git a/src/main/index.ts b/src/main/index.ts index c9b19bb2..644d8ac6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -23,8 +23,6 @@ if (!gotTheLock) { app.quit() } else { init() - startCore() - app.on('second-instance', () => { window?.show() window?.focusOnWebView() @@ -51,7 +49,7 @@ if (!gotTheLock) { app.whenReady().then(() => { // Set app user model id for windows electronApp.setAppUserModelId('party.mihomo.app') - + startCore() // Default open or close DevTools by F12 in development // and ignore CommandOrControl + R in production. // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils diff --git a/src/main/resolve/factory.ts b/src/main/resolve/factory.ts index 77cdcdb0..f65fc4a1 100644 --- a/src/main/resolve/factory.ts +++ b/src/main/resolve/factory.ts @@ -10,7 +10,6 @@ export function generateProfile(): void { const { tun: controledTun } = controledMihomoConfig const tun = Object.assign(profileTun, controledTun) const profile = Object.assign(currentProfile, controledMihomoConfig) - console.log('profile', profile) profile.tun = tun fs.writeFileSync(mihomoWorkConfigPath(), yaml.stringify(profile)) } diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 176e3aef..b158de92 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -1,4 +1,4 @@ -import { app, ipcMain } from 'electron' +import { app, ipcMain, safeStorage } from 'electron' import { mihomoChangeProxy, mihomoCloseAllConnections, @@ -25,7 +25,7 @@ import { addProfileItem, removeProfileItem } from '../config' -import { restartCore } from '../core/manager' +import { isEncryptionAvailable, restartCore } from '../core/manager' import { triggerSysProxy } from '../resolve/sysproxy' import { changeCurrentProfile } from '../config/profile' @@ -57,5 +57,7 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('removeProfileItem', (_e, id) => removeProfileItem(id)) ipcMain.handle('restartCore', restartCore) ipcMain.handle('triggerSysProxy', (_e, enable) => triggerSysProxy(enable)) + ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable) + ipcMain.handle('encryptString', (_e, str) => safeStorage.encryptString(str)) ipcMain.handle('quitApp', () => app.quit()) } diff --git a/src/renderer/src/components/base/base-password-modal.tsx b/src/renderer/src/components/base/base-password-modal.tsx new file mode 100644 index 00000000..f53b54d4 --- /dev/null +++ b/src/renderer/src/components/base/base-password-modal.tsx @@ -0,0 +1,41 @@ +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + Button, + Input +} from '@nextui-org/react' +import React, { useState } from 'react' + +interface Props { + onCancel: () => void + onConfirm: (script: string) => void +} + +const BasePasswordModal: React.FC = (props) => { + const { onCancel, onConfirm } = props + const [password, setPassword] = useState('') + + return ( + + + 请输入root密码 + + + + + + + + + + ) +} + +export default BasePasswordModal diff --git a/src/renderer/src/components/sider/tun-switcher.tsx b/src/renderer/src/components/sider/tun-switcher.tsx index fc26a140..e25e62ec 100644 --- a/src/renderer/src/components/sider/tun-switcher.tsx +++ b/src/renderer/src/components/sider/tun-switcher.tsx @@ -3,50 +3,73 @@ 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 { patchMihomoConfig } from '@renderer/utils/ipc' -import React from 'react' +import { encryptString, patchMihomoConfig, isEncryptionAvailable } from '@renderer/utils/ipc' +import React, { useState } from 'react' +import { useAppConfig } from '@renderer/hooks/use-app-config' +import BasePasswordModal from '../base/base-password-modal' const TunSwitcher: React.FC = () => { const navigate = useNavigate() const location = useLocation() const match = location.pathname.includes('/tun') - + const [openPasswordModal, setOpenPasswordModal] = useState(false) + const { appConfig, patchAppConfig } = useAppConfig() const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig(true) const { tun } = controledMihomoConfig || {} const { enable } = tun || {} const onChange = async (enable: boolean): Promise => { + const encryptionAvailable = await isEncryptionAvailable() + if (!appConfig?.encryptedPassword && encryptionAvailable) { + setOpenPasswordModal(true) + return + } + if (!encryptionAvailable) { + alert('加密不可用,请手动给内核授权') + } await patchControledMihomoConfig({ tun: { enable } }) await patchMihomoConfig({ tun: { enable } }) } return ( - navigate('/tun')} - > - -
- - -
-
- -

虚拟网卡

-
-
+ <> + {openPasswordModal && ( + setOpenPasswordModal(false)} + onConfirm={async (password: string) => { + const encrypted = await encryptString(password) + patchAppConfig({ encryptedPassword: encrypted }) + setOpenPasswordModal(false) + }} + /> + )} + navigate('/tun')} + > + +
+ + +
+
+ +

虚拟网卡

+
+
+ ) } diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index b246f0ef..a376565c 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -106,6 +106,13 @@ export async function triggerSysProxy(enable: boolean): Promise { return await window.electron.ipcRenderer.invoke('triggerSysProxy', enable) } +export async function isEncryptionAvailable(): Promise { + return await window.electron.ipcRenderer.invoke('isEncryptionAvailable') +} + +export async function encryptString(str: string): Promise { + return await window.electron.ipcRenderer.invoke('encryptString', str) +} export async function quitApp(): Promise { return await window.electron.ipcRenderer.invoke('quitApp') } diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index a52b5daf..6ba4e87b 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -136,6 +136,7 @@ interface IAppConfig { userAgent?: string delayTestUrl?: string delayTestTimeout?: number + encryptedPassword?: Buffer } interface IMihomoTunConfig {