From 95ae550216b31d8eab85203a35471c8c2aea2de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=90=E6=AE=87?= <95160953+xishang0128@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:13:17 +0800 Subject: [PATCH] add dns and sniff (#3) Co-authored-by: pompurin404 --- src/main/config/controledMihomo.ts | 10 + src/main/resolve/factory.ts | 8 + src/main/utils/template.ts | 26 +++ src/renderer/src/App.tsx | 6 + .../src/components/sider/dns-card.tsx | 56 +++++ .../src/components/sider/proxy-card.tsx | 4 +- .../src/components/sider/rule-card.tsx | 4 +- .../src/components/sider/sniff-card.tsx | 56 +++++ .../src/components/sider/tun-switcher.tsx | 8 +- src/renderer/src/pages/dns.tsx | 209 ++++++++++++++++++ src/renderer/src/pages/sniffer.tsx | 188 ++++++++++++++++ src/renderer/src/routes/index.tsx | 10 + src/shared/types.d.ts | 60 ++++- 13 files changed, 637 insertions(+), 8 deletions(-) create mode 100644 src/renderer/src/components/sider/dns-card.tsx create mode 100644 src/renderer/src/components/sider/sniff-card.tsx create mode 100644 src/renderer/src/pages/dns.tsx create mode 100644 src/renderer/src/pages/sniffer.tsx diff --git a/src/main/config/controledMihomo.ts b/src/main/config/controledMihomo.ts index 83e692f..46bed81 100644 --- a/src/main/config/controledMihomo.ts +++ b/src/main/config/controledMihomo.ts @@ -18,6 +18,16 @@ export function setControledMihomoConfig(patch: Partial): void { const newTun = Object.assign(oldTun, patch.tun) patch.tun = newTun } + if (patch.dns) { + const oldDns = controledMihomoConfig.dns || {} + const newDns = Object.assign(oldDns, patch.dns) + patch.dns = newDns + } + if (patch.sniffer) { + const oldSniffer = controledMihomoConfig.sniffer || {} + const newSniffer = Object.assign(oldSniffer, patch.sniffer) + patch.sniffer = newSniffer + } controledMihomoConfig = Object.assign(controledMihomoConfig, patch) if (patch['external-controller'] || patch.secret) { getAxios(true) diff --git a/src/main/resolve/factory.ts b/src/main/resolve/factory.ts index f65fc4a..fa00ea1 100644 --- a/src/main/resolve/factory.ts +++ b/src/main/resolve/factory.ts @@ -9,7 +9,15 @@ export function generateProfile(): void { const { tun: profileTun = {} } = currentProfile const { tun: controledTun } = controledMihomoConfig const tun = Object.assign(profileTun, controledTun) + const { dns: profileDns = {} } = currentProfile + const { dns: controledDns } = controledMihomoConfig + const dns = Object.assign(profileDns, controledDns) + const { sniffer: profileSniffer = {} } = currentProfile + const { sniffer: controledSniffer } = controledMihomoConfig + const sniffer = Object.assign(profileSniffer, controledSniffer) const profile = Object.assign(currentProfile, controledMihomoConfig) profile.tun = tun + profile.dns = dns + profile.sniffer = sniffer fs.writeFileSync(mihomoWorkConfigPath(), yaml.stringify(profile)) } diff --git a/src/main/utils/template.ts b/src/main/utils/template.ts index 084417b..211de2f 100644 --- a/src/main/utils/template.ts +++ b/src/main/utils/template.ts @@ -29,6 +29,32 @@ export const defaultControledMihomoConfig: Partial = { 'auto-detect-interface': true, 'dns-hijack': ['any:53'], mtu: 1500 + }, + dns: { + enable: false, + ipv6: false, + 'enhanced-mode': 'fake-ip', + 'fake-ip-range': '198.18.0.1/16', + 'use-hosts': false, + 'use-system-hosts': false, + nameserver: ['https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query'] + }, + sniffer: { + enable: true, + 'parse-pure-ip': true, + 'override-destination': false, + sniff: { + HTTP: { + ports: [80, 443] + }, + TLS: { + ports: [443] + }, + QUIC: { + ports: [443] + } + }, + 'skip-domain': ['+.push.apple.com'] } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index cf3c54c..8334a63 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -10,6 +10,8 @@ import routes from '@renderer/routes' import ProfileCard from '@renderer/components/sider/profile-card' import ProxyCard from '@renderer/components/sider/proxy-card' import RuleCard from '@renderer/components/sider/rule-card' +import DNSCard from '@renderer/components/sider/dns-card' +import SniffCard from '@renderer/components/sider/sniff-card' import OverrideCard from '@renderer/components/sider/override-card' import ConnCard from '@renderer/components/sider/conn-card' import LogCard from '@renderer/components/sider/log-card' @@ -75,6 +77,10 @@ const App: React.FC = () => { +
+ + +
diff --git a/src/renderer/src/components/sider/dns-card.tsx b/src/renderer/src/components/sider/dns-card.tsx new file mode 100644 index 0000000..3caf9d0 --- /dev/null +++ b/src/renderer/src/components/sider/dns-card.tsx @@ -0,0 +1,56 @@ +import { Button, Card, CardBody, CardFooter } from '@nextui-org/react' +import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' +import BorderSwitch from '@renderer/components/base/border-swtich' +import { MdOutlineDns } from 'react-icons/md' +import { useLocation, useNavigate } from 'react-router-dom' +import { patchMihomoConfig } from '@renderer/utils/ipc' + +const DNSCard: React.FC = () => { + const navigate = useNavigate() + const location = useLocation() + const match = location.pathname.includes('/dns') + const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig(true) + const { dns, tun } = controledMihomoConfig || {} + const { enable } = dns || {} + + const onChange = async (enable: boolean): Promise => { + await patchControledMihomoConfig({ dns: { enable } }) + await patchMihomoConfig({ dns: { enable } }) + } + + return ( + navigate('/dns')} + > + +
+ + +
+
+ +

+ DNS +

+
+
+ ) +} + +export default DNSCard diff --git a/src/renderer/src/components/sider/proxy-card.tsx b/src/renderer/src/components/sider/proxy-card.tsx index d332a8f..9195b6d 100644 --- a/src/renderer/src/components/sider/proxy-card.tsx +++ b/src/renderer/src/components/sider/proxy-card.tsx @@ -1,7 +1,7 @@ import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react' import { mihomoProxies } from '@renderer/utils/ipc' import { useMemo } from 'react' -import { MdTableChart } from 'react-icons/md' +import { LuGroup } from 'react-icons/lu' import { useLocation, useNavigate } from 'react-router-dom' import useSWR from 'swr' @@ -30,7 +30,7 @@ const ProxyCard: React.FC = () => { variant="flat" color="default" > - diff --git a/src/renderer/src/components/sider/rule-card.tsx b/src/renderer/src/components/sider/rule-card.tsx index 214d599..22234f7 100644 --- a/src/renderer/src/components/sider/rule-card.tsx +++ b/src/renderer/src/components/sider/rule-card.tsx @@ -1,6 +1,6 @@ import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react' import { mihomoRules } from '@renderer/utils/ipc' -import { IoGitNetwork } from 'react-icons/io5' +import { MdOutlineAltRoute } from 'react-icons/md' import { useLocation, useNavigate } from 'react-router-dom' import useSWR from 'swr' @@ -26,7 +26,7 @@ const RuleCard: React.FC = () => { variant="flat" color="default" > - diff --git a/src/renderer/src/components/sider/sniff-card.tsx b/src/renderer/src/components/sider/sniff-card.tsx new file mode 100644 index 0000000..1bfb474 --- /dev/null +++ b/src/renderer/src/components/sider/sniff-card.tsx @@ -0,0 +1,56 @@ +import { Button, Card, CardBody, CardFooter } from '@nextui-org/react' +import BorderSwitch from '@renderer/components/base/border-swtich' +import { GrDomain } from 'react-icons/gr' +import { useLocation, useNavigate } from 'react-router-dom' +import { patchMihomoConfig } from '@renderer/utils/ipc' +import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' + +const SniffCard: React.FC = () => { + const navigate = useNavigate() + const location = useLocation() + const match = location.pathname.includes('/sniffer') + const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig(true) + const { sniffer } = controledMihomoConfig || {} + const { enable } = sniffer || {} + + const onChange = async (enable: boolean): Promise => { + await patchControledMihomoConfig({ sniffer: { enable } }) + await patchMihomoConfig({ sniffer: { enable } }) + } + + return ( + navigate('/sniffer')} + > + +
+ + +
+
+ +

+ 域名嗅探 +

+
+
+ ) +} + +export default SniffCard diff --git a/src/renderer/src/components/sider/tun-switcher.tsx b/src/renderer/src/components/sider/tun-switcher.tsx index 5fdd01b..90aebb9 100644 --- a/src/renderer/src/components/sider/tun-switcher.tsx +++ b/src/renderer/src/components/sider/tun-switcher.tsx @@ -31,8 +31,12 @@ const TunSwitcher: React.FC = () => { } } - await patchControledMihomoConfig({ tun: { enable } }) - await patchMihomoConfig({ tun: { enable } }) + if (enable) { + await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) + } else { + await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) + await patchMihomoConfig({ tun: { enable } }) + } } return ( diff --git a/src/renderer/src/pages/dns.tsx b/src/renderer/src/pages/dns.tsx new file mode 100644 index 0000000..ed678b5 --- /dev/null +++ b/src/renderer/src/pages/dns.tsx @@ -0,0 +1,209 @@ +import { Button, Tab, Input, Switch, Tabs, Divider } from '@nextui-org/react' +import BasePage from '@renderer/components/base/base-page' +import { MdDeleteForever } from 'react-icons/md' +import SettingCard from '@renderer/components/base/base-setting-card' +import SettingItem from '@renderer/components/base/base-setting-item' +import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' +import { restartCore } from '@renderer/utils/ipc' +import React, { Key, useState } from 'react' + +const DNS: React.FC = () => { + const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig() + const { dns, hosts } = controledMihomoConfig || {} + const { + ipv6 = false, + 'enhanced-mode': enhancedMode = 'fake-ip', + 'use-hosts': useHosts = false, + 'use-system-hosts': useSystemHosts = false, + nameserver = ['https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query'] + } = dns || {} + + const [values, setValues] = useState({ + ipv6, + useHosts, + enhancedMode, + useSystemHosts, + nameserver, + hosts: Object.entries(hosts || {}).map(([domain, value]) => ({ domain, value })) + }) + + const handleNameserverChange = (value: string, index: number): void => { + const newNameservers = [...values.nameserver] + if (index === newNameservers.length) { + if (value.trim() !== '') { + newNameservers.push(value) + } + } else { + if (value.trim() === '') { + newNameservers.splice(index, 1) + } else { + newNameservers[index] = value + } + } + setValues({ ...values, nameserver: newNameservers }) + } + const handleHostsChange = (domain: string, value: string, index: number): void => { + const newHosts = [...values.hosts] + + if (index === newHosts.length) { + if (domain.trim() !== '' || value.trim() !== '') { + newHosts.push({ domain: domain.trim(), value: value.trim() }) + } + } else { + if (domain.trim() === '' && value.trim() === '') { + newHosts.splice(index, 1) + } else { + newHosts[index] = { domain: domain.trim(), value: value.trim() } + } + } + setValues({ ...values, hosts: newHosts }) + } + + const onSave = async (patch: Partial): Promise => { + await patchControledMihomoConfig(patch) + await restartCore() + } + + return ( + { + const hostsObject = values.hosts.reduce((acc, { domain, value }) => { + if (domain) { + acc[domain] = value + } + return acc + }, {}) + onSave({ + dns: { + ipv6: values.ipv6, + 'enhanced-mode': values.enhancedMode, + 'use-hosts': values.useHosts, + 'use-system-hosts': values.useSystemHosts, + nameserver: values.nameserver + }, + hosts: hostsObject + }) + }} + > + 保存 + + } + > + + + setValues({ ...values, enhancedMode: key as DnsMode })} + > + + + + + + + { + setValues({ ...values, ipv6: v }) + }} + /> + +
+

DNS服务器

+ {[...values.nameserver, ''].map((ns, index) => ( +
+ handleNameserverChange(v, index)} + /> + {index < values.nameserver.length && ( + + )} +
+ ))} +
+ + + { + setValues({ ...values, useSystemHosts: v }) + }} + /> + + + { + setValues({ ...values, useHosts: v }) + }} + /> + + {values.useHosts && ( +
+

+ {[...values.hosts, { domain: '', value: '' }].map(({ domain, value }, index) => ( +
+
+ + handleHostsChange(v, Array.isArray(value) ? value.join(', ') : value, index) + } + /> +
+ : +
+ handleHostsChange(domain, v, index)} + /> + {index < values.hosts.length && ( + + )} +
+
+ ))} +
+ )} +
+
+ ) +} + +export default DNS diff --git a/src/renderer/src/pages/sniffer.tsx b/src/renderer/src/pages/sniffer.tsx new file mode 100644 index 0000000..30073d0 --- /dev/null +++ b/src/renderer/src/pages/sniffer.tsx @@ -0,0 +1,188 @@ +import { Button, Divider, Input, Switch } from '@nextui-org/react' +import BasePage from '@renderer/components/base/base-page' +import SettingCard from '@renderer/components/base/base-setting-card' +import SettingItem from '@renderer/components/base/base-setting-item' +import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' +import { restartCore } from '@renderer/utils/ipc' +import React, { useState } from 'react' +import { MdDeleteForever } from 'react-icons/md' + +const Sniffer: React.FC = () => { + const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig() + const { sniffer } = controledMihomoConfig || {} + const { + 'parse-pure-ip': parsePureIP = true, + 'override-destination': overrideDestination = false, + sniff = { + HTTP: { ports: [80, 443] }, + TLS: { ports: [443] }, + QUIC: { ports: [443] } + }, + 'skip-domain': skipDomain = ['+.push.apple.com'], + 'force-domain': forceDomain = [] + } = sniffer || {} + + const [values, setValues] = useState({ + parsePureIP, + overrideDestination, + sniff, + skipDomain, + forceDomain + }) + + const onSave = async (patch: Partial): Promise => { + await patchControledMihomoConfig(patch) + await restartCore() + } + + const handleSniffPortChange = (protocol: keyof typeof sniff, value: string): void => { + setValues({ + ...values, + sniff: { + ...values.sniff, + [protocol]: { + ...values.sniff[protocol], + ports: value.split(',').map((port) => port.trim()) + } + } + }) + } + const handleDomainChange = (type: string, value: string, index: number): void => { + const newDomains = [...values[type]] + if (index === newDomains.length) { + if (value.trim() !== '') { + newDomains.push(value) + } + } else { + if (value.trim() === '') { + newDomains.splice(index, 1) + } else { + newDomains[index] = value + } + } + setValues({ ...values, [type]: newDomains }) + } + + return ( + + onSave({ + sniffer: { + 'parse-pure-ip': values.parsePureIP, + 'override-destination': values.overrideDestination, + sniff: values.sniff, + 'skip-domain': values.skipDomain, + 'force-domain': values.forceDomain + } + }) + } + > + 保存 + + } + > + + + { + setValues({ ...values, overrideDestination: v }) + }} + /> + + + { + setValues({ ...values, parsePureIP: v }) + }} + /> + + + handleSniffPortChange('HTTP', v)} + /> + + + handleSniffPortChange('TLS', v)} + /> + + + handleSniffPortChange('QUIC', v)} + /> + +
+

跳过嗅探

+ {[...values.skipDomain, ''].map((d, index) => ( +
+ handleDomainChange('skipDomain', v, index)} + /> + {index < values.skipDomain.length && ( + + )} +
+ ))} +
+ +
+

强制嗅探

+ {[...values.forceDomain, ''].map((d, index) => ( +
+ handleDomainChange('forceDomain', v, index)} + /> + {index < values.forceDomain.length && ( + + )} +
+ ))} +
+
+
+ ) +} + +export default Sniffer diff --git a/src/renderer/src/routes/index.tsx b/src/renderer/src/routes/index.tsx index c62b749..abb3420 100644 --- a/src/renderer/src/routes/index.tsx +++ b/src/renderer/src/routes/index.tsx @@ -10,6 +10,8 @@ import Mihomo from '@renderer/pages/mihomo' import Sysproxy from '@renderer/pages/syspeoxy' import Tun from '@renderer/pages/tun' import Tests from '@renderer/pages/tests' +import DNS from '@renderer/pages/dns' +import Sniffer from '@renderer/pages/sniffer' const routes = [ { @@ -36,6 +38,14 @@ const routes = [ path: '/tests', element: }, + { + path: '/dns', + element: + }, + { + path: '/sniffer', + element: + }, { path: '/logs', element: diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index 1989d4b..e0e7b0a 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -1,10 +1,30 @@ type OutboundMode = 'rule' | 'global' | 'direct' type LogLevel = 'info' | 'debug' | 'warning' | 'error' | 'silent' type SysProxyMode = 'auto' | 'manual' -type MihomoGroupType = 'Selector' -type MihomoProxyType = 'Shadowsocks' +type MihomoGroupType = 'Selector' | 'URLTest' | 'LoadBalance' | 'Relay' +type MihomoProxyType = + | 'Direct' + | 'Reject' + | 'RejectDrop' + | 'Pass' + | 'Dns' + | 'Compatible' + | 'Socks5' + | 'Http' + | 'Ssh' + | 'Shadowsocks' + | 'ShadowsocksR' + | 'Snell' + | 'Vmess' + | 'Vless' + | 'Trojan' + | 'Hysteria' + | 'Hysteria2' + | 'Tuic' + | 'WireGuard' type TunStack = 'gvisor' | 'mixed' | 'system' type FindProcessMode = 'off' | 'strict' | 'always' +type DnsMode = 'normal' | 'fake-ip' | 'redir-host' interface IMihomoVersion { version: string @@ -174,6 +194,39 @@ interface IMihomoTunConfig { 'include-package'?: string[] 'exclude-package'?: string[] } +interface IMihomoDNSConfig { + enable?: boolean + ipv6?: boolean + 'enhanced-mode'?: DnsMode + 'fake-ip-range'?: string + 'use-hosts'?: boolean + 'use-system-hosts'?: boolean + 'respect-rules'?: boolean + nameserver?: string[] + 'proxy-server-nameserver'?: string[] + 'nameserver-policy'?: { [key: string]: string | string[] } +} + +interface IMihomoSnifferConfig { + enable?: boolean + 'parse-pure-ip'?: boolean + 'override-destination'?: boolean + 'force-dns-mapping'?: boolean + 'force-domain'?: string[] + 'skip-domain'?: string[] + sniff?: { + HTTP?: { + ports: (number | string)[] + 'override-destination'?: boolean + } + TLS?: { + ports: (number | string)[] + } + QUIC?: { + ports: (number | string)[] + } + } +} interface IMihomoConfig { 'external-controller': string secret?: string @@ -191,7 +244,10 @@ interface IMihomoConfig { proxies?: [] 'proxy-groups'?: [] rules?: [] + hosts?: { [key: string]: string | string[] } tun: IMihomoTunConfig + dns: IMihomoDNSConfig + sniffer: IMihomoSnifferConfig } interface IProfileConfig {