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