fix: validate system proxy bypass formats

This commit is contained in:
zjdndjf
2026-08-01 01:29:40 +08:00
parent ac7bf09612
commit 4c168ffa96
9 changed files with 105 additions and 24 deletions

View File

@@ -15,6 +15,7 @@
- 修复 macOS 下离线状态或慢 helper 可能导致系统代理和 DNS 清理长时间阻塞退出,以及延迟 DNS 任务可能在退出恢复后再次改写 DNS 的问题
- 修复 Windows 注销或关机时 `session-end` 与退出事件重复执行清理,并可能绕过有界退出流程的问题
- 修复 Windows 管理员权限检测失败结果被错误缓存、跳过备用检测的问题
- 修复系统代理绕过项未按平台格式校验,以及 Windows 绕过列表分隔符错误的问题
# 2.0.0

View File

@@ -93,6 +93,10 @@ async function enableSysProxy(helperTimeout?: number): Promise<void> {
const { mode, host, bypass = defaultBypass } = sysProxy
const { 'mixed-port': port = DEFAULT_MIHOMO_PORTS.mixed } = await getControledMihomoConfig()
const proxyHost = host || '127.0.0.1'
const formattedBypass = bypass
.map((entry) => entry.trim())
.filter(Boolean)
.join(process.platform === 'win32' ? ';' : ',')
if (process.platform === 'darwin') {
// macOS 需要 helper 提权
@@ -108,7 +112,7 @@ async function enableSysProxy(helperTimeout?: number): Promise<void> {
await helperRequest(() =>
axios.post(
'http://localhost/global',
{ host: proxyHost, port: port.toString(), bypass: bypass.join(',') },
{ host: proxyHost, port: port.toString(), bypass: formattedBypass },
helperAxiosOptions(helperTimeout)
)
)
@@ -119,7 +123,7 @@ async function enableSysProxy(helperTimeout?: number): Promise<void> {
if (mode === 'auto') {
triggerAutoProxy(true, `http://${proxyHost}:${pacPort}/pac`)
} else {
triggerManualProxy(true, proxyHost, port, bypass.join(','))
triggerManualProxy(true, proxyHost, port, formattedBypass)
}
} catch (error) {
await proxyLogger.error('Failed to enable system proxy', error)

View File

@@ -473,7 +473,10 @@
"sysproxy.pacEditor.title": "Edit PAC Script",
"sysproxy.bypass.title": "Proxy Bypass",
"sysproxy.bypass.addDefault": "Add Default Bypass",
"sysproxy.bypass.placeholder": "Example: *.baidu.com",
"sysproxy.bypass.placeholder": "Example: {{example}}",
"sysproxy.bypass.invalid.windows": "Enter a valid host, IP address, or Windows wildcard, for example 127.*",
"sysproxy.bypass.invalid.unix": "Enter a valid host, IP address, CIDR network, or wildcard, for example 192.168.0.0/16",
"sysproxy.bypass.notSaved": "Invalid bypass entries won't be saved; fix or remove them first.",
"tun.title": "TUN",
"tun.firewall.title": "Reset Firewall",
"tun.firewall.reset": "Reset Firewall",

View File

@@ -462,7 +462,10 @@
"sysproxy.pacEditor.title": "ویرایش اسکریپت PAC",
"sysproxy.bypass.title": "دور زدن پراکسی",
"sysproxy.bypass.addDefault": "افزودن دور زدن پیش‌فرض",
"sysproxy.bypass.placeholder": "مثال: *.baidu.com",
"sysproxy.bypass.placeholder": "مثال: {{example}}",
"sysproxy.bypass.invalid.windows": "یک میزبان، نشانی IP یا الگوی معتبر Windows وارد کنید، مانند 127.*",
"sysproxy.bypass.invalid.unix": "یک میزبان، نشانی IP، شبکه CIDR یا الگوی معتبر وارد کنید، مانند 192.168.0.0/16",
"sysproxy.bypass.notSaved": "موارد نامعتبر دور زدن ذخیره نمی‌شوند؛ ابتدا آن‌ها را اصلاح یا حذف کنید.",
"tun.title": "TUN",
"tun.firewall.title": "بازنشانی دیوار آتش",
"tun.firewall.reset": "بازنشانی دیوار آتش",

View File

@@ -464,7 +464,10 @@
"sysproxy.pacEditor.title": "Редактировать PAC скрипт",
"sysproxy.bypass.title": "Исключения прокси",
"sysproxy.bypass.addDefault": "Добавить стандартные исключения",
"sysproxy.bypass.placeholder": "Пример: vk.com",
"sysproxy.bypass.placeholder": "Пример: {{example}}",
"sysproxy.bypass.invalid.windows": "Введите корректное имя хоста, IP-адрес или шаблон Windows, например 127.*",
"sysproxy.bypass.invalid.unix": "Введите корректное имя хоста, IP-адрес, CIDR-сеть или шаблон, например 192.168.0.0/16",
"sysproxy.bypass.notSaved": "Некорректные исключения не будут сохранены; исправьте или удалите их.",
"tun.title": "TUN",
"tun.firewall.title": "Сбросить брандмауэр",
"tun.firewall.reset": "Сбросить брандмауэр",

View File

@@ -473,7 +473,10 @@
"sysproxy.pacEditor.title": "编辑 PAC 脚本",
"sysproxy.bypass.title": "代理绕过",
"sysproxy.bypass.addDefault": "添加默认代理绕过",
"sysproxy.bypass.placeholder": "例:*.baidu.com",
"sysproxy.bypass.placeholder": "例:{{example}}",
"sysproxy.bypass.invalid.windows": "请输入有效的主机、IP 地址或 Windows 通配符,例如 127.*",
"sysproxy.bypass.invalid.unix": "请输入有效的主机、IP 地址、CIDR 网段或通配符,例如 192.168.0.0/16",
"sysproxy.bypass.notSaved": "无效的代理绕过项不会被保存,请先修正或删除。",
"tun.title": "虚拟网卡",
"tun.firewall.title": "重设防火墙",
"tun.firewall.reset": "重设防火墙",

View File

@@ -473,7 +473,10 @@
"sysproxy.pacEditor.title": "編輯 PAC 腳本",
"sysproxy.bypass.title": "代理繞過",
"sysproxy.bypass.addDefault": "添加默認代理繞過",
"sysproxy.bypass.placeholder": "例:*.baidu.com",
"sysproxy.bypass.placeholder": "例:{{example}}",
"sysproxy.bypass.invalid.windows": "請輸入有效的主機、IP 位址或 Windows 萬用字元,例如 127.*",
"sysproxy.bypass.invalid.unix": "請輸入有效的主機、IP 位址、CIDR 網段或萬用字元,例如 192.168.0.0/16",
"sysproxy.bypass.notSaved": "無效的代理繞過項不會被儲存,請先修正或刪除。",
"tun.title": "虛擬網卡",
"tun.firewall.title": "重設防火牆",
"tun.firewall.reset": "重設防火牆",

View File

@@ -7,6 +7,7 @@ import PacEditorModal from '@renderer/components/sysproxy/pac-editor-modal'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { platform } from '@renderer/utils/init'
import { openUWPTool, triggerSysProxy } from '@renderer/utils/ipc'
import { sysProxyBypassValidator } from '@renderer/utils/validate'
import React, { Key, useState } from 'react'
import { MdDeleteForever } from 'react-icons/md'
import { useTranslation } from 'react-i18next'
@@ -73,6 +74,20 @@ const Sysproxy: React.FC = () => {
setChanged(true)
}
const normalizedBypass = values.bypass.map((entry) => entry.trim()).filter(Boolean)
const hasInvalidBypass = normalizedBypass.some(
(entry) => !sysProxyBypassValidator(entry, platform)
)
const bypassInputs = hasInvalidBypass ? values.bypass : [...values.bypass, '']
const bypassExample = platform === 'win32' ? '127.*' : '192.168.0.0/16'
const getBypassError = (entry: string): string | undefined => {
const trimmedEntry = entry.trim()
if (trimmedEntry === '' || sysProxyBypassValidator(trimmedEntry, platform)) return undefined
return t(
platform === 'win32' ? 'sysproxy.bypass.invalid.windows' : 'sysproxy.bypass.invalid.unix'
)
}
const [openPacEditor, setOpenPacEditor] = useState(false)
const handleBypassChange = (value: string, index: number): void => {
@@ -96,9 +111,16 @@ const Sysproxy: React.FC = () => {
// 保存当前的开关状态,以便在失败时恢复
const previousState = values.enable
const sysProxyPatch: ISysProxyConfig = { ...values }
if (hasInvalidBypass) {
// 存在非法条目时不覆盖已保存的绕过列表,其它系统代理设置照常保存
delete sysProxyPatch.bypass
} else {
sysProxyPatch.bypass = normalizedBypass
}
try {
await patchAppConfig({ sysProxy: values })
await patchAppConfig({ sysProxy: sysProxyPatch })
await triggerSysProxy(true)
await patchAppConfig({ sysProxy: { enable: true } })
@@ -189,13 +211,15 @@ const Sysproxy: React.FC = () => {
</SettingItem>
<div className="flex flex-col items-stretch">
<h3 className="mb-2">{t('sysproxy.bypass.title')}</h3>
{[...values.bypass, ''].map((domain, index) => (
{bypassInputs.map((domain, index) => (
<div key={index} className="mb-2 flex">
<Input
fullWidth
size="sm"
placeholder={t('sysproxy.bypass.placeholder')}
placeholder={t('sysproxy.bypass.placeholder', { example: bypassExample })}
value={domain}
isInvalid={Boolean(getBypassError(domain))}
errorMessage={getBypassError(domain)}
onValueChange={(v) => handleBypassChange(v, index)}
/>
{index < values.bypass.length && (
@@ -211,6 +235,9 @@ const Sysproxy: React.FC = () => {
)}
</div>
))}
{hasInvalidBypass && (
<div className="px-1 text-xs text-danger">{t('sysproxy.bypass.notSaved')}</div>
)}
</div>
</>
)}

View File

@@ -40,7 +40,7 @@ const portValidator = (value: string): boolean => {
}
const ipv4CIDRValidator = (value: string): boolean => {
// 验证 IPv4 CIDR 格式 (例如: 192.168.1.0/24)
// 验证 IPv4 CIDR 格式 (例如192.168.1.0/24)
if (!value.includes('/')) return false
const [ip, cidr] = value.split('/')
@@ -50,7 +50,7 @@ const ipv4CIDRValidator = (value: string): boolean => {
}
const ipv6CIDRValidator = (value: string): boolean => {
// 验证 IPv6 CIDR 格式 (例如: 2001:db8::/32)
// 验证 IPv6 CIDR 格式 (例如2001:db8::/32)
if (!value.includes('/')) return false
const [ip, cidr] = value.split('/')
@@ -70,6 +70,39 @@ const ipCIDRValidator = (value: string): boolean => {
return ipv4CIDRValidator(value) || ipv6CIDRValidator(value)
}
const sysProxyBypassValidator = (
value: string,
targetPlatform: NodeJS.Platform | string
): boolean => {
const entry = value.trim()
if (entry === '') return false
if (validator.isIP(entry)) return true
if (targetPlatform !== 'win32' && validator.isIPRange(entry)) return true
if (
(targetPlatform === 'win32' || targetPlatform === 'darwin') &&
entry.toLowerCase() === '<local>'
) {
return true
}
if (targetPlatform === 'win32' && /[*?]/.test(entry)) {
const normalizedPattern = entry.replace(/\*/g, 'wildcard').replace(/\?/g, 'q')
return validator.isFQDN(normalizedPattern, {
require_tld: false,
allow_numeric_tld: true
})
}
return validator.isFQDN(entry, {
require_tld: false,
allow_numeric_tld: true,
allow_wildcard: true
})
}
// DOMAIN-WILDCARD 验证器 - 仅支持 * 和 ? 通配符
const domainWildcardValidator = (value: string): boolean => {
if (value.length === 0) return false
@@ -90,25 +123,25 @@ const geositeValidator = (value: string): boolean => {
// GEOIP 验证器 - 国家代码验证ISO 3166-1 alpha-2
const geoipValidator = (value: string): boolean => {
// 支持2位国家代码(大小写不敏感)
// 支持 2 位国家代码(大小写不敏感)
return validator.isAlpha(value) && value.length === 2
}
// ASN 验证器 - 自治系统号验证
const asnValidator = (value: string): boolean => {
// ASN 范围: 1 - 4294967295 (32-bit)
// ASN 范围1 - 4294967295 (32-bit)
return validator.isInt(value, { min: 1, max: 4294967295 })
}
// UID 验证器 - Linux 用户 ID 验证
const uidValidator = (value: string): boolean => {
// UID 范围: 0 - 65535 (大多数系统)
// UID 范围0 - 65535 (大多数系统)
return validator.isInt(value, { min: 0, max: 65535 })
}
// DSCP 验证器 - 区分服务代码点验证
const dscpValidator = (value: string): boolean => {
// DSCP 范围: 0 - 63 (6-bit)
// DSCP 范围0 - 63 (6-bit)
return validator.isInt(value, { min: 0, max: 63 })
}
@@ -218,7 +251,7 @@ const logicRuleValidator = (value: string): boolean => {
// SUB-RULE 验证器 - 子规则验证
const subRuleValidator = (value: string): boolean => {
if (value.length === 0) return false
// 格式: (RULE_TYPE,payload) 或 provider_name
// 格式(RULE_TYPE,payload) 或 provider_name
if (value.startsWith('(') && value.endsWith(')')) {
return logicRuleValidator(value)
}
@@ -228,7 +261,7 @@ const subRuleValidator = (value: string): boolean => {
// 端口范围验证器(支持单个端口或范围)
const portRangeValidator = (value: string): boolean => {
// 支持单个端口或范围格式,如: 80 或 8000-9000
// 支持单个端口或范围格式,如80 或 8000-9000
if (value.includes('-')) {
const [start, end] = value.split('-')
return validator.isPort(start) && validator.isPort(end) && parseInt(start) <= parseInt(end)
@@ -264,7 +297,8 @@ export {
portRangeValidator,
ipv4CIDRValidator,
ipv6CIDRValidator,
ipCIDRValidator
ipCIDRValidator,
sysProxyBypassValidator
}
// 通用验证结果类型
@@ -303,7 +337,7 @@ export const isValidListenAddress = (s: string | undefined): ValidationResult =>
const v = s.trim()
// 格式: :port (仅端口)
// 格式:port (仅端口)
if (v.startsWith(':')) {
return isValidPort(v.slice(1))
}
@@ -318,7 +352,7 @@ export const isValidListenAddress = (s: string | undefined): ValidationResult =>
const portResult = isValidPort(port)
if (!portResult.ok) return portResult
// 格式: [IPv6]:port
// 格式[IPv6]:port
if (host.startsWith('[') && host.endsWith(']')) {
const inner = host.slice(1, -1)
return isIPv6(inner)
@@ -346,7 +380,7 @@ export const isValidListenAddressFull = (s: string | undefined): ValidationResul
const v = s.trim()
// 格式: :port (仅端口)
// 格式:port (仅端口)
if (v.startsWith(':')) {
return isValidPort(v.slice(1))
}
@@ -361,13 +395,13 @@ export const isValidListenAddressFull = (s: string | undefined): ValidationResul
const portResult = isValidPort(port)
if (!portResult.ok) return portResult
// 格式: [IPv6]:port
// 格式[IPv6]:port
if (host.startsWith('[') && host.endsWith(']')) {
const inner = host.slice(1, -1)
return isIPv6(inner)
}
// 特殊地址: 0.0.0.0 (监听所有 IPv4) 或 :: (监听所有 IPv6)
// 特殊地址0.0.0.0 (监听所有 IPv4) 或 :: (监听所有 IPv6)
if (host === '0.0.0.0' || host === '::') {
return { ok: true }
}