feat: add dns settings

This commit is contained in:
wonfen 2025-03-06 14:30:43 +08:00
parent 69cb9769c1
commit f80591242e
7 changed files with 990 additions and 6 deletions

View File

@ -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)";

View File

@ -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<DialogRef>((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<string, any> => {
const result: Record<string, any> = {};
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 (
<BaseDialog
open={open}
title={
<Box display="flex" justifyContent="space-between" alignItems="center">
{t("DNS Settings")}
<Box display="flex" alignItems="center" gap={1}>
<Button
variant="outlined"
size="small"
color="warning"
startIcon={<RestartAltRounded />}
onClick={resetToDefaults}
>
{t("Reset to Default")}
</Button>
<Button
variant="contained"
size="small"
onClick={() => {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
</Button>
</Box>
</Box>
}
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 */}
<Typography
variant="body2"
color="warning.main"
sx={{ mb: 2, mt: 0, fontStyle: "italic" }}
>
{t("DNS Settings Warning")}
</Typography>
{visualization ? (
<List>
<Item>
<ListItemText primary={t("Enable DNS")} />
<Switch
edge="end"
checked={values.enable}
onChange={handleChange("enable")}
/>
</Item>
<Item>
<ListItemText primary={t("DNS Listen")} />
<TextField
size="small"
autoComplete="off"
value={values.listen}
onChange={handleChange("listen")}
placeholder=":53"
sx={{ width: 150 }}
/>
</Item>
<Item>
<ListItemText primary={t("Enhanced Mode")} />
<FormControl size="small" sx={{ width: 150 }}>
<Select
value={values.enhancedMode}
onChange={handleChange("enhancedMode")}
>
<MenuItem value="fake-ip">fake-ip</MenuItem>
<MenuItem value="redir-host">redir-host</MenuItem>
</Select>
</FormControl>
</Item>
<Item>
<ListItemText primary={t("Fake IP Range")} />
<TextField
size="small"
autoComplete="off"
value={values.fakeIpRange}
onChange={handleChange("fakeIpRange")}
placeholder="198.18.0.1/16"
sx={{ width: 150 }}
/>
</Item>
<Item>
<ListItemText primary={t("Fake IP Filter Mode")} />
<FormControl size="small" sx={{ width: 150 }}>
<Select
value={values.fakeIpFilterMode}
onChange={handleChange("fakeIpFilterMode")}
>
<MenuItem value="blacklist">blacklist</MenuItem>
<MenuItem value="whitelist">whitelist</MenuItem>
</Select>
</FormControl>
</Item>
<Item>
<ListItemText
primary={t("Prefer H3")}
secondary={t("DNS DOH使用HTTP/3")}
/>
<Switch
edge="end"
checked={values.preferH3}
onChange={handleChange("preferH3")}
/>
</Item>
<Item>
<ListItemText
primary={t("Respect Rules")}
secondary={t("DNS连接遵守路由规则")}
/>
<Switch
edge="end"
checked={values.respectRules}
onChange={handleChange("respectRules")}
/>
</Item>
<Item>
<ListItemText
primary={t("Use Hosts")}
secondary={t("Enable to resolve hosts through hosts file")}
/>
<Switch
edge="end"
checked={values.useHosts}
onChange={handleChange("useHosts")}
/>
</Item>
<Item>
<ListItemText
primary={t("Use System Hosts")}
secondary={t("Enable to resolve hosts through system hosts file")}
/>
<Switch
edge="end"
checked={values.useSystemHosts}
onChange={handleChange("useSystemHosts")}
/>
</Item>
<Item>
<ListItemText
primary={t("Direct Nameserver Follow Policy")}
secondary={t("是否遵循nameserver-policy")}
/>
<Switch
edge="end"
checked={values.directNameserverFollowPolicy}
onChange={handleChange("directNameserverFollowPolicy")}
/>
</Item>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Default Nameserver")}
secondary={t("Default DNS servers used to resolve DNS servers")}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
value={values.defaultNameserver}
onChange={handleChange("defaultNameserver")}
placeholder="223.6.6.6, 8.8.8.8"
/>
</Item>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Nameserver")}
secondary={t("List of DNS servers")}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
value={values.nameserver}
onChange={handleChange("nameserver")}
placeholder="8.8.8.8, https://doh.pub/dns-query, https://dns.alidns.com/dns-query"
/>
</Item>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fallback")}
secondary={t("List of fallback DNS servers")}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
value={values.fallback}
onChange={handleChange("fallback")}
placeholder="https://dns.alidns.com/dns-query, https://dns.google/dns-query, https://cloudflare-dns.com/dns-query"
/>
</Item>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Proxy Server Nameserver")}
secondary={t("Proxy Node Nameserver")}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
value={values.proxyServerNameserver}
onChange={handleChange("proxyServerNameserver")}
placeholder="https://doh.pub/dns-query, https://dns.alidns.com/dns-query"
/>
</Item>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Direct Nameserver")}
secondary={t("Direct outbound Nameserver")}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
value={values.directNameserver}
onChange={handleChange("directNameserver")}
placeholder="system, 223.6.6.6"
/>
</Item>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fake IP Filter")}
secondary={t("Domains that skip fake IP resolution")}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
value={values.fakeIpFilter}
onChange={handleChange("fakeIpFilter")}
placeholder="*.lan, *.local, localhost.ptlogin2.qq.com"
/>
</Item>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Nameserver Policy")}
secondary={t("Domain-specific DNS server")}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={4}
size="small"
value={values.nameserverPolicy}
onChange={handleChange("nameserverPolicy")}
placeholder="+.arpa=10.0.0.1, rule-set:cn=https://doh.pub/dns-query;https://dns.alidns.com/dns-query"
/>
</Item>
<Typography
variant="subtitle2"
sx={{ mt: 2, mb: 1, fontWeight: "bold" }}
>
{t("Fallback Filter Settings")}
</Typography>
<Item>
<ListItemText
primary={t("GeoIP Filtering")}
secondary={t("Enable GeoIP filtering for fallback")}
/>
<Switch
edge="end"
checked={values.fallbackGeoip}
onChange={handleChange("fallbackGeoip")}
/>
</Item>
<Item>
<ListItemText primary={t("GeoIP Code")} />
<TextField
size="small"
autoComplete="off"
value={values.fallbackGeoipCode}
onChange={handleChange("fallbackGeoipCode")}
placeholder="CN"
sx={{ width: 100 }}
/>
</Item>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fallback IP CIDR")}
secondary={t("IP CIDRs not using fallback servers")}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
value={values.fallbackIpcidr}
onChange={handleChange("fallbackIpcidr")}
placeholder="240.0.0.0/4, 127.0.0.1/8"
/>
</Item>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fallback Domain")}
secondary={t("Domains using fallback servers")}
/>
<TextField
fullWidth
multiline
minRows={2}
maxRows={3}
size="small"
value={values.fallbackDomain}
onChange={handleChange("fallbackDomain")}
placeholder="+.google.com, +.facebook.com, +.youtube.com"
/>
</Item>
</List>
) : (
<MonacoEditor
height="100vh"
language="yaml"
value={yamlContent}
theme={themeMode === "light" ? "vs" : "vs-dark"}
className="flex-grow"
options={{
tabSize: 2,
minimap: {
enabled: document.documentElement.clientWidth >= 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}
/>
)}
</BaseDialog>
);
});

View File

@ -47,7 +47,7 @@ export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
</Box>
</Box>
}
contentSx={{ width: 450, maxHeight: 330 }}
contentSx={{ width: 450 }}
disableOk
cancelBtn={t("Close")}
onCancel={() => setOpen(false)}
@ -66,7 +66,7 @@ export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
label={t("Ip Address")}
content={address.V4.ip}
/>
)
),
)}
<AddressDisplay
label={t("Mac Address")}
@ -84,7 +84,7 @@ export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
label={t("Ip Address")}
content={address.V6.ip}
/>
)
),
)}
<AddressDisplay
label={t("Mac Address")}

View File

@ -5,6 +5,7 @@ import {
SettingsRounded,
ShuffleRounded,
LanRounded,
DnsRounded,
} from "@mui/icons-material";
import { DialogRef, Notice, Switch } from "@/components/base";
import { useClash } from "@/hooks/use-clash";
@ -20,6 +21,7 @@ import { useVerge } from "@/hooks/use-verge";
import { updateGeoData } from "@/services/api";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
import { DnsViewer } from "./mods/dns-viewer";
const isWIN = getSystem() === "windows";
@ -38,6 +40,7 @@ const SettingClash = ({ onError }: Props) => {
"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<DialogRef>(null);
const coreRef = useRef<DialogRef>(null);
const networkRef = useRef<DialogRef>(null);
const dnsRef = useRef<DialogRef>(null);
const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IConfigData>) => {
@ -71,6 +75,7 @@ const SettingClash = ({ onError }: Props) => {
<ControllerViewer ref={ctrlRef} />
<ClashCoreViewer ref={coreRef} />
<NetworkInterfaceViewer ref={networkRef} />
<DnsViewer ref={dnsRef} />
<SettingItem
label={t("Allow Lan")}
@ -97,6 +102,27 @@ const SettingClash = ({ onError }: Props) => {
</GuardState>
</SettingItem>
<SettingItem
label={t("DNS Settings")}
extra={
<TooltipIcon
icon={SettingsRounded}
onClick={() => dnsRef.current?.open()}
/>
}
>
<GuardState
value={dns?.enable ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ dns: { ...dns, enable: e } })}
onGuard={(e) => patchClash({ dns: { enable: e } })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem label={t("IPv6")}>
<GuardState
value={ipv6 ?? false}

View File

@ -471,5 +471,44 @@
"Validate Merge File": "Validate Merge File",
"Validation Success": "Validation Success",
"Validation Failed": "Validation Failed",
"Service Administrator Prompt": "Clash Verge requires administrator privileges to reinstall the system service"
"Service Administrator Prompt": "Clash Verge requires administrator privileges to reinstall the system service",
"DNS Settings": "DNS Settings",
"DNS Settings Warning": "If you are not familiar with these settings, please do not modify them and keep DNS Settings enabled",
"Enable DNS": "Enable DNS",
"DNS Listen": "DNS Listen",
"Enhanced Mode": "Enhanced Mode",
"Fake IP Range": "Fake IP Range",
"Fake IP Filter Mode": "Fake IP Filter Mode",
"Prefer H3": "Prefer H3",
"DNS DOH使用HTTP/3": "DNS DOH uses HTTP/3",
"Respect Rules": "Respect Rules",
"DNS连接遵守路由规则": "DNS connections follow routing rules",
"Use Hosts": "Use Hosts",
"Enable to resolve hosts through hosts file": "Enable to resolve hosts through hosts file",
"Use System Hosts": "Use System Hosts",
"Enable to resolve hosts through system hosts file": "Enable to resolve hosts through system hosts file",
"Direct Nameserver Follow Policy": "Direct Nameserver Follow Policy",
"是否遵循nameserver-policy": "Whether to follow nameserver policy",
"Default Nameserver": "Default Nameserver",
"Default DNS servers used to resolve DNS servers": "Default DNS servers used to resolve DNS servers",
"Nameserver": "Nameserver",
"List of DNS servers": "List of DNS servers, comma separated",
"Fallback": "Fallback",
"List of fallback DNS servers": "List of fallback DNS servers, comma separated",
"Proxy Server Nameserver": "Proxy Server Nameserver",
"Proxy Node Nameserver": "DNS servers for proxy node domain resolution",
"Direct Nameserver": "Direct Nameserver",
"Direct outbound Nameserver": "DNS servers for direct exit domain resolution, supports 'system' keyword, comma separated",
"Fake IP Filter": "Fake IP Filter",
"Domains that skip fake IP resolution": "Domains that skip fake IP resolution, comma separated",
"Nameserver Policy": "Nameserver Policy",
"Domain-specific DNS server": "Domain-specific DNS server, multiple servers separated by semicolons, format: domain=server1;server2",
"Fallback Filter Settings": "Fallback Filter Settings",
"GeoIP Filtering": "GeoIP Filtering",
"Enable GeoIP filtering for fallback": "Enable GeoIP filtering for fallback",
"GeoIP Code": "GeoIP Code",
"Fallback IP CIDR": "Fallback IP CIDR",
"IP CIDRs not using fallback servers": "IP CIDRs not using fallback servers, comma separated",
"Fallback Domain": "Fallback Domain",
"Domains using fallback servers": "Domains using fallback servers, comma separated"
}

View File

@ -463,5 +463,45 @@
"Validation Failed": "验证失败",
"Verge Basic Setting": "Verge 基础设置",
"Verge Advanced Setting": "Verge 高级设置",
"Service Administrator Prompt": "Clash Verge 需要使用管理员权限来重新安装系统服务"
"Service Administrator Prompt": "Clash Verge 需要使用管理员权限来重新安装系统服务",
"DNS Settings": "DNS 设置",
"DNS Settings Warning": "如果你不清楚这里的设置请不要修改并保持dns设置开启",
"Enable DNS": "启用 DNS",
"DNS Listen": "DNS 监听地址",
"Enhanced Mode": "增强模式",
"Fake IP Range": "Fake IP 范围",
"Fake IP Filter Mode": "Fake IP 过滤模式",
"Prefer H3": "优先使用 HTTP/3",
"DNS DOH使用HTTP/3": "DNS DOH 使用 HTTP/3 协议",
"Respect Rules": "遵循路由规则",
"DNS连接遵守路由规则": "DNS 连接遵循路由规则",
"Use Hosts": "使用 Hosts",
"Enable to resolve hosts through hosts file": "启用通过 hosts 文件解析域名",
"Use System Hosts": "使用系统 Hosts",
"Enable to resolve hosts through system hosts file": "启用通过系统 hosts 文件解析域名",
"Direct Nameserver Follow Policy": "直连域名服务器遵循策略",
"是否遵循nameserver-policy": "是否遵循 nameserver-policy 设置",
"Default Nameserver": "默认域名服务器",
"Default DNS servers used to resolve DNS servers": "用于解析 DNS 服务器的默认 DNS 服务器",
"Nameserver": "域名服务器",
"List of DNS servers": "DNS 服务器列表,用逗号分隔",
"Fallback": "回退服务器",
"List of fallback DNS servers": "回退 DNS 服务器列表,用逗号分隔",
"Proxy Server Nameserver": "代理节点DNS",
"Proxy Node Nameserver": "代理节点域名解析服务器,仅用于解析代理节点的域名,用逗号分隔",
"Direct Nameserver": "直连域名服务器",
"Direct outbound Nameserver": "直连出口域名解析服务器,支持 system 关键字,用逗号分隔",
"Fake IP Filter": "Fake IP 过滤",
"Domains that skip fake IP resolution": "跳过 Fake IP 解析的域名,用逗号分隔",
"Nameserver Policy": "域名服务器策略",
"Domain-specific DNS server": "特定域名的 DNS 服务器,多个服务器使用分号分隔,格式: domain=server1;server2",
"Fallback Filter Settings": "回退过滤设置",
"GeoIP Filtering": "GeoIP 过滤",
"Enable GeoIP filtering for fallback": "启用 GeoIP 回退过滤",
"GeoIP Code": "GeoIP 国家代码",
"Fallback IP CIDR": "回退 IP CIDR",
"IP CIDRs not using fallback servers": "不使用回退服务器的 IP CIDR用逗号分隔",
"Fallback Domain": "回退域名",
"Domains using fallback servers": "使用回退服务器的域名,用逗号分隔",
"Fallback Geosite": "回退 Geosite"
}

View File

@ -42,6 +42,31 @@ interface IConfigData {
"strict-route": boolean;
mtu: number;
};
dns?: {
enable?: boolean;
listen?: string;
"enhanced-mode"?: "fake-ip" | "redir-host";
"fake-ip-range"?: string;
"fake-ip-filter"?: string[];
"fake-ip-filter-mode"?: "blacklist" | "whitelist";
"prefer-h3"?: boolean;
"respect-rules"?: boolean;
nameserver?: string[];
fallback?: string[];
"default-nameserver"?: string[];
"proxy-server-nameserver"?: string[];
"direct-nameserver"?: string[];
"direct-nameserver-follow-policy"?: boolean;
"nameserver-policy"?: Record<string, any>;
"use-hosts"?: boolean;
"use-system-hosts"?: boolean;
"fallback-filter"?: {
geoip?: boolean;
"geoip-code"?: string;
ipcidr?: string[];
domain?: string[];
};
};
}
interface IRuleItem {