import { RestartAltRounded } from "@mui/icons-material"; import { Box, Button, FormControl, List, ListItem, ListItemText, MenuItem, Select, styled, TextField, Typography, } from "@mui/material"; import { invoke } from "@tauri-apps/api/core"; import { useLockFn } from "ahooks"; import yaml from "js-yaml"; import type { Ref } from "react"; import { useCallback, useEffect, useImperativeHandle, useReducer, useRef, useState, } from "react"; import { useTranslation } from "react-i18next"; import MonacoEditor from "react-monaco-editor"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; import getSystem from "@/utils/get-system"; const Item = styled(ListItem)(() => ({ padding: "5px 2px", "& textarea": { lineHeight: 1.5, fontSize: 14, resize: "vertical", }, })); type NameserverPolicy = Record; function parseNameserverPolicy(str: string): NameserverPolicy { const result: NameserverPolicy = {}; if (!str) return result; const ruleRegex = /\s*([^=]+?)\s*=\s*([^,]+)(?:,|$)/g; let match: RegExpExecArray | null; while ((match = ruleRegex.exec(str)) !== null) { const [, domainsPart, serversPart] = match; const domains = [domainsPart.trim()]; const servers = serversPart.split(";").map((s) => s.trim()); domains.forEach((domain) => { result[domain] = servers; }); } return result; } function formatNameserverPolicy(policy: unknown): string { if (!policy || typeof policy !== "object") return ""; return Object.entries(policy as Record) .map(([domain, servers]) => { const serversStr = Array.isArray(servers) ? servers.join(";") : servers; return `${domain}=${serversStr}`; }) .join(", "); } function formatHosts(hosts: unknown): string { if (!hosts || typeof hosts !== "object") return ""; const result: string[] = []; Object.entries(hosts as Record).forEach( ([domain, value]) => { if (Array.isArray(value)) { const ipsStr = value.join(";"); result.push(`${domain}=${ipsStr}`); } else { result.push(`${domain}=${value}`); } }, ); return result.join(", "); } function parseHosts(str: string): NameserverPolicy { const result: NameserverPolicy = {}; if (!str) return result; str.split(",").forEach((item) => { const parts = item.trim().split("="); if (parts.length < 2) return; const domain = parts[0].trim(); const valueStr = parts.slice(1).join("=").trim(); if (valueStr.includes(";")) { result[domain] = valueStr .split(";") .map((s) => s.trim()) .filter(Boolean); } else { result[domain] = valueStr; } }); return result; } function parseList(str: string): string[] { if (!str?.trim()) return []; return str .split(",") .map((item) => item.trim()) .filter(Boolean); } // 默认DNS配置 const DEFAULT_DNS_CONFIG = { enable: true, listen: ":53", "enhanced-mode": "fake-ip" as "fake-ip" | "redir-host", "fake-ip-range": "198.18.0.1/16", "fake-ip-filter-mode": "blacklist" as "blacklist" | "whitelist", "prefer-h3": false, "respect-rules": false, "use-hosts": false, "use-system-hosts": false, ipv6: true, "fake-ip-filter": [ "*.lan", "*.local", "*.arpa", "time.*.com", "ntp.*.com", "time.*.com", "+.market.xiaomi.com", "localhost.ptlogin2.qq.com", "*.msftncsi.com", "www.msftconnecttest.com", ], "default-nameserver": [ "system", "223.6.6.6", "8.8.8.8", "2400:3200::1", "2001:4860:4860::8888", ], nameserver: [ "8.8.8.8", "https://doh.pub/dns-query", "https://dns.alidns.com/dns-query", ], fallback: [], "nameserver-policy": {}, "proxy-server-nameserver": [ "https://doh.pub/dns-query", "https://dns.alidns.com/dns-query", "tls://223.5.5.5", ], "direct-nameserver": [], "direct-nameserver-follow-policy": false, "fallback-filter": { geoip: true, "geoip-code": "CN", ipcidr: ["240.0.0.0/4", "0.0.0.0/32"], domain: ["+.google.com", "+.facebook.com", "+.youtube.com"], }, }; export function DnsViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const { clash, mutateClash } = useClash(); const themeMode = useThemeMode(); const [open, setOpen] = useState(false); const [visualization, setVisualization] = useState(true); const skipYamlSyncRef = useRef(false); const [values, setValues] = useState<{ enable: boolean; listen: string; enhancedMode: "fake-ip" | "redir-host"; fakeIpRange: string; fakeIpFilterMode: "blacklist" | "whitelist"; preferH3: boolean; respectRules: boolean; useHosts: boolean; useSystemHosts: boolean; ipv6: boolean; fakeIpFilter: string; nameserver: string; fallback: string; defaultNameserver: string; proxyServerNameserver: string; directNameserver: string; directNameserverFollowPolicy: boolean; fallbackGeoip: boolean; fallbackGeoipCode: string; fallbackIpcidr: string; fallbackDomain: string; nameserverPolicy: string; hosts: string; // hosts设置,独立于dns }>({ enable: DEFAULT_DNS_CONFIG.enable, listen: DEFAULT_DNS_CONFIG.listen, enhancedMode: DEFAULT_DNS_CONFIG["enhanced-mode"], fakeIpRange: DEFAULT_DNS_CONFIG["fake-ip-range"], fakeIpFilterMode: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"], preferH3: DEFAULT_DNS_CONFIG["prefer-h3"], respectRules: DEFAULT_DNS_CONFIG["respect-rules"], useHosts: DEFAULT_DNS_CONFIG["use-hosts"], useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"], ipv6: DEFAULT_DNS_CONFIG.ipv6, fakeIpFilter: DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "), defaultNameserver: DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), nameserver: DEFAULT_DNS_CONFIG.nameserver.join(", "), fallback: DEFAULT_DNS_CONFIG.fallback.join(", "), proxyServerNameserver: DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || "", directNameserver: DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || "", directNameserverFollowPolicy: DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"] || false, fallbackGeoip: DEFAULT_DNS_CONFIG["fallback-filter"].geoip, fallbackGeoipCode: DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"], fallbackIpcidr: DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr?.join(", ") || "", fallbackDomain: DEFAULT_DNS_CONFIG["fallback-filter"].domain?.join(", ") || "", nameserverPolicy: "", hosts: "", }); // 用于YAML编辑模式 const [yamlContent, setYamlContent] = useReducer( (_: string, next: string) => next, "", ); // 从配置对象更新表单值 const updateValuesFromConfig = useCallback( (config: any) => { if (!config) return; const dnsConfig = config.dns || {}; const hostsConfig = config.hosts || {}; const enhancedMode = dnsConfig["enhanced-mode"] || DEFAULT_DNS_CONFIG["enhanced-mode"]; const validEnhancedMode = enhancedMode === "fake-ip" || enhancedMode === "redir-host" ? enhancedMode : DEFAULT_DNS_CONFIG["enhanced-mode"]; const fakeIpFilterMode = dnsConfig["fake-ip-filter-mode"] || DEFAULT_DNS_CONFIG["fake-ip-filter-mode"]; const validFakeIpFilterMode = fakeIpFilterMode === "blacklist" || fakeIpFilterMode === "whitelist" ? fakeIpFilterMode : DEFAULT_DNS_CONFIG["fake-ip-filter-mode"]; setValues({ enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable, listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen, enhancedMode: validEnhancedMode, fakeIpRange: dnsConfig["fake-ip-range"] ?? DEFAULT_DNS_CONFIG["fake-ip-range"], fakeIpFilterMode: validFakeIpFilterMode, preferH3: dnsConfig["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"], respectRules: dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"], useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"], useSystemHosts: dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"], ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6, fakeIpFilter: dnsConfig["fake-ip-filter"]?.join(", ") ?? DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "), nameserver: dnsConfig.nameserver?.join(", ") ?? DEFAULT_DNS_CONFIG.nameserver.join(", "), fallback: dnsConfig.fallback?.join(", ") ?? DEFAULT_DNS_CONFIG.fallback.join(", "), defaultNameserver: dnsConfig["default-nameserver"]?.join(", ") ?? DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), proxyServerNameserver: dnsConfig["proxy-server-nameserver"]?.join(", ") ?? (DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || ""), directNameserver: dnsConfig["direct-nameserver"]?.join(", ") ?? (DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || ""), directNameserverFollowPolicy: dnsConfig["direct-nameserver-follow-policy"] ?? DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"], fallbackGeoip: dnsConfig["fallback-filter"]?.geoip ?? DEFAULT_DNS_CONFIG["fallback-filter"].geoip, fallbackGeoipCode: dnsConfig["fallback-filter"]?.["geoip-code"] ?? DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"], fallbackIpcidr: dnsConfig["fallback-filter"]?.ipcidr?.join(", ") ?? DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr.join(", "), fallbackDomain: dnsConfig["fallback-filter"]?.domain?.join(", ") ?? DEFAULT_DNS_CONFIG["fallback-filter"].domain.join(", "), nameserverPolicy: formatNameserverPolicy(dnsConfig["nameserver-policy"]) || "", hosts: formatHosts(hostsConfig) || "", }); }, [setValues], ); const generateDnsConfig = useCallback(() => { const dnsConfig: any = { enable: values.enable, listen: values.listen, "enhanced-mode": values.enhancedMode, "fake-ip-range": values.fakeIpRange, "fake-ip-filter-mode": values.fakeIpFilterMode, "prefer-h3": values.preferH3, "respect-rules": values.respectRules, "use-hosts": values.useHosts, "use-system-hosts": values.useSystemHosts, ipv6: values.ipv6, "fake-ip-filter": parseList(values.fakeIpFilter), "default-nameserver": parseList(values.defaultNameserver), nameserver: parseList(values.nameserver), "direct-nameserver-follow-policy": values.directNameserverFollowPolicy, "fallback-filter": { geoip: values.fallbackGeoip, "geoip-code": values.fallbackGeoipCode, ipcidr: parseList(values.fallbackIpcidr), domain: parseList(values.fallbackDomain), }, fallback: parseList(values.fallback), "proxy-server-nameserver": parseList(values.proxyServerNameserver), "direct-nameserver": parseList(values.directNameserver), }; const policy = parseNameserverPolicy(values.nameserverPolicy); if (Object.keys(policy).length > 0) { dnsConfig["nameserver-policy"] = policy; } return dnsConfig; }, [values]); const updateYamlFromValues = useCallback(() => { const config: Record = {}; const dnsConfig = generateDnsConfig(); if (Object.keys(dnsConfig).length > 0) { config.dns = dnsConfig; } const hosts = parseHosts(values.hosts); if (Object.keys(hosts).length > 0) { config.hosts = hosts; } setYamlContent(yaml.dump(config, { forceQuotes: true })); }, [generateDnsConfig, setYamlContent, values.hosts]); // 重置为默认值 const resetToDefaults = useCallback(() => { setValues({ enable: DEFAULT_DNS_CONFIG.enable, listen: DEFAULT_DNS_CONFIG.listen, enhancedMode: DEFAULT_DNS_CONFIG["enhanced-mode"], fakeIpRange: DEFAULT_DNS_CONFIG["fake-ip-range"], fakeIpFilterMode: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"], preferH3: DEFAULT_DNS_CONFIG["prefer-h3"], respectRules: DEFAULT_DNS_CONFIG["respect-rules"], useHosts: DEFAULT_DNS_CONFIG["use-hosts"], useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"], ipv6: DEFAULT_DNS_CONFIG.ipv6, fakeIpFilter: DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "), defaultNameserver: DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), nameserver: DEFAULT_DNS_CONFIG.nameserver.join(", "), fallback: DEFAULT_DNS_CONFIG.fallback.join(", "), proxyServerNameserver: DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || "", directNameserver: DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || "", directNameserverFollowPolicy: DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"] || false, fallbackGeoip: DEFAULT_DNS_CONFIG["fallback-filter"].geoip, fallbackGeoipCode: DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"], fallbackIpcidr: DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr?.join(", ") || "", fallbackDomain: DEFAULT_DNS_CONFIG["fallback-filter"].domain?.join(", ") || "", nameserverPolicy: "", hosts: "", }); updateYamlFromValues(); }, [setValues, updateYamlFromValues]); // 从YAML更新表单值 const updateValuesFromYaml = useCallback(() => { try { const parsedYaml = yaml.load(yamlContent) as any; if (!parsedYaml) return; skipYamlSyncRef.current = true; updateValuesFromConfig(parsedYaml); } catch { showNotice("error", t("Invalid YAML format")); } }, [yamlContent, t, updateValuesFromConfig]); useEffect(() => { if (skipYamlSyncRef.current) { skipYamlSyncRef.current = false; return; } updateYamlFromValues(); }, [updateYamlFromValues]); const latestUpdateValuesFromYamlRef = useRef(updateValuesFromYaml); const latestUpdateYamlFromValuesRef = useRef(updateYamlFromValues); useEffect(() => { latestUpdateValuesFromYamlRef.current = updateValuesFromYaml; latestUpdateYamlFromValuesRef.current = updateYamlFromValues; }, [updateValuesFromYaml, updateYamlFromValues]); useEffect(() => { if (visualization) { latestUpdateValuesFromYamlRef.current(); } else { latestUpdateYamlFromValuesRef.current(); } }, [visualization]); const initDnsConfig = useCallback(async () => { try { const dnsConfigExists = await invoke( "check_dns_config_exists", {}, ); if (dnsConfigExists) { const dnsConfig = await invoke("get_dns_config_content", {}); const config = yaml.load(dnsConfig) as any; updateValuesFromConfig(config); setYamlContent(dnsConfig); } else { resetToDefaults(); } } catch (err) { console.error("Failed to initialize DNS config", err); resetToDefaults(); } }, [resetToDefaults, setYamlContent, updateValuesFromConfig]); useImperativeHandle( ref, () => ({ open: () => { setOpen(true); void initDnsConfig(); }, close: () => setOpen(false), }), [initDnsConfig], ); // 生成DNS配置对象 // 处理保存操作 const onSave = useLockFn(async () => { try { let config: Record; if (visualization) { // 使用表单值生成配置 config = {}; const dnsConfig = generateDnsConfig(); if (Object.keys(dnsConfig).length > 0) { config.dns = dnsConfig; } const hosts = parseHosts(values.hosts); if (Object.keys(hosts).length > 0) { config.hosts = hosts; } } else { // 使用YAML编辑器的值 const parsedConfig = yaml.load(yamlContent); if (typeof parsedConfig !== "object" || parsedConfig === null) { throw new Error(t("Invalid configuration")); } config = parsedConfig as Record; } // 保存配置 await invoke("save_dns_config", { dnsConfig: config }); // 验证配置 const [isValid, errorMsg] = await invoke<[boolean, string]>( "validate_dns_config", {}, ); if (!isValid) { let cleanErrorMsg = errorMsg; // 提取关键错误信息 if (errorMsg.includes("level=error")) { const errorLines = errorMsg .split("\n") .filter( (line) => line.includes("level=error") || line.includes("level=fatal") || line.includes("failed"), ); if (errorLines.length > 0) { cleanErrorMsg = errorLines .map((line) => { const msgMatch = line.match(/msg="([^"]+)"/); return msgMatch ? msgMatch[1] : line; }) .join(", "); } } showNotice( "error", t("DNS configuration error") + ": " + cleanErrorMsg, ); return; } // 如果DNS开关当前是打开的,则需要应用新的DNS配置 if (clash?.dns?.enable) { await invoke("apply_dns_config", { apply: true }); mutateClash(); } setOpen(false); showNotice("success", t("DNS settings saved")); } catch (err: any) { showNotice("error", err.message || err.toString()); } }); // YAML编辑器内容变更处理 const handleYamlChange = (value: string) => { setYamlContent(value || ""); // 允许YAML编辑后立即分析和更新表单值 try { const config = yaml.load(value) as any; if (config && typeof config === "object") { setTimeout(() => { updateValuesFromConfig(config); }, 300); } } catch (err) { console.log("YAML解析错误,忽略自动更新", err); } }; // 处理表单值变化 const handleChange = (field: string) => (event: any) => { const value = event.target.type === "checkbox" ? event.target.checked : event.target.value; setValues((prev) => { const newValues = { ...prev, [field]: value, }; // 当可视化编辑模式下的值变化时,自动更新YAML if (visualization) { setTimeout(() => { updateYamlFromValues(); }, 0); } return newValues; }); }; return ( {t("DNS Overwrite")} } contentSx={{ width: 550, overflow: "auto", ...(visualization ? {} : { padding: "0 24px", display: "flex", flexDirection: "column" }), }} okBtn={t("Save")} cancelBtn={t("Cancel")} onClose={() => setOpen(false)} onCancel={() => setOpen(false)} onOk={onSave} > {/* Warning message */} {t("DNS Settings Warning")} {visualization ? ( {t("DNS Settings")} {t("Fallback Filter Settings")} {/* Hosts 配置部分 */} {t("Hosts Settings")} ) : ( = 1500, }, mouseWheelZoom: true, quickSuggestions: { strings: true, comments: true, other: true, }, padding: { top: 33, }, fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${ getSystem() === "windows" ? ", twemoji mozilla" : "" }`, fontLigatures: false, smoothScrolling: true, }} onChange={handleYamlChange} /> )} ); }