mirror of
https://github.com/2dust/v2rayN.git
synced 2026-08-12 02:02:04 +03:00
Compare commits
121 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 |
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.3
|
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 "$@"
|
||||||
|
|||||||
@@ -54,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.7</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.17" />
|
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
|
||||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.17" />
|
<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="IPNetwork2" Version="4.3.0" />
|
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
||||||
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.13" />
|
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" />
|
||||||
<PackageVersion Include="CliWrap" Version="3.10.1" />
|
<PackageVersion Include="CliWrap" Version="3.10.4" />
|
||||||
<PackageVersion Include="Downloader" Version="5.7.0" />
|
<PackageVersion Include="Downloader" Version="5.9.5" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
||||||
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
||||||
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
||||||
<PackageVersion Include="ReactiveUI" Version="23.2.28" />
|
<PackageVersion Include="ReactiveUI" Version="24.1.0" />
|
||||||
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
|
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
|
||||||
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" />
|
<PackageVersion Include="ReactiveUI.WPF" Version="24.1.0" />
|
||||||
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
|
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
|
||||||
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
|
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
|
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
|
||||||
<PackageVersion Include="NLog" Version="6.1.3" />
|
<PackageVersion Include="NLog" Version="6.1.4" />
|
||||||
<PackageVersion Include="sqlite-net-e" Version="1.11.0" />
|
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
|
||||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.2" />
|
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
|
||||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
<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="18.0.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" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Submodule v2rayN/GlobalHotKeys updated: 569a95bb0f...162d401dfe
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -557,4 +559,107 @@ public class CoreConfigSingboxServiceTests
|
|||||||
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
|
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
|
||||||
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
|
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,7 +567,7 @@ 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]
|
[Fact]
|
||||||
@@ -559,4 +592,43 @@ public class CoreConfigV2rayServiceTests
|
|||||||
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
|
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
|
||||||
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
|
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -324,6 +324,29 @@ public class Utils
|
|||||||
.ReplaceLineBreaks(",");
|
.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
|
||||||
{
|
{
|
||||||
return Enum.GetValues(typeof(TEnum))
|
return Enum.GetValues(typeof(TEnum))
|
||||||
@@ -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";
|
||||||
@@ -92,6 +95,7 @@ public class Global
|
|||||||
public const string LinuxBash = "/bin/bash";
|
public const string LinuxBash = "/bin/bash";
|
||||||
public const string StringTrue = "true";
|
public const string StringTrue = "true";
|
||||||
public const string StringFalse = "false";
|
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";
|
||||||
@@ -100,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 =
|
||||||
@@ -210,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://";
|
||||||
@@ -406,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",
|
||||||
@@ -476,7 +492,8 @@ public class Global
|
|||||||
"fa",
|
"fa",
|
||||||
"fr",
|
"fr",
|
||||||
"ru",
|
"ru",
|
||||||
"hu"
|
"hu",
|
||||||
|
"id"
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly List<string> Alpns =
|
public static readonly List<string> Alpns =
|
||||||
@@ -536,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 =
|
||||||
@@ -666,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,
|
||||||
@@ -708,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;
|
||||||
}
|
}
|
||||||
@@ -91,28 +97,51 @@ public class CoreConfigContextBuilder
|
|||||||
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
|
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
|
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)
|
foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IPNetwork2.Parse(addr);
|
IPNetwork2.Parse(addr);
|
||||||
|
routeExcludeAddressList.Add(addr);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
validatorResult.Errors.Add(string.Format(ResUI.MsgTunRouteExcludeInvalidAddress, addr));
|
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)
|
||||||
{
|
{
|
||||||
@@ -132,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)
|
||||||
{
|
{
|
||||||
@@ -153,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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,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,
|
||||||
@@ -202,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();
|
||||||
|
|
||||||
@@ -248,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,
|
||||||
@@ -266,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)
|
||||||
{
|
{
|
||||||
@@ -275,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())
|
||||||
{
|
{
|
||||||
@@ -293,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)
|
||||||
{
|
{
|
||||||
@@ -347,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)
|
||||||
@@ -364,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,
|
||||||
@@ -394,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 =>
|
||||||
@@ -430,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,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
|
||||||
@@ -43,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();
|
||||||
@@ -75,25 +84,25 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,10 +124,11 @@ public class NodeValidator
|
|||||||
// 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
|
// Check for deprecated allowInsecure property when TLS is enabled
|
||||||
@@ -131,19 +141,19 @@ public class NodeValidator
|
|||||||
|
|
||||||
if ((coreType == ECoreType.Xray
|
if ((coreType == ECoreType.Xray
|
||||||
&& item.GetAllowInsecure()
|
&& item.GetAllowInsecure()
|
||||||
&& item.Cert.IsNullOrEmpty()
|
&& !isCertProvided
|
||||||
&& item.CertSha.IsNullOrEmpty())
|
&& item.CertSha.IsNullOrEmpty())
|
||||||
|| (coreType == ECoreType.sing_box
|
|| (coreType == ECoreType.sing_box
|
||||||
&& item.GetAllowInsecure()
|
&& item.GetAllowInsecure()
|
||||||
&& item.Cert.IsNullOrEmpty()))
|
&& !isCertProvided))
|
||||||
{
|
{
|
||||||
v.Warning("Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks.");
|
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();
|
||||||
@@ -151,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,9 +92,13 @@ 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();
|
||||||
@@ -111,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)
|
||||||
@@ -164,9 +170,24 @@ public static class ConfigHandler
|
|||||||
config.Fragment4RayItem ??= new()
|
config.Fragment4RayItem ??= new()
|
||||||
{
|
{
|
||||||
Packets = "tlshello",
|
Packets = "tlshello",
|
||||||
Length = "50-100",
|
|
||||||
Interval = "10-20"
|
|
||||||
};
|
};
|
||||||
|
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 ??= [];
|
config.GlobalHotkeys ??= [];
|
||||||
|
|
||||||
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
|
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
|
||||||
@@ -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;
|
||||||
@@ -1419,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() ?? [];
|
||||||
@@ -1450,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
|
||||||
@@ -1468,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,
|
||||||
@@ -1615,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 is { 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1777,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)
|
||||||
@@ -1978,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())
|
||||||
@@ -2018,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,7 +77,8 @@ 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 = [];
|
List<int> oneTime = [];
|
||||||
|
|||||||
@@ -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,7 +65,6 @@ 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())
|
||||||
{
|
{
|
||||||
@@ -196,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.GetAllowInsecure())
|
|
||||||
{
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,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.StringTrue;
|
|
||||||
}
|
|
||||||
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
|
|
||||||
{
|
|
||||||
item.AllowInsecure = Global.StringFalse;
|
|
||||||
}
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(itemMap.GetValueOrDefault)
|
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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,188 +10,25 @@ 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;
|
||||||
|
|
||||||
/// <summary>
|
public async Task Init(Config config)
|
||||||
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
|
|
||||||
/// </summary>
|
|
||||||
private static readonly HashSet<string> TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
{
|
||||||
"EBD41040E4BB3EC742C9E381D31EF2A41A48B6685C96E7CEF3C1DF6CD4331C99", // GlobalSign Root CA
|
if (_config != null)
|
||||||
"6DC47172E01CBCB0BF62580D895FE2B8AC9AD4F873801E0C10B9C837D21EB177", // Entrust.net Premium 2048 Secure Server CA
|
{
|
||||||
"73C176434F1BC6D5ADF45B0E76E727287C8DE57616C1E6E6141A2B2CBC7D8E4C", // Entrust Root Certification Authority
|
return;
|
||||||
"D8E0FEBC1DB2E38D00940F37D27D41344D993E734B99D5656D9778D4D8143624", // Certum Root CA
|
}
|
||||||
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root
|
_config = config;
|
||||||
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2
|
await Task.CompletedTask;
|
||||||
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3
|
}
|
||||||
"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
|
|
||||||
"0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority
|
|
||||||
"1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority
|
|
||||||
"41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA
|
|
||||||
"E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna
|
|
||||||
"C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority
|
|
||||||
"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
|
|
||||||
"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
|
|
||||||
"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
|
|
||||||
"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
|
|
||||||
"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
|
|
||||||
"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
|
|
||||||
};
|
|
||||||
|
|
||||||
public static CertPemManager Instance => _instance.Value;
|
public static CertPemManager Instance => _instance.Value;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get certificate in PEM format from a server with CA pinning validation
|
/// Get certificate in PEM format from a server with CA pinning validation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4,
|
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
|
||||||
List<string>? verifyPeerCertByName = null, bool allowInsecure = false)
|
List<string>? verifyPeerCertByName = null, int timeout = 4)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -204,8 +41,7 @@ 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, verifyPeerCertByName ?? [],
|
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
|
||||||
allowInsecure));
|
|
||||||
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
|
||||||
@@ -240,8 +76,8 @@ 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,
|
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
|
||||||
List<string>? verifyPeerCertByName = null, bool allowInsecure = false)
|
List<string>? verifyPeerCertByName = null, int timeout = 4)
|
||||||
{
|
{
|
||||||
var pemList = new List<string>();
|
var pemList = new List<string>();
|
||||||
try
|
try
|
||||||
@@ -255,8 +91,7 @@ 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, verifyPeerCertByName ?? [],
|
ValidateServerCertificate(sender, certificate, chain, sslPolicyErrors, verifyPeerCertByName ?? []));
|
||||||
allowInsecure));
|
|
||||||
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
|
||||||
@@ -294,25 +129,18 @@ public class CertPemManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validate server certificate with CA pinning
|
/// Validate server certificate with CA pinning
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool ValidateServerCertificate(
|
private bool ValidateServerCertificate(
|
||||||
object _,
|
object _,
|
||||||
X509Certificate? certificate,
|
X509Certificate? certificate,
|
||||||
X509Chain? chain,
|
X509Chain? chain,
|
||||||
SslPolicyErrors sslPolicyErrors,
|
SslPolicyErrors sslPolicyErrors,
|
||||||
List<string> verifyPeerCertByName,
|
List<string> verifyPeerCertByName)
|
||||||
bool allowInsecure)
|
|
||||||
{
|
{
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
||||||
@@ -336,9 +164,10 @@ public class CertPemManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
var rootCert = certChain.ChainElements[^1].Certificate;
|
var rootCert = certChain.ChainElements[^1].Certificate;
|
||||||
var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256);
|
|
||||||
|
|
||||||
if (!TrustedCaThumbprints.Contains(rootThumbprint))
|
var trustedCerts = BuildTrustedCertificateCollection();
|
||||||
|
|
||||||
|
if (!trustedCerts.Contains(rootCert))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -451,4 +280,50 @@ public class CertPemManager
|
|||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,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]
|
||||||
@@ -102,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]
|
||||||
@@ -145,8 +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 List<string>? RouteExcludeAddress { get; set; }
|
||||||
|
public string IPv4Address { get; set; }
|
||||||
|
public string IPv6Address { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -247,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]
|
||||||
@@ -266,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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,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
|
||||||
@@ -334,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; }
|
||||||
@@ -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; }
|
||||||
@@ -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; }
|
||||||
@@ -507,11 +510,18 @@ public class MaskSettings4Ray
|
|||||||
|
|
||||||
public string? password { get; set; }
|
public string? password { 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; }
|
||||||
@@ -542,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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public class ProfileItem
|
|||||||
|
|
||||||
public bool IsValid()
|
public bool IsValid()
|
||||||
{
|
{
|
||||||
if (IsComplex())
|
if (IsComplex() || ConfigType == EConfigType.Outbound)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
336
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
336
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>
|
||||||
@@ -1915,7 +1951,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration. 的本地化字符串。
|
/// 查找类似 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>
|
/// </summary>
|
||||||
public static string MsgAllowInsecureDeprecated {
|
public static string MsgAllowInsecureDeprecated {
|
||||||
get {
|
get {
|
||||||
@@ -1959,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>
|
||||||
@@ -2067,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>
|
||||||
@@ -2599,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 {
|
||||||
@@ -2670,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>
|
||||||
@@ -2916,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>
|
||||||
@@ -3051,6 +3096,33 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
@@ -3070,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 {
|
||||||
@@ -3159,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>
|
||||||
@@ -3222,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>
|
||||||
@@ -3276,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>
|
||||||
@@ -3303,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>
|
||||||
@@ -3465,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>
|
||||||
@@ -3564,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>
|
||||||
@@ -3826,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 {
|
||||||
@@ -4113,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>
|
||||||
@@ -4833,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>
|
||||||
@@ -1746,4 +1770,4 @@ 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>
|
</root>
|
||||||
@@ -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>
|
||||||
@@ -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>Pour obtenir des certificats auto-signés uniquement. Risque MITM.</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||||
<value>Adresse de sortie 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1701,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>
|
||||||
@@ -1719,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>
|
||||||
@@ -1750,7 +1780,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<value>New Update</value>
|
<value>New Update</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
||||||
<value>Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration.</value>
|
<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>
|
||||||
<data name="TbRouteExcludeAddress" xml:space="preserve">
|
<data name="TbRouteExcludeAddress" xml:space="preserve">
|
||||||
<value>Route Exclude Address</value>
|
<value>Route Exclude Address</value>
|
||||||
@@ -1770,4 +1800,64 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||||
<value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value>
|
<value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1044,6 +1044,12 @@
|
|||||||
<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>
|
||||||
@@ -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>
|
||||||
@@ -1312,7 +1351,7 @@
|
|||||||
<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>
|
||||||
@@ -1372,7 +1411,7 @@
|
|||||||
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</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>
|
||||||
@@ -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>
|
||||||
@@ -1549,7 +1588,7 @@
|
|||||||
<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>
|
||||||
@@ -1746,4 +1779,85 @@
|
|||||||
<data name="menuNewUpdate" xml:space="preserve">
|
<data name="menuNewUpdate" xml:space="preserve">
|
||||||
<value>Доступно обновление</value>
|
<value>Доступно обновление</value>
|
||||||
</data>
|
</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>
|
||||||
</root>
|
</root>
|
||||||
@@ -1030,7 +1030,7 @@
|
|||||||
<value>添加 [Hysteria2]</value>
|
<value>添加 [Hysteria2]</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
|
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
|
||||||
<value>Hysteria 最大带宽 (Up/Dw)</value>
|
<value>Hysteria 最大带宽 (上传/下载)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
|
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
|
||||||
<value>使用系统 hosts</value>
|
<value>使用系统 hosts</value>
|
||||||
@@ -1041,6 +1041,12 @@
|
|||||||
<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>
|
||||||
@@ -1101,6 +1107,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-10000, 0=无限制). 仅 Xray. 留空使用默认值.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>回退延迟</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>ACK 检测不可用时的回退延迟 (如 500ms). 仅 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>
|
||||||
@@ -1459,7 +1498,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>默认全局生效,仅在 sing-box 中内置 FakeIP 过滤。</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||||
<value>请至少添加一个配置</value>
|
<value>请至少添加一个配置</value>
|
||||||
@@ -1695,12 +1734,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>仅用于抓取自签证书,存在中间人风险。</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>测试 UDP 延迟 (多选)</value>
|
<value>测试 UDP 延迟 (多选)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1713,9 +1746,6 @@
|
|||||||
<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>请填写正确的 SendThrough IPv4 地址。</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>绑定网口</value>
|
<value>绑定网口</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1744,7 +1774,11 @@
|
|||||||
<value>有更新</value>
|
<value>有更新</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
||||||
<value>警告:Xray 将在 2026.8.1 禁用跳过证书验证 allowInsecure ,请尽快改用证书固定指纹 pinnedPeerCertSha256。到期后无法使用 </value>
|
<value>当前节点使用未加密连接,您的通信可能会被威权政府掌控的网络中间设施直接查看
|
||||||
|
为了安全,此类节点无法通过 26.2.6+ 版本的Xray核心连接
|
||||||
|
如果是自建节点请启用 TLS 等安全加密,或固定证书 pinSHA256
|
||||||
|
如果机场节点请联系服务商完成技术升级,如服务商拒绝配合,建议更换更重视用户安全的服务商
|
||||||
|
更多的信息,请访问 https://github.com/2dust/v2rayN/discussions/9460</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbVerifyPeerCertByName" xml:space="preserve">
|
<data name="TbVerifyPeerCertByName" xml:space="preserve">
|
||||||
<value>Verify Peer Cert By Name</value>
|
<value>Verify Peer Cert By Name</value>
|
||||||
@@ -1767,4 +1801,64 @@
|
|||||||
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||||
<value>将数据包末端拆分为更小片段发送,可能影响吞吐与延迟。</value>
|
<value>将数据包末端拆分为更小片段发送,可能影响吞吐与延迟。</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
|
||||||
|
<value>DNS 通过 Bridge</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgInsecureConfiguration" xml:space="preserve">
|
||||||
|
<value>检测到不安全配置:AllowInsecure 已启用,但未提供证书。这可能会导致中间人攻击。</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>请填写合法的HTTP请求头JSON。</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>连接代理解析策略</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>添加自定义出站</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
|
||||||
|
<value>自定义出站 {0} 的文件未找到:{1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbCustomOutboundTip" xml:space="preserve">
|
||||||
|
<value>仅支持 xray/sing-box 的单个 outbound/endpoin</value>
|
||||||
|
</data>
|
||||||
|
<data name="LvCustomCoreType" xml:space="preserve">
|
||||||
|
<value>自定义配置核心</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1041,6 +1041,12 @@
|
|||||||
<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>
|
||||||
@@ -1101,6 +1107,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-10000, 0=無限制)。僅 Xray。留空使用預設值。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||||
|
<value>回退延遲</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||||
|
<value>ACK 偵測不可用時的回退延遲 (如 500ms)。僅 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>
|
||||||
@@ -1459,7 +1498,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>默認全局生效,僅在 sing-box 中內置 FakeIP 過濾。</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||||
<value>請至少添加一個節點</value>
|
<value>請至少添加一個節點</value>
|
||||||
@@ -1695,12 +1734,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>僅用於抓取自簽證書,存在中間人風險。</value>
|
|
||||||
</data>
|
|
||||||
<data name="menuUdpTestServer" xml:space="preserve">
|
<data name="menuUdpTestServer" xml:space="preserve">
|
||||||
<value>測試 UDP 延遲(多選)</value>
|
<value>測試 UDP 延遲(多選)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1713,9 +1746,6 @@
|
|||||||
<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>請填寫正確的 SendThrough IPv4 位址。</value>
|
|
||||||
</data>
|
|
||||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||||
<value>綁定網路介面</value>
|
<value>綁定網路介面</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1744,7 +1774,11 @@
|
|||||||
<value>有更新</value>
|
<value>有更新</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
||||||
<value>警告:Xray 將在 2026.8.1 停用跳過憑證驗證 allowInsecure ,請盡快改用憑證固定指紋 pinnedPeerCertSha256。到期後無法使用 allowInsecure。</value>
|
<value>目前節點使用未加密連接,您的通訊可能會被威權政府掌控的網路中間設施直接查看
|
||||||
|
為了安全,此類節點無法透過 26.2.6+ 版本的Xray核心連接
|
||||||
|
如果是自建節點請啟用 TLS 等安全加密,或固定憑證 pinSHA256
|
||||||
|
若機場節點請聯絡服務商完成技術升級,如服務商拒絕配合,建議更換更重視用戶安全的服務商
|
||||||
|
更多的信息,請訪問 https://github.com/2dust/v2rayN/discussions/9460</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TbVerifyPeerCertByName" xml:space="preserve">
|
<data name="TbVerifyPeerCertByName" xml:space="preserve">
|
||||||
<value>Verify Peer Cert By Name</value>
|
<value>Verify Peer Cert By Name</value>
|
||||||
@@ -1767,4 +1801,64 @@
|
|||||||
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||||
<value>將封包末端拆分為更小的片段進行傳送,可能會影響吞吐量與延遲</value>
|
<value>將封包末端拆分為更小的片段進行傳送,可能會影響吞吐量與延遲</value>
|
||||||
</data>
|
</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="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 封包大小 (min/max)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbLegacyProtectTip" xml:space="preserve">
|
||||||
|
<value>啟用則使用 sing-box TUN ,否則使用 xray TUN</value>
|
||||||
|
</data>
|
||||||
|
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
|
||||||
|
<value>請填寫合法的HTTP請求頭JSON。</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRootCertificateProvider" xml:space="preserve">
|
||||||
|
<value>根憑證提供者</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbRootCertificateProviderTip" xml:space="preserve">
|
||||||
|
<value>僅適用於 v2rayN GUI 的下載與網路請求,不影響核心的憑證驗證</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
|
||||||
|
<value>連線代理解析策略</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>新增自訂出站</value>
|
||||||
|
</data>
|
||||||
|
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
|
||||||
|
<value>自訂出站 {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>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
{
|
{
|
||||||
"tag": "tun",
|
"tag": "tun",
|
||||||
"protocol": "tun",
|
"protocol": "tun",
|
||||||
"settings": {
|
"settings": {
|
||||||
"name": "xray_tun",
|
"name": "xray_tun",
|
||||||
"MTU": 9000,
|
"MTU": 9000,
|
||||||
"gateway": [
|
"gateway": [
|
||||||
"172.18.0.1/30",
|
"172.18.0.1/30",
|
||||||
"fdfe:dcba:9876::1/126"
|
"fdfe:dcba:9876::1/126"
|
||||||
],
|
],
|
||||||
"dns": [
|
"dns": [
|
||||||
"172.18.0.1"
|
"1.1.1.1",
|
||||||
],
|
"8.8.8.8"
|
||||||
"autoSystemRoutingTable": [
|
],
|
||||||
"0.0.0.0/0",
|
"autoSystemRoutingTable": [
|
||||||
"::/0"
|
"0.0.0.0/0",
|
||||||
],
|
"::/0"
|
||||||
"autoOutboundsInterface": "auto"
|
],
|
||||||
},
|
"autoOutboundsInterface": "auto"
|
||||||
"sniffing": {
|
},
|
||||||
"enabled": true,
|
"sniffing": {
|
||||||
"destOverride": [
|
"enabled": true,
|
||||||
"http",
|
"destOverride": [
|
||||||
"tls"
|
"http",
|
||||||
]
|
"tls"
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
2697
v2rayN/ServiceLib/Sample/chrome_roots_pem
Normal file
2697
v2rayN/ServiceLib/Sample/chrome_roots_pem
Normal file
File diff suppressed because it is too large
Load Diff
@@ -66,6 +66,11 @@ kill_children "$PID"
|
|||||||
echo "Terminating main process: $PID"
|
echo "Terminating main process: $PID"
|
||||||
kill -9 "$PID" 2>/dev/null || true
|
kill -9 "$PID" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Wait a little for process/port resources to be fully released
|
||||||
|
FINAL_WAIT_SECONDS=1
|
||||||
|
echo "Waiting ${FINAL_WAIT_SECONDS}s for resources to settle..."
|
||||||
|
sleep "$FINAL_WAIT_SECONDS"
|
||||||
|
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo "Process $PID and all its children have been terminated"
|
echo "Process $PID and all its children have been terminated"
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ kill_descendants "$PID"
|
|||||||
echo "Terminating main process: $PID"
|
echo "Terminating main process: $PID"
|
||||||
kill -9 "$PID" 2>/dev/null || true
|
kill -9 "$PID" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Wait a little for process/port resources to be fully released
|
||||||
|
FINAL_WAIT_SECONDS=1
|
||||||
|
echo "Waiting ${FINAL_WAIT_SECONDS}s for resources to settle..."
|
||||||
|
sleep "$FINAL_WAIT_SECONDS"
|
||||||
|
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo "Process $PID and all its descendants have been terminated"
|
echo "Process $PID and all its descendants have been terminated"
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
|
|||||||
3111
v2rayN/ServiceLib/Sample/mozilla_roots_pem
Normal file
3111
v2rayN/ServiceLib/Sample/mozilla_roots_pem
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"network": "udp",
|
"network": [
|
||||||
|
"udp"
|
||||||
|
],
|
||||||
"port": [
|
"port": [
|
||||||
135,
|
135,
|
||||||
137,
|
137,
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
<PackageReference Include="ReactiveUI">
|
<PackageReference Include="ReactiveUI">
|
||||||
<TreatAsUsed>true</TreatAsUsed>
|
<TreatAsUsed>true</TreatAsUsed>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="ReactiveUI.Fody" />
|
<PackageReference Include="ReactiveUI.SourceGenerators">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="sqlite-net-e" />
|
<PackageReference Include="sqlite-net-e" />
|
||||||
<PackageReference Include="Repobot.SQLite.Unofficial" />
|
<PackageReference Include="Repobot.SQLite.Unofficial" />
|
||||||
<PackageReference Include="NLog" />
|
<PackageReference Include="NLog" />
|
||||||
@@ -23,6 +26,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Sample\chrome_roots_pem" />
|
||||||
<EmbeddedResource Include="Sample\clash_mixin_yaml" />
|
<EmbeddedResource Include="Sample\clash_mixin_yaml" />
|
||||||
<EmbeddedResource Include="Sample\clash_tun_yaml" />
|
<EmbeddedResource Include="Sample\clash_tun_yaml" />
|
||||||
<EmbeddedResource Include="Sample\custom_routing_black" />
|
<EmbeddedResource Include="Sample\custom_routing_black" />
|
||||||
@@ -32,6 +36,7 @@
|
|||||||
<EmbeddedResource Include="Sample\dns_v2ray_normal" />
|
<EmbeddedResource Include="Sample\dns_v2ray_normal" />
|
||||||
<EmbeddedResource Include="Sample\kill_as_sudo_linux_sh" />
|
<EmbeddedResource Include="Sample\kill_as_sudo_linux_sh" />
|
||||||
<EmbeddedResource Include="Sample\kill_as_sudo_osx_sh" />
|
<EmbeddedResource Include="Sample\kill_as_sudo_osx_sh" />
|
||||||
|
<EmbeddedResource Include="Sample\mozilla_roots_pem" />
|
||||||
<EmbeddedResource Include="Sample\pac" />
|
<EmbeddedResource Include="Sample\pac" />
|
||||||
<EmbeddedResource Include="Sample\proxy_set_linux_sh" />
|
<EmbeddedResource Include="Sample\proxy_set_linux_sh" />
|
||||||
<EmbeddedResource Include="Sample\proxy_set_osx_sh" />
|
<EmbeddedResource Include="Sample\proxy_set_osx_sh" />
|
||||||
@@ -67,6 +72,9 @@
|
|||||||
<EmbeddedResource Update="Resx\ResUI.hu.resx">
|
<EmbeddedResource Update="Resx\ResUI.hu.resx">
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Update="Resx\ResUI.id.resx">
|
||||||
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Update="Resx\ResUI.resx">
|
<EmbeddedResource Update="Resx\ResUI.resx">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<LastGenOutput>ResUI.Designer.cs</LastGenOutput>
|
<LastGenOutput>ResUI.Designer.cs</LastGenOutput>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
namespace ServiceLib.Services.CoreConfig;
|
namespace ServiceLib.Services.CoreConfig;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Core configuration file processing class
|
/// Core configuration file processing class.
|
||||||
|
/// The TUN state is taken as a snapshot so the generated config always agrees
|
||||||
|
/// with the launch elevation decision (see CoreManager.ShouldRunAsSudo).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CoreConfigClashService(Config config)
|
public class CoreConfigClashService(Config config, bool isTunEnabled)
|
||||||
{
|
{
|
||||||
private static readonly string _tag = "CoreConfigClashService";
|
private static readonly string _tag = "CoreConfigClashService";
|
||||||
|
|
||||||
@@ -102,7 +104,7 @@ public class CoreConfigClashService(Config config)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//enable tun mode
|
//enable tun mode
|
||||||
if (config.TunModeItem.EnableTun)
|
if (isTunEnabled)
|
||||||
{
|
{
|
||||||
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
|
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
|
||||||
if (tun.IsNotEmpty())
|
if (tun.IsNotEmpty())
|
||||||
@@ -171,7 +173,7 @@ public class CoreConfigClashService(Config config)
|
|||||||
}
|
}
|
||||||
foreach (var item in mixinContent)
|
foreach (var item in mixinContent)
|
||||||
{
|
{
|
||||||
if (!config.TunModeItem.EnableTun && item.Key == "tun")
|
if (!isTunEnabled && item.Key == "tun")
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,13 +57,10 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
|
|||||||
|
|
||||||
ConvertGeo2Ruleset();
|
ConvertGeo2Ruleset();
|
||||||
|
|
||||||
ApplyOutboundBindInterface();
|
|
||||||
ApplyOutboundSendThrough();
|
|
||||||
|
|
||||||
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
||||||
ret.Success = true;
|
ret.Success = true;
|
||||||
|
|
||||||
ret.Data = ApplyFullConfigTemplate();
|
ret.Data = ApplyFinalConfigModifiers();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -107,7 +104,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
|
|||||||
|
|
||||||
foreach (var it in selecteds)
|
foreach (var it in selecteds)
|
||||||
{
|
{
|
||||||
if (!(Global.SingboxSupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType()))
|
if (!(Global.SingboxSupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType() || it.ConfigType is EConfigType.Outbound))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -174,7 +171,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
|
|||||||
ApplyOutboundBindInterface();
|
ApplyOutboundBindInterface();
|
||||||
ApplyOutboundSendThrough();
|
ApplyOutboundSendThrough();
|
||||||
ret.Success = true;
|
ret.Success = true;
|
||||||
ret.Data = JsonUtils.Serialize(_coreConfig);
|
ret.Data = ApplyCustomOutboundReplace();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -236,7 +233,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
|
|||||||
|
|
||||||
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
||||||
ret.Success = true;
|
ret.Success = true;
|
||||||
ret.Data = JsonUtils.Serialize(_coreConfig);
|
ret.Data = ApplyCustomOutboundReplace();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -2,54 +2,137 @@ namespace ServiceLib.Services.CoreConfig;
|
|||||||
|
|
||||||
public partial class CoreConfigSingboxService
|
public partial class CoreConfigSingboxService
|
||||||
{
|
{
|
||||||
private string ApplyFullConfigTemplate()
|
private string ApplyFinalConfigModifiers()
|
||||||
|
{
|
||||||
|
ApplyOutboundBindInterface();
|
||||||
|
ApplyOutboundSendThrough();
|
||||||
|
|
||||||
|
var coreConfigContent = ApplyCustomOutboundReplace();
|
||||||
|
|
||||||
|
return ApplyFullConfigTemplate(coreConfigContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ApplyCustomOutboundReplace()
|
||||||
|
{
|
||||||
|
var coreConfigContent = JsonUtils.Serialize(_coreConfig);
|
||||||
|
if (context.CustomOutboundMap.Count == 0)
|
||||||
|
{
|
||||||
|
return coreConfigContent;
|
||||||
|
}
|
||||||
|
var coreConfigNode = JsonNode.Parse(coreConfigContent) as JsonObject;
|
||||||
|
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
|
||||||
|
ReplaceCustomOutbounds(_coreConfig.outbounds, coreConfigOutboundsNode);
|
||||||
|
coreConfigNode!["outbounds"] = coreConfigOutboundsNode;
|
||||||
|
var coreConfigEndpointsNode = coreConfigNode?["endpoints"] as JsonArray ?? [];
|
||||||
|
ReplaceCustomOutbounds(_coreConfig.endpoints, coreConfigEndpointsNode);
|
||||||
|
if (coreConfigEndpointsNode.Count > 0)
|
||||||
|
{
|
||||||
|
coreConfigNode!["endpoints"] = coreConfigEndpointsNode;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
coreConfigNode?.Remove("endpoints");
|
||||||
|
}
|
||||||
|
return JsonUtils.Serialize(coreConfigNode);
|
||||||
|
|
||||||
|
void ReplaceCustomOutbounds(IReadOnlyList<BaseServer4Sbox>? source, JsonArray jsonArrayOutbounds)
|
||||||
|
{
|
||||||
|
foreach (var outbound in source ?? [])
|
||||||
|
{
|
||||||
|
if (!context.CustomOutboundMap.TryGetValue(outbound, out var customOutboundIndex))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var outboundTag = outbound.tag;
|
||||||
|
var outboundDetour = outbound.detour ?? string.Empty;
|
||||||
|
var outboundBindInterface = outbound.bind_interface ?? string.Empty;
|
||||||
|
var customOutboundContent = context.CustomOutboundContent[customOutboundIndex];
|
||||||
|
var containTagPlaceholder = customOutboundContent.Contains("{{tag}}");
|
||||||
|
var containDetourPlaceholder = customOutboundContent.Contains("{{detour}}");
|
||||||
|
var containBindInterfacePlaceholder = customOutboundContent.Contains("{{interface}}");
|
||||||
|
customOutboundContent = customOutboundContent.Replace("{{tag}}", outboundTag);
|
||||||
|
customOutboundContent = customOutboundContent.Replace("{{detour}}", outboundDetour);
|
||||||
|
customOutboundContent = customOutboundContent.Replace("{{interface}}", outboundBindInterface);
|
||||||
|
var customOutboundObj = JsonUtils.ParseJson(customOutboundContent) as JsonObject;
|
||||||
|
|
||||||
|
if (!containTagPlaceholder)
|
||||||
|
{
|
||||||
|
customOutboundObj?["tag"] = outboundTag;
|
||||||
|
}
|
||||||
|
if (!containDetourPlaceholder && !outboundDetour.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
customOutboundObj?["detour"] = outboundDetour;
|
||||||
|
}
|
||||||
|
else if (outboundDetour.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
customOutboundObj?.Remove("detour");
|
||||||
|
}
|
||||||
|
if (!containBindInterfacePlaceholder && !outboundBindInterface.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
customOutboundObj?["bind_interface"] = outboundBindInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = jsonArrayOutbounds
|
||||||
|
.Select((node, idx) => new { node, idx })
|
||||||
|
.FirstOrDefault(x => x.node?["tag"]?.ToString() == outboundTag)?.idx ?? -1;
|
||||||
|
if (index != -1)
|
||||||
|
{
|
||||||
|
jsonArrayOutbounds[index] = customOutboundObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ApplyFullConfigTemplate(string coreConfigContent)
|
||||||
{
|
{
|
||||||
var fullConfigTemplate = context.FullConfigTemplate;
|
var fullConfigTemplate = context.FullConfigTemplate;
|
||||||
if (fullConfigTemplate is not { Enabled: true })
|
if (fullConfigTemplate is not { Enabled: true })
|
||||||
{
|
{
|
||||||
return JsonUtils.Serialize(_coreConfig);
|
return coreConfigContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
|
var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
|
||||||
if (fullConfigTemplateItem.IsNullOrEmpty())
|
if (fullConfigTemplateItem.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
return JsonUtils.Serialize(_coreConfig);
|
return coreConfigContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
|
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
|
||||||
if (fullConfigTemplateNode == null)
|
if (fullConfigTemplateNode == null)
|
||||||
{
|
{
|
||||||
return JsonUtils.Serialize(_coreConfig);
|
return coreConfigContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process outbounds
|
// Process outbounds
|
||||||
var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? [];
|
var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? [];
|
||||||
foreach (var outbound in _coreConfig.outbounds)
|
var coreConfigNode = JsonNode.Parse(coreConfigContent);
|
||||||
|
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
|
||||||
|
foreach (var outbound in coreConfigOutboundsNode)
|
||||||
{
|
{
|
||||||
if (outbound.type.ToLower() is "direct" or "block")
|
if (outbound["type"]?.ToString()?.ToLower() is "direct" or "block")
|
||||||
{
|
{
|
||||||
if (fullConfigTemplate.AddProxyOnly == true)
|
if (fullConfigTemplate.AddProxyOnly == true)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (outbound.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound.server ?? string.Empty))
|
if (outbound["detour"] is null && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound["server"]?.ToString() ?? string.Empty))
|
||||||
{
|
{
|
||||||
outbound.detour = fullConfigTemplate.ProxyDetour;
|
outbound["detour"] = fullConfigTemplate.ProxyDetour;
|
||||||
}
|
}
|
||||||
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
|
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
|
||||||
}
|
}
|
||||||
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
|
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
|
||||||
|
|
||||||
// Process endpoints
|
// Process endpoints
|
||||||
if (_coreConfig.endpoints is { Count: > 0 })
|
if (fullConfigTemplateNode["endpoints"] is JsonArray { Count: > 0 } coreConfigEndpointsNode)
|
||||||
{
|
{
|
||||||
var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? [];
|
var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? [];
|
||||||
foreach (var endpoint in _coreConfig.endpoints)
|
foreach (var endpoint in coreConfigEndpointsNode)
|
||||||
{
|
{
|
||||||
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
|
if (endpoint["detour"] is null && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
endpoint.detour = fullConfigTemplate.ProxyDetour;
|
endpoint["detour"] = fullConfigTemplate.ProxyDetour;
|
||||||
}
|
}
|
||||||
customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint));
|
customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint));
|
||||||
}
|
}
|
||||||
@@ -66,10 +149,6 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(context.IsTunEnabled || context.IsWindows))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
foreach (var outbound in _coreConfig.outbounds ?? [])
|
foreach (var outbound in _coreConfig.outbounds ?? [])
|
||||||
{
|
{
|
||||||
outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null;
|
outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null;
|
||||||
@@ -83,6 +162,7 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var outbound in _coreConfig.outbounds ?? [])
|
foreach (var outbound in _coreConfig.outbounds ?? [])
|
||||||
{
|
{
|
||||||
outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null;
|
outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null;
|
||||||
|
|||||||
@@ -137,12 +137,12 @@ public partial class CoreConfigSingboxService
|
|||||||
// fake ip
|
// fake ip
|
||||||
if (simpleDnsItem.FakeIP == true)
|
if (simpleDnsItem.FakeIP == true)
|
||||||
{
|
{
|
||||||
|
var fakeipRange = simpleDnsItem.FakeIPRange.IsNullOrEmpty() ? Global.FakeIPRanges.First() : simpleDnsItem.FakeIPRange;
|
||||||
var fakeip = new Server4Sbox
|
var fakeip = new Server4Sbox
|
||||||
{
|
{
|
||||||
tag = Global.SingboxFakeDNSTag,
|
tag = Global.SingboxFakeDNSTag,
|
||||||
type = "fakeip",
|
type = "fakeip",
|
||||||
inet4_range = "198.18.0.0/15",
|
inet4_range = fakeipRange,
|
||||||
inet6_range = "fc00::/18",
|
|
||||||
};
|
};
|
||||||
_coreConfig.dns.servers.Add(fakeip);
|
_coreConfig.dns.servers.Add(fakeip);
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ public partial class CoreConfigSingboxService
|
|||||||
_coreConfig.dns.rules.Add(new()
|
_coreConfig.dns.rules.Add(new()
|
||||||
{
|
{
|
||||||
server = Global.SingboxDirectDNSTag,
|
server = Global.SingboxDirectDNSTag,
|
||||||
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
|
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4ProxyDial),
|
||||||
domain = context.ProtectDomainList.ToList(),
|
domain = context.ProtectDomainList.ToList(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -266,7 +266,7 @@ public partial class CoreConfigSingboxService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp == true)
|
if (simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp != false)
|
||||||
{
|
{
|
||||||
var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName));
|
var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName));
|
||||||
fakeipFilterRule.invert = true;
|
fakeipFilterRule.invert = true;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user