mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-15 15:32:21 +08:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99013ff982 | ||
|
|
a61887954e | ||
|
|
9004a8cdf8 | ||
|
|
e75b6c9a46 | ||
|
|
a12c414279 | ||
|
|
b2689b4220 | ||
|
|
5321eb44b8 | ||
|
|
f0dd12dfb8 | ||
|
|
423b51e780 |
@@ -5,7 +5,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "26.13.1",
|
||||
"pnpmVersion": "12.21.0"
|
||||
"pnpmVersion": "12.18.0"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.100.0",
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
name: Sync Mihomo Changelog
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Preview the Changelog diff without creating a PR
|
||||
type: boolean
|
||||
default: true
|
||||
schedule:
|
||||
# UTC+8 Monday 11:30
|
||||
- cron: '30 3 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: sync-mihomo-changelog
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '26.13.1'
|
||||
|
||||
- name: Sync Mihomo version
|
||||
id: sync
|
||||
env:
|
||||
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
||||
run: |
|
||||
args=()
|
||||
if [[ "$DRY_RUN" == 'true' ]]; then args+=(--dry-run); fi
|
||||
node scripts/sync-mihomo-version.mjs "${args[@]}"
|
||||
|
||||
- name: Create labeled PR and enqueue
|
||||
if: steps.sync.outputs.changed == 'true' && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
MIHOMO_VERSION: ${{ steps.sync.outputs.version }}
|
||||
with:
|
||||
script: |
|
||||
const { readFileSync } = await import('node:fs');
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
const { owner, repo } = context.repo;
|
||||
const base = context.payload.repository.default_branch;
|
||||
const version = process.env.MIHOMO_VERSION;
|
||||
const appVersion = JSON.parse(readFileSync('package.json', 'utf8')).version;
|
||||
const branch = `chore/sync-mihomo-${appVersion}-${version}`;
|
||||
const title = `chore: sync Mihomo changelog to ${version}`;
|
||||
const pulls = await github.rest.pulls.list({
|
||||
owner, repo, base, head: `${owner}:${branch}`, state: 'all',
|
||||
});
|
||||
let pr = pulls.data[0];
|
||||
if (pr && (pr.state !== 'open' || pr.user.login !== 'github-actions[bot]')) {
|
||||
throw new Error(`Existing candidate needs maintainer attention: ${pr.html_url}`);
|
||||
}
|
||||
if (!pr) {
|
||||
const sha = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
||||
await github.rest.git.createRef({ owner, repo, ref: `refs/heads/${branch}`, sha });
|
||||
await github.graphql(`mutation($input: CreateCommitOnBranchInput!) {
|
||||
createCommitOnBranch(input: $input) { commit { oid } }
|
||||
}`, {
|
||||
input: {
|
||||
branch: { repositoryNameWithOwner: `${owner}/${repo}`, branchName: branch },
|
||||
expectedHeadOid: sha,
|
||||
message: { headline: title },
|
||||
fileChanges: { additions: [{
|
||||
path: 'Changelog.md',
|
||||
contents: readFileSync('Changelog.md').toString('base64'),
|
||||
}] },
|
||||
},
|
||||
});
|
||||
({ data: pr } = await github.rest.pulls.create({
|
||||
owner, repo, base, head: branch, title,
|
||||
body: `## Problem\n\nChangelog.md needs to record the latest stable Mihomo version.\n\n` +
|
||||
`## Approach\n\nRead the same upstream version.txt used by the core download script.\n\n` +
|
||||
`## Scope\n\nOnly the Mihomo version line in Changelog.md changes to ${version}.`,
|
||||
}));
|
||||
}
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: pr.number }));
|
||||
if (pr.mergeable !== null) break;
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
if (!pr.mergeable) throw new Error(`Sync PR is not mergeable: ${pr.html_url}`);
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner, repo, pull_number: pr.number,
|
||||
});
|
||||
if (files.length !== 1 || files[0].filename !== 'Changelog.md') {
|
||||
throw new Error('The sync PR must change only Changelog.md.');
|
||||
}
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: pr.number, labels: ['dependencies', 'upstream'],
|
||||
});
|
||||
const result = await github.graphql(`query($id: ID!) {
|
||||
node(id: $id) { ... on PullRequest { mergeQueueEntry { id } } }
|
||||
}`, { id: pr.node_id });
|
||||
if (!result.node.mergeQueueEntry) {
|
||||
await github.graphql(`mutation($input: EnqueuePullRequestInput!) {
|
||||
enqueuePullRequest(input: $input) { mergeQueueEntry { id } }
|
||||
}`, { input: { pullRequestId: pr.node_id, expectedHeadOid: pr.head.sha } });
|
||||
}
|
||||
await core.summary.addRaw(`Queued Mihomo sync PR: ${pr.html_url}\n`).write();
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
nodejs 26.13.1
|
||||
pnpm 12.21.0
|
||||
pnpm 12.18.0
|
||||
rust 1.100.0
|
||||
|
||||
Generated
+20
-20
@@ -1328,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2043,7 +2043,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2302,7 +2302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2818,7 +2818,7 @@ dependencies = [
|
||||
"gobject-sys 0.21.5",
|
||||
"libc",
|
||||
"system-deps 7.0.8",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3372,7 +3372,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.61.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4252,7 +4252,7 @@ dependencies = [
|
||||
"png",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4419,7 +4419,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4896,7 +4896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5576,7 +5576,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6129,7 +6129,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6188,7 +6188,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6808,7 +6808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7867,7 +7867,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8533,7 +8533,7 @@ dependencies = [
|
||||
"png",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8642,7 +8642,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8820,9 +8820,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warp"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0a808122a8a77eecdabaefd88ddb1913c4be5ea1465399f63ba64c7aa705fea"
|
||||
checksum = "c92be475cc42b6b3913eeac77f0cbc0dd8f6a1ad6400c0722dbe36ba0c3ee799"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -9184,7 +9184,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9205,7 +9205,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
@@ -9780,7 +9780,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
GitHub, so they never reach users. -->
|
||||
## v2.6.0
|
||||
|
||||
- **Mihomo 内核升级至 v1.19.34**
|
||||
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
@@ -35,6 +37,7 @@
|
||||
- 修复断开链式代理时清空已选节点,导致再次连接需要重新选择
|
||||
- 修复「连接」页表格排序时表头换行、标题位置跳动
|
||||
- 修复「TUN 设置」未手动保存时,界面显示的默认 MTU 1500 未生效
|
||||
- 修复连接详情较窄时操作按钮偏离右侧
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
@@ -48,6 +51,7 @@
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 新增连接页面按网络协议(TCP/UDP)筛选连接
|
||||
- 新增从连接详情设置域名、进程或源/目标 IP 规则,支持自动识别主域、匹配子域及修改已有规则
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"duplicates",
|
||||
"cycles"
|
||||
],
|
||||
"ignoreBinaries": ["rustc"],
|
||||
"ignoreBinaries": ["diff", "rustc"],
|
||||
"ignoreDependencies": ["lint-staged"],
|
||||
"ignoreIssues": {
|
||||
"src/types/generated/**": ["exports", "types"],
|
||||
|
||||
+2
-1
@@ -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"
|
||||
},
|
||||
@@ -138,5 +139,5 @@
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@12.21.0"
|
||||
"packageManager": "pnpm@12.18.0"
|
||||
}
|
||||
|
||||
Generated
+77
-70
@@ -7,162 +7,153 @@ importers:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
pnpm:
|
||||
specifier: 12.21.0
|
||||
version: 12.21.0
|
||||
specifier: 12.18.0
|
||||
version: 12.18.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe.android-arm64@12.21.0':
|
||||
resolution: {integrity: sha512-iaWidxR5B9NUBpwMznCQu1yUpKB6BjTY5BYX2lORj76VsfINdj8Z8XWSzSNrGI8qlxa28Jzdp5fiaspzOaJZlA==}
|
||||
'@pnpm/exe.android-arm64@12.18.0':
|
||||
resolution: {integrity: sha512-fOF6IwgvnUKEmVedGqiAsNPfN2Ry8GvaqzSQOiF3RJFbpBfPKO/6YT54s1GjToyYinRxw9preHHPe4y2N3SzhA==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/exe.android-x64@12.21.0':
|
||||
resolution: {integrity: sha512-35e5v3wlwjIYxDywq+R6/z8CE4gLOEgRxt18yqz1tMRLmx2mnW+CFJI2Q8+srTNjn7gykXUC0x1k195JNikC5A==}
|
||||
'@pnpm/exe.android-x64@12.18.0':
|
||||
resolution: {integrity: sha512-6OiolnaCsB3/GcJ9XNrenbUH3zFHRJ+5QynrMUUEyThbgnnCzWxa199hpQlOmxN4rAXuYQ0IYgWFNUZxyzg31Q==}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.21.0':
|
||||
resolution: {integrity: sha512-F5OyMX/6zLSowpTZEQRojbCmPS1Pq+fyRaIinslsIe6Glh3qbyQ52u+Fe0eEzSaRuIbWZkXmAAXSWh6Q5Wrlgg==}
|
||||
'@pnpm/exe.darwin-arm64@12.18.0':
|
||||
resolution: {integrity: sha512-zkq+4YzVIX39ETYLyC3W7LY7MpHEU/J3nRhHl4i2yPld2Cf2+rEKRh3ejJ4AmaQ77HHJLCktY4Z3jq0fpcxFXg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.21.0':
|
||||
resolution: {integrity: sha512-EzpvcsaE6hrrgYeEiAFYd2rSgFybYyxXhkAY7K3y6Hg3loVfcyJ81hHczpFzi8lW5RXtUlFECRPZ8ZAMRFQYYg==}
|
||||
'@pnpm/exe.darwin-x64@12.18.0':
|
||||
resolution: {integrity: sha512-qMsL9qx92unOTa6hgkfiTJv8ceZaTx0IQg7InhYDVX2ZrTECujaVEEVypnJK5m3eGUsP1KD7Y4qIgleCwfRsYw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.freebsd-x64@12.21.0':
|
||||
resolution: {integrity: sha512-wNjuEnmdLks4aayPu/QZWQ5tQM9Jn+odUp/vwQM8BXRYoulkTZhtZTHoD60N6mUdLWef1hgUeUh8llNGEUsimw==}
|
||||
'@pnpm/exe.freebsd-x64@12.18.0':
|
||||
resolution: {integrity: sha512-/9vy/4ms52cU4MjZ5NhHd6N2d9L9W0kARf5/ff2tweAHu0VeUzs/0qp/7ZHFuddm2Wa/m0AKQoPum9AVUCqKsw==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.21.0':
|
||||
resolution: {integrity: sha512-Ceit9zn4g5ZqCJtx1fV8eJE7p8aeXMrGZ+Dc01vRqZLIUNgNeI+Ur0ruyqxtsnJx6rGADyXzohumxPLHjaJ5AQ==}
|
||||
'@pnpm/exe.linux-arm64-musl@12.18.0':
|
||||
resolution: {integrity: sha512-Vp+TIMPPyackVCXkOdEKH/RmAn/r07EcIyrgZYQK7VrkksqZOie0GnWIx+85mPqeFEYCRtXIZAFJ9L4ceTndDg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.21.0':
|
||||
resolution: {integrity: sha512-4Oj/iF2TDCe2lcH0n4TupPR5sSdrNLclJB6xBElLMPCUOAvBhdf2rRlSaO5M4erJ6g4Fyy7YTXTpcmfFsKyxEA==}
|
||||
'@pnpm/exe.linux-arm64@12.18.0':
|
||||
resolution: {integrity: sha512-2dpyVBCHT+FfpuQh9i+7MXAqNWYLCvD5vYr5p6Eimxoga8USLnjbUgyj72AOrO0hjc5ri3TEFP3jiyBHjkSZpQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-ppc64@12.21.0':
|
||||
resolution: {integrity: sha512-s2o7rdSDeUTBFsNqxptpZXlTgT6cojmMVUOvTQvFNSv6Easian02ha4tR32fwwD97Au4YzIIjMCwywCF4D82Sw==}
|
||||
'@pnpm/exe.linux-ppc64@12.18.0':
|
||||
resolution: {integrity: sha512-BBOOOfNJ2QwIIya3eKqOjiN4WNTszfRz7HCvw9XggcT7+ftMjNP8XJ/GfW+ltCBeUFR9d8WBVsr2m/NLjX2CZQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-riscv64@12.21.0':
|
||||
resolution: {integrity: sha512-EYv3GzxeBTITCuM3yGHHbpRFpUvyulkXpQ6LsjKAQNL3iaba1E6h9iDmlouLb3Io4AMMsfdk0/uG6poQ41uewA==}
|
||||
'@pnpm/exe.linux-riscv64@12.18.0':
|
||||
resolution: {integrity: sha512-7kMN77xSOYJX8GPSH5RbNAc5oM61LMmcSUukDDUwllnZPXm8GRPskCNsTWy8SCw01Kr1FB+5SwGNMiTdiziLNQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-s390x@12.21.0':
|
||||
resolution: {integrity: sha512-8VXGNGVNEfs7pL9GTZOzFxjFEnMt4WL3rLOADR5/kPQ7ROTV9GfpVI2DPO/zArXFSkvfc9Zad/WypHLbvzoaTQ==}
|
||||
'@pnpm/exe.linux-s390x@12.18.0':
|
||||
resolution: {integrity: sha512-htuSCpEcmDzhKoJqXbPkvfr176dBHU0QIF5mWcRiqxRXAB/1JoYiMYotfjWpk+ztgpB9qlx4XnWlLFAf7gzc6g==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.21.0':
|
||||
resolution: {integrity: sha512-MnC5vpIXYubhDZCgCwlfJMg7Cg6HupN2IuouT9gD/tUmOfVdQYqeyYccWbthyo/Jeto8de1oQlxeRN1+1Hl2DQ==}
|
||||
'@pnpm/exe.linux-x64-musl@12.18.0':
|
||||
resolution: {integrity: sha512-Uj+vzfIQ62kDP10x36yHZIXVs5ohdNtiPYwn5FhCLlNScHPo0OAI+ShGDtLofiBXKE7umu71Jmjm7N8a8zVcDw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-x64@12.21.0':
|
||||
resolution: {integrity: sha512-8eufCjHXspKbNS/kh8q4Q0gqJoOuvC4Q5pgos6XS8By/nAMtP+jf8F1Cw9er840kfAX+PJ/ts+A9qnLE91rvXQ==}
|
||||
'@pnpm/exe.linux-x64@12.18.0':
|
||||
resolution: {integrity: sha512-OJ+X2wcraldCwj75pCIerWARQSRpdfj0eBTs/2ODyhfeW46ylOnjTIE/GKra6Mn6E72/gBmXS8igrUioa+F9fQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.netbsd-x64@12.21.0':
|
||||
resolution: {integrity: sha512-eyh1M8hQZCkLWxuva7S/fc1SbFbgqg0/5di0dgWmyiuc2Io3dbWVihDun8X+XM8zsR5uhIDvvZpbS9aZQbCovg==}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.21.0':
|
||||
resolution: {integrity: sha512-7xTxxUXo/grRLivSeIl6B21gnsz7ooe5EMPcJmQqwxc8WLJVReN8MzrlHru8bqXfVr4X3+6ZGZscntKenlCBqQ==}
|
||||
'@pnpm/exe.win32-arm64@12.18.0':
|
||||
resolution: {integrity: sha512-wEY6jiACGcbhhi5yD59doHqNTndS7AaujaPmtWAu1DKkHru2hy5fJ9yUGecW72Ypaw2/I3BEprfMOopap2NQtw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/exe.win32-x64@12.21.0':
|
||||
resolution: {integrity: sha512-9TpkJemVApxJ1Z1w0ZFGN+AuSx4nJOqXSJUqMqDvaXTYwGOzvBiiWdFP9GN0ketiWCQS1Q0/pza2E7I3pP0OBQ==}
|
||||
'@pnpm/exe.win32-x64@12.18.0':
|
||||
resolution: {integrity: sha512-5FJkghO7RLRkQpWxeVEygW9XsmnNuxzPfXbgwFWCHsFyFhpDn/vOdL88ToQEM0Kt5igdW5s6U0/IDH8Dvkt3pg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
pnpm@12.21.0:
|
||||
resolution: {integrity: sha512-Zv2CeO4ok1zpUXktNDZ/869cQyonVkRYIwKVM4NbjaUpu+C+UTCv1W7QcT+4eJ5Hutgtk71a4v5UsVVi1idu8A==}
|
||||
pnpm@12.18.0:
|
||||
resolution: {integrity: sha512-nXiC1dyCFH7PxOBiqjSDTgrQKWcQoYN6ERWuASEn02txCeMhrBWYod0KZVl2gbHpmRfhLtDXSerpEYeUM2O+sw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe.android-arm64@12.21.0':
|
||||
'@pnpm/exe.android-arm64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.android-x64@12.21.0':
|
||||
'@pnpm/exe.android-x64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.21.0':
|
||||
'@pnpm/exe.darwin-arm64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.21.0':
|
||||
'@pnpm/exe.darwin-x64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.freebsd-x64@12.21.0':
|
||||
'@pnpm/exe.freebsd-x64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.21.0':
|
||||
'@pnpm/exe.linux-arm64-musl@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.21.0':
|
||||
'@pnpm/exe.linux-arm64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-ppc64@12.21.0':
|
||||
'@pnpm/exe.linux-ppc64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-riscv64@12.21.0':
|
||||
'@pnpm/exe.linux-riscv64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-s390x@12.21.0':
|
||||
'@pnpm/exe.linux-s390x@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.21.0':
|
||||
'@pnpm/exe.linux-x64-musl@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64@12.21.0':
|
||||
'@pnpm/exe.linux-x64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.netbsd-x64@12.21.0':
|
||||
'@pnpm/exe.win32-arm64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.21.0':
|
||||
'@pnpm/exe.win32-x64@12.18.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-x64@12.21.0':
|
||||
optional: true
|
||||
|
||||
pnpm@12.21.0:
|
||||
pnpm@12.18.0:
|
||||
optionalDependencies:
|
||||
'@pnpm/exe.android-arm64': 12.21.0
|
||||
'@pnpm/exe.android-x64': 12.21.0
|
||||
'@pnpm/exe.darwin-arm64': 12.21.0
|
||||
'@pnpm/exe.darwin-x64': 12.21.0
|
||||
'@pnpm/exe.freebsd-x64': 12.21.0
|
||||
'@pnpm/exe.linux-arm64': 12.21.0
|
||||
'@pnpm/exe.linux-arm64-musl': 12.21.0
|
||||
'@pnpm/exe.linux-ppc64': 12.21.0
|
||||
'@pnpm/exe.linux-riscv64': 12.21.0
|
||||
'@pnpm/exe.linux-s390x': 12.21.0
|
||||
'@pnpm/exe.linux-x64': 12.21.0
|
||||
'@pnpm/exe.linux-x64-musl': 12.21.0
|
||||
'@pnpm/exe.netbsd-x64': 12.21.0
|
||||
'@pnpm/exe.win32-arm64': 12.21.0
|
||||
'@pnpm/exe.win32-x64': 12.21.0
|
||||
'@pnpm/exe.android-arm64': 12.18.0
|
||||
'@pnpm/exe.android-x64': 12.18.0
|
||||
'@pnpm/exe.darwin-arm64': 12.18.0
|
||||
'@pnpm/exe.darwin-x64': 12.18.0
|
||||
'@pnpm/exe.freebsd-x64': 12.18.0
|
||||
'@pnpm/exe.linux-arm64': 12.18.0
|
||||
'@pnpm/exe.linux-arm64-musl': 12.18.0
|
||||
'@pnpm/exe.linux-ppc64': 12.18.0
|
||||
'@pnpm/exe.linux-riscv64': 12.18.0
|
||||
'@pnpm/exe.linux-s390x': 12.18.0
|
||||
'@pnpm/exe.linux-x64': 12.18.0
|
||||
'@pnpm/exe.linux-x64-musl': 12.18.0
|
||||
'@pnpm/exe.win32-arm64': 12.18.0
|
||||
'@pnpm/exe.win32-x64': 12.18.0
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
@@ -304,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
|
||||
@@ -4044,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==}
|
||||
|
||||
@@ -8147,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: {}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { parseArgs } from 'node:util'
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
'dry-run': { type: 'boolean', default: false },
|
||||
check: { type: 'boolean', default: false },
|
||||
version: { type: 'string' },
|
||||
file: { type: 'string', default: 'Changelog.md' },
|
||||
},
|
||||
})
|
||||
|
||||
async function main() {
|
||||
let version = values.version
|
||||
if (!version) {
|
||||
const response = await fetch(
|
||||
'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt',
|
||||
{ signal: AbortSignal.timeout(30_000) },
|
||||
)
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to fetch Mihomo version: ${response.status}`)
|
||||
version = await response.text()
|
||||
}
|
||||
version = version.trim().replace(/^v?/, 'v')
|
||||
if (!/^v\d+\.\d+\.\d+$/.test(version))
|
||||
throw new Error(`Invalid stable Mihomo version: ${version}`)
|
||||
|
||||
const content = readFileSync(values.file, 'utf8')
|
||||
const title = /^## .+$/m.exec(content)
|
||||
if (!title) throw new Error('Could not find changelog title.')
|
||||
const start = title.index + title[0].length
|
||||
const end = content.indexOf('\n## ', start)
|
||||
const section = content.slice(start, end === -1 ? undefined : end)
|
||||
const line =
|
||||
/^(\s*-\s*(?:\*\*)?\s*Mihomo(?:\(Meta\))?\s*内核升级至\s*)v?\d+(?:\.\d+)+(?:[-+][^\s*]+)?((?:\*\*)?[\t ]*)$/m
|
||||
const nextSection = line.test(section)
|
||||
? section.replace(line, `$1${version}$2`)
|
||||
: `\n\n- **Mihomo 内核升级至 ${version}**${section.replace(/^\n*/, '\n\n')}`
|
||||
const nextContent =
|
||||
content.slice(0, start) +
|
||||
nextSection +
|
||||
(end === -1 ? '' : content.slice(end))
|
||||
const changed = content !== nextContent
|
||||
|
||||
if (process.env.GITHUB_OUTPUT)
|
||||
appendFileSync(
|
||||
process.env.GITHUB_OUTPUT,
|
||||
`version=${version}\nchanged=${changed}\n`,
|
||||
)
|
||||
if (!changed) {
|
||||
console.log(`Mihomo version in changelog is already ${version}`)
|
||||
return
|
||||
}
|
||||
|
||||
const diff = spawnSync(
|
||||
'diff',
|
||||
[
|
||||
'-u',
|
||||
'--label',
|
||||
`a/${values.file}`,
|
||||
'--label',
|
||||
`b/${values.file}`,
|
||||
values.file,
|
||||
'-',
|
||||
],
|
||||
{ input: nextContent, encoding: 'utf8' },
|
||||
)
|
||||
if (diff.error) throw diff.error
|
||||
if (diff.status !== 1)
|
||||
throw new Error(diff.stderr || 'Could not preview changelog diff.')
|
||||
process.stdout.write(diff.stdout)
|
||||
if (process.env.GITHUB_STEP_SUMMARY)
|
||||
appendFileSync(
|
||||
process.env.GITHUB_STEP_SUMMARY,
|
||||
`### Mihomo ${version}\n\n\`\`\`diff\n${diff.stdout}\`\`\`\n`,
|
||||
)
|
||||
if (values.check) {
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (!values['dry-run']) writeFileSync(values.file, nextContent)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('sync-mihomo-version failed:', error.message)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -56,7 +56,7 @@ serde_json = { workspace = true }
|
||||
serde_yaml_ng = { workspace = true }
|
||||
smartstring = { workspace = true, features = ["serde"] }
|
||||
socket2 = { workspace = true }
|
||||
warp = { version = "0.4.3", features = ["server"] }
|
||||
warp = { version = "0.4.4", features = ["server"] }
|
||||
open = "5.4.5"
|
||||
rfd = { version = "0.18.0", default-features = false, features = ["common-controls-v6", "xdg-portal"] }
|
||||
dirs = "7.0.0"
|
||||
@@ -153,7 +153,7 @@ tauri-plugin-updater = "2.14.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["test-util"] }
|
||||
warp = { version = "0.4.3", features = ["test"] }
|
||||
warp = { version = "0.4.4", features = ["test"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -5,10 +5,24 @@ import { useCallback, useImperativeHandle, useState, type Ref } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { closeConnection } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
import parseTraffic from '@/utils/parse-traffic'
|
||||
|
||||
import {
|
||||
getConnectionRuleTargets,
|
||||
type ConnectionRuleKind,
|
||||
} from './connection-rule'
|
||||
import {
|
||||
ConnectionRuleDialog,
|
||||
type ConnectionRuleTarget,
|
||||
} from './connection-rule-dialog'
|
||||
|
||||
export interface ConnectionDetailRef {
|
||||
open: (detail: IConnectionsItem, closed: boolean) => void
|
||||
open: (
|
||||
detail: IConnectionsItem,
|
||||
closed: boolean,
|
||||
preferredKind?: ConnectionRuleKind,
|
||||
) => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
@@ -16,7 +30,12 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [detail, setDetail] = useState<IConnectionsItem | null>(null)
|
||||
const [closed, setClosed] = useState(false)
|
||||
const [preferredKind, setPreferredKind] =
|
||||
useState<ConnectionRuleKind>('domain')
|
||||
const theme = useTheme()
|
||||
const [ruleTarget, setRuleTarget] = useState<ConnectionRuleTarget | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setOpen(false)
|
||||
@@ -25,51 +44,90 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
|
||||
}, [])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: (detail: IConnectionsItem, closed: boolean) => {
|
||||
open: (
|
||||
detail: IConnectionsItem,
|
||||
closed: boolean,
|
||||
preferredKind = 'domain',
|
||||
) => {
|
||||
if (open) return
|
||||
setOpen(true)
|
||||
setDetail(detail)
|
||||
setClosed(closed)
|
||||
setPreferredKind(preferredKind)
|
||||
},
|
||||
close: onClose,
|
||||
}))
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{
|
||||
'.MuiSnackbarContent-root': {
|
||||
maxWidth: '520px',
|
||||
maxHeight: '480px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
message={
|
||||
detail ? (
|
||||
<InnerConnectionDetail
|
||||
data={detail}
|
||||
closed={closed}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{
|
||||
'.MuiSnackbarContent-root': {
|
||||
width: '520px',
|
||||
maxWidth: 'calc(100vw - 32px)',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '480px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'.MuiSnackbarContent-message': { width: '100%', minWidth: 0 },
|
||||
}}
|
||||
message={
|
||||
detail ? (
|
||||
<InnerConnectionDetail
|
||||
data={detail}
|
||||
closed={closed}
|
||||
preferredKind={preferredKind}
|
||||
onClose={onClose}
|
||||
onAddRule={(target) => {
|
||||
setRuleTarget(target)
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
{ruleTarget && (
|
||||
<ConnectionRuleDialog
|
||||
target={ruleTarget}
|
||||
onClose={() => setRuleTarget(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface InnerProps {
|
||||
data: IConnectionsItem
|
||||
closed: boolean
|
||||
preferredKind: ConnectionRuleKind
|
||||
onClose?: () => void
|
||||
onAddRule: (target: ConnectionRuleTarget) => void
|
||||
}
|
||||
|
||||
const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
||||
const InnerConnectionDetail = ({
|
||||
data,
|
||||
closed,
|
||||
preferredKind,
|
||||
onClose,
|
||||
onAddRule,
|
||||
}: InnerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { current } = useProfiles()
|
||||
const { metadata, rulePayload } = data
|
||||
const targets = getConnectionRuleTargets(metadata)
|
||||
const canAddRule = Boolean(
|
||||
current?.uid &&
|
||||
current.option?.rules &&
|
||||
(targets.domain ||
|
||||
targets.process ||
|
||||
targets.source ||
|
||||
targets.destination),
|
||||
)
|
||||
const theme = useTheme()
|
||||
const chains = [...data.chains].reverse().join(' / ')
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule
|
||||
@@ -105,7 +163,7 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
||||
{ label: t('connections.components.fields.rule'), value: rule },
|
||||
{
|
||||
label: t('connections.components.fields.process'),
|
||||
value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ''}`,
|
||||
value: `${metadata.process ?? ''}${metadata.processPath ? `(${metadata.processPath})` : ''}`,
|
||||
},
|
||||
{
|
||||
label: t('connections.components.fields.time'),
|
||||
@@ -132,7 +190,14 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
||||
const onDelete = useLockFn(async () => closeConnection(data.id))
|
||||
|
||||
return (
|
||||
<Box sx={{ userSelect: 'text', color: theme.palette.text.secondary }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
userSelect: 'text',
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
{information.map((each) => (
|
||||
<div key={each.label}>
|
||||
<b>{each.label}</b>
|
||||
@@ -147,8 +212,32 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!closed && (
|
||||
<Box sx={{ textAlign: 'right' }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap',
|
||||
mt: 1,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={!canAddRule}
|
||||
onClick={() => {
|
||||
if (!current?.uid || !current.option?.rules) return
|
||||
onAddRule({
|
||||
metadata: { ...metadata },
|
||||
profileUid: current.uid,
|
||||
profileName: current.name || current.uid,
|
||||
rulesUid: current.option.rules,
|
||||
preferredKind,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{t('connections.ruleDialog.entry')}
|
||||
</Button>
|
||||
{!closed && (
|
||||
<Button
|
||||
variant="contained"
|
||||
title={t('connections.components.actions.closeConnection')}
|
||||
@@ -159,6 +248,18 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
||||
>
|
||||
{t('connections.components.actions.closeConnection')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
{!canAddRule && (
|
||||
<Box sx={{ mt: 1, typography: 'caption' }}>
|
||||
{t(
|
||||
targets.domain ||
|
||||
targets.process ||
|
||||
targets.source ||
|
||||
targets.destination
|
||||
? 'connections.ruleDialog.noProfile'
|
||||
: 'connections.ruleDialog.noMetadata',
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -7,11 +7,12 @@ import { closeConnection } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import { RelativeTime } from './connection-relative-time'
|
||||
import type { ConnectionRowView } from './connection-row-view'
|
||||
import type { ConnectionRuleKind } from './connection-rule'
|
||||
|
||||
interface Props {
|
||||
row: ConnectionRowView
|
||||
closed: boolean
|
||||
onShowDetail: (id: string) => void
|
||||
onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void
|
||||
}
|
||||
|
||||
const tagStyle = {
|
||||
@@ -86,7 +87,17 @@ export const ConnectionRowItem = memo(
|
||||
<div style={tagsStyle}>
|
||||
<span style={tagStyle}>{row.network}</span>
|
||||
<span style={tagStyle}>{row.type}</span>
|
||||
{row.process && <span style={tagStyle}>{row.process}</span>}
|
||||
{row.process && (
|
||||
<span
|
||||
style={tagStyle}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onShowDetail(row.id, 'process')
|
||||
}}
|
||||
>
|
||||
{row.process}
|
||||
</span>
|
||||
)}
|
||||
{row.chains && <span style={tagStyle}>{row.chains}</span>}
|
||||
<span style={tagStyle}>
|
||||
<RelativeTime start={row.time} />
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
import {
|
||||
Alert,
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
MenuItem,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
useClashConfigData,
|
||||
useProxiesData,
|
||||
} from '@/providers/app-data-context'
|
||||
import { getProfiles, readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
import {
|
||||
getConnectionRuleTargets,
|
||||
getConnectionRules,
|
||||
getConnectionRootDomain,
|
||||
isConnectionRuleDomain,
|
||||
findConnectionRule,
|
||||
saveConnectionRule,
|
||||
type ConnectionRuleKind,
|
||||
} from './connection-rule'
|
||||
|
||||
export interface ConnectionRuleTarget {
|
||||
metadata: IConnectionsItem['metadata']
|
||||
profileUid: string
|
||||
profileName: string
|
||||
rulesUid: string
|
||||
preferredKind: ConnectionRuleKind
|
||||
}
|
||||
|
||||
type DomainScope = 'DOMAIN' | 'DOMAIN-SUFFIX' | 'ROOT-DOMAIN'
|
||||
|
||||
function getInitialKind(target: ConnectionRuleTarget) {
|
||||
const targets = getConnectionRuleTargets(target.metadata)
|
||||
return targets[target.preferredKind]
|
||||
? target.preferredKind
|
||||
: ((['domain', 'process', 'source', 'destination'] as const).find(
|
||||
(kind) => targets[kind],
|
||||
) ?? 'domain')
|
||||
}
|
||||
|
||||
export function ConnectionRuleDialog({
|
||||
target,
|
||||
onClose,
|
||||
}: {
|
||||
target: ConnectionRuleTarget
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
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<DomainScope>('DOMAIN')
|
||||
const [domainValue, setDomainValue] = useState(targets.domain?.value ?? '')
|
||||
const [rules, setRules] = useState<string[] | null>(null)
|
||||
const [loadAttempt, setLoadAttempt] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
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 !== 'DOMAIN'
|
||||
? 'DOMAIN-SUFFIX'
|
||||
: selected.type,
|
||||
value: kind === 'domain' ? matchingValue.trim() : selected.value,
|
||||
}
|
||||
: undefined
|
||||
const existing =
|
||||
rules && condition && !invalidDomain
|
||||
? findConnectionRule(rules, condition)
|
||||
: undefined
|
||||
const modifying = Boolean(existing)
|
||||
const groups = proxyView
|
||||
? proxyView.groups.length || !proxyView.global
|
||||
? proxyView.groups
|
||||
: [proxyView.global]
|
||||
: []
|
||||
const policies = [
|
||||
...new Set(['DIRECT', 'REJECT', ...groups.map((group) => group.name)]),
|
||||
]
|
||||
|
||||
const kindLabels = {
|
||||
domain: t('connections.ruleDialog.domain'),
|
||||
process: t('connections.components.fields.process'),
|
||||
source: t('connections.ruleDialog.sourceIp'),
|
||||
destination: t('connections.ruleDialog.destinationIp'),
|
||||
}
|
||||
|
||||
const unavailablePolicy = Boolean(policy && !policies.includes(policy))
|
||||
const blocked = loading || saving || rules === null
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
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 initialKind = getInitialKind(target)
|
||||
const selectedTarget = ruleTargets[initialKind]
|
||||
const initial = selectedTarget
|
||||
? (findConnectionRule(loadedRules, selectedTarget) ??
|
||||
(initialKind === 'domain'
|
||||
? findConnectionRule(loadedRules, {
|
||||
...selectedTarget,
|
||||
type: 'DOMAIN-SUFFIX',
|
||||
})
|
||||
: undefined))
|
||||
: undefined
|
||||
if (cancelled) return
|
||||
setRootDomain(root)
|
||||
setRules(loadedRules)
|
||||
setPolicy(initial?.policy ?? null)
|
||||
setScope(initial?.type === 'DOMAIN-SUFFIX' ? 'DOMAIN-SUFFIX' : 'DOMAIN')
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!cancelled)
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [target, loadAttempt])
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
if (
|
||||
blocked ||
|
||||
!condition ||
|
||||
invalidDomain ||
|
||||
!policy ||
|
||||
!policies.includes(policy)
|
||||
)
|
||||
return
|
||||
if (/[\r\n,]/u.test(condition.value) || /[\r\n,]/u.test(policy)) {
|
||||
setError(t('connections.ruleDialog.unsupportedValue'))
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
setError('')
|
||||
try {
|
||||
const result = await mutate(
|
||||
async () => {
|
||||
const checkProfile = async () => {
|
||||
const profiles = await getProfiles()
|
||||
const current = profiles.items?.find(
|
||||
(item) => item.uid === profiles.current,
|
||||
)
|
||||
if (
|
||||
profiles.current !== target.profileUid ||
|
||||
current?.option?.rules !== target.rulesUid
|
||||
) {
|
||||
throw new Error(t('connections.ruleDialog.profileChanged'))
|
||||
}
|
||||
}
|
||||
await checkProfile()
|
||||
const content = await readProfileFile(target.rulesUid)
|
||||
const updated = saveConnectionRule(content, condition, policy)
|
||||
await checkProfile()
|
||||
return saveProfileFile(target.rulesUid, updated)
|
||||
},
|
||||
{ id: `save-profile-file:${target.rulesUid}`, errorNotice: false },
|
||||
)
|
||||
if (!result.ok || !result.value) {
|
||||
setError(t('connections.ruleDialog.applyFailed'))
|
||||
return
|
||||
}
|
||||
showNotice.success('connections.ruleDialog.saved')
|
||||
onClose()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog open onClose={saving ? undefined : onClose} fullWidth maxWidth="sm">
|
||||
<DialogTitle>
|
||||
{t(
|
||||
modifying
|
||||
? 'connections.ruleDialog.modifyTitle'
|
||||
: 'connections.ruleDialog.title',
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
size="small"
|
||||
label={t('connections.ruleDialog.matchBy')}
|
||||
value={kind}
|
||||
disabled={blocked}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value as ConnectionRuleKind
|
||||
const nextTarget = targets[next]
|
||||
const rule =
|
||||
rules && nextTarget
|
||||
? (findConnectionRule(rules, nextTarget) ??
|
||||
(next === 'domain'
|
||||
? findConnectionRule(rules, {
|
||||
...nextTarget,
|
||||
type: 'DOMAIN-SUFFIX',
|
||||
})
|
||||
: undefined))
|
||||
: undefined
|
||||
setDomainValue(targets.domain?.value ?? '')
|
||||
setKind(next)
|
||||
setPolicy(rule?.policy ?? null)
|
||||
setScope(
|
||||
rule?.type === 'DOMAIN-SUFFIX' ? 'DOMAIN-SUFFIX' : 'DOMAIN',
|
||||
)
|
||||
}}
|
||||
>
|
||||
{(['domain', 'process', 'source', 'destination'] as const).map(
|
||||
(option) => (
|
||||
<MenuItem
|
||||
key={option}
|
||||
value={option}
|
||||
disabled={!targets[option]}
|
||||
>
|
||||
{kindLabels[option]}
|
||||
{!targets[option] &&
|
||||
` · ${t('connections.ruleDialog.unavailable')}`}
|
||||
</MenuItem>
|
||||
),
|
||||
)}
|
||||
</TextField>
|
||||
{kind === 'process' ? (
|
||||
<>
|
||||
{target.metadata.process?.trim() && (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={kindLabels.process}
|
||||
value={target.metadata.process}
|
||||
slotProps={{
|
||||
input: { readOnly: true },
|
||||
htmlInput: { title: target.metadata.process },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{selected?.type === 'PROCESS-PATH' && (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={t('connections.ruleDialog.fullPath')}
|
||||
value={selected.value}
|
||||
slotProps={{
|
||||
input: { readOnly: true },
|
||||
htmlInput: { title: selected.value },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={
|
||||
editingDomain || matchingRoot
|
||||
? t('connections.ruleDialog.matchingDomain')
|
||||
: kindLabels[kind]
|
||||
}
|
||||
value={matchingValue}
|
||||
disabled={editingDomain && blocked}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value
|
||||
setDomainValue(value)
|
||||
const match =
|
||||
rules &&
|
||||
isConnectionRuleDomain(value, targets.domain?.value ?? '')
|
||||
? findConnectionRule(rules, {
|
||||
type: 'DOMAIN-SUFFIX',
|
||||
value: value.trim(),
|
||||
})
|
||||
: undefined
|
||||
if (match) setPolicy(match.policy)
|
||||
}}
|
||||
error={invalidDomain}
|
||||
helperText={
|
||||
editingDomain || matchingRoot ? (
|
||||
<>
|
||||
{t('connections.ruleDialog.connectionHost', {
|
||||
host: targets.domain?.value,
|
||||
})}
|
||||
{invalidDomain && (
|
||||
<>
|
||||
<br />
|
||||
{t('connections.ruleDialog.invalidDomain')}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
slotProps={{
|
||||
input: { readOnly: !editingDomain },
|
||||
htmlInput: {
|
||||
title: condition?.value,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{kind === 'domain' && (
|
||||
<RadioGroup
|
||||
row
|
||||
aria-label={t('connections.ruleDialog.domainScope')}
|
||||
value={scope}
|
||||
onChange={(_, value) => {
|
||||
const nextScope = value as DomainScope
|
||||
setScope(nextScope)
|
||||
setDomainValue(targets.domain?.value ?? '')
|
||||
const match =
|
||||
rules && targets.domain
|
||||
? findConnectionRule(rules, {
|
||||
type: nextScope === 'DOMAIN' ? 'DOMAIN' : 'DOMAIN-SUFFIX',
|
||||
value:
|
||||
nextScope === 'ROOT-DOMAIN'
|
||||
? (rootDomain ?? '')
|
||||
: targets.domain.value,
|
||||
})
|
||||
: undefined
|
||||
setPolicy(match?.policy ?? null)
|
||||
}}
|
||||
sx={{ mt: 0.5 }}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="DOMAIN"
|
||||
control={<Radio size="small" />}
|
||||
disabled={blocked}
|
||||
label={t('connections.ruleDialog.exactDomain')}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="DOMAIN-SUFFIX"
|
||||
control={<Radio size="small" />}
|
||||
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') && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
{t(
|
||||
kind === 'source'
|
||||
? 'connections.ruleDialog.sourceHint'
|
||||
: 'connections.ruleDialog.destinationHint',
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
{kind === 'process' && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
{selected?.type === 'PROCESS-PATH'
|
||||
? t('connections.ruleDialog.processPathHint')
|
||||
: t('connections.ruleDialog.processNameHint')}
|
||||
</Typography>
|
||||
)}
|
||||
<Autocomplete
|
||||
size="small"
|
||||
options={
|
||||
unavailablePolicy && policy ? [policy, ...policies] : policies
|
||||
}
|
||||
getOptionLabel={(value) =>
|
||||
value === 'DIRECT'
|
||||
? t('proxies.components.enums.policies.DIRECT')
|
||||
: value === 'REJECT'
|
||||
? t('proxies.components.enums.policies.REJECT')
|
||||
: value
|
||||
}
|
||||
getOptionDisabled={(value) => !policies.includes(value)}
|
||||
value={policy}
|
||||
onChange={(_, value) => setPolicy(value)}
|
||||
disabled={blocked}
|
||||
loading={loading}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={t('rules.modals.editor.form.labels.proxyPolicy')}
|
||||
margin="normal"
|
||||
error={unavailablePolicy}
|
||||
helperText={
|
||||
unavailablePolicy
|
||||
? t('connections.ruleDialog.policyUnavailable')
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{clashConfig && clashConfig.mode !== 'rule' && (
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
{t('connections.ruleDialog.ruleModeHint')}
|
||||
</Alert>
|
||||
)}
|
||||
{error && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Alert severity="error">
|
||||
{error}
|
||||
{rules === null && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setLoadAttempt((attempt) => attempt + 1)
|
||||
}}
|
||||
>
|
||||
{t('shared.actions.retry')}
|
||||
</Button>
|
||||
)}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: 'block', mt: 2, overflowWrap: 'anywhere' }}
|
||||
>
|
||||
{t(
|
||||
modifying
|
||||
? 'connections.ruleDialog.editDestination'
|
||||
: 'connections.ruleDialog.destination',
|
||||
{
|
||||
name: target.profileName,
|
||||
},
|
||||
)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: 'block' }}
|
||||
>
|
||||
{t('connections.ruleDialog.newConnections')}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button disabled={saving} onClick={onClose}>
|
||||
{t('shared.actions.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
loading={saving}
|
||||
disabled={
|
||||
blocked ||
|
||||
!condition ||
|
||||
invalidDomain ||
|
||||
!policy ||
|
||||
!policies.includes(policy)
|
||||
}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t(
|
||||
modifying
|
||||
? 'connections.ruleDialog.saveAndApply'
|
||||
: 'connections.ruleDialog.addAndApply',
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { dump, load } from 'js-yaml'
|
||||
import isFQDN from 'validator/es/lib/isFQDN'
|
||||
import isIP from 'validator/es/lib/isIP'
|
||||
|
||||
export type ConnectionRuleKind = 'domain' | 'process' | 'source' | 'destination'
|
||||
|
||||
function getAddressRule(address: string | undefined, source: boolean) {
|
||||
const ip = address?.trim()
|
||||
if (!ip || !isIP(ip)) return undefined
|
||||
const ipv4 = isIP(ip, 4)
|
||||
return {
|
||||
type: source ? 'SRC-IP-CIDR' : ipv4 ? 'IP-CIDR' : 'IP-CIDR6',
|
||||
value: `${ip}/${ipv4 ? 32 : 128}`,
|
||||
}
|
||||
}
|
||||
|
||||
export function getConnectionRuleTargets(
|
||||
metadata: Pick<
|
||||
IConnectionsItem['metadata'],
|
||||
| 'host'
|
||||
| 'process'
|
||||
| 'processPath'
|
||||
| 'sourceIP'
|
||||
| 'destinationIP'
|
||||
| 'remoteDestination'
|
||||
>,
|
||||
) {
|
||||
const host = metadata.host?.trim() ?? ''
|
||||
const path = metadata.processPath
|
||||
const name = metadata.process
|
||||
const domain =
|
||||
host && !isIP(host) && isFQDN(host, { require_tld: false })
|
||||
? { type: 'DOMAIN', value: host }
|
||||
: undefined
|
||||
const process = path?.trim()
|
||||
? { type: 'PROCESS-PATH', value: path }
|
||||
: name?.trim()
|
||||
? { type: 'PROCESS-NAME', value: name }
|
||||
: undefined
|
||||
const source = getAddressRule(metadata.sourceIP, true)
|
||||
const destination =
|
||||
getAddressRule(metadata.destinationIP, false) ??
|
||||
getAddressRule(metadata.remoteDestination, false)
|
||||
return { domain, process, source, destination }
|
||||
}
|
||||
|
||||
export function isConnectionRuleDomain(value: string, host: string) {
|
||||
const domain = value.trim()
|
||||
if (!domain || isIP(domain) || !isFQDN(domain, { require_tld: false }))
|
||||
return false
|
||||
const normalized = domain.toLowerCase()
|
||||
const current = host.toLowerCase()
|
||||
return (
|
||||
normalized === current ||
|
||||
(domain.split('.').length >= 2 && current.endsWith(`.${normalized}`))
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
parsed !== undefined &&
|
||||
parsed !== null &&
|
||||
(typeof parsed !== 'object' ||
|
||||
Array.isArray(parsed) ||
|
||||
Object.getPrototypeOf(parsed) !== Object.prototype)
|
||||
) {
|
||||
throw new Error('The rule extension must be a YAML mapping.')
|
||||
}
|
||||
const config = (parsed ?? {}) as Record<string, unknown>
|
||||
for (const key of ['prepend', 'append', 'delete']) {
|
||||
const sequence = config[key]
|
||||
if (
|
||||
sequence != null &&
|
||||
(!Array.isArray(sequence) ||
|
||||
sequence.some((item) => typeof item !== 'string'))
|
||||
) {
|
||||
throw new Error('The rule extension contains an invalid rule list.')
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
interface RuleCondition {
|
||||
type: string
|
||||
value: string
|
||||
}
|
||||
|
||||
function matchesConnectionRule(raw: string, target: RuleCondition) {
|
||||
const parts = raw.split(',').map((part) => part.trim())
|
||||
const hasNoResolve =
|
||||
(target.type === 'IP-CIDR' || target.type === 'IP-CIDR6') &&
|
||||
parts.length === 4 &&
|
||||
parts[3] === 'no-resolve'
|
||||
if (parts.length !== 3 && !hasNoResolve) return false
|
||||
if (target.type === 'DOMAIN' || target.type === 'DOMAIN-SUFFIX') {
|
||||
return (
|
||||
parts[0] === target.type &&
|
||||
parts[1].toLowerCase() === target.value.toLowerCase()
|
||||
)
|
||||
}
|
||||
return parts[0] === target.type && parts[1] === target.value
|
||||
}
|
||||
|
||||
export function getConnectionRules(content: string): string[] {
|
||||
const config = parseRuleExtension(content)
|
||||
return [
|
||||
...((config.prepend ?? []) as string[]),
|
||||
...((config.append ?? []) as string[]),
|
||||
]
|
||||
}
|
||||
|
||||
export function findConnectionRule(rules: string[], target: RuleCondition) {
|
||||
for (const raw of rules) {
|
||||
if (matchesConnectionRule(raw, target)) {
|
||||
const [type, , policy] = raw.split(',').map((part) => part.trim())
|
||||
return { type, policy }
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function saveConnectionRule(
|
||||
content: string,
|
||||
target: RuleCondition,
|
||||
policy: string,
|
||||
): string {
|
||||
const config = parseRuleExtension(content)
|
||||
const rule = `${target.type},${target.value},${policy}`
|
||||
let replaced = false
|
||||
for (const key of ['prepend', 'append']) {
|
||||
if (!Array.isArray(config[key])) continue
|
||||
config[key] = (config[key] as string[]).flatMap((raw) => {
|
||||
if (!matchesConnectionRule(raw, target)) return [raw]
|
||||
if (replaced) return []
|
||||
replaced = true
|
||||
return [raw.split(',').length === 4 ? `${rule},no-resolve` : rule]
|
||||
})
|
||||
}
|
||||
if (!replaced) {
|
||||
config.prepend = [rule, ...((config.prepend ?? []) as string[])]
|
||||
}
|
||||
return dump(config, { forceQuotes: true })
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
getConnectionStartTime,
|
||||
getConnectionTypeLabel,
|
||||
} from './connection-row-view'
|
||||
import type { ConnectionRuleKind } from './connection-rule'
|
||||
|
||||
const ROW_HEIGHT = 40
|
||||
const RESIZE_HANDLE_WIDTH = 6
|
||||
@@ -247,7 +248,7 @@ const renderCell = (
|
||||
interface RowComponentProps {
|
||||
row: IConnectionsItem
|
||||
columns: DisplayColumn[]
|
||||
onShowDetail: (id: string) => void
|
||||
onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void
|
||||
getSnapshot: (row: IConnectionsItem) => TableRowSnapshot
|
||||
borderColor: string
|
||||
virtualTop: number
|
||||
@@ -263,7 +264,22 @@ const RowComponent = memo(
|
||||
virtualTop,
|
||||
}: RowComponentProps) {
|
||||
const handleClick = useCallback(
|
||||
() => onShowDetail(row.id),
|
||||
(event: ReactMouseEvent<HTMLDivElement>) => {
|
||||
const field =
|
||||
event.target instanceof Element
|
||||
? event.target.closest<HTMLElement>('[data-connection-field]')
|
||||
?.dataset.connectionField
|
||||
: undefined
|
||||
const preferredKind =
|
||||
field === 'process'
|
||||
? 'process'
|
||||
: field === 'source'
|
||||
? 'source'
|
||||
: field === 'remoteDestination'
|
||||
? 'destination'
|
||||
: 'domain'
|
||||
onShowDetail(row.id, preferredKind)
|
||||
},
|
||||
[onShowDetail, row.id],
|
||||
)
|
||||
const snapshot = getSnapshot(row)
|
||||
@@ -285,6 +301,7 @@ const RowComponent = memo(
|
||||
{columns.map((column) => (
|
||||
<div
|
||||
key={column.field}
|
||||
data-connection-field={column.field}
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
flex: `0 0 ${column.size}px`,
|
||||
@@ -318,7 +335,7 @@ const RowComponent = memo(
|
||||
|
||||
interface Props {
|
||||
connections: IConnectionsItem[]
|
||||
onShowDetail: (id: string) => void
|
||||
onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void
|
||||
columnManagerOpen: boolean
|
||||
onCloseColumnManager: () => void
|
||||
}
|
||||
@@ -333,7 +350,8 @@ export const ConnectionTable = (props: Props) => {
|
||||
const onShowDetailRef = useRef(rawOnShowDetail)
|
||||
onShowDetailRef.current = rawOnShowDetail
|
||||
const onShowDetail = useCallback(
|
||||
(id: string) => onShowDetailRef.current(id),
|
||||
(id: string, preferredKind?: ConnectionRuleKind) =>
|
||||
onShowDetailRef.current(id, preferredKind),
|
||||
[],
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "الأعمدة",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "Spalten",
|
||||
"dragHandle": "Ziehpunkt"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "Columns",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "Columnas",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "ستونها",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "Kolom",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "列",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "열",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "Colunas",
|
||||
"dragHandle": "Alça de arrastar"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "Столбцы",
|
||||
"dragHandle": "Маркер перетаскивания"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "Sütunlar",
|
||||
"dragHandle": "Sürükleme tutamacı"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "Баганалар",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "列设置",
|
||||
"dragHandle": "拖拽控件"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "添加规则…",
|
||||
"matchBy": "匹配对象",
|
||||
"domain": "域名",
|
||||
"unavailable": "未获取",
|
||||
"destination": "添加到「{{name}}」的前置规则。",
|
||||
"newConnections": "新建立的连接将按更新后的配置匹配。",
|
||||
"processPathHint": "匹配此路径对应进程访问的所有目标。",
|
||||
"processNameHint": "匹配所有同名进程访问的所有目标。",
|
||||
"ruleModeHint": "规则分流在规则模式下生效。",
|
||||
"addAndApply": "添加并应用",
|
||||
"profileChanged": "当前订阅已切换,请关闭弹窗后重新添加规则。",
|
||||
"applyFailed": "规则未能应用,已保留当前选择。",
|
||||
"unsupportedValue": "此内容包含快捷规则不支持的字符。",
|
||||
"saved": "规则已保存,配置已应用。",
|
||||
"noMetadata": "此连接没有可用于设置规则的域名、进程或 IP 信息。",
|
||||
"noProfile": "当前没有可编辑规则扩展的活动订阅。",
|
||||
"entry": "设置规则…",
|
||||
"modifyTitle": "修改规则",
|
||||
"domainScope": "域名匹配范围",
|
||||
"exactDomain": "仅此域名",
|
||||
"allSubdomains": "此域名及所有子域名",
|
||||
"rootDomain": "主域名及所有子域名({{domain}})",
|
||||
"policyUnavailable": "此策略已不可用,请选择其他策略。",
|
||||
"editDestination": "修改「{{name}}」中的现有规则,保留排序。",
|
||||
"saveAndApply": "保存并应用",
|
||||
"sourceIp": "源 IP",
|
||||
"destinationIp": "目标 IP",
|
||||
"sourceHint": "匹配来自此 IP 的所有请求,可能包含其他应用,不限定端口。",
|
||||
"destinationHint": "匹配访问此目标 IP 的请求,覆盖所有域名,不限定端口。",
|
||||
"fullPath": "完整路径",
|
||||
"connectionHost": "当前连接域名:{{host}}",
|
||||
"invalidDomain": "请输入当前连接域名或其父域名,父域名至少包含两级。",
|
||||
"matchingDomain": "匹配域名"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,40 @@
|
||||
"title": "欄位設定",
|
||||
"dragHandle": "拖曳以移動"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"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",
|
||||
"sourceIp": "Source IP",
|
||||
"destinationIp": "Destination IP",
|
||||
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
|
||||
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
|
||||
"fullPath": "Full path",
|
||||
"connectionHost": "Current connection domain: {{host}}",
|
||||
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||
"matchingDomain": "Matching domain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
getConnectionStartTime,
|
||||
useConnectionRowViews,
|
||||
} from '@/components/connection/connection-row-view'
|
||||
import type { ConnectionRuleKind } from '@/components/connection/connection-rule'
|
||||
import { ConnectionTable } from '@/components/connection/connection-table'
|
||||
import { useConnectionData } from '@/hooks/use-connection-data'
|
||||
import { useConnectionSetting } from '@/hooks/use-connection-setting'
|
||||
@@ -161,10 +162,14 @@ const ConnectionsPage = () => {
|
||||
)
|
||||
|
||||
const showDetailById = useCallback(
|
||||
(id: string) => {
|
||||
(id: string, preferredKind: ConnectionRuleKind = 'domain') => {
|
||||
const connection = filterConn.find((item) => item.id === id)
|
||||
if (connection) {
|
||||
detailRef.current?.open(connection, connectionsType === 'closed')
|
||||
detailRef.current?.open(
|
||||
connection,
|
||||
connectionsType === 'closed',
|
||||
preferredKind,
|
||||
)
|
||||
}
|
||||
},
|
||||
[connectionsType, filterConn],
|
||||
|
||||
@@ -22,6 +22,39 @@ export const translationKeys = [
|
||||
'connections.components.actions.closeConnection',
|
||||
'connections.components.columnManager.title',
|
||||
'connections.components.columnManager.dragHandle',
|
||||
'connections.ruleDialog.title',
|
||||
'connections.ruleDialog.matchBy',
|
||||
'connections.ruleDialog.domain',
|
||||
'connections.ruleDialog.unavailable',
|
||||
'connections.ruleDialog.destination',
|
||||
'connections.ruleDialog.newConnections',
|
||||
'connections.ruleDialog.processPathHint',
|
||||
'connections.ruleDialog.processNameHint',
|
||||
'connections.ruleDialog.ruleModeHint',
|
||||
'connections.ruleDialog.addAndApply',
|
||||
'connections.ruleDialog.profileChanged',
|
||||
'connections.ruleDialog.applyFailed',
|
||||
'connections.ruleDialog.unsupportedValue',
|
||||
'connections.ruleDialog.saved',
|
||||
'connections.ruleDialog.noMetadata',
|
||||
'connections.ruleDialog.noProfile',
|
||||
'connections.ruleDialog.entry',
|
||||
'connections.ruleDialog.modifyTitle',
|
||||
'connections.ruleDialog.domainScope',
|
||||
'connections.ruleDialog.exactDomain',
|
||||
'connections.ruleDialog.allSubdomains',
|
||||
'connections.ruleDialog.rootDomain',
|
||||
'connections.ruleDialog.policyUnavailable',
|
||||
'connections.ruleDialog.editDestination',
|
||||
'connections.ruleDialog.saveAndApply',
|
||||
'connections.ruleDialog.sourceIp',
|
||||
'connections.ruleDialog.destinationIp',
|
||||
'connections.ruleDialog.sourceHint',
|
||||
'connections.ruleDialog.destinationHint',
|
||||
'connections.ruleDialog.fullPath',
|
||||
'connections.ruleDialog.connectionHost',
|
||||
'connections.ruleDialog.invalidDomain',
|
||||
'connections.ruleDialog.matchingDomain',
|
||||
'home.page.tooltips.lightweightMode',
|
||||
'home.page.tooltips.manual',
|
||||
'home.page.tooltips.settings',
|
||||
|
||||
@@ -36,6 +36,41 @@ export interface TranslationResources {
|
||||
page: {
|
||||
title: string
|
||||
}
|
||||
ruleDialog: {
|
||||
addAndApply: string
|
||||
allSubdomains: string
|
||||
applyFailed: string
|
||||
connectionHost: string
|
||||
destination: string
|
||||
destinationHint: string
|
||||
destinationIp: string
|
||||
domain: string
|
||||
domainScope: string
|
||||
editDestination: string
|
||||
entry: string
|
||||
exactDomain: string
|
||||
fullPath: string
|
||||
invalidDomain: string
|
||||
matchBy: string
|
||||
matchingDomain: string
|
||||
modifyTitle: string
|
||||
newConnections: string
|
||||
noMetadata: string
|
||||
noProfile: string
|
||||
policyUnavailable: string
|
||||
processNameHint: string
|
||||
processPathHint: string
|
||||
profileChanged: string
|
||||
rootDomain: string
|
||||
ruleModeHint: string
|
||||
saveAndApply: string
|
||||
saved: string
|
||||
sourceHint: string
|
||||
sourceIp: string
|
||||
title: string
|
||||
unavailable: string
|
||||
unsupportedValue: string
|
||||
}
|
||||
}
|
||||
home: {
|
||||
components: {
|
||||
|
||||
Reference in New Issue
Block a user