mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-31 11:12:07 +08:00
Support the dns-mode field introduced in mihomo v1.19.33. A button group below the stack selector offers Auto/Disabled/Native/Hijack; Auto writes an empty string, which the core treats as the legacy behavior. Adding dns-mode to GUI_KEYS makes the value GUI-authoritative once saved, matching the other TUN dialog fields.
382 lines
12 KiB
TypeScript
382 lines
12 KiB
TypeScript
import {
|
|
Box,
|
|
Button,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
TextField,
|
|
Typography,
|
|
} from '@mui/material'
|
|
import { useLockFn } from 'ahooks'
|
|
import type { Ref } from 'react'
|
|
import { useImperativeHandle, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import {
|
|
BaseDialog,
|
|
BaseSplitChipEditor,
|
|
TooltipIcon,
|
|
DialogRef,
|
|
Switch,
|
|
} from '@/components/base'
|
|
import { useClash } from '@/hooks/use-clash'
|
|
import { enhanceProfiles } from '@/services/cmds'
|
|
import { mutate } from '@/services/mutate'
|
|
import { showNotice } from '@/services/notice-service'
|
|
import getSystem from '@/utils/get-system'
|
|
import { areValidIpCidrs } from '@/utils/network'
|
|
|
|
import { DnsModeSwitch } from './dns-mode-switch'
|
|
import { StackModeSwitch } from './stack-mode-switch'
|
|
|
|
const OS = getSystem()
|
|
|
|
const splitRouteExcludeAddress = (value: string) =>
|
|
value
|
|
.split(/[,\n;\r]+/)
|
|
.map((item) => item.trim())
|
|
.filter(Boolean)
|
|
|
|
export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|
const { t } = useTranslation()
|
|
|
|
const { clash, mutateClash, patchClash } = useClash()
|
|
|
|
const [open, setOpen] = useState(false)
|
|
const [values, setValues] = useState({
|
|
stack: 'mips',
|
|
device: OS === 'macos' ? 'utun1024' : 'Mihomo',
|
|
autoRoute: true,
|
|
routeExcludeAddress: '',
|
|
autoRedirect: false,
|
|
autoDetectInterface: true,
|
|
dnsHijack: ['any:53'],
|
|
strictRoute: false,
|
|
mtu: 1500,
|
|
dnsMode: '',
|
|
})
|
|
|
|
const routeExcludeAddressItems = splitRouteExcludeAddress(
|
|
values.routeExcludeAddress,
|
|
)
|
|
const routeExcludeAddressError =
|
|
values.autoRoute &&
|
|
routeExcludeAddressItems.length > 0 &&
|
|
!areValidIpCidrs(routeExcludeAddressItems)
|
|
const routeExcludeAddressHelperText = routeExcludeAddressError
|
|
? t('settings.modals.tun.messages.invalidRouteExcludeAddress')
|
|
: t('settings.modals.tun.messages.routeExcludeAddressHint')
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
open: () => {
|
|
setOpen(true)
|
|
const nextAutoRoute = clash?.tun['auto-route'] ?? true
|
|
const rawAutoRedirect = clash?.tun['auto-redirect'] ?? false
|
|
const computedAutoRedirect =
|
|
OS === 'linux' ? (nextAutoRoute ? rawAutoRedirect : false) : false
|
|
setValues({
|
|
stack: clash?.tun.stack ?? 'mips',
|
|
device: clash?.tun.device ?? (OS === 'macos' ? 'utun1024' : 'Mihomo'),
|
|
autoRoute: nextAutoRoute,
|
|
routeExcludeAddress: (clash?.tun['route-exclude-address'] ?? []).join(
|
|
',',
|
|
),
|
|
autoRedirect: computedAutoRedirect,
|
|
autoDetectInterface: clash?.tun['auto-detect-interface'] ?? true,
|
|
dnsHijack: clash?.tun['dns-hijack'] ?? ['any:53'],
|
|
strictRoute: clash?.tun['strict-route'] ?? false,
|
|
mtu: clash?.tun.mtu ?? 1500,
|
|
dnsMode: clash?.tun['dns-mode'] ?? '',
|
|
})
|
|
},
|
|
close: () => setOpen(false),
|
|
}))
|
|
|
|
const onSave = useLockFn(async () => {
|
|
try {
|
|
const routeExcludeAddress = routeExcludeAddressItems
|
|
|
|
if (routeExcludeAddressError) {
|
|
showNotice.error(
|
|
'settings.modals.tun.messages.invalidRouteExcludeAddress',
|
|
)
|
|
return
|
|
}
|
|
|
|
const tun: IConfigData['tun'] = {
|
|
stack: values.stack,
|
|
device:
|
|
values.device === ''
|
|
? OS === 'macos'
|
|
? 'utun1024'
|
|
: 'Mihomo'
|
|
: values.device,
|
|
'auto-route': values.autoRoute,
|
|
'route-exclude-address': routeExcludeAddress,
|
|
...(OS === 'linux'
|
|
? {
|
|
'auto-redirect': values.autoRedirect,
|
|
}
|
|
: {}),
|
|
'auto-detect-interface': values.autoDetectInterface,
|
|
'dns-hijack': values.dnsHijack[0] === '' ? [] : values.dnsHijack,
|
|
'dns-mode': values.dnsMode,
|
|
'strict-route': values.strictRoute,
|
|
mtu: values.mtu ?? 1500,
|
|
}
|
|
await patchClash({ tun })
|
|
await mutateClash(
|
|
(old) => ({
|
|
...old!,
|
|
tun,
|
|
}),
|
|
false,
|
|
)
|
|
setOpen(false)
|
|
showNotice.success('settings.modals.tun.messages.applied')
|
|
void mutate(() => enhanceProfiles(), {
|
|
id: 'enhance-profiles',
|
|
errorNotice: false,
|
|
}).catch((err: any) => {
|
|
showNotice.error(err)
|
|
})
|
|
} catch (err: any) {
|
|
showNotice.error(err)
|
|
}
|
|
})
|
|
|
|
return (
|
|
<BaseDialog
|
|
open={open}
|
|
title={
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
|
<Typography variant="h6">{t('settings.modals.tun.title')}</Typography>
|
|
<Button
|
|
variant="outlined"
|
|
size="small"
|
|
onClick={async () => {
|
|
const tun: IConfigData['tun'] = {
|
|
stack: 'mips',
|
|
device: OS === 'macos' ? 'utun1024' : 'Mihomo',
|
|
'auto-route': true,
|
|
...(OS === 'linux'
|
|
? {
|
|
'auto-redirect': false,
|
|
}
|
|
: {}),
|
|
'auto-detect-interface': true,
|
|
'dns-hijack': ['any:53'],
|
|
'route-exclude-address': [],
|
|
'dns-mode': '',
|
|
'strict-route': false,
|
|
mtu: 1500,
|
|
}
|
|
setValues({
|
|
stack: 'mips',
|
|
device: OS === 'macos' ? 'utun1024' : 'Mihomo',
|
|
autoRoute: true,
|
|
routeExcludeAddress: '',
|
|
autoRedirect: false,
|
|
autoDetectInterface: true,
|
|
dnsHijack: ['any:53'],
|
|
strictRoute: false,
|
|
mtu: 1500,
|
|
dnsMode: '',
|
|
})
|
|
await patchClash({ tun })
|
|
await mutateClash(
|
|
(old) => ({
|
|
...old!,
|
|
tun,
|
|
}),
|
|
false,
|
|
)
|
|
}}
|
|
>
|
|
{t('shared.actions.resetToDefault')}
|
|
</Button>
|
|
</Box>
|
|
}
|
|
contentSx={{ width: 450 }}
|
|
okBtn={t('shared.actions.save')}
|
|
cancelBtn={t('shared.actions.cancel')}
|
|
onClose={() => setOpen(false)}
|
|
onCancel={() => setOpen(false)}
|
|
onOk={onSave}
|
|
>
|
|
<List>
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText primary={t('settings.modals.tun.fields.stack')} />
|
|
<StackModeSwitch
|
|
value={values.stack}
|
|
onChange={(value) => {
|
|
setValues((v) => ({
|
|
...v,
|
|
stack: value,
|
|
}))
|
|
}}
|
|
/>
|
|
</ListItem>
|
|
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.tun.fields.dnsMode')}
|
|
sx={{ maxWidth: 'fit-content' }}
|
|
/>
|
|
<TooltipIcon
|
|
title={t('settings.modals.tun.tooltips.dnsMode')}
|
|
sx={{ opacity: 0.7 }}
|
|
/>
|
|
<Box sx={{ marginLeft: 'auto' }}>
|
|
<DnsModeSwitch
|
|
value={values.dnsMode}
|
|
onChange={(value) => {
|
|
setValues((v) => ({ ...v, dnsMode: value }))
|
|
}}
|
|
/>
|
|
</Box>
|
|
</ListItem>
|
|
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText primary={t('settings.modals.tun.fields.device')} />
|
|
<TextField
|
|
autoComplete="new-password"
|
|
size="small"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
sx={{ width: 250 }}
|
|
value={values.device}
|
|
placeholder="Mihomo"
|
|
onChange={(e) =>
|
|
setValues((v) => ({ ...v, device: e.target.value }))
|
|
}
|
|
/>
|
|
</ListItem>
|
|
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText primary={t('settings.modals.tun.fields.autoRoute')} />
|
|
<Switch
|
|
edge="end"
|
|
checked={values.autoRoute}
|
|
onChange={(_, c) =>
|
|
setValues((v) => ({
|
|
...v,
|
|
autoRoute: c,
|
|
autoRedirect: c ? v.autoRedirect : false,
|
|
}))
|
|
}
|
|
/>
|
|
</ListItem>
|
|
|
|
{OS === 'linux' && (
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.tun.fields.autoRedirect')}
|
|
sx={{ maxWidth: 'fit-content' }}
|
|
/>
|
|
<TooltipIcon
|
|
title={t('settings.modals.tun.tooltips.autoRedirect')}
|
|
sx={{ opacity: values.autoRoute ? 0.7 : 0.3 }}
|
|
/>
|
|
<Switch
|
|
edge="end"
|
|
checked={values.autoRedirect}
|
|
onChange={(_, c) =>
|
|
setValues((v) => ({
|
|
...v,
|
|
autoRedirect: v.autoRoute ? c : v.autoRedirect,
|
|
}))
|
|
}
|
|
disabled={!values.autoRoute}
|
|
sx={{ marginLeft: 'auto' }}
|
|
/>
|
|
</ListItem>
|
|
)}
|
|
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText primary={t('settings.modals.tun.fields.strictRoute')} />
|
|
<Switch
|
|
edge="end"
|
|
checked={values.strictRoute}
|
|
onChange={(_, c) => setValues((v) => ({ ...v, strictRoute: c }))}
|
|
/>
|
|
</ListItem>
|
|
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.tun.fields.autoDetectInterface')}
|
|
/>
|
|
<Switch
|
|
edge="end"
|
|
checked={values.autoDetectInterface}
|
|
onChange={(_, c) =>
|
|
setValues((v) => ({ ...v, autoDetectInterface: c }))
|
|
}
|
|
/>
|
|
</ListItem>
|
|
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText primary={t('settings.modals.tun.fields.dnsHijack')} />
|
|
<TextField
|
|
autoComplete="new-password"
|
|
size="small"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
sx={{ width: 250 }}
|
|
value={values.dnsHijack.join(',')}
|
|
placeholder={t('settings.modals.tun.tooltips.dnsHijack')}
|
|
onChange={(e) =>
|
|
setValues((v) => ({ ...v, dnsHijack: e.target.value.split(',') }))
|
|
}
|
|
/>
|
|
</ListItem>
|
|
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText primary={t('settings.modals.tun.fields.mtu')} />
|
|
<TextField
|
|
autoComplete="new-password"
|
|
size="small"
|
|
type="number"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
sx={{ width: 250 }}
|
|
value={values.mtu}
|
|
placeholder="1500"
|
|
onChange={(e) =>
|
|
setValues((v) => ({
|
|
...v,
|
|
mtu: parseInt(e.target.value),
|
|
}))
|
|
}
|
|
/>
|
|
</ListItem>
|
|
|
|
<BaseSplitChipEditor
|
|
value={values.routeExcludeAddress}
|
|
placeholder="192.168.0.0/16"
|
|
ariaLabel={t('settings.modals.tun.fields.routeExcludeAddress')}
|
|
disabled={!values.autoRoute}
|
|
error={routeExcludeAddressError}
|
|
helperText={routeExcludeAddressHelperText}
|
|
onChange={(nextValue) =>
|
|
setValues((v) => ({ ...v, routeExcludeAddress: nextValue }))
|
|
}
|
|
renderHeader={(modeToggle) => (
|
|
<ListItem sx={{ padding: '5px 2px' }}>
|
|
<ListItemText
|
|
primary={t('settings.modals.tun.fields.routeExcludeAddress')}
|
|
/>
|
|
{modeToggle ? (
|
|
<Box sx={{ marginLeft: 'auto' }}>{modeToggle}</Box>
|
|
) : null}
|
|
</ListItem>
|
|
)}
|
|
/>
|
|
</List>
|
|
</BaseDialog>
|
|
)
|
|
}
|