mirror of
https://github.com/pompurin404/mihomo-party.git
synced 2024-11-16 03:32:17 +08:00
support manual grant core permition
This commit is contained in:
parent
41efcd910c
commit
c72618570a
|
@ -1,3 +1,7 @@
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- Linux支持手动授权内核
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- 修改混合端口后系统代理没有更新
|
- 修改混合端口后系统代理没有更新
|
||||||
|
|
28
package.json
28
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1233
pnpm-lock.yaml
1233
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user