mirror of
https://github.com/2dust/v2rayN.git
synced 2026-08-12 02:02:04 +03:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31044f449d | ||
|
|
eacf8b0784 | ||
|
|
f347fe1c03 | ||
|
|
71208eb711 | ||
|
|
339eaa6bfd | ||
|
|
717ef7f0a7 | ||
|
|
4a9d83f9dc | ||
|
|
1ac14d0903 | ||
|
|
08d423e1b4 | ||
|
|
9638b4c368 | ||
|
|
1034ce6e02 | ||
|
|
5efff37acb | ||
|
|
e8cdd1cc02 | ||
|
|
d381d429e4 | ||
|
|
b27ae11d1e | ||
|
|
eff584597f | ||
|
|
ccf18ba0fb | ||
|
|
15ba2bdf93 | ||
|
|
e1e6c5ddb0 | ||
|
|
d924c6f557 | ||
|
|
635a04d74c | ||
|
|
2e1bb23da7 | ||
|
|
6f3bacd5b4 | ||
|
|
074ce5de04 | ||
|
|
02df430172 | ||
|
|
611ce0fd2c | ||
|
|
a846e74e8f | ||
|
|
92c3df45ae | ||
|
|
ee7e21268a | ||
|
|
dc216c2b02 | ||
|
|
beffa919d3 | ||
|
|
a654826231 | ||
|
|
b01476d147 | ||
|
|
cf8dd45abe | ||
|
|
9bba6f53f8 | ||
|
|
cd77f1d882 | ||
|
|
0427037638 | ||
|
|
e749b81ecf | ||
|
|
fd2c942231 | ||
|
|
fa42eb670f | ||
|
|
0d42996573 | ||
|
|
a37772f12b | ||
|
|
531e0e9443 | ||
|
|
984c77f0c4 | ||
|
|
b5e0be47a5 | ||
|
|
6df0e6de1b | ||
|
|
2b5328665f | ||
|
|
4584ecc5c9 | ||
|
|
d9dc2d7cf5 | ||
|
|
36f8565b7a | ||
|
|
f55d8b2565 | ||
|
|
09ea4890a7 | ||
|
|
5cc2aaba13 | ||
|
|
b8889bad86 | ||
|
|
5dd5b25869 | ||
|
|
fafdb4a0b6 | ||
|
|
223642dd67 | ||
|
|
165e0bf9e9 | ||
|
|
e615314582 | ||
|
|
ca9978b625 | ||
|
|
74ab7ad097 | ||
|
|
8f5cbad988 | ||
|
|
1ab9757498 | ||
|
|
50ab02e8a9 | ||
|
|
b9a1c129c4 | ||
|
|
996eb51f9b | ||
|
|
b9a58e7137 | ||
|
|
9664ee380e | ||
|
|
6f50206606 | ||
|
|
1de83f96ed | ||
|
|
994bc1ca6a | ||
|
|
3ca49dd9db | ||
|
|
a0bd8f9934 | ||
|
|
88d59488fd | ||
|
|
2c70b018ce | ||
|
|
b6ab428dfc | ||
|
|
45ab7503e3 | ||
|
|
1768b4a7ee | ||
|
|
3e9f78f831 | ||
|
|
8c4bd52389 | ||
|
|
fa61fedf11 | ||
|
|
ad12a6c456 | ||
|
|
66b48c231f | ||
|
|
52766d66bf | ||
|
|
e1a5c360ac | ||
|
|
0c570fcb38 | ||
|
|
a236fab27c | ||
|
|
f8e1186095 | ||
|
|
ca186e2636 | ||
|
|
1e8ba9ad9b | ||
|
|
31e32f174e | ||
|
|
bf376a8fac | ||
|
|
5a9a9b7ab1 | ||
|
|
e7a9aa66dc | ||
|
|
709b9a0958 | ||
|
|
191fe48257 | ||
|
|
48d58d482a | ||
|
|
7100b3f0a5 | ||
|
|
8d94997ba0 | ||
|
|
c97dc1d894 | ||
|
|
b198fd8be7 | ||
|
|
e0c1ff24be | ||
|
|
3cebf492d1 | ||
|
|
ef46f4e7e6 | ||
|
|
81c118c9b2 | ||
|
|
f1b88ccf2a | ||
|
|
ce380b861c | ||
|
|
46e9e4fead | ||
|
|
00903d3531 | ||
|
|
616eea39f9 | ||
|
|
ff456a9389 | ||
|
|
0d225cd29d | ||
|
|
67426cff12 | ||
|
|
a8c27027b4 | ||
|
|
1a681695a2 | ||
|
|
2126ea5a87 | ||
|
|
be005abf9e | ||
|
|
da7dfde706 | ||
|
|
7491e86d80 | ||
|
|
af037ed087 | ||
|
|
c9298db607 | ||
|
|
da81101cfd | ||
|
|
3d4f202cd7 | ||
|
|
35fbb137e8 | ||
|
|
1869a95700 | ||
|
|
34359885ab | ||
|
|
93832656dd | ||
|
|
b1a400b393 | ||
|
|
879c736924 | ||
|
|
8147b39723 | ||
|
|
5fed566a32 | ||
|
|
f0ee792778 | ||
|
|
cd4aa5531c | ||
|
|
e6d08c2db1 | ||
|
|
4dc9c60b85 | ||
|
|
eb9c974436 | ||
|
|
27b8f889fe | ||
|
|
0d5169e4cd | ||
|
|
abcdd9497c | ||
|
|
da65faa49f | ||
|
|
b6cfce069a | ||
|
|
39ec9239d2 | ||
|
|
a2a5940d91 | ||
|
|
ce8caa89a4 | ||
|
|
af3d579890 | ||
|
|
b2bbc865af | ||
|
|
2eb325643a | ||
|
|
8609e2b8d1 | ||
|
|
fcf6c1e3ae | ||
|
|
ab907e371b | ||
|
|
ef4dda0996 | ||
|
|
f2e0bd4eba | ||
|
|
bca07f801d | ||
|
|
54a3388c99 | ||
|
|
5140954b37 | ||
|
|
c7efe39e1c |
4
.github/ISSUE_TEMPLATE/01_bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/01_bug_report.yml
vendored
@@ -18,8 +18,8 @@ body:
|
|||||||
- type: input
|
- type: input
|
||||||
id: "os-version"
|
id: "os-version"
|
||||||
attributes:
|
attributes:
|
||||||
label: "操作系统和版本"
|
label: "操作系统 / v2rayN / Core 版本"
|
||||||
description: "操作系统和版本"
|
description: "操作系统 / v2rayN / Core 版本"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
113
.github/scripts/UpdateCert.cs
vendored
Normal file
113
.github/scripts/UpdateCert.cs
vendored
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
THIRD-PARTY CA CERTIFICATE DATA ATTRIBUTION
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
This component includes CA certificate data obtained from third-party sources.
|
||||||
|
|
||||||
|
1. Common CA Database (CCADB)
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
Website:
|
||||||
|
https://www.ccadb.org/
|
||||||
|
|
||||||
|
Data source:
|
||||||
|
https://ccadb.my.salesforce-sites.com/mozilla/IncludedRootsPEMTxt?TrustBitsInclude=Websites
|
||||||
|
|
||||||
|
License:
|
||||||
|
Community Data License Agreement – Permissive, Version 2.0 (CDLA-2.0 Permissive)
|
||||||
|
|
||||||
|
The names and trademarks of CCADB contributors may not be used to endorse or
|
||||||
|
promote products derived from this data without prior written permission.
|
||||||
|
|
||||||
|
License text:
|
||||||
|
https://cdla.dev/permissive-2-0/
|
||||||
|
|
||||||
|
|
||||||
|
2. Chromium Root Store
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
Source:
|
||||||
|
Chromium Project
|
||||||
|
|
||||||
|
Data source:
|
||||||
|
https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/root_store.certs?format=TEXT
|
||||||
|
|
||||||
|
License:
|
||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright:
|
||||||
|
Copyright (c) The Chromium Authors
|
||||||
|
|
||||||
|
The Chromium Root Store data is distributed under the BSD 3-Clause License.
|
||||||
|
|
||||||
|
License text:
|
||||||
|
https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
var client = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
|
||||||
|
|
||||||
|
var outputDir = args.Length > 0
|
||||||
|
? args[0]
|
||||||
|
: Path.Combine(Environment.CurrentDirectory, "v2rayN", "ServiceLib", "Sample");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(outputDir);
|
||||||
|
|
||||||
|
Console.WriteLine("=== Mozilla + Chrome Root Store PEM Converter ===\n");
|
||||||
|
Console.WriteLine($"Output directory: {outputDir}\n");
|
||||||
|
|
||||||
|
// Process Mozilla
|
||||||
|
await ProcessMozillaAsync(outputDir);
|
||||||
|
|
||||||
|
// Process Chrome
|
||||||
|
await ProcessChromeAsync(outputDir);
|
||||||
|
|
||||||
|
Console.WriteLine("\nAll done!");
|
||||||
|
|
||||||
|
async Task ProcessMozillaAsync(string outputDir)
|
||||||
|
{
|
||||||
|
const string certdataUrl = "https://ccadb.my.salesforce-sites.com/mozilla/IncludedRootsPEMTxt?TrustBitsInclude=Websites";
|
||||||
|
var outputFile = Path.Combine(outputDir, "mozilla_roots_pem");
|
||||||
|
|
||||||
|
Console.WriteLine("Downloading Mozilla certdata.txt...");
|
||||||
|
var content = await client.GetStringAsync(certdataUrl);
|
||||||
|
|
||||||
|
var pemMatches = Regex.Matches(content, @"(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)");
|
||||||
|
var pems = new List<string>();
|
||||||
|
foreach (Match m in pemMatches)
|
||||||
|
{
|
||||||
|
pems.Add(m.Groups[1].Value.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(outputFile, string.Join("\n\n", pems));
|
||||||
|
Console.WriteLine($"Mozilla roots saved to: {outputFile} ({pems.Count} certificates)");
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task ProcessChromeAsync(string outputDir)
|
||||||
|
{
|
||||||
|
const string chromeCertsUrl = "https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/root_store.certs?format=TEXT";
|
||||||
|
var chromeOutput = Path.Combine(outputDir, "chrome_roots_pem");
|
||||||
|
|
||||||
|
Console.WriteLine("Downloading Chrome root_store.certs...");
|
||||||
|
var base64Content = await client.GetStringAsync(chromeCertsUrl);
|
||||||
|
var decoded = Convert.FromBase64String(base64Content.Replace("\n", ""));
|
||||||
|
|
||||||
|
var text = Encoding.UTF8.GetString(decoded);
|
||||||
|
|
||||||
|
var pemMatches = Regex.Matches(text, @"(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)");
|
||||||
|
var pems = new List<string>();
|
||||||
|
foreach (Match m in pemMatches)
|
||||||
|
{
|
||||||
|
pems.Add(m.Groups[1].Value.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(chromeOutput, string.Join("\n\n", pems));
|
||||||
|
Console.WriteLine($"Chrome roots saved to: {chromeOutput} ({pems.Count} certificates)");
|
||||||
|
}
|
||||||
23
.github/workflows/build-all.yml
vendored
23
.github/workflows/build-all.yml
vendored
@@ -9,8 +9,17 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
public-key:
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
uses: ./.github/workflows/pub-key.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
|
||||||
update:
|
update:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -70,3 +79,17 @@ jobs:
|
|||||||
\"release_tag\": \"${{ inputs.release_tag }}\"
|
\"release_tag\": \"${{ inputs.release_tag }}\"
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
|
|
||||||
|
- name: Trigger build windows x86
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
run: |
|
||||||
|
curl -X POST \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
https://api.github.com/repos/${{ github.repository }}/actions/workflows/build-windows-x86.yml/dispatches \
|
||||||
|
-d "{
|
||||||
|
\"ref\": \"master\",
|
||||||
|
\"inputs\": {
|
||||||
|
\"release_tag\": \"${{ inputs.release_tag }}\"
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
|||||||
535
.github/workflows/build-linux.yml
vendored
535
.github/workflows/build-linux.yml
vendored
@@ -12,6 +12,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -26,17 +27,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
target: linux
|
target: linux
|
||||||
release_tag: ${{ inputs.release_tag }}
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
deb:
|
deb:
|
||||||
name: build and release deb x64 & arm64
|
name: build and release deb x64 & arm64
|
||||||
if: |
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
|
runs-on: ubuntu-26.04
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
container: debian:13
|
container: debian:13
|
||||||
env:
|
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
|
||||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare tools (Debian)
|
- name: Prepare tools (Debian)
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -44,124 +42,73 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \
|
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev
|
||||||
ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev \
|
|
||||||
libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev
|
|
||||||
|
|
||||||
- name: Checkout repo (for scripts)
|
- name: Checkout repo (for scripts)
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with: { submodules: 'recursive', fetch-depth: '0' }
|
||||||
submodules: 'recursive'
|
|
||||||
fetch-depth: '0'
|
|
||||||
|
|
||||||
- name: Ensure script permissions
|
- name: Ensure script permissions
|
||||||
run: chmod 755 package-debian.sh
|
run: chmod 755 package-debian.sh
|
||||||
|
|
||||||
- name: Package DEB (Debian-family)
|
- name: Package DEB (Debian-family)
|
||||||
run: ./package-debian.sh "${RELEASE_TAG}" --arch all
|
run: ./package-debian.sh "${RELEASE_TAG}" --arch all
|
||||||
|
|
||||||
- name: Collect DEBs into workspace
|
- name: Collect DEBs into workspace
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/deb"
|
mkdir -p "$GITHUB_WORKSPACE/dist/deb"
|
||||||
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true
|
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true
|
||||||
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" \
|
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true
|
||||||
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true
|
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
|
||||||
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" \
|
|
||||||
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
|
|
||||||
echo "==== Dist tree ===="
|
echo "==== Dist tree ===="
|
||||||
ls -R "$GITHUB_WORKSPACE/dist/deb" || true
|
ls -R "$GITHUB_WORKSPACE/dist/deb" || true
|
||||||
|
- name: Upload DEB artifact
|
||||||
- name: Upload DEBs to release
|
uses: actions/upload-artifact@v7
|
||||||
uses: svenstaro/upload-release-action@v2
|
with: { name: release-deb, path: "dist/deb/**/*.deb", if-no-files-found: error }
|
||||||
with:
|
|
||||||
file: dist/deb/**/*.deb
|
|
||||||
tag: ${{ env.RELEASE_TAG }}
|
|
||||||
file_glob: true
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
rpm:
|
rpm:
|
||||||
name: build and release rpm x64 & arm64
|
name: build and release rpm x64 & arm64
|
||||||
if: |
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
|
runs-on: ubuntu-26.04
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
container: registry.access.redhat.com/ubi10/ubi
|
container: registry.access.redhat.com/ubi10/ubi
|
||||||
env:
|
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
|
||||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare tools (Red Hat)
|
- name: Prepare tools (Red Hat)
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
EL_MAJOR="${VERSION_ID%%.*}"
|
EL_MAJOR="${VERSION_ID%%.*}"
|
||||||
echo "EL_MAJOR=${EL_MAJOR}"
|
echo "EL_MAJOR=${EL_MAJOR}"
|
||||||
|
|
||||||
dnf -y makecache || true
|
dnf -y makecache || true
|
||||||
command -v curl >/dev/null || dnf -y install curl ca-certificates
|
command -v curl >/dev/null || dnf -y install curl ca-certificates
|
||||||
|
|
||||||
ARCH="$(uname -m)"
|
ARCH="$(uname -m)"
|
||||||
case "$ARCH" in x86_64|aarch64) ;; *) echo "Unsupported arch: $ARCH"; exit 1 ;; esac
|
case "$ARCH" in x86_64|aarch64) ;; *) echo "Unsupported arch: $ARCH"; exit 1 ;; esac
|
||||||
|
|
||||||
install_epel_from_dir() {
|
install_epel_from_dir() {
|
||||||
local base="$1" rpm
|
local base="$1" rpm
|
||||||
echo "Try: $base"
|
echo "Try: $base"
|
||||||
|
rpm="$({ curl -fsSL "$base/Packages/" 2>/dev/null; curl -fsSL "$base/Packages/e/" 2>/dev/null | sed 's|href="|href="e/|'; } |
|
||||||
rpm="$(
|
sed -n 's/.*href="\([^"]*epel-release-[^"]*\.noarch\.rpm\)".*/\1/p' | sort -V | tail -n1)" || true
|
||||||
{
|
[[ -n "$rpm" ]] || return 1
|
||||||
curl -fsSL "$base/Packages/" 2>/dev/null
|
dnf -y install "$base/Packages/$rpm"
|
||||||
curl -fsSL "$base/Packages/e/" 2>/dev/null | sed 's|href="|href="e/|'
|
|
||||||
} |
|
|
||||||
sed -n 's/.*href="\([^"]*epel-release-[^"]*\.noarch\.rpm\)".*/\1/p' |
|
|
||||||
sort -V | tail -n1
|
|
||||||
)" || true
|
|
||||||
|
|
||||||
if [[ -n "$rpm" ]]; then
|
|
||||||
dnf -y install "$base/Packages/$rpm"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FEDORA="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm"
|
FEDORA="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm"
|
||||||
|
ROCKY="https://dl.rockylinux.org/pub/rocky/${EL_MAJOR}/extras/${ARCH}/os"
|
||||||
|
ALMA="https://repo.almalinux.org/almalinux/${EL_MAJOR}/extras/${ARCH}/os"
|
||||||
|
|
||||||
echo "Try Fedora: $FEDORA"
|
echo "Try Fedora: $FEDORA"
|
||||||
|
curl -fsSLI "$FEDORA" >/dev/null && dnf -y install "$FEDORA" ||
|
||||||
|
install_epel_from_dir "$ROCKY" ||
|
||||||
|
install_epel_from_dir "$ALMA" ||
|
||||||
|
{ echo "EPEL bootstrap failed (Fedora/Rocky/Alma)"; exit 1; }
|
||||||
|
|
||||||
if curl -fsSLI "$FEDORA" >/dev/null; then
|
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which
|
||||||
dnf -y install "$FEDORA"
|
|
||||||
else
|
|
||||||
ROCKY="https://dl.rockylinux.org/pub/rocky/${EL_MAJOR}/extras/${ARCH}/os"
|
|
||||||
if install_epel_from_dir "$ROCKY"; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
ALMA="https://repo.almalinux.org/almalinux/${EL_MAJOR}/extras/${ARCH}/os"
|
|
||||||
if install_epel_from_dir "$ALMA"; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
echo "EPEL bootstrap failed (Fedora/Rocky/Alma)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core \
|
|
||||||
rsync findutils tar gzip unzip which
|
|
||||||
|
|
||||||
dnf repolist | grep -i epel || true
|
dnf repolist | grep -i epel || true
|
||||||
|
|
||||||
- name: Checkout repo (for scripts)
|
- name: Checkout repo (for scripts)
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with: { submodules: 'recursive', fetch-depth: '0' }
|
||||||
submodules: 'recursive'
|
|
||||||
fetch-depth: '0'
|
|
||||||
|
|
||||||
- name: Ensure script permissions
|
- name: Ensure script permissions
|
||||||
run: chmod 755 package-rhel.sh
|
run: chmod 755 package-rhel.sh
|
||||||
|
|
||||||
- name: Package RPM (RHEL-family)
|
- name: Package RPM (RHEL-family)
|
||||||
run: ./package-rhel.sh "${RELEASE_TAG}" --arch all
|
run: ./package-rhel.sh "${RELEASE_TAG}" --arch all
|
||||||
|
|
||||||
- name: Collect RPMs into workspace
|
- name: Collect RPMs into workspace
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/rpm"
|
mkdir -p "$GITHUB_WORKSPACE/dist/rpm"
|
||||||
@@ -170,39 +117,26 @@ jobs:
|
|||||||
find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.aarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true
|
find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.aarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true
|
||||||
echo "==== Dist tree ===="
|
echo "==== Dist tree ===="
|
||||||
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
|
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
|
||||||
|
- name: Upload RPM artifact
|
||||||
- name: Upload RPMs to release
|
uses: actions/upload-artifact@v7
|
||||||
uses: svenstaro/upload-release-action@v2
|
with: { name: release-rpm, path: "dist/rpm/**/*.rpm", if-no-files-found: error }
|
||||||
with:
|
|
||||||
file: dist/rpm/**/*.rpm
|
|
||||||
tag: ${{ env.RELEASE_TAG }}
|
|
||||||
file_glob: true
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
rpm-riscv64:
|
rpm-riscv64:
|
||||||
name: build and release rpm riscv64
|
name: build and release rpm riscv64
|
||||||
if: |
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
|
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: ubuntu-24.04-riscv
|
runs-on: ubuntu-24.04-riscv
|
||||||
container: rockylinux/rockylinux:10
|
container: rockylinux/rockylinux:10
|
||||||
env:
|
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
|
||||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare tools (Red Hat)
|
- name: Prepare tools (Red Hat)
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
dnf -y makecache
|
dnf -y makecache
|
||||||
dnf -y install \
|
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which jq
|
||||||
sudo git rpm-build rpmdevtools dnf-plugins-core \
|
|
||||||
rsync findutils tar gzip unzip which jq
|
|
||||||
|
|
||||||
- name: Checkout repo (for scripts)
|
- name: Checkout repo (for scripts)
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
rm -rf ./*
|
rm -rf ./*
|
||||||
@@ -212,56 +146,27 @@ jobs:
|
|||||||
git fetch --depth=1 origin "${GITHUB_SHA}"
|
git fetch --depth=1 origin "${GITHUB_SHA}"
|
||||||
git checkout FETCH_HEAD
|
git checkout FETCH_HEAD
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
|
|
||||||
- name: Ensure script permissions
|
- name: Ensure script permissions
|
||||||
run: chmod 755 package-rhel-riscv.sh
|
run: chmod 755 package-rhel-riscv.sh
|
||||||
|
|
||||||
- name: Package RPM (RHEL-family)
|
- name: Package RPM (RHEL-family)
|
||||||
run: ./package-rhel-riscv.sh "${RELEASE_TAG}"
|
run: ./package-rhel-riscv.sh "${RELEASE_TAG}"
|
||||||
|
|
||||||
- name: Collect RPMs into workspace
|
- name: Collect RPMs into workspace
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-riscv64"
|
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-riscv64"
|
||||||
rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-riscv64/" || true
|
rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-riscv64/" || true
|
||||||
find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" \
|
find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
|
||||||
-exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
|
|
||||||
echo "==== Dist tree ===="
|
echo "==== Dist tree ===="
|
||||||
ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true
|
ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true
|
||||||
|
- name: Upload RPM artifact
|
||||||
- name: Upload RPMs to release
|
uses: actions/upload-artifact@v7
|
||||||
shell: bash
|
with: { name: release-rpm-riscv64, path: "dist/rpm-riscv64/**/*.rpm", if-no-files-found: error }
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
shopt -s globstar nullglob
|
|
||||||
|
|
||||||
files=(dist/rpm-riscv64/**/*.rpm)
|
|
||||||
(( ${#files[@]} )) || { echo "No RPMs found."; exit 1; }
|
|
||||||
|
|
||||||
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}"
|
|
||||||
upload_url="$(curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$api" | jq -r '.upload_url // empty' | sed 's/{?name,label}//')"
|
|
||||||
[[ "$upload_url" ]] || { echo "Release upload URL not found: ${RELEASE_TAG}"; exit 1; }
|
|
||||||
|
|
||||||
for f in "${files[@]}"; do
|
|
||||||
echo "Uploading ${f##*/}"
|
|
||||||
curl -fsSL -X POST \
|
|
||||||
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
|
||||||
-H "Content-Type: application/x-rpm" \
|
|
||||||
--data-binary @"$f" \
|
|
||||||
"${upload_url}?name=${f##*/}"
|
|
||||||
done
|
|
||||||
|
|
||||||
deb-riscv64:
|
deb-riscv64:
|
||||||
name: build and release deb riscv64
|
name: build and release deb riscv64
|
||||||
if: |
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
|
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: ubuntu-24.04-riscv
|
runs-on: ubuntu-24.04-riscv
|
||||||
container: debian:13
|
container: debian:13
|
||||||
env:
|
env: { RELEASE_TAG: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}" }
|
||||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare tools (Debian)
|
- name: Prepare tools (Debian)
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -269,14 +174,10 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \
|
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1
|
||||||
ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev \
|
|
||||||
gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1
|
|
||||||
|
|
||||||
- name: Checkout repo (for scripts)
|
- name: Checkout repo (for scripts)
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
rm -rf ./*
|
rm -rf ./*
|
||||||
@@ -286,53 +187,25 @@ jobs:
|
|||||||
git fetch --depth=1 origin "${GITHUB_SHA}"
|
git fetch --depth=1 origin "${GITHUB_SHA}"
|
||||||
git checkout FETCH_HEAD
|
git checkout FETCH_HEAD
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
|
|
||||||
- name: Ensure script permissions
|
- name: Ensure script permissions
|
||||||
run: chmod 755 package-debian-riscv.sh
|
run: chmod 755 package-debian-riscv.sh
|
||||||
|
|
||||||
- name: Package DEB (Debian-family)
|
- name: Package DEB (Debian-family)
|
||||||
run: ./package-debian-riscv.sh "${RELEASE_TAG}"
|
run: ./package-debian-riscv.sh "${RELEASE_TAG}"
|
||||||
|
|
||||||
- name: Collect DEBs into workspace
|
- name: Collect DEBs into workspace
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/deb-riscv64"
|
mkdir -p "$GITHUB_WORKSPACE/dist/deb-riscv64"
|
||||||
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-riscv64/" || true
|
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-riscv64/" || true
|
||||||
find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" \
|
find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
|
||||||
-exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
|
|
||||||
echo "==== Dist tree ===="
|
echo "==== Dist tree ===="
|
||||||
ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true
|
ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true
|
||||||
|
- name: Upload DEB artifact
|
||||||
- name: Upload DEBs to release
|
uses: actions/upload-artifact@v7
|
||||||
shell: bash
|
with: { name: release-deb-riscv64, path: "dist/deb-riscv64/**/*.deb", if-no-files-found: error }
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
shopt -s globstar nullglob
|
|
||||||
|
|
||||||
files=(dist/deb-riscv64/**/*.deb)
|
|
||||||
(( ${#files[@]} )) || { echo "No DEBs found."; exit 1; }
|
|
||||||
|
|
||||||
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}"
|
|
||||||
upload_url="$(curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$api" | jq -r '.upload_url // empty' | sed 's/{?name,label}//')"
|
|
||||||
[[ "$upload_url" ]] || { echo "Release upload URL not found: ${RELEASE_TAG}"; exit 1; }
|
|
||||||
|
|
||||||
for f in "${files[@]}"; do
|
|
||||||
echo "Uploading ${f##*/}"
|
|
||||||
curl -fsSL -X POST \
|
|
||||||
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
|
||||||
-H "Content-Type: application/vnd.debian.binary-package" \
|
|
||||||
--data-binary @"$f" \
|
|
||||||
"${upload_url}?name=${f##*/}"
|
|
||||||
done
|
|
||||||
|
|
||||||
deb-loong64:
|
deb-loong64:
|
||||||
name: build and release deb loong64
|
name: build and release deb loong64
|
||||||
if: |
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
|
runs-on: ubuntu-26.04-arm
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
||||||
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
|
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
|
||||||
@@ -341,75 +214,73 @@ jobs:
|
|||||||
QCOW2_IMAGE: debian13-loong64.qcow2
|
QCOW2_IMAGE: debian13-loong64.qcow2
|
||||||
EFI_CODE: edk2-loongarch64-code.fd
|
EFI_CODE: edk2-loongarch64-code.fd
|
||||||
EFI_VARS: edk2-loongarch64-vars.fd
|
EFI_VARS: edk2-loongarch64-vars.fd
|
||||||
QEMU_VERSION: 10.2.2
|
QEMU_VERSION: 10.2.4
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare host tools
|
- name: Prepare host tools
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y rsync qemu-utils expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0
|
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
|
||||||
|
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
|
||||||
- name: Checkout repo
|
virtiofsd --version || virtiofsd --help | head
|
||||||
uses: actions/checkout@v6
|
- name: Host clone repo
|
||||||
with:
|
shell: bash
|
||||||
submodules: recursive
|
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||||
fetch-depth: 0
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rm -rf repo
|
||||||
|
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
|
||||||
- name: Download QEMU prebuild
|
- name: Download QEMU prebuild
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
|
||||||
wget -O qemu-linux-x64.tar.gz \
|
tar -xzf qemu-linux-arm64-v8.2.tar.gz
|
||||||
"https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64.tar.gz"
|
|
||||||
|
|
||||||
tar -xzf qemu-linux-x64.tar.gz
|
|
||||||
|
|
||||||
mkdir -p "$HOME/qemu-install"
|
mkdir -p "$HOME/qemu-install"
|
||||||
|
rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
|
||||||
rsync -a qemu-linux-x64/ "$HOME/qemu-install/"
|
|
||||||
|
|
||||||
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
|
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
|
||||||
|
|
||||||
- name: Download loong64 qcow2 and EFI firmware
|
- name: Download loong64 qcow2 and EFI firmware
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
|
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
|
||||||
wget -O "$EFI_CODE" "$EFI_CODE_URL"
|
wget -O "$EFI_CODE" "$EFI_CODE_URL"
|
||||||
wget -O "$EFI_VARS" "$EFI_VARS_URL"
|
wget -O "$EFI_VARS" "$EFI_VARS_URL"
|
||||||
|
|
||||||
qemu-img info "$QCOW2_IMAGE"
|
qemu-img info "$QCOW2_IMAGE"
|
||||||
|
|
||||||
- name: Build loong64 DEB in VM through serial console
|
- name: Build loong64 DEB in VM through serial console
|
||||||
shell: bash
|
shell: bash
|
||||||
timeout-minutes: 180
|
timeout-minutes: 180
|
||||||
env:
|
env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
|
||||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
|
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
|
||||||
|
|
||||||
|
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-deb-loong64.sock"
|
||||||
|
rm -f "$VIRTIOFSD_SOCKET"
|
||||||
|
|
||||||
|
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
|
||||||
|
|
||||||
|
VIRTIOFSD_PID=$!
|
||||||
|
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
|
||||||
|
|
||||||
|
for i in {1..100}; do
|
||||||
|
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
|
||||||
|
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
|
||||||
|
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
|
||||||
expect <<'EOF'
|
expect <<'EOF'
|
||||||
log_user 1
|
log_user 1
|
||||||
set timeout -1
|
set timeout -1
|
||||||
|
|
||||||
set release_tag $env(RELEASE_TAG)
|
set release_tag $env(RELEASE_TAG)
|
||||||
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
|
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
|
||||||
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
|
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
|
||||||
|
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
|
||||||
set workspace $env(GITHUB_WORKSPACE)
|
|
||||||
set repo $env(GITHUB_REPOSITORY)
|
|
||||||
set sha $env(GITHUB_SHA)
|
|
||||||
|
|
||||||
set qcow2 $env(QCOW2_IMAGE)
|
set qcow2 $env(QCOW2_IMAGE)
|
||||||
set efi_code $env(EFI_CODE)
|
set efi_code $env(EFI_CODE)
|
||||||
set efi_vars $env(EFI_VARS)
|
set efi_vars $env(EFI_VARS)
|
||||||
|
|
||||||
proc wait_prompt {} {
|
proc wait_prompt {} {
|
||||||
expect {
|
expect {
|
||||||
-re "__CI_PROMPT__# " {}
|
-re "__CI_PROMPT__# " {}
|
||||||
@@ -417,86 +288,258 @@ jobs:
|
|||||||
eof { exit 1 }
|
eof { exit 1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proc run_cmd {cmd} {
|
proc run_cmd {cmd} {
|
||||||
send -- "$cmd\r"
|
send -- "$cmd\r"
|
||||||
wait_prompt
|
wait_prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn $qemu_bin \
|
spawn $qemu_bin \
|
||||||
-L $qemu_rom_dir \
|
-L $qemu_rom_dir \
|
||||||
-machine virt \
|
-machine virt \
|
||||||
-accel tcg,thread=multi,tb-size=2048 \
|
-accel tcg,thread=multi,tb-size=2048 \
|
||||||
-cpu la464 \
|
-cpu la464 \
|
||||||
-m 9216 \
|
-m 9216 \
|
||||||
|
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
|
||||||
|
-numa node,memdev=mem0 \
|
||||||
-smp 4 \
|
-smp 4 \
|
||||||
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
|
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
|
||||||
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
|
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
|
||||||
-device virtio-blk-pci,drive=hd0,bootindex=1 \
|
-device virtio-blk-pci,drive=hd0,bootindex=1 \
|
||||||
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
|
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
|
||||||
-netdev user,id=net0 \
|
-netdev user,id=net0 \
|
||||||
-device virtio-net-pci,netdev=net0 \
|
-device virtio-net-pci-non-transitional,netdev=net0 \
|
||||||
-virtfs local,path=$workspace,mount_tag=workspace,security_model=none,id=workspace \
|
-chardev socket,id=charfs0,path=$virtiofs_socket \
|
||||||
|
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
|
||||||
-display none \
|
-display none \
|
||||||
-serial stdio \
|
-serial stdio \
|
||||||
-monitor none
|
-monitor none
|
||||||
|
|
||||||
expect -re "login:|debian login:"
|
expect -re "login:|debian login:"
|
||||||
send -- "root\r"
|
send -- "root\r"
|
||||||
|
|
||||||
expect -re "Password:|密码:|密码:"
|
expect -re "Password:|密码:|密码:"
|
||||||
send -- "password\r"
|
send -- "password\r"
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
-re "# " {}
|
-re "# " {}
|
||||||
timeout { exit 1 }
|
timeout { exit 1 }
|
||||||
eof { exit 1 }
|
eof { exit 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
|
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
|
||||||
wait_prompt
|
wait_prompt
|
||||||
|
|
||||||
run_cmd "mkdir -p /workspace"
|
run_cmd "mkdir -p /workspace"
|
||||||
run_cmd "mount -t 9p -o trans=virtio,version=9p2000.L workspace /workspace || mount -t 9p -o trans=virtio workspace /workspace"
|
run_cmd "mount -t virtiofs workspace /workspace"
|
||||||
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
|
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
|
||||||
run_cmd "dhclient \$IFACE || true"
|
run_cmd "dhclient \$IFACE || true"
|
||||||
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
|
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
|
||||||
run_cmd "apt-get update || apt-get update || apt-get update"
|
run_cmd "apt-get update || apt-get update || apt-get update"
|
||||||
run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1"
|
run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 fuse3"
|
||||||
run_cmd "rm -rf /root/v2rayN-src"
|
run_cmd "rm -rf /root/v2rayN-src"
|
||||||
run_cmd "git clone --recursive https://github.com/$repo.git /root/v2rayN-src"
|
run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
|
||||||
run_cmd "cd /root/v2rayN-src && git fetch --depth=1 origin $sha && git checkout FETCH_HEAD && git submodule update --init --recursive"
|
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
|
||||||
run_cmd "cd /root/v2rayN-src && chmod 755 package-debian-loong.sh"
|
run_cmd "cd /root/v2rayN-src && chmod 755 package-debian-loong.sh"
|
||||||
|
|
||||||
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-debian-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/deb-loong64\ncp -av /root/debbuild/*.deb /workspace/dist/deb-loong64/ 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-build.sh\r"
|
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-debian-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/deb-loong64\ncp -av /root/debbuild/*.deb /workspace/dist/deb-loong64/ 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-build.sh\r"
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
-re "__BUILD_DONE__0" {
|
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
|
||||||
send -- "poweroff\r"
|
default { exit 1 }
|
||||||
}
|
|
||||||
default {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Collect DEBs
|
- name: Collect DEBs
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
|
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
|
||||||
|
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
|
||||||
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" \
|
|
||||||
-exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
|
|
||||||
|
|
||||||
echo "==== Dist tree ===="
|
echo "==== Dist tree ===="
|
||||||
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
|
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
|
||||||
|
- name: Upload DEB artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with: { name: release-deb-loong64, path: "dist/deb-loong64/**/*.deb", if-no-files-found: error }
|
||||||
|
|
||||||
- name: Upload DEBs to release
|
rpm-loong64:
|
||||||
uses: svenstaro/upload-release-action@v2
|
name: build and release rpm loong64
|
||||||
with:
|
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
file: dist/deb-loong64/**/*.deb
|
runs-on: ubuntu-26.04-arm
|
||||||
tag: ${{ env.RELEASE_TAG }}
|
env:
|
||||||
file_glob: true
|
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
||||||
prerelease: true
|
QCOW2_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/fedora43-loong64.qcow2
|
||||||
|
EFI_CODE_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-code.fd
|
||||||
|
EFI_VARS_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-vars.fd
|
||||||
|
QCOW2_IMAGE: fedora43-loong64.qcow2
|
||||||
|
EFI_CODE: edk2-loongarch64-code.fd
|
||||||
|
EFI_VARS: edk2-loongarch64-vars.fd
|
||||||
|
QEMU_VERSION: 10.2.4
|
||||||
|
steps:
|
||||||
|
- name: Prepare host tools
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
|
||||||
|
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
|
||||||
|
virtiofsd --version || virtiofsd --help | head
|
||||||
|
- name: Host clone repo
|
||||||
|
shell: bash
|
||||||
|
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rm -rf repo
|
||||||
|
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
|
||||||
|
- name: Download QEMU prebuild
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
|
||||||
|
tar -xzf qemu-linux-arm64-v8.2.tar.gz
|
||||||
|
mkdir -p "$HOME/qemu-install"
|
||||||
|
rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
|
||||||
|
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
|
||||||
|
- name: Download loong64 qcow2 and EFI firmware
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
|
||||||
|
wget -O "$EFI_CODE" "$EFI_CODE_URL"
|
||||||
|
wget -O "$EFI_VARS" "$EFI_VARS_URL"
|
||||||
|
qemu-img info "$QCOW2_IMAGE"
|
||||||
|
- name: Build loong64 RPM in VM through serial console
|
||||||
|
shell: bash
|
||||||
|
timeout-minutes: 180
|
||||||
|
env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
|
||||||
|
|
||||||
|
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-rpm-loong64.sock"
|
||||||
|
rm -f "$VIRTIOFSD_SOCKET"
|
||||||
|
|
||||||
|
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
|
||||||
|
|
||||||
|
VIRTIOFSD_PID=$!
|
||||||
|
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
|
||||||
|
|
||||||
|
for i in {1..100}; do
|
||||||
|
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
|
||||||
|
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
|
||||||
|
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
|
||||||
|
expect <<'EOF'
|
||||||
|
log_user 1
|
||||||
|
set timeout -1
|
||||||
|
set release_tag $env(RELEASE_TAG)
|
||||||
|
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
|
||||||
|
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
|
||||||
|
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
|
||||||
|
set qcow2 $env(QCOW2_IMAGE)
|
||||||
|
set efi_code $env(EFI_CODE)
|
||||||
|
set efi_vars $env(EFI_VARS)
|
||||||
|
proc wait_prompt {} {
|
||||||
|
expect {
|
||||||
|
-re "__CI_PROMPT__# " {}
|
||||||
|
timeout { exit 1 }
|
||||||
|
eof { exit 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proc run_cmd {cmd} {
|
||||||
|
send -- "$cmd\r"
|
||||||
|
wait_prompt
|
||||||
|
}
|
||||||
|
spawn $qemu_bin \
|
||||||
|
-L $qemu_rom_dir \
|
||||||
|
-machine virt \
|
||||||
|
-accel tcg,thread=multi,tb-size=2048 \
|
||||||
|
-cpu la464 \
|
||||||
|
-m 9216 \
|
||||||
|
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
|
||||||
|
-numa node,memdev=mem0 \
|
||||||
|
-smp 4 \
|
||||||
|
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
|
||||||
|
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
|
||||||
|
-device virtio-blk-pci,drive=hd0,bootindex=1 \
|
||||||
|
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
|
||||||
|
-netdev user,id=net0 \
|
||||||
|
-device virtio-net-pci-non-transitional,netdev=net0 \
|
||||||
|
-chardev socket,id=charfs0,path=$virtiofs_socket \
|
||||||
|
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
|
||||||
|
-display none \
|
||||||
|
-serial stdio \
|
||||||
|
-monitor none
|
||||||
|
expect -re "login:|fedora login:"
|
||||||
|
send -- "root\r"
|
||||||
|
expect -re "Password:|密码:|密码:"
|
||||||
|
send -- "password\r"
|
||||||
|
expect {
|
||||||
|
-re "# " {}
|
||||||
|
timeout { exit 1 }
|
||||||
|
eof { exit 1 }
|
||||||
|
}
|
||||||
|
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
|
||||||
|
wait_prompt
|
||||||
|
run_cmd "mkdir -p /workspace"
|
||||||
|
run_cmd "mount -t virtiofs workspace /workspace"
|
||||||
|
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
|
||||||
|
run_cmd "dhclient \$IFACE || true"
|
||||||
|
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
|
||||||
|
run_cmd "dnf -y makecache || dnf -y makecache || dnf -y makecache"
|
||||||
|
run_cmd "dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which curl jq file ca-certificates fuse3 --nogpgcheck"
|
||||||
|
run_cmd "rm -rf /root/v2rayN-src"
|
||||||
|
run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
|
||||||
|
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
|
||||||
|
run_cmd "cd /root/v2rayN-src && chmod 755 package-rhel-loong.sh"
|
||||||
|
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-rpm-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-rhel-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/rpm-loong64\nfind /root/rpmbuild/RPMS -name '*.rpm' -exec cp -av {} /workspace/dist/rpm-loong64/ \\; 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-rpm-build.sh\r"
|
||||||
|
expect {
|
||||||
|
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
|
||||||
|
default { exit 1 }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
- name: Collect RPMs
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
|
||||||
|
find "$GITHUB_WORKSPACE/dist/rpm-loong64" -name "*.loongarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-loong64/v2rayN-linux-rhel-loong64.rpm" \; || true
|
||||||
|
echo "==== Dist tree ===="
|
||||||
|
ls -R "$GITHUB_WORKSPACE/dist/rpm-loong64"
|
||||||
|
- name: Upload RPM artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with: { name: release-rpm-loong64, path: "dist/rpm-loong64/**/*.rpm", if-no-files-found: error }
|
||||||
|
|
||||||
|
release-deb:
|
||||||
|
name: sign and upload deb x64 arm64
|
||||||
|
needs: deb
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb, file_pattern: "*.deb" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-rpm:
|
||||||
|
name: sign and upload rpm x64 arm64
|
||||||
|
needs: rpm
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm, file_pattern: "*.rpm" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-rpm-riscv64:
|
||||||
|
name: sign and upload rpm riscv64
|
||||||
|
needs: rpm-riscv64
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm-riscv64, file_pattern: "*.rpm" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-deb-riscv64:
|
||||||
|
name: sign and upload deb riscv64
|
||||||
|
needs: deb-riscv64
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb-riscv64, file_pattern: "*.deb" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-deb-loong64:
|
||||||
|
name: sign and upload deb loong64
|
||||||
|
needs: deb-loong64
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-deb-loong64, file_pattern: "*.deb" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-rpm-loong64:
|
||||||
|
name: sign and upload rpm loong64
|
||||||
|
needs: rpm-loong64
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with: { release_tag: "${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}", artifact_name: release-rpm-loong64, file_pattern: "*.rpm" }
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|||||||
33
.github/workflows/build-osx.yml
vendored
33
.github/workflows/build-osx.yml
vendored
@@ -12,6 +12,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -26,6 +27,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
target: macos
|
target: macos
|
||||||
release_tag: ${{ inputs.release_tag }}
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
dmg:
|
dmg:
|
||||||
name: package and release macOS dmg
|
name: package and release macOS dmg
|
||||||
@@ -45,7 +47,7 @@ jobs:
|
|||||||
}}
|
}}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Restore build artifacts
|
- name: Restore build artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
@@ -62,13 +64,24 @@ jobs:
|
|||||||
- name: Package dmg
|
- name: Package dmg
|
||||||
run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }}
|
run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }}
|
||||||
|
|
||||||
- name: Sleep for race condition between matrix jobs
|
- name: Upload dmg artifact
|
||||||
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')"
|
uses: actions/upload-artifact@v7
|
||||||
|
|
||||||
- name: Upload dmg to release
|
|
||||||
uses: svenstaro/upload-release-action@v2
|
|
||||||
with:
|
with:
|
||||||
file: ${{ github.workspace }}/v2rayN*.dmg
|
name: release-dmg-${{ matrix.arch }}
|
||||||
tag: ${{ inputs.release_tag }}
|
path: v2rayN*.dmg
|
||||||
file_glob: true
|
if-no-files-found: error
|
||||||
prerelease: true
|
|
||||||
|
release-dmg:
|
||||||
|
name: sign and upload macOS dmg
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
needs: dmg
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: [ x64, arm64 ]
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
artifact_name: release-dmg-${{ matrix.arch }}
|
||||||
|
file_pattern: '*.dmg'
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
|||||||
2
.github/workflows/build-windows-desktop.yml
vendored
2
.github/workflows/build-windows-desktop.yml
vendored
@@ -12,6 +12,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -26,3 +27,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
target: windows-desktop
|
target: windows-desktop
|
||||||
release_tag: ${{ inputs.release_tag }}
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets: inherit
|
||||||
|
|||||||
247
.github/workflows/build-windows-x86.yml
vendored
Normal file
247
.github/workflows/build-windows-x86.yml
vendored
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
name: release Windows x86
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package-x86:
|
||||||
|
name: build and release windows x86 WPF & Avalonia
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- flavor: wpf
|
||||||
|
project: ./v2rayN/v2rayN/v2rayN.csproj
|
||||||
|
zip_name: v2rayN-windows-86.zip
|
||||||
|
- flavor: desktop
|
||||||
|
project: ./v2rayN/v2rayN.Desktop/v2rayN.Desktop.csproj
|
||||||
|
zip_name: v2rayN-windows-86-desktop.zip
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Purge SDKs
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
echo "Before:"
|
||||||
|
dotnet --list-sdks
|
||||||
|
|
||||||
|
dotnet --list-sdks | % {
|
||||||
|
$_ -match '^\S+\s+\[(.+)\]$' > $null
|
||||||
|
$d = Join-Path $Matches[1] $_.Split()[0]
|
||||||
|
Write-Host "Removing $d"
|
||||||
|
if ($IsWindows) { rm $d -r -fo } else { sudo rm -rf "$d" }
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "After:"
|
||||||
|
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
|
||||||
|
|
||||||
|
- name: Setup .NET 10.0.1xx
|
||||||
|
uses: actions/setup-dotnet@v6.0.0
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.1xx
|
||||||
|
|
||||||
|
- name: Publish ${{ matrix.flavor }} win-x86
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
dotnet restore "${{ matrix.project }}" -r win-x86
|
||||||
|
|
||||||
|
dotnet publish "${{ matrix.project }}" `
|
||||||
|
-c Release `
|
||||||
|
-r win-x86 `
|
||||||
|
-p:SelfContained=true `
|
||||||
|
-p:EnableWindowsTargeting=true `
|
||||||
|
-o "publish/${{ matrix.flavor }}"
|
||||||
|
|
||||||
|
- name: Publish AmazTool win-x86
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
dotnet publish "./v2rayN/AmazTool/AmazTool.csproj" `
|
||||||
|
-c Release `
|
||||||
|
-r win-x86 `
|
||||||
|
-p:SelfContained=true `
|
||||||
|
-p:PublishTrimmed=true `
|
||||||
|
-p:EnableWindowsTargeting=true `
|
||||||
|
-o "publish/${{ matrix.flavor }}"
|
||||||
|
|
||||||
|
- name: Detect bundled core versions
|
||||||
|
id: detect-core
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path "tmp/probe" | Out-Null
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-windows-64/bin/xray/xray.exe" -OutFile "tmp/probe/xray.exe"
|
||||||
|
$xrayOutput = (& "tmp/probe/xray.exe" version) -join "`n"
|
||||||
|
Write-Host $xrayOutput
|
||||||
|
|
||||||
|
$xrayMatch = [regex]::Match($xrayOutput, 'Xray\s+([0-9.]+)')
|
||||||
|
if ($xrayMatch.Success) {
|
||||||
|
$xrayVer = $xrayMatch.Groups[1].Value
|
||||||
|
} else {
|
||||||
|
throw "Failed to detect Xray version"
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-windows-64/bin/sing_box/sing-box.exe" -OutFile "tmp/probe/sing-box.exe"
|
||||||
|
$singOutput = (& "tmp/probe/sing-box.exe" version) -join "`n"
|
||||||
|
Write-Host $singOutput
|
||||||
|
|
||||||
|
$singMatch = [regex]::Match($singOutput, 'sing-box version\s+([0-9.]+)')
|
||||||
|
if ($singMatch.Success) {
|
||||||
|
$singVer = $singMatch.Groups[1].Value
|
||||||
|
} else {
|
||||||
|
throw "Failed to detect sing-box version"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Bundled Xray version: $xrayVer"
|
||||||
|
Write-Host "Bundled sing-box version: $singVer"
|
||||||
|
|
||||||
|
"xray_version=$xrayVer" >> $env:GITHUB_OUTPUT
|
||||||
|
"sing_version=$singVer" >> $env:GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Download x86 cores and rule assets
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$headers = @{
|
||||||
|
Authorization = "Bearer $env:GH_TOKEN"
|
||||||
|
Accept = "application/vnd.github+json"
|
||||||
|
"X-GitHub-Api-Version" = "2022-11-28"
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = "publish/${{ matrix.flavor }}"
|
||||||
|
$bin = Join-Path $root "bin"
|
||||||
|
$xrayDir = Join-Path $bin "xray"
|
||||||
|
$singDir = Join-Path $bin "sing_box"
|
||||||
|
$srssDir = Join-Path $bin "srss"
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $xrayDir, $singDir, $srssDir | Out-Null
|
||||||
|
|
||||||
|
$xrayVer = "${{ steps.detect-core.outputs.xray_version }}"
|
||||||
|
$singVer = "${{ steps.detect-core.outputs.sing_version }}"
|
||||||
|
|
||||||
|
$xrayTag = "v$xrayVer"
|
||||||
|
$singTag = "v$singVer"
|
||||||
|
|
||||||
|
$xrayUrl = "https://github.com/autorepobot/Xray/releases/download/$xrayTag/Xray-windows-32.zip"
|
||||||
|
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
|
||||||
|
|
||||||
|
Write-Host "Bundled Xray version: $xrayVer"
|
||||||
|
Write-Host "Bundled sing-box version: $singVer"
|
||||||
|
Write-Host "Xray: $xrayUrl"
|
||||||
|
Write-Host "sing-box: $singUrl"
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri $xrayUrl -OutFile "tmp/xray.zip"
|
||||||
|
Expand-Archive -Force "tmp/xray.zip" "tmp/xray"
|
||||||
|
|
||||||
|
Get-ChildItem "tmp/xray" -Recurse -Include "*.exe","*.dll" | ForEach-Object {
|
||||||
|
Copy-Item $_.FullName (Join-Path $xrayDir $_.Name) -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri $singUrl -OutFile "tmp/sing-box.zip"
|
||||||
|
Expand-Archive -Force "tmp/sing-box.zip" "tmp/sing-box"
|
||||||
|
|
||||||
|
$singExe = Get-ChildItem "tmp/sing-box" -Recurse -Filter "sing-box.exe" | Select-Object -First 1
|
||||||
|
if (-not $singExe) {
|
||||||
|
throw "sing-box.exe not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item $singExe.FullName "$singDir/sing-box.exe" -Force
|
||||||
|
|
||||||
|
$cronet = Get-ChildItem "tmp/sing-box" -Recurse -Filter "libcronet*.dll" | Select-Object -First 1
|
||||||
|
if ($cronet) {
|
||||||
|
Copy-Item $cronet.FullName "$singDir/libcronet.dll" -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "$bin/geosite.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "$bin/geoip.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat" -OutFile "$bin/geoip-only-cn-private.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb" -OutFile "$bin/Country.mmdb"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" -OutFile "$bin/geoip.metadb"
|
||||||
|
|
||||||
|
$geoipRules = @(
|
||||||
|
"geoip-private.srs",
|
||||||
|
"geoip-cn.srs",
|
||||||
|
"geoip-facebook.srs",
|
||||||
|
"geoip-fastly.srs",
|
||||||
|
"geoip-google.srs",
|
||||||
|
"geoip-netflix.srs",
|
||||||
|
"geoip-telegram.srs",
|
||||||
|
"geoip-twitter.srs"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($f in $geoipRules) {
|
||||||
|
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geoip/$f" -OutFile "$srssDir/$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
$geositeRules = @(
|
||||||
|
"geosite-cn.srs",
|
||||||
|
"geosite-gfw.srs",
|
||||||
|
"geosite-google.srs",
|
||||||
|
"geosite-greatfire.srs",
|
||||||
|
"geosite-geolocation-cn.srs",
|
||||||
|
"geosite-category-ads-all.srs",
|
||||||
|
"geosite-private.srs"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($f in $geositeRules) {
|
||||||
|
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geosite/$f" -OutFile "$srssDir/$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Package zip
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
Compress-Archive `
|
||||||
|
-Path "publish/${{ matrix.flavor }}/*" `
|
||||||
|
-DestinationPath "${{ matrix.zip_name }}" `
|
||||||
|
-Force
|
||||||
|
|
||||||
|
- name: Upload zip artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: release-windows-x86-${{ matrix.flavor }}
|
||||||
|
path: ${{ matrix.zip_name }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
release-x86:
|
||||||
|
name: sign and upload windows x86
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
needs: package-x86
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
flavor: [ wpf, desktop ]
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
artifact_name: release-windows-x86-${{ matrix.flavor }}
|
||||||
|
file_pattern: '*.zip'
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
2
.github/workflows/build-windows.yml
vendored
2
.github/workflows/build-windows.yml
vendored
@@ -12,6 +12,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -27,3 +28,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
target: windows
|
target: windows
|
||||||
release_tag: ${{ inputs.release_tag }}
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets: inherit
|
||||||
|
|||||||
30
.github/workflows/build.yml
vendored
30
.github/workflows/build.yml
vendored
@@ -22,6 +22,7 @@ jobs:
|
|||||||
case(
|
case(
|
||||||
inputs.target == 'macos', 'macos-latest',
|
inputs.target == 'macos', 'macos-latest',
|
||||||
inputs.target == 'linux', 'ubuntu-24.04',
|
inputs.target == 'linux', 'ubuntu-24.04',
|
||||||
|
inputs.target == 'windows', 'windows-latest',
|
||||||
'ubuntu-latest'
|
'ubuntu-latest'
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -46,26 +47,49 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
|
- name: Purge SDKs
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
echo "Before:"
|
||||||
|
dotnet --list-sdks
|
||||||
|
|
||||||
|
dotnet --list-sdks | % {
|
||||||
|
$_ -match '^\S+\s+\[(.+)\]$' > $null
|
||||||
|
$d = Join-Path $Matches[1] $_.Split()[0]
|
||||||
|
Write-Host "Removing $d"
|
||||||
|
if ($IsWindows) { rm $d -r -fo } else { sudo rm -rf "$d" }
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "After:"
|
||||||
|
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v5.3.0
|
uses: actions/setup-dotnet@v6.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '10.0.1xx'
|
dotnet-version: '10.0.1xx'
|
||||||
|
|
||||||
- name: Build v2rayN
|
- name: Build v2rayN
|
||||||
|
shell: bash
|
||||||
working-directory: ./v2rayN
|
working-directory: ./v2rayN
|
||||||
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
|
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
|
||||||
|
|
||||||
- name: Build AmazTool
|
- name: Build AmazTool
|
||||||
|
shell: bash
|
||||||
working-directory: ./v2rayN
|
working-directory: ./v2rayN
|
||||||
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
|
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
|
||||||
|
|
||||||
|
- name: Remove Debug Symbols
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
find ${{ matrix.arch }} -type f -name '*.pdb' -delete
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v7.0.1
|
uses: actions/upload-artifact@v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.arch }}
|
name: ${{ matrix.arch }}
|
||||||
path: ${{ matrix.arch }}
|
path: ${{ matrix.arch }}
|
||||||
|
|||||||
34
.github/workflows/package-zip.yml
vendored
34
.github/workflows/package-zip.yml
vendored
@@ -9,9 +9,13 @@ on:
|
|||||||
target: # windows linux macos windows-desktop
|
target: # windows linux macos windows-desktop
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY:
|
||||||
|
required: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
package:
|
package:
|
||||||
@@ -37,7 +41,7 @@ jobs:
|
|||||||
}}
|
}}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Restore build artifacts
|
- name: Restore build artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
@@ -55,13 +59,23 @@ jobs:
|
|||||||
if: inputs.target == 'windows-desktop'
|
if: inputs.target == 'windows-desktop'
|
||||||
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
|
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
|
||||||
|
|
||||||
- name: Sleep for race condition between matrix jobs
|
- name: Upload zip artifact
|
||||||
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')"
|
uses: actions/upload-artifact@v7
|
||||||
|
|
||||||
- name: Upload zip archive to release
|
|
||||||
uses: svenstaro/upload-release-action@v2
|
|
||||||
with:
|
with:
|
||||||
file: ${{ github.workspace }}/v2rayN*.zip
|
name: release-${{ inputs.target }}-${{ matrix.arch }}
|
||||||
tag: ${{ inputs.release_tag }}
|
path: v2rayN*.zip
|
||||||
file_glob: true
|
if-no-files-found: error
|
||||||
prerelease: true
|
|
||||||
|
release:
|
||||||
|
name: sign and upload zip x64 arm64
|
||||||
|
needs: package
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: [ x64, arm64 ]
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
artifact_name: release-${{ inputs.target }}-${{ matrix.arch }}
|
||||||
|
file_pattern: '*.zip'
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
|||||||
48
.github/workflows/pub-key.yml
vendored
Normal file
48
.github/workflows/pub-key.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: publish GPG public key
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY:
|
||||||
|
required: true
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
steps:
|
||||||
|
- name: Import GPG private key
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
install -m 700 -d ~/.gnupg
|
||||||
|
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||||
|
|
||||||
|
- name: Export GPG public key
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
fingerprint="$(gpg --batch --list-secret-keys --with-colons | awk -F: '$1 == "fpr" { print $10; exit }')"
|
||||||
|
[[ -n "$fingerprint" ]] || { echo "No GPG secret key found."; exit 1; }
|
||||||
|
gpg --batch --armor --export "$fingerprint" > v2rayN-public-key.asc
|
||||||
|
|
||||||
|
- name: Upload GPG public key to release
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
file: v2rayN-public-key.asc
|
||||||
|
tag: ${{ inputs.release_tag }}
|
||||||
|
prerelease: true
|
||||||
|
overwrite: true
|
||||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -14,13 +14,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v5.3.0
|
uses: actions/setup-dotnet@v6.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
|||||||
76
.github/workflows/upload-sign.yml
vendored
Normal file
76
.github/workflows/upload-sign.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: sign and upload release files
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
artifact_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
file_pattern:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: '**/*'
|
||||||
|
content_type:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: application/octet-stream
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upload:
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
steps:
|
||||||
|
- name: Download release artifact
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact_name }}
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Import GPG private key
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
install -m 700 -d ~/.gnupg
|
||||||
|
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||||
|
|
||||||
|
- name: Sign release files
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cd dist
|
||||||
|
shopt -s globstar nullglob
|
||||||
|
files=( ${{ inputs.file_pattern }} )
|
||||||
|
(( ${#files[@]} )) || { echo "No files matched: ${{ inputs.file_pattern }}"; exit 1; }
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
gpg --batch --yes --armor --detach-sign --output "$f.sig" "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Upload release files
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
file: dist/${{ inputs.file_pattern }}
|
||||||
|
tag: ${{ inputs.release_tag }}
|
||||||
|
file_glob: true
|
||||||
|
prerelease: true
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
|
- name: Upload release signatures
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
file: dist/${{ inputs.file_pattern }}.sig
|
||||||
|
tag: ${{ inputs.release_tag }}
|
||||||
|
file_glob: true
|
||||||
|
prerelease: true
|
||||||
|
overwrite: true
|
||||||
9
.github/workflows/winget-publish.yml
vendored
9
.github/workflows/winget-publish.yml
vendored
@@ -23,8 +23,13 @@ jobs:
|
|||||||
|
|
||||||
$targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1
|
$targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1
|
||||||
|
|
||||||
$x64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-64\.zip' | Select -ExpandProperty browser_download_url
|
$assets = $targetRelease | Select -ExpandProperty assets -First 1
|
||||||
$arm64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-arm64\.zip' | Select -ExpandProperty browser_download_url
|
$x64InstallerUrl = $assets | Where-Object { $_.name -eq 'v2rayN-windows-64.zip' } | Select -ExpandProperty browser_download_url
|
||||||
|
$arm64InstallerUrl = $assets | Where-Object { $_.name -eq 'v2rayN-windows-arm64.zip' } | Select -ExpandProperty browser_download_url
|
||||||
|
|
||||||
|
if (-not $x64InstallerUrl -or -not $arm64InstallerUrl) {
|
||||||
|
throw "Could not find required installers in release assets."
|
||||||
|
}
|
||||||
|
|
||||||
$ver = $targetRelease.tag_name
|
$ver = $targetRelease.tag_name
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -84,6 +84,9 @@ StyleCopReport.xml
|
|||||||
*.rsp
|
*.rsp
|
||||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||||
!Directory.Build.rsp
|
!Directory.Build.rsp
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
build/
|
||||||
*.sbr
|
*.sbr
|
||||||
*.tlb
|
*.tlb
|
||||||
*.tli
|
*.tli
|
||||||
|
|||||||
82
README.md
82
README.md
@@ -1,18 +1,80 @@
|
|||||||
# v2rayN
|
# v2rayN
|
||||||
|
|
||||||
A GUI client for Windows, Linux and macOS, support [Xray](https://github.com/XTLS/Xray-core)
|
### A GUI client for Windows, Linux and macOS. Support [Xray](https://github.com/XTLS/Xray-core) and [sing-box](https://github.com/SagerNet/sing-box) and [others](https://github.com/2dust/v2rayN/wiki/List-of-supported-cores)
|
||||||
and [sing-box](https://github.com/SagerNet/sing-box)
|
|
||||||
and [others](https://github.com/2dust/v2rayN/wiki/List-of-supported-cores)
|
|
||||||
|
|
||||||
[](https://github.com/2dust/v2rayN/commits/master)
|
|
||||||
[](https://www.codefactor.io/repository/github/2dust/v2rayn)
|
[](https://www.codefactor.io/repository/github/2dust/v2rayn)
|
||||||
[](https://github.com/2dust/v2rayN/releases)
|
[](https://github.com/2dust/v2rayN/releases)
|
||||||
[](https://t.me/v2rayn)
|
[](https://github.com/2dust/v2rayN/releases)
|
||||||
|
[](https://t.me/v2rayn)
|
||||||
|
|
||||||
|
[](https://github.com/2dust/v2rayN)
|
||||||
|
[](https://github.com/2dust/v2rayN)
|
||||||
|
[](https://github.com/2dust/v2rayN)
|
||||||
|
[](https://github.com/2dust/v2rayN)
|
||||||
|
|
||||||
## How to use
|
|
||||||
|
|
||||||
Read the [Wiki](https://github.com/2dust/v2rayN/wiki) for details.
|
---
|
||||||
|
|
||||||
## Telegram Channel
|
## Download / 下载
|
||||||
|
|
||||||
[github_2dust](https://t.me/github_2dust)
|
Download the latest release here:
|
||||||
|
|
||||||
|
在这里下载最新版本:
|
||||||
|
|
||||||
|
[https://github.com/2dust/v2rayN/releases](https://github.com/2dust/v2rayN/releases)
|
||||||
|
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> v2rayN is the desktop version. For the mobile version, please visit the v2rayNG \
|
||||||
|
> v2rayN 是电脑版,手机版请访问 v2rayNG
|
||||||
|
>
|
||||||
|
> https://github.com/2dust/v2rayNG
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation / 使用文档
|
||||||
|
|
||||||
|
Read the Wiki for usage guides and configuration details.
|
||||||
|
|
||||||
|
请阅读 Wiki 获取使用说明和配置教程。
|
||||||
|
|
||||||
|
[https://github.com/2dust/v2rayN/wiki](https://github.com/2dust/v2rayN/wiki)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported Platforms / 支持平台
|
||||||
|
|
||||||
|
| Platform / 平台 | x64 | x86 | arm64 | riscv64 | loong64 |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| Windows | ✅ | ✅ | ✅ | - | - |
|
||||||
|
| Linux | ✅ | - | ✅ | ✅ | ✅ |
|
||||||
|
| macOS | ✅ | - | ✅ | - | - |
|
||||||
|
|
||||||
|
Minimum OS requirements: [Release files introduction](https://github.com/2dust/v2rayN/wiki/Release-files-introduction) / 最低系统要求:[发布文件介绍](https://github.com/2dust/v2rayN/wiki/Release-files-introduction)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GPG Verification / GPG 签名校验
|
||||||
|
|
||||||
|
Release files are signed with GPG to verify authenticity and integrity, helping prevent mirror, ISP, or CDN hijacking.
|
||||||
|
|
||||||
|
发布文件已使用 GPG 签名,可用于校验文件真实性与完整性,预防镜像站、运营商或 CDN 劫持。
|
||||||
|
|
||||||
|
### Fingerprint / 公钥指纹
|
||||||
|
|
||||||
|
```text
|
||||||
|
7694 5E9F 3E9A 168F 8070 F195 805D 661C
|
||||||
|
134D FAF6 8903 C199 463C 31E5 AE90 3AE0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Community / 社区
|
||||||
|
|
||||||
|
Telegram Group / Telegram 群组:
|
||||||
|
|
||||||
|
[https://t.me/v2rayN](https://t.me/v2rayN)
|
||||||
|
|
||||||
|
Telegram Channel / Telegram 频道:
|
||||||
|
|
||||||
|
[https://t.me/github_2dust](https://t.me/github_2dust)
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ BUILD_FROM=""
|
|||||||
XRAY_VER="${XRAY_VER:-}"
|
XRAY_VER="${XRAY_VER:-}"
|
||||||
SING_VER="${SING_VER:-}"
|
SING_VER="${SING_VER:-}"
|
||||||
|
|
||||||
MIN_KERNEL="5.10"
|
MIN_KERNEL="6.12"
|
||||||
PKGROOT="v2rayN-publish"
|
PKGROOT="v2rayN-publish"
|
||||||
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||||
OUTPUT_DIR="${HOME}/debbuild"
|
OUTPUT_DIR="${HOME}/debbuild"
|
||||||
DOTNET_TFM="net10.0"
|
DOTNET_TFM="net10.0"
|
||||||
DOTNET_LOONGARCH_VERSION="10.0.108"
|
DOTNET_LOONGARCH_VERSION="10.0.110"
|
||||||
DOTNET_LOONGARCH_TAG="v10.0.108-loongarch64"
|
DOTNET_LOONGARCH_TAG="v10.0.110-loongarch64"
|
||||||
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
|
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
|
||||||
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
|
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
|
||||||
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
|
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
|
||||||
@@ -116,20 +116,7 @@ install_dependencies() {
|
|||||||
|
|
||||||
export PATH="$HOME/.dotnet:$PATH"
|
export PATH="$HOME/.dotnet:$PATH"
|
||||||
export DOTNET_ROOT="$HOME/.dotnet"
|
export DOTNET_ROOT="$HOME/.dotnet"
|
||||||
|
|
||||||
mkdir -p "$HOME/.nuget/NuGet"
|
|
||||||
|
|
||||||
cat > "$HOME/.nuget/NuGet/NuGet.Config" <<EOF
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<configuration>
|
|
||||||
<packageSources>
|
|
||||||
<clear />
|
|
||||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
|
||||||
<add key="loongnix" value="https://lnuget.loongnix.cn/v3/index.json" allowInsecureConnections="true" />
|
|
||||||
</packageSources>
|
|
||||||
</configuration>
|
|
||||||
EOF
|
|
||||||
|
|
||||||
dotnet --info >/dev/null 2>&1 && install_ok=1
|
dotnet --info >/dev/null 2>&1 && install_ok=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -610,8 +597,8 @@ package_binary() {
|
|||||||
write_desktop_file "$stage"
|
write_desktop_file "$stage"
|
||||||
write_maintainer_scripts "$debian_dir"
|
write_maintainer_scripts "$debian_dir"
|
||||||
|
|
||||||
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)"
|
extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 9.4), bash (>= 5.2.21), libfreetype6 (>= 2.13)"
|
||||||
|
|
||||||
mkdir -p "$workdir/debian"
|
mkdir -p "$workdir/debian"
|
||||||
cat > "$workdir/debian/control" <<EOF
|
cat > "$workdir/debian/control" <<EOF
|
||||||
Source: v2rayn
|
Source: v2rayn
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
|
|||||||
PKGROOT="v2rayN-publish"
|
PKGROOT="v2rayN-publish"
|
||||||
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||||
OUTPUT_DIR="${HOME}/debbuild"
|
OUTPUT_DIR="${HOME}/debbuild"
|
||||||
DOTNET_RISCV_VERSION="10.0.108"
|
DOTNET_RISCV_VERSION="10.0.110"
|
||||||
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
|
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
|
||||||
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
|
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
|
||||||
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
|
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
|
||||||
@@ -595,8 +595,8 @@ package_binary() {
|
|||||||
write_desktop_file "$stage"
|
write_desktop_file "$stage"
|
||||||
write_maintainer_scripts "$debian_dir"
|
write_maintainer_scripts "$debian_dir"
|
||||||
|
|
||||||
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)"
|
extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 9.4), bash (>= 5.2.21), libfreetype6 (>= 2.13)"
|
||||||
|
|
||||||
mkdir -p "$workdir/debian"
|
mkdir -p "$workdir/debian"
|
||||||
cat > "$workdir/debian/control" <<EOF
|
cat > "$workdir/debian/control" <<EOF
|
||||||
Source: v2rayn
|
Source: v2rayn
|
||||||
|
|||||||
@@ -609,7 +609,7 @@ package_binary() {
|
|||||||
write_desktop_file "$stage"
|
write_desktop_file "$stage"
|
||||||
write_maintainer_scripts "$debian_dir"
|
write_maintainer_scripts "$debian_dir"
|
||||||
|
|
||||||
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)"
|
extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 9.4), bash (>= 5.2.21), libfreetype6 (>= 2.13)"
|
||||||
|
|
||||||
mkdir -p "$workdir/debian"
|
mkdir -p "$workdir/debian"
|
||||||
cat > "$workdir/debian/control" <<EOF
|
cat > "$workdir/debian/control" <<EOF
|
||||||
@@ -753,4 +753,4 @@ main() {
|
|||||||
print_summary
|
print_summary
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -22,7 +22,17 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>English</string>
|
<string>en</string>
|
||||||
|
<key>CFBundleLocalizations</key>
|
||||||
|
<array>
|
||||||
|
<string>zh-Hans</string>
|
||||||
|
<string>zh-Hant</string>
|
||||||
|
<string>en</string>
|
||||||
|
<string>fa</string>
|
||||||
|
<string>fr</string>
|
||||||
|
<string>ru</string>
|
||||||
|
<string>hu</string>
|
||||||
|
</array>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>v2rayN</string>
|
<string>v2rayN</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
@@ -44,7 +54,7 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
|
|||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.7</string>
|
<string>13.6</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
696
package-rhel-loong.sh
Normal file
696
package-rhel-loong.sh
Normal file
@@ -0,0 +1,696 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION_ARG=""
|
||||||
|
WITH_CORE="both"
|
||||||
|
FORCE_NETCORE=0
|
||||||
|
BUILD_FROM=""
|
||||||
|
XRAY_VER="${XRAY_VER:-}"
|
||||||
|
SING_VER="${SING_VER:-}"
|
||||||
|
|
||||||
|
MIN_KERNEL="6.12"
|
||||||
|
PKGROOT="v2rayN-publish"
|
||||||
|
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||||
|
RPM_TOPDIR="${HOME}/rpmbuild"
|
||||||
|
DOTNET_LOONGARCH_VERSION="10.0.110"
|
||||||
|
DOTNET_LOONGARCH_TAG="v10.0.110-loongarch64"
|
||||||
|
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
|
||||||
|
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
|
||||||
|
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
|
||||||
|
|
||||||
|
OS_ID=""
|
||||||
|
OS_NAME=""
|
||||||
|
OS_VERSION_ID=""
|
||||||
|
HOST_ARCH=""
|
||||||
|
SCRIPT_DIR=""
|
||||||
|
PROJECT=""
|
||||||
|
VERSION=""
|
||||||
|
|
||||||
|
declare -a BUILT_RPMS=()
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
local first_arg="${1:-}"
|
||||||
|
|
||||||
|
if [[ -n "$first_arg" && "$first_arg" != --* ]]; then
|
||||||
|
VERSION_ARG="$first_arg"
|
||||||
|
shift || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--with-core) WITH_CORE="${2:-both}"; shift 2 ;;
|
||||||
|
--xray-ver) XRAY_VER="${2:-}"; shift 2 ;;
|
||||||
|
--singbox-ver) SING_VER="${2:-}"; shift 2 ;;
|
||||||
|
--netcore) FORCE_NETCORE=1; shift ;;
|
||||||
|
--buildfrom) BUILD_FROM="${2:-}"; shift 2 ;;
|
||||||
|
*)
|
||||||
|
[[ -n "${VERSION_ARG:-}" ]] || VERSION_ARG="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then
|
||||||
|
die "You cannot specify both an explicit version and --buildfrom at the same time.
|
||||||
|
Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_environment() {
|
||||||
|
local current_kernel=""
|
||||||
|
local lowest=""
|
||||||
|
|
||||||
|
. /etc/os-release
|
||||||
|
|
||||||
|
OS_ID="${ID:-}"
|
||||||
|
OS_NAME="${NAME:-$OS_ID}"
|
||||||
|
OS_VERSION_ID="${VERSION_ID:-}"
|
||||||
|
HOST_ARCH="$(uname -m)"
|
||||||
|
|
||||||
|
case "$OS_ID" in
|
||||||
|
rhel|rocky|almalinux|fedora|centos)
|
||||||
|
echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}).
|
||||||
|
This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$HOST_ARCH" in
|
||||||
|
loongarch64) ;;
|
||||||
|
*) die "Only supports loongarch64" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
current_kernel="$(uname -r)"
|
||||||
|
lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$current_kernel" | sort -V | head -n1)"
|
||||||
|
|
||||||
|
[[ "$lowest" == "$MIN_KERNEL" ]] || die "Kernel $current_kernel is below $MIN_KERNEL"
|
||||||
|
echo "[OK] Kernel $current_kernel verified."
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dependencies() {
|
||||||
|
local install_ok=0
|
||||||
|
local tmp_dotnet=""
|
||||||
|
|
||||||
|
if command -v dnf >/dev/null 2>&1; then
|
||||||
|
sudo dnf -y --nogpgcheck install \
|
||||||
|
rpm-build rpmdevtools curl unzip tar jq rsync git python3 \
|
||||||
|
glibc-devel kernel-headers libatomic file ca-certificates libicu \
|
||||||
|
&& install_ok=1
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.dotnet"
|
||||||
|
tmp_dotnet="$(mktemp -d)"
|
||||||
|
curl -fL "$DOTNET_SDK_URL" -o "$tmp_dotnet/$DOTNET_LOONGARCH_FILE"
|
||||||
|
tar -C "$HOME/.dotnet" -xzf "$tmp_dotnet/$DOTNET_LOONGARCH_FILE"
|
||||||
|
rm -rf "$tmp_dotnet"
|
||||||
|
|
||||||
|
export PATH="$HOME/.dotnet:$PATH"
|
||||||
|
export DOTNET_ROOT="$HOME/.dotnet"
|
||||||
|
|
||||||
|
dotnet --info >/dev/null 2>&1 || install_ok=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$install_ok" -ne 1 ]]; then
|
||||||
|
echo "Could not auto-install dependencies for '$OS_ID'. Make sure these are available:"
|
||||||
|
echo "dotnet-loongarch SDK, curl, unzip, tar, rsync, git, python3, rpm, rpmdevtools, rpm-build (on Red Hat branch)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_workspace() {
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
if [[ -f .gitmodules ]]; then
|
||||||
|
git submodule sync --recursive || true
|
||||||
|
git submodule update --init --recursive || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT="$PROJECT_HINT"
|
||||||
|
[[ -f "$PROJECT" ]] || PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)"
|
||||||
|
[[ -f "$PROJECT" ]] || die "v2rayN.Desktop.csproj not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
choose_channel() {
|
||||||
|
local ch="latest"
|
||||||
|
local sel=""
|
||||||
|
|
||||||
|
if [[ -n "${BUILD_FROM:-}" ]]; then
|
||||||
|
case "$BUILD_FROM" in
|
||||||
|
1) echo "latest"; return 0 ;;
|
||||||
|
2) echo "prerelease"; return 0 ;;
|
||||||
|
3) echo "keep"; return 0 ;;
|
||||||
|
*) die "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
echo "[?] Choose v2rayN release channel:" >&2
|
||||||
|
echo " 1) Latest (stable) [default]" >&2
|
||||||
|
echo " 2) Pre-release (preview)" >&2
|
||||||
|
echo " 3) Keep current (do nothing)" >&2
|
||||||
|
printf "Enter 1, 2 or 3 [default 1]: " >&2
|
||||||
|
|
||||||
|
if read -r sel </dev/tty; then
|
||||||
|
case "${sel:-}" in
|
||||||
|
2) ch="prerelease" ;;
|
||||||
|
3) ch="keep" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$ch"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_latest_tag_latest() {
|
||||||
|
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases/latest" \
|
||||||
|
| jq -re '.tag_name' \
|
||||||
|
| sed 's/^v//'
|
||||||
|
}
|
||||||
|
|
||||||
|
get_latest_tag_prerelease() {
|
||||||
|
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases?per_page=20" \
|
||||||
|
| jq -re 'first(.[] | select(.prerelease == true) | .tag_name)' \
|
||||||
|
| sed 's/^v//'
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_submodules() {
|
||||||
|
if [[ -f .gitmodules ]]; then
|
||||||
|
git submodule sync --recursive || true
|
||||||
|
git submodule update --init --recursive || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
git_try_checkout() {
|
||||||
|
local want="$1"
|
||||||
|
local ref=""
|
||||||
|
|
||||||
|
if git rev-parse --git-dir >/dev/null 2>&1; then
|
||||||
|
git fetch --tags --force --prune --depth=1 || true
|
||||||
|
git rev-parse "refs/tags/${want}" >/dev/null 2>&1 && ref="$want"
|
||||||
|
|
||||||
|
if [[ -n "$ref" ]]; then
|
||||||
|
echo "[OK] Found ref '${ref}', checking out..."
|
||||||
|
git checkout -f "$ref"
|
||||||
|
sync_submodules
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_channel_or_keep() {
|
||||||
|
local ch="$1"
|
||||||
|
local tag=""
|
||||||
|
|
||||||
|
if [[ "$ch" == "keep" ]]; then
|
||||||
|
echo "[*] Keep current repository state (no checkout)."
|
||||||
|
VERSION="$(git describe --tags --abbrev=0 2>/dev/null || echo '0.0.0+git')"
|
||||||
|
VERSION="${VERSION#v}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Resolving ${ch} tag from GitHub releases..."
|
||||||
|
|
||||||
|
case "$ch" in
|
||||||
|
latest) tag="$(get_latest_tag_latest || true)" ;;
|
||||||
|
prerelease) tag="$(get_latest_tag_prerelease || true)" ;;
|
||||||
|
*) die "Failed to resolve latest tag for channel '${ch}'." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[[ -n "$tag" ]] || die "Failed to resolve latest tag for channel '${ch}'."
|
||||||
|
|
||||||
|
echo "[*] Latest tag for '${ch}': ${tag}"
|
||||||
|
git_try_checkout "$tag" || die "Failed to checkout '${tag}'."
|
||||||
|
VERSION="${tag#v}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_version() {
|
||||||
|
if git rev-parse --git-dir >/dev/null 2>&1; then
|
||||||
|
if [[ -n "${VERSION_ARG:-}" ]]; then
|
||||||
|
local clean_ver="${VERSION_ARG#v}"
|
||||||
|
|
||||||
|
if git_try_checkout "$clean_ver"; then
|
||||||
|
VERSION="$clean_ver"
|
||||||
|
else
|
||||||
|
echo "[WARN] Tag '${VERSION_ARG}' not found."
|
||||||
|
apply_channel_or_keep "$(choose_channel)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
apply_channel_or_keep "$(choose_channel)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Current directory is not a git repo; proceeding on current tree."
|
||||||
|
VERSION="${VERSION_ARG:-0.0.0}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="${VERSION#v}"
|
||||||
|
echo "[*] GUI version resolved as: ${VERSION}"
|
||||||
|
}
|
||||||
|
|
||||||
|
xray_url_for_rid() {
|
||||||
|
local rid="$1"
|
||||||
|
local ver="$2"
|
||||||
|
|
||||||
|
case "$rid" in
|
||||||
|
linux-loongarch64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-loong64.zip" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
singbox_url_for_rid() {
|
||||||
|
local rid="$1"
|
||||||
|
local ver="$2"
|
||||||
|
|
||||||
|
case "$rid" in
|
||||||
|
linux-loongarch64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-loong64.tar.gz" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle_url_for_rid() {
|
||||||
|
local rid="$1"
|
||||||
|
|
||||||
|
case "$rid" in
|
||||||
|
linux-loongarch64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-loong64.zip" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
download_xray() {
|
||||||
|
local outdir="$1"
|
||||||
|
local rid="$2"
|
||||||
|
local ver="${XRAY_VER:-}"
|
||||||
|
local url=""
|
||||||
|
local tmp=""
|
||||||
|
|
||||||
|
mkdir -p "$outdir"
|
||||||
|
|
||||||
|
if [[ -z "$ver" ]]; then
|
||||||
|
ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \
|
||||||
|
| grep -Eo '"tag_name":\s*"v[^"]+"' \
|
||||||
|
| sed -E 's/.*"v([^"]+)".*/\1/' \
|
||||||
|
| head -n1)" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$ver" ]] || { echo "[xray] Failed to get version"; return 1; }
|
||||||
|
url="$(xray_url_for_rid "$rid" "$ver")" || { echo "[xray] Unsupported RID: $rid"; return 1; }
|
||||||
|
|
||||||
|
echo "[+] Download xray: $url"
|
||||||
|
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
curl -fL "$url" -o "$tmp/xray.zip" || { rm -rf "$tmp"; return 1; }
|
||||||
|
unzip -q "$tmp/xray.zip" -d "$tmp" || { rm -rf "$tmp"; return 1; }
|
||||||
|
install -m 755 "$tmp/xray" "$outdir/xray" || { rm -rf "$tmp"; return 1; }
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
download_singbox() {
|
||||||
|
local outdir="$1"
|
||||||
|
local rid="$2"
|
||||||
|
local ver="${SING_VER:-}"
|
||||||
|
local url=""
|
||||||
|
local tmp=""
|
||||||
|
local bin=""
|
||||||
|
local cronet=""
|
||||||
|
|
||||||
|
mkdir -p "$outdir"
|
||||||
|
|
||||||
|
if [[ -z "$ver" ]]; then
|
||||||
|
ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \
|
||||||
|
| grep -Eo '"tag_name":\s*"v[^"]+"' \
|
||||||
|
| sed -E 's/.*"v([^"]+)".*/\1/' \
|
||||||
|
| head -n1)" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$ver" ]] || { echo "[sing-box] Failed to get version"; return 1; }
|
||||||
|
url="$(singbox_url_for_rid "$rid" "$ver")" || { echo "[sing-box] Unsupported RID: $rid"; return 1; }
|
||||||
|
|
||||||
|
echo "[+] Download sing-box: $url"
|
||||||
|
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
curl -fL "$url" -o "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; }
|
||||||
|
tar -C "$tmp" -xzf "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; }
|
||||||
|
|
||||||
|
bin="$(find "$tmp" -type f -name 'sing-box' | head -n1 || true)"
|
||||||
|
[[ -n "$bin" ]] || { echo "[!] sing-box unpack failed"; rm -rf "$tmp"; return 1; }
|
||||||
|
|
||||||
|
install -m 755 "$bin" "$outdir/sing-box" || { rm -rf "$tmp"; return 1; }
|
||||||
|
|
||||||
|
cronet="$(find "$tmp" -type f -name 'libcronet*.so*' | head -n1 || true)"
|
||||||
|
[[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" || true
|
||||||
|
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
unify_geo_layout() {
|
||||||
|
local outroot="$1"
|
||||||
|
local n
|
||||||
|
local names=(
|
||||||
|
geosite.dat
|
||||||
|
geoip.dat
|
||||||
|
geoip-only-cn-private.dat
|
||||||
|
Country.mmdb
|
||||||
|
geoip.metadb
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "$outroot/bin"
|
||||||
|
|
||||||
|
for n in "${names[@]}"; do
|
||||||
|
if [[ -f "$outroot/bin/xray/$n" ]]; then
|
||||||
|
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
download_geo_assets() {
|
||||||
|
local outroot="$1"
|
||||||
|
local bin_dir="$outroot/bin"
|
||||||
|
local srss_dir="$bin_dir/srss"
|
||||||
|
local f=""
|
||||||
|
|
||||||
|
mkdir -p "$bin_dir" "$srss_dir"
|
||||||
|
|
||||||
|
echo "[+] Download Xray Geo to ${bin_dir}"
|
||||||
|
curl -fsSL -o "$bin_dir/geosite.dat" "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat"
|
||||||
|
curl -fsSL -o "$bin_dir/geoip.dat" "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat"
|
||||||
|
curl -fsSL -o "$bin_dir/geoip-only-cn-private.dat" "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat"
|
||||||
|
curl -fsSL -o "$bin_dir/Country.mmdb" "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
|
||||||
|
|
||||||
|
echo "[+] Download sing-box rule DB & rule-sets"
|
||||||
|
curl -fsSL -o "$bin_dir/geoip.metadb" "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb"
|
||||||
|
|
||||||
|
for f in geoip-private.srs geoip-cn.srs geoip-facebook.srs geoip-fastly.srs geoip-google.srs geoip-netflix.srs geoip-telegram.srs geoip-twitter.srs; do
|
||||||
|
curl -fsSL -o "$srss_dir/$f" "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geoip/$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in geosite-cn.srs geosite-gfw.srs geosite-google.srs geosite-greatfire.srs geosite-geolocation-cn.srs geosite-category-ads-all.srs geosite-private.srs; do
|
||||||
|
curl -fsSL -o "$srss_dir/$f" "https://raw.githubusercontent.com/2dust/sing-box-rules/refs/heads/rule-set-geosite/$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
unify_geo_layout "$outroot"
|
||||||
|
}
|
||||||
|
|
||||||
|
populate_assets_zip_mode() {
|
||||||
|
local outroot="$1"
|
||||||
|
local rid="$2"
|
||||||
|
local url=""
|
||||||
|
local tmp=""
|
||||||
|
local nested_dir=""
|
||||||
|
|
||||||
|
url="$(bundle_url_for_rid "$rid")" || { echo "[!] Bundle unsupported RID: $rid"; return 1; }
|
||||||
|
|
||||||
|
echo "[+] Try v2rayN bundle archive: $url"
|
||||||
|
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
curl -fL "$url" -o "$tmp/v2rayn.zip" || { echo "[!] Bundle download failed"; rm -rf "$tmp"; return 1; }
|
||||||
|
unzip -q "$tmp/v2rayn.zip" -d "$tmp" || { echo "[!] Bundle unzip failed"; rm -rf "$tmp"; return 1; }
|
||||||
|
|
||||||
|
if [[ -d "$tmp/bin" ]]; then
|
||||||
|
mkdir -p "$outroot/bin"
|
||||||
|
rsync -a "$tmp/bin/" "$outroot/bin/"
|
||||||
|
else
|
||||||
|
rsync -a "$tmp/" "$outroot/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$outroot/v2rayn.zip" 2>/dev/null || true
|
||||||
|
find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
|
nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)"
|
||||||
|
if [[ -n "$nested_dir" && -d "$nested_dir/bin" ]]; then
|
||||||
|
mkdir -p "$outroot/bin"
|
||||||
|
rsync -a "$nested_dir/bin/" "$outroot/bin/"
|
||||||
|
rm -rf "$nested_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
unify_geo_layout "$outroot"
|
||||||
|
rm -rf "$tmp"
|
||||||
|
|
||||||
|
echo "[+] Bundle extracted to $outroot"
|
||||||
|
}
|
||||||
|
|
||||||
|
populate_assets_netcore_mode() {
|
||||||
|
local outroot="$1"
|
||||||
|
local rid="$2"
|
||||||
|
|
||||||
|
mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box"
|
||||||
|
|
||||||
|
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
|
||||||
|
download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray download failed (skipped)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
|
||||||
|
download_singbox "$outroot/bin/sing_box" "$rid" || echo "[!] sing-box download failed (skipped)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)"
|
||||||
|
}
|
||||||
|
|
||||||
|
stage_runtime_assets() {
|
||||||
|
local outroot="$1"
|
||||||
|
local rid="$2"
|
||||||
|
|
||||||
|
mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box"
|
||||||
|
|
||||||
|
if [[ "$FORCE_NETCORE" -eq 0 ]]; then
|
||||||
|
if populate_assets_zip_mode "$outroot" "$rid"; then
|
||||||
|
echo "[*] Using v2rayN bundle archive."
|
||||||
|
else
|
||||||
|
echo "[*] Bundle failed, fallback to separate core + rules."
|
||||||
|
populate_assets_netcore_mode "$outroot" "$rid"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[*] --netcore specified: use separate core + rules."
|
||||||
|
populate_assets_netcore_mode "$outroot" "$rid"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
describe_target() {
|
||||||
|
local short="$1"
|
||||||
|
|
||||||
|
case "$short" in
|
||||||
|
loongarch64) printf '%s\n%s\n%s\n' "linux-loongarch64" "loongarch64" "loongarch64" ;;
|
||||||
|
*) echo "Unknown arch '$short' (use loongarch64)" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
publish_binary() {
|
||||||
|
local rid="$1"
|
||||||
|
|
||||||
|
dotnet clean "$PROJECT" -c Release
|
||||||
|
rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true
|
||||||
|
dotnet restore "$PROJECT"
|
||||||
|
dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true
|
||||||
|
}
|
||||||
|
|
||||||
|
write_spec_file() {
|
||||||
|
local specfile="$1"
|
||||||
|
|
||||||
|
cat > "$specfile" <<'SPEC'
|
||||||
|
%global debug_package %{nil}
|
||||||
|
%undefine _debuginfo_subpackages
|
||||||
|
%undefine _debugsource_packages
|
||||||
|
%global __requires_exclude ^liblttng-ust\.so\..*$
|
||||||
|
|
||||||
|
Name: v2rayN
|
||||||
|
Version: __VERSION__
|
||||||
|
Release: 1%{?dist}
|
||||||
|
Summary: v2rayN (Avalonia) GUI client for Linux
|
||||||
|
License: GPL-3.0-only
|
||||||
|
URL: https://github.com/2dust/v2rayN
|
||||||
|
BugURL: https://github.com/2dust/v2rayN/issues
|
||||||
|
ExclusiveArch: loongarch64
|
||||||
|
Source0: __PKGROOT__.tar.gz
|
||||||
|
|
||||||
|
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
|
||||||
|
Requires: glibc >= 2.39
|
||||||
|
Requires: fontconfig >= 2.15.0
|
||||||
|
Requires: desktop-file-utils >= 0.26
|
||||||
|
Requires: xdg-utils >= 1.1.3
|
||||||
|
Requires: coreutils >= 9.4
|
||||||
|
Requires: bash >= 5.2.21
|
||||||
|
Requires: freetype >= 2.13
|
||||||
|
|
||||||
|
%description
|
||||||
|
v2rayN Linux for Red Hat Enterprise Linux
|
||||||
|
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard
|
||||||
|
Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS
|
||||||
|
For more information, Please visit our website
|
||||||
|
https://github.com/2dust/v2rayN
|
||||||
|
|
||||||
|
%prep
|
||||||
|
%setup -q -n __PKGROOT__
|
||||||
|
|
||||||
|
%build
|
||||||
|
|
||||||
|
%install
|
||||||
|
install -dm0755 %{buildroot}/opt/v2rayN
|
||||||
|
cp -a * %{buildroot}/opt/v2rayN/
|
||||||
|
|
||||||
|
find %{buildroot}/opt/v2rayN -type d -exec chmod 0755 {} +
|
||||||
|
find %{buildroot}/opt/v2rayN -type f -exec chmod 0644 {} +
|
||||||
|
[ -f %{buildroot}/opt/v2rayN/v2rayN ] && chmod 0755 %{buildroot}/opt/v2rayN/v2rayN || :
|
||||||
|
|
||||||
|
install -dm0755 %{buildroot}%{_bindir}
|
||||||
|
install -m0755 /dev/stdin %{buildroot}%{_bindir}/v2rayn << 'EOF'
|
||||||
|
#!/usr/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
DIR="/opt/v2rayN"
|
||||||
|
|
||||||
|
if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi
|
||||||
|
|
||||||
|
for dll in v2rayN.Desktop.dll v2rayN.dll; do
|
||||||
|
if [[ -f "$DIR/$dll" ]]; then exec /usr/bin/dotnet "$DIR/$dll" "$@"; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "v2rayN launcher: no executable found in $DIR" >&2
|
||||||
|
ls -l "$DIR" >&2 || true
|
||||||
|
exit 1
|
||||||
|
EOF
|
||||||
|
|
||||||
|
install -dm0755 %{buildroot}%{_datadir}/applications
|
||||||
|
install -m0644 /dev/stdin %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=v2rayN
|
||||||
|
Comment=v2rayN for Red Hat Enterprise Linux
|
||||||
|
Exec=v2rayn
|
||||||
|
Icon=v2rayn
|
||||||
|
Terminal=false
|
||||||
|
Categories=Network;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps
|
||||||
|
install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
|
||||||
|
|
||||||
|
%post
|
||||||
|
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true
|
||||||
|
/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
%postun
|
||||||
|
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true
|
||||||
|
/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
%files
|
||||||
|
%{_bindir}/v2rayn
|
||||||
|
/opt/v2rayN
|
||||||
|
%{_datadir}/applications/v2rayn.desktop
|
||||||
|
%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
|
||||||
|
SPEC
|
||||||
|
|
||||||
|
sed -i "s/__VERSION__/${VERSION}/g" "$specfile"
|
||||||
|
sed -i "s/__PKGROOT__/${PKGROOT}/g" "$specfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
package_binary() {
|
||||||
|
local short="$1"
|
||||||
|
local rid="$2"
|
||||||
|
local rpm_target="$3"
|
||||||
|
local archdir="$4"
|
||||||
|
local pubdir=""
|
||||||
|
local workdir=""
|
||||||
|
local specfile=""
|
||||||
|
local sourcedir=""
|
||||||
|
local specdir=""
|
||||||
|
local project_dir=""
|
||||||
|
local icon_candidate=""
|
||||||
|
local f=""
|
||||||
|
|
||||||
|
pubdir="$(dirname "$PROJECT")/bin/Release/net10.0/${rid}/publish"
|
||||||
|
[[ -d "$pubdir" ]] || { echo "Publish directory not found: $pubdir"; return 1; }
|
||||||
|
|
||||||
|
workdir="$(mktemp -d)"
|
||||||
|
trap '[[ -n "${workdir:-}" ]] && rm -rf "$workdir"' RETURN
|
||||||
|
|
||||||
|
mkdir -p "$workdir/$PKGROOT"
|
||||||
|
cp -a "$pubdir/." "$workdir/$PKGROOT/"
|
||||||
|
|
||||||
|
project_dir="$(cd "$(dirname "$PROJECT")" && pwd)"
|
||||||
|
icon_candidate="$project_dir/v2rayN.png"
|
||||||
|
[[ -f "$icon_candidate" ]] || { echo "Required icon not found: $icon_candidate"; return 1; }
|
||||||
|
cp "$icon_candidate" "$workdir/$PKGROOT/v2rayn.png"
|
||||||
|
|
||||||
|
stage_runtime_assets "$workdir/$PKGROOT" "$rid"
|
||||||
|
|
||||||
|
rpmdev-setuptree
|
||||||
|
sourcedir="${RPM_TOPDIR}/SOURCES"
|
||||||
|
specdir="${RPM_TOPDIR}/SPECS"
|
||||||
|
specfile="${specdir}/v2rayN.spec"
|
||||||
|
|
||||||
|
mkdir -p "$sourcedir" "$specdir"
|
||||||
|
tar -C "$workdir" -czf "$sourcedir/$PKGROOT.tar.gz" "$PKGROOT"
|
||||||
|
|
||||||
|
write_spec_file "$specfile"
|
||||||
|
rpmbuild -ba "$specfile" --target "$rpm_target"
|
||||||
|
|
||||||
|
echo "Build done for $short. RPM at:"
|
||||||
|
for f in "${RPM_TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do
|
||||||
|
[[ -e "$f" ]] || continue
|
||||||
|
echo " $f"
|
||||||
|
BUILT_RPMS+=("$f")
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
select_targets() {
|
||||||
|
printf '%s\n' loongarch64
|
||||||
|
}
|
||||||
|
|
||||||
|
build_one_target() {
|
||||||
|
local short="$1"
|
||||||
|
local meta=()
|
||||||
|
local rid=""
|
||||||
|
local rpm_target=""
|
||||||
|
local archdir=""
|
||||||
|
|
||||||
|
mapfile -t meta < <(describe_target "$short") || return 1
|
||||||
|
rid="${meta[0]}"
|
||||||
|
rpm_target="${meta[1]}"
|
||||||
|
archdir="${meta[2]}"
|
||||||
|
|
||||||
|
echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)"
|
||||||
|
publish_binary "$rid"
|
||||||
|
package_binary "$short" "$rid" "$rpm_target" "$archdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
local rp=""
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "================ Build Summary ================"
|
||||||
|
if [[ "${#BUILT_RPMS[@]}" -gt 0 ]]; then
|
||||||
|
for rp in "${BUILT_RPMS[@]}"; do
|
||||||
|
echo "$rp"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "No RPMs detected in summary (check build logs above)."
|
||||||
|
fi
|
||||||
|
echo "=============================================="
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local targets=()
|
||||||
|
local arch=""
|
||||||
|
|
||||||
|
parse_args "$@"
|
||||||
|
detect_environment
|
||||||
|
install_dependencies
|
||||||
|
prepare_workspace
|
||||||
|
resolve_version
|
||||||
|
|
||||||
|
mapfile -t targets < <(select_targets)
|
||||||
|
|
||||||
|
for arch in "${targets[@]}"; do
|
||||||
|
build_one_target "$arch"
|
||||||
|
done
|
||||||
|
|
||||||
|
print_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
|
|||||||
PKGROOT="v2rayN-publish"
|
PKGROOT="v2rayN-publish"
|
||||||
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||||
RPM_TOPDIR="${HOME}/rpmbuild"
|
RPM_TOPDIR="${HOME}/rpmbuild"
|
||||||
DOTNET_RISCV_VERSION="10.0.108"
|
DOTNET_RISCV_VERSION="10.0.110"
|
||||||
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
|
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
|
||||||
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
|
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
|
||||||
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
|
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
|
||||||
@@ -501,7 +501,7 @@ write_spec_file() {
|
|||||||
Name: v2rayN
|
Name: v2rayN
|
||||||
Version: __VERSION__
|
Version: __VERSION__
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: v2rayN (Avalonia) GUI client for Linux (riscv64)
|
Summary: v2rayN (Avalonia) GUI client for Linux
|
||||||
License: GPL-3.0-only
|
License: GPL-3.0-only
|
||||||
URL: https://github.com/2dust/v2rayN
|
URL: https://github.com/2dust/v2rayN
|
||||||
BugURL: https://github.com/2dust/v2rayN/issues
|
BugURL: https://github.com/2dust/v2rayN/issues
|
||||||
@@ -509,13 +509,13 @@ ExclusiveArch: riscv64
|
|||||||
Source0: __PKGROOT__.tar.gz
|
Source0: __PKGROOT__.tar.gz
|
||||||
|
|
||||||
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
|
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
|
||||||
Requires: glibc >= 2.34
|
Requires: glibc >= 2.39
|
||||||
Requires: fontconfig >= 2.13.1
|
Requires: fontconfig >= 2.15.0
|
||||||
Requires: desktop-file-utils >= 0.26
|
Requires: desktop-file-utils >= 0.26
|
||||||
Requires: xdg-utils >= 1.1.3
|
Requires: xdg-utils >= 1.1.3
|
||||||
Requires: coreutils >= 8.32
|
Requires: coreutils >= 9.4
|
||||||
Requires: bash >= 5.1
|
Requires: bash >= 5.2.21
|
||||||
Requires: freetype >= 2.10
|
Requires: freetype >= 2.13
|
||||||
|
|
||||||
%description
|
%description
|
||||||
v2rayN Linux for Red Hat Enterprise Linux
|
v2rayN Linux for Red Hat Enterprise Linux
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ write_spec_file() {
|
|||||||
Name: v2rayN
|
Name: v2rayN
|
||||||
Version: __VERSION__
|
Version: __VERSION__
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64)
|
Summary: v2rayN (Avalonia) GUI client for Linux
|
||||||
License: GPL-3.0-only
|
License: GPL-3.0-only
|
||||||
URL: https://github.com/2dust/v2rayN
|
URL: https://github.com/2dust/v2rayN
|
||||||
BugURL: https://github.com/2dust/v2rayN/issues
|
BugURL: https://github.com/2dust/v2rayN/issues
|
||||||
@@ -498,13 +498,13 @@ ExclusiveArch: aarch64 x86_64
|
|||||||
Source0: __PKGROOT__.tar.gz
|
Source0: __PKGROOT__.tar.gz
|
||||||
|
|
||||||
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
|
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
|
||||||
Requires: glibc >= 2.34
|
Requires: glibc >= 2.39
|
||||||
Requires: fontconfig >= 2.13.1
|
Requires: fontconfig >= 2.15.0
|
||||||
Requires: desktop-file-utils >= 0.26
|
Requires: desktop-file-utils >= 0.26
|
||||||
Requires: xdg-utils >= 1.1.3
|
Requires: xdg-utils >= 1.1.3
|
||||||
Requires: coreutils >= 8.32
|
Requires: coreutils >= 9.4
|
||||||
Requires: bash >= 5.1
|
Requires: bash >= 5.2.21
|
||||||
Requires: freetype >= 2.10
|
Requires: freetype >= 2.13
|
||||||
|
|
||||||
%description
|
%description
|
||||||
v2rayN Linux for Red Hat Enterprise Linux
|
v2rayN Linux for Red Hat Enterprise Linux
|
||||||
@@ -698,4 +698,4 @@ main() {
|
|||||||
print_summary
|
print_summary
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>7.22.4</Version>
|
<Version>7.24.6</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||||
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
|
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
|
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
|
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
|
||||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.16" />
|
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
|
||||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.16" />
|
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
||||||
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
|
<PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
|
||||||
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
|
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
|
||||||
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" />
|
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
||||||
<PackageVersion Include="CliWrap" Version="3.10.1" />
|
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" />
|
||||||
<PackageVersion Include="Downloader" Version="5.5.0" />
|
<PackageVersion Include="CliWrap" Version="3.10.4" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
|
<PackageVersion Include="Downloader" Version="5.9.5" />
|
||||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||||
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
||||||
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
||||||
<PackageVersion Include="ReactiveUI" Version="23.2.27" />
|
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
||||||
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
|
<PackageVersion Include="ReactiveUI" Version="24.1.0" />
|
||||||
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.27" />
|
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
|
||||||
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
|
<PackageVersion Include="ReactiveUI.WPF" Version="24.1.0" />
|
||||||
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
|
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
|
||||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
|
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||||
<PackageVersion Include="NLog" Version="6.1.3" />
|
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
|
||||||
<PackageVersion Include="sqlite-net-e" Version="1.11.0" />
|
<PackageVersion Include="NLog" Version="6.1.4" />
|
||||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.1.7" />
|
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
|
||||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
|
||||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||||
<PackageVersion Include="YamlDotNet" Version="17.1.0" />
|
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
||||||
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
|
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
|
||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4-preview.1.1" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Submodule v2rayN/GlobalHotKeys updated: 569a95bb0f...162d401dfe
@@ -17,7 +17,7 @@ internal static class CoreConfigTestFactory
|
|||||||
{
|
{
|
||||||
return new Config
|
return new Config
|
||||||
{
|
{
|
||||||
CoreBasicItem = new CoreBasicItem { Loglevel = "warning", MuxEnabled = false },
|
CoreBasicItem = new CoreBasicItem { Loglevel = "warning" },
|
||||||
TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" },
|
TunModeItem = new TunModeItem { EnableTun = false, IcmpRouting = "default" },
|
||||||
KcpItem = new KcpItem(),
|
KcpItem = new KcpItem(),
|
||||||
GrpcItem = new GrpcItem(),
|
GrpcItem = new GrpcItem(),
|
||||||
@@ -59,7 +59,7 @@ internal static class CoreConfigTestFactory
|
|||||||
},
|
},
|
||||||
WebDavItem = new WebDavItem(),
|
WebDavItem = new WebDavItem(),
|
||||||
CheckUpdateItem = new CheckUpdateItem(),
|
CheckUpdateItem = new CheckUpdateItem(),
|
||||||
Fragment4RayItem = new Fragment4RayItem { Packets = "tlshello", Length = "100-200", Interval = "10-20" },
|
Fragment4RayItem = new Fragment4RayItem { Packets = "tlshello", Lengths = ["100-200"], Delays = ["10-20"] },
|
||||||
Inbound =
|
Inbound =
|
||||||
[
|
[
|
||||||
new InItem
|
new InItem
|
||||||
@@ -84,6 +84,7 @@ internal static class CoreConfigTestFactory
|
|||||||
ParallelQuery = false,
|
ParallelQuery = false,
|
||||||
Strategy4Freedom = Global.AsIs,
|
Strategy4Freedom = Global.AsIs,
|
||||||
Strategy4Proxy = Global.AsIs,
|
Strategy4Proxy = Global.AsIs,
|
||||||
|
Strategy4ProxyDial = Global.AsIs,
|
||||||
},
|
},
|
||||||
IndexId = string.Empty,
|
IndexId = string.Empty,
|
||||||
SubIndexId = string.Empty,
|
SubIndexId = string.Empty,
|
||||||
@@ -130,6 +131,42 @@ internal static class CoreConfigTestFactory
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ProfileItem CreateHttpNode(ECoreType coreType, string indexId = "node-http-1",
|
||||||
|
string remarks = "demo-http")
|
||||||
|
{
|
||||||
|
return new ProfileItem
|
||||||
|
{
|
||||||
|
IndexId = indexId,
|
||||||
|
ConfigType = EConfigType.HTTP,
|
||||||
|
CoreType = coreType,
|
||||||
|
Remarks = remarks,
|
||||||
|
Address = "proxy.example.com",
|
||||||
|
Port = 8080,
|
||||||
|
Password = "pass",
|
||||||
|
Username = "user",
|
||||||
|
Network = nameof(ETransport.raw),
|
||||||
|
StreamSecurity = string.Empty,
|
||||||
|
Subid = string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProfileItem CreateCustomOutboundNode(ECoreType coreType, string indexId = "node-custom-1",
|
||||||
|
string remarks = "demo-custom-outbound", string address = "custom_outbound.json")
|
||||||
|
{
|
||||||
|
return new ProfileItem
|
||||||
|
{
|
||||||
|
IndexId = indexId,
|
||||||
|
ConfigType = EConfigType.Outbound,
|
||||||
|
CoreType = coreType,
|
||||||
|
Remarks = remarks,
|
||||||
|
Address = address,
|
||||||
|
Port = 0,
|
||||||
|
Network = nameof(ETransport.raw),
|
||||||
|
StreamSecurity = string.Empty,
|
||||||
|
Subid = string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks,
|
public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks,
|
||||||
IEnumerable<string> childIndexIds)
|
IEnumerable<string> childIndexIds)
|
||||||
{
|
{
|
||||||
@@ -187,7 +224,7 @@ internal static class CoreConfigTestFactory
|
|||||||
SimpleDnsItem = config.SimpleDNSItem,
|
SimpleDnsItem = config.SimpleDNSItem,
|
||||||
AllProxiesMap = new Dictionary<string, ProfileItem> { [node.IndexId] = node },
|
AllProxiesMap = new Dictionary<string, ProfileItem> { [node.IndexId] = node },
|
||||||
FullConfigTemplate = null,
|
FullConfigTemplate = null,
|
||||||
IsTunEnabled = false,
|
IsTunEnabled = config.TunModeItem.EnableTun,
|
||||||
ProtectDomainList = [],
|
ProtectDomainList = [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -206,4 +243,12 @@ internal static class CoreConfigTestFactory
|
|||||||
config.SimpleDNSItem.BootstrapDNS = bootstrapDns;
|
config.SimpleDNSItem.BootstrapDNS = bootstrapDns;
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Config CreateConfigWithTunRouteExcludeAddress(ECoreType coreType)
|
||||||
|
{
|
||||||
|
var config = CreateConfig(coreType);
|
||||||
|
config.TunModeItem.EnableTun = true;
|
||||||
|
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
|
||||||
|
return config;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using AwesomeAssertions;
|
using AwesomeAssertions;
|
||||||
using ServiceLib.Common;
|
using ServiceLib.Common;
|
||||||
using ServiceLib.Enums;
|
using ServiceLib.Enums;
|
||||||
|
using ServiceLib.Handler.Fmt;
|
||||||
using ServiceLib.Manager;
|
using ServiceLib.Manager;
|
||||||
using ServiceLib.Models;
|
using ServiceLib.Models;
|
||||||
|
using ServiceLib.Models.Dto;
|
||||||
using ServiceLib.Services.CoreConfig;
|
using ServiceLib.Services.CoreConfig;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -554,7 +556,110 @@ public class CoreConfigSingboxServiceTests
|
|||||||
|
|
||||||
cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8");
|
cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8");
|
||||||
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
|
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
|
||||||
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Global.ToString());
|
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
|
||||||
cfg.dns.rules.Should().Contain(r => r.clash_mode == ERuleMode.Direct.ToString());
|
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
|
||||||
|
{
|
||||||
|
var shareLink =
|
||||||
|
"hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
|
||||||
|
var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
|
||||||
|
node.Should().NotBeNull();
|
||||||
|
node!.CoreType = ECoreType.sing_box;
|
||||||
|
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
config.CoreTypeItem =
|
||||||
|
[
|
||||||
|
new CoreTypeItem { ConfigType = EConfigType.Hysteria2, CoreType = ECoreType.sing_box }
|
||||||
|
];
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
|
||||||
|
|
||||||
|
proxy.type.Should().Be("hysteria2");
|
||||||
|
proxy.realm.Should().NotBeNull();
|
||||||
|
proxy.realm!.server_url.Should().StartWith("https://");
|
||||||
|
proxy.realm.server_url.Should().Contain("realm.hy2.io");
|
||||||
|
proxy.realm.token.Should().Be("public");
|
||||||
|
proxy.realm.realm_id.Should().Be("my-realm-id");
|
||||||
|
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
|
||||||
|
proxy.server.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_TunSystemStackWithIpv6_ShouldUsePrefixWithPeerAddress()
|
||||||
|
{
|
||||||
|
// Regression test for #9820: sing-box fails with "need one more IPv6 address in
|
||||||
|
// first prefix for system stack" when the TUN inbound uses a /128 IPv6 prefix.
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
config.TunModeItem.EnableTun = true;
|
||||||
|
config.TunModeItem.Stack = "system";
|
||||||
|
config.TunModeItem.EnableIPv6Address = true;
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||||
|
{
|
||||||
|
IsTunEnabled = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||||
|
var tun = cfg.inbounds.First(i => i.type == "tun");
|
||||||
|
|
||||||
|
tun.address.Should().NotBeNullOrEmpty();
|
||||||
|
foreach (var address in tun.address!)
|
||||||
|
{
|
||||||
|
var prefixLength = int.Parse(address[(address.LastIndexOf('/') + 1)..]);
|
||||||
|
var isIpv6 = address.Contains(':');
|
||||||
|
prefixLength.Should().BeLessThanOrEqualTo(isIpv6 ? 126 : 30,
|
||||||
|
$"'{address}' must leave room for the peer address the system stack derives");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var customNode = CoreConfigTestFactory.CreateCustomOutboundNode(ECoreType.sing_box, "n-custom", "custom-singbox");
|
||||||
|
var customJsonContent = """
|
||||||
|
{
|
||||||
|
"type": "shadowsocks",
|
||||||
|
"server": "1.2.3.4",
|
||||||
|
"server_port": 8388,
|
||||||
|
"method": "aes-128-gcm",
|
||||||
|
"password": "custom_password"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, customNode, ECoreType.sing_box);
|
||||||
|
context.CustomOutboundContent[customNode.IndexId] = customJsonContent;
|
||||||
|
|
||||||
|
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
result.Data.Should().NotBeNull();
|
||||||
|
|
||||||
|
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
|
||||||
|
cfg.Should().NotBeNull();
|
||||||
|
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
|
||||||
|
proxyOutbound.Should().NotBeNull();
|
||||||
|
proxyOutbound!.type.Should().Be("shadowsocks");
|
||||||
|
proxyOutbound.server.Should().Be("1.2.3.4");
|
||||||
|
proxyOutbound.server_port.Should().Be(8388);
|
||||||
|
proxyOutbound.method.Should().Be("aes-128-gcm");
|
||||||
|
proxyOutbound.password.Should().Be("custom_password");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,39 @@ public class CoreConfigV2rayServiceTests
|
|||||||
v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
|
v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
var node = CoreConfigTestFactory.CreateHttpNode(ECoreType.Xray);
|
||||||
|
node.SetProtocolExtra(node.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
HttpHeaders = "{\"User-Agent\":\"v2rayN\",\"Set-Cookie\":[\"a=1\",\"b=2\"]}",
|
||||||
|
});
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var outbound = cfg.outbounds.First(o => o.tag == Global.ProxyTag && o.protocol == "http");
|
||||||
|
|
||||||
|
outbound.settings.address?.ToString().Should().Be("proxy.example.com");
|
||||||
|
outbound.settings.port.Should().Be(8080);
|
||||||
|
outbound.settings.user.Should().Be("user");
|
||||||
|
outbound.settings.pass.Should().Be("pass");
|
||||||
|
outbound.settings.level.Should().Be(1);
|
||||||
|
outbound.settings.headers.Should().NotBeNull();
|
||||||
|
var headers = JsonUtils.ParseJson(outbound.settings.headers.ToString());
|
||||||
|
headers["User-Agent"]!.GetValue<string>().Should().Be("v2rayN");
|
||||||
|
headers["Set-Cookie"]!.AsArray()
|
||||||
|
.Select(item => item!.GetValue<string>())
|
||||||
|
.Should().Equal("a=1", "b=2");
|
||||||
|
outbound.settings.servers.Should().BeNull();
|
||||||
|
outbound.settings.vnext.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
|
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
|
||||||
{
|
{
|
||||||
@@ -534,6 +567,68 @@ public class CoreConfigV2rayServiceTests
|
|||||||
|
|
||||||
var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom");
|
var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom");
|
||||||
directOutbound.Should().NotBeNull();
|
directOutbound.Should().NotBeNull();
|
||||||
directOutbound!.settings.domainStrategy.Should().Be("UseIPv4");
|
directOutbound!.streamSettings.sockopt!.domainStrategy.Should().Be("UseIPv4");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_TunRouteExcludeAddress()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||||
|
|
||||||
|
tunInbound.Should().NotBeNull();
|
||||||
|
|
||||||
|
tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0");
|
||||||
|
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
|
||||||
|
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
|
||||||
|
{
|
||||||
|
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var customNode = CoreConfigTestFactory.CreateCustomOutboundNode(ECoreType.Xray, "n-custom", "custom-xray");
|
||||||
|
var customJsonContent = """
|
||||||
|
{
|
||||||
|
"protocol": "shadowsocks",
|
||||||
|
"settings": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"address": "1.2.3.4",
|
||||||
|
"port": 8388,
|
||||||
|
"method": "aes-128-gcm",
|
||||||
|
"password": "custom_password"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, customNode, ECoreType.Xray);
|
||||||
|
context.CustomOutboundContent[customNode.IndexId] = customJsonContent;
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||||
|
result.Data.Should().NotBeNull();
|
||||||
|
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
|
||||||
|
cfg.Should().NotBeNull();
|
||||||
|
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
|
||||||
|
proxyOutbound.Should().NotBeNull();
|
||||||
|
proxyOutbound!.protocol.Should().Be("shadowsocks");
|
||||||
|
proxyOutbound.settings.servers.Should().NotBeNull();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
91
v2rayN/ServiceLib.Tests/Fmt/HyRealmTests.cs
Normal file
91
v2rayN/ServiceLib.Tests/Fmt/HyRealmTests.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
using ServiceLib.Handler.Fmt;
|
||||||
|
using ServiceLib.Models.Dto;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.Fmt;
|
||||||
|
|
||||||
|
public class HyRealmTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void TryParse_ShouldParseValidRealm()
|
||||||
|
{
|
||||||
|
var str = "realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478";
|
||||||
|
var result = HyRealm.TryParse(str, out var realm);
|
||||||
|
result.Should().BeTrue();
|
||||||
|
realm.Should().NotBeNull();
|
||||||
|
|
||||||
|
realm.IsHttp.Should().BeFalse();
|
||||||
|
realm.Token.Should().Be("public");
|
||||||
|
realm.RendezvousHost.Should().Be("realm.hy2.io");
|
||||||
|
realm.RendezvousPort.Should().Be(443);
|
||||||
|
realm.RealmName.Should().Be("57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
|
||||||
|
realm.StunList.Should().HaveCount(2);
|
||||||
|
realm.StunList.Should().Contain("example.stun:3478");
|
||||||
|
realm.StunList.Should().Contain("example2.stun:3478");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToUri_ShouldGenerateValidUri()
|
||||||
|
{
|
||||||
|
var realm = new HyRealm(
|
||||||
|
IsHttp: false,
|
||||||
|
Token: "public",
|
||||||
|
RendezvousHost: "realm.hy2.io",
|
||||||
|
RendezvousPort: 443,
|
||||||
|
RealmName: "57f9be7c-2810-4f5b-8cb9-260bc84d6c90",
|
||||||
|
StunList: ["example.stun:3478", "example2.stun:3478"]
|
||||||
|
);
|
||||||
|
var uri = realm.ToUri();
|
||||||
|
uri.Should().Contain("realm://public@realm.hy2.io");
|
||||||
|
uri.Should().Contain("/57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
|
||||||
|
uri.Should().Contain("stun=example.stun:3478");
|
||||||
|
uri.Should().Contain("stun=example2.stun:3478");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetShareUriAndResolveConfig_Hy2Realm_ShouldRoundTripBasicFields()
|
||||||
|
{
|
||||||
|
var str = "hysteria2+realm://mytoken@rendezvous.example.com/my-cabin-1f3a8c2e9b?auth=your_password&insecure=1&pinSHA256=deadbeef#remark";
|
||||||
|
var resolved = Hysteria2Fmt.ResolveRealm(str, out var msg);
|
||||||
|
resolved.Should().NotBeNull();
|
||||||
|
resolved.Password.Should().Be("your_password");
|
||||||
|
var result = HyRealm.TryParse(resolved.GetProtocolExtra().Hy2RealmUrl, out var realm);
|
||||||
|
result.Should().BeTrue();
|
||||||
|
realm.Should().NotBeNull();
|
||||||
|
realm.Token.Should().Be("mytoken");
|
||||||
|
|
||||||
|
// To uri
|
||||||
|
var uri = Hysteria2Fmt.ToUri(resolved);
|
||||||
|
uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com");
|
||||||
|
uri.Should().EndWith("#remark");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToServerUrl_ShouldIncludeSchemeForSingbox()
|
||||||
|
{
|
||||||
|
var realm = new HyRealm(
|
||||||
|
IsHttp: false,
|
||||||
|
Token: "public",
|
||||||
|
RendezvousHost: "realm.hy2.io",
|
||||||
|
RendezvousPort: 443,
|
||||||
|
RealmName: "my-realm-id",
|
||||||
|
StunList: ["turn.cloudflare.com:3478"]
|
||||||
|
);
|
||||||
|
|
||||||
|
realm.ToServerUrl().Should().Be("https://realm.hy2.io:443");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveRealm_Issue9635_ShouldProduceHttpsServerUrl()
|
||||||
|
{
|
||||||
|
var str = "hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
|
||||||
|
var resolved = Hysteria2Fmt.ResolveRealm(str, out _);
|
||||||
|
resolved.Should().NotBeNull();
|
||||||
|
|
||||||
|
HyRealm.TryParse(resolved!.GetProtocolExtra().Hy2RealmUrl, out var realm).Should().BeTrue();
|
||||||
|
realm!.ToServerUrl().Should().StartWith("https://");
|
||||||
|
realm.ToServerUrl().Should().Contain("realm.hy2.io");
|
||||||
|
realm.StunList.Should().Contain("turn.cloudflare.com:3478");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,6 @@ global using System.Diagnostics;
|
|||||||
global using System.Net;
|
global using System.Net;
|
||||||
global using System.Net.NetworkInformation;
|
global using System.Net.NetworkInformation;
|
||||||
global using System.Net.Sockets;
|
global using System.Net.Sockets;
|
||||||
global using System.Reactive;
|
|
||||||
global using System.Reactive.Disposables;
|
|
||||||
global using System.Reactive.Linq;
|
|
||||||
global using System.Reflection;
|
global using System.Reflection;
|
||||||
global using System.Runtime.InteropServices;
|
global using System.Runtime.InteropServices;
|
||||||
global using System.Security.Cryptography;
|
global using System.Security.Cryptography;
|
||||||
@@ -15,10 +12,7 @@ global using System.Text.Json;
|
|||||||
global using System.Text.Json.Nodes;
|
global using System.Text.Json.Nodes;
|
||||||
global using System.Text.Json.Serialization;
|
global using System.Text.Json.Serialization;
|
||||||
global using System.Text.RegularExpressions;
|
global using System.Text.RegularExpressions;
|
||||||
global using DynamicData;
|
|
||||||
global using DynamicData.Binding;
|
|
||||||
global using ReactiveUI;
|
global using ReactiveUI;
|
||||||
global using ReactiveUI.Fody.Helpers;
|
|
||||||
global using ServiceLib.Base;
|
global using ServiceLib.Base;
|
||||||
global using ServiceLib.Common;
|
global using ServiceLib.Common;
|
||||||
global using ServiceLib.Enums;
|
global using ServiceLib.Enums;
|
||||||
|
|||||||
44
v2rayN/ServiceLib.Tests/Manager/CoreManagerTests.cs
Normal file
44
v2rayN/ServiceLib.Tests/Manager/CoreManagerTests.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
using ServiceLib.Enums;
|
||||||
|
using ServiceLib.Manager;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.Manager;
|
||||||
|
|
||||||
|
public class CoreManagerTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ECoreType.sing_box)]
|
||||||
|
[InlineData(ECoreType.mihomo)]
|
||||||
|
[InlineData(ECoreType.Xray)]
|
||||||
|
public void ShouldRunAsSudo_TunLaunchOnNonWindows_RequiresElevation(ECoreType coreType)
|
||||||
|
{
|
||||||
|
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldRunAsSudo_NonTunLaunch_ShouldNotElevate()
|
||||||
|
{
|
||||||
|
// Regression guard for the macOS TUN failure: the elevation decision must follow
|
||||||
|
// the context snapshot that generated the config. A launch whose snapshot has TUN
|
||||||
|
// disabled must never elevate, and a launch whose snapshot has TUN enabled must
|
||||||
|
// elevate regardless of later changes to the live config.
|
||||||
|
CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.sing_box, isNonWindows: true).Should().BeFalse();
|
||||||
|
CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.Xray, isNonWindows: true).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldRunAsSudo_OnWindows_ShouldNotElevate()
|
||||||
|
{
|
||||||
|
CoreManager.ShouldRunAsSudo(isTunLaunch: true, ECoreType.sing_box, isNonWindows: false).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ECoreType.v2fly)]
|
||||||
|
[InlineData(ECoreType.hysteria)]
|
||||||
|
[InlineData(null)]
|
||||||
|
public void ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType)
|
||||||
|
{
|
||||||
|
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
66
v2rayN/ServiceLib/Base/BulkObservableCollection.cs
Normal file
66
v2rayN/ServiceLib/Base/BulkObservableCollection.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
namespace ServiceLib.Base;
|
||||||
|
|
||||||
|
public class BulkObservableCollection<T> : ObservableCollection<T>
|
||||||
|
{
|
||||||
|
private bool _suppressNotification = false;
|
||||||
|
|
||||||
|
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_suppressNotification)
|
||||||
|
{
|
||||||
|
base.OnCollectionChanged(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_suppressNotification)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRange(IEnumerable<T>? collection)
|
||||||
|
{
|
||||||
|
if (collection == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_suppressNotification = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var item in collection)
|
||||||
|
{
|
||||||
|
Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressNotification = false;
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
|
||||||
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Replace(T oldItem, T newItem)
|
||||||
|
{
|
||||||
|
var index = Items.IndexOf(oldItem);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Items[index] = newItem;
|
||||||
|
|
||||||
|
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
|
||||||
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
|
||||||
|
NotifyCollectionChangedAction.Replace,
|
||||||
|
newItem,
|
||||||
|
oldItem,
|
||||||
|
index));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
v2rayN/ServiceLib/Base/ICloseable.cs
Normal file
6
v2rayN/ServiceLib/Base/ICloseable.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ServiceLib.Base;
|
||||||
|
|
||||||
|
public interface ICloseable
|
||||||
|
{
|
||||||
|
public event EventHandler? RequestClose;
|
||||||
|
}
|
||||||
7
v2rayN/ServiceLib/Base/IWindowDialog.cs
Normal file
7
v2rayN/ServiceLib/Base/IWindowDialog.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ServiceLib.Base;
|
||||||
|
|
||||||
|
public interface IWindowDialog
|
||||||
|
{
|
||||||
|
public Task<bool> ShowDialogAsync<TViewModel>(TViewModel vm)
|
||||||
|
where TViewModel : class;
|
||||||
|
}
|
||||||
@@ -3,5 +3,4 @@ namespace ServiceLib.Base;
|
|||||||
public class MyReactiveObject : ReactiveObject
|
public class MyReactiveObject : ReactiveObject
|
||||||
{
|
{
|
||||||
protected static Config? _config;
|
protected static Config? _config;
|
||||||
protected Func<EViewAction, object?, Task<bool>>? _updateView;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,6 @@ public static class CountryExtension
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CountryEmojiMap.TryGetValue(countryCode, out var emoji) ? emoji : null;
|
return CountryEmojiMap.GetValueOrDefault(countryCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public static class Extension
|
|||||||
|
|
||||||
public static string TrimEx(this string? value)
|
public static string TrimEx(this string? value)
|
||||||
{
|
{
|
||||||
return value == null ? string.Empty : value.Trim();
|
return value?.Trim() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string RemovePrefix(this string value, char prefix)
|
public static string RemovePrefix(this string value, char prefix)
|
||||||
@@ -92,7 +92,7 @@ public static class Extension
|
|||||||
|
|
||||||
public static bool IsComplexType(this EConfigType configType)
|
public static bool IsComplexType(this EConfigType configType)
|
||||||
{
|
{
|
||||||
return configType is EConfigType.Custom or EConfigType.PolicyGroup or EConfigType.ProxyChain;
|
return configType is EConfigType.Custom or EConfigType.Outbound or EConfigType.PolicyGroup or EConfigType.ProxyChain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -118,4 +118,20 @@ public static class Extension
|
|||||||
destination.Add(item);
|
destination.Add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replace all cross-platform newline characters with the specified string
|
||||||
|
/// </summary>
|
||||||
|
public static string ReplaceLineBreaks(this string input, string replacement)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// You must replace \r\n first, and then replace the single characters \r and \n.
|
||||||
|
return input.Replace("\r\n", replacement)
|
||||||
|
.Replace("\r", replacement)
|
||||||
|
.Replace("\n", replacement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ public static class FileUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DeleteExpiredFiles(string sourceDir, DateTime dtLine)
|
public static void DeleteExpiredFiles(string sourceDir, DateTime dtLine, string? contains = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -214,6 +214,10 @@ public static class FileUtils
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (contains.IsNotEmpty() && !file.Name.Contains(contains))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
file.Delete();
|
file.Delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class Utils
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
str = str.Replace(Environment.NewLine, string.Empty);
|
str = str.ReplaceLineBreaks(string.Empty);
|
||||||
return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
return new List<string>(str.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -114,9 +114,7 @@ public class Utils
|
|||||||
}
|
}
|
||||||
|
|
||||||
plainText = plainText.Trim()
|
plainText = plainText.Trim()
|
||||||
.Replace(Environment.NewLine, "")
|
.ReplaceLineBreaks("")
|
||||||
.Replace("\n", "")
|
|
||||||
.Replace("\r", "")
|
|
||||||
.Replace('_', '/')
|
.Replace('_', '/')
|
||||||
.Replace('-', '+')
|
.Replace('-', '+')
|
||||||
.Replace(" ", "");
|
.Replace(" ", "");
|
||||||
@@ -321,7 +319,32 @@ public class Utils
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return text.Replace(",", ",").Replace(Environment.NewLine, ",");
|
return text.Replace(",", ",")
|
||||||
|
.Replace(" ", "")
|
||||||
|
.ReplaceLineBreaks(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ParseProcess(string text)
|
||||||
|
{
|
||||||
|
if (text.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
if (text.StartsWith('"'))
|
||||||
|
{
|
||||||
|
text = text[1..];
|
||||||
|
}
|
||||||
|
if (text.EndsWith('"'))
|
||||||
|
{
|
||||||
|
text = text[..^1];
|
||||||
|
}
|
||||||
|
return List2String(text.Replace(",", ",")
|
||||||
|
.Replace("\\", "/")
|
||||||
|
.ReplaceLineBreaks(",")
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(x => x.TrimEx())
|
||||||
|
.Where(x => x.IsNotEmpty())
|
||||||
|
.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<string> GetEnumNames<TEnum>() where TEnum : Enum
|
public static List<string> GetEnumNames<TEnum>() where TEnum : Enum
|
||||||
@@ -462,12 +485,17 @@ public class Utils
|
|||||||
|
|
||||||
public static string? DomainStrategy4Sbox(string? strategy)
|
public static string? DomainStrategy4Sbox(string? strategy)
|
||||||
{
|
{
|
||||||
|
if (strategy is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return strategy switch
|
return strategy switch
|
||||||
{
|
{
|
||||||
not null when strategy.StartsWith("UseIPv4") => "prefer_ipv4",
|
_ when strategy.StartsWith("UseIPv6") => "prefer_ipv6",
|
||||||
not null when strategy.StartsWith("UseIPv6") => "prefer_ipv6",
|
_ when strategy.StartsWith("UseIP") => "prefer_ipv4",
|
||||||
not null when strategy.StartsWith("ForceIPv4") => "ipv4_only",
|
_ when strategy.StartsWith("ForceIPv6") => "ipv6_only",
|
||||||
not null when strategy.StartsWith("ForceIPv6") => "ipv6_only",
|
_ when strategy.StartsWith("ForceIP") => "ipv4_only",
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -576,6 +604,58 @@ public class Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryParseRange(string? input, int min, int max, out int from, out int to)
|
||||||
|
{
|
||||||
|
from = to = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var parts = input.Split('-');
|
||||||
|
if (parts.Length == 1)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(parts[0], out from))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
to = from;
|
||||||
|
return from >= min && to <= max;
|
||||||
|
}
|
||||||
|
if (parts.Length != 2
|
||||||
|
|| !int.TryParse(parts[0], out from)
|
||||||
|
|| !int.TryParse(parts[1], out to))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return from >= min && to <= max && from <= to;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseMaxSplit(string? input, int min, int max, out int from, out int to)
|
||||||
|
{
|
||||||
|
from = to = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var parts = input.Split('-');
|
||||||
|
if (parts.Length == 1)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(parts[0], out from))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
to = from;
|
||||||
|
return from >= min && to <= max;
|
||||||
|
}
|
||||||
|
if (parts.Length != 2
|
||||||
|
|| !int.TryParse(parts[0], out from)
|
||||||
|
|| !int.TryParse(parts[1], out to))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return from >= min && to <= max && from <= to;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool IsPrivateNetwork(string ip)
|
public static bool IsPrivateNetwork(string ip)
|
||||||
{
|
{
|
||||||
if (IPAddress.TryParse(ip, out var address))
|
if (IPAddress.TryParse(ip, out var address))
|
||||||
@@ -673,11 +753,11 @@ public class Utils
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int GetFreePort(int defaultPort = 0)
|
public static int GetFreePort(int defaultPort)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!(defaultPort == 0 || Utils.PortInUse(defaultPort)))
|
if (!PortInUse(defaultPort))
|
||||||
{
|
{
|
||||||
return defaultPort;
|
return defaultPort;
|
||||||
}
|
}
|
||||||
@@ -716,6 +796,24 @@ public class Utils
|
|||||||
return (endpoints, connections);
|
return (endpoints, connections);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsLocalIP(string ipAddress)
|
||||||
|
{
|
||||||
|
if (!IPAddress.TryParse(ipAddress, out var targetAddress))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NetworkInterface.GetAllNetworkInterfaces()
|
||||||
|
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
|
||||||
|
.Any(ua => ua.Address.Equals(targetAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ContainsInterfaceName(string inInterfaceName)
|
||||||
|
{
|
||||||
|
return NetworkInterface.GetAllNetworkInterfaces()
|
||||||
|
.Any(ni => ni.Name.Equals(inInterfaceName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Speed Test
|
#endregion Speed Test
|
||||||
|
|
||||||
#region Miscellaneous
|
#region Miscellaneous
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ internal static class WindowsUtils
|
|||||||
|
|
||||||
public static async Task RemoveTunDevice()
|
public static async Task RemoveTunDevice()
|
||||||
{
|
{
|
||||||
var tunNameList = new List<string> { "wintunsingbox_tun", "xray_tun" };
|
var tunNameList = new List<string> { "singbox_tun", "xray_tun" };
|
||||||
foreach (var tunName in tunNameList)
|
foreach (var tunName in tunNameList)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sum = MD5.HashData(Encoding.UTF8.GetBytes(tunName));
|
var sum = MD5.HashData(Encoding.UTF8.GetBytes($"wintun{tunName}"));
|
||||||
var guid = new Guid(sum);
|
var guid = new Guid(sum);
|
||||||
var pnpUtilPath = @"C:\Windows\System32\pnputil.exe";
|
var pnpUtilPath = @"C:\Windows\System32\pnputil.exe";
|
||||||
var arg = $$""" /remove-device "SWD\Wintun\{{{guid}}}" """;
|
var arg = $$""" /remove-device "SWD\Wintun\{{{guid}}}" """;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public enum EConfigType
|
|||||||
HTTP = 10,
|
HTTP = 10,
|
||||||
Anytls = 11,
|
Anytls = 11,
|
||||||
Naive = 12,
|
Naive = 12,
|
||||||
|
Outbound = 13,
|
||||||
PolicyGroup = 101,
|
PolicyGroup = 101,
|
||||||
ProxyChain = 102,
|
ProxyChain = 102,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
namespace ServiceLib.Enums;
|
|
||||||
|
|
||||||
public enum EViewAction
|
|
||||||
{
|
|
||||||
CloseWindow,
|
|
||||||
ShowYesNo,
|
|
||||||
SaveFileDialog,
|
|
||||||
AddBatchRoutingRulesYesNo,
|
|
||||||
SetClipboardData,
|
|
||||||
AddServerViaClipboard,
|
|
||||||
ImportRulesFromClipboard,
|
|
||||||
ProfilesFocus,
|
|
||||||
ShareSub,
|
|
||||||
ShareServer,
|
|
||||||
ScanScreenTask,
|
|
||||||
ScanImageTask,
|
|
||||||
BrowseServer,
|
|
||||||
ImportRulesFromFile,
|
|
||||||
InitSettingFont,
|
|
||||||
PasswordInput,
|
|
||||||
SubEditWindow,
|
|
||||||
RoutingRuleSettingWindow,
|
|
||||||
RoutingRuleDetailsWindow,
|
|
||||||
AddServerWindow,
|
|
||||||
AddServer2Window,
|
|
||||||
AddGroupServerWindow,
|
|
||||||
DNSSettingWindow,
|
|
||||||
RoutingSettingWindow,
|
|
||||||
OptionSettingWindow,
|
|
||||||
FullConfigTemplateWindow,
|
|
||||||
GlobalHotkeySettingWindow,
|
|
||||||
SubSettingWindow,
|
|
||||||
DispatcherRefreshServersBiz,
|
|
||||||
DispatcherRefreshIcon,
|
|
||||||
DispatcherShowMsg,
|
|
||||||
}
|
|
||||||
@@ -2,30 +2,16 @@ namespace ServiceLib.Events;
|
|||||||
|
|
||||||
public static class AppEvents
|
public static class AppEvents
|
||||||
{
|
{
|
||||||
public static readonly EventChannel<Unit> ReloadRequested = new();
|
public static readonly EventChannel<RxVoid> AddServerViaClipboardRequested = new();
|
||||||
public static readonly EventChannel<bool?> ShowHideWindowRequested = new();
|
|
||||||
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
|
|
||||||
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
|
|
||||||
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
|
|
||||||
public static readonly EventChannel<bool> HasUpdateNotified = new();
|
public static readonly EventChannel<bool> HasUpdateNotified = new();
|
||||||
|
|
||||||
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
|
|
||||||
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
|
|
||||||
public static readonly EventChannel<Unit> ProxiesReloadRequested = new();
|
|
||||||
public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new();
|
public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new();
|
||||||
|
|
||||||
public static readonly EventChannel<string> SendSnackMsgRequested = new();
|
public static readonly EventChannel<string> SendSnackMsgRequested = new();
|
||||||
public static readonly EventChannel<string> SendMsgViewRequested = new();
|
public static readonly EventChannel<string> SendMsgViewRequested = new();
|
||||||
|
|
||||||
public static readonly EventChannel<Unit> AppExitRequested = new();
|
public static readonly EventChannel<RxVoid> AppExitRequested = new();
|
||||||
public static readonly EventChannel<bool> ShutdownRequested = new();
|
public static readonly EventChannel<bool> ShutdownRequested = new();
|
||||||
|
|
||||||
public static readonly EventChannel<Unit> AdjustMainLvColWidthRequested = new();
|
|
||||||
|
|
||||||
public static readonly EventChannel<string> SetDefaultServerRequested = new();
|
|
||||||
|
|
||||||
public static readonly EventChannel<Unit> RoutingsMenuRefreshRequested = new();
|
|
||||||
public static readonly EventChannel<Unit> TestServerRequested = new();
|
|
||||||
public static readonly EventChannel<Unit> InboundDisplayRequested = new();
|
|
||||||
public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new();
|
public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,37 @@
|
|||||||
using System.Reactive.Subjects;
|
|
||||||
|
|
||||||
namespace ServiceLib.Events;
|
namespace ServiceLib.Events;
|
||||||
|
|
||||||
public sealed class EventChannel<T>
|
public sealed class EventChannel<T>
|
||||||
{
|
{
|
||||||
private readonly ISubject<T> _subject = Subject.Synchronize(new Subject<T>());
|
private readonly Signal<T> _signal = new();
|
||||||
|
private readonly Lock _gate = new();
|
||||||
|
private readonly IObservable<T> _observable;
|
||||||
|
public EventChannel()
|
||||||
|
{
|
||||||
|
_observable = _signal.Synchronize(_gate);
|
||||||
|
}
|
||||||
|
|
||||||
public IObservable<T> AsObservable()
|
public IObservable<T> AsObservable()
|
||||||
{
|
{
|
||||||
return _subject.AsObservable();
|
return _observable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Publish(T value)
|
public void Publish(T value)
|
||||||
{
|
{
|
||||||
_subject.OnNext(value);
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_signal.OnNext(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Publish()
|
public void Publish()
|
||||||
{
|
{
|
||||||
if (typeof(T) != typeof(Unit))
|
if (typeof(T) != typeof(RxVoid))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Publish() without value is only valid for EventChannel<Unit>.");
|
throw new InvalidOperationException("Publish() without value is only valid for EventChannel<RxVoid>.");
|
||||||
|
}
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_signal.OnNext((T)(object)RxVoid.Default);
|
||||||
}
|
}
|
||||||
_subject.OnNext((T)(object)Unit.Default);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
|
||||||
<ReactiveUI />
|
|
||||||
</Weavers>
|
|
||||||
@@ -2,8 +2,6 @@ namespace ServiceLib;
|
|||||||
|
|
||||||
public class Global
|
public class Global
|
||||||
{
|
{
|
||||||
#region const
|
|
||||||
|
|
||||||
public const string AppName = "v2rayN";
|
public const string AppName = "v2rayN";
|
||||||
public const string GithubUrl = "https://github.com";
|
public const string GithubUrl = "https://github.com";
|
||||||
public const string GithubApiUrl = "https://api.github.com/repos";
|
public const string GithubApiUrl = "https://api.github.com/repos";
|
||||||
@@ -42,6 +40,11 @@ public class Global
|
|||||||
public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh";
|
public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh";
|
||||||
public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh";
|
public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh";
|
||||||
public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter";
|
public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter";
|
||||||
|
public const string ChromeRootCertFileName = NamespaceSample + "chrome_roots_pem";
|
||||||
|
public const string MozillaRootCertFileName = NamespaceSample + "mozilla_roots_pem";
|
||||||
|
|
||||||
|
public const string ChromeRootProvider = "chrome";
|
||||||
|
public const string MozillaRootProvider = "mozilla";
|
||||||
|
|
||||||
public const string DefaultSecurity = "auto";
|
public const string DefaultSecurity = "auto";
|
||||||
public const string DefaultNetwork = "raw";
|
public const string DefaultNetwork = "raw";
|
||||||
@@ -90,6 +93,9 @@ public class Global
|
|||||||
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
|
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
|
||||||
public const int SpeedTestPageSize = 1000;
|
public const int SpeedTestPageSize = 1000;
|
||||||
public const string LinuxBash = "/bin/bash";
|
public const string LinuxBash = "/bin/bash";
|
||||||
|
public const string StringTrue = "true";
|
||||||
|
public const string StringFalse = "false";
|
||||||
|
public const int SqliteMaxBatchSize = 10000;
|
||||||
|
|
||||||
public const string SingboxDirectDNSTag = "direct_dns";
|
public const string SingboxDirectDNSTag = "direct_dns";
|
||||||
public const string SingboxRemoteDNSTag = "remote_dns";
|
public const string SingboxRemoteDNSTag = "remote_dns";
|
||||||
@@ -98,9 +104,7 @@ public class Global
|
|||||||
public const string SingboxFakeDNSTag = "fake_dns";
|
public const string SingboxFakeDNSTag = "fake_dns";
|
||||||
|
|
||||||
public const int Hysteria2DefaultHopInt = 30;
|
public const int Hysteria2DefaultHopInt = 30;
|
||||||
|
|
||||||
public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset";
|
public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset";
|
||||||
|
|
||||||
public const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$";
|
public const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$";
|
||||||
|
|
||||||
public static readonly List<string> PolicyGroupDefaultFilterList =
|
public static readonly List<string> PolicyGroupDefaultFilterList =
|
||||||
@@ -208,6 +212,10 @@ public class Global
|
|||||||
|
|
||||||
public const string Hysteria2ProtocolShare = "hy2://";
|
public const string Hysteria2ProtocolShare = "hy2://";
|
||||||
|
|
||||||
|
public const string Hysteria2RealmProtocolShare = "hysteria2+realm://";
|
||||||
|
|
||||||
|
public const string Hysteria2HttpRealmProtocolShare = "hysteria2+realm+http://";
|
||||||
|
|
||||||
public const string NaiveHttpsProtocolShare = "naive+https://";
|
public const string NaiveHttpsProtocolShare = "naive+https://";
|
||||||
|
|
||||||
public const string NaiveQuicProtocolShare = "naive+quic://";
|
public const string NaiveQuicProtocolShare = "naive+quic://";
|
||||||
@@ -330,12 +338,12 @@ public class Global
|
|||||||
|
|
||||||
public static readonly Dictionary<string, string> KcpHeaderMaskMap = new()
|
public static readonly Dictionary<string, string> KcpHeaderMaskMap = new()
|
||||||
{
|
{
|
||||||
{ "srtp", "header-srtp" },
|
{ "srtp", "srtp" },
|
||||||
{ "utp", "header-utp" },
|
{ "utp", "utp" },
|
||||||
{ "wechat-video", "header-wechat" },
|
{ "wechat-video", "wechat" },
|
||||||
{ "dtls", "header-dtls" },
|
{ "dtls", "dtls" },
|
||||||
{ "wireguard", "header-wireguard" },
|
{ "wireguard", "wireguard" },
|
||||||
{ "dns", "header-dns" }
|
{ "dns", "dns" }
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly List<string> CoreTypes =
|
public static readonly List<string> CoreTypes =
|
||||||
@@ -404,6 +412,16 @@ public class Global
|
|||||||
""
|
""
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static readonly List<string> FragmentPacketsOptions =
|
||||||
|
[
|
||||||
|
"tlshello",
|
||||||
|
"1-1",
|
||||||
|
"1-2",
|
||||||
|
"1-3",
|
||||||
|
"1-4",
|
||||||
|
"1-5"
|
||||||
|
];
|
||||||
|
|
||||||
public static readonly List<string> UserAgent =
|
public static readonly List<string> UserAgent =
|
||||||
[
|
[
|
||||||
"chrome",
|
"chrome",
|
||||||
@@ -421,13 +439,6 @@ public class Global
|
|||||||
"stream-one"
|
"stream-one"
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly List<string> AllowInsecure =
|
|
||||||
[
|
|
||||||
"true",
|
|
||||||
"false",
|
|
||||||
""
|
|
||||||
];
|
|
||||||
|
|
||||||
public static readonly List<string> DomainStrategy =
|
public static readonly List<string> DomainStrategy =
|
||||||
[
|
[
|
||||||
"AsIs",
|
"AsIs",
|
||||||
@@ -478,10 +489,11 @@ public class Global
|
|||||||
"zh-Hans",
|
"zh-Hans",
|
||||||
"zh-Hant",
|
"zh-Hant",
|
||||||
"en",
|
"en",
|
||||||
"fa-Ir",
|
"fa",
|
||||||
"fr",
|
"fr",
|
||||||
"ru",
|
"ru",
|
||||||
"hu"
|
"hu",
|
||||||
|
"id"
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly List<string> Alpns =
|
public static readonly List<string> Alpns =
|
||||||
@@ -541,7 +553,6 @@ public class Global
|
|||||||
"http",
|
"http",
|
||||||
"tls",
|
"tls",
|
||||||
"quic",
|
"quic",
|
||||||
"fakedns",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly List<int> TunMtus =
|
public static readonly List<int> TunMtus =
|
||||||
@@ -671,6 +682,14 @@ public class Global
|
|||||||
"mcbe:bedrock.talonmc.net",
|
"mcbe:bedrock.talonmc.net",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static readonly List<string> DefaultRealmStunList =
|
||||||
|
[
|
||||||
|
"turn.cloudflare.com:3478",
|
||||||
|
"stun.nextcloud.com:3478",
|
||||||
|
"stun.sip.us:3478",
|
||||||
|
"global.stun.twilio.com:3478",
|
||||||
|
];
|
||||||
|
|
||||||
public static readonly List<string> OutboundTags =
|
public static readonly List<string> OutboundTags =
|
||||||
[
|
[
|
||||||
ProxyTag,
|
ProxyTag,
|
||||||
@@ -685,15 +704,15 @@ public class Global
|
|||||||
{ "one.one.one.one", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
|
{ "one.one.one.one", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
|
||||||
{ "1dot1dot1dot1.cloudflare-dns.com", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
|
{ "1dot1dot1dot1.cloudflare-dns.com", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] },
|
||||||
{ "cloudflare-dns.com", ["104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9"] },
|
{ "cloudflare-dns.com", ["104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9"] },
|
||||||
{ "dns.cloudflare.com", ["104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5"] },
|
{ "dns.cloudflare.com", ["162.159.61.8", "172.64.41.8", "2a06:98c1:52::8", "2803:f800:53::8"] },
|
||||||
{ "dot.pub", ["1.12.12.12", "120.53.53.53"] },
|
{ "dot.pub", ["1.12.12.12", "120.53.53.53"] },
|
||||||
{ "doh.pub", ["1.12.12.12", "120.53.53.53"] },
|
{ "doh.pub", ["1.12.12.12", "120.53.53.53"] },
|
||||||
{ "dns.quad9.net", ["9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9"] },
|
{ "dns.quad9.net", ["9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9"] },
|
||||||
{ "dns.yandex.net", ["77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff"] },
|
{ "dns.yandex.net", ["77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff"] },
|
||||||
{ "dns.sb", ["185.222.222.222", "2a09::"] },
|
{ "dns.sb", ["45.11.45.11", "185.222.222.222", "2a09::", "2a11::"] },
|
||||||
{ "dns.umbrella.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] },
|
{ "dns.umbrella.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] },
|
||||||
{ "dns.sse.cisco.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] },
|
{ "dns.sse.cisco.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] },
|
||||||
{ "engage.cloudflareclient.com", ["162.159.192.1"] }
|
{ "engage.cloudflareclient.com", ["162.159.192.1", "2606:4700:d0::a29f:c001"] }
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly List<string> ExpectedIPs =
|
public static readonly List<string> ExpectedIPs =
|
||||||
@@ -713,5 +732,42 @@ public class Global
|
|||||||
"reply",
|
"reply",
|
||||||
];
|
];
|
||||||
|
|
||||||
#endregion const
|
public static readonly List<string> FakeIPRanges =
|
||||||
|
[
|
||||||
|
"198.18.0.0/15",
|
||||||
|
"11.0.0.0/8",
|
||||||
|
];
|
||||||
|
|
||||||
|
public static readonly List<string> RootCertProviders =
|
||||||
|
[
|
||||||
|
"system",
|
||||||
|
ChromeRootProvider,
|
||||||
|
MozillaRootProvider,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<string> TunIPv4Address =
|
||||||
|
[
|
||||||
|
"172.18.0.1/30",
|
||||||
|
"172.31.0.1/30",
|
||||||
|
"172.20.0.1/30",
|
||||||
|
"172.16.0.1/30",
|
||||||
|
"192.168.100.1/30",
|
||||||
|
"10.10.14.1/30",
|
||||||
|
"10.1.0.1/30",
|
||||||
|
"10.0.0.1/30",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Prefixes must leave room for a peer address (max /126); the sing-box system
|
||||||
|
// stack derives a gateway from the first prefix and rejects single-address prefixes.
|
||||||
|
public static readonly IReadOnlyList<string> TunIPv6Address =
|
||||||
|
[
|
||||||
|
"fc00::172:18:0:1/126",
|
||||||
|
"fc00::172:31:0:1/126",
|
||||||
|
"fc00::172:20:0:1/126",
|
||||||
|
"fc00::172:16:0:1/126",
|
||||||
|
"fc00::192:168:100:1/126",
|
||||||
|
"fc00::10:10:14:1/126",
|
||||||
|
"fc00::10:1:0:1/126",
|
||||||
|
"fc00::10:0:0:1/126",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
global using System.Collections.Concurrent;
|
global using System.Collections.Concurrent;
|
||||||
|
global using System.Collections.ObjectModel;
|
||||||
|
global using System.Collections.Specialized;
|
||||||
|
global using System.ComponentModel;
|
||||||
global using System.Diagnostics;
|
global using System.Diagnostics;
|
||||||
global using System.Net;
|
global using System.Net;
|
||||||
global using System.Net.NetworkInformation;
|
global using System.Net.NetworkInformation;
|
||||||
global using System.Net.Sockets;
|
global using System.Net.Sockets;
|
||||||
global using System.Reactive;
|
global using ReactiveUI.Primitives;
|
||||||
global using System.Reactive.Disposables;
|
global using ReactiveUI.Primitives.Concurrency;
|
||||||
global using System.Reactive.Linq;
|
global using ReactiveUI.Primitives.Disposables;
|
||||||
|
global using ReactiveUI.Primitives.Signals;
|
||||||
global using System.Reflection;
|
global using System.Reflection;
|
||||||
global using System.Runtime.InteropServices;
|
global using System.Runtime.InteropServices;
|
||||||
global using System.Runtime.Versioning;
|
global using System.Runtime.Versioning;
|
||||||
@@ -16,10 +20,8 @@ global using System.Text.Json;
|
|||||||
global using System.Text.Json.Nodes;
|
global using System.Text.Json.Nodes;
|
||||||
global using System.Text.Json.Serialization;
|
global using System.Text.Json.Serialization;
|
||||||
global using System.Text.RegularExpressions;
|
global using System.Text.RegularExpressions;
|
||||||
global using DynamicData;
|
|
||||||
global using DynamicData.Binding;
|
|
||||||
global using ReactiveUI;
|
global using ReactiveUI;
|
||||||
global using ReactiveUI.Fody.Helpers;
|
global using ReactiveUI.SourceGenerators;
|
||||||
global using ServiceLib.Base;
|
global using ServiceLib.Base;
|
||||||
global using ServiceLib.Common;
|
global using ServiceLib.Common;
|
||||||
global using ServiceLib.Enums;
|
global using ServiceLib.Enums;
|
||||||
@@ -30,8 +32,8 @@ global using ServiceLib.Handler.Fmt;
|
|||||||
global using ServiceLib.Handler.SysProxy;
|
global using ServiceLib.Handler.SysProxy;
|
||||||
global using ServiceLib.Helper;
|
global using ServiceLib.Helper;
|
||||||
global using ServiceLib.Manager;
|
global using ServiceLib.Manager;
|
||||||
global using ServiceLib.Models.CoreConfigs;
|
|
||||||
global using ServiceLib.Models.Configs;
|
global using ServiceLib.Models.Configs;
|
||||||
|
global using ServiceLib.Models.CoreConfigs;
|
||||||
global using ServiceLib.Models.Dto;
|
global using ServiceLib.Models.Dto;
|
||||||
global using ServiceLib.Models.Entities;
|
global using ServiceLib.Models.Entities;
|
||||||
global using ServiceLib.Resx;
|
global using ServiceLib.Resx;
|
||||||
@@ -39,3 +41,5 @@ global using ServiceLib.Services;
|
|||||||
global using ServiceLib.Services.CoreConfig;
|
global using ServiceLib.Services.CoreConfig;
|
||||||
global using ServiceLib.Services.Statistics;
|
global using ServiceLib.Services.Statistics;
|
||||||
global using SQLite;
|
global using SQLite;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace ServiceLib.Handler.Builder;
|
namespace ServiceLib.Handler.Builder;
|
||||||
|
|
||||||
public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult)
|
public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult)
|
||||||
@@ -6,8 +8,8 @@ public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeVali
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Holds the results of a full context build, including the main context and an optional
|
/// Holds the results of a full context build, including the main context and an optional
|
||||||
/// pre-socks context (e.g. for TUN protection or pre-socks chaining).
|
/// pre-socks context (e.g. for TUN protection or pre-socks chaining).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record CoreConfigContextBuilderAllResult(
|
public record CoreConfigContextBuilderAllResult(
|
||||||
CoreConfigContextBuilderResult MainResult,
|
CoreConfigContextBuilderResult MainResult,
|
||||||
@@ -17,8 +19,8 @@ public record CoreConfigContextBuilderAllResult(
|
|||||||
public bool Success => MainResult.Success && (PreSocksResult?.Success ?? true);
|
public bool Success => MainResult.Success && (PreSocksResult?.Success ?? true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges all errors and warnings from the main result and the optional pre-socks result
|
/// Merges all errors and warnings from the main result and the optional pre-socks result
|
||||||
/// into a single <see cref="NodeValidatorResult"/> for unified notification.
|
/// into a single <see cref="NodeValidatorResult" /> for unified notification.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NodeValidatorResult CombinedValidatorResult => new(
|
public NodeValidatorResult CombinedValidatorResult => new(
|
||||||
[.. MainResult.ValidatorResult.Errors, .. PreSocksResult?.ValidatorResult.Errors ?? []],
|
[.. MainResult.ValidatorResult.Errors, .. PreSocksResult?.ValidatorResult.Errors ?? []],
|
||||||
@@ -28,14 +30,14 @@ public record CoreConfigContextBuilderAllResult(
|
|||||||
public class CoreConfigContextBuilder
|
public class CoreConfigContextBuilder
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a <see cref="CoreConfigContext"/> for the given node, resolves its proxy map,
|
/// Builds a <see cref="CoreConfigContext" /> for the given node, resolves its proxy map,
|
||||||
/// and processes outbound nodes referenced by routing rules.
|
/// and processes outbound nodes referenced by routing rules.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<CoreConfigContextBuilderResult> Build(Config config, ProfileItem node)
|
public static async Task<CoreConfigContextBuilderResult> Build(Config config, ProfileItem node)
|
||||||
{
|
{
|
||||||
var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
|
var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
|
||||||
var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
|
var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
|
||||||
var context = new CoreConfigContext()
|
var context = new CoreConfigContext
|
||||||
{
|
{
|
||||||
Node = node,
|
Node = node,
|
||||||
RunCoreType = runCoreType,
|
RunCoreType = runCoreType,
|
||||||
@@ -49,6 +51,7 @@ public class CoreConfigContextBuilder
|
|||||||
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
|
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
|
||||||
IsWindows = Utils.IsWindows(),
|
IsWindows = Utils.IsWindows(),
|
||||||
IsMacOS = Utils.IsMacOS(),
|
IsMacOS = Utils.IsMacOS(),
|
||||||
|
ProtectCoreTypeList = config.TunModeItem.EnableTun ? [ECoreType.Xray, ECoreType.sing_box] : []
|
||||||
};
|
};
|
||||||
var validatorResult = NodeValidatorResult.Empty();
|
var validatorResult = NodeValidatorResult.Empty();
|
||||||
var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node);
|
var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node);
|
||||||
@@ -61,7 +64,8 @@ public class CoreConfigContextBuilder
|
|||||||
if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true))
|
if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true))
|
||||||
{
|
{
|
||||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet) ?? [];
|
var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet) ?? [];
|
||||||
foreach (var ruleItem in rules.Where(ruleItem => ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag)))
|
foreach (var ruleItem in rules.Where(ruleItem =>
|
||||||
|
ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag)))
|
||||||
{
|
{
|
||||||
if (ruleItem.OutboundTag.IsNullOrEmpty())
|
if (ruleItem.OutboundTag.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -72,7 +76,8 @@ public class CoreConfigContextBuilder
|
|||||||
var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag);
|
var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag);
|
||||||
if (ruleOutboundNode == null)
|
if (ruleOutboundNode == null)
|
||||||
{
|
{
|
||||||
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound, ruleItem.Remarks, ruleItem.OutboundTag));
|
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound,
|
||||||
|
ruleItem.Remarks, ruleItem.OutboundTag));
|
||||||
ruleItem.OutboundTag = Global.ProxyTag;
|
ruleItem.OutboundTag = Global.ProxyTag;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -83,7 +88,8 @@ public class CoreConfigContextBuilder
|
|||||||
if (!ruleNodeValidatorResult.Success)
|
if (!ruleNodeValidatorResult.Success)
|
||||||
{
|
{
|
||||||
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e =>
|
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e =>
|
||||||
string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag, e)));
|
string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag,
|
||||||
|
e)));
|
||||||
ruleItem.OutboundTag = Global.ProxyTag;
|
ruleItem.OutboundTag = Global.ProxyTag;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -92,13 +98,50 @@ public class CoreConfigContextBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
|
||||||
|
{
|
||||||
|
var appConfig = JsonUtils.DeepCopy(config);
|
||||||
|
var routeExcludeAddressList = new List<string>();
|
||||||
|
foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IPNetwork2.Parse(addr);
|
||||||
|
routeExcludeAddressList.Add(addr);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
validatorResult.Warnings.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appConfig.TunModeItem.RouteExcludeAddress = routeExcludeAddressList;
|
||||||
|
context = context with { AppConfig = appConfig };
|
||||||
|
}
|
||||||
|
if (!context.AppConfig.CoreBasicItem.SendThrough.IsNullOrEmpty()
|
||||||
|
&& !Utils.IsLocalIP(context.AppConfig.CoreBasicItem.SendThrough))
|
||||||
|
{
|
||||||
|
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsSendThrough));
|
||||||
|
var appConfig = JsonUtils.DeepCopy(config);
|
||||||
|
appConfig.CoreBasicItem.SendThrough = string.Empty;
|
||||||
|
context = context with { AppConfig = appConfig };
|
||||||
|
}
|
||||||
|
if (!context.AppConfig.CoreBasicItem.BindInterface.IsNullOrEmpty()
|
||||||
|
&& (!Utils.ContainsInterfaceName(context.AppConfig.CoreBasicItem.BindInterface)
|
||||||
|
|| !(context.IsTunEnabled || context.IsWindows)))
|
||||||
|
{
|
||||||
|
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsBindInterface));
|
||||||
|
var appConfig = JsonUtils.DeepCopy(config);
|
||||||
|
appConfig.CoreBasicItem.BindInterface = string.Empty;
|
||||||
|
context = context with { AppConfig = appConfig };
|
||||||
|
}
|
||||||
|
|
||||||
return new CoreConfigContextBuilderResult(context, validatorResult);
|
return new CoreConfigContextBuilderResult(context, validatorResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds the main <see cref="CoreConfigContext"/> for <paramref name="node"/> and, when
|
/// Builds the main <see cref="CoreConfigContext" /> for <paramref name="node" /> and, when
|
||||||
/// the main build succeeds, also builds the optional pre-socks context required for TUN
|
/// the main build succeeds, also builds the optional pre-socks context required for TUN
|
||||||
/// protection or pre-socks proxy chaining.
|
/// protection or pre-socks proxy chaining.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<CoreConfigContextBuilderAllResult> BuildAll(Config config, ProfileItem node)
|
public static async Task<CoreConfigContextBuilderAllResult> BuildAll(Config config, ProfileItem node)
|
||||||
{
|
{
|
||||||
@@ -118,16 +161,31 @@ public class CoreConfigContextBuilder
|
|||||||
{
|
{
|
||||||
Context = mainResult.Context with
|
Context = mainResult.Context with
|
||||||
{
|
{
|
||||||
IsTunEnabled = false, // main core doesn't handle tun directly when pre-socks is used
|
IsTunEnabled = false,
|
||||||
|
// main core doesn't handle tun directly when pre-socks is used
|
||||||
ProtectDomainList = [.. mainResult.Context.ProtectDomainList, .. preResult.Context.ProtectDomainList],
|
ProtectDomainList = [.. mainResult.Context.ProtectDomainList, .. preResult.Context.ProtectDomainList],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
if (mainResult.Context.IsTunEnabled
|
||||||
|
&& mainResult.Context.AppConfig.TunModeItem.StrictRoute)
|
||||||
|
{
|
||||||
|
var appConfig = JsonUtils.DeepCopy(mainResult.Context.AppConfig);
|
||||||
|
appConfig.CoreBasicItem.BindInterface = string.Empty;
|
||||||
|
appConfig.CoreBasicItem.SendThrough = string.Empty;
|
||||||
|
resolvedMainResult = resolvedMainResult with
|
||||||
|
{
|
||||||
|
Context = resolvedMainResult.Context with
|
||||||
|
{
|
||||||
|
AppConfig = appConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
return new CoreConfigContextBuilderAllResult(resolvedMainResult, preResult);
|
return new CoreConfigContextBuilderAllResult(resolvedMainResult, preResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether a pre-socks context is required for <paramref name="nodeContext"/>
|
/// Determines whether a pre-socks context is required for <paramref name="nodeContext" />
|
||||||
/// and, if so, builds and returns it. Returns <c>null</c> when no pre-socks core is needed.
|
/// and, if so, builds and returns it. Returns <c>null</c> when no pre-socks core is needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<CoreConfigContextBuilderResult?> BuildPreSocksIfNeeded(CoreConfigContext nodeContext)
|
private static async Task<CoreConfigContextBuilderResult?> BuildPreSocksIfNeeded(CoreConfigContext nodeContext)
|
||||||
{
|
{
|
||||||
@@ -139,12 +197,17 @@ public class CoreConfigContextBuilder
|
|||||||
if (preSocksItem != null)
|
if (preSocksItem != null)
|
||||||
{
|
{
|
||||||
var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem);
|
var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem);
|
||||||
|
|
||||||
|
var protectCoreTypeList = new HashSet<ECoreType>(nodeContext.ProtectCoreTypeList) { nodeContext.RunCoreType };
|
||||||
|
|
||||||
return preSocksResult with
|
return preSocksResult with
|
||||||
{
|
{
|
||||||
Context = preSocksResult.Context with
|
Context = preSocksResult.Context with
|
||||||
{
|
{
|
||||||
ProtectDomainList = [.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
|
ProtectDomainList =
|
||||||
}
|
[.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
|
||||||
|
ProtectCoreTypeList = protectCoreTypeList,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +215,8 @@ public class CoreConfigContextBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves a node into the context, optionally wrapping it in a subscription-level proxy chain.
|
/// Resolves a node into the context, optionally wrapping it in a subscription-level proxy chain.
|
||||||
/// Returns the effective (possibly replaced) node and the validation result.
|
/// Returns the effective (possibly replaced) node and the validation result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<(ProfileItem, NodeValidatorResult)> ResolveNodeAsync(CoreConfigContext context,
|
public static async Task<(ProfileItem, NodeValidatorResult)> ResolveNodeAsync(CoreConfigContext context,
|
||||||
ProfileItem node,
|
ProfileItem node,
|
||||||
@@ -188,12 +251,13 @@ public class CoreConfigContextBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If the node's subscription defines prev/next profiles, creates a virtual
|
/// If the node's subscription defines prev/next profiles, creates a virtual
|
||||||
/// <see cref="EConfigType.ProxyChain"/> node that wraps them together.
|
/// <see cref="EConfigType.ProxyChain" /> node that wraps them together.
|
||||||
/// Returns <c>null</c> as the chain item when no chain is needed.
|
/// Returns <c>null</c> as the chain item when no chain is needed.
|
||||||
/// Any warnings (e.g. missing prev/next profile) are returned in the validator result.
|
/// Any warnings (e.g. missing prev/next profile) are returned in the validator result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)> BuildSubscriptionChainNodeAsync(ProfileItem node)
|
private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)>
|
||||||
|
BuildSubscriptionChainNodeAsync(ProfileItem node)
|
||||||
{
|
{
|
||||||
var result = NodeValidatorResult.Empty();
|
var result = NodeValidatorResult.Empty();
|
||||||
|
|
||||||
@@ -234,7 +298,7 @@ public class CoreConfigContextBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build new proxy chain node
|
// Build new proxy chain node
|
||||||
var chainNode = new ProfileItem()
|
var chainNode = new ProfileItem
|
||||||
{
|
{
|
||||||
IndexId = $"inner-{Utils.GetGuid(false)}",
|
IndexId = $"inner-{Utils.GetGuid(false)}",
|
||||||
ConfigType = EConfigType.ProxyChain,
|
ConfigType = EConfigType.ProxyChain,
|
||||||
@@ -252,8 +316,8 @@ public class CoreConfigContextBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispatches registration to either <see cref="RegisterGroupNodeAsync"/> or
|
/// Dispatches registration to either <see cref="RegisterGroupNodeAsync" /> or
|
||||||
/// <see cref="RegisterSingleNodeAsync"/> based on the node's config type.
|
/// <see cref="RegisterSingleNodeAsync" /> based on the node's config type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<NodeValidatorResult> RegisterNodeAsync(CoreConfigContext context, ProfileItem node)
|
private static async Task<NodeValidatorResult> RegisterNodeAsync(CoreConfigContext context, ProfileItem node)
|
||||||
{
|
{
|
||||||
@@ -261,17 +325,14 @@ public class CoreConfigContextBuilder
|
|||||||
{
|
{
|
||||||
return await RegisterGroupNodeAsync(context, node);
|
return await RegisterGroupNodeAsync(context, node);
|
||||||
}
|
}
|
||||||
else
|
return await RegisterSingleNodeAsync(context, node);
|
||||||
{
|
|
||||||
return RegisterSingleNodeAsync(context, node);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates a single (non-group) node and, on success, adds it to the proxy map
|
/// Validates a single (non-group) node and, on success, adds it to the proxy map
|
||||||
/// and records any domain addresses that should bypass the proxy.
|
/// and records any domain addresses that should bypass the proxy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node)
|
private static async Task<NodeValidatorResult> RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node)
|
||||||
{
|
{
|
||||||
if (node.ConfigType.IsGroupType())
|
if (node.ConfigType.IsGroupType())
|
||||||
{
|
{
|
||||||
@@ -279,6 +340,29 @@ public class CoreConfigContextBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType);
|
var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType);
|
||||||
|
|
||||||
|
if (node.ConfigType == EConfigType.Outbound)
|
||||||
|
{
|
||||||
|
var addressFileName = node.Address;
|
||||||
|
if (!File.Exists(addressFileName))
|
||||||
|
{
|
||||||
|
addressFileName = Utils.GetConfigPath(addressFileName);
|
||||||
|
}
|
||||||
|
if (!File.Exists(addressFileName))
|
||||||
|
{
|
||||||
|
nodeValidatorResult.Errors.Add(string.Format(ResUI.MsgCustomOutboundFileNotFound, node.Remarks, addressFileName));
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileContent = await File.ReadAllTextAsync(addressFileName);
|
||||||
|
context.CustomOutboundContent[node.IndexId] = fileContent;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
nodeValidatorResult.Errors.Add(string.Format(ResUI.MsgCustomOutboundFileNotFound, node.Remarks, addressFileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var msgs = new List<string>([.. nodeValidatorResult.Errors, .. nodeValidatorResult.Warnings]);
|
var msgs = new List<string>([.. nodeValidatorResult.Errors, .. nodeValidatorResult.Warnings]);
|
||||||
if (msgs.Count > 0)
|
if (msgs.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -333,8 +417,8 @@ public class CoreConfigContextBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entry point for registering a group node. Initialises the visited/ancestor sets
|
/// Entry point for registering a group node. Initialises the visited/ancestor sets
|
||||||
/// and delegates to <see cref="TraverseGroupNodeAsync"/>.
|
/// and delegates to <see cref="TraverseGroupNodeAsync" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<NodeValidatorResult> RegisterGroupNodeAsync(CoreConfigContext context,
|
private static async Task<NodeValidatorResult> RegisterGroupNodeAsync(CoreConfigContext context,
|
||||||
ProfileItem node)
|
ProfileItem node)
|
||||||
@@ -350,9 +434,9 @@ public class CoreConfigContextBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recursively walks the children of a group node, registering valid leaf nodes
|
/// Recursively walks the children of a group node, registering valid leaf nodes
|
||||||
/// and nested groups. Detects cycles via <paramref name="ancestorsGroup"/> and
|
/// and nested groups. Detects cycles via <paramref name="ancestorsGroup" /> and
|
||||||
/// deduplicates shared nodes via <paramref name="globalVisitedGroup"/>.
|
/// deduplicates shared nodes via <paramref name="globalVisitedGroup" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<NodeValidatorResult> TraverseGroupNodeAsync(
|
private static async Task<NodeValidatorResult> TraverseGroupNodeAsync(
|
||||||
CoreConfigContext context,
|
CoreConfigContext context,
|
||||||
@@ -380,7 +464,7 @@ public class CoreConfigContextBuilder
|
|||||||
|
|
||||||
if (!childNode.ConfigType.IsGroupType())
|
if (!childNode.ConfigType.IsGroupType())
|
||||||
{
|
{
|
||||||
var childNodeResult = RegisterSingleNodeAsync(context, childNode);
|
var childNodeResult = await RegisterSingleNodeAsync(context, childNode);
|
||||||
childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w =>
|
childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w =>
|
||||||
string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w)));
|
string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w)));
|
||||||
childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e =>
|
childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e =>
|
||||||
@@ -416,13 +500,10 @@ public class CoreConfigContextBuilder
|
|||||||
childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks));
|
childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks));
|
||||||
return childNodeValidatorResult;
|
return childNodeValidatorResult;
|
||||||
}
|
}
|
||||||
else
|
childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
|
||||||
{
|
childNodeValidatorResult.Errors.Clear();
|
||||||
childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
|
|
||||||
childNodeValidatorResult.Errors.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList), });
|
node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList) });
|
||||||
context.AllProxiesMap[node.IndexId] = node;
|
context.AllProxiesMap[node.IndexId] = node;
|
||||||
return childNodeValidatorResult;
|
return childNodeValidatorResult;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,35 +29,6 @@ public class NodeValidator
|
|||||||
return v.ToResult();
|
return v.ToResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ValidationContext
|
|
||||||
{
|
|
||||||
public List<string> Errors { get; } = [];
|
|
||||||
public List<string> Warnings { get; } = [];
|
|
||||||
|
|
||||||
public void Error(string message)
|
|
||||||
{
|
|
||||||
Errors.Add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Warning(string message)
|
|
||||||
{
|
|
||||||
Warnings.Add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Assert(bool condition, string errorMsg)
|
|
||||||
{
|
|
||||||
if (!condition)
|
|
||||||
{
|
|
||||||
Error(errorMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public NodeValidatorResult ToResult()
|
|
||||||
{
|
|
||||||
return new NodeValidatorResult(Errors, Warnings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreType, ValidationContext v)
|
private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreType, ValidationContext v)
|
||||||
{
|
{
|
||||||
if (item.ConfigType is EConfigType.Custom)
|
if (item.ConfigType is EConfigType.Custom)
|
||||||
@@ -65,6 +36,15 @@ public class NodeValidator
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.ConfigType is EConfigType.Outbound)
|
||||||
|
{
|
||||||
|
if (item.CoreType != coreType)
|
||||||
|
{
|
||||||
|
v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, coreType.ToString(), item.ConfigType));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.ConfigType.IsGroupType())
|
if (item.ConfigType.IsGroupType())
|
||||||
{
|
{
|
||||||
// Group logic is handled in ValidateGroupNode
|
// Group logic is handled in ValidateGroupNode
|
||||||
@@ -72,8 +52,8 @@ public class NodeValidator
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Basic Property Validation
|
// Basic Property Validation
|
||||||
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Address"));
|
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbAddress));
|
||||||
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, "Port"));
|
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, ResUI.TbPort));
|
||||||
|
|
||||||
// Network & Core Logic
|
// Network & Core Logic
|
||||||
var net = item.GetNetwork();
|
var net = item.GetNetwork();
|
||||||
@@ -104,41 +84,76 @@ public class NodeValidator
|
|||||||
{
|
{
|
||||||
case EConfigType.VMess:
|
case EConfigType.VMess:
|
||||||
v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password),
|
v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password),
|
||||||
string.Format(ResUI.MsgInvalidProperty, "Password"));
|
string.Format(ResUI.MsgInvalidProperty, ResUI.TbId));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EConfigType.VLESS:
|
case EConfigType.VLESS:
|
||||||
v.Assert(
|
v.Assert(
|
||||||
!item.Password.IsNullOrEmpty()
|
!item.Password.IsNullOrEmpty()
|
||||||
&& (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30),
|
&& (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30),
|
||||||
string.Format(ResUI.MsgInvalidProperty, "Password")
|
string.Format(ResUI.MsgInvalidProperty, ResUI.TbId5)
|
||||||
);
|
);
|
||||||
v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty),
|
v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty),
|
||||||
string.Format(ResUI.MsgInvalidProperty, "Flow"));
|
string.Format(ResUI.MsgInvalidProperty, ResUI.TbFlow5));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EConfigType.Shadowsocks:
|
case EConfigType.Shadowsocks:
|
||||||
v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password"));
|
v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbId3));
|
||||||
v.Assert(
|
v.Assert(
|
||||||
!string.IsNullOrEmpty(protocolExtra.SsMethod) &&
|
!string.IsNullOrEmpty(protocolExtra.SsMethod) &&
|
||||||
Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod),
|
Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod),
|
||||||
string.Format(ResUI.MsgInvalidProperty, "SsMethod"));
|
string.Format(ResUI.MsgInvalidProperty, ResUI.TbSecurity3));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (coreType is ECoreType.Xray
|
||||||
|
&& (protocolExtra.Flow ?? string.Empty).StartsWith("xtls", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& item.MuxEnabled == true)
|
||||||
|
{
|
||||||
|
v.Warning(string.Format(ResUI.MsgOptionsConflict, "XTLS", "Mux.Cool"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.GetNetwork() is nameof(ETransport.ws)
|
||||||
|
&& item.EchConfigList.IsNullOrEmpty()
|
||||||
|
&& item.GetAlpn()?.FirstOrDefault() == "h3")
|
||||||
|
{
|
||||||
|
v.Warning(
|
||||||
|
"WebSocket but ALPN is set to h3, the core may ignore the ALPN setting or cause unexpected issues.");
|
||||||
|
}
|
||||||
|
|
||||||
// TLS & Security
|
// TLS & Security
|
||||||
if (item.StreamSecurity == Global.StreamSecurity)
|
if (item.StreamSecurity == Global.StreamSecurity)
|
||||||
{
|
{
|
||||||
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 &&
|
var isCertProvided = !item.Cert.IsNullOrEmpty();
|
||||||
!item.CertSha.IsNullOrEmpty())
|
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0)
|
||||||
{
|
{
|
||||||
v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate"));
|
v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TbFullCertTips));
|
||||||
|
isCertProvided = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for deprecated allowInsecure property when TLS is enabled
|
||||||
|
if (item.GetAllowInsecure()
|
||||||
|
&& item.Cert.IsNullOrEmpty()
|
||||||
|
&& item.CertSha.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
v.Warning(ResUI.MsgAllowInsecureDeprecated);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((coreType == ECoreType.Xray
|
||||||
|
&& item.GetAllowInsecure()
|
||||||
|
&& !isCertProvided
|
||||||
|
&& item.CertSha.IsNullOrEmpty())
|
||||||
|
|| (coreType == ECoreType.sing_box
|
||||||
|
&& item.GetAllowInsecure()
|
||||||
|
&& !isCertProvided))
|
||||||
|
{
|
||||||
|
v.Warning(ResUI.MsgInsecureConfiguration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.StreamSecurity == Global.StreamSecurityReality)
|
if (item.StreamSecurity == Global.StreamSecurityReality)
|
||||||
{
|
{
|
||||||
v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "PublicKey"));
|
v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbPublicKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
var transport = item.GetTransportExtra();
|
var transport = item.GetTransportExtra();
|
||||||
@@ -146,7 +161,7 @@ public class NodeValidator
|
|||||||
{
|
{
|
||||||
if (JsonUtils.ParseJson(transport.XhttpExtra) is not JsonObject)
|
if (JsonUtils.ParseJson(transport.XhttpExtra) is not JsonObject)
|
||||||
{
|
{
|
||||||
v.Error(string.Format(ResUI.MsgInvalidProperty, "XHTTP Extra"));
|
v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TransportExtra));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +169,7 @@ public class NodeValidator
|
|||||||
{
|
{
|
||||||
if (JsonUtils.ParseJson(item.Finalmask) is not JsonObject)
|
if (JsonUtils.ParseJson(item.Finalmask) is not JsonObject)
|
||||||
{
|
{
|
||||||
v.Error(string.Format(ResUI.MsgInvalidProperty, "Finalmask"));
|
v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TbFinalmask));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,4 +198,33 @@ public class NodeValidator
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ValidationContext
|
||||||
|
{
|
||||||
|
public List<string> Errors { get; } = [];
|
||||||
|
public List<string> Warnings { get; } = [];
|
||||||
|
|
||||||
|
public void Error(string message)
|
||||||
|
{
|
||||||
|
Errors.Add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warning(string message)
|
||||||
|
{
|
||||||
|
Warnings.Add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Assert(bool condition, string errorMsg)
|
||||||
|
{
|
||||||
|
if (!condition)
|
||||||
|
{
|
||||||
|
Error(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public NodeValidatorResult ToResult()
|
||||||
|
{
|
||||||
|
return new NodeValidatorResult(Errors, Warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,15 +39,14 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
LogEnabled = false,
|
LogEnabled = false,
|
||||||
Loglevel = "warning",
|
Loglevel = "warning",
|
||||||
MuxEnabled = false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (config.Inbound == null)
|
if (config.Inbound == null)
|
||||||
{
|
{
|
||||||
config.Inbound = new List<InItem>();
|
config.Inbound = [];
|
||||||
InItem inItem = new()
|
InItem inItem = new()
|
||||||
{
|
{
|
||||||
Protocol = EInboundProtocol.socks.ToString(),
|
Protocol = nameof(EInboundProtocol.socks),
|
||||||
LocalPort = 10808,
|
LocalPort = 10808,
|
||||||
UdpEnabled = true,
|
UdpEnabled = true,
|
||||||
SniffingEnabled = true,
|
SniffingEnabled = true,
|
||||||
@@ -60,7 +59,7 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
if (config.Inbound.Count > 0)
|
if (config.Inbound.Count > 0)
|
||||||
{
|
{
|
||||||
config.Inbound.First().Protocol = EInboundProtocol.socks.ToString();
|
config.Inbound.First().Protocol = nameof(EInboundProtocol.socks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,14 +92,18 @@ public static class ConfigHandler
|
|||||||
EnableTun = false,
|
EnableTun = false,
|
||||||
Mtu = 9000,
|
Mtu = 9000,
|
||||||
IcmpRouting = Global.TunIcmpRoutingPolicies.First(),
|
IcmpRouting = Global.TunIcmpRoutingPolicies.First(),
|
||||||
EnableLegacyProtect = false,
|
EnableLegacyProtect = true,
|
||||||
};
|
};
|
||||||
config.GuiItem ??= new();
|
config.GuiItem ??= new();
|
||||||
|
if (!Global.RootCertProviders.Contains(config.GuiItem.RootCertProvider))
|
||||||
|
{
|
||||||
|
config.GuiItem.RootCertProvider = Global.RootCertProviders.First();
|
||||||
|
}
|
||||||
config.MsgUIItem ??= new();
|
config.MsgUIItem ??= new();
|
||||||
|
|
||||||
config.UiItem ??= new();
|
config.UiItem ??= new();
|
||||||
config.UiItem.MainColumnItem ??= new();
|
config.UiItem.MainColumnItem ??= [];
|
||||||
config.UiItem.WindowSizeItem ??= new();
|
config.UiItem.WindowSizeItem ??= [];
|
||||||
|
|
||||||
if (config.UiItem.CurrentLanguage.IsNullOrEmpty())
|
if (config.UiItem.CurrentLanguage.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -112,10 +115,12 @@ public static class ConfigHandler
|
|||||||
config.ConstItem ??= new ConstItem();
|
config.ConstItem ??= new ConstItem();
|
||||||
|
|
||||||
config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
|
config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
|
||||||
|
config.SimpleDNSItem.FakeIPRange ??= Global.FakeIPRanges.FirstOrDefault();
|
||||||
config.SimpleDNSItem.GlobalFakeIp ??= true;
|
config.SimpleDNSItem.GlobalFakeIp ??= true;
|
||||||
config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault();
|
config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault();
|
||||||
config.SimpleDNSItem.ServeStale ??= false;
|
config.SimpleDNSItem.ServeStale ??= false;
|
||||||
config.SimpleDNSItem.ParallelQuery ??= false;
|
config.SimpleDNSItem.ParallelQuery ??= false;
|
||||||
|
config.SimpleDNSItem.EnableHappyEyeballs ??= false;
|
||||||
|
|
||||||
config.SpeedTestItem ??= new();
|
config.SpeedTestItem ??= new();
|
||||||
if (config.SpeedTestItem.SpeedTestTimeout < 10)
|
if (config.SpeedTestItem.SpeedTestTimeout < 10)
|
||||||
@@ -158,17 +163,32 @@ public static class ConfigHandler
|
|||||||
DownMbps = 100
|
DownMbps = 100
|
||||||
};
|
};
|
||||||
config.ClashUIItem ??= new();
|
config.ClashUIItem ??= new();
|
||||||
config.ClashUIItem.ConnectionsColumnItem ??= new();
|
config.ClashUIItem.ConnectionsColumnItem ??= [];
|
||||||
config.SystemProxyItem ??= new();
|
config.SystemProxyItem ??= new();
|
||||||
config.WebDavItem ??= new();
|
config.WebDavItem ??= new();
|
||||||
config.CheckUpdateItem ??= new();
|
config.CheckUpdateItem ??= new();
|
||||||
config.Fragment4RayItem ??= new()
|
config.Fragment4RayItem ??= new()
|
||||||
{
|
{
|
||||||
Packets = "tlshello",
|
Packets = "tlshello",
|
||||||
Length = "50-100",
|
|
||||||
Interval = "10-20"
|
|
||||||
};
|
};
|
||||||
config.GlobalHotkeys ??= new();
|
config.Fragment4RayItem.MaxSplit ??= "0";
|
||||||
|
|
||||||
|
config.HappyEyeballs4RayItem ??= new()
|
||||||
|
{
|
||||||
|
TryDelayMs = 250,
|
||||||
|
PrioritizeIPv6 = false,
|
||||||
|
Interleave = 1,
|
||||||
|
MaxConcurrentTry = 4,
|
||||||
|
};
|
||||||
|
if ((config.Fragment4RayItem.Lengths ?? []).Count == 0)
|
||||||
|
{
|
||||||
|
config.Fragment4RayItem.Lengths = [config.Fragment4RayItem.Length ?? "50-100"];
|
||||||
|
}
|
||||||
|
if ((config.Fragment4RayItem.Delays ?? []).Count == 0)
|
||||||
|
{
|
||||||
|
config.Fragment4RayItem.Delays = [config.Fragment4RayItem.Interval ?? "10-20"];
|
||||||
|
}
|
||||||
|
config.GlobalHotkeys ??= [];
|
||||||
|
|
||||||
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
|
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -255,6 +275,7 @@ public static class ConfigHandler
|
|||||||
item.Cert = profileItem.Cert;
|
item.Cert = profileItem.Cert;
|
||||||
item.CertSha = profileItem.CertSha;
|
item.CertSha = profileItem.CertSha;
|
||||||
item.EchConfigList = profileItem.EchConfigList;
|
item.EchConfigList = profileItem.EchConfigList;
|
||||||
|
item.VerifyPeerCertByName = profileItem.VerifyPeerCertByName;
|
||||||
item.Finalmask = profileItem.Finalmask;
|
item.Finalmask = profileItem.Finalmask;
|
||||||
item.ProtoExtra = profileItem.ProtoExtra;
|
item.ProtoExtra = profileItem.ProtoExtra;
|
||||||
item.TransportExtra = profileItem.TransportExtra;
|
item.TransportExtra = profileItem.TransportExtra;
|
||||||
@@ -361,6 +382,13 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (profileItem.ConfigType == EConfigType.Outbound)
|
||||||
|
{
|
||||||
|
profileItem.Address = Utils.GetConfigPath(profileItem.Address);
|
||||||
|
if (await AddCustomOutboundServer(config, profileItem, false) == 0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await AddServerCommon(config, profileItem, true);
|
await AddServerCommon(config, profileItem, true);
|
||||||
@@ -558,6 +586,43 @@ public static class ConfigHandler
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<int> AddCustomOutboundServer(Config config, ProfileItem profileItem, bool blDelete, bool toFile = true)
|
||||||
|
{
|
||||||
|
var fileName = profileItem.Address;
|
||||||
|
if (!File.Exists(fileName))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
var ext = Path.GetExtension(fileName);
|
||||||
|
var newFileName = $"{Utils.GetGuid()}{ext}";
|
||||||
|
//newFileName = Path.Combine(Utile.GetTempPath(), newFileName);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Copy(fileName, Utils.GetConfigPath(newFileName));
|
||||||
|
if (blDelete)
|
||||||
|
{
|
||||||
|
File.Delete(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logging.SaveLog(_tag, ex);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
profileItem.Address = newFileName;
|
||||||
|
profileItem.ConfigType = EConfigType.Outbound;
|
||||||
|
if (profileItem.Remarks.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
profileItem.Remarks = $"import custom outbound@{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")}";
|
||||||
|
}
|
||||||
|
|
||||||
|
await AddServerCommon(config, profileItem, toFile);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Edit an existing custom server configuration
|
/// Edit an existing custom server configuration
|
||||||
/// Updates the server's properties without changing the file
|
/// Updates the server's properties without changing the file
|
||||||
@@ -579,6 +644,8 @@ public static class ConfigHandler
|
|||||||
item.CoreType = profileItem.CoreType;
|
item.CoreType = profileItem.CoreType;
|
||||||
item.DisplayLog = profileItem.DisplayLog;
|
item.DisplayLog = profileItem.DisplayLog;
|
||||||
item.PreSocksPort = profileItem.PreSocksPort;
|
item.PreSocksPort = profileItem.PreSocksPort;
|
||||||
|
|
||||||
|
item.ProtoExtra = profileItem.ProtoExtra;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await SQLiteHelper.Instance.UpdateAsync(item) > 0)
|
if (await SQLiteHelper.Instance.UpdateAsync(item) > 0)
|
||||||
@@ -720,12 +787,51 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
|
var protocolExtra = profileItem.GetProtocolExtra();
|
||||||
|
profileItem.SetProtocolExtra(protocolExtra with
|
||||||
{
|
{
|
||||||
SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(),
|
SalamanderPass = protocolExtra.SalamanderPass?.TrimEx(),
|
||||||
HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(),
|
HopInterval = protocolExtra.HopInterval?.TrimEx(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!protocolExtra.Hy2RealmUrl.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
var realmResult = HyRealm.TryParse(protocolExtra.Hy2RealmUrl, out var realm);
|
||||||
|
if (!realmResult || realm is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (realm.StunList.Count == 0)
|
||||||
|
{
|
||||||
|
realm = realm with
|
||||||
|
{
|
||||||
|
StunList = Global.DefaultRealmStunList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
Hy2RealmUrl = realm.ToUri(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var isGecko = !protocolExtra.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtra.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||||
|
if (isGecko)
|
||||||
|
{
|
||||||
|
var minPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
|
||||||
|
var maxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
|
||||||
|
if (minPacketSize <= 0
|
||||||
|
|| minPacketSize > maxPacketSize
|
||||||
|
|| maxPacketSize > 2048)
|
||||||
|
{
|
||||||
|
minPacketSize = 512;
|
||||||
|
maxPacketSize = 1200;
|
||||||
|
}
|
||||||
|
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
GeckoMinPacketSize = minPacketSize.ToString(),
|
||||||
|
GeckoMaxPacketSize = maxPacketSize.ToString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await AddServerCommon(config, profileItem, toFile);
|
await AddServerCommon(config, profileItem, toFile);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -874,7 +980,7 @@ public static class ConfigHandler
|
|||||||
profileItem.Fingerprint = string.Empty;
|
profileItem.Fingerprint = string.Empty;
|
||||||
profileItem.Alpn = string.Empty;
|
profileItem.Alpn = string.Empty;
|
||||||
profileItem.Network = string.Empty;
|
profileItem.Network = string.Empty;
|
||||||
profileItem.AllowInsecure = "false";
|
profileItem.AllowInsecure = string.Empty;
|
||||||
if (profileItem.StreamSecurity.IsNullOrEmpty())
|
if (profileItem.StreamSecurity.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
profileItem.StreamSecurity = Global.StreamSecurity;
|
profileItem.StreamSecurity = Global.StreamSecurity;
|
||||||
@@ -1056,8 +1162,8 @@ public static class ConfigHandler
|
|||||||
return new Tuple<int, int>(0, 0);
|
return new Tuple<int, int>(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ProfileItem> lstKeep = new();
|
List<ProfileItem> lstKeep = [];
|
||||||
List<ProfileItem> lstRemove = new();
|
List<ProfileItem> lstRemove = [];
|
||||||
if (!config.GuiItem.KeepOlderDedupl)
|
if (!config.GuiItem.KeepOlderDedupl)
|
||||||
{
|
{
|
||||||
lstProfile.Reverse();
|
lstProfile.Reverse();
|
||||||
@@ -1106,10 +1212,6 @@ public static class ConfigHandler
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (profileItem.AllowInsecure.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
profileItem.AllowInsecure = config.CoreBasicItem.DefAllowInsecure.ToString().ToLower();
|
|
||||||
}
|
|
||||||
if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality)
|
if (profileItem.Fingerprint.IsNullOrEmpty() && profileItem.StreamSecurity == Global.StreamSecurityReality)
|
||||||
{
|
{
|
||||||
profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint;
|
profileItem.Fingerprint = config.CoreBasicItem.DefFingerprint;
|
||||||
@@ -1423,7 +1525,7 @@ public static class ConfigHandler
|
|||||||
var matchedChildProfiles = childProfiles?.Where(p =>
|
var matchedChildProfiles = childProfiles?.Where(p =>
|
||||||
p != null &&
|
p != null &&
|
||||||
p.IsValid() &&
|
p.IsValid() &&
|
||||||
!p.ConfigType.IsComplexType() &&
|
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
||||||
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
|
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
|
||||||
)
|
)
|
||||||
.ToList() ?? [];
|
.ToList() ?? [];
|
||||||
@@ -1454,8 +1556,7 @@ public static class ConfigHandler
|
|||||||
public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
|
public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
|
||||||
{
|
{
|
||||||
ProfileItem? itemSocks = null;
|
ProfileItem? itemSocks = null;
|
||||||
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect
|
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect;
|
||||||
|| Utils.IsNonWindows();
|
|
||||||
if (node.ConfigType != EConfigType.Custom
|
if (node.ConfigType != EConfigType.Custom
|
||||||
&& coreType != ECoreType.sing_box
|
&& coreType != ECoreType.sing_box
|
||||||
&& config.TunModeItem.EnableTun
|
&& config.TunModeItem.EnableTun
|
||||||
@@ -1472,7 +1573,8 @@ public static class ConfigHandler
|
|||||||
else if (node.ConfigType == EConfigType.Custom
|
else if (node.ConfigType == EConfigType.Custom
|
||||||
&& node.PreSocksPort is > 0 and <= 65535)
|
&& node.PreSocksPort is > 0 and <= 65535)
|
||||||
{
|
{
|
||||||
var preCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;
|
var customPreCoreType = AppManager.Instance.GetCoreType(null, EConfigType.Custom);
|
||||||
|
var preCoreType = (enableLegacyProtect && config.TunModeItem.EnableTun) ? ECoreType.sing_box : customPreCoreType;
|
||||||
itemSocks = new ProfileItem()
|
itemSocks = new ProfileItem()
|
||||||
{
|
{
|
||||||
CoreType = preCoreType,
|
CoreType = preCoreType,
|
||||||
@@ -1537,7 +1639,7 @@ public static class ConfigHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
var countServers = 0;
|
var countServers = 0;
|
||||||
List<ProfileItem> lstAdd = new();
|
List<ProfileItem> lstAdd = [];
|
||||||
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
|
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
|
||||||
if (isSub)
|
if (isSub)
|
||||||
{
|
{
|
||||||
@@ -1619,68 +1721,198 @@ public static class ConfigHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
var subItem = await AppManager.Instance.GetSubItem(subid);
|
var subItem = await AppManager.Instance.GetSubItem(subid);
|
||||||
|
|
||||||
|
if (subItem?.CustomCoreType is null)
|
||||||
|
{
|
||||||
|
return await AddBatchServersDefaultCustom(config, strData, subid, isSub, subItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await AddBatchServersSpecificCustom(config, strData, subid, isSub, subItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> AddBatchServersDefaultCustom(
|
||||||
|
Config config,
|
||||||
|
string strData,
|
||||||
|
string subid,
|
||||||
|
bool isSub,
|
||||||
|
SubItem? subItem)
|
||||||
|
{
|
||||||
var subRemarks = subItem?.Remarks;
|
var subRemarks = subItem?.Remarks;
|
||||||
var preSocksPort = subItem?.PreSocksPort;
|
// Prioritize using complete custom parsing, followed by custom outbound parsing.
|
||||||
|
var lstProfiles = V2rayFmt.ResolveToCustom(strData, subRemarks);
|
||||||
List<ProfileItem>? lstProfiles = null;
|
if (lstProfiles.Count == 0)
|
||||||
//Is sing-box array configuration
|
|
||||||
if (lstProfiles is null || lstProfiles.Count <= 0)
|
|
||||||
{
|
{
|
||||||
lstProfiles = SingboxFmt.ResolveFullArray(strData, subRemarks);
|
lstProfiles = SingboxFmt.ResolveToCustom(strData, subRemarks);
|
||||||
}
|
}
|
||||||
//Is v2ray array configuration
|
if (lstProfiles.Count == 0)
|
||||||
if (lstProfiles is null || lstProfiles.Count <= 0)
|
|
||||||
{
|
{
|
||||||
lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks);
|
lstProfiles = V2rayFmt.ResolveToCustomOutbound(strData, subRemarks);
|
||||||
}
|
}
|
||||||
if (lstProfiles != null && lstProfiles.Count > 0)
|
if (lstProfiles.Count == 0)
|
||||||
{
|
{
|
||||||
var count = 0;
|
lstProfiles = SingboxFmt.ResolveToCustomOutbound(strData, subRemarks);
|
||||||
foreach (var it in lstProfiles)
|
|
||||||
{
|
|
||||||
it.Subid = subid;
|
|
||||||
it.IsSub = isSub;
|
|
||||||
it.PreSocksPort = preSocksPort;
|
|
||||||
if (await AddCustomServer(config, it, true) == 0)
|
|
||||||
{
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (count > 0)
|
|
||||||
{
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (lstProfiles.Count == 0)
|
||||||
ProfileItem? profileItem = null;
|
|
||||||
//Is sing-box configuration
|
|
||||||
profileItem ??= SingboxFmt.ResolveFull(strData, subRemarks);
|
|
||||||
//Is v2ray configuration
|
|
||||||
profileItem ??= V2rayFmt.ResolveFull(strData, subRemarks);
|
|
||||||
//Is Html Page
|
|
||||||
if (profileItem is null && HtmlPageFmt.IsHtmlPage(strData))
|
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
//Is Clash configuration
|
|
||||||
profileItem ??= ClashFmt.ResolveFull(strData, subRemarks);
|
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
||||||
//Is hysteria configuration
|
if (count > 0)
|
||||||
profileItem ??= Hysteria2Fmt.ResolveFull2(strData, subRemarks);
|
{
|
||||||
if (profileItem is null || profileItem.Address.IsNullOrEmpty())
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HtmlPageFmt.IsHtmlPage(strData))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var profileItem = ClashFmt.ResolveFull(strData, subRemarks)
|
||||||
|
?? Hysteria2Fmt.ResolveFull2(strData, subRemarks);
|
||||||
|
|
||||||
|
if (profileItem == null)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
profileItem.Subid = subid;
|
profileItem.Subid = subid;
|
||||||
profileItem.IsSub = isSub;
|
profileItem.IsSub = isSub;
|
||||||
profileItem.PreSocksPort = preSocksPort;
|
profileItem.PreSocksPort = subItem?.PreSocksPort;
|
||||||
if (await AddCustomServer(config, profileItem, true) == 0)
|
|
||||||
|
return await AddCustomServer(config, profileItem, true) == 0 ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> AddBatchServersSpecificCustom(
|
||||||
|
Config config,
|
||||||
|
string strData,
|
||||||
|
string subid,
|
||||||
|
bool isSub,
|
||||||
|
SubItem subItem)
|
||||||
|
{
|
||||||
|
var subRemarks = subItem.Remarks;
|
||||||
|
var customCoreType = subItem.CustomCoreType!.Value;
|
||||||
|
|
||||||
|
var lstProfiles = customCoreType switch
|
||||||
{
|
{
|
||||||
return 1;
|
ECoreType.Xray => V2rayFmt.ResolveToCustom(strData, subRemarks),
|
||||||
|
ECoreType.sing_box => SingboxFmt.ResolveToCustom(strData, subRemarks),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lstProfiles is not null)
|
||||||
|
{
|
||||||
|
if (lstProfiles.Count == 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
return count;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> AddBatchCustomServers(
|
||||||
|
Config config,
|
||||||
|
List<ProfileItem> lstProfiles,
|
||||||
|
string subid,
|
||||||
|
bool isSub)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
foreach (var it in lstProfiles)
|
||||||
{
|
{
|
||||||
return -1;
|
it.Subid = subid;
|
||||||
|
it.IsSub = isSub;
|
||||||
|
|
||||||
|
if (it.ConfigType == EConfigType.Custom)
|
||||||
|
{
|
||||||
|
if (await AddCustomServer(config, it, true) == 0)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (await AddCustomOutboundServer(config, it, true) == 0)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> SaveCustomRawFileServer(
|
||||||
|
Config config,
|
||||||
|
string strData,
|
||||||
|
string subid,
|
||||||
|
bool isSub,
|
||||||
|
SubItem subItem,
|
||||||
|
ECoreType customCoreType)
|
||||||
|
{
|
||||||
|
var ext = DetectFileExtension(strData);
|
||||||
|
var fileName = Utils.GetTempPath($"{Utils.GetGuid(false)}{ext}");
|
||||||
|
await File.WriteAllTextAsync(fileName, strData);
|
||||||
|
|
||||||
|
var profileItem = new ProfileItem
|
||||||
|
{
|
||||||
|
CoreType = customCoreType,
|
||||||
|
ConfigType = EConfigType.Custom,
|
||||||
|
Address = fileName,
|
||||||
|
Remarks = subItem.Remarks ?? customCoreType.ToString(),
|
||||||
|
Subid = subid,
|
||||||
|
IsSub = isSub,
|
||||||
|
PreSocksPort = subItem.PreSocksPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await AddCustomServer(config, profileItem, true) == 0 ? 1 : -1;
|
||||||
|
|
||||||
|
static string DetectFileExtension(string data)
|
||||||
|
{
|
||||||
|
var trimmed = data.AsSpan().TrimStart();
|
||||||
|
if (trimmed.IsEmpty)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed[0] is '{' or '[')
|
||||||
|
{
|
||||||
|
return ".json";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.StartsWith("---"))
|
||||||
|
{
|
||||||
|
return ".yaml";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var line in trimmed.EnumerateLines())
|
||||||
|
{
|
||||||
|
var lineTrimmed = line.TrimStart();
|
||||||
|
if (lineTrimmed.IsEmpty || lineTrimmed.StartsWith("#"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var colonIndex = lineTrimmed.IndexOf(':');
|
||||||
|
if (colonIndex > 0)
|
||||||
|
{
|
||||||
|
var keySpan = lineTrimmed[..colonIndex];
|
||||||
|
if (!keySpan.Contains(' ') && !keySpan.Contains('\t'))
|
||||||
|
{
|
||||||
|
if (colonIndex == lineTrimmed.Length - 1 || lineTrimmed[colonIndex + 1] is ' ' or '\t' or '\r' or '\n')
|
||||||
|
{
|
||||||
|
return ".yaml";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1781,6 +2013,7 @@ public static class ConfigHandler
|
|||||||
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
|
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
|
||||||
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
|
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
|
||||||
EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false),
|
EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false),
|
||||||
|
EConfigType.Outbound => await AddCustomOutboundServer(config, profileItem, true, false),
|
||||||
_ => -1,
|
_ => -1,
|
||||||
};
|
};
|
||||||
if (addStatus == 0)
|
if (addStatus == 0)
|
||||||
@@ -1982,6 +2215,7 @@ public static class ConfigHandler
|
|||||||
item.NextProfile = subItem.NextProfile;
|
item.NextProfile = subItem.NextProfile;
|
||||||
item.PreSocksPort = subItem.PreSocksPort;
|
item.PreSocksPort = subItem.PreSocksPort;
|
||||||
item.Memo = subItem.Memo;
|
item.Memo = subItem.Memo;
|
||||||
|
item.CustomCoreType = subItem.CustomCoreType;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Id.IsNullOrEmpty())
|
if (item.Id.IsNullOrEmpty())
|
||||||
@@ -2022,7 +2256,7 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
var customProfile = await SQLiteHelper.Instance.TableAsync<ProfileItem>().Where(t => t.Subid == subid && t.ConfigType == EConfigType.Custom).ToListAsync();
|
var customProfile = await SQLiteHelper.Instance.TableAsync<ProfileItem>().Where(t => t.Subid == subid && (t.ConfigType == EConfigType.Custom || t.ConfigType == EConfigType.Outbound)).ToListAsync();
|
||||||
if (isSub)
|
if (isSub)
|
||||||
{
|
{
|
||||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileItem where isSub = 1 and subid = '{subid}'");
|
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileItem where isSub = 1 and subid = '{subid}'");
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public static class ConnectionHandler
|
|||||||
|
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
responseTime = await GetRealPingTime(webProxy, 10);
|
responseTime = await GetRealPingTime(webProxy);
|
||||||
if (responseTime > 0)
|
if (responseTime > 0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -66,7 +66,7 @@ public static class ConnectionHandler
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Measures response time by sending HTTP requests through proxy.
|
/// Measures response time by sending HTTP requests through proxy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout)
|
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout = 9)
|
||||||
{
|
{
|
||||||
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
|
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
|
||||||
var responseTime = -1;
|
var responseTime = -1;
|
||||||
@@ -77,17 +77,18 @@ public static class ConnectionHandler
|
|||||||
using var client = new HttpClient(new SocketsHttpHandler()
|
using var client = new HttpClient(new SocketsHttpHandler()
|
||||||
{
|
{
|
||||||
Proxy = webProxy,
|
Proxy = webProxy,
|
||||||
UseProxy = webProxy != null
|
UseProxy = webProxy != null,
|
||||||
|
ConnectTimeout = TimeSpan.FromSeconds(3)
|
||||||
});
|
});
|
||||||
|
|
||||||
List<int> oneTime = new();
|
List<int> oneTime = [];
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var timer = Stopwatch.StartNew();
|
var timer = Stopwatch.StartNew();
|
||||||
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
|
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
|
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
|
||||||
await Task.Delay(100);
|
await Task.Delay(100, cts.Token);
|
||||||
}
|
}
|
||||||
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
|
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public static class CoreConfigHandler
|
|||||||
{
|
{
|
||||||
result = node.CoreType switch
|
result = node.CoreType switch
|
||||||
{
|
{
|
||||||
ECoreType.mihomo => await new CoreConfigClashService(config).GenerateClientCustomConfig(node, fileName),
|
ECoreType.mihomo => await new CoreConfigClashService(config, context.IsTunEnabled).GenerateClientCustomConfig(node, fileName),
|
||||||
_ => await GenerateClientCustomConfig(node, fileName)
|
_ => await GenerateClientCustomConfig(node, fileName)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ public class AnytlsFmt : BaseFmt
|
|||||||
var query = Utils.ParseQueryString(parsedUrl.Query);
|
var query = Utils.ParseQueryString(parsedUrl.Query);
|
||||||
ResolveUriQuery(query, ref item);
|
ResolveUriQuery(query, ref item);
|
||||||
|
|
||||||
|
if (GetQueryValue(query, "insecure") == "1")
|
||||||
|
{
|
||||||
|
item.AllowInsecure = Global.StringTrue;
|
||||||
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +46,10 @@ public class AnytlsFmt : BaseFmt
|
|||||||
}
|
}
|
||||||
var pw = item.Password;
|
var pw = item.Password;
|
||||||
var dicQuery = new Dictionary<string, string>();
|
var dicQuery = new Dictionary<string, string>();
|
||||||
|
if (item.GetAllowInsecure())
|
||||||
|
{
|
||||||
|
dicQuery.Add("insecure", "1");
|
||||||
|
}
|
||||||
ToUriQuery(item, Global.None, ref dicQuery);
|
ToUriQuery(item, Global.None, ref dicQuery);
|
||||||
|
|
||||||
return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark);
|
return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark);
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ namespace ServiceLib.Handler.Fmt;
|
|||||||
|
|
||||||
public class BaseFmt
|
public class BaseFmt
|
||||||
{
|
{
|
||||||
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure" };
|
|
||||||
|
|
||||||
private static string UrlEncodeSafe(string? value) => Utils.UrlEncode(value ?? string.Empty);
|
private static string UrlEncodeSafe(string? value) => Utils.UrlEncode(value ?? string.Empty);
|
||||||
|
|
||||||
protected static string GetIpv6(string address)
|
protected static string GetIpv6(string address)
|
||||||
@@ -67,12 +65,15 @@ public class BaseFmt
|
|||||||
{
|
{
|
||||||
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
|
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
|
||||||
}
|
}
|
||||||
ToUriQueryAllowInsecure(item, ref dicQuery);
|
|
||||||
}
|
}
|
||||||
if (item.EchConfigList.IsNotEmpty())
|
if (item.EchConfigList.IsNotEmpty())
|
||||||
{
|
{
|
||||||
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
|
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
|
||||||
}
|
}
|
||||||
|
if (item.VerifyPeerCertByName.IsNotEmpty())
|
||||||
|
{
|
||||||
|
dicQuery.Add("vcn", Utils.UrlEncode(item.VerifyPeerCertByName));
|
||||||
|
}
|
||||||
if (item.CertSha.IsNotEmpty())
|
if (item.CertSha.IsNotEmpty())
|
||||||
{
|
{
|
||||||
dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha));
|
dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha));
|
||||||
@@ -192,25 +193,6 @@ public class BaseFmt
|
|||||||
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
|
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
|
||||||
}
|
}
|
||||||
|
|
||||||
ToUriQueryAllowInsecure(item, ref dicQuery);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary<string, string> dicQuery)
|
|
||||||
{
|
|
||||||
if (item.AllowInsecure.Equals(Global.AllowInsecure.First()))
|
|
||||||
{
|
|
||||||
// Add two for compatibility
|
|
||||||
dicQuery.Add("insecure", "1");
|
|
||||||
dicQuery.Add("allowInsecure", "1");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dicQuery.Add("insecure", "0");
|
|
||||||
dicQuery.Add("allowInsecure", "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +209,7 @@ public class BaseFmt
|
|||||||
item.SpiderX = GetQueryDecoded(query, "spx");
|
item.SpiderX = GetQueryDecoded(query, "spx");
|
||||||
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
|
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
|
||||||
item.EchConfigList = GetQueryDecoded(query, "ech");
|
item.EchConfigList = GetQueryDecoded(query, "ech");
|
||||||
|
item.VerifyPeerCertByName = GetQueryDecoded(query, "vcn");
|
||||||
item.CertSha = GetQueryDecoded(query, "pcs");
|
item.CertSha = GetQueryDecoded(query, "pcs");
|
||||||
|
|
||||||
var finalmaskDecoded = GetQueryDecoded(query, "fm");
|
var finalmaskDecoded = GetQueryDecoded(query, "fm");
|
||||||
@@ -247,19 +230,6 @@ public class BaseFmt
|
|||||||
item.Finalmask = string.Empty;
|
item.Finalmask = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
|
|
||||||
{
|
|
||||||
item.AllowInsecure = Global.AllowInsecure.First();
|
|
||||||
}
|
|
||||||
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
|
|
||||||
{
|
|
||||||
item.AllowInsecure = Global.AllowInsecure.Skip(1).First();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
item.AllowInsecure = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var net = GetQueryValue(query, "type", nameof(ETransport.raw));
|
var net = GetQueryValue(query, "type", nameof(ETransport.raw));
|
||||||
if (net == Global.RawNetworkAlias)
|
if (net == Global.RawNetworkAlias)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ public class FmtHandler
|
|||||||
{
|
{
|
||||||
return Hysteria2Fmt.Resolve(str, out msg);
|
return Hysteria2Fmt.Resolve(str, out msg);
|
||||||
}
|
}
|
||||||
|
else if (str.StartsWith(Global.Hysteria2RealmProtocolShare)
|
||||||
|
|| str.StartsWith(Global.Hysteria2HttpRealmProtocolShare))
|
||||||
|
{
|
||||||
|
return Hysteria2Fmt.ResolveRealm(str, out msg);
|
||||||
|
}
|
||||||
else if (str.StartsWith(Global.ProtocolShares[EConfigType.TUIC]))
|
else if (str.StartsWith(Global.ProtocolShares[EConfigType.TUIC]))
|
||||||
{
|
{
|
||||||
return TuicFmt.Resolve(str, out msg);
|
return TuicFmt.Resolve(str, out msg);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
|
||||||
namespace ServiceLib.Handler.Fmt;
|
namespace ServiceLib.Handler.Fmt;
|
||||||
|
|
||||||
public class Hysteria2Fmt : BaseFmt
|
public class Hysteria2Fmt : BaseFmt
|
||||||
@@ -23,15 +25,7 @@ public class Hysteria2Fmt : BaseFmt
|
|||||||
|
|
||||||
var query = Utils.ParseQueryString(url.Query);
|
var query = Utils.ParseQueryString(url.Query);
|
||||||
ResolveUriQuery(query, ref item);
|
ResolveUriQuery(query, ref item);
|
||||||
if (item.CertSha.IsNullOrEmpty())
|
ResolveHy2UriQuery(query, ref item);
|
||||||
{
|
|
||||||
item.CertSha = GetQueryDecoded(query, "pinSHA256");
|
|
||||||
}
|
|
||||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
|
||||||
{
|
|
||||||
Ports = GetQueryDecoded(query, "mport"),
|
|
||||||
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
@@ -42,8 +36,11 @@ public class Hysteria2Fmt : BaseFmt
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
var protocolExtraItem = item.GetProtocolExtra();
|
||||||
var url = string.Empty;
|
if (!protocolExtraItem.Hy2RealmUrl.TrimEx().IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return ToUriForRealm(item);
|
||||||
|
}
|
||||||
|
|
||||||
var remark = string.Empty;
|
var remark = string.Empty;
|
||||||
if (item.Remarks.IsNotEmpty())
|
if (item.Remarks.IsNotEmpty())
|
||||||
@@ -52,27 +49,7 @@ public class Hysteria2Fmt : BaseFmt
|
|||||||
}
|
}
|
||||||
var dicQuery = new Dictionary<string, string>();
|
var dicQuery = new Dictionary<string, string>();
|
||||||
ToUriQueryLite(item, ref dicQuery);
|
ToUriQueryLite(item, ref dicQuery);
|
||||||
var protocolExtraItem = item.GetProtocolExtra();
|
ToHy2UriQuery(item, ref dicQuery);
|
||||||
|
|
||||||
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
dicQuery.Add("obfs", "salamander");
|
|
||||||
dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass));
|
|
||||||
}
|
|
||||||
if (!protocolExtraItem.Ports.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-')));
|
|
||||||
}
|
|
||||||
if (!item.CertSha.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
var sha = item.CertSha;
|
|
||||||
var idx = sha.IndexOf(',');
|
|
||||||
if (idx > 0)
|
|
||||||
{
|
|
||||||
sha = sha[..idx];
|
|
||||||
}
|
|
||||||
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark);
|
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark);
|
||||||
}
|
}
|
||||||
@@ -94,4 +71,163 @@ public class Hysteria2Fmt : BaseFmt
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ProfileItem? ResolveRealm(string str, out string msg)
|
||||||
|
{
|
||||||
|
msg = ResUI.ConfigurationFormatIncorrect;
|
||||||
|
ProfileItem item = new()
|
||||||
|
{
|
||||||
|
ConfigType = EConfigType.Hysteria2
|
||||||
|
};
|
||||||
|
var realmStr = str["hysteria2+".Length..];
|
||||||
|
var result = HyRealm.TryParse(realmStr, out var realm);
|
||||||
|
if (!result || realm == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = Utils.TryUri(str);
|
||||||
|
if (url == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Address = realm.RendezvousHost;
|
||||||
|
item.Port = realm.RendezvousPort;
|
||||||
|
item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped);
|
||||||
|
|
||||||
|
var query = Utils.ParseQueryString(url.Query);
|
||||||
|
ResolveUriQuery(query, ref item);
|
||||||
|
var authPassword = GetQueryDecoded(query, "auth");
|
||||||
|
if (authPassword.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
item.Password = authPassword;
|
||||||
|
ResolveHy2UriQuery(query, ref item);
|
||||||
|
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
Hy2RealmUrl = realm.ToUri(),
|
||||||
|
});
|
||||||
|
|
||||||
|
msg = string.Empty;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? ToUriForRealm(ProfileItem? item)
|
||||||
|
{
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var protocolExtraItem = item.GetProtocolExtra();
|
||||||
|
var result = HyRealm.TryParse(protocolExtraItem.Hy2RealmUrl, out var realm);
|
||||||
|
if (!result || realm == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remark = string.Empty;
|
||||||
|
if (item.Remarks.IsNotEmpty())
|
||||||
|
{
|
||||||
|
remark = "#" + Utils.UrlEncode(item.Remarks);
|
||||||
|
}
|
||||||
|
var dicQuery = new Dictionary<string, string>();
|
||||||
|
ToUriQueryLite(item, ref dicQuery);
|
||||||
|
ToHy2UriQuery(item, ref dicQuery);
|
||||||
|
|
||||||
|
dicQuery.Add("auth", Utils.UrlEncode(item.Password));
|
||||||
|
|
||||||
|
var queryBuilder = new StringBuilder();
|
||||||
|
queryBuilder.Append('?');
|
||||||
|
foreach (var kv in dicQuery)
|
||||||
|
{
|
||||||
|
queryBuilder.Append(kv.Key);
|
||||||
|
queryBuilder.Append('=');
|
||||||
|
queryBuilder.Append(kv.Value);
|
||||||
|
queryBuilder.Append('&');
|
||||||
|
}
|
||||||
|
foreach (var stun in realm.StunList)
|
||||||
|
{
|
||||||
|
queryBuilder.Append("stun=");
|
||||||
|
queryBuilder.Append(Utils.UrlEncode(stun));
|
||||||
|
queryBuilder.Append('&');
|
||||||
|
}
|
||||||
|
var query = queryBuilder.ToString().TrimEnd('&');
|
||||||
|
|
||||||
|
var url = $"{Utils.UrlEncode(realm.Token)}@{GetIpv6(realm.RendezvousHost)}:{realm.RendezvousPort}";
|
||||||
|
var scheme = realm.IsHttp ? Global.Hysteria2HttpRealmProtocolShare : Global.Hysteria2RealmProtocolShare;
|
||||||
|
return $"{scheme}{url}/{realm.RealmName}{query}{remark}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResolveHy2UriQuery(NameValueCollection query, ref ProfileItem item)
|
||||||
|
{
|
||||||
|
if (GetQueryValue(query, "insecure") == "1")
|
||||||
|
{
|
||||||
|
item.AllowInsecure = Global.StringTrue;
|
||||||
|
}
|
||||||
|
if (item.CertSha.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
item.CertSha = GetQueryDecoded(query, "pinSHA256");
|
||||||
|
}
|
||||||
|
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||||
|
{
|
||||||
|
Ports = GetQueryDecoded(query, "mport"),
|
||||||
|
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
|
||||||
|
// NOTE: The "PacketSize" parameter is not defined by the official URI Scheme, may remove or rename it in the future.
|
||||||
|
GeckoMinPacketSize = GetQueryDecoded(query, "minPacketSize"),
|
||||||
|
GeckoMaxPacketSize = GetQueryDecoded(query, "maxPacketSize"),
|
||||||
|
});
|
||||||
|
if (GetQueryDecoded(query, "obfs") == "gecko")
|
||||||
|
{
|
||||||
|
// Ensure the "PacketSize" parameters are present for gecko obfs.
|
||||||
|
var protocolExtraItem = item.GetProtocolExtra();
|
||||||
|
if (protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
protocolExtraItem = protocolExtraItem with
|
||||||
|
{
|
||||||
|
GeckoMinPacketSize = "512",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
protocolExtraItem = protocolExtraItem with
|
||||||
|
{
|
||||||
|
GeckoMaxPacketSize = "1200",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
item.SetProtocolExtra(protocolExtraItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ToHy2UriQuery(ProfileItem item, ref Dictionary<string, string> dicQuery)
|
||||||
|
{
|
||||||
|
if (item.GetAllowInsecure())
|
||||||
|
{
|
||||||
|
dicQuery.Add("insecure", "1");
|
||||||
|
}
|
||||||
|
if (!item.CertSha.IsNullOrEmpty()
|
||||||
|
&& !item.CertSha.Contains(','))
|
||||||
|
{
|
||||||
|
var sha = item.CertSha;
|
||||||
|
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
|
||||||
|
}
|
||||||
|
var protocolExtraItem = item.GetProtocolExtra();
|
||||||
|
var isGecko = !protocolExtraItem.GeckoMinPacketSize.IsNullOrEmpty() || !protocolExtraItem.GeckoMaxPacketSize.IsNullOrEmpty();
|
||||||
|
if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
dicQuery.Add("obfs", isGecko ? "gecko" : "salamander");
|
||||||
|
dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass));
|
||||||
|
if (isGecko)
|
||||||
|
{
|
||||||
|
// NOTE: The "PacketSize" parameter is not defined by the official URI Scheme, may remove or rename it in the future.
|
||||||
|
dicQuery.Add("minPacketSize", protocolExtraItem.GeckoMinPacketSize);
|
||||||
|
dicQuery.Add("maxPacketSize", protocolExtraItem.GeckoMaxPacketSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!protocolExtraItem.Ports.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-')));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace ServiceLib.Handler.Fmt;
|
namespace ServiceLib.Handler.Fmt;
|
||||||
|
|
||||||
public class InnerFmt
|
public class InnerFmt : BaseFmt
|
||||||
{
|
{
|
||||||
private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false));
|
private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false));
|
||||||
|
|
||||||
@@ -50,19 +50,19 @@ public class InnerFmt
|
|||||||
var protocolExtra = item.GetProtocolExtra();
|
var protocolExtra = item.GetProtocolExtra();
|
||||||
// Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks,
|
// Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks,
|
||||||
// which means it will be replaced with the subid, otherwise set it to null
|
// which means it will be replaced with the subid, otherwise set it to null
|
||||||
//if (!protocolExtra.SubChildItems.IsNullOrEmpty())
|
// if (!protocolExtra.SubChildItems.IsNullOrEmpty())
|
||||||
if (protocolExtra.SubChildItems == "self")
|
if (protocolExtra.SubChildItems == "self")
|
||||||
{
|
{
|
||||||
protocolExtra = protocolExtra with
|
protocolExtra = protocolExtra with
|
||||||
{
|
{
|
||||||
SubChildItems = subid
|
SubChildItems = subid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
protocolExtra = protocolExtra with
|
protocolExtra = protocolExtra with
|
||||||
{
|
{
|
||||||
SubChildItems = null
|
SubChildItems = null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
|
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
|
||||||
@@ -73,14 +73,14 @@ public class InnerFmt
|
|||||||
.ToList();
|
.ToList();
|
||||||
protocolExtra = protocolExtra with
|
protocolExtra = protocolExtra with
|
||||||
{
|
{
|
||||||
ChildItems = Utils.List2String(newChildIndexIds)
|
ChildItems = Utils.List2String(newChildIndexIds),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
protocolExtra = protocolExtra with
|
protocolExtra = protocolExtra with
|
||||||
{
|
{
|
||||||
ChildItems = null
|
ChildItems = null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
item.SetProtocolExtra(protocolExtra);
|
item.SetProtocolExtra(protocolExtra);
|
||||||
@@ -120,7 +120,7 @@ public class InnerFmt
|
|||||||
{
|
{
|
||||||
protocolExtra = protocolExtra with
|
protocolExtra = protocolExtra with
|
||||||
{
|
{
|
||||||
SubChildItems = "self"
|
SubChildItems = "self",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
|
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
|
||||||
@@ -131,7 +131,7 @@ public class InnerFmt
|
|||||||
.ToList();
|
.ToList();
|
||||||
protocolExtra = protocolExtra with
|
protocolExtra = protocolExtra with
|
||||||
{
|
{
|
||||||
ChildItems = Utils.List2String(newChildIndexIds)
|
ChildItems = Utils.List2String(newChildIndexIds),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
itemClone.SetProtocolExtra(protocolExtra);
|
itemClone.SetProtocolExtra(protocolExtra);
|
||||||
@@ -175,6 +175,19 @@ public class InnerFmt
|
|||||||
jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false);
|
jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false);
|
||||||
jsonObj.Remove("TransportExtraObj");
|
jsonObj.Remove("TransportExtraObj");
|
||||||
}
|
}
|
||||||
|
var customOutboundFilePath = string.Empty;
|
||||||
|
if (jsonObj.TryGetPropertyValue("CustomOutboundObj", out var customOutboundNode)
|
||||||
|
&& customOutboundNode is JsonObject customOutboundObj)
|
||||||
|
{
|
||||||
|
var customOutboundContent = JsonUtils.Serialize(customOutboundObj, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
|
});
|
||||||
|
customOutboundFilePath = WriteAllText(customOutboundContent);
|
||||||
|
jsonObj.Remove("CustomOutboundObj");
|
||||||
|
}
|
||||||
var profileItem = JsonUtils.Deserialize<ProfileItem>(JsonUtils.Serialize(jsonObj, false));
|
var profileItem = JsonUtils.Deserialize<ProfileItem>(JsonUtils.Serialize(jsonObj, false));
|
||||||
if (profileItem is null)
|
if (profileItem is null)
|
||||||
{
|
{
|
||||||
@@ -185,7 +198,7 @@ public class InnerFmt
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Check Enum.IsDefined
|
// Check Enum.IsDefined
|
||||||
if (!Enum.IsDefined(typeof(EConfigType), profileItem.ConfigType))
|
if (!Enum.IsDefined(profileItem.ConfigType))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -193,6 +206,14 @@ public class InnerFmt
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (profileItem.ConfigType is EConfigType.Outbound)
|
||||||
|
{
|
||||||
|
if (customOutboundFilePath.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
profileItem.Address = customOutboundFilePath;
|
||||||
|
}
|
||||||
var protocolExtra = profileItem.GetProtocolExtra();
|
var protocolExtra = profileItem.GetProtocolExtra();
|
||||||
var multipleLoad = protocolExtra.MultipleLoad;
|
var multipleLoad = protocolExtra.MultipleLoad;
|
||||||
if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad))
|
if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad))
|
||||||
@@ -209,6 +230,26 @@ public class InnerFmt
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (item.ConfigType is EConfigType.Outbound)
|
||||||
|
{
|
||||||
|
var customOutboundFilePath = item.Address;
|
||||||
|
if (!File.Exists(customOutboundFilePath))
|
||||||
|
{
|
||||||
|
customOutboundFilePath = Utils.GetConfigPath(customOutboundFilePath);
|
||||||
|
}
|
||||||
|
if (!File.Exists(customOutboundFilePath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!customOutboundFilePath.IsNullOrEmpty()
|
||||||
|
&& File.Exists(customOutboundFilePath)
|
||||||
|
&& File.ReadAllText(customOutboundFilePath) is { Length: > 0 } customOutboundContent
|
||||||
|
&& JsonUtils.ParseJson(customOutboundContent) is JsonObject customOutboundObj)
|
||||||
|
{
|
||||||
|
jsonObj["CustomOutboundObj"] = customOutboundObj;
|
||||||
|
jsonObj.Remove("Address");
|
||||||
|
}
|
||||||
|
}
|
||||||
// unflatten
|
// unflatten
|
||||||
// move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj
|
// move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj
|
||||||
// move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj
|
// move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj
|
||||||
@@ -296,7 +337,7 @@ public class InnerFmt
|
|||||||
JsonValue value when value.TryGetValue<string>(out var str) => string.IsNullOrEmpty(str),
|
JsonValue value when value.TryGetValue<string>(out var str) => string.IsNullOrEmpty(str),
|
||||||
JsonObject obj => obj.Count == 0,
|
JsonObject obj => obj.Count == 0,
|
||||||
JsonArray arr => arr.Count == 0,
|
JsonArray arr => arr.Count == 0,
|
||||||
_ => false
|
_ => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,103 @@ namespace ServiceLib.Handler.Fmt;
|
|||||||
|
|
||||||
public class SingboxFmt : BaseFmt
|
public class SingboxFmt : BaseFmt
|
||||||
{
|
{
|
||||||
public static List<ProfileItem>? ResolveFullArray(string strData, string? subRemarks)
|
public static List<ProfileItem> ResolveToCustom(string strData, string? subRemarks)
|
||||||
{
|
{
|
||||||
var configObjects = JsonUtils.Deserialize<object[]>(strData);
|
var jsonNode = JsonUtils.ParseJson(strData);
|
||||||
if (configObjects is not { Length: > 0 })
|
return ResolveCommon(jsonNode, subRemarks, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ProfileItem> ResolveToCustomOutbound(string strData, string? subRemarks)
|
||||||
|
{
|
||||||
|
var jsonNode = JsonUtils.ParseJson(strData);
|
||||||
|
return ResolveCommon(jsonNode, subRemarks, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ProfileItem> ResolveCommon(JsonNode? jsonNode, string? subRemarks, bool isOutbound)
|
||||||
|
{
|
||||||
|
if (jsonNode is JsonArray jsonArray)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
.. jsonArray.Select(item => ResolveCommon(item, subRemarks, isOutbound))
|
||||||
|
.Where(list => list is { Count: > 0 })
|
||||||
|
.SelectMany(list => list),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (jsonNode is not JsonObject jsonObject)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// Process the individual JSON object
|
||||||
|
var profileList = new List<ProfileItem>();
|
||||||
|
if (!isOutbound)
|
||||||
|
{
|
||||||
|
var fullProfile = ResolveFull(jsonObject, subRemarks);
|
||||||
|
profileList.Add(fullProfile);
|
||||||
|
if (fullProfile is not null)
|
||||||
|
{
|
||||||
|
return profileList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profileList.AddRange(ResolveFullToOutbound(jsonObject, subRemarks));
|
||||||
|
if (profileList.Count != 0)
|
||||||
|
{
|
||||||
|
return profileList;
|
||||||
|
}
|
||||||
|
var outboundProfile = ResolveOutbound(jsonObject, subRemarks);
|
||||||
|
if (outboundProfile is not null)
|
||||||
|
{
|
||||||
|
profileList.Add(outboundProfile);
|
||||||
|
}
|
||||||
|
return profileList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfileItem? ResolveFull(JsonObject jsonObject, string? subRemarks)
|
||||||
|
{
|
||||||
|
if (jsonObject?["inbounds"] == null
|
||||||
|
|| jsonObject["outbounds"] == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ProfileItem> lstResult = [];
|
if (jsonObject["outbounds"] is JsonArray outboundsArray)
|
||||||
foreach (var configObject in configObjects)
|
|
||||||
{
|
{
|
||||||
var objectString = JsonUtils.Serialize(configObject);
|
if (!outboundsArray.Any(IsValidSingboxOutbound))
|
||||||
var profileIt = ResolveFull(objectString, subRemarks);
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
|
||||||
|
var profileItem = new ProfileItem
|
||||||
|
{
|
||||||
|
ConfigType = EConfigType.Custom,
|
||||||
|
CoreType = ECoreType.sing_box,
|
||||||
|
Address = fileName,
|
||||||
|
Remarks = subRemarks ?? "singbox_custom",
|
||||||
|
};
|
||||||
|
|
||||||
|
return profileItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ProfileItem> ResolveFullToOutbound(JsonObject jsonObject, string? subRemarks)
|
||||||
|
{
|
||||||
|
if (jsonObject?["outbounds"] is not JsonArray outboundsArray)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
List<ProfileItem> lstResult = [];
|
||||||
|
foreach (var outbound in outboundsArray)
|
||||||
|
{
|
||||||
|
if (outbound is not JsonObject outboundObj)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var profileIt = ResolveOutbound(outboundObj, subRemarks);
|
||||||
if (profileIt != null)
|
if (profileIt != null)
|
||||||
{
|
{
|
||||||
lstResult.Add(profileIt);
|
lstResult.Add(profileIt);
|
||||||
@@ -23,25 +107,58 @@ public class SingboxFmt : BaseFmt
|
|||||||
return lstResult;
|
return lstResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ProfileItem? ResolveFull(string strData, string? subRemarks)
|
private static ProfileItem? ResolveOutbound(JsonObject jsonObject, string? subRemarks)
|
||||||
{
|
{
|
||||||
var config = JsonUtils.ParseJson(strData);
|
if (!IsValidSingboxOutbound(jsonObject))
|
||||||
if (config?["inbounds"] == null
|
|
||||||
|| config["outbounds"] == null
|
|
||||||
|| config["route"] == null
|
|
||||||
|| config["dns"] == null)
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
var type = jsonObject["type"]?.ToString();
|
||||||
var fileName = WriteAllText(strData);
|
if (type is null or "direct" or "block" or "dns" or "selector" or "urltest")
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var tag = jsonObject["tag"]?.ToString();
|
||||||
|
var remarks = $"{type}_{tag}";
|
||||||
|
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
|
||||||
var profileItem = new ProfileItem
|
var profileItem = new ProfileItem
|
||||||
{
|
{
|
||||||
|
ConfigType = EConfigType.Outbound,
|
||||||
CoreType = ECoreType.sing_box,
|
CoreType = ECoreType.sing_box,
|
||||||
Address = fileName,
|
Address = fileName,
|
||||||
Remarks = subRemarks ?? "singbox_custom"
|
Remarks = remarks,
|
||||||
};
|
};
|
||||||
|
|
||||||
return profileItem;
|
return profileItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsValidSingboxOutbound(JsonNode? jsonNode)
|
||||||
|
{
|
||||||
|
if (jsonNode is not JsonObject jsonObject)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var matchedCounter = 0;
|
||||||
|
if (string.IsNullOrEmpty(jsonObject["type"]?.ToString()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
matchedCounter += 1;
|
||||||
|
if (!string.IsNullOrEmpty(jsonObject["tag"]?.ToString()))
|
||||||
|
{
|
||||||
|
matchedCounter += 1;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(jsonObject["server"]?.ToString()))
|
||||||
|
{
|
||||||
|
matchedCounter += 1;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(jsonObject["server_port"]?.ToString()))
|
||||||
|
{
|
||||||
|
matchedCounter += 1;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(jsonObject["tls"]?.ToString()))
|
||||||
|
{
|
||||||
|
matchedCounter += 1;
|
||||||
|
}
|
||||||
|
return matchedCounter >= 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public class SocksFmt : BaseFmt
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var arr21 = arr1.First().Split(':');
|
var arr21 = arr1.First().Split(':');
|
||||||
var indexPort = arr1.Last().LastIndexOf(":");
|
var indexPort = arr1.Last().LastIndexOf(':');
|
||||||
if (arr21.Length != 2 || indexPort < 0)
|
if (arr21.Length != 2 || indexPort < 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ namespace ServiceLib.Handler.Fmt;
|
|||||||
|
|
||||||
public class TrojanFmt : BaseFmt
|
public class TrojanFmt : BaseFmt
|
||||||
{
|
{
|
||||||
|
private static readonly List<string> _insecureQueryKeys = new() { "allowInsecure", "insecure" };
|
||||||
|
|
||||||
public static ProfileItem? Resolve(string str, out string msg)
|
public static ProfileItem? Resolve(string str, out string msg)
|
||||||
{
|
{
|
||||||
msg = ResUI.ConfigurationFormatIncorrect;
|
msg = ResUI.ConfigurationFormatIncorrect;
|
||||||
@@ -23,6 +25,10 @@ public class TrojanFmt : BaseFmt
|
|||||||
item.Password = Utils.UrlDecode(url.UserInfo);
|
item.Password = Utils.UrlDecode(url.UserInfo);
|
||||||
|
|
||||||
var query = Utils.ParseQueryString(url.Query);
|
var query = Utils.ParseQueryString(url.Query);
|
||||||
|
if (_insecureQueryKeys.Any(q => GetQueryValue(query, q) == "1"))
|
||||||
|
{
|
||||||
|
item.AllowInsecure = Global.StringTrue;
|
||||||
|
}
|
||||||
item.SetProtocolExtra(item.GetProtocolExtra() with { Flow = GetQueryValue(query, "flow") });
|
item.SetProtocolExtra(item.GetProtocolExtra() with { Flow = GetQueryValue(query, "flow") });
|
||||||
ResolveUriQuery(query, ref item);
|
ResolveUriQuery(query, ref item);
|
||||||
|
|
||||||
@@ -41,6 +47,10 @@ public class TrojanFmt : BaseFmt
|
|||||||
remark = "#" + Utils.UrlEncode(item.Remarks);
|
remark = "#" + Utils.UrlEncode(item.Remarks);
|
||||||
}
|
}
|
||||||
var dicQuery = new Dictionary<string, string>();
|
var dicQuery = new Dictionary<string, string>();
|
||||||
|
if (item.GetAllowInsecure())
|
||||||
|
{
|
||||||
|
_insecureQueryKeys.ForEach(q => dicQuery.Add(q, "1"));
|
||||||
|
}
|
||||||
if (!item.GetProtocolExtra().Flow.IsNullOrEmpty())
|
if (!item.GetProtocolExtra().Flow.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
dicQuery.Add("flow", item.GetProtocolExtra().Flow);
|
dicQuery.Add("flow", item.GetProtocolExtra().Flow);
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ public class TuicFmt : BaseFmt
|
|||||||
|
|
||||||
var query = Utils.ParseQueryString(url.Query);
|
var query = Utils.ParseQueryString(url.Query);
|
||||||
ResolveUriQuery(query, ref item);
|
ResolveUriQuery(query, ref item);
|
||||||
|
if (GetQueryValue(query, "allow_insecure") == "1")
|
||||||
|
{
|
||||||
|
item.AllowInsecure = Global.StringTrue;
|
||||||
|
}
|
||||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||||
{
|
{
|
||||||
CongestionControl = GetQueryValue(query, "congestion_control")
|
CongestionControl = GetQueryValue(query, "congestion_control")
|
||||||
@@ -53,7 +57,10 @@ public class TuicFmt : BaseFmt
|
|||||||
|
|
||||||
var dicQuery = new Dictionary<string, string>();
|
var dicQuery = new Dictionary<string, string>();
|
||||||
ToUriQueryLite(item, ref dicQuery);
|
ToUriQueryLite(item, ref dicQuery);
|
||||||
|
if (item.GetAllowInsecure())
|
||||||
|
{
|
||||||
|
dicQuery.Add("allow_insecure", "1");
|
||||||
|
}
|
||||||
if (!item.GetProtocolExtra().CongestionControl.IsNullOrEmpty())
|
if (!item.GetProtocolExtra().CongestionControl.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
dicQuery.Add("congestion_control", item.GetProtocolExtra().CongestionControl);
|
dicQuery.Add("congestion_control", item.GetProtocolExtra().CongestionControl);
|
||||||
|
|||||||
@@ -2,47 +2,166 @@ namespace ServiceLib.Handler.Fmt;
|
|||||||
|
|
||||||
public class V2rayFmt : BaseFmt
|
public class V2rayFmt : BaseFmt
|
||||||
{
|
{
|
||||||
public static List<ProfileItem>? ResolveFullArray(string strData, string? subRemarks)
|
public static List<ProfileItem> ResolveToCustom(string strData, string? subRemarks)
|
||||||
{
|
{
|
||||||
var configObjects = JsonUtils.Deserialize<object[]>(strData);
|
var jsonNode = JsonUtils.ParseJson(strData);
|
||||||
if (configObjects is not { Length: > 0 })
|
return ResolveCommon(jsonNode, subRemarks, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ProfileItem> ResolveToCustomOutbound(string strData, string? subRemarks)
|
||||||
|
{
|
||||||
|
var jsonNode = JsonUtils.ParseJson(strData);
|
||||||
|
return ResolveCommon(jsonNode, subRemarks, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ProfileItem> ResolveCommon(JsonNode? jsonNode, string? subRemarks, bool isOutbound)
|
||||||
|
{
|
||||||
|
if (jsonNode is JsonArray jsonArray)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
.. jsonArray.Select(item => ResolveCommon(item, subRemarks, isOutbound))
|
||||||
|
.Where(list => list is { Count: > 0 })
|
||||||
|
.SelectMany(list => list),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (jsonNode is not JsonObject jsonObject)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// Process the individual JSON object
|
||||||
|
var profileList = new List<ProfileItem>();
|
||||||
|
if (!isOutbound)
|
||||||
|
{
|
||||||
|
var fullProfile = ResolveFull(jsonObject, subRemarks);
|
||||||
|
profileList.Add(fullProfile);
|
||||||
|
if (fullProfile is not null)
|
||||||
|
{
|
||||||
|
return profileList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profileList.AddRange(ResolveFullToOutbound(jsonObject, subRemarks));
|
||||||
|
if (profileList.Count != 0)
|
||||||
|
{
|
||||||
|
return profileList;
|
||||||
|
}
|
||||||
|
var outboundProfile = ResolveOutbound(jsonObject, subRemarks);
|
||||||
|
if (outboundProfile is not null)
|
||||||
|
{
|
||||||
|
profileList.Add(outboundProfile);
|
||||||
|
}
|
||||||
|
return profileList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfileItem? ResolveFull(JsonObject jsonObject, string? subRemarks)
|
||||||
|
{
|
||||||
|
if (jsonObject?["inbounds"] == null
|
||||||
|
|| jsonObject["outbounds"] == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ProfileItem> lstResult = [];
|
if (jsonObject["outbounds"] is JsonArray outboundsArray)
|
||||||
foreach (var configObject in configObjects)
|
|
||||||
{
|
{
|
||||||
var objectString = JsonUtils.Serialize(configObject);
|
if (!outboundsArray.Any(IsValidV2rayOutbound))
|
||||||
var profileIt = ResolveFull(objectString, subRemarks);
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
|
||||||
|
|
||||||
|
var profileItem = new ProfileItem
|
||||||
|
{
|
||||||
|
ConfigType = EConfigType.Custom,
|
||||||
|
CoreType = ECoreType.Xray,
|
||||||
|
Address = fileName,
|
||||||
|
Remarks = jsonObject["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom",
|
||||||
|
};
|
||||||
|
|
||||||
|
return profileItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ProfileItem> ResolveFullToOutbound(JsonObject jsonObject, string? subRemarks)
|
||||||
|
{
|
||||||
|
if (jsonObject["outbounds"] is not JsonArray outboundsArray)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
List<ProfileItem> lstResult = [];
|
||||||
|
foreach (var outbound in outboundsArray)
|
||||||
|
{
|
||||||
|
if (outbound is not JsonObject outboundObj)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var profileIt = ResolveOutbound(outboundObj, subRemarks);
|
||||||
if (profileIt != null)
|
if (profileIt != null)
|
||||||
{
|
{
|
||||||
lstResult.Add(profileIt);
|
lstResult.Add(profileIt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lstResult;
|
return lstResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ProfileItem? ResolveFull(string strData, string? subRemarks)
|
public static ProfileItem? ResolveOutbound(JsonObject jsonObject, string? subRemarks)
|
||||||
{
|
{
|
||||||
var config = JsonUtils.ParseJson(strData);
|
if (!IsValidV2rayOutbound(jsonObject))
|
||||||
if (config?["inbounds"] == null
|
|
||||||
|| config["outbounds"] == null
|
|
||||||
|| config["routing"] == null)
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
var protocol = jsonObject["protocol"]?.ToString();
|
||||||
var fileName = WriteAllText(strData);
|
if (protocol is null or "freedom" or "blackhole" or "dns" or "loopback")
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var tag = jsonObject["tag"]?.ToString();
|
||||||
|
var remarks = $"{protocol}_{tag}";
|
||||||
|
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
|
||||||
var profileItem = new ProfileItem
|
var profileItem = new ProfileItem
|
||||||
{
|
{
|
||||||
|
ConfigType = EConfigType.Outbound,
|
||||||
CoreType = ECoreType.Xray,
|
CoreType = ECoreType.Xray,
|
||||||
Address = fileName,
|
Address = fileName,
|
||||||
Remarks = config?["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom"
|
Remarks = remarks,
|
||||||
};
|
};
|
||||||
|
|
||||||
return profileItem;
|
return profileItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsValidV2rayOutbound(JsonNode? jsonNode)
|
||||||
|
{
|
||||||
|
if (jsonNode is not JsonObject jsonObject)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchedCounter = 0;
|
||||||
|
if (string.IsNullOrEmpty(jsonObject["protocol"]?.ToString()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
matchedCounter += 1;
|
||||||
|
if (!string.IsNullOrEmpty(jsonObject["settings"]?.ToString()))
|
||||||
|
{
|
||||||
|
matchedCounter += 1;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(jsonObject["streamSettings"]?.ToString()))
|
||||||
|
{
|
||||||
|
matchedCounter += 1;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(jsonObject["tag"]?.ToString()))
|
||||||
|
{
|
||||||
|
matchedCounter += 1;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(jsonObject["mux"]?.ToString()))
|
||||||
|
{
|
||||||
|
matchedCounter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedCounter >= 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ public class VmessFmt : BaseFmt
|
|||||||
sni = item.Sni,
|
sni = item.Sni,
|
||||||
alpn = item.Alpn,
|
alpn = item.Alpn,
|
||||||
fp = item.Fingerprint,
|
fp = item.Fingerprint,
|
||||||
insecure = item.AllowInsecure.Equals(Global.AllowInsecure.First()) ? "1" : "0"
|
insecure = item.GetAllowInsecure() ? "1" : "0",
|
||||||
|
vcn = item.VerifyPeerCertByName,
|
||||||
|
pcs = item.CertSha
|
||||||
};
|
};
|
||||||
|
|
||||||
var url = JsonUtils.Serialize(vmessQRCode);
|
var url = JsonUtils.Serialize(vmessQRCode);
|
||||||
@@ -140,7 +142,9 @@ public class VmessFmt : BaseFmt
|
|||||||
item.Sni = Utils.ToString(vmessQRCode.sni);
|
item.Sni = Utils.ToString(vmessQRCode.sni);
|
||||||
item.Alpn = Utils.ToString(vmessQRCode.alpn);
|
item.Alpn = Utils.ToString(vmessQRCode.alpn);
|
||||||
item.Fingerprint = Utils.ToString(vmessQRCode.fp);
|
item.Fingerprint = Utils.ToString(vmessQRCode.fp);
|
||||||
item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.AllowInsecure.First() : string.Empty;
|
item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.StringTrue : string.Empty;
|
||||||
|
item.VerifyPeerCertByName = Utils.ToString(vmessQRCode.vcn);
|
||||||
|
item.CertSha = Utils.ToString(vmessQRCode.pcs);
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,12 +133,12 @@ public static class SubscriptionHandler
|
|||||||
|
|
||||||
if (!url.Contains("target="))
|
if (!url.Contains("target="))
|
||||||
{
|
{
|
||||||
url += string.Format("&target={0}", item.ConvertTarget);
|
url += $"&target={item.ConvertTarget}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.Contains("config="))
|
if (!url.Contains("config="))
|
||||||
{
|
{
|
||||||
url += string.Format("&config={0}", Global.SubConvertConfig.FirstOrDefault());
|
url += $"&config={Global.SubConvertConfig.FirstOrDefault()}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ public static class SysProxyHandler
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||||
var exceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
|
|
||||||
if (port <= 0)
|
if (port <= 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -24,17 +23,18 @@ public static class SysProxyHandler
|
|||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case ESysProxyType.ForcedChange when Utils.IsWindows():
|
case ESysProxyType.ForcedChange when Utils.IsWindows():
|
||||||
{
|
var (strProxy, strExceptions) = GetWindowsProxyString(config, port);
|
||||||
GetWindowsProxyString(config, port, out var strProxy, out var strExceptions);
|
ProxySettingWindows.SetProxy(strProxy, strExceptions, 2);
|
||||||
ProxySettingWindows.SetProxy(strProxy, strExceptions, 2);
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ESysProxyType.ForcedChange when Utils.IsLinux():
|
case ESysProxyType.ForcedChange when Utils.IsLinux():
|
||||||
|
var exceptions = SanitizeExceptions(config);
|
||||||
await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions);
|
await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ESysProxyType.ForcedChange when Utils.IsMacOS():
|
case ESysProxyType.ForcedChange when Utils.IsMacOS():
|
||||||
await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions);
|
var exceptions2 = SanitizeExceptions(config);
|
||||||
|
await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions2);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ESysProxyType.ForcedClear when Utils.IsWindows():
|
case ESysProxyType.ForcedClear when Utils.IsWindows():
|
||||||
@@ -66,15 +66,31 @@ public static class SysProxyHandler
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void GetWindowsProxyString(Config config, int port, out string strProxy, out string strExceptions)
|
private static string SanitizeExceptions(Config config)
|
||||||
{
|
{
|
||||||
strExceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
|
var exceptions = config.SystemProxyItem.SystemProxyExceptions;
|
||||||
|
if (exceptions.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = exceptions
|
||||||
|
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(item => item.Replace(" ", string.Empty))
|
||||||
|
.Where(item => item.Length > 0);
|
||||||
|
|
||||||
|
return string.Join(',', items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string strProxy, string strExceptions) GetWindowsProxyString(Config config, int port)
|
||||||
|
{
|
||||||
|
var strExceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
|
||||||
if (config.SystemProxyItem.NotProxyLocalAddress)
|
if (config.SystemProxyItem.NotProxyLocalAddress)
|
||||||
{
|
{
|
||||||
strExceptions = $"<local>;{strExceptions}";
|
strExceptions = $"<local>;{strExceptions}";
|
||||||
}
|
}
|
||||||
|
|
||||||
strProxy = string.Empty;
|
var strProxy = string.Empty;
|
||||||
if (config.SystemProxyItem.SystemProxyAdvancedProtocol.IsNullOrEmpty())
|
if (config.SystemProxyItem.SystemProxyAdvancedProtocol.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
strProxy = $"{Global.Loopback}:{port}";
|
strProxy = $"{Global.Loopback}:{port}";
|
||||||
@@ -86,6 +102,8 @@ public static class SysProxyHandler
|
|||||||
.Replace("{http_port}", port.ToString())
|
.Replace("{http_port}", port.ToString())
|
||||||
.Replace("{socks_port}", port.ToString());
|
.Replace("{socks_port}", port.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (strProxy, strExceptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Security.Authentication;
|
||||||
using Downloader;
|
using Downloader;
|
||||||
|
|
||||||
namespace ServiceLib.Helper;
|
namespace ServiceLib.Helper;
|
||||||
@@ -14,6 +15,8 @@ public class DownloaderHelper
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
|
||||||
|
|
||||||
Uri uri = new(url);
|
Uri uri = new(url);
|
||||||
//Authorization Header
|
//Authorization Header
|
||||||
var headers = new WebHeaderCollection();
|
var headers = new WebHeaderCollection();
|
||||||
@@ -22,17 +25,19 @@ public class DownloaderHelper
|
|||||||
headers.Add(HttpRequestHeader.Authorization, "Basic " + Utils.Base64Encode(uri.UserInfo));
|
headers.Add(HttpRequestHeader.Authorization, "Basic " + Utils.Base64Encode(uri.UserInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requestConfiguration = new RequestConfiguration()
|
||||||
|
{
|
||||||
|
Headers = headers,
|
||||||
|
UserAgent = userAgent,
|
||||||
|
ConnectTimeout = connectTimeout * 1000,
|
||||||
|
Proxy = webProxy
|
||||||
|
};
|
||||||
var downloadOpt = new DownloadConfiguration()
|
var downloadOpt = new DownloadConfiguration()
|
||||||
{
|
{
|
||||||
BlockTimeout = timeout * 1000,
|
BlockTimeout = timeout * 1000,
|
||||||
MaxTryAgainOnFailure = 2,
|
MaxTryAgainOnFailure = 2,
|
||||||
RequestConfiguration =
|
RequestConfiguration = requestConfiguration,
|
||||||
{
|
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
||||||
Headers = headers,
|
|
||||||
UserAgent = userAgent,
|
|
||||||
ConnectTimeout = timeout * 1000,
|
|
||||||
Proxy = webProxy
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
||||||
@@ -45,7 +50,9 @@ public class DownloaderHelper
|
|||||||
};
|
};
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource();
|
||||||
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token);
|
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
|
||||||
|
|
||||||
|
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
|
||||||
using StreamReader reader = new(stream);
|
using StreamReader reader = new(stream);
|
||||||
|
|
||||||
downloadOpt = null;
|
downloadOpt = null;
|
||||||
@@ -60,15 +67,18 @@ public class DownloaderHelper
|
|||||||
throw new ArgumentNullException(nameof(url));
|
throw new ArgumentNullException(nameof(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
|
||||||
|
var requestConfiguration = new RequestConfiguration()
|
||||||
|
{
|
||||||
|
ConnectTimeout = connectTimeout * 1000,
|
||||||
|
Proxy = webProxy
|
||||||
|
};
|
||||||
var downloadOpt = new DownloadConfiguration()
|
var downloadOpt = new DownloadConfiguration()
|
||||||
{
|
{
|
||||||
BlockTimeout = timeout * 1000,
|
BlockTimeout = timeout * 1000,
|
||||||
MaxTryAgainOnFailure = 2,
|
MaxTryAgainOnFailure = 2,
|
||||||
RequestConfiguration =
|
RequestConfiguration = requestConfiguration,
|
||||||
{
|
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
||||||
ConnectTimeout= timeout * 1000,
|
|
||||||
Proxy = webProxy
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var lastUpdateTime = DateTime.Now;
|
var lastUpdateTime = DateTime.Now;
|
||||||
@@ -116,7 +126,7 @@ public class DownloaderHelper
|
|||||||
};
|
};
|
||||||
//progress.Report("......");
|
//progress.Report("......");
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource();
|
||||||
cts.CancelAfter(timeout * 1000);
|
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
|
||||||
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
|
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
|
||||||
|
|
||||||
downloadOpt = null;
|
downloadOpt = null;
|
||||||
@@ -137,15 +147,18 @@ public class DownloaderHelper
|
|||||||
File.Delete(fileName);
|
File.Delete(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
|
||||||
|
var requestConfiguration = new RequestConfiguration()
|
||||||
|
{
|
||||||
|
ConnectTimeout = connectTimeout * 1000,
|
||||||
|
Proxy = webProxy
|
||||||
|
};
|
||||||
var downloadOpt = new DownloadConfiguration()
|
var downloadOpt = new DownloadConfiguration()
|
||||||
{
|
{
|
||||||
BlockTimeout = timeout * 1000,
|
BlockTimeout = timeout * 1000,
|
||||||
MaxTryAgainOnFailure = 2,
|
MaxTryAgainOnFailure = 2,
|
||||||
RequestConfiguration =
|
RequestConfiguration = requestConfiguration,
|
||||||
{
|
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
||||||
ConnectTimeout= timeout * 1000,
|
|
||||||
Proxy = webProxy
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var progressPercentage = 0;
|
var progressPercentage = 0;
|
||||||
@@ -182,4 +195,75 @@ public class DownloaderHelper
|
|||||||
|
|
||||||
downloadOpt = null;
|
downloadOpt = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/bezzad/Downloader/blob/a75a6e431acd6cbba6293f7afdcf676544a09174/src/Downloader/SocketClient.cs#L45
|
||||||
|
// There is a risk of MITM attacks
|
||||||
|
// https://github.com/bezzad/Downloader/blob/a75a6e431acd6cbba6293f7afdcf676544a09174/src/Downloader/Extensions/ExceptionHelper.cs#L111
|
||||||
|
private static SocketsHttpHandler GetSocketsHttpHandler(RequestConfiguration config)
|
||||||
|
{
|
||||||
|
SocketsHttpHandler handler = new()
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = config.AllowAutoRedirect,
|
||||||
|
MaxAutomaticRedirections = config.MaximumAutomaticRedirections,
|
||||||
|
AutomaticDecompression = config.AutomaticDecompression,
|
||||||
|
PreAuthenticate = config.PreAuthenticate,
|
||||||
|
UseCookies = config.CookieContainer != null,
|
||||||
|
UseProxy = config.Proxy != null,
|
||||||
|
MaxConnectionsPerServer = 1000,
|
||||||
|
PooledConnectionIdleTimeout = config.KeepAliveTimeout,
|
||||||
|
PooledConnectionLifetime = Timeout.InfiniteTimeSpan,
|
||||||
|
EnableMultipleHttp2Connections = true,
|
||||||
|
ConnectTimeout = TimeSpan.FromMilliseconds(config.ConnectTimeout)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up the SslClientAuthenticationOptions for custom certificate validation
|
||||||
|
if (config.ClientCertificates?.Count > 0)
|
||||||
|
{
|
||||||
|
handler.SslOptions.ClientCertificates = config.ClientCertificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.SslOptions.EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12;
|
||||||
|
//handler.SslOptions.RemoteCertificateValidationCallback = ExceptionHelper.CertificateValidationCallBack;
|
||||||
|
|
||||||
|
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
||||||
|
if (certificateChainPolicy != null)
|
||||||
|
{
|
||||||
|
handler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
|
||||||
|
handler.SslOptions.RemoteCertificateValidationCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure keep-alive
|
||||||
|
if (config.KeepAlive)
|
||||||
|
{
|
||||||
|
handler.KeepAlivePingTimeout = config.KeepAliveTimeout;
|
||||||
|
handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure credentials
|
||||||
|
if (config.Credentials != null)
|
||||||
|
{
|
||||||
|
handler.Credentials = config.Credentials;
|
||||||
|
handler.PreAuthenticate = config.PreAuthenticate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure cookies
|
||||||
|
if (handler.UseCookies && config.CookieContainer != null)
|
||||||
|
{
|
||||||
|
handler.CookieContainer = config.CookieContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure proxy
|
||||||
|
if (handler.UseProxy && config.Proxy != null)
|
||||||
|
{
|
||||||
|
handler.Proxy = config.Proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add expect header
|
||||||
|
if (!string.IsNullOrWhiteSpace(config.Expect))
|
||||||
|
{
|
||||||
|
handler.Expect100ContinueTimeout = TimeSpan.FromSeconds(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public sealed class AppManager
|
|||||||
private int? _statePort2;
|
private int? _statePort2;
|
||||||
public static AppManager Instance => _instance.Value;
|
public static AppManager Instance => _instance.Value;
|
||||||
public Config Config => _config;
|
public Config Config => _config;
|
||||||
|
public IWindowDialog WindowDialog { get; set; } = null!;
|
||||||
|
|
||||||
public int StatePort
|
public int StatePort
|
||||||
{
|
{
|
||||||
@@ -244,9 +245,26 @@ public sealed class AppManager
|
|||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
|
|
||||||
.Where(it => ids.Contains(it.IndexId))
|
if (ids.Count <= Global.SqliteMaxBatchSize)
|
||||||
.ToListAsync();
|
{
|
||||||
|
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
|
||||||
|
.Where(it => ids.Contains(it.IndexId))
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = new List<ProfileItem>();
|
||||||
|
for (var size = 0; size < ids.Count; size += Global.SqliteMaxBatchSize)
|
||||||
|
{
|
||||||
|
var chunk = ids.Skip(size).Take(Global.SqliteMaxBatchSize).ToList();
|
||||||
|
var chunkItems = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
|
||||||
|
.Where(it => chunk.Contains(it.IndexId))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
items.AddRange(chunkItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
|
public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
|
||||||
@@ -257,18 +275,11 @@ public sealed class AppManager
|
|||||||
|
|
||||||
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
|
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
|
||||||
{
|
{
|
||||||
var idList = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
|
var ids = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
|
||||||
if (idList.Count == 0)
|
var items = await GetProfileItemsByIndexIds(ids);
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var items = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
|
|
||||||
.Where(it => idList.Contains(it.IndexId))
|
|
||||||
.ToListAsync();
|
|
||||||
var itemMap = items.ToDictionary(it => it.IndexId);
|
var itemMap = items.ToDictionary(it => it.IndexId);
|
||||||
|
|
||||||
return idList.Select(id => itemMap.GetValueOrDefault(id))
|
return ids.Select(itemMap.GetValueOrDefault)
|
||||||
.Where(item => item != null)
|
.Where(item => item != null)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -652,7 +663,7 @@ public sealed class AppManager
|
|||||||
return Global.SsSecuritiesInSingbox;
|
return Global.SsSecuritiesInSingbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ECoreType GetCoreType(ProfileItem profileItem, EConfigType eConfigType)
|
public ECoreType GetCoreType(ProfileItem? profileItem, EConfigType eConfigType)
|
||||||
{
|
{
|
||||||
if (profileItem?.CoreType != null)
|
if (profileItem?.CoreType != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,206 +4,31 @@ using System.Security.Cryptography.X509Certificates;
|
|||||||
namespace ServiceLib.Manager;
|
namespace ServiceLib.Manager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manager for certificate operations with CA pinning to prevent MITM attacks
|
/// Manager for certificate operations with CA pinning to prevent MITM attacks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CertPemManager
|
public class CertPemManager
|
||||||
{
|
{
|
||||||
private static readonly string _tag = "CertPemManager";
|
private static readonly string _tag = "CertPemManager";
|
||||||
private static readonly Lazy<CertPemManager> _instance = new(() => new());
|
private static readonly Lazy<CertPemManager> _instance = new(() => new());
|
||||||
|
private Config _config;
|
||||||
|
|
||||||
|
public async Task Init(Config config)
|
||||||
|
{
|
||||||
|
if (_config != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_config = config;
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public static CertPemManager Instance => _instance.Value;
|
public static CertPemManager Instance => _instance.Value;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
|
/// Get certificate in PEM format from a server with CA pinning validation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly HashSet<string> TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase)
|
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
|
||||||
{
|
List<string>? verifyPeerCertByName = null, int timeout = 4)
|
||||||
"EBD41040E4BB3EC742C9E381D31EF2A41A48B6685C96E7CEF3C1DF6CD4331C99", // GlobalSign Root CA
|
|
||||||
"6DC47172E01CBCB0BF62580D895FE2B8AC9AD4F873801E0C10B9C837D21EB177", // Entrust.net Premium 2048 Secure Server CA
|
|
||||||
"73C176434F1BC6D5ADF45B0E76E727287C8DE57616C1E6E6141A2B2CBC7D8E4C", // Entrust Root Certification Authority
|
|
||||||
"D8E0FEBC1DB2E38D00940F37D27D41344D993E734B99D5656D9778D4D8143624", // Certum Root CA
|
|
||||||
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root
|
|
||||||
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2
|
|
||||||
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3
|
|
||||||
"CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root
|
|
||||||
"C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA
|
|
||||||
"1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA
|
|
||||||
"3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA
|
|
||||||
"4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA
|
|
||||||
"7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA
|
|
||||||
"62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2
|
|
||||||
"F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA
|
|
||||||
"4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA
|
|
||||||
"0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority
|
|
||||||
"1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority
|
|
||||||
"41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA
|
|
||||||
"E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna
|
|
||||||
"C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority
|
|
||||||
"EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA
|
|
||||||
"6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány
|
|
||||||
"3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009
|
|
||||||
"CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3
|
|
||||||
"2530CC8E98321502BAD96F9B1FBA1B099E2D299E0F4548BB914F363BC0D4531F", // Izenpe.com
|
|
||||||
"45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2
|
|
||||||
"2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2
|
|
||||||
"568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services Root Certificate Authority - G2
|
|
||||||
"0376AB1D54C5F9803CE4B2E201A0EE7EEF7B57B636E8A93C9B8D4860C96F5FA7", // AffirmTrust Commercial
|
|
||||||
"0A81EC5A929777F145904AF38D5D509F66B5E2C58FCDB531058B0E17F3F0B41B", // AffirmTrust Networking
|
|
||||||
"70A73F7F376B60074248904534B11482D5BF0E698ECC498DF52577EBF2E93B9A", // AffirmTrust Premium
|
|
||||||
"BD71FDF6DA97E4CF62D1647ADD2581B07D79ADF8397EB4ECBA9C5E8488821423", // AffirmTrust Premium ECC
|
|
||||||
"5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA
|
|
||||||
"BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority
|
|
||||||
"513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2
|
|
||||||
"55926084EC963A64B96E2ABE01CE0BA86A64FBFEBCC7AAB5AFC155B37FD76066", // Actalis Authentication Root CA
|
|
||||||
"9A114025197C5BB95D94E63D55CD43790847B646B23CDF11ADA4A00EFF15FB48", // Buypass Class 2 Root CA
|
|
||||||
"EDF7EBBCA27A2A384D387B7D4010C666E2EDB4843E4C29B4AE1D5B9332E6B24D", // Buypass Class 3 Root CA
|
|
||||||
"FD73DAD31C644FF1B43BEF0CCDDA96710B9CD9875ECA7E31707AF3E96D522BBD", // T-TeleSec GlobalRoot Class 3
|
|
||||||
"49E7A442ACF0EA6287050054B52564B650E4F49E42E348D6AA38E039E957B1C1", // D-TRUST Root Class 3 CA 2 2009
|
|
||||||
"EEC5496B988CE98625B934092EEC2908BED0B0F316C2D4730C84EAF1F3D34881", // D-TRUST Root Class 3 CA 2 EV 2009
|
|
||||||
"E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2
|
|
||||||
"9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1
|
|
||||||
"59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA
|
|
||||||
"DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1
|
|
||||||
"91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2
|
|
||||||
"F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011
|
|
||||||
"8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3
|
|
||||||
"8FE4FB0AF93A4D0D67DB0BEBB23E37C71BF325DCBCDD240EA04DAF58B47E1840", // QuoVadis Root CA 2 G3
|
|
||||||
"88EF81DE202EB018452E43F864725CEA5FBD1FC2D9D205730709C5D8B8690F46", // QuoVadis Root CA 3 G3
|
|
||||||
"7D05EBB682339F8C9451EE094EEBFEFA7953A114EDB2F44949452FAB7D2FC185", // DigiCert Assured ID Root G2
|
|
||||||
"7E37CB8B4C47090CAB36551BA6F45DB840680FBA166A952DB100717F43053FC2", // DigiCert Assured ID Root G3
|
|
||||||
"CB3CCBB76031E5E0138F8DD39A23F9DE47FFC35E43C1144CEA27D46A5AB1CB5F", // DigiCert Global Root G2
|
|
||||||
"31AD6648F8104138C738F39EA4320133393E3A18CC02296EF97C2AC9EF6731D0", // DigiCert Global Root G3
|
|
||||||
"552F7BDCF1A7AF9E6CE672017F4F12ABF77240C78E761AC203D1D9D20AC89988", // DigiCert Trusted Root G4
|
|
||||||
"52F0E1C4E58EC629291B60317F074671B85D7EA80D5B07273463534B32B40234", // COMODO RSA Certification Authority
|
|
||||||
"E793C9B02FD8AA13E21C31228ACCB08119643B749C898964B1746D46C3D4CBD2", // USERTrust RSA Certification Authority
|
|
||||||
"4FF460D54B9C86DABFBCFC5712E0400D2BED3FBC4D4FBDAA86E06ADCD2A9AD7A", // USERTrust ECC Certification Authority
|
|
||||||
"179FBC148A3DD00FD24EA13458CC43BFA7F59C8182D783A513F6EBEC100C8924", // GlobalSign ECC Root CA - R5
|
|
||||||
"3C4FB0B95AB8B30032F432B86F535FE172C185D0FD39865837CF36187FA6F428", // Staat der Nederlanden Root CA - G3
|
|
||||||
"5D56499BE4D2E08BCFCAD08A3E38723D50503BDE706948E42F55603019E528AE", // IdenTrust Commercial Root CA 1
|
|
||||||
"30D0895A9A448A262091635522D1F52010B5867ACAE12C78EF958FD4F4389F2F", // IdenTrust Public Sector Root CA 1
|
|
||||||
"43DF5774B03E7FEF5FE40D931A7BEDF1BB2E6B42738C4E6D3841103D3AA7F339", // Entrust Root Certification Authority - G2
|
|
||||||
"02ED0EB28C14DA45165C566791700D6451D7FB56F0B2AB1D3B8EB070E56EDFF5", // Entrust Root Certification Authority - EC1
|
|
||||||
"5CC3D78E4E1D5E45547A04E6873E64F90CF9536D1CCC2EF800F355C4C5FD70FD", // CFCA EV ROOT
|
|
||||||
"6B9C08E86EB0F767CFAD65CD98B62149E5494A67F5845E7BD1ED019F27B86BD6", // OISTE WISeKey Global Root GB CA
|
|
||||||
"A1339D33281A0B56E557D3D32B1CE7F9367EB094BD5FA72A7E5004C8DED7CAFE", // SZAFIR ROOT CA2
|
|
||||||
"B676F2EDDAE8775CD36CB0F63CD1D4603961F49E6265BA013A2F0307B6D0B804", // Certum Trusted Network CA 2
|
|
||||||
"A040929A02CE53B4ACF4F2FFC6981CE4496F755E6D45FE0B2A692BCD52523F36", // Hellenic Academic and Research Institutions RootCA 2015
|
|
||||||
"44B545AA8A25E65A73CA15DC27FC36D24C1CB9953A066539B11582DC487B4833", // Hellenic Academic and Research Institutions ECC RootCA 2015
|
|
||||||
"96BCEC06264976F37460779ACF28C5A7CFE8A3C0AAE11A8FFCEE05C0BDDF08C6", // ISRG Root X1
|
|
||||||
"EBC5570C29018C4D67B1AA127BAF12F703B4611EBC17B7DAB5573894179B93FA", // AC RAIZ FNMT-RCM
|
|
||||||
"8ECDE6884F3D87B1125BA31AC3FCB13D7016DE7F57CC904FE1CB97C6AE98196E", // Amazon Root CA 1
|
|
||||||
"1BA5B2AA8C65401A82960118F80BEC4F62304D83CEC4713A19C39C011EA46DB4", // Amazon Root CA 2
|
|
||||||
"18CE6CFE7BF14E60B2E347B8DFE868CB31D02EBB3ADA271569F50343B46DB3A4", // Amazon Root CA 3
|
|
||||||
"E35D28419ED02025CFA69038CD623962458DA5C695FBDEA3C22B0BFB25897092", // Amazon Root CA 4
|
|
||||||
"A1A86D04121EB87F027C66F53303C28E5739F943FC84B38AD6AF009035DD9457", // D-TRUST Root CA 3 2013
|
|
||||||
"46EDC3689046D53A453FB3104AB80DCAEC658B2660EA1629DD7E867990648716", // TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1
|
|
||||||
"BFFF8FD04433487D6A8AA60C1A29767A9FC2BBB05E420F713A13B992891D3893", // GDCA TrustAUTH R5 ROOT
|
|
||||||
"85666A562EE0BE5CE925C1D8890A6F76A87EC16D4D7D5F29EA7419CF20123B69", // SSL.com Root Certification Authority RSA
|
|
||||||
"3417BB06CC6007DA1B961C920B8AB4CE3FAD820E4AA30B9ACBC4A74EBDCEBC65", // SSL.com Root Certification Authority ECC
|
|
||||||
"2E7BF16CC22485A7BBE2AA8696750761B0AE39BE3B2FE9D0CC6D4EF73491425C", // SSL.com EV Root Certification Authority RSA R2
|
|
||||||
"22A2C1F7BDED704CC1E701B5F408C310880FE956B5DE2A4A44F99C873A25A7C8", // SSL.com EV Root Certification Authority ECC
|
|
||||||
"2CABEAFE37D06CA22ABA7391C0033D25982952C453647349763A3AB5AD6CCF69", // GlobalSign Root CA - R6
|
|
||||||
"8560F91C3624DABA9570B5FEA0DBE36FF11A8323BE9486854FB3F34A5571198D", // OISTE WISeKey Global Root GC CA
|
|
||||||
"9BEA11C976FE014764C1BE56A6F914B5A560317ABD9988393382E5161AA0493C", // UCA Global G2 Root
|
|
||||||
"D43AF9B35473755C9684FC06D7D8CB70EE5C28E773FB294EB41EE71722924D24", // UCA Extended Validation Root
|
|
||||||
"D48D3D23EEDB50A459E55197601C27774B9D7B18C94D5A059511A10250B93168", // Certigna Root CA
|
|
||||||
"40F6AF0346A99AA1CD1D555A4E9CCE62C7F9634603EE406615833DC8C8D00367", // emSign Root CA - G1
|
|
||||||
"86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B", // emSign ECC Root CA - G3
|
|
||||||
"125609AA301DA0A249B97A8239CB6A34216F44DCAC9F3954B14292F2E8C8608F", // emSign Root CA - C1
|
|
||||||
"BC4D809B15189D78DB3E1D8CF4F9726A795DA1643CA5F1358E1DDB0EDC0D7EB3", // emSign ECC Root CA - C3
|
|
||||||
"5A2FC03F0C83B090BBFA40604B0988446C7636183DF9846E17101A447FB8EFD6", // Hongkong Post Root CA 3
|
|
||||||
"DB3517D1F6732A2D5AB97C533EC70779EE3270A62FB4AC4238372460E6F01E88", // Entrust Root Certification Authority - G4
|
|
||||||
"358DF39D764AF9E1B766E9C972DF352EE15CFAC227AF6AD1D70E8E4A6EDCBA02", // Microsoft ECC Root Certificate Authority 2017
|
|
||||||
"C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017
|
|
||||||
"BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017
|
|
||||||
"657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2
|
|
||||||
"97552015F5DDFC3C8788C006944555408894450084F100867086BC1A2BB58DC8", // Trustwave Global Certification Authority
|
|
||||||
"945BBC825EA554F489D1FD51A73DDF2EA624AC7019A05205225C22A78CCFA8B4", // Trustwave Global ECC P256 Certification Authority
|
|
||||||
"55903859C8C0C3EBB8759ECE4E2557225FF5758BBD38EBD48276601E1BD58097", // Trustwave Global ECC P384 Certification Authority
|
|
||||||
"88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority
|
|
||||||
"554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS
|
|
||||||
"319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45
|
|
||||||
"5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45
|
|
||||||
"4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46
|
|
||||||
"CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46
|
|
||||||
"9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020
|
|
||||||
"FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA
|
|
||||||
"6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA
|
|
||||||
"FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA
|
|
||||||
"2E44102AB58CB85419451C8E19D9ACF3662CAFBC614B6A53960A30F7D0E2EB41", // TunTrust Root CA
|
|
||||||
"D95D0E8EDA79525BF9BEB11B14D2100D3294985F0C62D9FABD9CD999ECCB7B1D", // HARICA TLS RSA Root CA 2021
|
|
||||||
"3F99CC474ACFCE4DFED58794665E478D1547739F2E780F1BB4CA9B133097D401", // HARICA TLS ECC Root CA 2021
|
|
||||||
"1BE7ABE30686B16348AFD1C61B6866A0EA7F4821E67D5E8AF937CF8011BC750D", // HARICA Client RSA Root CA 2021
|
|
||||||
"8DD4B5373CB0DE36769C12339280D82746B3AA6CD426E797A31BABE4279CF00B", // HARICA Client ECC Root CA 2021
|
|
||||||
"57DE0583EFD2B26E0361DA99DA9DF4648DEF7EE8441C3B728AFA9BCDE0F9B26A", // Autoridad de Certificacion Firmaprofesional CIF A62634068
|
|
||||||
"30FBBA2C32238E2A98547AF97931E550428B9B3F1C8EEB6633DCFA86C5B27DD3", // vTrus ECC Root CA
|
|
||||||
"8A71DE6559336F426C26E53880D00D88A18DA4C6A91F0DCB6194E206C5C96387", // vTrus Root CA
|
|
||||||
"69729B8E15A86EFC177A57AFB7171DFC64ADD28C2FCA8CF1507E34453CCB1470", // ISRG Root X2
|
|
||||||
"F015CE3CC239BFEF064BE9F1D2C417E1A0264A0A94BE1F0C8D121864EB6949CC", // HiPKI Root CA - G1
|
|
||||||
"B085D70B964F191A73E4AF0D54AE7A0E07AAFDAF9B71DD0862138AB7325A24A2", // GlobalSign ECC Root CA - R4
|
|
||||||
"D947432ABDE7B7FA90FC2E6B59101B1280E0E1C7E4E40FA3C6887FFF57A7F4CF", // GTS Root R1
|
|
||||||
"8D25CD97229DBF70356BDA4EB3CC734031E24CF00FAFCFD32DC76EB5841C7EA8", // GTS Root R2
|
|
||||||
"34D8A73EE208D9BCDB0D956520934B4E40E69482596E8B6F73C8426B010A6F48", // GTS Root R3
|
|
||||||
"349DFA4058C5E263123B398AE795573C4E1313C83FE68F93556CD5E8031B3C7D", // GTS Root R4
|
|
||||||
"242B69742FCB1E5B2ABF98898B94572187544E5B4D9911786573621F6A74B82C", // Telia Root CA v2
|
|
||||||
"E59AAA816009C22BFF5B25BAD37DF306F049797C1F81D85AB089E657BD8F0044", // D-TRUST BR Root CA 1 2020
|
|
||||||
"08170D1AA36453901A2F959245E347DB0C8D37ABAABC56B81AA100DC958970DB", // D-TRUST EV Root CA 1 2020
|
|
||||||
"018E13F0772532CF809BD1B17281867283FC48C6E13BE9C69812854A490C1B05", // DigiCert TLS ECC P384 Root G5
|
|
||||||
"371A00DC0533B3721A7EEB40E8419E70799D2B0A0F2C1D80693165F7CEC4AD75", // DigiCert TLS RSA4096 Root G5
|
|
||||||
"E8E8176536A60CC2C4E10187C3BEFCA20EF263497018F566D5BEA0F94D0C111B", // DigiCert SMIME ECC P384 Root G5
|
|
||||||
"90370D3EFA88BF58C30105BA25104A358460A7FA52DFC2011DF233A0F417912A", // DigiCert SMIME RSA4096 Root G5
|
|
||||||
"77B82CD8644C4305F7ACC5CB156B45675004033D51C60C6202A8E0C33467D3A0", // Certainly Root R1
|
|
||||||
"B4585F22E4AC756A4E8612A1361C5D9D031A93FD84FEBB778FA3068B0FC42DC2", // Certainly Root E1
|
|
||||||
"82BD5D851ACF7F6E1BA7BFCBC53030D0E7BC3C21DF772D858CAB41D199BDF595", // DIGITALSIGN GLOBAL ROOT RSA CA
|
|
||||||
"261D7114AE5F8FF2D8C7209A9DE4289E6AFC9D717023D85450909199F1857CFE", // DIGITALSIGN GLOBAL ROOT ECDSA CA
|
|
||||||
"E74FBDA55BD564C473A36B441AA799C8A68E077440E8288B9FA1E50E4BBACA11", // Security Communication ECC RootCA1
|
|
||||||
"F3896F88FE7C0A882766A7FA6AD2749FB57A7F3E98FB769C1FA7B09C2C44D5AE", // BJCA Global Root CA1
|
|
||||||
"574DF6931E278039667B720AFDC1600FC27EB66DD3092979FB73856487212882", // BJCA Global Root CA2
|
|
||||||
"48E1CF9E43B688A51044160F46D773B8277FE45BEAAD0E4DF90D1974382FEA99", // LAWtrust Root CA2 (4096)
|
|
||||||
"22D9599234D60F1D4BC7C7E96F43FA555B07301FD475175089DAFB8C25E477B3", // Sectigo Public Email Protection Root E46
|
|
||||||
"D5917A7791EB7CF20A2E57EB98284A67B28A57E89182DA53D546678C9FDE2B4F", // Sectigo Public Email Protection Root R46
|
|
||||||
"C90F26F0FB1B4018B22227519B5CA2B53E2CA5B3BE5CF18EFE1BEF47380C5383", // Sectigo Public Server Authentication Root E46
|
|
||||||
"7BB647A62AEEAC88BF257AA522D01FFEA395E0AB45C73F93F65654EC38F25A06", // Sectigo Public Server Authentication Root R46
|
|
||||||
"8FAF7D2E2CB4709BB8E0B33666BF75A5DD45B5DE480F8EA8D4BFE6BEBC17F2ED", // SSL.com TLS RSA Root CA 2022
|
|
||||||
"C32FFD9F46F936D16C3673990959434B9AD60AAFBB9E7CF33654F144CC1BA143", // SSL.com TLS ECC Root CA 2022
|
|
||||||
"AD7DD58D03AEDB22A30B5084394920CE12230C2D8017AD9B81AB04079BDD026B", // SSL.com Client ECC Root CA 2022
|
|
||||||
"1D4CA4A2AB21D0093659804FC0EB2175A617279B56A2475245C9517AFEB59153", // SSL.com Client RSA Root CA 2022
|
|
||||||
"E38655F4B0190C84D3B3893D840A687E190A256D98052F159E6D4A39F589A6EB", // Atos TrustedRoot Root CA ECC G2 2020
|
|
||||||
"78833A783BB2986C254B9370D3C20E5EBA8FA7840CBF63FE17297A0B0119685E", // Atos TrustedRoot Root CA RSA G2 2020
|
|
||||||
"B2FAE53E14CCD7AB9212064701AE279C1D8988FACB775FA8A008914E663988A8", // Atos TrustedRoot Root CA ECC TLS 2021
|
|
||||||
"81A9088EA59FB364C548A6F85559099B6F0405EFBF18E5324EC9F457BA00112F", // Atos TrustedRoot Root CA RSA TLS 2021
|
|
||||||
"E0D3226AEB1163C2E48FF9BE3B50B4C6431BE7BB1EACC5C36B5D5EC509039A08", // TrustAsia Global Root CA G3
|
|
||||||
"BE4B56CB5056C0136A526DF444508DAA36A0B54F42E4AC38F72AF470E479654C", // TrustAsia Global Root CA G4
|
|
||||||
"D92C171F5CF890BA428019292927FE22F3207FD2B54449CB6F675AF4922146E2", // D-Trust SBR Root CA 1 2022
|
|
||||||
"DBA84DD7EF622D485463A90137EA4D574DF8550928F6AFA03B4D8B1141E636CC", // D-Trust SBR Root CA 2 2022
|
|
||||||
"3AE6DF7E0D637A65A8C81612EC6F9A142F85A16834C10280D88E707028518755", // Telekom Security SMIME ECC Root 2021
|
|
||||||
"578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020
|
|
||||||
"78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023
|
|
||||||
"EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023
|
|
||||||
"BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB
|
|
||||||
"3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA
|
|
||||||
"3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2
|
|
||||||
"3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12
|
|
||||||
"4B009C1034494F9AB56BBA3BA1D62731FC4D20D8955ADCEC10A925607261E338", // SecureSign Root CA14
|
|
||||||
"E778F0F095FE843729CD1A0082179E5314A9C291442805E1FB1D8FB6B8886C3A", // SecureSign Root CA15
|
|
||||||
"0552E6F83FDF65E8FA9670E666DF28A4E21340B510CBE52566F97C4FB94B2BD1", // D-TRUST BR Root CA 2 2023
|
|
||||||
"436472C1009A325C54F1A5BBB5468A7BAEECCBE05DE5F099CB70D3FE41E13C16", // TrustAsia SMIME ECC Root CA
|
|
||||||
"C7796BEB62C101BB143D262A7C96A0C6168183223EF50D699632D86E03B8CC9B", // TrustAsia SMIME RSA Root CA
|
|
||||||
"C0076B9EF0531FB1A656D67C4EBE97CD5DBAA41EF44598ACC2489878C92D8711", // TrustAsia TLS ECC Root CA
|
|
||||||
"06C08D7DAFD876971EB1124FE67F847EC0C7A158D3EA53CBE940E2EA9791F4C3", // TrustAsia TLS RSA Root CA
|
|
||||||
"8E8221B2E7D4007836A1672F0DCC299C33BC07D316F132FA1A206D587150F1CE", // D-TRUST EV Root CA 2 2023
|
|
||||||
"9A12C392BFE57891A0C545309D4D9FD567E480CB613D6342278B195C79A7931F", // SwissSign RSA SMIME Root CA 2022 - 1
|
|
||||||
"193144F431E0FDDB740717D4DE926A571133884B4360D30E272913CBE660CE41", // SwissSign RSA TLS Root CA 2022 - 1
|
|
||||||
"D9A32485A8CCA85539CEF12FFFFF711378A17851D73DA2732AB4302D763BD62B", // OISTE Client Root ECC G1
|
|
||||||
"D02A0F994A868C66395F2E7A880DF509BD0C29C96DE16015A0FD501EDA4F96A9", // OISTE Client Root RSA G1
|
|
||||||
"EEC997C0C30F216F7E3B8B307D2BAE42412D753FC8219DAFD1520B2572850F49", // OISTE Server Root ECC G1
|
|
||||||
"9AE36232A5189FFDDB353DFD26520C015395D22777DAC59DB57B98C089A651E6", // OISTE Server Root RSA G1
|
|
||||||
"B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get certificate in PEM format from a server with CA pinning validation
|
|
||||||
/// </summary>
|
|
||||||
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -216,13 +41,13 @@ public class CertPemManager
|
|||||||
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
||||||
|
|
||||||
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
|
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
|
||||||
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure));
|
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
|
||||||
await using var ssl = new SslStream(client.GetStream(), false, callback);
|
await using var ssl = new SslStream(client.GetStream(), false, callback);
|
||||||
|
|
||||||
var sslOptions = new SslClientAuthenticationOptions
|
var sslOptions = new SslClientAuthenticationOptions
|
||||||
{
|
{
|
||||||
TargetHost = serverName,
|
TargetHost = serverName,
|
||||||
RemoteCertificateValidationCallback = callback
|
RemoteCertificateValidationCallback = callback,
|
||||||
};
|
};
|
||||||
|
|
||||||
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
|
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
|
||||||
@@ -249,9 +74,10 @@ public class CertPemManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get certificate chain in PEM format from a server with CA pinning validation
|
/// Get certificate chain in PEM format from a server with CA pinning validation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4, bool allowInsecure = false)
|
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
|
||||||
|
List<string>? verifyPeerCertByName = null, int timeout = 4)
|
||||||
{
|
{
|
||||||
var pemList = new List<string>();
|
var pemList = new List<string>();
|
||||||
try
|
try
|
||||||
@@ -265,13 +91,13 @@ public class CertPemManager
|
|||||||
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
||||||
|
|
||||||
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
|
var callback = new RemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) =>
|
||||||
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, allowInsecure));
|
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
|
||||||
await using var ssl = new SslStream(client.GetStream(), false, callback);
|
await using var ssl = new SslStream(client.GetStream(), false, callback);
|
||||||
|
|
||||||
var sslOptions = new SslClientAuthenticationOptions
|
var sslOptions = new SslClientAuthenticationOptions
|
||||||
{
|
{
|
||||||
TargetHost = serverName,
|
TargetHost = serverName,
|
||||||
RemoteCertificateValidationCallback = callback
|
RemoteCertificateValidationCallback = callback,
|
||||||
};
|
};
|
||||||
|
|
||||||
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
|
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
|
||||||
@@ -301,42 +127,35 @@ public class CertPemManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validate server certificate with CA pinning
|
/// Validate server certificate with CA pinning
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool ValidateServerCertificate(
|
private bool ValidateServerCertificate(
|
||||||
object _,
|
object _,
|
||||||
X509Certificate? certificate,
|
X509Certificate? certificate,
|
||||||
X509Chain? chain,
|
X509Chain? chain,
|
||||||
SslPolicyErrors sslPolicyErrors,
|
SslPolicyErrors sslPolicyErrors,
|
||||||
bool allowInsecure)
|
List<string> verifyPeerCertByName)
|
||||||
{
|
{
|
||||||
if (certificate == null)
|
if (certificate == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In insecure mode, accept any certificate so self-signed certs can be fetched.
|
|
||||||
if (allowInsecure)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check certificate name mismatch
|
|
||||||
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build certificate chain
|
// Build certificate chain
|
||||||
var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
|
var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
|
||||||
var certChain = chain ?? new X509Chain();
|
var certChain = chain ?? new X509Chain();
|
||||||
|
|
||||||
certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
|
if (chain == null)
|
||||||
certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
|
{
|
||||||
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
|
certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
||||||
certChain.ChainPolicy.VerificationTime = DateTime.Now;
|
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||||
|
certChain.ChainPolicy.VerificationTime = DateTime.UtcNow;
|
||||||
|
|
||||||
certChain.Build(cert2);
|
if (!certChain.Build(cert2))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find root CA
|
// Find root CA
|
||||||
if (certChain.ChainElements.Count == 0)
|
if (certChain.ChainElements.Count == 0)
|
||||||
@@ -344,10 +163,38 @@ public class CertPemManager
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate;
|
var rootCert = certChain.ChainElements[^1].Certificate;
|
||||||
var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256);
|
|
||||||
|
|
||||||
return TrustedCaThumbprints.Contains(rootThumbprint);
|
var trustedCerts = BuildTrustedCertificateCollection();
|
||||||
|
|
||||||
|
if (!trustedCerts.Contains(rootCert))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sslPolicyErrors.HasFlag(
|
||||||
|
SslPolicyErrors.RemoteCertificateNameMismatch))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verifyPeerCertByName.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var ext in cert2.Extensions)
|
||||||
|
{
|
||||||
|
if (ext is not X509SubjectAlternativeNameExtension san)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return san.EnumerateDnsNames().Any(dnsName =>
|
||||||
|
verifyPeerCertByName.Contains(dnsName, StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ExportCertToPem(X509Certificate2 cert)
|
public static string ExportCertToPem(X509Certificate2 cert)
|
||||||
@@ -358,8 +205,8 @@ public class CertPemManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse concatenated PEM certificates string into a list of individual certificates
|
/// Parse concatenated PEM certificates string into a list of individual certificates
|
||||||
/// Normalizes format: removes line breaks from base64 content for better compatibility
|
/// Normalizes format: removes line breaks from base64 content for better compatibility
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pemChain">Concatenated PEM certificates string (supports both \r\n and \n line endings)</param>
|
/// <param name="pemChain">Concatenated PEM certificates string (supports both \r\n and \n line endings)</param>
|
||||||
/// <returns>List of individual PEM certificate strings with normalized format</returns>
|
/// <returns>List of individual PEM certificate strings with normalized format</returns>
|
||||||
@@ -411,18 +258,13 @@ public class CertPemManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Concatenate a list of PEM certificates into a single string
|
/// Concatenate a list of PEM certificates into a single string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pemList">List of individual PEM certificate strings</param>
|
/// <param name="pemList">List of individual PEM certificate strings</param>
|
||||||
/// <returns>Concatenated PEM certificates string</returns>
|
/// <returns>Concatenated PEM certificates string</returns>
|
||||||
public static string ConcatenatePemChain(IEnumerable<string> pemList)
|
public static string ConcatenatePemChain(IEnumerable<string>? pemList)
|
||||||
{
|
{
|
||||||
if (pemList == null)
|
return pemList == null ? string.Empty : string.Concat(pemList);
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Concat(pemList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false)
|
public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false)
|
||||||
@@ -431,15 +273,57 @@ public class CertPemManager
|
|||||||
{
|
{
|
||||||
var cert = X509Certificate2.CreateFromPem(pemCert);
|
var cert = X509Certificate2.CreateFromPem(pemCert);
|
||||||
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
|
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
|
||||||
if (includeColon)
|
return includeColon ? string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))) : thumbprint;
|
||||||
{
|
|
||||||
return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c)));
|
|
||||||
}
|
|
||||||
return thumbprint;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly Lazy<X509Certificate2Collection> _chromeRootCerts = new(() =>
|
||||||
|
{
|
||||||
|
var pemText = EmbedUtils.GetEmbedText(Global.ChromeRootCertFileName);
|
||||||
|
var collection = new X509Certificate2Collection();
|
||||||
|
collection.ImportFromPem(pemText);
|
||||||
|
return collection;
|
||||||
|
});
|
||||||
|
|
||||||
|
private static readonly Lazy<X509Certificate2Collection> _mozillaRootCerts = new(() =>
|
||||||
|
{
|
||||||
|
var pemText = EmbedUtils.GetEmbedText(Global.MozillaRootCertFileName);
|
||||||
|
var collection = new X509Certificate2Collection();
|
||||||
|
collection.ImportFromPem(pemText);
|
||||||
|
return collection;
|
||||||
|
});
|
||||||
|
|
||||||
|
private X509Certificate2Collection BuildTrustedCertificateCollection()
|
||||||
|
{
|
||||||
|
if (_config.GuiItem.RootCertProvider == Global.ChromeRootProvider)
|
||||||
|
{
|
||||||
|
return _chromeRootCerts.Value;
|
||||||
|
}
|
||||||
|
return _mozillaRootCerts.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsSystemRootCertProvider()
|
||||||
|
{
|
||||||
|
return _config.GuiItem.RootCertProvider != Global.ChromeRootProvider && _config.GuiItem.RootCertProvider != Global.MozillaRootProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509ChainPolicy? BuildCertificateChainPolicy()
|
||||||
|
{
|
||||||
|
if (IsSystemRootCertProvider())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var trustedCerts = BuildTrustedCertificateCollection();
|
||||||
|
var chainPolicy = new X509ChainPolicy
|
||||||
|
{
|
||||||
|
TrustMode = X509ChainTrustMode.CustomRootTrust,
|
||||||
|
RevocationMode = X509RevocationMode.NoCheck,
|
||||||
|
};
|
||||||
|
chainPolicy.CustomTrustStore.AddRange(trustedCerts);
|
||||||
|
return chainPolicy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,19 +45,14 @@ public sealed class ClashApiManager
|
|||||||
{
|
{
|
||||||
await GetClashProxiesAsync();
|
await GetClashProxiesAsync();
|
||||||
}
|
}
|
||||||
lstProxy = new List<ClashProxyModel>();
|
lstProxy = [];
|
||||||
foreach (var kv in _proxies ?? [])
|
lstProxy.AddRange(from kv in _proxies ?? []
|
||||||
{
|
where !Global.notAllowTestType.Contains(kv.Value.type?.ToLower())
|
||||||
if (Global.notAllowTestType.Contains(kv.Value.type?.ToLower()))
|
select new ClashProxyModel()
|
||||||
{
|
{
|
||||||
continue;
|
Name = kv.Value.name,
|
||||||
}
|
Type = kv.Value.type?.ToLower(),
|
||||||
lstProxy.Add(new ClashProxyModel()
|
});
|
||||||
{
|
|
||||||
Name = kv.Value.name,
|
|
||||||
Type = kv.Value.type?.ToLower(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lstProxy is not { Count: > 0 })
|
if (lstProxy is not { Count: > 0 })
|
||||||
|
|||||||
@@ -34,7 +34,18 @@ public class CoreAdminManager
|
|||||||
StringBuilder sb = new();
|
StringBuilder sb = new();
|
||||||
sb.AppendLine("#!/bin/bash");
|
sb.AppendLine("#!/bin/bash");
|
||||||
var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}";
|
var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}";
|
||||||
sb.AppendLine($"exec sudo -S -- {cmdLine}");
|
|
||||||
|
// Passing environment variables to the sudo command, here it only xray or sing-box.
|
||||||
|
if (coreInfo.Environment.Count > 0)
|
||||||
|
{
|
||||||
|
var envArgs = string.Join(" ", coreInfo.Environment.Where(kv => kv.Value.IsNotEmpty()).Select(kv => $"{kv.Key}={kv.Value.AppendQuotes()}"));
|
||||||
|
sb.AppendLine($"exec sudo -S -- env {envArgs} {cmdLine}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.AppendLine($"exec sudo -S -- {cmdLine}");
|
||||||
|
}
|
||||||
|
|
||||||
var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
|
var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
|
||||||
|
|
||||||
var procService = new ProcessService(
|
var procService = new ProcessService(
|
||||||
@@ -80,6 +91,8 @@ public class CoreAdminManager
|
|||||||
.ExecuteBufferedAsync();
|
.ExecuteBufferedAsync();
|
||||||
|
|
||||||
await UpdateFunc(false, result.StandardOutput.ToString());
|
await UpdateFunc(false, result.StandardOutput.ToString());
|
||||||
|
|
||||||
|
await Task.Delay(1000); // Wait for a second to ensure the process is killed
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ public sealed class CoreInfoManager
|
|||||||
DownloadUrlLinux64 = urlN + "/download/{0}/v2rayN-linux-64.zip",
|
DownloadUrlLinux64 = urlN + "/download/{0}/v2rayN-linux-64.zip",
|
||||||
DownloadUrlLinuxArm64 = urlN + "/download/{0}/v2rayN-linux-arm64.zip",
|
DownloadUrlLinuxArm64 = urlN + "/download/{0}/v2rayN-linux-arm64.zip",
|
||||||
DownloadUrlLinuxRiscV64 = urlN + "/download/{0}/v2rayN-linux-riscv64.zip",
|
DownloadUrlLinuxRiscV64 = urlN + "/download/{0}/v2rayN-linux-riscv64.zip",
|
||||||
|
DownloadUrlLinuxLoong64 = urlN + "/download/{0}/v2rayN-linux-loong64.zip",
|
||||||
DownloadUrlOSX64 = urlN + "/download/{0}/v2rayN-macos-64.zip",
|
DownloadUrlOSX64 = urlN + "/download/{0}/v2rayN-macos-64.zip",
|
||||||
DownloadUrlOSXArm64 = urlN + "/download/{0}/v2rayN-macos-arm64.zip",
|
DownloadUrlOSXArm64 = urlN + "/download/{0}/v2rayN-macos-arm64.zip",
|
||||||
},
|
},
|
||||||
@@ -157,6 +158,7 @@ public sealed class CoreInfoManager
|
|||||||
DownloadUrlLinux64 = urlXray + "/download/{0}/Xray-linux-64.zip",
|
DownloadUrlLinux64 = urlXray + "/download/{0}/Xray-linux-64.zip",
|
||||||
DownloadUrlLinuxArm64 = urlXray + "/download/{0}/Xray-linux-arm64-v8a.zip",
|
DownloadUrlLinuxArm64 = urlXray + "/download/{0}/Xray-linux-arm64-v8a.zip",
|
||||||
DownloadUrlLinuxRiscV64 = urlXray + "/download/{0}/Xray-linux-riscv64.zip",
|
DownloadUrlLinuxRiscV64 = urlXray + "/download/{0}/Xray-linux-riscv64.zip",
|
||||||
|
DownloadUrlLinuxLoong64 = urlXray + "/download/{0}/Xray-linux-loong64.zip",
|
||||||
DownloadUrlOSX64 = urlXray + "/download/{0}/Xray-macos-64.zip",
|
DownloadUrlOSX64 = urlXray + "/download/{0}/Xray-macos-64.zip",
|
||||||
DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip",
|
DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip",
|
||||||
Match = "Xray",
|
Match = "Xray",
|
||||||
@@ -180,6 +182,7 @@ public sealed class CoreInfoManager
|
|||||||
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz",
|
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz",
|
||||||
DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz",
|
DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz",
|
||||||
DownloadUrlLinuxRiscV64 = urlMihomo + "/download/{0}/mihomo-linux-riscv64-{0}.gz",
|
DownloadUrlLinuxRiscV64 = urlMihomo + "/download/{0}/mihomo-linux-riscv64-{0}.gz",
|
||||||
|
DownloadUrlLinuxLoong64 = urlMihomo + "/download/{0}/mihomo-linux-loong64-abi2-{0}.gz",
|
||||||
DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-v1-{0}.gz",
|
DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-v1-{0}.gz",
|
||||||
DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz",
|
DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz",
|
||||||
Match = "Mihomo",
|
Match = "Mihomo",
|
||||||
@@ -223,6 +226,7 @@ public sealed class CoreInfoManager
|
|||||||
DownloadUrlLinux64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz",
|
DownloadUrlLinux64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz",
|
||||||
DownloadUrlLinuxArm64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz",
|
DownloadUrlLinuxArm64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz",
|
||||||
DownloadUrlLinuxRiscV64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-riscv64.tar.gz",
|
DownloadUrlLinuxRiscV64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-riscv64.tar.gz",
|
||||||
|
DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz",
|
||||||
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
|
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
|
||||||
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
|
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
|
||||||
Match = "sing-box",
|
Match = "sing-box",
|
||||||
@@ -314,6 +318,7 @@ public sealed class CoreInfoManager
|
|||||||
names.Add("mihomo-linux-amd64");
|
names.Add("mihomo-linux-amd64");
|
||||||
names.Add("mihomo-linux-arm64");
|
names.Add("mihomo-linux-arm64");
|
||||||
names.Add("mihomo-linux-riscv64");
|
names.Add("mihomo-linux-riscv64");
|
||||||
|
names.Add("mihomo-linux-loong64-abi2");
|
||||||
}
|
}
|
||||||
else if (Utils.IsMacOS())
|
else if (Utils.IsMacOS())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ public class CoreManager
|
|||||||
private static readonly Lazy<CoreManager> _instance = new(() => new());
|
private static readonly Lazy<CoreManager> _instance = new(() => new());
|
||||||
public static CoreManager Instance => _instance.Value;
|
public static CoreManager Instance => _instance.Value;
|
||||||
private Config _config;
|
private Config _config;
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
private WindowsJobService? _processJob;
|
private WindowsJobService? _processJob;
|
||||||
|
|
||||||
private ProcessService? _processService;
|
private ProcessService? _processService;
|
||||||
private ProcessService? _processPreService;
|
private ProcessService? _processPreService;
|
||||||
private bool _linuxSudo = false;
|
private bool _linuxSudo = false;
|
||||||
@@ -83,13 +85,14 @@ public class CoreManager
|
|||||||
await CoreStop();
|
await CoreStop();
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
|
|
||||||
if (Utils.IsWindows() && _config.TunModeItem.EnableTun)
|
if (Utils.IsWindows() && (mainContext?.IsTunEnabled == true || preContext?.IsTunEnabled == true))
|
||||||
{
|
{
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
await WindowsUtils.RemoveTunDevice();
|
await WindowsUtils.RemoveTunDevice();
|
||||||
}
|
}
|
||||||
|
|
||||||
await CoreStart(mainContext);
|
await CoreStart(mainContext);
|
||||||
|
await WaitForProxyPort(preContext);
|
||||||
await CoreStartPreService(preContext);
|
await CoreStartPreService(preContext);
|
||||||
|
|
||||||
AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType;
|
AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType;
|
||||||
@@ -180,7 +183,7 @@ public class CoreManager
|
|||||||
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
|
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
|
||||||
|
|
||||||
var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog;
|
var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog;
|
||||||
var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true);
|
var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true, context.IsTunEnabled);
|
||||||
if (proc is null)
|
if (proc is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -198,7 +201,7 @@ public class CoreManager
|
|||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType);
|
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType);
|
||||||
var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true);
|
var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true, preContext.IsTunEnabled);
|
||||||
if (proc is null)
|
if (proc is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -213,11 +216,93 @@ public class CoreManager
|
|||||||
await _updateFunc?.Invoke(notify, msg);
|
await _updateFunc?.Invoke(notify, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000)
|
||||||
|
{
|
||||||
|
if (preContext is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!preContext.IsTunEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs));
|
||||||
|
var rootToken = rootCts.Token;
|
||||||
|
|
||||||
|
var port = preContext.Node.Port;
|
||||||
|
// SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth)
|
||||||
|
ReadOnlyMemory<byte> greeting = new byte[] { 0x05, 0x01, 0x00 };
|
||||||
|
var buf = new byte[2];
|
||||||
|
|
||||||
|
while (!rootToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
using var tcp = new TcpClient();
|
||||||
|
using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
|
||||||
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token);
|
||||||
|
var linkedToken = linkedCts.Token;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await tcp.ConnectAsync(Global.Loopback, port, linkedToken);
|
||||||
|
var stream = tcp.GetStream();
|
||||||
|
|
||||||
|
await stream.WriteAsync(greeting, linkedToken);
|
||||||
|
|
||||||
|
var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken);
|
||||||
|
|
||||||
|
// Server selection: VER=5, METHOD=0x00 — proxy is fully ready
|
||||||
|
if (read == 2 && buf[0] == 0x05)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (!rootToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
|
||||||
|
{
|
||||||
|
// Connection refused, proxy not ready yet, wait 50ms before retrying
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(50, rootToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore other exceptions and continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Private
|
#endregion Private
|
||||||
|
|
||||||
#region Process
|
#region Process
|
||||||
|
|
||||||
private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
|
/// <summary>
|
||||||
|
/// Decides whether a core launch must be elevated on non-Windows platforms.
|
||||||
|
/// The TUN state comes from the immutable <see cref="CoreConfigContext" /> snapshot that
|
||||||
|
/// generated the config, never from the live mutable config: the generated config and the
|
||||||
|
/// launch mode must always agree, even if TUN is toggled while a reload is in flight.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ShouldRunAsSudo(bool isTunLaunch, ECoreType? coreType, bool isNonWindows)
|
||||||
|
{
|
||||||
|
return isTunLaunch
|
||||||
|
&& coreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray
|
||||||
|
&& isNonWindows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo, bool isTunLaunch = false)
|
||||||
{
|
{
|
||||||
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
|
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
|
||||||
if (fileName.IsNullOrEmpty())
|
if (fileName.IsNullOrEmpty())
|
||||||
@@ -229,9 +314,7 @@ public class CoreManager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (mayNeedSudo
|
if (mayNeedSudo
|
||||||
&& _config.TunModeItem.EnableTun
|
&& ShouldRunAsSudo(isTunLaunch, coreInfo.CoreType, Utils.IsNonWindows()))
|
||||||
&& (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo)
|
|
||||||
&& Utils.IsNonWindows())
|
|
||||||
{
|
{
|
||||||
_linuxSudo = true;
|
_linuxSudo = true;
|
||||||
await CoreAdminManager.Instance.Init(_config, _updateFunc);
|
await CoreAdminManager.Instance.Init(_config, _updateFunc);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public class GroupProfileManager
|
|||||||
|
|
||||||
public static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo)
|
public static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo)
|
||||||
{
|
{
|
||||||
return await HasCycle(indexId, extraInfo, new HashSet<string>(), new HashSet<string>());
|
return await HasCycle(indexId, extraInfo, [], []);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet<string> visited, HashSet<string> stack)
|
private static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet<string> visited, HashSet<string> stack)
|
||||||
@@ -55,7 +55,7 @@ public class GroupProfileManager
|
|||||||
var childItems = await AppManager.Instance.GetProfileItemsByIndexIds(childIds);
|
var childItems = await AppManager.Instance.GetProfileItemsByIndexIds(childIds);
|
||||||
foreach (var childItem in childItems)
|
foreach (var childItem in childItems)
|
||||||
{
|
{
|
||||||
if (await HasCycle(childItem.IndexId, childItem?.GetProtocolExtra(), visited, stack))
|
if (await HasCycle(childItem.IndexId, childItem.GetProtocolExtra(), visited, stack))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ public class GroupProfileManager
|
|||||||
|
|
||||||
public static async Task<(List<ProfileItem> Items, ProtocolExtraItem? Extra)> GetChildProfileItems(ProfileItem profileItem)
|
public static async Task<(List<ProfileItem> Items, ProtocolExtraItem? Extra)> GetChildProfileItems(ProfileItem profileItem)
|
||||||
{
|
{
|
||||||
var protocolExtra = profileItem?.GetProtocolExtra();
|
var protocolExtra = profileItem.GetProtocolExtra();
|
||||||
return (await GetChildProfileItemsByProtocolExtra(protocolExtra), protocolExtra);
|
return (await GetChildProfileItemsByProtocolExtra(protocolExtra), protocolExtra);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ public class GroupProfileManager
|
|||||||
return childProfiles?.Where(p =>
|
return childProfiles?.Where(p =>
|
||||||
p != null &&
|
p != null &&
|
||||||
p.IsValid() &&
|
p.IsValid() &&
|
||||||
!p.ConfigType.IsComplexType() &&
|
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
||||||
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
|
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
|
||||||
)
|
)
|
||||||
.ToList() ?? [];
|
.ToList() ?? [];
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//using System.Reactive.Linq;
|
|
||||||
|
|
||||||
namespace ServiceLib.Manager;
|
namespace ServiceLib.Manager;
|
||||||
|
|
||||||
public class ProfileExManager
|
public class ProfileExManager
|
||||||
@@ -111,7 +109,7 @@ public class ProfileExManager
|
|||||||
public async Task ClearAll()
|
public async Task ClearAll()
|
||||||
{
|
{
|
||||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
|
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
|
||||||
_lstProfileEx = new();
|
_lstProfileEx = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveTo()
|
public async Task SaveTo()
|
||||||
@@ -182,6 +180,6 @@ public class ProfileExManager
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return _lstProfileEx.Max(t => t == null ? 0 : t.Sort);
|
return _lstProfileEx.Max(t => t?.Sort ?? 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public class StatisticsManager
|
|||||||
{
|
{
|
||||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem ");
|
await SQLiteHelper.Instance.ExecuteAsync($"delete from ServerStatItem ");
|
||||||
_serverStatItem = null;
|
_serverStatItem = null;
|
||||||
_lstServerStat = new();
|
_lstServerStat = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveTo()
|
public async Task SaveTo()
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class TaskManager
|
|||||||
{
|
{
|
||||||
//Logging.SaveLog("Execute delete expired files");
|
//Logging.SaveLog("Execute delete expired files");
|
||||||
|
|
||||||
FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1));
|
FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1), "Test");
|
||||||
FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1));
|
FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1));
|
||||||
FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
|
FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public class Config
|
|||||||
public List<KeyEventItem> GlobalHotkeys { get; set; }
|
public List<KeyEventItem> GlobalHotkeys { get; set; }
|
||||||
public List<CoreTypeItem> CoreTypeItem { get; set; }
|
public List<CoreTypeItem> CoreTypeItem { get; set; }
|
||||||
public SimpleDNSItem SimpleDNSItem { get; set; }
|
public SimpleDNSItem SimpleDNSItem { get; set; }
|
||||||
|
public HappyEyeballs4RayItem HappyEyeballs4RayItem { get; set; }
|
||||||
|
|
||||||
#endregion other entities
|
#endregion other entities
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ public class CoreBasicItem
|
|||||||
|
|
||||||
public string Loglevel { get; set; }
|
public string Loglevel { get; set; }
|
||||||
|
|
||||||
public bool MuxEnabled { get; set; }
|
|
||||||
|
|
||||||
public bool DefAllowInsecure { get; set; }
|
|
||||||
|
|
||||||
public string DefFingerprint { get; set; }
|
public string DefFingerprint { get; set; }
|
||||||
|
|
||||||
public string DefUserAgent { get; set; }
|
public string DefUserAgent { get; set; }
|
||||||
@@ -21,6 +17,8 @@ public class CoreBasicItem
|
|||||||
|
|
||||||
public bool EnableFragment { get; set; }
|
public bool EnableFragment { get; set; }
|
||||||
|
|
||||||
|
public bool EnableFinalFragment { get; set; }
|
||||||
|
|
||||||
public bool EnableCacheFile4Sbox { get; set; } = true;
|
public bool EnableCacheFile4Sbox { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +74,7 @@ public class GUIItem
|
|||||||
public int TrayMenuServersLimit { get; set; } = 20;
|
public int TrayMenuServersLimit { get; set; } = 20;
|
||||||
public bool EnableHWA { get; set; } = false;
|
public bool EnableHWA { get; set; } = false;
|
||||||
public bool EnableLog { get; set; } = true;
|
public bool EnableLog { get; set; } = true;
|
||||||
|
public string? RootCertProvider { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -104,6 +103,7 @@ public class UIItem
|
|||||||
public bool MacOSShowInDock { get; set; }
|
public bool MacOSShowInDock { get; set; }
|
||||||
public List<ColumnItem> MainColumnItem { get; set; }
|
public List<ColumnItem> MainColumnItem { get; set; }
|
||||||
public List<WindowSizeItem> WindowSizeItem { get; set; }
|
public List<WindowSizeItem> WindowSizeItem { get; set; }
|
||||||
|
public bool HideColumnIpInfo { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -147,7 +147,10 @@ public class TunModeItem
|
|||||||
public int Mtu { get; set; }
|
public int Mtu { get; set; }
|
||||||
public bool EnableIPv6Address { get; set; }
|
public bool EnableIPv6Address { get; set; }
|
||||||
public string IcmpRouting { get; set; }
|
public string IcmpRouting { get; set; }
|
||||||
public bool EnableLegacyProtect { get; set; }
|
public bool EnableLegacyProtect { get; set; } = true;
|
||||||
|
public List<string>? RouteExcludeAddress { get; set; }
|
||||||
|
public string IPv4Address { get; set; }
|
||||||
|
public string IPv6Address { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -248,8 +251,15 @@ public class CheckUpdateItem
|
|||||||
public class Fragment4RayItem
|
public class Fragment4RayItem
|
||||||
{
|
{
|
||||||
public string? Packets { get; set; }
|
public string? Packets { get; set; }
|
||||||
|
public List<string>? Lengths { get; set; }
|
||||||
|
public List<string>? Delays { get; set; }
|
||||||
|
public string? MaxSplit { get; set; }
|
||||||
|
|
||||||
|
// For migration from old version, remove those properties in the future
|
||||||
public string? Length { get; set; }
|
public string? Length { get; set; }
|
||||||
|
|
||||||
public string? Interval { get; set; }
|
public string? Interval { get; set; }
|
||||||
|
// migration end
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -267,14 +277,26 @@ public class SimpleDNSItem
|
|||||||
public bool? AddCommonHosts { get; set; }
|
public bool? AddCommonHosts { get; set; }
|
||||||
public bool? FakeIP { get; set; }
|
public bool? FakeIP { get; set; }
|
||||||
public bool? GlobalFakeIp { get; set; }
|
public bool? GlobalFakeIp { get; set; }
|
||||||
|
public string? FakeIPRange { get; set; }
|
||||||
public bool? BlockBindingQuery { get; set; }
|
public bool? BlockBindingQuery { get; set; }
|
||||||
public string? DirectDNS { get; set; }
|
public string? DirectDNS { get; set; }
|
||||||
public string? RemoteDNS { get; set; }
|
public string? RemoteDNS { get; set; }
|
||||||
public string? BootstrapDNS { get; set; }
|
public string? BootstrapDNS { get; set; }
|
||||||
public string? Strategy4Freedom { get; set; }
|
public string? Strategy4Freedom { get; set; }
|
||||||
public string? Strategy4Proxy { get; set; }
|
public string? Strategy4Proxy { get; set; }
|
||||||
|
public string? Strategy4ProxyDial { get; set; }
|
||||||
public bool? ServeStale { get; set; }
|
public bool? ServeStale { get; set; }
|
||||||
public bool? ParallelQuery { get; set; }
|
public bool? ParallelQuery { get; set; }
|
||||||
public string? Hosts { get; set; }
|
public string? Hosts { get; set; }
|
||||||
public string? DirectExpectedIPs { get; set; }
|
public string? DirectExpectedIPs { get; set; }
|
||||||
|
public bool? EnableHappyEyeballs { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class HappyEyeballs4RayItem
|
||||||
|
{
|
||||||
|
public int? TryDelayMs { get; set; }
|
||||||
|
public bool? PrioritizeIPv6 { get; set; }
|
||||||
|
public int? Interleave { get; set; }
|
||||||
|
public int? MaxConcurrentTry { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,20 @@ public record CoreConfigContext
|
|||||||
public Config AppConfig { get; init; } = new();
|
public Config AppConfig { get; init; } = new();
|
||||||
public FullConfigTemplateItem? FullConfigTemplate { get; init; } = new();
|
public FullConfigTemplateItem? FullConfigTemplate { get; init; } = new();
|
||||||
|
|
||||||
|
public Dictionary<string, string> CustomOutboundContent { get; init; } = new();
|
||||||
|
|
||||||
// Test ServerTestItem Map
|
// Test ServerTestItem Map
|
||||||
public Dictionary<string, string> ServerTestItemMap { get; init; } = new();
|
public Dictionary<string, string> ServerTestItemMap { get; init; } = new();
|
||||||
|
|
||||||
// TUN Compatibility
|
// TUN Compatibility
|
||||||
public bool IsTunEnabled { get; init; } = false;
|
public bool IsTunEnabled { get; init; } = false;
|
||||||
public HashSet<string> ProtectDomainList { get; init; } = [];
|
public HashSet<string> ProtectDomainList { get; init; } = [];
|
||||||
|
// Typically, it is the core of the outbound chain
|
||||||
|
public HashSet<ECoreType> ProtectCoreTypeList { get; init; } = [];
|
||||||
|
|
||||||
public bool IsWindows { get; init; }
|
public bool IsWindows { get; init; }
|
||||||
public bool IsMacOS { get; init; }
|
public bool IsMacOS { get; init; }
|
||||||
|
|
||||||
|
// Generation Context
|
||||||
|
public Dictionary<object, string> CustomOutboundMap { get; init; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class CoreInfo
|
|||||||
public string? DownloadUrlLinux64 { get; set; }
|
public string? DownloadUrlLinux64 { get; set; }
|
||||||
public string? DownloadUrlLinuxArm64 { get; set; }
|
public string? DownloadUrlLinuxArm64 { get; set; }
|
||||||
public string? DownloadUrlLinuxRiscV64 { get; set; }
|
public string? DownloadUrlLinuxRiscV64 { get; set; }
|
||||||
|
public string? DownloadUrlLinuxLoong64 { get; set; }
|
||||||
public string? DownloadUrlOSX64 { get; set; }
|
public string? DownloadUrlOSX64 { get; set; }
|
||||||
public string? DownloadUrlOSXArm64 { get; set; }
|
public string? DownloadUrlOSXArm64 { get; set; }
|
||||||
public string? Match { get; set; }
|
public string? Match { get; set; }
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ public class Rule4Sbox
|
|||||||
public List<string>? wifi_bssid { get; set; }
|
public List<string>? wifi_bssid { get; set; }
|
||||||
public bool? rule_set_ip_cidr_match_source { get; set; }
|
public bool? rule_set_ip_cidr_match_source { get; set; }
|
||||||
public bool? rule_set_ip_cidr_accept_empty { get; set; }
|
public bool? rule_set_ip_cidr_accept_empty { get; set; }
|
||||||
|
public bool? tls_record_fragment { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -110,6 +111,7 @@ public class Inbound4Sbox
|
|||||||
public bool? endpoint_independent_nat { get; set; }
|
public bool? endpoint_independent_nat { get; set; }
|
||||||
public string? stack { get; set; }
|
public string? stack { get; set; }
|
||||||
public List<User4Sbox> users { get; set; }
|
public List<User4Sbox> users { get; set; }
|
||||||
|
public List<string>? route_exclude_address { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class User4Sbox
|
public class User4Sbox
|
||||||
@@ -246,6 +248,16 @@ public class HyObfs4Sbox
|
|||||||
{
|
{
|
||||||
public string? type { get; set; }
|
public string? type { get; set; }
|
||||||
public string? password { get; set; }
|
public string? password { get; set; }
|
||||||
|
public int? min_packet_size { get; set; }
|
||||||
|
public int? max_packet_size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HyRealm4Sbox
|
||||||
|
{
|
||||||
|
public string? server_url { get; set; }
|
||||||
|
public string? token { get; set; }
|
||||||
|
public string? realm_id { get; set; }
|
||||||
|
public List<string>? stun_servers { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Server4Sbox : BaseServer4Sbox
|
public class Server4Sbox : BaseServer4Sbox
|
||||||
@@ -332,6 +344,7 @@ public abstract class DialFields4Sbox
|
|||||||
public Multiplex4Sbox? multiplex { get; set; }
|
public Multiplex4Sbox? multiplex { get; set; }
|
||||||
public Transport4Sbox? transport { get; set; }
|
public Transport4Sbox? transport { get; set; }
|
||||||
public HyObfs4Sbox? obfs { get; set; }
|
public HyObfs4Sbox? obfs { get; set; }
|
||||||
|
public HyRealm4Sbox? realm { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class BaseServer4Sbox : DialFields4Sbox
|
public abstract class BaseServer4Sbox : DialFields4Sbox
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ public class V2rayConfig
|
|||||||
{
|
{
|
||||||
public Log4Ray log { get; set; }
|
public Log4Ray log { get; set; }
|
||||||
public object dns { get; set; }
|
public object dns { get; set; }
|
||||||
|
public FakeDns4Ray? fakedns { get; set; }
|
||||||
public List<Inbounds4Ray> inbounds { get; set; }
|
public List<Inbounds4Ray> inbounds { get; set; }
|
||||||
public List<Outbounds4Ray> outbounds { get; set; }
|
public List<Outbounds4Ray> outbounds { get; set; }
|
||||||
public Routing4Ray routing { get; set; }
|
public Routing4Ray routing { get; set; }
|
||||||
@@ -20,7 +21,7 @@ public class Stats4Ray
|
|||||||
|
|
||||||
public class Metrics4Ray
|
public class Metrics4Ray
|
||||||
{
|
{
|
||||||
public string tag { get; set; }
|
public string listen { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Policy4Ray
|
public class Policy4Ray
|
||||||
@@ -43,6 +44,12 @@ public class Log4Ray
|
|||||||
public string? loglevel { get; set; }
|
public string? loglevel { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FakeDns4Ray
|
||||||
|
{
|
||||||
|
public string? ipPool { get; set; }
|
||||||
|
public long? poolSize { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class Inbounds4Ray
|
public class Inbounds4Ray
|
||||||
{
|
{
|
||||||
public string tag { get; set; }
|
public string tag { get; set; }
|
||||||
@@ -86,7 +93,7 @@ public class Inboundsettings4Ray
|
|||||||
|
|
||||||
public string? autoOutboundsInterface { get; set; }
|
public string? autoOutboundsInterface { get; set; }
|
||||||
|
|
||||||
// public List<string>? dns { get; set; }
|
public List<string>? dns { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UsersItem4Ray
|
public class UsersItem4Ray
|
||||||
@@ -136,8 +143,6 @@ public class Outboundsettings4Ray
|
|||||||
|
|
||||||
public Response4Ray? response { get; set; }
|
public Response4Ray? response { get; set; }
|
||||||
|
|
||||||
public string domainStrategy { get; set; }
|
|
||||||
|
|
||||||
public int? userLevel { get; set; }
|
public int? userLevel { get; set; }
|
||||||
|
|
||||||
public string? secretKey { get; set; }
|
public string? secretKey { get; set; }
|
||||||
@@ -146,6 +151,16 @@ public class Outboundsettings4Ray
|
|||||||
|
|
||||||
public int? port { get; set; }
|
public int? port { get; set; }
|
||||||
|
|
||||||
|
public string? user { get; set; }
|
||||||
|
|
||||||
|
public string? pass { get; set; }
|
||||||
|
|
||||||
|
public int? level { get; set; }
|
||||||
|
|
||||||
|
public string? email { get; set; }
|
||||||
|
|
||||||
|
public object? headers { get; set; }
|
||||||
|
|
||||||
public List<WireguardPeer4Ray>? peers { get; set; }
|
public List<WireguardPeer4Ray>? peers { get; set; }
|
||||||
|
|
||||||
public bool? noKernelTun { get; set; }
|
public bool? noKernelTun { get; set; }
|
||||||
@@ -276,7 +291,6 @@ public class BalancersItem4Ray
|
|||||||
public List<string>? selector { get; set; }
|
public List<string>? selector { get; set; }
|
||||||
public BalancersStrategy4Ray? strategy { get; set; }
|
public BalancersStrategy4Ray? strategy { get; set; }
|
||||||
public string? tag { get; set; }
|
public string? tag { get; set; }
|
||||||
public string? fallbackTag { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BalancersStrategy4Ray
|
public class BalancersStrategy4Ray
|
||||||
@@ -289,16 +303,7 @@ public class BalancersStrategySettings4Ray
|
|||||||
{
|
{
|
||||||
public int? expected { get; set; }
|
public int? expected { get; set; }
|
||||||
public string? maxRTT { get; set; }
|
public string? maxRTT { get; set; }
|
||||||
public float? tolerance { get; set; }
|
public double? tolerance { get; set; }
|
||||||
public List<string>? baselines { get; set; }
|
|
||||||
public List<BalancersStrategySettingsCosts4Ray>? costs { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BalancersStrategySettingsCosts4Ray
|
|
||||||
{
|
|
||||||
public bool? regexp { get; set; }
|
|
||||||
public string? match { get; set; }
|
|
||||||
public float? value { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Observatory4Ray
|
public class Observatory4Ray
|
||||||
@@ -359,8 +364,6 @@ public class StreamSettings4Ray
|
|||||||
|
|
||||||
public class TlsSettings4Ray
|
public class TlsSettings4Ray
|
||||||
{
|
{
|
||||||
public bool? allowInsecure { get; set; }
|
|
||||||
|
|
||||||
public string? serverName { get; set; }
|
public string? serverName { get; set; }
|
||||||
|
|
||||||
public List<string>? alpn { get; set; }
|
public List<string>? alpn { get; set; }
|
||||||
@@ -373,6 +376,7 @@ public class TlsSettings4Ray
|
|||||||
public string? spiderX { get; set; }
|
public string? spiderX { get; set; }
|
||||||
public string? mldsa65Verify { get; set; }
|
public string? mldsa65Verify { get; set; }
|
||||||
public List<CertificateSettings4Ray>? certificates { get; set; }
|
public List<CertificateSettings4Ray>? certificates { get; set; }
|
||||||
|
public string? verifyPeerCertByName { get; set; }
|
||||||
public string? pinnedPeerCertSha256 { get; set; }
|
public string? pinnedPeerCertSha256 { get; set; }
|
||||||
public bool? disableSystemRoot { get; set; }
|
public bool? disableSystemRoot { get; set; }
|
||||||
public string? echConfigList { get; set; }
|
public string? echConfigList { get; set; }
|
||||||
@@ -488,6 +492,7 @@ public class UdpHop4Ray
|
|||||||
|
|
||||||
public class Finalmask4Ray
|
public class Finalmask4Ray
|
||||||
{
|
{
|
||||||
|
public List<Mask4Ray>? tcp { get; set; }
|
||||||
public List<Mask4Ray>? udp { get; set; }
|
public List<Mask4Ray>? udp { get; set; }
|
||||||
public QuicParams4Ray? quicParams { get; set; }
|
public QuicParams4Ray? quicParams { get; set; }
|
||||||
}
|
}
|
||||||
@@ -500,14 +505,23 @@ public class Mask4Ray
|
|||||||
|
|
||||||
public class MaskSettings4Ray
|
public class MaskSettings4Ray
|
||||||
{
|
{
|
||||||
|
public string? header { get; set; }
|
||||||
|
public string? value { get; set; }
|
||||||
|
|
||||||
public string? password { get; set; }
|
public string? password { get; set; }
|
||||||
public string? domain { get; set; }
|
|
||||||
|
public string? url { get; set; }
|
||||||
|
public List<string>? stunServers { get; set; }
|
||||||
|
public string? packetSize { get; set; }
|
||||||
|
|
||||||
// fragment
|
// fragment
|
||||||
public string? packets { get; set; }
|
public string? packets { get; set; }
|
||||||
|
|
||||||
public string? length { get; set; }
|
public string? length { get; set; }
|
||||||
public string? delay { get; set; }
|
public string? delay { get; set; }
|
||||||
|
public List<string>? lengths { get; set; }
|
||||||
|
public List<string>? delays { get; set; }
|
||||||
|
public int? maxSplit { get; set; }
|
||||||
|
|
||||||
// noise
|
// noise
|
||||||
public int? reset { get; set; }
|
public int? reset { get; set; }
|
||||||
@@ -538,15 +552,20 @@ public class AccountsItem4Ray
|
|||||||
|
|
||||||
public class Sockopt4Ray
|
public class Sockopt4Ray
|
||||||
{
|
{
|
||||||
|
public string? domainStrategy { get; set; }
|
||||||
|
|
||||||
public string? dialerProxy { get; set; }
|
public string? dialerProxy { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("interface")]
|
[JsonPropertyName("interface")]
|
||||||
public string? Interface { get; set; }
|
public string? Interface { get; set; }
|
||||||
|
|
||||||
|
public HappyEyeballs4Ray? happyEyeballs { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FragmentItem4Ray
|
public class HappyEyeballs4Ray
|
||||||
{
|
{
|
||||||
public string? packets { get; set; }
|
public int? tryDelayMs { get; set; }
|
||||||
public string? length { get; set; }
|
public bool? prioritizeIPv6 { get; set; }
|
||||||
public string? interval { get; set; }
|
public int? interleave { get; set; }
|
||||||
|
public int? maxConcurrentTry { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
namespace ServiceLib.Models.Dto;
|
namespace ServiceLib.Models.Dto;
|
||||||
|
|
||||||
public class CheckUpdateModel : ReactiveObject
|
public partial class CheckUpdateModel : ReactiveObject
|
||||||
{
|
{
|
||||||
public bool? IsSelected { get; set; }
|
public bool? IsSelected { get; set; }
|
||||||
public ECoreType? CoreType { get; set; }
|
public ECoreType? CoreType { get; set; }
|
||||||
[Reactive] public string? Remarks { get; set; }
|
[Reactive] public partial string? Remarks { get; set; }
|
||||||
public string? FileName { get; set; }
|
public string? FileName { get; set; }
|
||||||
public bool? IsFinished { get; set; }
|
public bool? IsFinished { get; set; }
|
||||||
public bool IsGeoFile { get; set; }
|
public bool IsGeoFile { get; set; }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
namespace ServiceLib.Models.Dto;
|
namespace ServiceLib.Models.Dto;
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class ClashProxyModel : ReactiveObject
|
public partial class ClashProxyModel : ReactiveObject
|
||||||
{
|
{
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
@@ -9,9 +9,9 @@ public class ClashProxyModel : ReactiveObject
|
|||||||
|
|
||||||
public string? Now { get; set; }
|
public string? Now { get; set; }
|
||||||
|
|
||||||
[Reactive] public int Delay { get; set; }
|
[Reactive] public partial int Delay { get; set; }
|
||||||
|
|
||||||
[Reactive] public string? DelayName { get; set; }
|
[Reactive] public partial string? DelayName { get; set; }
|
||||||
|
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
115
v2rayN/ServiceLib/Models/Dto/HyRealm.cs
Normal file
115
v2rayN/ServiceLib/Models/Dto/HyRealm.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
namespace ServiceLib.Models.Dto;
|
||||||
|
|
||||||
|
// realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
|
||||||
|
// realm+http://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
|
||||||
|
// example:
|
||||||
|
// realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478
|
||||||
|
public record HyRealm(
|
||||||
|
bool IsHttp,
|
||||||
|
string Token,
|
||||||
|
string RendezvousHost,
|
||||||
|
int RendezvousPort,
|
||||||
|
string RealmName,
|
||||||
|
List<string> StunList
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public string RendezvousHostPort => RendezvousPort > 0 ? $"{RendezvousHost}:{RendezvousPort}" : RendezvousHost;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// sing-box realm.server_url requires a scheme (https:// or http://).
|
||||||
|
/// </summary>
|
||||||
|
public string ToServerUrl()
|
||||||
|
{
|
||||||
|
var scheme = IsHttp ? "http" : "https";
|
||||||
|
return $"{scheme}://{RendezvousHostPort}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParse(string? str, out HyRealm? realm)
|
||||||
|
{
|
||||||
|
realm = null;
|
||||||
|
if (str == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var isHttp = str.StartsWith("realm+http://");
|
||||||
|
var prefix = isHttp ? "realm+http://" : "realm://";
|
||||||
|
if (!str.StartsWith(prefix))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var uri = new Uri(str);
|
||||||
|
var token = Utils.UrlDecode(uri.UserInfo);
|
||||||
|
var rendezvousHost = uri.Host;
|
||||||
|
var rendezvousPort = uri.IsDefaultPort ? (isHttp ? 80 : 443) : uri.Port;
|
||||||
|
var realmName = uri.AbsolutePath.TrimStart('/');
|
||||||
|
var stunList = new List<string>();
|
||||||
|
var query = uri.Query;
|
||||||
|
if (!query.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
if (query.StartsWith('?'))
|
||||||
|
{
|
||||||
|
query = query.Substring(1);
|
||||||
|
}
|
||||||
|
var pairs = query.Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var pair in pairs)
|
||||||
|
{
|
||||||
|
var idx = pair.IndexOf('=');
|
||||||
|
if (idx <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var key = pair.Substring(0, idx);
|
||||||
|
if (!key.Equals("stun", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var val = pair.Substring(idx + 1);
|
||||||
|
stunList.Add(Utils.UrlDecode(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
realm = new HyRealm(isHttp, token, rendezvousHost, rendezvousPort, realmName, stunList);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToUri()
|
||||||
|
{
|
||||||
|
var prefix = IsHttp ? "realm+http://" : "realm://";
|
||||||
|
var uriBuilder = new UriBuilder
|
||||||
|
{
|
||||||
|
Scheme = IsHttp ? "http" : "realm",
|
||||||
|
Host = RendezvousHost,
|
||||||
|
Port = RendezvousPort,
|
||||||
|
Path = "/" + RealmName,
|
||||||
|
UserName = Utils.UrlEncode(Token),
|
||||||
|
};
|
||||||
|
if (StunList is { Count: > 0 })
|
||||||
|
{
|
||||||
|
// var query = string.Join('&', StunList.Select(s => "stun=" + Utils.UrlEncode(s)));
|
||||||
|
// NOTE: maybe we don't need to encode the stun host:port, since it should be a valid URI component already, and encoding will make it unreadable
|
||||||
|
var query = string.Join('&', StunList.Select(s => "stun=" + s));
|
||||||
|
uriBuilder.Query = query;
|
||||||
|
}
|
||||||
|
return prefix + uriBuilder.Uri.GetComponents(
|
||||||
|
UriComponents.UserInfo | UriComponents.HostAndPort | UriComponents.PathAndQuery, UriFormat.UriEscaped);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToUriForFinalmask()
|
||||||
|
{
|
||||||
|
var prefix = IsHttp ? "realm+http://" : "realm://";
|
||||||
|
var uriBuilder = new StringBuilder();
|
||||||
|
uriBuilder.Append(prefix);
|
||||||
|
uriBuilder.Append(Utils.UrlEncode(Token));
|
||||||
|
uriBuilder.Append('@');
|
||||||
|
uriBuilder.Append(RendezvousHostPort);
|
||||||
|
uriBuilder.Append('/');
|
||||||
|
uriBuilder.Append(Utils.UrlEncode(RealmName));
|
||||||
|
return uriBuilder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ public readonly record struct IpInfoResult(string Country, string? Ip)
|
|||||||
{
|
{
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var emoji = Country.CountryToEmoji();
|
var emoji = Utils.IsWindows() ? null : Country.CountryToEmoji();
|
||||||
return $"{emoji}({Country}) {Ip}";
|
return $"{emoji}({Country}) {Ip}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
namespace ServiceLib.Models.Dto;
|
namespace ServiceLib.Models.Dto;
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class ProfileItemModel : ReactiveObject
|
public partial class ProfileItemModel : ReactiveObject
|
||||||
{
|
{
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
public string IndexId { get; set; }
|
public string IndexId { get; set; }
|
||||||
@@ -16,30 +16,30 @@ public class ProfileItemModel : ReactiveObject
|
|||||||
public int Sort { get; set; }
|
public int Sort { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public int Delay { get; set; }
|
public partial int Delay { get; set; }
|
||||||
|
|
||||||
public decimal Speed { get; set; }
|
public decimal Speed { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string DelayVal { get; set; }
|
public partial string DelayVal { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string SpeedVal { get; set; }
|
public partial string SpeedVal { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string IpInfo { get; set; }
|
public partial string IpInfo { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string TodayUp { get; set; }
|
public partial string TodayUp { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string TodayDown { get; set; }
|
public partial string TodayDown { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string TotalUp { get; set; }
|
public partial string TotalUp { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public string TotalDown { get; set; }
|
public partial string TotalDown { get; set; }
|
||||||
|
|
||||||
public string GetSummary()
|
public string GetSummary()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
namespace ServiceLib.Models.Dto;
|
namespace ServiceLib.Models.Dto;
|
||||||
|
|
||||||
public class RetResult
|
public class RetResult(bool success, string? msg, object? data)
|
||||||
{
|
{
|
||||||
public bool Success { get; set; }
|
public RetResult(bool success = false) : this(success, null, null)
|
||||||
public string? Msg { get; set; }
|
|
||||||
public object? Data { get; set; }
|
|
||||||
|
|
||||||
public RetResult(bool success = false)
|
|
||||||
{
|
{
|
||||||
Success = success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RetResult(bool success, string? msg)
|
public RetResult(bool success, string? msg) : this(success, msg, null)
|
||||||
{
|
{
|
||||||
Success = success;
|
|
||||||
Msg = msg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RetResult(bool success, string? msg, object? data)
|
public bool Success { get; set; } = success;
|
||||||
{
|
public string? Msg { get; set; } = msg;
|
||||||
Success = success;
|
public object? Data { get; set; } = data;
|
||||||
Msg = msg;
|
|
||||||
Data = data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,4 +40,8 @@ public class VmessQRCode
|
|||||||
public string fp { get; set; } = string.Empty;
|
public string fp { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string insecure { get; set; } = string.Empty;
|
public string insecure { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string vcn { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string pcs { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public class ProfileItem
|
|||||||
|
|
||||||
public bool IsValid()
|
public bool IsValid()
|
||||||
{
|
{
|
||||||
if (IsComplex())
|
if (IsComplex() || ConfigType == EConfigType.Outbound)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -146,6 +146,11 @@ public class ProfileItem
|
|||||||
TransportExtra = JsonUtils.Serialize(transportExtra, false);
|
TransportExtra = JsonUtils.Serialize(transportExtra, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool GetAllowInsecure()
|
||||||
|
{
|
||||||
|
return AllowInsecure == Global.StringTrue;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion function
|
#endregion function
|
||||||
|
|
||||||
[PrimaryKey]
|
[PrimaryKey]
|
||||||
@@ -191,6 +196,7 @@ public class ProfileItem
|
|||||||
public string Cert { get; set; }
|
public string Cert { get; set; }
|
||||||
public string CertSha { get; set; }
|
public string CertSha { get; set; }
|
||||||
public string EchConfigList { get; set; }
|
public string EchConfigList { get; set; }
|
||||||
|
public string VerifyPeerCertByName { get; set; }
|
||||||
public string Finalmask { get; set; }
|
public string Finalmask { get; set; }
|
||||||
|
|
||||||
public string ProtoExtra { get; set; }
|
public string ProtoExtra { get; set; }
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ public record ProtocolExtraItem
|
|||||||
public bool? Uot { get; init; }
|
public bool? Uot { get; init; }
|
||||||
public string? CongestionControl { get; init; }
|
public string? CongestionControl { get; init; }
|
||||||
|
|
||||||
|
// http outbound
|
||||||
|
public string? HttpHeaders { get; init; }
|
||||||
|
|
||||||
// vmess
|
// vmess
|
||||||
public string? AlterId { get; init; }
|
public string? AlterId { get; init; }
|
||||||
public string? VmessSecurity { get; init; }
|
public string? VmessSecurity { get; init; }
|
||||||
@@ -31,6 +34,12 @@ public record ProtocolExtraItem
|
|||||||
public int? DownMbps { get; init; }
|
public int? DownMbps { get; init; }
|
||||||
public string? Ports { get; init; }
|
public string? Ports { get; init; }
|
||||||
public string? HopInterval { get; init; }
|
public string? HopInterval { get; init; }
|
||||||
|
// realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]&stun=<stun-host>[:port]...
|
||||||
|
// example:
|
||||||
|
// realm://public@realm.hy2.io/57f9be7c-2810-4f5b-8cb9-260bc84d6c90?stun=example.stun:3478&stun=example2.stun:3478
|
||||||
|
public string? Hy2RealmUrl { get; init; }
|
||||||
|
public string? GeckoMinPacketSize { get; init; }
|
||||||
|
public string? GeckoMaxPacketSize { get; init; }
|
||||||
|
|
||||||
// naiveproxy
|
// naiveproxy
|
||||||
public int? InsecureConcurrency { get; init; }
|
public int? InsecureConcurrency { get; init; }
|
||||||
@@ -42,4 +51,7 @@ public record ProtocolExtraItem
|
|||||||
public string? SubChildItems { get; init; }
|
public string? SubChildItems { get; init; }
|
||||||
public string? Filter { get; init; }
|
public string? Filter { get; init; }
|
||||||
public EMultipleLoad? MultipleLoad { get; init; }
|
public EMultipleLoad? MultipleLoad { get; init; }
|
||||||
|
|
||||||
|
// custom outbound
|
||||||
|
public bool? IsSingboxEndpoint { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,6 @@ public class SubItem
|
|||||||
public int? PreSocksPort { get; set; }
|
public int? PreSocksPort { get; set; }
|
||||||
|
|
||||||
public string? Memo { get; set; }
|
public string? Memo { get; set; }
|
||||||
|
|
||||||
|
public ECoreType? CustomCoreType { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
406
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
406
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
@@ -178,7 +178,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Failed to import custom configuration Configuration 的本地化字符串。
|
/// 查找类似 Failed to import configuration 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string FailedImportedCustomServer {
|
public static string FailedImportedCustomServer {
|
||||||
get {
|
get {
|
||||||
@@ -222,15 +222,6 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查找类似 Please fill in the correct IPv4 address for SendThrough. 的本地化字符串。
|
|
||||||
/// </summary>
|
|
||||||
public static string FillCorrectSendThroughIPv4 {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("FillCorrectSendThroughIPv4", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Please enter the correct port format. 的本地化字符串。
|
/// 查找类似 Please enter the correct port format. 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -240,6 +231,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Invalid range format. Use 'from-to' (e.g., 50-100). 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string FillFragmentParameterError {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("FillFragmentParameterError", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Please enter the local listening port. 的本地化字符串。
|
/// 查找类似 Please enter the local listening port. 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -268,7 +268,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Please browse to import Configuration configuration 的本地化字符串。
|
/// 查找类似 Please browse to import configuration 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string FillServerAddressCustom {
|
public static string FillServerAddressCustom {
|
||||||
get {
|
get {
|
||||||
@@ -321,6 +321,24 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Please enter valid HTTP request headers JSON. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string InvalidHttpOutboundHeaders {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("InvalidHttpOutboundHeaders", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Invalid Realm URL. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string InvalidHy2RealmUrl {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("InvalidHy2RealmUrl", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Invalid address (URL) 的本地化字符串。
|
/// 查找类似 Invalid address (URL) 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -420,6 +438,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Custom config core 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string LvCustomCoreType {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LvCustomCoreType", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Custom icon 的本地化字符串。
|
/// 查找类似 Custom icon 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -717,6 +744,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Add a custom outbound 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string menuAddCustomOutboundServer {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("menuAddCustomOutboundServer", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Add a custom configuration 的本地化字符串。
|
/// 查找类似 Add a custom configuration 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1914,6 +1950,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 The current node uses an unencrypted connection, meaning your communications could be directly monitored by network intermediaries controlled by authoritarian governments. For security reasons, nodes of this type cannot connect via Xray-core versions 26.2.6 or higher. If this is a self-built node, please enable TLS or other secure encryption, or pin the certificate using pinSHA256. If this is an airport/provider node, please contact your service provider for a technical upgrade. If the provider refuses to c [字符串的其余部分被截断]"; 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string MsgAllowInsecureDeprecated {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MsgAllowInsecureDeprecated", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 {0} has a new version available: {1} 的本地化字符串。
|
/// 查找类似 {0} has a new version available: {1} 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1950,6 +1995,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Custom outbound {0} file not found: {1} 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string MsgCustomOutboundFileNotFound {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MsgCustomOutboundFileNotFound", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。
|
/// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2058,6 +2112,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string MsgInsecureConfiguration {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MsgInsecureConfiguration", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。
|
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2103,6 +2166,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Conflict between {0} and {1} 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string MsgOptionsConflict {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MsgOptionsConflict", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Resolved {0} successfully 的本地化字符串。
|
/// 查找类似 Resolved {0} successfully 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2220,6 +2292,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Invalid address in TUN route exclude list: {0} 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string MsgTunRouteExcludeInvalidAddress {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MsgTunRouteExcludeInvalidAddress", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Unpacking... 的本地化字符串。
|
/// 查找类似 Unpacking... 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2572,7 +2653,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Custom configuration Configuration imported successfully 的本地化字符串。
|
/// 查找类似 Custom configuration imported successfully 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string SuccessfullyImportedCustomServer {
|
public static string SuccessfullyImportedCustomServer {
|
||||||
get {
|
get {
|
||||||
@@ -2643,24 +2724,6 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查找类似 Allow insecure cert fetch (self-signed) 的本地化字符串。
|
|
||||||
/// </summary>
|
|
||||||
public static string TbAllowInsecureCertFetch {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("TbAllowInsecureCertFetch", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查找类似 Only for fetching self-signed certificates. This may expose you to MITM risks. 的本地化字符串。
|
|
||||||
/// </summary>
|
|
||||||
public static string TbAllowInsecureCertFetchTips {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("TbAllowInsecureCertFetchTips", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 ALPN 的本地化字符串。
|
/// 查找类似 ALPN 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2889,6 +2952,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Only single outbound/endpoint supported for xray/sing-box 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbCustomOutboundTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbCustomOutboundTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Direct Target Resolution Strategy 的本地化字符串。
|
/// 查找类似 Direct Target Resolution Strategy 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3006,6 +3078,51 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Enable Final Fragment 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbEnableFinalFragment {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbEnableFinalFragment", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Split the tail of packets into smaller fragments. This may affect throughput and latency. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbEnableFinalFragmentTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbEnableFinalFragmentTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Enable Happy Eyeballs 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbEnableHappyEyeballs {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbEnableHappyEyeballs", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Requires the UseIP Strategy. When enabled, it attempts IPv4 and IPv6 connections simultaneously and automatically selects the faster available path. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbEnableHappyEyeballsTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbEnableHappyEyeballsTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 DNS via Bridge 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbEnabletDnsViaProxy {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbEnabletDnsViaProxy", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Enable Tun 的本地化字符串。
|
/// 查找类似 Enable Tun 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3025,7 +3142,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Applies globally by default, with built-in FakeIP filtering (sing-box only). 的本地化字符串。
|
/// 查找类似 Applies globally by default, and built-in FakeIP filtering is only built into sing-box. 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string TbFakeIPTips {
|
public static string TbFakeIPTips {
|
||||||
get {
|
get {
|
||||||
@@ -3114,6 +3231,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Gecko Packet Size (min/max) 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbGeckoPacketSize {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbGeckoPacketSize", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Global Hotkey Settings 的本地化字符串。
|
/// 查找类似 Global Hotkey Settings 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3177,6 +3303,33 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 HTTP headers 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbHttpOutboundHeaders {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbHttpOutboundHeaders", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Realm URL 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbHy2RealmUrl {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbHy2RealmUrl", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Format: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port] 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbHy2RealmUrlTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbHy2RealmUrlTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 ICMP routing policy 的本地化字符串。
|
/// 查找类似 ICMP routing policy 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3231,6 +3384,24 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Ipv4 Address 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbIpv4Address {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbIpv4Address", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Ipv6 Address 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbIpv6Address {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbIpv6Address", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Most Stable 的本地化字符串。
|
/// 查找类似 Most Stable 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3258,6 +3429,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 If enabled, use sing-box TUN; otherwise, use xray TUN. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbLegacyProtectTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbLegacyProtectTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Address (IPv4, IPv6) 的本地化字符串。
|
/// 查找类似 Address (IPv4, IPv6) 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3420,6 +3600,24 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Proxy Dial Resolution Strategy 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbProxyDialResolveStrategy {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbProxyDialResolveStrategy", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Not recommended; may cause routing loops. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbProxyDialResolveStrategyTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbProxyDialResolveStrategyTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Public Key 的本地化字符串。
|
/// 查找类似 Public Key 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3519,6 +3717,24 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Root Certificate Provider 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbRootCertificateProvider {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbRootCertificateProvider", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Only applies to the v2rayN GUI's downloads and network requests. Does not affect the core's certificate validation. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbRootCertificateProviderTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbRootCertificateProviderTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Round Robin 的本地化字符串。
|
/// 查找类似 Round Robin 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3528,6 +3744,24 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Route Exclude Address 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbRouteExcludeAddress {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbRouteExcludeAddress", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Use commas (,) to separate. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbRouteExcludeAddressTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbRouteExcludeAddressTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
|
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3763,7 +3997,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode 的本地化字符串。
|
/// 查找类似 For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled. 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string TbSettingsBindInterfaceTip {
|
public static string TbSettingsBindInterfaceTip {
|
||||||
get {
|
get {
|
||||||
@@ -4050,6 +4284,96 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fallback Delay 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentFallbackDelay {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentFallbackDelay", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentFallbackDelayTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentFallbackDelayTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fragment Interval 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentInterval {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentInterval", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be <= second. Empty = default. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentIntervalTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentIntervalTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fragment Length 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentLength {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentLength", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fragment size range in bytes (e.g., 50-100). First value must be <= second. Empty = default. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentLengthTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentLengthTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Max Split 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentMaxSplit {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentMaxSplit", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentMaxSplitTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentMaxSplitTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Fragment Packets 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentPackets {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentPackets", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets) 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbSettingsFragmentPacketsTip {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbSettingsFragmentPacketsTip", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Geo files source (optional) 的本地化字符串。
|
/// 查找类似 Geo files source (optional) 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -4698,6 +5022,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Verify Peer Cert By Name 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TbVerifyPeerCertByName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TbVerifyPeerCertByName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
|
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -4761,6 +5094,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Custom HTTP outbound request headers as a JSON object with string or string array values. 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string TipHttpOutboundHeaders {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TipHttpOutboundHeaders", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 *Default value raw 的本地化字符串。
|
/// 查找类似 *Default value raw 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1104,6 +1104,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>فعال کردن فرگمنت</value>
|
<value>فعال کردن فرگمنت</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>پکتهای فرگمنت</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>پکتهای قابل فرگمنت: tlshello (TLS ClientHello) یا 1-1 تا 1-5 (N اول پکت TCP)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>طول فرگمنت</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>محدوده اندازه فرگمنت بایت (مثلا 50-100). اول ≤ دوم. خالی = پیشفرض.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>فاصله فرگمنت</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>تأخیر بین فرگمنتها میلیثانیه (مثلا 10-20). بازه 1-100. اول ≤ دوم. خالی = پیشفرض.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>حداکثر تقسیم</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>مکسیمم تعداد تقسیم (0 = نامحدود، 0-10000). فقط Xray-core. خالی = پیشفرض.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>تأخیر فولبک</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>تأخیر فولبک فرگمنت TLS. فقط sing-box. خالی = پیشفرض.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>فرمت محدوده نامعتبر. از 'from-to' استفاده کنید (مثلا 50-100).</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>فعال کردن کش فایل مجموعه قوانین برای sing-box</value>
|
<value>فعال کردن کش فایل مجموعه قوانین برای sing-box</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1462,7 +1495,7 @@
|
|||||||
<value>Select Profile</value>
|
<value>Select Profile</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFakeIPTips" xml:space="preserve">
|
<data name="TbFakeIPTips" xml:space="preserve">
|
||||||
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
|
<value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||||
<value>Please Add At Least One Configuration</value>
|
<value>Please Add At Least One Configuration</value>
|
||||||
@@ -1698,12 +1731,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>XHTTP Extra</value>
|
<value>XHTTP Extra</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>Allow insecure cert fetch (self-signed)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>Test Configurations UDP Delay</value>
|
<value>Test Configurations UDP Delay</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1716,9 +1743,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>Please fill in the correct IPv4 address for SendThrough.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>Bind Interface</value>
|
<value>Bind Interface</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1101,6 +1101,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>Activer le fragmentation (Fragment)</value>
|
<value>Activer le fragmentation (Fragment)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>Paquets fragmentés</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>Paquets à fragmenter : tlshello (TLS ClientHello) ou 1-1 à 1-5 (premiers N paquets TCP)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>Longueur fragment</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>Plage de taille fragment (ex: 50-100). Première valeur ≤ seconde. Vide = défaut.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>Intervalle fragment</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>Délai entre fragments en ms (ex: 10-20). Plage 1-100. Première valeur ≤ seconde. Vide = défaut.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>Max Split</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>Nb max de splits (0 = illimité, 0-10000). Xray-core seulement. Vide = défaut.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>Délai fallback</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>Délai fallback pour fragment TLS. sing-box seulement. Vide = défaut.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>Format de plage invalide. Utilisez 'from-to' (ex: 50-100).</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>Activer le fichier de cache de sing-box (fichiers règles)</value>
|
<value>Activer le fichier de cache de sing-box (fichiers règles)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1459,7 +1492,7 @@
|
|||||||
<value>Choisir une config.</value>
|
<value>Choisir une config.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFakeIPTips" xml:space="preserve">
|
<data name="TbFakeIPTips" xml:space="preserve">
|
||||||
<value>Actif globalement par défaut, avec filtre FakeIP intégré ; ne fonctionne que dans sing-box</value>
|
<value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||||
<value>Veuillez ajouter au moins une configuration</value>
|
<value>Veuillez ajouter au moins une configuration</value>
|
||||||
@@ -1675,7 +1708,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<value>Ajouter [NaïveProxy]</value>
|
<value>Ajouter [NaïveProxy]</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbInsecureConcurrency" xml:space="preserve">
|
<data name="TbInsecureConcurrency" xml:space="preserve">
|
||||||
<value>Insecure Concurrency</value>
|
<value>Concurrence non sûre</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbUsername" xml:space="preserve">
|
<data name="TbUsername" xml:space="preserve">
|
||||||
<value>Nom d’utilisateur</value>
|
<value>Nom d’utilisateur</value>
|
||||||
@@ -1695,21 +1728,12 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>XHTTP Extra</value>
|
<value>XHTTP Extra</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>Allow insecure cert fetch (self-signed)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||||
<value>Adresse sortante locale (SendThrough)</value>
|
<value>Adresse de sortie locale (SendThrough)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value>
|
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>Veuillez saisir l’adresse IPv4 correcte de SendThrough.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>Lier l'interface</value>
|
<value>Lier l'interface</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1717,30 +1741,30 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<value>Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN</value>
|
<value>Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>Test Configurations UDP Delay</value>
|
<value>Tester la latence UDP des profils</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
||||||
<value>UDP Test Url</value>
|
<value>URL de test UDP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuExport2InnerUri" xml:space="preserve">
|
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||||
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
<value>Exporter le lien interne v2rayN vers le presse-papiers</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
|
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
|
||||||
<value>{0} has a new version available: {1}</value>
|
<value>{0} a une nouvelle version disponible: {1}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuCheckOnly" xml:space="preserve">
|
<data name="menuCheckOnly" xml:space="preserve">
|
||||||
<value>Only Check</value>
|
<value>Uniquement vérifier</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>Not Support</value>
|
<value>Non supporté</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvTestIpInfo" xml:space="preserve">
|
<data name="LvTestIpInfo" xml:space="preserve">
|
||||||
<value>IP Info</value>
|
<value>Détails IP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuNewUpdate" xml:space="preserve">
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
<value>New Update</value>
|
<value>Nouvelle MAJ</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1104,6 +1104,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>Fragment engedélyezése</value>
|
<value>Fragment engedélyezése</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>Fragmentált csomagok</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>Fragmentálandó csomagok: tlshello (TLS ClientHello) vagy 1-1 – 1-5 (az első N TCP csomag)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>Fragment hossz</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>Fragment méret tartománya bájtokban (pl. 50-100). Az első érték ≤ második. Üres = alapértelmezett.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>Fragment intervallum</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>Késleltetés a fragmented csomagok között ms-ben (pl. 10-20). Tartomány 1-100. Az első érték ≤ második. Üres = alapértelmezett.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>Max darabolás</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>Maximális darabolások száma (0 = korlátlan, 0-10000). Csak Xray-core. Üres = alapértelmezett.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>Fallback késleltetés</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>TLS fragment fallback késleltetés. Csak sing-box. Üres = alapértelmezett.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>Érvénytelen tartomány formátum. Használja 'from-to' (pl. 50-100).</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok)</value>
|
<value>Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1462,7 +1495,7 @@
|
|||||||
<value>Select Profile</value>
|
<value>Select Profile</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFakeIPTips" xml:space="preserve">
|
<data name="TbFakeIPTips" xml:space="preserve">
|
||||||
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
|
<value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||||
<value>Please Add At Least One Configuration</value>
|
<value>Please Add At Least One Configuration</value>
|
||||||
@@ -1698,12 +1731,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>XHTTP Extra</value>
|
<value>XHTTP Extra</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>Allow insecure cert fetch (self-signed)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>Test Configurations UDP Delay</value>
|
<value>Test Configurations UDP Delay</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1716,9 +1743,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>Please fill in the correct IPv4 address for SendThrough.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>Bind Interface</value>
|
<value>Bind Interface</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
1797
v2rayN/ServiceLib/Resx/ResUI.id.resx
Normal file
1797
v2rayN/ServiceLib/Resx/ResUI.id.resx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -142,7 +142,7 @@
|
|||||||
<value>Failed to get the default configuration</value>
|
<value>Failed to get the default configuration</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FailedImportedCustomServer" xml:space="preserve">
|
<data name="FailedImportedCustomServer" xml:space="preserve">
|
||||||
<value>Failed to import custom configuration Configuration</value>
|
<value>Failed to import configuration</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FailedReadConfiguration" xml:space="preserve">
|
<data name="FailedReadConfiguration" xml:space="preserve">
|
||||||
<value>Failed to read configuration file</value>
|
<value>Failed to read configuration file</value>
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
<value>Configuration successful. {0}</value>
|
<value>Configuration successful. {0}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SuccessfullyImportedCustomServer" xml:space="preserve">
|
<data name="SuccessfullyImportedCustomServer" xml:space="preserve">
|
||||||
<value>Custom configuration Configuration imported successfully</value>
|
<value>Custom configuration imported successfully</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve">
|
<data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve">
|
||||||
<value>{0} Configurations have been imported from clipboard</value>
|
<value>{0} Configurations have been imported from clipboard</value>
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
<value>All</value>
|
<value>All</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillServerAddressCustom" xml:space="preserve">
|
<data name="FillServerAddressCustom" xml:space="preserve">
|
||||||
<value>Please browse to import Configuration configuration</value>
|
<value>Please browse to import configuration</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Speedtesting" xml:space="preserve">
|
<data name="Speedtesting" xml:space="preserve">
|
||||||
<value>Testing...</value>
|
<value>Testing...</value>
|
||||||
@@ -1044,6 +1044,12 @@
|
|||||||
<data name="TbHeaderType8" xml:space="preserve">
|
<data name="TbHeaderType8" xml:space="preserve">
|
||||||
<value>Congestion control</value>
|
<value>Congestion control</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbHttpOutboundHeaders" xml:space="preserve">
|
||||||
|
<value>HTTP headers</value>
|
||||||
|
</data>
|
||||||
|
<data name="TipHttpOutboundHeaders" xml:space="preserve">
|
||||||
|
<value>Custom HTTP outbound request headers as a JSON object with string or string array values.</value>
|
||||||
|
</data>
|
||||||
<data name="LvPrevProfile" xml:space="preserve">
|
<data name="LvPrevProfile" xml:space="preserve">
|
||||||
<value>Previous proxy remarks</value>
|
<value>Previous proxy remarks</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1104,6 +1110,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>Enable fragment</value>
|
<value>Enable fragment</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>Fragment Packets</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>Packets to fragment: tlshello (TLS ClientHello) or 1-1 to 1-5 (first N TCP packets)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>Fragment Length</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>Fragment size range in bytes (e.g., 50-100). First value must be <= second. Empty = default.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>Fragment Interval</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>Delay between fragments in ms (e.g., 10-20). Range 1-100. First value must be <= second. Empty = default.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>Max Split</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>Maximum number of fragments per packet (0-10000, 0 = unlimited). Xray only. Empty = default.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>Fallback Delay</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>Fallback delay when ACK detection unavailable (e.g., 500ms). sing-box only. Empty = default.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>Invalid range format. Use 'from-to' (e.g., 50-100).</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>Enable cache file for sing-box (ruleset files)</value>
|
<value>Enable cache file for sing-box (ruleset files)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1462,7 +1501,7 @@
|
|||||||
<value>Select Profile</value>
|
<value>Select Profile</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFakeIPTips" xml:space="preserve">
|
<data name="TbFakeIPTips" xml:space="preserve">
|
||||||
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
|
<value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||||
<value>Please Add At Least One Configuration</value>
|
<value>Please Add At Least One Configuration</value>
|
||||||
@@ -1581,6 +1620,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TbEchConfigList" xml:space="preserve">
|
<data name="TbEchConfigList" xml:space="preserve">
|
||||||
<value>EchConfigList</value>
|
<value>EchConfigList</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbVerifyPeerCertByName" xml:space="preserve">
|
||||||
|
<value>Verify Peer Cert By Name</value>
|
||||||
|
</data>
|
||||||
<data name="TbFullCertTips" xml:space="preserve">
|
<data name="TbFullCertTips" xml:space="preserve">
|
||||||
<value>Full certificate (chain), PEM format</value>
|
<value>Full certificate (chain), PEM format</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1698,12 +1740,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>XHTTP Extra</value>
|
<value>XHTTP Extra</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>Allow insecure cert fetch (self-signed)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>Test Configurations UDP Delay</value>
|
<value>Test Configurations UDP Delay</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1716,14 +1752,11 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>Please fill in the correct IPv4 address for SendThrough.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>Bind Interface</value>
|
<value>Bind Interface</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||||
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems or TUN mode</value>
|
<value>For multi-interface environments, enter the interface name for outbound connections. On Linux/macOS, it only works when TUN mode is enabled.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
@@ -1746,4 +1779,85 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="menuNewUpdate" xml:space="preserve">
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
<value>New Update</value>
|
<value>New Update</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
||||||
|
<value>The current node uses an unencrypted connection, meaning your communications could be directly monitored by network intermediaries controlled by authoritarian governments. For security reasons, nodes of this type cannot connect via Xray-core versions 26.2.6 or higher. If this is a self-built node, please enable TLS or other secure encryption, or pin the certificate using pinSHA256. If this is an airport/provider node, please contact your service provider for a technical upgrade. If the provider refuses to cooperate, it is recommended to switch to a service provider that prioritizes user security. For more information, please visit https://github.com/2dust/v2rayN/discussions/9460.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRouteExcludeAddress" xml:space="preserve">
|
||||||
|
<value>Route Exclude Address</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
|
||||||
|
<value>Use commas (,) to separate.</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
|
||||||
|
<value>Invalid address in TUN route exclude list: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgOptionsConflict" xml:space="preserve">
|
||||||
|
<value>Conflict between {0} and {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableFinalFragment" xml:space="preserve">
|
||||||
|
<value>Enable Final Fragment</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||||
|
<value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
|
||||||
|
<value>DNS via Bridge</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgInsecureConfiguration" xml:space="preserve">
|
||||||
|
<value>Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbHy2RealmUrl" xml:space="preserve">
|
||||||
|
<value>Realm URL</value>
|
||||||
|
</data>
|
||||||
|
<data name="InvalidHy2RealmUrl" xml:space="preserve">
|
||||||
|
<value>Invalid Realm URL.</value>
|
||||||
|
</data>
|
||||||
|
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
|
||||||
|
<value>Please enter valid HTTP request headers JSON.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbHy2RealmUrlTip" xml:space="preserve">
|
||||||
|
<value>Format: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbGeckoPacketSize" xml:space="preserve">
|
||||||
|
<value>Gecko Packet Size (min/max)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbLegacyProtectTip" xml:space="preserve">
|
||||||
|
<value>If enabled, use sing-box TUN; otherwise, use xray TUN.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRootCertificateProvider" xml:space="preserve">
|
||||||
|
<value>Root Certificate Provider</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRootCertificateProviderTip" xml:space="preserve">
|
||||||
|
<value>Only applies to the v2rayN GUI's downloads and network requests. Does not affect the core's certificate validation.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
|
||||||
|
<value>Proxy Dial Resolution Strategy</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbProxyDialResolveStrategyTip" xml:space="preserve">
|
||||||
|
<value>Not recommended; may cause routing loops.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableHappyEyeballs" xml:space="preserve">
|
||||||
|
<value>Enable Happy Eyeballs</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableHappyEyeballsTip" xml:space="preserve">
|
||||||
|
<value>Requires the UseIP Strategy. When enabled, it attempts IPv4 and IPv6 connections simultaneously and automatically selects the faster available path.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbIpv6Address" xml:space="preserve">
|
||||||
|
<value>Ipv6 Address</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbIpv4Address" xml:space="preserve">
|
||||||
|
<value>Ipv4 Address</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuAddCustomOutboundServer" xml:space="preserve">
|
||||||
|
<value>Add a custom outbound</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
|
||||||
|
<value>Custom outbound {0} file not found: {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbCustomOutboundTip" xml:space="preserve">
|
||||||
|
<value>Only single outbound/endpoint supported for xray/sing-box</value>
|
||||||
|
</data>
|
||||||
|
<data name="LvCustomCoreType" xml:space="preserve">
|
||||||
|
<value>Custom config core</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
<value>Ссылка успешно скопирована в буфер обмена</value>
|
<value>Ссылка успешно скопирована в буфер обмена</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CheckServerSettings" xml:space="preserve">
|
<data name="CheckServerSettings" xml:space="preserve">
|
||||||
<value>Сначала проверьте настройки сервера</value>
|
<value>Недопустимая конфигурация, проверьте или выберите заново</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ConfigurationFormatIncorrect" xml:space="preserve">
|
<data name="ConfigurationFormatIncorrect" xml:space="preserve">
|
||||||
<value>Недопустимый формат конфигурации</value>
|
<value>Недопустимый формат конфигурации</value>
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
<value>Некорректная конфигурация. Проверьте</value>
|
<value>Некорректная конфигурация. Проверьте</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="InitialConfiguration" xml:space="preserve">
|
<data name="InitialConfiguration" xml:space="preserve">
|
||||||
<value>Исходная конфигурация</value>
|
<value>Инициализация конфигурации</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IsLatestCore" xml:space="preserve">
|
<data name="IsLatestCore" xml:space="preserve">
|
||||||
<value>{0} {1} является последней версией</value>
|
<value>{0} {1} является последней версией</value>
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
<value>Операция не удалась, проверьте и повторите попытку</value>
|
<value>Операция не удалась, проверьте и повторите попытку</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseFillRemarks" xml:space="preserve">
|
<data name="PleaseFillRemarks" xml:space="preserve">
|
||||||
<value>Введите примечания</value>
|
<value>Введите псевдоним</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseSelectEncryption" xml:space="preserve">
|
<data name="PleaseSelectEncryption" xml:space="preserve">
|
||||||
<value>Выберите метод шифрования</value>
|
<value>Выберите метод шифрования</value>
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
<value>Сначала выберите сервер</value>
|
<value>Сначала выберите сервер</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RemoveDuplicateServerResult" xml:space="preserve">
|
<data name="RemoveDuplicateServerResult" xml:space="preserve">
|
||||||
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}.</value>
|
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RemoveServer" xml:space="preserve">
|
<data name="RemoveServer" xml:space="preserve">
|
||||||
<value>Вы уверены, что хотите удалить сервер?</value>
|
<value>Вы уверены, что хотите удалить сервер?</value>
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
<value>{0}: одно из обязательных полей</value>
|
<value>{0}: одно из обязательных полей</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvRemarks" xml:space="preserve">
|
<data name="LvRemarks" xml:space="preserve">
|
||||||
<value>Примечания</value>
|
<value>Псевдоним</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvUrl" xml:space="preserve">
|
<data name="LvUrl" xml:space="preserve">
|
||||||
<value>URL (необязательно)</value>
|
<value>URL (необязательно)</value>
|
||||||
@@ -334,13 +334,13 @@
|
|||||||
<value>Введите корректный пользовательский DNS</value>
|
<value>Введите корректный пользовательский DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportPathTip1" xml:space="preserve">
|
<data name="TransportPathTip1" xml:space="preserve">
|
||||||
<value>*Путь ws/http upgrade/xhttp</value>
|
<value>*Путь ws/httpupgrade/xhttp</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportPathTip2" xml:space="preserve">
|
<data name="TransportPathTip2" xml:space="preserve">
|
||||||
<value>*HTTP2-путь</value>
|
<value>*HTTP2-путь</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportPathTip3" xml:space="preserve">
|
<data name="TransportPathTip3" xml:space="preserve">
|
||||||
<value>*QUIC-ключ / KCP-seed</value>
|
<value>*Ключ шифрования QUIC</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportPathTip4" xml:space="preserve">
|
<data name="TransportPathTip4" xml:space="preserve">
|
||||||
<value>Имя сервиса gRPC</value>
|
<value>Имя сервиса gRPC</value>
|
||||||
@@ -349,13 +349,13 @@
|
|||||||
<value>*http-хосты, разделённые запятыми (,)</value>
|
<value>*http-хосты, разделённые запятыми (,)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportRequestHostTip2" xml:space="preserve">
|
<data name="TransportRequestHostTip2" xml:space="preserve">
|
||||||
<value>*Хост ws/http upgrade/xhttp</value>
|
<value>*Хост ws/httpupgrade/xhttp</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportRequestHostTip3" xml:space="preserve">
|
<data name="TransportRequestHostTip3" xml:space="preserve">
|
||||||
<value>*HTTP2-хосты, разделённые запятыми (,)</value>
|
<value>*HTTP2-хосты, разделённые запятыми (,)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportRequestHostTip4" xml:space="preserve">
|
<data name="TransportRequestHostTip4" xml:space="preserve">
|
||||||
<value>Безопасность *QUIC</value>
|
<value>*Метод шифрования QUIC</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportHeaderType1" xml:space="preserve">
|
<data name="TransportHeaderType1" xml:space="preserve">
|
||||||
<value>Тип raw-камуфляжа</value>
|
<value>Тип raw-камуфляжа</value>
|
||||||
@@ -400,7 +400,7 @@
|
|||||||
<value>Фильтр, нажмите Enter для выполнения</value>
|
<value>Фильтр, нажмите Enter для выполнения</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuCheckUpdate" xml:space="preserve">
|
<data name="menuCheckUpdate" xml:space="preserve">
|
||||||
<value>Проверить обновления</value>
|
<value>Обновить</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuClose" xml:space="preserve">
|
<data name="menuClose" xml:space="preserve">
|
||||||
<value>Закрыть</value>
|
<value>Закрыть</value>
|
||||||
@@ -415,13 +415,13 @@
|
|||||||
<value>Помощь</value>
|
<value>Помощь</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuOptionSetting" xml:space="preserve">
|
<data name="menuOptionSetting" xml:space="preserve">
|
||||||
<value>Настройки</value>
|
<value>Параметры</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuPromotion" xml:space="preserve">
|
<data name="menuPromotion" xml:space="preserve">
|
||||||
<value>Продвижение</value>
|
<value>Продвижение</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuReload" xml:space="preserve">
|
<data name="menuReload" xml:space="preserve">
|
||||||
<value>Перезагрузить</value>
|
<value>Перезапустить службу</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRoutingSetting" xml:space="preserve">
|
<data name="menuRoutingSetting" xml:space="preserve">
|
||||||
<value>Настройки маршрутизации</value>
|
<value>Настройки маршрутизации</value>
|
||||||
@@ -445,10 +445,10 @@
|
|||||||
<value>Настройки групп подписки</value>
|
<value>Настройки групп подписки</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuSubUpdate" xml:space="preserve">
|
<data name="menuSubUpdate" xml:space="preserve">
|
||||||
<value>Обновить подписку без прокси</value>
|
<value>Обновить все подписки без прокси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuSubUpdateViaProxy" xml:space="preserve">
|
<data name="menuSubUpdateViaProxy" xml:space="preserve">
|
||||||
<value>Обновить подписку с прокси</value>
|
<value>Обновить все подписки через прокси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuSystemproxy" xml:space="preserve">
|
<data name="menuSystemproxy" xml:space="preserve">
|
||||||
<value>Системный прокси</value>
|
<value>Системный прокси</value>
|
||||||
@@ -562,7 +562,7 @@
|
|||||||
<value>Сортировка</value>
|
<value>Сортировка</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvUserAgent" xml:space="preserve">
|
<data name="LvUserAgent" xml:space="preserve">
|
||||||
<value>User-Agent</value>
|
<value>User-Agent (необязательно)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbCancel" xml:space="preserve">
|
<data name="TbCancel" xml:space="preserve">
|
||||||
<value>Отмена</value>
|
<value>Отмена</value>
|
||||||
@@ -577,7 +577,7 @@
|
|||||||
<value>Адрес</value>
|
<value>Адрес</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecure" xml:space="preserve">
|
<data name="TbAllowInsecure" xml:space="preserve">
|
||||||
<value>Разрешить небезопасные</value>
|
<value>Пропустить проверку сертификата (allowInsecure)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAlpn" xml:space="preserve">
|
<data name="TbAlpn" xml:space="preserve">
|
||||||
<value>ALPN</value>
|
<value>ALPN</value>
|
||||||
@@ -616,10 +616,10 @@
|
|||||||
<value>TLS</value>
|
<value>TLS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TipNetwork" xml:space="preserve">
|
<data name="TipNetwork" xml:space="preserve">
|
||||||
<value>*По-умолчанию raw</value>
|
<value>*По умолчанию raw; при неверном выборе подключение будет невозможно</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbCoreType" xml:space="preserve">
|
<data name="TbCoreType" xml:space="preserve">
|
||||||
<value>Ядро</value>
|
<value>Тип ядра</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFlow5" xml:space="preserve">
|
<data name="TbFlow5" xml:space="preserve">
|
||||||
<value>Управление потоком (Flow)</value>
|
<value>Управление потоком (Flow)</value>
|
||||||
@@ -640,7 +640,7 @@
|
|||||||
<value>Шифрование</value>
|
<value>Шифрование</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSecurity4" xml:space="preserve">
|
<data name="TbSecurity4" xml:space="preserve">
|
||||||
<value>Пользователь (необязательно)</value>
|
<value>Имя пользователя (необязательно)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSecurity5" xml:space="preserve">
|
<data name="TbSecurity5" xml:space="preserve">
|
||||||
<value>Шифрование</value>
|
<value>Шифрование</value>
|
||||||
@@ -649,7 +649,7 @@
|
|||||||
<value>Порт SOCKS</value>
|
<value>Порт SOCKS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TipPreSocksPort" xml:space="preserve">
|
<data name="TipPreSocksPort" xml:space="preserve">
|
||||||
<value>* После установки этого значения служба SOCKS будет запущена с использованием Xray/sing-box(TUN) для обеспечения таких функций, как отображение скорости</value>
|
<value>*Порт SOCKS пользовательской конфигурации можно не указывать. Если он задан, Xray/sing-box (TUN) дополнительно запустит предварительную службу SOCKS для разделения трафика, отображения скорости и других функций</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbBrowse" xml:space="preserve">
|
<data name="TbBrowse" xml:space="preserve">
|
||||||
<value>Обзор</value>
|
<value>Обзор</value>
|
||||||
@@ -682,7 +682,7 @@
|
|||||||
<value>Настройки типа ядра</value>
|
<value>Настройки типа ядра</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsDefAllowInsecure" xml:space="preserve">
|
<data name="TbSettingsDefAllowInsecure" xml:space="preserve">
|
||||||
<value>Разрешить небезопасные</value>
|
<value>По умолчанию пропускать проверку сертификата (allowInsecure)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsDomainStrategy4Freedom" xml:space="preserve">
|
<data name="TbSettingsDomainStrategy4Freedom" xml:space="preserve">
|
||||||
<value>«Freedom»: стратегия обработки доменов исходящего трафика</value>
|
<value>«Freedom»: стратегия обработки доменов исходящего трафика</value>
|
||||||
@@ -703,7 +703,7 @@
|
|||||||
<value>Показывать скорость в реальном времени (требуется перезапуск)</value>
|
<value>Показывать скорость в реальном времени (требуется перезапуск)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsKeepOlderDedupl" xml:space="preserve">
|
<data name="TbSettingsKeepOlderDedupl" xml:space="preserve">
|
||||||
<value>Сохранить старые при удалении дублей</value>
|
<value>При удалении дубликатов сохранять элементы с меньшим порядковым номером</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsLogEnabled" xml:space="preserve">
|
<data name="TbSettingsLogEnabled" xml:space="preserve">
|
||||||
<value>Записывать логи</value>
|
<value>Записывать логи</value>
|
||||||
@@ -730,13 +730,13 @@
|
|||||||
<value>Смешанный порт</value>
|
<value>Смешанный порт</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsStartBoot" xml:space="preserve">
|
<data name="TbSettingsStartBoot" xml:space="preserve">
|
||||||
<value>Запускать при старте системы</value>
|
<value>Запускать при старте системы (может не сработать)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsStatistics" xml:space="preserve">
|
<data name="TbSettingsStatistics" xml:space="preserve">
|
||||||
<value>Включить статистику трафика (требуется перезапуск)</value>
|
<value>Включить статистику трафика (требуется перезапуск)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsSubConvert" xml:space="preserve">
|
<data name="TbSettingsSubConvert" xml:space="preserve">
|
||||||
<value>URL конвертации подписок</value>
|
<value>URL конвертации подписок (необязательно)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsSystemproxy" xml:space="preserve">
|
<data name="TbSettingsSystemproxy" xml:space="preserve">
|
||||||
<value>Настройки системного прокси</value>
|
<value>Настройки системного прокси</value>
|
||||||
@@ -787,13 +787,13 @@
|
|||||||
<value>Запущено от имени администратора</value>
|
<value>Запущено от имени администратора</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuMoveBottom" xml:space="preserve">
|
<data name="menuMoveBottom" xml:space="preserve">
|
||||||
<value>Спуститься вниз</value>
|
<value>Переместить в самый низ</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuMoveDown" xml:space="preserve">
|
<data name="menuMoveDown" xml:space="preserve">
|
||||||
<value>Вниз</value>
|
<value>Вниз</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuMoveTop" xml:space="preserve">
|
<data name="menuMoveTop" xml:space="preserve">
|
||||||
<value>Подняться наверх</value>
|
<value>Переместить в самый верх</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuMoveUp" xml:space="preserve">
|
<data name="menuMoveUp" xml:space="preserve">
|
||||||
<value>Вверх</value>
|
<value>Вверх</value>
|
||||||
@@ -805,7 +805,7 @@
|
|||||||
<value>Веб-сайт {0}</value>
|
<value>Веб-сайт {0}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRoutingAdvancedAdd" xml:space="preserve">
|
<data name="menuRoutingAdvancedAdd" xml:space="preserve">
|
||||||
<value>Добавить</value>
|
<value>Добавить набор правил</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRoutingAdvancedImportRules" xml:space="preserve">
|
<data name="menuRoutingAdvancedImportRules" xml:space="preserve">
|
||||||
<value>Импортировать правила</value>
|
<value>Импортировать правила</value>
|
||||||
@@ -841,7 +841,7 @@
|
|||||||
<value>Добавить правило</value>
|
<value>Добавить правило</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRuleExportSelected" xml:space="preserve">
|
<data name="menuRuleExportSelected" xml:space="preserve">
|
||||||
<value>Экспорт выделенных правил</value>
|
<value>Экспортировать выбранные правила в буфер обмена</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRuleList" xml:space="preserve">
|
<data name="menuRuleList" xml:space="preserve">
|
||||||
<value>Список правил</value>
|
<value>Список правил</value>
|
||||||
@@ -859,7 +859,7 @@
|
|||||||
<value>Документация RuleObject</value>
|
<value>Документация RuleObject</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDnsObjectDoc" xml:space="preserve">
|
<data name="TbDnsObjectDoc" xml:space="preserve">
|
||||||
<value>Поддерживаются DNS-объекты, нажмите для просмотра документации</value>
|
<value>Заполните DNS-объект (формат JSON), нажмите для просмотра документации</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SubUrlTips" xml:space="preserve">
|
<data name="SubUrlTips" xml:space="preserve">
|
||||||
<value>Для группы оставьте это поле пустым</value>
|
<value>Для группы оставьте это поле пустым</value>
|
||||||
@@ -871,13 +871,13 @@
|
|||||||
<value>Системные прокси изменены</value>
|
<value>Системные прокси изменены</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsRouteOnly" xml:space="preserve">
|
<data name="TbSettingsRouteOnly" xml:space="preserve">
|
||||||
<value>Только маршрут</value>
|
<value>Только для маршрутизации (routeOnly)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsNotProxyLocalAddress" xml:space="preserve">
|
<data name="TbSettingsNotProxyLocalAddress" xml:space="preserve">
|
||||||
<value>Не использовать прокси для локальных (интранет) адресов</value>
|
<value>Не использовать прокси для локальных (интранет) адресов</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuMixedTestServer" xml:space="preserve">
|
<data name="menuMixedTestServer" xml:space="preserve">
|
||||||
<value>Тест задержки и скорости всех серверов (Ctrl+E)</value>
|
<value>Многопоточный тест задержки и скорости (Ctrl+E)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvTestDelay" xml:space="preserve">
|
<data name="LvTestDelay" xml:space="preserve">
|
||||||
<value>Задержка (мс)</value>
|
<value>Задержка (мс)</value>
|
||||||
@@ -886,10 +886,10 @@
|
|||||||
<value>Скорость (МБ/с)</value>
|
<value>Скорость (МБ/с)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FailedToRunCore" xml:space="preserve">
|
<data name="FailedToRunCore" xml:space="preserve">
|
||||||
<value>Не удалось запустить ядро, посмотрите логи</value>
|
<value>Не удалось запустить ядро, см. сообщение</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvFilter" xml:space="preserve">
|
<data name="LvFilter" xml:space="preserve">
|
||||||
<value>Фильтр по примечаниям (регулярные выражения)</value>
|
<value>Фильтр по псевдониму (регулярные выражения)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDisplayLog" xml:space="preserve">
|
<data name="TbDisplayLog" xml:space="preserve">
|
||||||
<value>Отображать журнал</value>
|
<value>Отображать журнал</value>
|
||||||
@@ -937,7 +937,7 @@
|
|||||||
<value>Шрифт (требуется перезапуск)</value>
|
<value>Шрифт (требуется перезапуск)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsCurrentFontFamilyTip" xml:space="preserve">
|
<data name="TbSettingsCurrentFontFamilyTip" xml:space="preserve">
|
||||||
<value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и заново откройте окно настроек</value>
|
<value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и перезапустите приложение</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsSocksPortTip" xml:space="preserve">
|
<data name="TbSettingsSocksPortTip" xml:space="preserve">
|
||||||
<value>PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5;</value>
|
<value>PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5;</value>
|
||||||
@@ -1009,7 +1009,7 @@
|
|||||||
<value>Пользовательский DNS для sing-box</value>
|
<value>Пользовательский DNS для sing-box</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
|
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
|
||||||
<value>Заполните структуру DNS, нажмите, чтобы открыть документ</value>
|
<value>Заполните структуру DNS (формат JSON), нажмите для просмотра документации</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingDnsImportDefConfig" xml:space="preserve">
|
<data name="TbSettingDnsImportDefConfig" xml:space="preserve">
|
||||||
<value>Нажмите, чтобы импортировать конфигурацию DNS по умолчанию</value>
|
<value>Нажмите, чтобы импортировать конфигурацию DNS по умолчанию</value>
|
||||||
@@ -1033,7 +1033,7 @@
|
|||||||
<value>Добавить сервер [Hysteria2]</value>
|
<value>Добавить сервер [Hysteria2]</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
|
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
|
||||||
<value>Максимальная пропускная способность Hysteria (загрузка/отдача)</value>
|
<value>Максимальная пропускная способность Hysteria (отдача/загрузка)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
|
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
|
||||||
<value>Использовать системный файл hosts</value>
|
<value>Использовать системный файл hosts</value>
|
||||||
@@ -1044,14 +1044,20 @@
|
|||||||
<data name="TbHeaderType8" xml:space="preserve">
|
<data name="TbHeaderType8" xml:space="preserve">
|
||||||
<value>Управление перегрузками</value>
|
<value>Управление перегрузками</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbHttpOutboundHeaders" xml:space="preserve">
|
||||||
|
<value>HTTP-заголовки</value>
|
||||||
|
</data>
|
||||||
|
<data name="TipHttpOutboundHeaders" xml:space="preserve">
|
||||||
|
<value>Пользовательские заголовки исходящего HTTP-запроса. Введите JSON-объект, значения которого — строка или массив строк</value>
|
||||||
|
</data>
|
||||||
<data name="LvPrevProfile" xml:space="preserve">
|
<data name="LvPrevProfile" xml:space="preserve">
|
||||||
<value>Примечания к предыдущему прокси</value>
|
<value>Псевдоним предыдущего прокси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvNextProfile" xml:space="preserve">
|
<data name="LvNextProfile" xml:space="preserve">
|
||||||
<value>Примечания к следующему прокси</value>
|
<value>Псевдоним следующего прокси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvPrevProfileTip" xml:space="preserve">
|
<data name="LvPrevProfileTip" xml:space="preserve">
|
||||||
<value>Убедитесь, что примечание существует и является уникальным</value>
|
<value>Убедитесь, что псевдоним существует и является уникальным</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
|
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
|
||||||
<value>Автоматическая маршрутизация</value>
|
<value>Автоматическая маршрутизация</value>
|
||||||
@@ -1104,6 +1110,39 @@
|
|||||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||||
<value>Включить фрагментацию (Fragment)</value>
|
<value>Включить фрагментацию (Fragment)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||||
|
<value>Пакеты для фрагментации</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||||
|
<value>Пакеты для фрагментации: tlshello (TLS ClientHello) или 1-1 — 1-5 (первые N TCP-пакетов)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||||
|
<value>Длина фрагмента</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||||
|
<value>Диапазон размера фрагмента в байтах (например, 50-100). Первое значение не больше второго. Пусто = по умолчанию</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||||
|
<value>Интервал фрагментации</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||||
|
<value>Задержка между фрагментами в мс (например, 10-20). Диапазон 1-100. Первое значение не больше второго. Пусто = по умолчанию</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||||
|
<value>Макс. разделение</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||||
|
<value>Максимальное число разделений (0 = без лимита, 0-10000). Только для Xray-core. Пусто = по умолчанию</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>Резервная задержка</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>Резервная задержка для TLS-фрагмента. Только для sing-box. Пусто = по умолчанию</value>
|
||||||
|
</data>
|
||||||
|
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||||
|
<value>Неверный формат диапазона. Используйте 'from-to' (например, 50-100)</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||||
<value>Включить файл кэша для sing-box (файлы наборов правил)</value>
|
<value>Включить файл кэша для sing-box (файлы наборов правил)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1177,7 +1216,7 @@
|
|||||||
<value>Глобальный режим</value>
|
<value>Глобальный режим</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuModeNothing" xml:space="preserve">
|
<data name="menuModeNothing" xml:space="preserve">
|
||||||
<value>Не менять</value>
|
<value>Как в исходной конфигурации</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuModeRule" xml:space="preserve">
|
<data name="menuModeRule" xml:space="preserve">
|
||||||
<value>Правила</value>
|
<value>Правила</value>
|
||||||
@@ -1210,7 +1249,7 @@
|
|||||||
<value>Экспорт ссылок в формате Base64 в буфер обмена</value>
|
<value>Экспорт ссылок в формате Base64 в буфер обмена</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuExport2ClientConfigClipboard" xml:space="preserve">
|
<data name="menuExport2ClientConfigClipboard" xml:space="preserve">
|
||||||
<value>Экспортировать полную конфигурацию в буфер обмена</value>
|
<value>Экспортировать выбранную полную конфигурацию в буфер обмена</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuShowOrHideMainWindow" xml:space="preserve">
|
<data name="menuShowOrHideMainWindow" xml:space="preserve">
|
||||||
<value>Показать или скрыть главное окно</value>
|
<value>Показать или скрыть главное окно</value>
|
||||||
@@ -1300,7 +1339,7 @@
|
|||||||
<value>Не используйте небезопасный адрес подписки по протоколу HTTP</value>
|
<value>Не используйте небезопасный адрес подписки по протоколу HTTP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsCurrentFontFamilyLinuxTip" xml:space="preserve">
|
<data name="TbSettingsCurrentFontFamilyLinuxTip" xml:space="preserve">
|
||||||
<value>Установите шрифт в систему, выберите или введите имя шрифта, перезапустите настройки</value>
|
<value>Установите шрифт в систему, выберите или введите имя шрифта и перезапустите приложение</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuExitTips" xml:space="preserve">
|
<data name="menuExitTips" xml:space="preserve">
|
||||||
<value>Вы уверены, что хотите выйти?</value>
|
<value>Вы уверены, что хотите выйти?</value>
|
||||||
@@ -1312,19 +1351,19 @@
|
|||||||
<value>Пароль sudo системы</value>
|
<value>Пароль sudo системы</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
|
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
|
||||||
<value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска.</value>
|
<value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportHeaderType5" xml:space="preserve">
|
<data name="TransportHeaderType5" xml:space="preserve">
|
||||||
<value>XHTTP-режим</value>
|
<value>XHTTP-режим</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TransportExtraTip" xml:space="preserve">
|
<data name="TransportExtraTip" xml:space="preserve">
|
||||||
<value>Сырой JSON, формат: { XHTTP Object }</value>
|
<value>Сырой JSON, формат: { XHTTPObject }</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsHide2TrayWhenClose" xml:space="preserve">
|
<data name="TbSettingsHide2TrayWhenClose" xml:space="preserve">
|
||||||
<value>Сворачивать в трей при закрытии окна</value>
|
<value>Сворачивать в трей при закрытии окна</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsMixedConcurrencyCount" xml:space="preserve">
|
<data name="TbSettingsMixedConcurrencyCount" xml:space="preserve">
|
||||||
<value>Количество одновременно выполняемых тестов при многоэтапном тестировании</value>
|
<value>Количество параллельных задач при многопоточном тестировании</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsExceptionTip2" xml:space="preserve">
|
<data name="TbSettingsExceptionTip2" xml:space="preserve">
|
||||||
<value>Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,)</value>
|
<value>Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,)</value>
|
||||||
@@ -1336,7 +1375,7 @@
|
|||||||
<value>Включить второй смешанный порт</value>
|
<value>Включить второй смешанный порт</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRoutingInboundTagTips" xml:space="preserve">
|
<data name="TbRoutingInboundTagTips" xml:space="preserve">
|
||||||
<value>socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value>
|
<value>tun: входящий TUN, socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsTheme" xml:space="preserve">
|
<data name="TbSettingsTheme" xml:space="preserve">
|
||||||
<value>Тема</value>
|
<value>Тема</value>
|
||||||
@@ -1357,7 +1396,7 @@
|
|||||||
<value>Удалено {0} недействительных</value>
|
<value>Удалено {0} недействительных</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPorts7" xml:space="preserve">
|
<data name="TbPorts7" xml:space="preserve">
|
||||||
<value>Диапазон портов сервера</value>
|
<value>Диапазон портов для переключения (Port Hopping)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPorts7Tips" xml:space="preserve">
|
<data name="TbPorts7Tips" xml:space="preserve">
|
||||||
<value>Заменит указанный порт, перечисляйте через запятую (,)</value>
|
<value>Заменит указанный порт, перечисляйте через запятую (,)</value>
|
||||||
@@ -1369,10 +1408,10 @@
|
|||||||
<value>URL для тестирования текущего соединения</value>
|
<value>URL для тестирования текущего соединения</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRuleOutboundTagTip" xml:space="preserve">
|
<data name="TbRuleOutboundTagTip" xml:space="preserve">
|
||||||
<value>Можно указать название (Remarks) из конфигурации, убедитесь, что оно существует и уникально</value>
|
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
|
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
|
||||||
<value>Неверный пароль, попробуйте ещё раз.</value>
|
<value>Неверный пароль, попробуйте ещё раз</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbMldsa65Verify" xml:space="preserve">
|
<data name="TbMldsa65Verify" xml:space="preserve">
|
||||||
<value>Mldsa65Verify</value>
|
<value>Mldsa65Verify</value>
|
||||||
@@ -1384,7 +1423,7 @@
|
|||||||
<value>Удалённый DNS</value>
|
<value>Удалённый DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDomesticDNS" xml:space="preserve">
|
<data name="TbDomesticDNS" xml:space="preserve">
|
||||||
<value>Внутренний DNS</value>
|
<value>DNS для прямых подключений</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDirectResolveStrategy" xml:space="preserve">
|
<data name="TbDirectResolveStrategy" xml:space="preserve">
|
||||||
<value>Стратегия разрешения прямых соединений</value>
|
<value>Стратегия разрешения прямых соединений</value>
|
||||||
@@ -1453,7 +1492,7 @@
|
|||||||
<value>Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ</value>
|
<value>Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||||
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value>
|
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||||
<value>Начинается разбор и обработка содержимого подписки</value>
|
<value>Начинается разбор и обработка содержимого подписки</value>
|
||||||
@@ -1462,7 +1501,7 @@
|
|||||||
<value>Выбрать профиль</value>
|
<value>Выбрать профиль</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFakeIPTips" xml:space="preserve">
|
<data name="TbFakeIPTips" xml:space="preserve">
|
||||||
<value>По умолчанию применяется глобально, со встроенной фильтрацией FakeIP (только sing-box).</value>
|
<value>Применяется глобально по умолчанию; встроенная фильтрация FakeIP есть только в sing-box</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||||
<value>Добавьте хотя бы одну конфигурацию</value>
|
<value>Добавьте хотя бы одну конфигурацию</value>
|
||||||
@@ -1489,13 +1528,13 @@
|
|||||||
<value>Тип группы политик</value>
|
<value>Тип группы политик</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuAddPolicyGroupServer" xml:space="preserve">
|
<data name="menuAddPolicyGroupServer" xml:space="preserve">
|
||||||
<value>Добавить группу политик </value>
|
<value>Добавить группу политик</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuAddProxyChainServer" xml:space="preserve">
|
<data name="menuAddProxyChainServer" xml:space="preserve">
|
||||||
<value>Добавить цепочку прокси</value>
|
<value>Добавить цепочку прокси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuAddChildServer" xml:space="preserve">
|
<data name="menuAddChildServer" xml:space="preserve">
|
||||||
<value>Добавить дочернюю конфигурацию </value>
|
<value>Добавить дочернюю конфигурацию</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuRemoveChildServer" xml:space="preserve">
|
<data name="menuRemoveChildServer" xml:space="preserve">
|
||||||
<value>Удалить дочернюю конфигурацию </value>
|
<value>Удалить дочернюю конфигурацию </value>
|
||||||
@@ -1534,7 +1573,7 @@
|
|||||||
<value>Bootstrap DNS</value>
|
<value>Bootstrap DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbBootstrapDNSTips" xml:space="preserve">
|
<data name="TbBootstrapDNSTips" xml:space="preserve">
|
||||||
<value>Разрешает домены DNS-серверов, требуется IP-адрес</value>
|
<value>Для разрешения доменных имён DNS-серверов необходимо указать IP-адрес</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuFastRealPing" xml:space="preserve">
|
<data name="menuFastRealPing" xml:space="preserve">
|
||||||
<value>Тест реальной задержки</value>
|
<value>Тест реальной задержки</value>
|
||||||
@@ -1547,9 +1586,9 @@
|
|||||||
</data>
|
</data>
|
||||||
<data name="TbCertPinningTips" xml:space="preserve">
|
<data name="TbCertPinningTips" xml:space="preserve">
|
||||||
<value>Привязанный сертификат (заполните любое из полей)
|
<value>Привязанный сертификат (заполните любое из полей)
|
||||||
При указании сертификат будет привязан, а «Разрешить небезопасные» отключится.
|
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
|
||||||
|
|
||||||
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
|
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbFetchCert" xml:space="preserve">
|
<data name="TbFetchCert" xml:space="preserve">
|
||||||
<value>Получить сертификат</value>
|
<value>Получить сертификат</value>
|
||||||
@@ -1581,6 +1620,9 @@
|
|||||||
<data name="TbEchConfigList" xml:space="preserve">
|
<data name="TbEchConfigList" xml:space="preserve">
|
||||||
<value>EchConfigList</value>
|
<value>EchConfigList</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbVerifyPeerCertByName" xml:space="preserve">
|
||||||
|
<value>Проверять сертификат узла по имени</value>
|
||||||
|
</data>
|
||||||
<data name="TbFullCertTips" xml:space="preserve">
|
<data name="TbFullCertTips" xml:space="preserve">
|
||||||
<value>Полный сертификат (цепочка) в формате PEM</value>
|
<value>Полный сертификат (цепочка) в формате PEM</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1600,10 +1642,10 @@
|
|||||||
<value>По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS</value>
|
<value>По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbDirectResolveStrategyTips" xml:space="preserve">
|
<data name="TbDirectResolveStrategyTips" xml:space="preserve">
|
||||||
<value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль.</value>
|
<value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbRemoteResolveStrategyTips" xml:space="preserve">
|
<data name="TbRemoteResolveStrategyTips" xml:space="preserve">
|
||||||
<value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль.</value>
|
<value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbHopInt7" xml:space="preserve">
|
<data name="TbHopInt7" xml:space="preserve">
|
||||||
<value>Интервал смены портов (Port Hopping)</value>
|
<value>Интервал смены портов (Port Hopping)</value>
|
||||||
@@ -1618,37 +1660,37 @@
|
|||||||
<value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value>
|
<value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
|
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
|
||||||
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел.</value>
|
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupCycleDependency" xml:space="preserve">
|
<data name="MsgGroupCycleDependency" xml:space="preserve">
|
||||||
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен.</value>
|
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
|
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
|
||||||
<value>Группа {0}: предупреждение дочернего узла {1}: {2}</value>
|
<value>Группа {0}: предупреждение дочернего узла {1}: {2}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupChildNodeError" xml:space="preserve">
|
<data name="MsgGroupChildNodeError" xml:space="preserve">
|
||||||
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен.</value>
|
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
|
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
|
||||||
<value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value>
|
<value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
|
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
|
||||||
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен.</value>
|
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
|
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
|
||||||
<value>У группы {0} нет допустимых дочерних узлов.</value>
|
<value>У группы {0} нет допустимых дочерних узлов</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
|
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
|
||||||
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел.</value>
|
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
|
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
|
||||||
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел.</value>
|
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
|
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
|
||||||
<value>Предыдущий прокси подписки {0} не найден. Пропущено.</value>
|
<value>Предыдущий прокси подписки {0} не найден. Пропущено</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
|
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
|
||||||
<value>Следующий прокси подписки {0} не найден. Пропущено.</value>
|
<value>Следующий прокси подписки {0} не найден. Пропущено</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuGenGroupServer" xml:space="preserve">
|
<data name="menuGenGroupServer" xml:space="preserve">
|
||||||
<value>Сгенерировать группу политик</value>
|
<value>Сгенерировать группу политик</value>
|
||||||
@@ -1698,12 +1740,6 @@
|
|||||||
<data name="TransportExtra" xml:space="preserve">
|
<data name="TransportExtra" xml:space="preserve">
|
||||||
<value>Дополнительные параметры XHTTP (Extra)</value>
|
<value>Дополнительные параметры XHTTP (Extra)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
|
||||||
<value>Разрешить небезопасную загрузку сертификата (самоподписанного)</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
|
||||||
<value>Только для загрузки самоподписанных сертификатов. Это может подвергнуть вас риску атаки «человек посередине» (MITM).</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>Тест UDP-задержки конфигураций</value>
|
<value>Тест UDP-задержки конфигураций</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1716,14 +1752,11 @@
|
|||||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||||
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
|
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
|
||||||
<value>Укажите корректный IPv4-адрес для SendThrough.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>Привязать интерфейс</value>
|
<value>Привязать интерфейс</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||||
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN</value>
|
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>Общий ключ (PSK)</value>
|
<value>Общий ключ (PSK)</value>
|
||||||
@@ -1732,18 +1765,99 @@
|
|||||||
<value>Экспорт внутренней ссылки v2rayN в буфер обмена</value>
|
<value>Экспорт внутренней ссылки v2rayN в буфер обмена</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
|
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
|
||||||
<value>{0} has a new version available: {1}</value>
|
<value>Доступна новая версия {0}: {1}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuCheckOnly" xml:space="preserve">
|
<data name="menuCheckOnly" xml:space="preserve">
|
||||||
<value>Only Check</value>
|
<value>Проверить</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>Not Support</value>
|
<value>Не поддерживается</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvTestIpInfo" xml:space="preserve">
|
<data name="LvTestIpInfo" xml:space="preserve">
|
||||||
<value>IP Info</value>
|
<value>Информация об IP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuNewUpdate" xml:space="preserve">
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
<value>New Update</value>
|
<value>Доступно обновление</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
||||||
|
<value>Предупреждение: 1 августа 2026 г. Xray отключит пропуск проверки сертификата (allowInsecure). Как можно скорее перейдите на привязанный отпечаток сертификата (pinnedPeerCertSha256). После этой даты allowInsecure использовать будет нельзя</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRouteExcludeAddress" xml:space="preserve">
|
||||||
|
<value>Адреса, исключаемые из маршрутизации</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
|
||||||
|
<value>Разделяйте запятыми (,)</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
|
||||||
|
<value>Недопустимый адрес в списке исключений маршрутизации TUN: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgOptionsConflict" xml:space="preserve">
|
||||||
|
<value>Конфликт между {0} и {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableFinalFragment" xml:space="preserve">
|
||||||
|
<value>Включить финальную фрагментацию (Final Fragment)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||||
|
<value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
|
||||||
|
<value>DNS через Bridge</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgInsecureConfiguration" xml:space="preserve">
|
||||||
|
<value>Обнаружена небезопасная конфигурация: AllowInsecure включён, но сертификат не предоставлен. Это может привести к атаке «человек посередине» (MITM)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbHy2RealmUrl" xml:space="preserve">
|
||||||
|
<value>Realm URL</value>
|
||||||
|
</data>
|
||||||
|
<data name="InvalidHy2RealmUrl" xml:space="preserve">
|
||||||
|
<value>Некорректный Realm URL</value>
|
||||||
|
</data>
|
||||||
|
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
|
||||||
|
<value>Введите корректный JSON заголовков HTTP-запроса</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbHy2RealmUrlTip" xml:space="preserve">
|
||||||
|
<value>Формат: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbGeckoPacketSize" xml:space="preserve">
|
||||||
|
<value>Размер пакета Gecko (мин/макс)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbLegacyProtectTip" xml:space="preserve">
|
||||||
|
<value>Если включено, используется sing-box TUN; иначе — xray TUN</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRootCertificateProvider" xml:space="preserve">
|
||||||
|
<value>Поставщик корневых сертификатов</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRootCertificateProviderTip" xml:space="preserve">
|
||||||
|
<value>Применяется только к загрузкам и сетевым запросам графического интерфейса v2rayN. Не влияет на проверку сертификатов ядром</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
|
||||||
|
<value>Стратегия разрешения DNS при подключении к прокси</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbProxyDialResolveStrategyTip" xml:space="preserve">
|
||||||
|
<value>Не рекомендуется; может вызвать петли маршрутизации</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableHappyEyeballs" xml:space="preserve">
|
||||||
|
<value>Включить Happy Eyeballs</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbEnableHappyEyeballsTip" xml:space="preserve">
|
||||||
|
<value>Требуется стратегия UseIP. При включении одновременно устанавливаются соединения по IPv4 и IPv6 и автоматически выбирается более быстрый доступный путь</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbIpv6Address" xml:space="preserve">
|
||||||
|
<value>Адрес IPv6</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbIpv4Address" xml:space="preserve">
|
||||||
|
<value>Адрес IPv4</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuAddCustomOutboundServer" xml:space="preserve">
|
||||||
|
<value>Добавить пользовательский outbound</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
|
||||||
|
<value>Файл пользовательского outbound {0} не найден: {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbCustomOutboundTip" xml:space="preserve">
|
||||||
|
<value>Для xray/sing-box поддерживается только один outbound/endpoint</value>
|
||||||
|
</data>
|
||||||
|
<data name="LvCustomCoreType" xml:space="preserve">
|
||||||
|
<value>Ядро пользовательской конфигурации</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user