From f80591242eb9d8d29b705f5c6df866a454e5d2fe Mon Sep 17 00:00:00 2001 From: wonfen Date: Thu, 6 Mar 2025 14:30:43 +0800 Subject: [PATCH] feat: add dns settings --- src/components/layout/use-custom-theme.ts | 2 +- src/components/setting/mods/dns-viewer.tsx | 854 ++++++++++++++++++ .../setting/mods/network-interface-viewer.tsx | 6 +- src/components/setting/setting-clash.tsx | 26 + src/locales/en.json | 41 +- src/locales/zh.json | 42 +- src/services/types.d.ts | 25 + 7 files changed, 990 insertions(+), 6 deletions(-) create mode 100644 src/components/setting/mods/dns-viewer.tsx diff --git a/src/components/layout/use-custom-theme.ts b/src/components/layout/use-custom-theme.ts index a58e7c73..3894e142 100644 --- a/src/components/layout/use-custom-theme.ts +++ b/src/components/layout/use-custom-theme.ts @@ -92,7 +92,7 @@ export const useCustomTheme = () => { // css const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d"; const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5"; - const scrollColor = mode === "light" ? "#90939980" : "#54545480"; + const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee"; const dividerColor = mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)"; diff --git a/src/components/setting/mods/dns-viewer.tsx b/src/components/setting/mods/dns-viewer.tsx new file mode 100644 index 00000000..48884339 --- /dev/null +++ b/src/components/setting/mods/dns-viewer.tsx @@ -0,0 +1,854 @@ +import { forwardRef, useImperativeHandle, useState, useEffect } from "react"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { + Box, + Button, + FormControl, + List, + ListItem, + ListItemText, + MenuItem, + Select, + styled, + Switch, + TextField, + Typography, +} from "@mui/material"; +import { RestartAltRounded } from "@mui/icons-material"; +import { useClash } from "@/hooks/use-clash"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import yaml from "js-yaml"; +import MonacoEditor from "react-monaco-editor"; +import { useThemeMode } from "@/services/states"; +import getSystem from "@/utils/get-system"; + +const Item = styled(ListItem)(({ theme }) => ({ + padding: "8px 0", + borderBottom: `1px solid ${theme.palette.divider}`, + "& textarea": { + lineHeight: 1.5, + fontSize: 14, + resize: "vertical", + }, +})); + +// 默认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, + "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": ["223.6.6.6", "8.8.8.8"], + nameserver: [ + "8.8.8.8", + "https://doh.pub/dns-query", + "https://dns.alidns.com/dns-query", + ], + fallback: [ + "https://dns.alidns.com/dns-query", + "https://dns.google/dns-query", + "https://cloudflare-dns.com/dns-query", + ], + "nameserver-policy": {}, + "proxy-server-nameserver": [ + "https://doh.pub/dns-query", + "https://dns.alidns.com/dns-query", + ], + "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 const DnsViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + const { clash, mutateClash, patchClash } = useClash(); + const themeMode = useThemeMode(); + + const [open, setOpen] = useState(false); + const [visualization, setVisualization] = useState(true); + const [values, setValues] = useState<{ + enable: boolean; + listen: string; + enhancedMode: "fake-ip" | "redir-host"; + fakeIpRange: string; + fakeIpFilterMode: "blacklist" | "whitelist"; + preferH3: boolean; + respectRules: boolean; + fakeIpFilter: string; + nameserver: string; + fallback: string; + defaultNameserver: string; + useHosts: boolean; + useSystemHosts: boolean; + proxyServerNameserver: string; + directNameserver: string; + directNameserverFollowPolicy: boolean; + fallbackGeoip: boolean; + fallbackGeoipCode: string; + fallbackIpcidr: string; + fallbackDomain: string; + nameserverPolicy: string; + }>({ + 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"], + 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(", "), + useHosts: DEFAULT_DNS_CONFIG["use-hosts"], + useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"], + 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: "", + }); + + // 用于YAML编辑模式 + const [yamlContent, setYamlContent] = useState(""); + + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + resetToDefaults(); + }, + close: () => setOpen(false), + })); + + // 重置为默认值 + const resetToDefaults = () => { + 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"], + 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(", "), + useHosts: DEFAULT_DNS_CONFIG["use-hosts"], + useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"], + 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: "", + }); + + // 更新YAML编辑器内容 + updateYamlFromValues(DEFAULT_DNS_CONFIG); + }; + + // 从表单值更新YAML内容 + const updateYamlFromValues = (dnsConfig: any = null) => { + // 如果提供了dnsConfig,直接使用它 + if (dnsConfig) { + setYamlContent(yaml.dump(dnsConfig, { forceQuotes: true })); + return; + } + + // 否则从当前表单值生成 + const config = generateDnsConfig(); + setYamlContent(yaml.dump(config, { forceQuotes: true })); + }; + + // 从YAML更新表单值 + const updateValuesFromYaml = () => { + try { + const dnsConfig = yaml.load(yamlContent) as any; + if (!dnsConfig) return; + + const enhancedMode = + dnsConfig["enhanced-mode"] || DEFAULT_DNS_CONFIG["enhanced-mode"]; + // 确保enhancedMode只能是"fake-ip"或"redir-host" + 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"]; + // 确保fakeIpFilterMode只能是"blacklist"或"whitelist" + 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"], + fakeIpFilter: + dnsConfig["fake-ip-filter"]?.join(", ") ?? + DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "), + defaultNameserver: + dnsConfig["default-nameserver"]?.join(", ") ?? + DEFAULT_DNS_CONFIG["default-nameserver"].join(", "), + nameserver: + dnsConfig.nameserver?.join(", ") ?? + DEFAULT_DNS_CONFIG.nameserver.join(", "), + fallback: + dnsConfig.fallback?.join(", ") ?? + DEFAULT_DNS_CONFIG.fallback.join(", "), + useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"], + useSystemHosts: + dnsConfig["use-system-hosts"] ?? + DEFAULT_DNS_CONFIG["use-system-hosts"], + 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"]) || "", + }); + } catch (err: any) { + Notice.error(t("Invalid YAML format")); + } + }; + + // 格式化nameserver-policy为字符串 + const formatNameserverPolicy = (policy: any): string => { + if (!policy) return ""; + + let result: string[] = []; + + Object.entries(policy).forEach(([domain, servers]) => { + if (Array.isArray(servers)) { + // 处理数组格式的服务器 + const serversStr = servers.join(";"); + result.push(`${domain}=${serversStr}`); + } else { + // 处理单个服务器 + result.push(`${domain}=${servers}`); + } + }); + + return result.join(", "); + }; + + // 解析nameserver-policy为对象 + const parseNameserverPolicy = (str: string): Record => { + const result: Record = {}; + if (!str) return result; + + str.split(",").forEach((item) => { + const parts = item.trim().split("="); + if (parts.length < 2) return; + + const domain = parts[0].trim(); + const serversStr = parts.slice(1).join("=").trim(); + + // 检查是否包含多个分号分隔的服务器 + if (serversStr.includes(";")) { + // 多个服务器,作为数组处理 + result[domain] = serversStr + .split(";") + .map((s) => s.trim()) + .filter(Boolean); + } else { + // 单个服务器 + result[domain] = serversStr; + } + }); + + return result; + }; + + // 初始化时设置默认YAML + useEffect(() => { + updateYamlFromValues(DEFAULT_DNS_CONFIG); + }, []); + + // 切换编辑模式时的处理 + useEffect(() => { + if (visualization) { + // 从YAML更新表单值 + updateValuesFromYaml(); + } else { + // 从表单值更新YAML + updateYamlFromValues(); + } + }, [visualization]); + + // 解析列表字符串为数组 + const parseList = (str: string): string[] => { + if (!str) return []; + return str + .split(",") + .map((item) => item.trim()) + .filter(Boolean); + }; + + // 生成DNS配置对象 + const generateDnsConfig = () => { + 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, + "fake-ip-filter": parseList(values.fakeIpFilter), + "default-nameserver": parseList(values.defaultNameserver), + nameserver: parseList(values.nameserver), + fallback: parseList(values.fallback), + "use-hosts": values.useHosts, + "use-system-hosts": values.useSystemHosts, + "fallback-filter": { + geoip: values.fallbackGeoip, + "geoip-code": values.fallbackGeoipCode, + ipcidr: parseList(values.fallbackIpcidr), + domain: parseList(values.fallbackDomain), + }, + }; + + // 只在有nameserverPolicy时添加 + const policy = parseNameserverPolicy(values.nameserverPolicy); + if (Object.keys(policy).length > 0) { + dnsConfig["nameserver-policy"] = policy; + } + + // 只在有值时添加其他可选字段 + if (values.proxyServerNameserver) { + dnsConfig["proxy-server-nameserver"] = parseList( + values.proxyServerNameserver, + ); + } + + if (values.directNameserver) { + dnsConfig["direct-nameserver"] = parseList(values.directNameserver); + } + + dnsConfig["direct-nameserver-follow-policy"] = + values.directNameserverFollowPolicy; + + return dnsConfig; + }; + + const onSave = useLockFn(async () => { + try { + let dnsConfig; + + if (visualization) { + // 使用表单值 + dnsConfig = generateDnsConfig(); + } else { + // 使用YAML编辑器的值 + const parsedConfig = yaml.load(yamlContent); + if (typeof parsedConfig !== "object" || parsedConfig === null) { + throw new Error(t("Invalid DNS configuration")); + } + dnsConfig = parsedConfig; + } + + await patchClash({ dns: dnsConfig }); + mutateClash(); + setOpen(false); + Notice.success(t("DNS settings saved")); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }); + + // YAML编辑器内容变更处理 + const handleYamlChange = (value: string) => { + setYamlContent(value || ""); + + // 允许YAML编辑后立即分析和更新表单值 + try { + const dnsConfig = yaml.load(value) as any; + if (dnsConfig && typeof dnsConfig === "object") { + // 稍微延迟更新,以避免性能问题 + setTimeout(() => { + updateValuesFromYaml(); + }, 300); + } + } catch (err) { + // 忽略解析错误,只有当YAML有效时才更新表单 + 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(null); + }, 0); + } + + return newValues; + }); + }; + + return ( + + {t("DNS Settings")} + + + + + + } + contentSx={{ + width: 550, + overflow: "auto", + ...(visualization + ? {} + : { padding: 0, 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("Fallback Filter 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: true, + smoothScrolling: true, + }} + onChange={handleYamlChange} + /> + )} + + ); +}); diff --git a/src/components/setting/mods/network-interface-viewer.tsx b/src/components/setting/mods/network-interface-viewer.tsx index 5c24eded..bd574b9b 100644 --- a/src/components/setting/mods/network-interface-viewer.tsx +++ b/src/components/setting/mods/network-interface-viewer.tsx @@ -47,7 +47,7 @@ export const NetworkInterfaceViewer = forwardRef((props, ref) => { } - contentSx={{ width: 450, maxHeight: 330 }} + contentSx={{ width: 450 }} disableOk cancelBtn={t("Close")} onCancel={() => setOpen(false)} @@ -66,7 +66,7 @@ export const NetworkInterfaceViewer = forwardRef((props, ref) => { label={t("Ip Address")} content={address.V4.ip} /> - ) + ), )} ((props, ref) => { label={t("Ip Address")} content={address.V6.ip} /> - ) + ), )} { "allow-lan": allowLan, "log-level": logLevel, "unified-delay": unifiedDelay, + dns, } = clash ?? {}; const { enable_random_port = false, verge_mixed_port } = verge ?? {}; @@ -47,6 +50,7 @@ const SettingClash = ({ onError }: Props) => { const ctrlRef = useRef(null); const coreRef = useRef(null); const networkRef = useRef(null); + const dnsRef = useRef(null); const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial) => { @@ -71,6 +75,7 @@ const SettingClash = ({ onError }: Props) => { + { + dnsRef.current?.open()} + /> + } + > + onChangeData({ dns: { ...dns, enable: e } })} + onGuard={(e) => patchClash({ dns: { enable: e } })} + > + + + + ; + "use-hosts"?: boolean; + "use-system-hosts"?: boolean; + "fallback-filter"?: { + geoip?: boolean; + "geoip-code"?: string; + ipcidr?: string[]; + domain?: string[]; + }; + }; } interface IRuleItem {