support manual grant core permition

This commit is contained in:
pompurin404 2024-08-25 14:42:09 +08:00
parent 41efcd910c
commit c72618570a
No known key found for this signature in database
7 changed files with 841 additions and 764 deletions

View File

@ -1,3 +1,7 @@
### New Features
- Linux支持手动授权内核
### Bug Fixes ### Bug Fixes
- 修改混合端口后系统代理没有更新 - 修改混合端口后系统代理没有更新

View File

@ -25,7 +25,7 @@
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@mihomo-party/sysproxy": "^2.0.0", "@mihomo-party/sysproxy": "^2.0.0",
"adm-zip": "^0.5.15", "adm-zip": "^0.5.15",
"axios": "^1.7.3", "axios": "^1.7.5",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"webdav": "^5.7.1", "webdav": "^5.7.1",
"ws": "^8.18.0", "ws": "^8.18.0",
@ -40,22 +40,22 @@
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@nextui-org/react": "^2.4.6", "@nextui-org/react": "^2.4.6",
"@types/adm-zip": "^0.5.5", "@types/adm-zip": "^0.5.5",
"@types/node": "^22.1.0", "@types/node": "^22.5.0",
"@types/pubsub-js": "^1.8.6", "@types/pubsub-js": "^1.8.6",
"@types/react": "^18.3.3", "@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",
"apexcharts": "^3.52.0", "apexcharts": "^3.52.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"electron": "^31.3.1", "electron": "^31.4.0",
"electron-builder": "^25.0.3", "electron-builder": "^25.0.5",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.0", "eslint-plugin-react": "^7.35.0",
"framer-motion": "^11.3.21", "framer-motion": "^11.3.30",
"meta-json-schema": "^1.18.6", "meta-json-schema": "^1.18.7",
"monaco-yaml": "^5.2.2", "monaco-yaml": "^5.2.2",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
@ -66,18 +66,18 @@
"react-apexcharts": "^1.4.1", "react-apexcharts": "^1.4.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13", "react-error-boundary": "^4.0.13",
"react-icons": "^5.2.1", "react-icons": "^5.3.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-monaco-editor": "^0.56.0", "react-monaco-editor": "^0.56.1",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.1",
"react-virtuoso": "^4.9.0", "react-virtuoso": "^4.10.1",
"swr": "^2.2.5", "swr": "^2.2.5",
"tailwindcss": "^3.4.7", "tailwindcss": "^3.4.10",
"tar": "^7.4.3", "tar": "^7.4.3",
"tsx": "^4.16.5", "tsx": "^4.18.0",
"types-pac": "^1.0.2", "types-pac": "^1.0.2",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.3.5", "vite": "^5.4.2",
"vite-plugin-monaco-editor": "^1.1.0" "vite-plugin-monaco-editor": "^1.1.0"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,12 @@ import {
mihomoWorkDir mihomoWorkDir
} from '../utils/dirs' } from '../utils/dirs'
import { generateProfile } from './factory' import { generateProfile } from './factory'
import { getAppConfig, patchAppConfig } from '../config' import { getAppConfig, patchAppConfig, patchControledMihomoConfig } from '../config'
import { dialog, safeStorage } from 'electron' import { dialog, safeStorage } from 'electron'
import { pauseWebsockets } from './mihomoApi' import { pauseWebsockets } from './mihomoApi'
import { writeFile } from 'fs/promises' import { writeFile } from 'fs/promises'
import { promisify } from 'util' import { promisify } from 'util'
import { mainWindow } from '..'
let child: ChildProcess let child: ChildProcess
let retry = 10 let retry = 10
@ -42,6 +43,11 @@ export async function startCore(): Promise<void> {
stopCore() stopCore()
await startCore() await startCore()
} }
if (data.toString().includes('configure tun interface: operation not permitted')) {
await patchControledMihomoConfig({ tun: { enable: false } })
mainWindow?.webContents.send('controledMihomoConfigUpdated')
dialog.showErrorBox('虚拟网卡启动失败', '请尝试手动授予内核权限')
}
if (data.toString().includes('External controller listen error')) { if (data.toString().includes('External controller listen error')) {
if (retry) { if (retry) {
retry-- retry--
@ -121,7 +127,7 @@ export async function autoGrantCorePermition(corePath: string): Promise<void> {
} }
} }
export async function manualGrantCorePermition(): Promise<void> { export async function manualGrantCorePermition(password?: string): Promise<void> {
const { core = 'mihomo' } = await getAppConfig() const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core) const corePath = mihomoCorePath(core)
const execPromise = promisify(exec) const execPromise = promisify(exec)
@ -130,6 +136,11 @@ export async function manualGrantCorePermition(): Promise<void> {
const command = `do shell script "${shell}" with administrator privileges` const command = `do shell script "${shell}" with administrator privileges`
await execPromise(`osascript -e '${command}'`) await execPromise(`osascript -e '${command}'`)
} }
if (process.platform === 'linux') {
await execPromise(
`echo "${password}" | sudo -S setcap cap_net_bind_service,cap_net_admin,cap_sys_ptrace,cap_dac_read_search,cap_dac_override,cap_net_raw=+ep ${corePath}`
)
}
} }
export function isEncryptionAvailable(): boolean { export function isEncryptionAvailable(): boolean {

View File

@ -143,7 +143,9 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable)) ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable) ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable)
ipcMain.handle('encryptString', (_e, str) => encryptString(str)) ipcMain.handle('encryptString', (_e, str) => encryptString(str))
ipcMain.handle('manualGrantCorePermition', ipcErrorWrapper(manualGrantCorePermition)) ipcMain.handle('manualGrantCorePermition', (_e, password) =>
ipcErrorWrapper(manualGrantCorePermition)(password)
)
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext)) ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath)) ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr)) ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))

View File

@ -6,11 +6,13 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import { manualGrantCorePermition, restartCore, setupFirewall } from '@renderer/utils/ipc' import { manualGrantCorePermition, restartCore, setupFirewall } from '@renderer/utils/ipc'
import { platform } from '@renderer/utils/init' import { platform } from '@renderer/utils/init'
import React, { Key, useState } from 'react' import React, { Key, useState } from 'react'
import BasePasswordModal from '@renderer/components/base/base-password-modal'
const Tun: React.FC = () => { const Tun: React.FC = () => {
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig() const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { tun } = controledMihomoConfig || {} const { tun } = controledMihomoConfig || {}
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [openPasswordModal, setOpenPasswordModal] = useState(false)
const { const {
device = 'Mihomo', device = 'Mihomo',
stack = 'mixed', stack = 'mixed',
@ -39,162 +41,179 @@ const Tun: React.FC = () => {
} }
return ( return (
<BasePage <>
title="Tun 设置" {openPasswordModal && (
header={ <BasePasswordModal
<Button onCancel={() => setOpenPasswordModal(false)}
size="sm" onConfirm={async (password: string) => {
color="primary" try {
onPress={() => await manualGrantCorePermition(password)
onSave({ new Notification('内核授权成功')
tun: { await restartCore()
device: values.device, setOpenPasswordModal(false)
stack: values.stack, } catch (e) {
'auto-route': values.autoRoute, alert(e)
'auto-redirect': values.autoRedirect, }
'auto-detect-interface': values.autoDetectInterface, }}
'dns-hijack': values.dnsHijack, />
'strict-route': values.strictRoute, )}
mtu: values.mtu <BasePage
} title="Tun 设置"
}) header={
} <Button
>
</Button>
}
>
<SettingCard>
{platform === 'win32' && (
<SettingItem title="重设防火墙" divider>
<Button
size="sm"
color="primary"
isLoading={loading}
onPress={async () => {
setLoading(true)
try {
await setupFirewall()
new Notification('防火墙重设成功')
await restartCore()
} catch (e) {
alert(e)
} finally {
setLoading(false)
}
}}
>
</Button>
</SettingItem>
)}
{platform === 'darwin' && (
<SettingItem title="手动授权内核" divider>
<Button
size="sm"
color="primary"
isLoading={loading}
onPress={async () => {
setLoading(true)
try {
await manualGrantCorePermition()
new Notification('内核授权成功')
await restartCore()
} catch (e) {
alert(e)
} finally {
setLoading(false)
}
}}
>
</Button>
</SettingItem>
)}
<SettingItem title="Tun 模式堆栈" divider>
<Tabs
size="sm" size="sm"
color="primary" color="primary"
selectedKey={values.stack} onPress={() =>
onSelectionChange={(key: Key) => setValues({ ...values, stack: key as TunStack })} onSave({
tun: {
device: values.device,
stack: values.stack,
'auto-route': values.autoRoute,
'auto-redirect': values.autoRedirect,
'auto-detect-interface': values.autoDetectInterface,
'dns-hijack': values.dnsHijack,
'strict-route': values.strictRoute,
mtu: values.mtu
}
})
}
> >
<Tab key="gvisor" title="用户" />
<Tab key="mixed" title="混合" /> </Button>
<Tab key="system" title="系统" /> }
</Tabs> >
</SettingItem> <SettingCard>
<SettingItem title="Tun 网卡名称" divider> {platform === 'win32' && (
<Input <SettingItem title="重设防火墙" divider>
size="sm" <Button
className="w-[100px]" size="sm"
value={values.device} color="primary"
onValueChange={(v) => { isLoading={loading}
setValues({ ...values, device: v }) onPress={async () => {
}} setLoading(true)
/> try {
</SettingItem> await setupFirewall()
<SettingItem title="严格路由" divider> new Notification('防火墙重设成功')
<Switch await restartCore()
size="sm" } catch (e) {
isSelected={values.strictRoute} alert(e)
onValueChange={(v) => { } finally {
setValues({ ...values, strictRoute: v }) setLoading(false)
}} }
/> }}
</SettingItem> >
<SettingItem title="自动设置全局路由" divider>
<Switch </Button>
size="sm" </SettingItem>
isSelected={values.autoRoute} )}
onValueChange={(v) => { {platform !== 'win32' && (
setValues({ ...values, autoRoute: v }) <SettingItem title="手动授权内核" divider>
}} <Button
/> size="sm"
</SettingItem> color="primary"
{platform === 'linux' && ( onPress={async () => {
<SettingItem title="自动设置TCP重定向" divider> if (platform === 'darwin') {
<Switch try {
await manualGrantCorePermition()
new Notification('内核授权成功')
await restartCore()
} catch (e) {
alert(e)
}
} else {
setOpenPasswordModal(true)
}
}}
>
</Button>
</SettingItem>
)}
<SettingItem title="Tun 模式堆栈" divider>
<Tabs
size="sm" size="sm"
isSelected={values.autoRedirect} color="primary"
selectedKey={values.stack}
onSelectionChange={(key: Key) => setValues({ ...values, stack: key as TunStack })}
>
<Tab key="gvisor" title="用户" />
<Tab key="mixed" title="混合" />
<Tab key="system" title="系统" />
</Tabs>
</SettingItem>
<SettingItem title="Tun 网卡名称" divider>
<Input
size="sm"
className="w-[100px]"
value={values.device}
onValueChange={(v) => { onValueChange={(v) => {
setValues({ ...values, autoRedirect: v }) setValues({ ...values, device: v })
}} }}
/> />
</SettingItem> </SettingItem>
)} <SettingItem title="严格路由" divider>
<SettingItem title="自动选择流量出口接口" divider> <Switch
<Switch size="sm"
size="sm" isSelected={values.strictRoute}
isSelected={values.autoDetectInterface} onValueChange={(v) => {
onValueChange={(v) => { setValues({ ...values, strictRoute: v })
setValues({ ...values, autoDetectInterface: v }) }}
}} />
/> </SettingItem>
</SettingItem> <SettingItem title="自动设置全局路由" divider>
<SettingItem title="MTU" divider> <Switch
<Input size="sm"
size="sm" isSelected={values.autoRoute}
type="number" onValueChange={(v) => {
className="w-[100px]" setValues({ ...values, autoRoute: v })
value={values.mtu.toString()} }}
onValueChange={(v) => { />
setValues({ ...values, mtu: parseInt(v) }) </SettingItem>
}} {platform === 'linux' && (
/> <SettingItem title="自动设置TCP重定向" divider>
</SettingItem> <Switch
<SettingItem title="DNS 劫持"> size="sm"
<Input isSelected={values.autoRedirect}
size="sm" onValueChange={(v) => {
className="w-[50%]" setValues({ ...values, autoRedirect: v })
value={values.dnsHijack.join(',')} }}
onValueChange={(v) => { />
const arr = v !== '' ? v.split(',') : [] </SettingItem>
setValues({ ...values, dnsHijack: arr }) )}
}} <SettingItem title="自动选择流量出口接口" divider>
/> <Switch
</SettingItem> size="sm"
</SettingCard> isSelected={values.autoDetectInterface}
</BasePage> onValueChange={(v) => {
setValues({ ...values, autoDetectInterface: v })
}}
/>
</SettingItem>
<SettingItem title="MTU" divider>
<Input
size="sm"
type="number"
className="w-[100px]"
value={values.mtu.toString()}
onValueChange={(v) => {
setValues({ ...values, mtu: parseInt(v) })
}}
/>
</SettingItem>
<SettingItem title="DNS 劫持">
<Input
size="sm"
className="w-[50%]"
value={values.dnsHijack.join(',')}
onValueChange={(v) => {
const arr = v !== '' ? v.split(',') : []
setValues({ ...values, dnsHijack: arr })
}}
/>
</SettingItem>
</SettingCard>
</BasePage>
</>
) )
} }

View File

@ -213,8 +213,10 @@ export async function encryptString(str: string): Promise<number[]> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('encryptString', str)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('encryptString', str))
} }
export async function manualGrantCorePermition(): Promise<void> { export async function manualGrantCorePermition(password?: string): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition')) return ipcErrorWrapper(
await window.electron.ipcRenderer.invoke('manualGrantCorePermition', password)
)
} }
export async function getFilePath(ext: string[]): Promise<string[] | undefined> { export async function getFilePath(ext: string[]): Promise<string[] | undefined> {