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 }) { 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 ( {t('settings.modals.tun.title')} } contentSx={{ width: 450 }} okBtn={t('shared.actions.save')} cancelBtn={t('shared.actions.cancel')} onClose={() => setOpen(false)} onCancel={() => setOpen(false)} onOk={onSave} > { setValues((v) => ({ ...v, stack: value, })) }} /> { setValues((v) => ({ ...v, dnsMode: value })) }} /> setValues((v) => ({ ...v, device: e.target.value })) } /> setValues((v) => ({ ...v, autoRoute: c, autoRedirect: c ? v.autoRedirect : false, })) } /> {OS === 'linux' && ( setValues((v) => ({ ...v, autoRedirect: v.autoRoute ? c : v.autoRedirect, })) } disabled={!values.autoRoute} sx={{ marginLeft: 'auto' }} /> )} setValues((v) => ({ ...v, strictRoute: c }))} /> setValues((v) => ({ ...v, autoDetectInterface: c })) } /> setValues((v) => ({ ...v, dnsHijack: e.target.value.split(',') })) } /> setValues((v) => ({ ...v, mtu: parseInt(e.target.value), })) } /> setValues((v) => ({ ...v, routeExcludeAddress: nextValue })) } renderHeader={(modeToggle) => ( {modeToggle ? ( {modeToggle} ) : null} )} /> ) }