diff --git a/Changelog.md b/Changelog.md index 26d3e8530..db32f327b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -51,7 +51,7 @@ ✨ 新增功能 - 新增连接页面按网络协议(TCP/UDP)筛选连接 -- 新增从连接详情快捷设置域名、进程或源/目标 IP 规则,支持父域及子域匹配与修改已有规则 +- 新增从连接详情设置域名、进程或源/目标 IP 规则,支持自动识别主域、匹配子域及修改已有规则 diff --git a/package.json b/package.json index fa53db6e5..5175c8af6 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "rehype-sanitize": "^6.0.0", "swr": "^2.5.1", "tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main", + "tldts": "7.4.19", "types-pac": "^1.0.3", "validator": "^13.15.35" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10c182e43..439c138c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -295,6 +295,9 @@ importers: tauri-plugin-mihomo-api: specifier: github:clash-verge-rev/tauri-plugin-mihomo#main version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/e49bbedcc67d60da2c10e81bc6433b0f8e8fa232 + tldts: + specifier: 7.4.19 + version: 7.4.19 types-pac: specifier: ^1.0.3 version: 1.0.3 @@ -4035,6 +4038,13 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tldts-core@7.5.2: + resolution: {integrity: sha512-8W1topazDwhMKbm2I83IPa57Ow5VUxFgLbXv42+vSILnv8FY5OHvdEY/tS6WoWeMbxkHZQhra5cmeFHriKptSQ==} + + tldts@7.4.19: + resolution: {integrity: sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==} + hasBin: true + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -8138,6 +8148,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tldts-core@7.5.2: {} + + tldts@7.4.19: + dependencies: + tldts-core: 7.5.2 + trim-lines@3.0.1: {} trough@2.2.0: {} diff --git a/src/components/connection/connection-rule-dialog.tsx b/src/components/connection/connection-rule-dialog.tsx index b8e40e9ad..e6d8fb874 100644 --- a/src/components/connection/connection-rule-dialog.tsx +++ b/src/components/connection/connection-rule-dialog.tsx @@ -29,6 +29,7 @@ import { showNotice } from '@/services/notice-service' import { getConnectionRuleTargets, getConnectionRules, + getConnectionRootDomain, isConnectionRuleDomain, findConnectionRule, saveConnectionRule, @@ -43,6 +44,8 @@ export interface ConnectionRuleTarget { preferredKind: ConnectionRuleKind } +type DomainScope = 'DOMAIN' | 'DOMAIN-SUFFIX' | 'ROOT-DOMAIN' + function getInitialKind(target: ConnectionRuleTarget) { const targets = getConnectionRuleTargets(target.metadata) return targets[target.preferredKind] @@ -63,11 +66,12 @@ export function ConnectionRuleDialog({ const { proxyView } = useProxiesData() const { clashConfig } = useClashConfigData() const targets = getConnectionRuleTargets(target.metadata) + const [rootDomain, setRootDomain] = useState() const [kind, setKind] = useState(() => getInitialKind(target), ) const [policy, setPolicy] = useState(null) - const [scope, setScope] = useState<'DOMAIN' | 'DOMAIN-SUFFIX'>('DOMAIN') + const [scope, setScope] = useState('DOMAIN') const [domainValue, setDomainValue] = useState(targets.domain?.value ?? '') const [rules, setRules] = useState(null) const [loadAttempt, setLoadAttempt] = useState(0) @@ -75,14 +79,23 @@ export function ConnectionRuleDialog({ const [saving, setSaving] = useState(false) const [error, setError] = useState('') const selected = targets[kind] + const matchingRoot = kind === 'domain' && scope === 'ROOT-DOMAIN' const editingDomain = kind === 'domain' && scope === 'DOMAIN-SUFFIX' + const matchingValue = matchingRoot + ? (rootDomain ?? '') + : editingDomain + ? domainValue + : (selected?.value ?? '') const invalidDomain = editingDomain && !isConnectionRuleDomain(domainValue, targets.domain?.value ?? '') const condition = selected ? { - type: kind === 'domain' ? scope : selected.type, - value: editingDomain ? domainValue.trim() : selected.value, + type: + kind === 'domain' && scope !== 'DOMAIN' + ? 'DOMAIN-SUFFIX' + : selected.type, + value: kind === 'domain' ? matchingValue.trim() : selected.value, } : undefined const existing = @@ -111,10 +124,15 @@ export function ConnectionRuleDialog({ useEffect(() => { let cancelled = false - readProfileFile(target.rulesUid) - .then((content) => { + const ruleTargets = getConnectionRuleTargets(target.metadata) + Promise.all([ + readProfileFile(target.rulesUid), + ruleTargets.domain + ? getConnectionRootDomain(ruleTargets.domain.value) + : undefined, + ]) + .then(([content, root]) => { const loadedRules = getConnectionRules(content) - const ruleTargets = getConnectionRuleTargets(target.metadata) const initialKind = getInitialKind(target) const selectedTarget = ruleTargets[initialKind] const initial = selectedTarget @@ -127,6 +145,7 @@ export function ConnectionRuleDialog({ : undefined)) : undefined if (cancelled) return + setRootDomain(root) setRules(loadedRules) setPolicy(initial?.policy ?? null) setScope(initial?.type === 'DOMAIN-SUFFIX' ? 'DOMAIN-SUFFIX' : 'DOMAIN') @@ -279,11 +298,11 @@ export function ConnectionRuleDialog({ fullWidth size="small" label={ - editingDomain + editingDomain || matchingRoot ? t('connections.ruleDialog.matchingDomain') : kindLabels[kind] } - value={editingDomain ? domainValue : (selected?.value ?? '')} + value={matchingValue} disabled={editingDomain && blocked} onChange={(event) => { const value = event.target.value @@ -300,7 +319,7 @@ export function ConnectionRuleDialog({ }} error={invalidDomain} helperText={ - editingDomain ? ( + editingDomain || matchingRoot ? ( <> {t('connections.ruleDialog.connectionHost', { host: targets.domain?.value, @@ -317,7 +336,7 @@ export function ConnectionRuleDialog({ slotProps={{ input: { readOnly: !editingDomain }, htmlInput: { - title: editingDomain ? domainValue : selected?.value, + title: condition?.value, }, }} /> @@ -329,14 +348,17 @@ export function ConnectionRuleDialog({ aria-label={t('connections.ruleDialog.domainScope')} value={scope} onChange={(_, value) => { - const nextScope = value as 'DOMAIN' | 'DOMAIN-SUFFIX' + const nextScope = value as DomainScope setScope(nextScope) setDomainValue(targets.domain?.value ?? '') const match = rules && targets.domain ? findConnectionRule(rules, { - ...targets.domain, - type: nextScope, + type: nextScope === 'DOMAIN' ? 'DOMAIN' : 'DOMAIN-SUFFIX', + value: + nextScope === 'ROOT-DOMAIN' + ? (rootDomain ?? '') + : targets.domain.value, }) : undefined setPolicy(match?.policy ?? null) @@ -355,6 +377,18 @@ export function ConnectionRuleDialog({ disabled={blocked} label={t('connections.ruleDialog.allSubdomains')} /> + {rootDomain && + rootDomain.toLowerCase() !== + targets.domain?.value.toLowerCase() && ( + } + disabled={blocked} + label={t('connections.ruleDialog.rootDomain', { + domain: rootDomain, + })} + /> + )} )} {(kind === 'source' || kind === 'destination') && ( diff --git a/src/components/connection/connection-rule.ts b/src/components/connection/connection-rule.ts index d3acce1a8..e7918f07c 100644 --- a/src/components/connection/connection-rule.ts +++ b/src/components/connection/connection-rule.ts @@ -56,6 +56,14 @@ export function isConnectionRuleDomain(value: string, host: string) { ) } +export async function getConnectionRootDomain(host: string) { + const { parse } = await import('tldts') + const { domain, isIcann, isPrivate } = parse(host, { + allowPrivateDomains: true, + }) + return domain && (isIcann || isPrivate) ? domain : undefined +} + function parseRuleExtension(content: string) { const parsed: unknown = load(content) if ( diff --git a/src/locales/ar/connections.json b/src/locales/ar/connections.json index cbd742ac3..a56cc0c19 100644 --- a/src/locales/ar/connections.json +++ b/src/locales/ar/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "النطاق الرئيسي وجميع نطاقاته الفرعية ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/de/connections.json b/src/locales/de/connections.json index e8b796468..737f3dead 100644 --- a/src/locales/de/connections.json +++ b/src/locales/de/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "Hauptdomain und alle Subdomains ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/en/connections.json b/src/locales/en/connections.json index 2ada48335..2fd34172c 100644 --- a/src/locales/en/connections.json +++ b/src/locales/en/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "Root domain and all its subdomains ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/es/connections.json b/src/locales/es/connections.json index 397fbedfb..32c3fbb41 100644 --- a/src/locales/es/connections.json +++ b/src/locales/es/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "Dominio principal y todos sus subdominios ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/fa/connections.json b/src/locales/fa/connections.json index 78e6f1a88..2eb4305bf 100644 --- a/src/locales/fa/connections.json +++ b/src/locales/fa/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "دامنهٔ اصلی و همهٔ زیردامنه‌های آن ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/id/connections.json b/src/locales/id/connections.json index 4c8a4716e..d2fe9aeba 100644 --- a/src/locales/id/connections.json +++ b/src/locales/id/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "Domain utama dan semua subdomainnya ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/jp/connections.json b/src/locales/jp/connections.json index 6ab8156e2..328ffb82d 100644 --- a/src/locales/jp/connections.json +++ b/src/locales/jp/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "メインドメインとすべてのサブドメイン({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/ko/connections.json b/src/locales/ko/connections.json index 81f631822..222c6ef28 100644 --- a/src/locales/ko/connections.json +++ b/src/locales/ko/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "기본 도메인 및 모든 하위 도메인 ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/pt/connections.json b/src/locales/pt/connections.json index 766dba027..7caeb03bb 100644 --- a/src/locales/pt/connections.json +++ b/src/locales/pt/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "Domínio principal e todos os seus subdomínios ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/ru/connections.json b/src/locales/ru/connections.json index b10c42be8..3f3527a95 100644 --- a/src/locales/ru/connections.json +++ b/src/locales/ru/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "Основной домен и все его поддомены ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/tr/connections.json b/src/locales/tr/connections.json index 71ffd9853..e61c85bfb 100644 --- a/src/locales/tr/connections.json +++ b/src/locales/tr/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "Ana alan adı ve tüm alt alan adları ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/tt/connections.json b/src/locales/tt/connections.json index 56d225521..bdf8f09ba 100644 --- a/src/locales/tt/connections.json +++ b/src/locales/tt/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "Төп домен һәм аның барлык субдоменнары ({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/locales/zh/connections.json b/src/locales/zh/connections.json index 9535f2676..491dbb8c8 100644 --- a/src/locales/zh/connections.json +++ b/src/locales/zh/connections.json @@ -53,6 +53,7 @@ "domainScope": "域名匹配范围", "exactDomain": "仅此域名", "allSubdomains": "此域名及所有子域名", + "rootDomain": "主域名及所有子域名({{domain}})", "policyUnavailable": "此策略已不可用,请选择其他策略。", "editDestination": "修改「{{name}}」中的现有规则,保留排序。", "saveAndApply": "保存并应用", diff --git a/src/locales/zhtw/connections.json b/src/locales/zhtw/connections.json index 73bfc1ed0..91939b9c3 100644 --- a/src/locales/zhtw/connections.json +++ b/src/locales/zhtw/connections.json @@ -53,6 +53,7 @@ "domainScope": "Domain scope", "exactDomain": "Only this domain", "allSubdomains": "This domain and all its subdomains", + "rootDomain": "主網域及所有子網域({{domain}})", "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index 63677c93d..c3825fc40 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -43,6 +43,7 @@ export const translationKeys = [ 'connections.ruleDialog.domainScope', 'connections.ruleDialog.exactDomain', 'connections.ruleDialog.allSubdomains', + 'connections.ruleDialog.rootDomain', 'connections.ruleDialog.policyUnavailable', 'connections.ruleDialog.editDestination', 'connections.ruleDialog.saveAndApply', diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index 9e53c7d10..2506e6183 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -61,6 +61,7 @@ export interface TranslationResources { processNameHint: string processPathHint: string profileChanged: string + rootDomain: string ruleModeHint: string saveAndApply: string saved: string