feat(connections): select the root domain for suffix rules

This commit is contained in:
Tunglies
2026-11-15 11:57:55 +08:00
parent a61887954e
commit 99013ff982
21 changed files with 89 additions and 14 deletions
+1 -1
View File
@@ -51,7 +51,7 @@
<summary><strong> ✨ 新增功能 </strong></summary>
- 新增连接页面按网络协议(TCP/UDP)筛选连接
- 新增从连接详情快捷设置域名、进程或源/目标 IP 规则,支持父域及子域匹配与修改已有规则
- 新增从连接详情设置域名、进程或源/目标 IP 规则,支持自动识别主域、匹配子域及修改已有规则
</details>
+1
View File
@@ -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"
},
+16
View File
@@ -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: {}
@@ -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<string>()
const [kind, setKind] = useState<ConnectionRuleKind>(() =>
getInitialKind(target),
)
const [policy, setPolicy] = useState<string | null>(null)
const [scope, setScope] = useState<'DOMAIN' | 'DOMAIN-SUFFIX'>('DOMAIN')
const [scope, setScope] = useState<DomainScope>('DOMAIN')
const [domainValue, setDomainValue] = useState(targets.domain?.value ?? '')
const [rules, setRules] = useState<string[] | null>(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() && (
<FormControlLabel
value="ROOT-DOMAIN"
control={<Radio size="small" />}
disabled={blocked}
label={t('connections.ruleDialog.rootDomain', {
domain: rootDomain,
})}
/>
)}
</RadioGroup>
)}
{(kind === 'source' || kind === 'destination') && (
@@ -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 (
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -53,6 +53,7 @@
"domainScope": "域名匹配范围",
"exactDomain": "仅此域名",
"allSubdomains": "此域名及所有子域名",
"rootDomain": "主域名及所有子域名({{domain}})",
"policyUnavailable": "此策略已不可用,请选择其他策略。",
"editDestination": "修改「{{name}}」中的现有规则,保留排序。",
"saveAndApply": "保存并应用",
+1
View File
@@ -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",
+1
View File
@@ -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',
+1
View File
@@ -61,6 +61,7 @@ export interface TranslationResources {
processNameHint: string
processPathHint: string
profileChanged: string
rootDomain: string
ruleModeHint: string
saveAndApply: string
saved: string