feat(dns): protect provider DNS from unconfirmed overrides (#7902)

Fixes #7292
This commit is contained in:
wonfen
2026-09-09 23:19:56 +08:00
committed by GitHub
parent b06a966a4b
commit 35a2b6c75c
36 changed files with 565 additions and 35 deletions
+4 -2
View File
@@ -33,6 +33,7 @@ import {
Switch,
} from '@/components/base'
import { useClash } from '@/hooks/use-clash'
import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service'
import { useThemeMode } from '@/services/states'
import type { MonacoEditorInstance } from '@/types/monaco'
@@ -187,7 +188,8 @@ const DEFAULT_DNS_CONFIG = {
export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation()
const { clash, mutateClash } = useClash()
const { mutateClash } = useClash()
const { verge } = useVerge()
const themeMode = useThemeMode()
const [open, setOpen] = useState(false)
@@ -563,7 +565,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
return
}
if (clash?.dns?.enable) {
if (verge?.enable_dns_settings) {
await invoke('apply_dns_config', { apply: true })
mutateClash()
}
+47 -24
View File
@@ -1,17 +1,16 @@
import { LanRounded, SettingsRounded } from '@mui/icons-material'
import { MenuItem, Select, TextField, Typography } from '@mui/material'
import { invoke } from '@tauri-apps/api/core'
import { useLockFn } from 'ahooks'
import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { updateGeo, type LogLevel } from 'tauri-plugin-mihomo-api'
import { DialogRef, Switch, TooltipIcon } from '@/components/base'
import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
import { useClash } from '@/hooks/use-clash'
import { useClashLog } from '@/hooks/use-clash-log'
import { useDisplayedMixedPort } from '@/hooks/use-displayed-mixed-port'
import { useVerge } from '@/hooks/use-verge'
import { invoke_uwp_tool } from '@/services/cmds'
import { invoke_uwp_tool, setDnsOverride } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
import getSystem from '@/utils/get-system'
@@ -36,7 +35,7 @@ const SettingClash = ({ onError }: Props) => {
const { t } = useTranslation()
const { clash, version, mutateClash, patchClash } = useClash()
const { verge, patchVerge } = useVerge()
const { verge, mutateVerge } = useVerge()
const displayedMixedPort = useDisplayedMixedPort()
const [, setClashLog] = useClashLog()
@@ -47,10 +46,8 @@ const SettingClash = ({ onError }: Props) => {
'unified-delay': unifiedDelay,
} = clash ?? {}
// 独立跟踪DNS设置开关状态
const [dnsSettingsEnabled, setDnsSettingsEnabled] = useState(() => {
return verge?.enable_dns_settings ?? false
})
const [dnsConfirmation, setDnsConfirmation] = useState<string | null>(null)
const [dnsUpdating, setDnsUpdating] = useState(false)
const webRef = useRef<DialogRef>(null)
const portRef = useRef<DialogRef>(null)
@@ -74,22 +71,29 @@ const SettingClash = ({ onError }: Props) => {
}
}
// 实现DNS设置开关处理函数
const handleDnsToggle = useLockFn(async (enable: boolean) => {
try {
setDnsSettingsEnabled(enable)
await patchVerge({ enable_dns_settings: enable })
await invoke('apply_dns_config', { apply: enable })
setTimeout(() => {
const handleDnsToggle = useLockFn(
async (enable: boolean, confirmation?: string) => {
setDnsUpdating(true)
try {
const outcome = await setDnsOverride(enable, confirmation)
if (outcome.status === 'confirmation_required') {
setDnsConfirmation(outcome.source)
} else {
setDnsConfirmation(null)
}
mutateVerge()
mutateClash()
}, 500)
} catch (err: any) {
setDnsSettingsEnabled(!enable)
showNotice.error(err)
await patchVerge({ enable_dns_settings: !enable }).catch(() => {})
throw err
}
})
} catch (err: any) {
showNotice.error(err)
} finally {
setDnsUpdating(false)
}
},
)
const closeDnsConfirmation = () => {
if (!dnsUpdating) setDnsConfirmation(null)
}
return (
<SettingList title={t('settings.sections.clash.title')}>
@@ -99,6 +103,24 @@ const SettingClash = ({ onError }: Props) => {
<ClashCoreViewer ref={coreRef} />
<NetworkInterfaceViewer ref={networkRef} />
<DnsViewer ref={dnsRef} />
<BaseDialog
open={dnsConfirmation !== null}
title={t('settings.modals.dns.protection.title')}
okBtn={t('settings.modals.dns.protection.enableAnyway')}
cancelBtn={t('settings.modals.dns.protection.keepDisabled')}
contentSx={{ width: { xs: 320, sm: 420 } }}
loading={dnsUpdating}
disableCancel={dnsUpdating}
onCancel={closeDnsConfirmation}
onClose={closeDnsConfirmation}
onOk={() => {
if (dnsConfirmation) void handleDnsToggle(true, dnsConfirmation)
}}
>
<Typography variant="body2">
{t('settings.modals.dns.protection.message')}
</Typography>
</BaseDialog>
<HeaderConfiguration ref={corsRef} />
<TunnelsViewer ref={tunnelRef} />
<SettingItem
@@ -137,7 +159,8 @@ const SettingClash = ({ onError }: Props) => {
>
<Switch
edge="end"
checked={dnsSettingsEnabled}
checked={verge?.enable_dns_settings ?? false}
disabled={dnsUpdating || dnsConfirmation !== null}
onChange={(_, checked) => handleDnsToggle(checked)}
/>
</SettingItem>
+7
View File
@@ -510,6 +510,13 @@
"title": "DNS Overwrite",
"warning": "If you are not familiar with these settings, please do not modify them and keep DNS Overwrite disabled"
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "DNS Settings",
"fallbackFilter": "Fallback Filter Settings",
+7
View File
@@ -510,6 +510,13 @@
"title": "DNS-Überschreibung",
"warning": "Wenn Sie sich nicht mit diesen Einstellungen auskennen, ändern Sie sie nicht und lassen Sie die DNS-Überschreibung deaktiviert."
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "DNS-Einstellungen",
"fallbackFilter": "Rückfallfilter-Einstellungen",
+7
View File
@@ -510,6 +510,13 @@
"title": "DNS Overwrite",
"warning": "If you are not familiar with these settings, please do not modify them and keep DNS Overwrite disabled"
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "DNS Settings",
"fallbackFilter": "Fallback Filter Settings",
+7
View File
@@ -510,6 +510,13 @@
"title": "Sobrescritura de DNS",
"warning": "Si no está seguro de cómo configurar esto, no realice cambios y mantenga deshabilitada la sobrescritura de DNS."
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "Configuración de DNS",
"fallbackFilter": "Configuración de filtrado de respaldo",
+7
View File
@@ -510,6 +510,13 @@
"title": "بازنویسی DNS",
"warning": "اگر با این تنظیمات آشنا نیستید، لطفاً آنها را تغییر ندهید و DNS Overwrite را غیرفعال نگه دارید."
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "تنظیمات DNS",
"fallbackFilter": "Fallback Filter Settings",
+7
View File
@@ -510,6 +510,13 @@
"title": "DNS Overwrite",
"warning": "If you are not familiar with these settings, please do not modify them and keep DNS Overwrite disabled"
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "DNS Settings",
"fallbackFilter": "Fallback Filter Settings",
+7
View File
@@ -510,6 +510,13 @@
"title": "DNS上書き",
"warning": "ここの設定がわからない場合は、変更しないでください。DNS上書きを無効にしたままにしてください。"
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "DNS設定",
"fallbackFilter": "フォールバックフィルター設定",
+7
View File
@@ -510,6 +510,13 @@
"title": "DNS 덮어쓰기",
"warning": "이 설정에 익숙하지 않다면 수정하지 말고 DNS 덮어쓰기를 비활성화 상태로 유지하세요"
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "DNS 설정",
"fallbackFilter": "폴백 필터 설정",
+7
View File
@@ -510,6 +510,13 @@
"title": "Переопределение настроек DNS",
"warning": "Если вы не знакомы с этими настройками, пожалуйста, не изменяйте их и оставьте переопределение DNS отключённым"
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "Настройки DNS",
"fallbackFilter": "Настройки фильтра fallback",
+7
View File
@@ -510,6 +510,13 @@
"title": "DNS Üzerine Yazma",
"warning": "Bu ayarlarla ilgili bilginiz yoksa, lütfen bunları değiştirmeyin ve DNS Üzerine Yazma'yı devre dışı tutun"
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "DNS Ayarları",
"fallbackFilter": "Yedek Filtre Ayarları",
+7
View File
@@ -510,6 +510,13 @@
"title": "DNS Overwrite",
"warning": "If you are not familiar with these settings, please do not modify them and keep DNS Overwrite disabled"
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
},
"sections": {
"general": "DNS Settings",
"fallbackFilter": "Fallback Filter Settings",
+7
View File
@@ -510,6 +510,13 @@
"title": "DNS 覆写",
"warning": "如果你不清楚这里的设置请不要修改,并保持 DNS 覆写关闭"
},
"protection": {
"title": "是否开启 DNS 覆写?",
"message": "当前订阅包含自定义 DNS 配置。开启 DNS 覆写可能覆盖这些配置,导致节点无法连接或部分网站无法访问。是否仍要开启?",
"enableAnyway": "仍然开启",
"keepDisabled": "保持关闭",
"autoDisabled": "检测到当前订阅包含自定义 DNS 配置,已自动关闭 DNS 覆写。"
},
"sections": {
"general": "DNS 设置",
"fallbackFilter": "回退过滤设置",
+7
View File
@@ -510,6 +510,13 @@
"title": "DNS 覆寫",
"warning": "如果你不清楚這裡的設定請不要修改,並保持 DNS 覆寫關閉"
},
"protection": {
"title": "是否開啟 DNS 覆寫?",
"message": "目前訂閱包含自訂 DNS 設定。開啟 DNS 覆寫可能覆蓋這些設定,導致節點無法連線或部分網站無法存取。是否仍要開啟?",
"enableAnyway": "仍然開啟",
"keepDisabled": "保持關閉",
"autoDisabled": "偵測到目前訂閱包含自訂 DNS 設定,已自動關閉 DNS 覆寫。"
},
"sections": {
"general": "DNS 設定",
"fallbackFilter": "備援篩選設定",
@@ -0,0 +1,52 @@
import { beforeEach, expect, it, vi } from 'vitest'
import { takeDnsOverrideNotice } from '@/services/cmds'
import { subscribeVergeEvents } from '@/services/events'
import { showNotice } from '@/services/notice-service'
import { revalidateQueries } from '@/services/query-client'
import { handleNoticeMessage } from '../utils/notification-handlers'
import { useLayoutEvents } from './use-layout-events'
vi.mock('react', () => ({ useEffect: (effect: () => void) => effect() }))
vi.mock('@/hooks/use-profiles', () => ({ revalidateProfiles: vi.fn() }))
vi.mock('@/hooks/use-system-state', () => ({ runStateQueryKey: ['state'] }))
vi.mock('@/services/events', () => ({ subscribeVergeEvents: vi.fn() }))
vi.mock('@/services/cmds', () => ({ takeDnsOverrideNotice: vi.fn() }))
vi.mock('@/services/notice-service', () => ({
showNotice: { info: vi.fn() },
}))
vi.mock('@/services/query-client', () => ({
revalidateQueries: vi.fn(),
setCacheData: vi.fn(),
}))
beforeEach(() => {
vi.clearAllMocks()
})
it('drains a DNS notice after listeners mount without duplicating its live event', async () => {
vi.mocked(takeDnsOverrideNotice)
.mockResolvedValueOnce(true)
.mockResolvedValue(false)
useLayoutEvents(([status, message]) => {
handleNoticeMessage(status, message, (key) => key, vi.fn())
})
expect(takeDnsOverrideNotice).not.toHaveBeenCalled()
const [handlers, onSubscribed] = vi.mocked(subscribeVergeEvents).mock.calls[0]
onSubscribed?.()
expect(takeDnsOverrideNotice).toHaveBeenCalledOnce()
handlers['verge://notice-message']?.(['dns_override::auto_disabled', ''])
await new Promise((resolve) => setTimeout(resolve, 0))
expect(showNotice.info).toHaveBeenCalledExactlyOnceWith(
'settings.modals.dns.protection.autoDisabled',
)
expect(revalidateQueries).toHaveBeenCalledWith([
['getRuntimeState'],
['getVergeConfig'],
])
})
+4 -1
View File
@@ -61,7 +61,10 @@ export const useLayoutEvents = (
'verge://notice-message': handleNotice,
},
// Re-read event-only state after subscribing to close the initial race window.
() => revalidateKeys(['getRuntimeState']),
() => {
revalidateKeys(['getRuntimeState', 'getVergeConfig'])
handleNotice(['dns_override::auto_disabled', ''])
},
)
}, [handleNotice])
}
@@ -1,3 +1,4 @@
import { takeDnsOverrideNotice } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
type NavigateFunction = (path: string, options?: any) => void
@@ -24,6 +25,17 @@ export const handleNoticeMessage = (
showNotice.error(msg)
},
'set_config::error': () => showNotice.error(msg),
'dns_override::auto_disabled': () => {
void takeDnsOverrideNotice()
.then((pending) => {
if (pending) {
showNotice.info('settings.modals.dns.protection.autoDisabled')
}
})
.catch((error) => {
console.error('Failed to read the pending DNS override notice', error)
})
},
'tun_mode::auto_disabled': () =>
showNotice.info(
'settings.sections.system.notifications.tunMode.autoDisabled',
+10
View File
@@ -172,6 +172,16 @@ export async function patchVergeConfig(payload: IVergeConfig) {
return invoke<void>('patch_verge_config', { payload })
}
export async function setDnsOverride(enabled: boolean, confirmation?: string) {
return invoke<
{ status: 'applied' } | { status: 'confirmation_required'; source: string }
>('set_dns_override', { enabled, confirmation })
}
export async function takeDnsOverrideNotice() {
return invoke<boolean>('take_dns_override_notice')
}
export async function getSystemProxy() {
return invoke<{
enable: boolean
+5
View File
@@ -697,6 +697,11 @@ export const translationKeys = [
'settings.modals.tun.messages.routeExcludeAddressHint',
'settings.modals.dns.dialog.title',
'settings.modals.dns.dialog.warning',
'settings.modals.dns.protection.title',
'settings.modals.dns.protection.message',
'settings.modals.dns.protection.enableAnyway',
'settings.modals.dns.protection.keepDisabled',
'settings.modals.dns.protection.autoDisabled',
'settings.modals.dns.sections.general',
'settings.modals.dns.sections.fallbackFilter',
'settings.modals.dns.sections.hosts',
+7
View File
@@ -1037,6 +1037,13 @@ export interface TranslationResources {
configError: string
saved: string
}
protection: {
autoDisabled: string
enableAnyway: string
keepDisabled: string
message: string
title: string
}
sections: {
fallbackFilter: string
general: string