mirror of
https://github.com/2dust/v2rayN.git
synced 2026-09-26 00:22:07 +03:00
Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 521230c40d | |||
| 426d5d5a21 | |||
| 3f14c86a04 | |||
| 70b8ecd3c8 | |||
| 0d452328a2 | |||
| 2a65e5ac80 | |||
| f332caf507 | |||
| 4b412e246f | |||
| 09ac4d181a | |||
| c575527725 | |||
| 589ccc6e06 | |||
| 0a2f3e4f22 | |||
| 007adb6403 | |||
| e50c0a9170 | |||
| af0eb9ed14 | |||
| 0436081436 | |||
| 2a89a2e597 | |||
| 2be63d1655 | |||
| 74bd28bbd0 | |||
| be501cbd2a | |||
| e20b4dbfab | |||
| 728cbe70f2 | |||
| 26a86f9bd2 | |||
| c66c55b05e | |||
| ebb4bd5daa | |||
| 14e27f3d23 | |||
| 2d514ff480 | |||
| 11bd52b6a5 | |||
| 3b3fe8def3 | |||
| c9e843aa3b | |||
| 7be264c65c | |||
| 5e6660aeee | |||
| 5b27f4836f | |||
| e0ad2c46e6 | |||
| 230a2f6773 | |||
| e7cc4c832e | |||
| fb7033aab5 | |||
| b6cc3913ac | |||
| bc06a5dc21 | |||
| 9c1951178c | |||
| b3df42b9ba | |||
| e01717d832 | |||
| 8b4063b44b | |||
| 3136f573dd | |||
| e101b1d7b0 | |||
| 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 |
@@ -6,6 +6,6 @@ updates:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "nuget"
|
||||
directory: "/"
|
||||
directory: "/v2rayN"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
@@ -9,8 +9,17 @@ on:
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -12,6 +12,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -26,6 +27,7 @@ jobs:
|
||||
with:
|
||||
target: linux
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
secrets: inherit
|
||||
|
||||
deb:
|
||||
name: build and release deb x64 & arm64
|
||||
@@ -56,9 +58,9 @@ jobs:
|
||||
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
|
||||
echo "==== Dist tree ===="
|
||||
ls -R "$GITHUB_WORKSPACE/dist/deb" || true
|
||||
- name: Upload DEBs to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with: { file: "dist/deb/**/*.deb", tag: "${{ env.RELEASE_TAG }}", file_glob: true, prerelease: true }
|
||||
- name: Upload DEB artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with: { name: release-deb, path: "dist/deb/**/*.deb", if-no-files-found: error }
|
||||
|
||||
rpm:
|
||||
name: build and release rpm x64 & arm64
|
||||
@@ -115,9 +117,9 @@ jobs:
|
||||
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 ===="
|
||||
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
|
||||
- name: Upload RPMs to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with: { file: "dist/rpm/**/*.rpm", tag: "${{ env.RELEASE_TAG }}", file_glob: true, prerelease: true }
|
||||
- name: Upload RPM artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with: { name: release-rpm, path: "dist/rpm/**/*.rpm", if-no-files-found: error }
|
||||
|
||||
rpm-riscv64:
|
||||
name: build and release rpm riscv64
|
||||
@@ -155,25 +157,9 @@ jobs:
|
||||
find "$GITHUB_WORKSPACE/dist/rpm-riscv64" -name "*.riscv64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-riscv64/v2rayN-linux-rhel-riscv64.rpm" \; || true
|
||||
echo "==== Dist tree ===="
|
||||
ls -R "$GITHUB_WORKSPACE/dist/rpm-riscv64" || true
|
||||
- name: Upload RPMs to release
|
||||
shell: bash
|
||||
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
|
||||
- name: Upload RPM artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with: { name: release-rpm-riscv64, path: "dist/rpm-riscv64/**/*.rpm", if-no-files-found: error }
|
||||
|
||||
deb-riscv64:
|
||||
name: build and release deb riscv64
|
||||
@@ -212,30 +198,14 @@ jobs:
|
||||
find "$GITHUB_WORKSPACE/dist/deb-riscv64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-riscv64/v2rayN-linux-riscv64.deb" \; || true
|
||||
echo "==== Dist tree ===="
|
||||
ls -R "$GITHUB_WORKSPACE/dist/deb-riscv64" || true
|
||||
- name: Upload DEBs to release
|
||||
shell: bash
|
||||
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
|
||||
- name: Upload DEB artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with: { name: release-deb-riscv64, path: "dist/deb-riscv64/**/*.deb", if-no-files-found: error }
|
||||
|
||||
deb-loong64:
|
||||
name: build and release deb loong64
|
||||
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-26.04-arm
|
||||
env:
|
||||
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
|
||||
@@ -244,7 +214,7 @@ jobs:
|
||||
QCOW2_IMAGE: debian13-loong64.qcow2
|
||||
EFI_CODE: edk2-loongarch64-code.fd
|
||||
EFI_VARS: edk2-loongarch64-vars.fd
|
||||
QEMU_VERSION: 10.2.3
|
||||
QEMU_VERSION: 10.2.4
|
||||
steps:
|
||||
- name: Prepare host tools
|
||||
shell: bash
|
||||
@@ -264,10 +234,10 @@ jobs:
|
||||
- name: Download QEMU prebuild
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz"
|
||||
tar -xzf qemu-linux-x64-v3.tar.gz
|
||||
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-x64-v3/ "$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
|
||||
@@ -288,7 +258,7 @@ jobs:
|
||||
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-deb-loong64.sock"
|
||||
rm -f "$VIRTIOFSD_SOCKET"
|
||||
|
||||
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" -o source="$GITHUB_WORKSPACE" -o cache=auto &
|
||||
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
|
||||
@@ -378,14 +348,14 @@ jobs:
|
||||
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
|
||||
echo "==== Dist tree ===="
|
||||
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
|
||||
- name: Upload DEBs to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with: { file: "dist/deb-loong64/**/*.deb", tag: "${{ env.RELEASE_TAG }}", file_glob: true, prerelease: true }
|
||||
- name: Upload DEB artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with: { name: release-deb-loong64, path: "dist/deb-loong64/**/*.deb", if-no-files-found: error }
|
||||
|
||||
rpm-loong64:
|
||||
name: build and release rpm loong64
|
||||
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-26.04-arm
|
||||
env:
|
||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
||||
QCOW2_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/fedora43-loong64.qcow2
|
||||
@@ -394,7 +364,7 @@ jobs:
|
||||
QCOW2_IMAGE: fedora43-loong64.qcow2
|
||||
EFI_CODE: edk2-loongarch64-code.fd
|
||||
EFI_VARS: edk2-loongarch64-vars.fd
|
||||
QEMU_VERSION: 10.2.3
|
||||
QEMU_VERSION: 10.2.4
|
||||
steps:
|
||||
- name: Prepare host tools
|
||||
shell: bash
|
||||
@@ -414,10 +384,10 @@ jobs:
|
||||
- name: Download QEMU prebuild
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz"
|
||||
tar -xzf qemu-linux-x64-v3.tar.gz
|
||||
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-x64-v3/ "$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
|
||||
@@ -438,7 +408,7 @@ jobs:
|
||||
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-rpm-loong64.sock"
|
||||
rm -f "$VIRTIOFSD_SOCKET"
|
||||
|
||||
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" -o source="$GITHUB_WORKSPACE" -o cache=auto &
|
||||
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
|
||||
@@ -528,6 +498,48 @@ jobs:
|
||||
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 RPMs to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with: { file: "dist/rpm-loong64/**/*.rpm", tag: "${{ env.RELEASE_TAG }}", file_glob: true, prerelease: true }
|
||||
- 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 }}" }
|
||||
|
||||
@@ -12,6 +12,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -26,6 +27,7 @@ jobs:
|
||||
with:
|
||||
target: macos
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
secrets: inherit
|
||||
|
||||
dmg:
|
||||
name: package and release macOS dmg
|
||||
@@ -62,13 +64,24 @@ jobs:
|
||||
- name: Package dmg
|
||||
run: ./package-osx.sh macos-$Arch v2rayN-macos-$Arch ${{ inputs.release_tag }}
|
||||
|
||||
- name: Sleep for race condition between matrix jobs
|
||||
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')"
|
||||
|
||||
- name: Upload dmg to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
- name: Upload dmg artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
file: ${{ github.workspace }}/v2rayN*.dmg
|
||||
tag: ${{ inputs.release_tag }}
|
||||
file_glob: true
|
||||
prerelease: true
|
||||
name: release-dmg-${{ matrix.arch }}
|
||||
path: v2rayN*.dmg
|
||||
if-no-files-found: error
|
||||
|
||||
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 }}
|
||||
|
||||
@@ -12,6 +12,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -26,3 +27,4 @@ jobs:
|
||||
with:
|
||||
target: windows-desktop
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
package-x86:
|
||||
@@ -51,7 +52,7 @@ jobs:
|
||||
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
|
||||
|
||||
- name: Setup .NET 10.0.1xx
|
||||
uses: actions/setup-dotnet@v5.3.0
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: 10.0.1xx
|
||||
|
||||
@@ -145,7 +146,7 @@ jobs:
|
||||
$xrayTag = "v$xrayVer"
|
||||
$singTag = "v$singVer"
|
||||
|
||||
$xrayUrl = "https://github.com/autorepobot/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip"
|
||||
$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"
|
||||
@@ -222,10 +223,25 @@ jobs:
|
||||
-DestinationPath "${{ matrix.zip_name }}" `
|
||||
-Force
|
||||
|
||||
- name: Upload zip archive to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
- name: Upload zip artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
file: ${{ matrix.zip_name }}
|
||||
tag: ${{ inputs.release_tag }}
|
||||
prerelease: true
|
||||
overwrite: true
|
||||
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 }}
|
||||
|
||||
@@ -12,6 +12,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -27,3 +28,4 @@ jobs:
|
||||
with:
|
||||
target: windows
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -22,6 +22,7 @@ jobs:
|
||||
case(
|
||||
inputs.target == 'macos', 'macos-latest',
|
||||
inputs.target == 'linux', 'ubuntu-24.04',
|
||||
inputs.target == 'windows', 'windows-latest',
|
||||
'ubuntu-latest'
|
||||
)
|
||||
}}
|
||||
@@ -68,20 +69,27 @@ jobs:
|
||||
dotnet --list-sdks 2>$null; $LASTEXITCODE=0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5.3.0
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '10.0.1xx'
|
||||
|
||||
dotnet-version: 10.0.1xx
|
||||
|
||||
- name: Build v2rayN
|
||||
shell: bash
|
||||
working-directory: ./v2rayN
|
||||
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
|
||||
|
||||
- name: Build AmazTool
|
||||
shell: bash
|
||||
working-directory: ./v2rayN
|
||||
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
|
||||
uses: actions/upload-artifact@v7.0.1
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.arch }}
|
||||
path: ${{ matrix.arch }}
|
||||
|
||||
@@ -9,9 +9,13 @@ on:
|
||||
target: # windows linux macos windows-desktop
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
GPG_PRIVATE_KEY:
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
package:
|
||||
@@ -55,13 +59,23 @@ jobs:
|
||||
if: inputs.target == 'windows-desktop'
|
||||
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
|
||||
|
||||
- name: Sleep for race condition between matrix jobs
|
||||
run: sleep "$(od -An -N2 -tu2 /dev/urandom | awk 'NR==1{printf "%.2f", $1/5461}')"
|
||||
|
||||
- name: Upload zip archive to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
- name: Upload zip artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
file: ${{ github.workspace }}/v2rayN*.zip
|
||||
tag: ${{ inputs.release_tag }}
|
||||
file_glob: true
|
||||
prerelease: true
|
||||
name: release-${{ inputs.target }}-${{ matrix.arch }}
|
||||
path: v2rayN*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
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 }}
|
||||
|
||||
@@ -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
|
||||
@@ -1,29 +1,52 @@
|
||||
name: Code Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'v2rayN/ServiceLib/**'
|
||||
- 'v2rayN/ServiceLib.UdpTest/**'
|
||||
- 'v2rayN/ServiceLib.Tests/**'
|
||||
- 'v2rayN/Directory.Build.*'
|
||||
- 'v2rayN/Directory.Packages.props'
|
||||
- 'global.json'
|
||||
- '.github/workflows/test.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'v2rayN/ServiceLib/Services/CoreConfig/**'
|
||||
- 'v2rayN/ServiceLib/Handler/Fmt/**'
|
||||
- 'v2rayN/ServiceLib/**'
|
||||
- 'v2rayN/ServiceLib.UdpTest/**'
|
||||
- 'v2rayN/ServiceLib.Tests/**'
|
||||
- 'v2rayN/Directory.Build.*'
|
||||
- 'v2rayN/Directory.Packages.props'
|
||||
- 'global.json'
|
||||
- '.github/workflows/test.yml'
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5.3.0
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
dotnet-version: '10.x'
|
||||
dotnet-quality: 'ga'
|
||||
|
||||
- name: Test Code
|
||||
working-directory: ./v2rayN
|
||||
run: dotnet test ./ServiceLib.Tests
|
||||
run: dotnet test --project ./v2rayN/ServiceLib.Tests -c Release --results-directory ./TestResults -- --report-trx
|
||||
|
||||
- name: Comment PR with results
|
||||
if: always()
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
with:
|
||||
files: ./TestResults/*.trx
|
||||
comment_mode: failures
|
||||
|
||||
@@ -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
|
||||
@@ -23,8 +23,13 @@ jobs:
|
||||
|
||||
$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
|
||||
$arm64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-arm64\.zip' | Select -ExpandProperty browser_download_url
|
||||
$assets = $targetRelease | Select -ExpandProperty assets -First 1
|
||||
$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
|
||||
|
||||
|
||||
@@ -84,6 +84,9 @@ StyleCopReport.xml
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
bin/
|
||||
obj/
|
||||
build/
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
[submodule "v2rayN/GlobalHotKeys"]
|
||||
path = v2rayN/GlobalHotKeys
|
||||
url = https://github.com/2dust/GlobalHotKeys
|
||||
[submodule "v2rayN/NetBridge"]
|
||||
path = v2rayN/NetBridge
|
||||
url = https://github.com/2dust/NetBridge
|
||||
|
||||
@@ -1,18 +1,80 @@
|
||||
# v2rayN
|
||||
|
||||
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)
|
||||
### 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)
|
||||
|
||||
[](https://github.com/2dust/v2rayN/commits/master)
|
||||
[](https://www.codefactor.io/repository/github/2dust/v2rayn)
|
||||
[](https://github.com/2dust/v2rayN/releases)
|
||||
[](https://t.me/v2rayn)
|
||||
[](https://github.com/2dust/v2rayN/releases)
|
||||
[](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)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"test": {
|
||||
"runner": "Microsoft.Testing.Platform"
|
||||
}
|
||||
}
|
||||
+6
-19
@@ -8,13 +8,13 @@ BUILD_FROM=""
|
||||
XRAY_VER="${XRAY_VER:-}"
|
||||
SING_VER="${SING_VER:-}"
|
||||
|
||||
MIN_KERNEL="5.10"
|
||||
MIN_KERNEL="6.12"
|
||||
PKGROOT="v2rayN-publish"
|
||||
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||
OUTPUT_DIR="${HOME}/debbuild"
|
||||
DOTNET_TFM="net10.0"
|
||||
DOTNET_LOONGARCH_VERSION="10.0.109"
|
||||
DOTNET_LOONGARCH_TAG="v10.0.109-loongarch64"
|
||||
DOTNET_LOONGARCH_VERSION="10.0.111"
|
||||
DOTNET_LOONGARCH_TAG="v10.0.111-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}"
|
||||
@@ -116,20 +116,7 @@ install_dependencies() {
|
||||
|
||||
export PATH="$HOME/.dotnet:$PATH"
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -610,8 +597,8 @@ package_binary() {
|
||||
write_desktop_file "$stage"
|
||||
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"
|
||||
cat > "$workdir/debian/control" <<EOF
|
||||
Source: v2rayn
|
||||
|
||||
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
|
||||
PKGROOT="v2rayN-publish"
|
||||
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||
OUTPUT_DIR="${HOME}/debbuild"
|
||||
DOTNET_RISCV_VERSION="10.0.109"
|
||||
DOTNET_RISCV_VERSION="10.0.111"
|
||||
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
|
||||
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
|
||||
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
|
||||
@@ -595,8 +595,8 @@ package_binary() {
|
||||
write_desktop_file "$stage"
|
||||
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"
|
||||
cat > "$workdir/debian/control" <<EOF
|
||||
Source: v2rayn
|
||||
|
||||
+2
-2
@@ -609,7 +609,7 @@ package_binary() {
|
||||
write_desktop_file "$stage"
|
||||
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"
|
||||
cat > "$workdir/debian/control" <<EOF
|
||||
@@ -753,4 +753,4 @@ main() {
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
main "$@"
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.7</string>
|
||||
<string>13.6</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
+9
-22
@@ -8,12 +8,12 @@ BUILD_FROM=""
|
||||
XRAY_VER="${XRAY_VER:-}"
|
||||
SING_VER="${SING_VER:-}"
|
||||
|
||||
MIN_KERNEL="5.10"
|
||||
MIN_KERNEL="6.12"
|
||||
PKGROOT="v2rayN-publish"
|
||||
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||
RPM_TOPDIR="${HOME}/rpmbuild"
|
||||
DOTNET_LOONGARCH_VERSION="10.0.109"
|
||||
DOTNET_LOONGARCH_TAG="v10.0.109-loongarch64"
|
||||
DOTNET_LOONGARCH_VERSION="10.0.111"
|
||||
DOTNET_LOONGARCH_TAG="v10.0.111-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}"
|
||||
@@ -112,20 +112,7 @@ install_dependencies() {
|
||||
|
||||
export PATH="$HOME/.dotnet:$PATH"
|
||||
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=0
|
||||
fi
|
||||
|
||||
@@ -523,13 +510,13 @@ ExclusiveArch: loongarch64
|
||||
Source0: __PKGROOT__.tar.gz
|
||||
|
||||
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
|
||||
Requires: glibc >= 2.34
|
||||
Requires: fontconfig >= 2.13.1
|
||||
Requires: glibc >= 2.39
|
||||
Requires: fontconfig >= 2.15.0
|
||||
Requires: desktop-file-utils >= 0.26
|
||||
Requires: xdg-utils >= 1.1.3
|
||||
Requires: coreutils >= 8.32
|
||||
Requires: bash >= 5.1
|
||||
Requires: freetype >= 2.10
|
||||
Requires: coreutils >= 9.4
|
||||
Requires: bash >= 5.2.21
|
||||
Requires: freetype >= 2.13
|
||||
|
||||
%description
|
||||
v2rayN Linux for Red Hat Enterprise Linux
|
||||
|
||||
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
|
||||
PKGROOT="v2rayN-publish"
|
||||
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
|
||||
RPM_TOPDIR="${HOME}/rpmbuild"
|
||||
DOTNET_RISCV_VERSION="10.0.109"
|
||||
DOTNET_RISCV_VERSION="10.0.111"
|
||||
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
|
||||
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
|
||||
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
|
||||
@@ -509,13 +509,13 @@ ExclusiveArch: riscv64
|
||||
Source0: __PKGROOT__.tar.gz
|
||||
|
||||
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
|
||||
Requires: glibc >= 2.34
|
||||
Requires: fontconfig >= 2.13.1
|
||||
Requires: glibc >= 2.39
|
||||
Requires: fontconfig >= 2.15.0
|
||||
Requires: desktop-file-utils >= 0.26
|
||||
Requires: xdg-utils >= 1.1.3
|
||||
Requires: coreutils >= 8.32
|
||||
Requires: bash >= 5.1
|
||||
Requires: freetype >= 2.10
|
||||
Requires: coreutils >= 9.4
|
||||
Requires: bash >= 5.2.21
|
||||
Requires: freetype >= 2.13
|
||||
|
||||
%description
|
||||
v2rayN Linux for Red Hat Enterprise Linux
|
||||
|
||||
+5
-5
@@ -498,13 +498,13 @@ ExclusiveArch: aarch64 x86_64
|
||||
Source0: __PKGROOT__.tar.gz
|
||||
|
||||
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
|
||||
Requires: glibc >= 2.34
|
||||
Requires: fontconfig >= 2.13.1
|
||||
Requires: glibc >= 2.39
|
||||
Requires: fontconfig >= 2.15.0
|
||||
Requires: desktop-file-utils >= 0.26
|
||||
Requires: xdg-utils >= 1.1.3
|
||||
Requires: coreutils >= 8.32
|
||||
Requires: bash >= 5.1
|
||||
Requires: freetype >= 2.10
|
||||
Requires: coreutils >= 9.4
|
||||
Requires: bash >= 5.2.21
|
||||
Requires: freetype >= 2.13
|
||||
|
||||
%description
|
||||
v2rayN Linux for Red Hat Enterprise Linux
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>7.23.0</Version>
|
||||
<Version>7.24.9</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.17" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.17" />
|
||||
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
|
||||
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
|
||||
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
||||
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.13" />
|
||||
<PackageVersion Include="CliWrap" Version="3.10.1" />
|
||||
<PackageVersion Include="Downloader" Version="5.7.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
||||
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
||||
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
||||
<PackageVersion Include="ReactiveUI" Version="23.2.28" />
|
||||
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
|
||||
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" />
|
||||
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
|
||||
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
|
||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
|
||||
<PackageVersion Include="NLog" Version="6.1.3" />
|
||||
<PackageVersion Include="sqlite-net-e" Version="1.11.0" />
|
||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.2" />
|
||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||
<PackageVersion Include="YamlDotNet" Version="18.0.0" />
|
||||
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
|
||||
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
||||
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
|
||||
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
||||
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" />
|
||||
<PackageVersion Include="CliWrap" Version="3.10.5" />
|
||||
<PackageVersion Include="Downloader" Version="5.9.5" />
|
||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
||||
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
||||
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
||||
<PackageVersion Include="ReactiveUI" Version="24.1.0" />
|
||||
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
|
||||
<PackageVersion Include="ReactiveUI.WPF" Version="24.1.0" />
|
||||
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
|
||||
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
|
||||
<PackageVersion Include="NLog" Version="6.2.0" />
|
||||
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
|
||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
|
||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||
<PackageVersion Include="TUnit" Version="1.65.68" />
|
||||
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
|
||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
||||
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+1
-1
Submodule v2rayN/GlobalHotKeys updated: 569a95bb0f...162d401dfe
Submodule v2rayN/NetBridge deleted from e9b16cd187
@@ -1,15 +1,8 @@
|
||||
using AwesomeAssertions;
|
||||
using ServiceLib.Enums;
|
||||
using ServiceLib.Handler.Builder;
|
||||
using ServiceLib.Helper;
|
||||
using ServiceLib.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace ServiceLib.Tests.CoreConfig.Context;
|
||||
|
||||
public class CoreConfigContextBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task ResolveNodeAsync_DirectCycleDependency_ShouldFailWithCycleError()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
@@ -27,13 +20,13 @@ public class CoreConfigContextBuilderTests
|
||||
|
||||
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||
|
||||
validatorResult.Success.Should().BeFalse();
|
||||
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
await validatorResult.Success.Should().BeFalse();
|
||||
await validatorResult.Errors.Should().Contain(ContainsCycleDependencyMessage);
|
||||
await context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
|
||||
await context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task ResolveNodeAsync_IndirectCycleDependency_ShouldFailWithCycleError()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
@@ -53,14 +46,14 @@ public class CoreConfigContextBuilderTests
|
||||
|
||||
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||
|
||||
validatorResult.Success.Should().BeFalse();
|
||||
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupC.IndexId);
|
||||
await validatorResult.Success.Should().BeFalse();
|
||||
await validatorResult.Errors.Should().Contain(ContainsCycleDependencyMessage);
|
||||
await context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
|
||||
await context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
await context.AllProxiesMap.Should().NotContainKey(groupC.IndexId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Test]
|
||||
public async Task ResolveNodeAsync_CycleWithValidBranch_ShouldSkipCycleAndKeepValidChild()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
@@ -80,14 +73,14 @@ public class CoreConfigContextBuilderTests
|
||||
|
||||
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||
|
||||
validatorResult.Success.Should().BeTrue();
|
||||
validatorResult.Errors.Should().BeEmpty();
|
||||
validatorResult.Warnings.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||
await validatorResult.Success.Should().BeTrue();
|
||||
await validatorResult.Errors.Should().BeEmpty();
|
||||
await validatorResult.Warnings.Should().Contain(ContainsCycleDependencyMessage);
|
||||
|
||||
context.AllProxiesMap.Should().ContainKey(leaf.IndexId);
|
||||
context.AllProxiesMap.Should().ContainKey(groupA.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
groupA.GetProtocolExtra().ChildItems.Should().Be(leaf.IndexId);
|
||||
await context.AllProxiesMap.Should().ContainKey(leaf.IndexId);
|
||||
await context.AllProxiesMap.Should().ContainKey(groupA.IndexId);
|
||||
await context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
await groupA.GetProtocolExtra().ChildItems.Should().BeEqualTo(leaf.IndexId);
|
||||
}
|
||||
|
||||
private static string NewId(string prefix)
|
||||
|
||||
@@ -59,7 +59,7 @@ internal static class CoreConfigTestFactory
|
||||
},
|
||||
WebDavItem = new WebDavItem(),
|
||||
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 =
|
||||
[
|
||||
new InItem
|
||||
@@ -84,6 +84,7 @@ internal static class CoreConfigTestFactory
|
||||
ParallelQuery = false,
|
||||
Strategy4Freedom = Global.AsIs,
|
||||
Strategy4Proxy = Global.AsIs,
|
||||
Strategy4ProxyDial = Global.AsIs,
|
||||
},
|
||||
IndexId = 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,
|
||||
IEnumerable<string> childIndexIds)
|
||||
{
|
||||
@@ -214,4 +251,12 @@ internal static class CoreConfigTestFactory
|
||||
config.TunModeItem.RouteExcludeAddress = ["10.0.0.1/32", "192.168.1.0/24", "fc00::/7"];
|
||||
return config;
|
||||
}
|
||||
|
||||
public static Config CreateConfigWithTun(ECoreType coreType, bool enableIPv6Address)
|
||||
{
|
||||
var config = CreateConfig(coreType);
|
||||
config.TunModeItem.EnableTun = true;
|
||||
config.TunModeItem.EnableIPv6Address = enableIPv6Address;
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
using AwesomeAssertions;
|
||||
using ServiceLib.Common;
|
||||
using ServiceLib.Enums;
|
||||
using ServiceLib.Manager;
|
||||
using ServiceLib.Models;
|
||||
using ServiceLib.Services.CoreConfig;
|
||||
using Xunit;
|
||||
|
||||
namespace ServiceLib.Tests.CoreConfig.Singbox;
|
||||
|
||||
public class CoreConfigSingboxServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -20,17 +12,17 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
result.Data.Should().NotBeNull();
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
await result.Data.Should().NotBeNull();
|
||||
|
||||
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
|
||||
singboxConfig.Should().NotBeNull();
|
||||
singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
|
||||
singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed));
|
||||
await singboxConfig.Should().NotBeNull();
|
||||
await singboxConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
|
||||
await singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -44,18 +36,100 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.inbounds.Should().Contain(i =>
|
||||
await cfg.inbounds.Should().Contain(i =>
|
||||
i.type == nameof(EInboundProtocol.mixed)
|
||||
&& i.listen == Global.Loopback
|
||||
&& i.listen_port == AppManager.Instance.GetLocalPort(EInboundProtocol.socks));
|
||||
cfg.inbounds.Should().Contain(i => i.type == "tun");
|
||||
await cfg.inbounds.Should().Contain(i => i.type == "tun");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules()
|
||||
{
|
||||
// The embedded tun rules reject local-network noise (NetBIOS/mDNS, multicast).
|
||||
// They are deserialized into List<Rule4Sbox>, so a schema mismatch in the
|
||||
// embedded template makes JsonUtils.Deserialize return null and silently
|
||||
// drops every one of them.
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
config.TunModeItem.EnableTun = 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();
|
||||
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
await cfg.route.rules.Should().Contain(
|
||||
r => r.action == "reject"
|
||||
&& r.network != null && r.network.Contains("udp")
|
||||
&& r.port != null && r.port.Contains(5353),
|
||||
"the embedded tun rules must reject mDNS/NetBIOS noise");
|
||||
await cfg.route.rules.Should().Contain(
|
||||
r => r.action == "reject"
|
||||
&& r.ip_cidr != null && r.ip_cidr.Contains("224.0.0.0/3"),
|
||||
"the embedded tun rules must reject multicast traffic");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses()
|
||||
{
|
||||
// Regression test: traffic addressed to the TUN interface's own addresses must
|
||||
// never reach an outbound. auto_route hijacks the default route, so `direct`
|
||||
// writes such a packet straight back into the TUN, which routes it to the
|
||||
// outbound again - an infinite loop that pins a CPU core. Observed in the wild
|
||||
// with WebRTC ICE connectivity checks against the TUN's own fc00::/7 ULA
|
||||
// address, sustaining ~8k packets/s out of the TUN interface.
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
config.TunModeItem.EnableTun = true;
|
||||
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();
|
||||
|
||||
await result.Success.Should().BeTrue().Because($"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();
|
||||
await tun.address.Should().NotBeNull();
|
||||
await tun.address.Should().NotBeEmpty();
|
||||
|
||||
foreach (var address in tun.address!)
|
||||
{
|
||||
var self = IPAddress.Parse(address.Split('/').First());
|
||||
var hostBits = self.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
|
||||
var expected = $"{self}/{hostBits}";
|
||||
await cfg.route.rules.Should().Contain(
|
||||
r => r.action == "reject" && r.ip_cidr != null && r.ip_cidr.Contains(expected),
|
||||
$"traffic to the TUN's own address '{address}' must be rejected, not routed");
|
||||
}
|
||||
|
||||
// The match has to stay on the addresses themselves. sing-tun derives the TUN's DNS
|
||||
// entry from the address right after the interface's own, and every prefix offered
|
||||
// here leaves room for it, so a prefix match would drop system name lookups too.
|
||||
var dropRule = cfg.route.rules.First(r =>
|
||||
r.action == "reject" && r.method == "drop" && r.ip_cidr?.Count > 0);
|
||||
//dropRule.ip_cidr!.Should().OnlyContain(c =>
|
||||
// c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
|
||||
await dropRule.ip_cidr.Should().All(c => c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
config.CoreBasicItem.BindInterface = "eth0";
|
||||
@@ -69,16 +143,16 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
|
||||
|
||||
proxy.bind_interface.Should().Be("eth0");
|
||||
proxy.detour.Should().BeNullOrEmpty();
|
||||
await proxy.bind_interface.Should().BeEqualTo("eth0");
|
||||
await proxy.detour.Should().BeNull().Or.BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -95,17 +169,17 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
|
||||
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
|
||||
await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_ProxyChain_ShouldBuildDetourChain()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -122,18 +196,18 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||
cfg.outbounds.Should().Contain(o =>
|
||||
await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "socks");
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o =>
|
||||
o.tag == Global.ProxyTag &&
|
||||
(o.detour ?? string.Empty).StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -155,18 +229,18 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
|
||||
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
|
||||
await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -188,25 +262,25 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
|
||||
cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.type == "selector");
|
||||
await cfg.outbounds.Should().Contain(o => o.tag == $"{Global.ProxyTag}-auto" && o.type == "urltest");
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal));
|
||||
|
||||
var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal));
|
||||
proxyCloneCount.Should().Be(2);
|
||||
await proxyCloneCount.Should().BeEqualTo(2);
|
||||
|
||||
var allCloneDetoursPointToGroupBranches = cfg.outbounds
|
||||
.Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal))
|
||||
.All(o => (o.detour ?? string.Empty).StartsWith("chain-proxy-1-group-", StringComparison.Ordinal));
|
||||
allCloneDetoursPointToGroupBranches.Should().BeTrue();
|
||||
await allCloneDetoursPointToGroupBranches.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -242,24 +316,24 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
var hasDirectRule = cfg.route.rules.Any(r =>
|
||||
r.domain != null
|
||||
&& r.domain.Contains("direct.example.com")
|
||||
&& r.outbound == Global.DirectTag);
|
||||
hasDirectRule.Should().BeTrue();
|
||||
await hasDirectRule.Should().BeTrue();
|
||||
|
||||
var hasBlockRule = cfg.route.rules.Any(r =>
|
||||
r.domain != null
|
||||
&& r.domain.Contains("block.example.com")
|
||||
&& r.action == "reject");
|
||||
hasBlockRule.Should().BeTrue();
|
||||
await hasBlockRule.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -291,21 +365,21 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}";
|
||||
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal));
|
||||
|
||||
var hasRouteRule = cfg.route.rules.Any(r =>
|
||||
r.domain != null
|
||||
&& r.domain.Contains("route.example.com")
|
||||
&& (r.outbound ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal));
|
||||
hasRouteRule.Should().BeTrue();
|
||||
await hasRouteRule.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyGeoipAndCidrToDirectDnsRule()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(
|
||||
ECoreType.sing_box,
|
||||
@@ -336,7 +410,7 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
var hasExpectedRule = cfg.dns.rules?.Any(r =>
|
||||
@@ -345,11 +419,11 @@ public class CoreConfigSingboxServiceTests
|
||||
&& r.rule_set?.Contains("geosite-cn") == true
|
||||
&& r.rule_set?.Contains("geoip-cn") == true) ?? false;
|
||||
|
||||
hasExpectedRule.Should().BeTrue();
|
||||
await hasExpectedRule.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_BootstrapDNS_ShouldConfigurePureIPResolver()
|
||||
{
|
||||
var bootstrapDns = "8.8.8.8";
|
||||
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.sing_box, bootstrapDns);
|
||||
@@ -360,17 +434,17 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
config.SimpleDNSItem.BootstrapDNS.Should().Be(bootstrapDns);
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
await config.SimpleDNSItem.BootstrapDNS.Should().BeEqualTo(bootstrapDns);
|
||||
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
var bootstrapServer = cfg.dns.servers?.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag);
|
||||
bootstrapServer.Should().NotBeNull();
|
||||
(bootstrapServer?.server ?? string.Empty).Should().Contain(bootstrapDns);
|
||||
var bootstrapServer = cfg.dns?.servers.FirstOrDefault(s => s.tag == Global.SingboxLocalDNSTag);
|
||||
await bootstrapServer.Should().NotBeNull();
|
||||
await bootstrapServer!.server.Should().Contain(bootstrapDns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectFinalDns()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
config.SimpleDNSItem.DirectDNS = "1.1.1.1";
|
||||
@@ -403,14 +477,14 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.dns.final.Should().Be(Global.SingboxDirectDNSTag);
|
||||
await cfg.dns.final.Should().BeEqualTo(Global.SingboxDirectDNSTag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedRule()
|
||||
{
|
||||
var config =
|
||||
CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.sing_box, "192.168.0.0/16,geoip:cn");
|
||||
@@ -440,21 +514,21 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
var hasExpectedRule = cfg.dns.rules?.Any(r =>
|
||||
r.server == Global.SingboxDirectDNSTag
|
||||
&& r.ip_cidr?.Contains("192.168.0.0/16") == true
|
||||
&& r.rule_set?.Contains("geoip-cn") == true) ?? false;
|
||||
hasExpectedRule.Should().BeFalse();
|
||||
await hasExpectedRule.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("geosite:cn", "geosite-cn")]
|
||||
[InlineData("geosite:geolocation-cn", "geosite-geolocation-cn")]
|
||||
[InlineData("geosite:tld-cn", "geosite-tld-cn")]
|
||||
public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag,
|
||||
[Test]
|
||||
[Arguments("geosite:cn", "geosite-cn")]
|
||||
[Arguments("geosite:geolocation-cn", "geosite-geolocation-cn")]
|
||||
[Arguments("geosite:tld-cn", "geosite-tld-cn")]
|
||||
public async Task GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedRule(string domainTag,
|
||||
string expectedRuleSetTag)
|
||||
{
|
||||
var config =
|
||||
@@ -482,7 +556,7 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
var hasExpectedRule = cfg.dns.rules?.Any(r =>
|
||||
@@ -490,11 +564,11 @@ public class CoreConfigSingboxServiceTests
|
||||
&& r.ip_cidr?.Contains("192.168.0.0/16") == true
|
||||
&& r.rule_set?.Contains(expectedRuleSetTag) == true
|
||||
&& r.rule_set?.Contains("geoip-cn") == true) ?? false;
|
||||
hasExpectedRule.Should().BeTrue();
|
||||
await hasExpectedRule.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_Hosts_ShouldPopulateHostsServerAndDomainResolver()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
|
||||
@@ -506,21 +580,21 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
var hostsServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxHostsDNSTag);
|
||||
hostsServer.Should().NotBeNull();
|
||||
hostsServer!.predefined.Should().ContainKey("resolver.example");
|
||||
hostsServer.predefined!["resolver.example"].Should().Contain("1.1.1.1");
|
||||
await hostsServer.Should().NotBeNull();
|
||||
await hostsServer!.predefined.Should().ContainKey("resolver.example");
|
||||
await hostsServer.predefined!["resolver.example"].Should().Contain("1.1.1.1");
|
||||
|
||||
var directServer = cfg.dns.servers.FirstOrDefault(s => s.tag == Global.SingboxDirectDNSTag);
|
||||
directServer.Should().NotBeNull();
|
||||
directServer!.domain_resolver.Should().Be(Global.SingboxHostsDNSTag);
|
||||
await directServer.Should().NotBeNull();
|
||||
await directServer!.domain_resolver.Should().BeEqualTo(Global.SingboxHostsDNSTag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsAndInjectLocalResolver()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -549,12 +623,116 @@ public class CoreConfigSingboxServiceTests
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8");
|
||||
cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
|
||||
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
|
||||
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
|
||||
await cfg.dns.servers.Should().Contain(s => s.tag == "remote" && s.type == "udp" && s.server == "8.8.8.8");
|
||||
await cfg.dns.servers.Should().Contain(s => s.tag == Global.SingboxLocalDNSTag);
|
||||
await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
|
||||
await cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task 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 _);
|
||||
await 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();
|
||||
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
|
||||
|
||||
await proxy.type.Should().BeEqualTo("hysteria2");
|
||||
await proxy.realm.Should().NotBeNull();
|
||||
await proxy.realm!.server_url.Should().StartWith("https://");
|
||||
await proxy.realm.server_url.Should().Contain("realm.hy2.io");
|
||||
await proxy.realm.token.Should().BeEqualTo("public");
|
||||
await proxy.realm.realm_id.Should().BeEqualTo("my-realm-id");
|
||||
await proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
|
||||
await proxy.server.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task 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();
|
||||
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
var tun = cfg.inbounds.First(i => i.type == "tun");
|
||||
|
||||
await tun.address.Should().NotBeNull();
|
||||
await tun.address.Should().NotBeEmpty();
|
||||
foreach (var address in tun.address!)
|
||||
{
|
||||
var prefixLength = int.Parse(address[(address.LastIndexOf('/') + 1)..]);
|
||||
var isIpv6 = address.Contains(':');
|
||||
await prefixLength.Should().BeLessThanOrEqualTo(isIpv6 ? 126 : 30,
|
||||
$"'{address}' must leave room for the peer address the system stack derives");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public async Task 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();
|
||||
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
await result.Data.Should().NotBeNull();
|
||||
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
|
||||
await cfg.Should().NotBeNull();
|
||||
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
|
||||
await proxyOutbound.Should().NotBeNull();
|
||||
await proxyOutbound!.type.Should().BeEqualTo("shadowsocks");
|
||||
await proxyOutbound.server.Should().BeEqualTo("1.2.3.4");
|
||||
await proxyOutbound.server_port.Should().BeEqualTo(8388);
|
||||
await proxyOutbound.method.Should().BeEqualTo("aes-128-gcm");
|
||||
await proxyOutbound.password.Should().BeEqualTo("custom_password");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
using AwesomeAssertions;
|
||||
using ServiceLib.Common;
|
||||
using ServiceLib.Enums;
|
||||
using ServiceLib.Models;
|
||||
using ServiceLib.Services.CoreConfig;
|
||||
using Xunit;
|
||||
|
||||
namespace ServiceLib.Tests.CoreConfig.V2ray;
|
||||
|
||||
public class CoreConfigV2rayServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -19,17 +12,50 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Data.Should().NotBeNull();
|
||||
await result.Success.Should().BeTrue();
|
||||
await result.Data.Should().NotBeNull();
|
||||
|
||||
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
|
||||
v2rayConfig.Should().NotBeNull();
|
||||
v2rayConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.protocol == "vmess");
|
||||
v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
|
||||
await v2rayConfig.Should().NotBeNull();
|
||||
await v2rayConfig!.outbounds.Should().Contain(o => o.tag == Global.ProxyTag && o.protocol == "vmess");
|
||||
await v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
|
||||
[Test]
|
||||
public async Task 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();
|
||||
|
||||
await 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");
|
||||
|
||||
await outbound.settings.address!.ToString().Should().BeEqualTo("proxy.example.com");
|
||||
await outbound.settings.port.Should().BeEqualTo(8080);
|
||||
await outbound.settings.user.Should().BeEqualTo("user");
|
||||
await outbound.settings.pass.Should().BeEqualTo("pass");
|
||||
await outbound.settings.level.Should().BeEqualTo(1);
|
||||
await outbound.settings.headers.Should().NotBeNull();
|
||||
var headers = JsonUtils.ParseJson(outbound.settings.headers!.ToString());
|
||||
await headers["User-Agent"]!.GetValue<string>().Should().BeEqualTo("v2rayN");
|
||||
await headers["Set-Cookie"]!.AsArray()
|
||||
.Select(item => item!.GetValue<string>())
|
||||
.Should().BeEquivalentTo(["a=1", "b=2"]);
|
||||
await outbound.settings.servers.Should().BeNull();
|
||||
await outbound.settings.vnext.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -46,17 +72,17 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||
cfg.routing.balancers.Should().NotBeNull();
|
||||
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||
await cfg.routing.balancers.Should().NotBeNull();
|
||||
await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_ProxyChain_ShouldBuildDialerProxyChain()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_ProxyChain_ShouldBuildDialerProxyChain()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -72,21 +98,21 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||
var hasDialerChain = cfg.outbounds.Any(o =>
|
||||
o.tag == Global.ProxyTag
|
||||
&& o.streamSettings is not null
|
||||
&& o.streamSettings.sockopt is not null
|
||||
&& (o.streamSettings.sockopt.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-",
|
||||
StringComparison.Ordinal));
|
||||
hasDialerChain.Should().BeTrue();
|
||||
await hasDialerChain.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_PolicyGroupWithProxyChain_ShouldBuildCombinedOutbounds()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -107,18 +133,18 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||
cfg.routing.balancers.Should().NotBeNull();
|
||||
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-1-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("proxy-2-", StringComparison.Ordinal));
|
||||
await cfg.routing.balancers.Should().NotBeNull();
|
||||
await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_ProxyChainWithPolicyGroup_ShouldBuildClonedChainBranches()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -140,27 +166,27 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-1-", StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith("chain-proxy-1-group-2-", StringComparison.Ordinal));
|
||||
|
||||
var proxyCloneCount = cfg.outbounds.Count(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal));
|
||||
proxyCloneCount.Should().Be(2);
|
||||
await proxyCloneCount.Should().BeEqualTo(2);
|
||||
|
||||
var allCloneDialersPointToGroupBranches = cfg.outbounds
|
||||
.Where(o => o.tag.StartsWith("proxy-clone-", StringComparison.Ordinal))
|
||||
.All(o => (o.streamSettings?.sockopt?.dialerProxy ?? string.Empty).StartsWith("chain-proxy-1-group-",
|
||||
StringComparison.Ordinal));
|
||||
allCloneDialersPointToGroupBranches.Should().BeTrue();
|
||||
await allCloneDialersPointToGroupBranches.Should().BeTrue();
|
||||
|
||||
cfg.routing.balancers.Should().NotBeNull();
|
||||
cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
|
||||
await cfg.routing.balancers.Should().NotBeNull();
|
||||
await cfg.routing.balancers!.Should().Contain(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_RoutingSplit_DirectAndBlock_ShouldApplyRules()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -196,24 +222,24 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
|
||||
var hasDirectRule = cfg.routing.rules.Any(r =>
|
||||
r.domain != null
|
||||
&& r.domain.Contains("full:direct.example.com")
|
||||
&& r.outboundTag == Global.DirectTag);
|
||||
hasDirectRule.Should().BeTrue();
|
||||
await hasDirectRule.Should().BeTrue();
|
||||
|
||||
var hasBlockRule = cfg.routing.rules.Any(r =>
|
||||
r.domain != null
|
||||
&& r.domain.Contains("full:block.example.com")
|
||||
&& r.outboundTag == Global.BlockTag);
|
||||
hasBlockRule.Should().BeTrue();
|
||||
await hasBlockRule.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_RoutingSplit_ByRemark_ShouldGenerateTargetOutbound()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -245,20 +271,20 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var expectedPrefix = $"{routeNode.IndexId}-{Global.ProxyTag}-{routeNode.Remarks}";
|
||||
|
||||
cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal));
|
||||
await cfg.outbounds.Should().Contain(o => o.tag.StartsWith(expectedPrefix, StringComparison.Ordinal));
|
||||
var hasRouteRule = cfg.routing.rules.Any(r =>
|
||||
r.domain != null
|
||||
&& r.domain.Contains("full:route.example.com")
|
||||
&& (r.outboundTag ?? string.Empty).StartsWith(expectedPrefix, StringComparison.Ordinal));
|
||||
hasRouteRule.Should().BeTrue();
|
||||
await hasRouteRule.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyExpectedIPsToDirectDnsServer()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_DirectExpectedIPs_ShouldApplyExpectedIPsToDirectDnsServer()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -287,7 +313,7 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||
|
||||
@@ -302,11 +328,11 @@ public class CoreConfigV2rayServiceTests
|
||||
&& s.domains?.Contains("geosite:cn") == true
|
||||
&& s.expectedIPs?.Contains("192.168.0.0/16") == true
|
||||
&& s.expectedIPs?.Contains("geoip:cn") == true);
|
||||
hasExpectedServer.Should().BeTrue();
|
||||
await hasExpectedServer.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_BootstrapDNS_ShouldApplyToDnsServerDomains()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_BootstrapDNS_ShouldApplyToDnsServerDomains()
|
||||
{
|
||||
var bootstrapDns = "8.8.8.8";
|
||||
var config = CoreConfigTestFactory.CreateConfigWithBootstrapDNS(ECoreType.Xray, bootstrapDns);
|
||||
@@ -319,7 +345,7 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||
|
||||
@@ -333,11 +359,11 @@ public class CoreConfigV2rayServiceTests
|
||||
s.address == bootstrapDns
|
||||
&& s.domains?.Contains("full:dns-direct.example") == true
|
||||
&& s.domains?.Contains("full:dns-remote.example") == true);
|
||||
hasBootstrapServer.Should().BeTrue();
|
||||
await hasBootstrapServer.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectDnsServers()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_DnsFallback_LastRuleDirect_ShouldUseDirectDnsServers()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
config.SimpleDNSItem.DirectDNS = "1.1.1.1";
|
||||
@@ -370,7 +396,7 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||
var dnsServers = dns.servers
|
||||
@@ -382,14 +408,14 @@ public class CoreConfigV2rayServiceTests
|
||||
var hasDirectFallback = dnsServers.Any(s =>
|
||||
(s.tag ?? string.Empty).StartsWith(Global.DirectDnsTag, StringComparison.Ordinal)
|
||||
&& s.address == "1.1.1.1");
|
||||
hasDirectFallback.Should().BeTrue();
|
||||
await hasDirectFallback.Should().BeTrue();
|
||||
|
||||
var hasRemoteFallback = dnsServers.Any(s => s.address == "9.9.9.9");
|
||||
hasRemoteFallback.Should().BeFalse();
|
||||
await hasRemoteFallback.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedIPs()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_DirectExpectedIPs_NonMatchingRegion_ShouldNotApplyExpectedIPs()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -418,7 +444,7 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||
var dnsServers = dns.servers
|
||||
@@ -430,14 +456,14 @@ public class CoreConfigV2rayServiceTests
|
||||
var hasExpectedIPs = dnsServers.Any(s =>
|
||||
s.expectedIPs?.Contains("192.168.0.0/16") == true
|
||||
|| s.expectedIPs?.Contains("geoip:cn") == true);
|
||||
hasExpectedIPs.Should().BeFalse();
|
||||
await hasExpectedIPs.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("geosite:cn")]
|
||||
[InlineData("geosite:geolocation-cn")]
|
||||
[InlineData("geosite:tld-cn")]
|
||||
public void GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedIPs(string domainTag)
|
||||
[Test]
|
||||
[Arguments("geosite:cn")]
|
||||
[Arguments("geosite:geolocation-cn")]
|
||||
[Arguments("geosite:tld-cn")]
|
||||
public async Task GenerateClientConfigContent_DirectExpectedIPs_RegionVariant_ShouldApplyExpectedIPs(string domainTag)
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithDirectExpectedIPs(ECoreType.Xray, "192.168.0.0/16,geoip:cn");
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -463,7 +489,7 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||
var dnsServers = dns.servers
|
||||
@@ -477,11 +503,11 @@ public class CoreConfigV2rayServiceTests
|
||||
&& s.domains?.Contains(domainTag) == true
|
||||
&& s.expectedIPs?.Contains("192.168.0.0/16") == true
|
||||
&& s.expectedIPs?.Contains("geoip:cn") == true);
|
||||
hasExpectedServer.Should().BeTrue();
|
||||
await hasExpectedServer.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_Hosts_ShouldPopulateDnsHosts()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_Hosts_ShouldPopulateDnsHosts()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
config.SimpleDNSItem.Hosts = "resolver.example 1.1.1.1";
|
||||
@@ -492,17 +518,17 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||
|
||||
dns.hosts.Should().NotBeNull();
|
||||
dns.hosts!.Should().ContainKey("resolver.example");
|
||||
JsonUtils.Serialize(dns.hosts!["resolver.example"]).Should().Contain("1.1.1.1");
|
||||
await dns.hosts.Should().NotBeNull();
|
||||
await dns.hosts!.Should().ContainKey("resolver.example");
|
||||
await JsonUtils.Serialize(dns.hosts!["resolver.example"]).Should().Contain("1.1.1.1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig()
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_RawDnsEnabled_ShouldUseCustomDnsConfig()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -523,22 +549,67 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(cfg.dns))!;
|
||||
|
||||
JsonUtils.Serialize(dns.servers).Should().Contain("8.8.8.8");
|
||||
dns.hosts.Should().NotBeNull();
|
||||
dns.hosts!.Should().ContainKey("raw.example");
|
||||
JsonUtils.Serialize(dns.hosts!["raw.example"]).Should().Contain("1.1.1.1");
|
||||
await JsonUtils.Serialize(dns.servers).Should().Contain("8.8.8.8");
|
||||
await dns.hosts.Should().NotBeNull();
|
||||
await dns.hosts!.Should().ContainKey("raw.example");
|
||||
await JsonUtils.Serialize(dns.hosts!["raw.example"]).Should().Contain("1.1.1.1");
|
||||
|
||||
var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom");
|
||||
directOutbound.Should().NotBeNull();
|
||||
directOutbound!.settings.domainStrategy.Should().Be("UseIPv4");
|
||||
await directOutbound.Should().NotBeNull();
|
||||
await directOutbound!.streamSettings.sockopt!.domainStrategy.Should().BeEqualTo("UseIPv4");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_TunRouteExcludeAddress()
|
||||
[Test]
|
||||
[Arguments(false)]
|
||||
[Arguments(true)]
|
||||
public async Task GenerateClientConfigContent_Tun_ShouldRouteIPv6IntoTunnel(bool enableIPv6Address)
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||
|
||||
await tunInbound.Should().NotBeNull();
|
||||
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("0.0.0.0/0");
|
||||
await tunInbound.settings.autoSystemRoutingTable.Should().Contain("::/0");
|
||||
|
||||
// EnableIPv6Address governs the interface address only, never the routing table.
|
||||
await tunInbound.settings.gateway.Should().HaveCount(enableIPv6Address ? 2 : 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldIncludeIPv6Ranges()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
|
||||
config.TunModeItem.EnableIPv6Address = false;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||
|
||||
await tunInbound.Should().NotBeNull();
|
||||
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain(x => x.Contains(':'));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunRouteExcludeAddress()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTunRouteExcludeAddress(ECoreType.Xray);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
@@ -548,15 +619,54 @@ public class CoreConfigV2rayServiceTests
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
await result.Success.Should().BeTrue();
|
||||
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||
|
||||
tunInbound.Should().NotBeNull();
|
||||
await tunInbound.Should().NotBeNull();
|
||||
|
||||
tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0");
|
||||
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
|
||||
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
|
||||
await tunInbound!.settings.autoSystemRoutingTable.Should().NotContain("0.0.0.0/0");
|
||||
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
|
||||
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task 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();
|
||||
|
||||
await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}");
|
||||
await result.Data.Should().NotBeNull();
|
||||
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
|
||||
await cfg.Should().NotBeNull();
|
||||
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
|
||||
await proxyOutbound.Should().NotBeNull();
|
||||
await proxyOutbound!.protocol.Should().BeEqualTo("shadowsocks");
|
||||
await proxyOutbound.settings.servers.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +1,354 @@
|
||||
using AwesomeAssertions;
|
||||
using ServiceLib.Enums;
|
||||
using ServiceLib.Handler.Fmt;
|
||||
using ServiceLib.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace ServiceLib.Tests.Fmt;
|
||||
|
||||
public class FmtHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields()
|
||||
/// <summary>
|
||||
/// One profile factory per protocol that <see cref="FmtHandler.GetShareUri" /> can export.
|
||||
/// The suite below asserts that this map and <see cref="Global.ProtocolShares" /> agree, so a
|
||||
/// newly exportable protocol cannot be added without a round-trip case.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<EConfigType, Func<ProfileItem>> ShareProfileFactories = new()
|
||||
{
|
||||
[EConfigType.VMess] = CreateVmessProfile,
|
||||
[EConfigType.Shadowsocks] = CreateShadowsocksProfile,
|
||||
[EConfigType.SOCKS] = CreateSocksProfile,
|
||||
[EConfigType.VLESS] = CreateVlessProfile,
|
||||
[EConfigType.Trojan] = CreateTrojanProfile,
|
||||
[EConfigType.Hysteria2] = CreateHysteria2Profile,
|
||||
[EConfigType.TUIC] = CreateTuicProfile,
|
||||
[EConfigType.WireGuard] = CreateWireguardProfile,
|
||||
[EConfigType.Anytls] = CreateAnytlsProfile,
|
||||
[EConfigType.Naive] = () => CreateNaiveProfile(false),
|
||||
};
|
||||
|
||||
[Test]
|
||||
public async Task ShareUriSuite_ShouldCoverAndRoundTripEveryExportableProtocol()
|
||||
{
|
||||
var uncovered = string.Join(", ", Global.ProtocolShares.Keys.Except(ShareProfileFactories.Keys));
|
||||
var unexpected = string.Join(", ", ShareProfileFactories.Keys.Except(Global.ProtocolShares.Keys));
|
||||
|
||||
await uncovered.Should().BeEqualTo(string.Empty);
|
||||
await unexpected.Should().BeEqualTo(string.Empty);
|
||||
|
||||
foreach (var (configType, factory) in ShareProfileFactories)
|
||||
{
|
||||
var source = factory();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
await resolved.ConfigType.Should().BeEqualTo(configType);
|
||||
await resolved.Address.Should().BeEqualTo(source.Address);
|
||||
await resolved.Port.Should().BeEqualTo(source.Port);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields()
|
||||
{
|
||||
var source = CreateVmessProfile();
|
||||
|
||||
var resolved = ExportThenImport(source);
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
resolved.ConfigType.Should().Be(EConfigType.VMess);
|
||||
resolved.Remarks.Should().Be(source.Remarks);
|
||||
resolved.Address.Should().Be(source.Address);
|
||||
resolved.Port.Should().Be(source.Port);
|
||||
resolved.Password.Should().Be(source.Password);
|
||||
resolved.GetProtocolExtra().AlterId.Should().Be(source.GetProtocolExtra().AlterId);
|
||||
await resolved.ConfigType.Should().BeEqualTo(EConfigType.VMess);
|
||||
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
|
||||
await resolved.Address.Should().BeEqualTo(source.Address);
|
||||
await resolved.Port.Should().BeEqualTo(source.Port);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.GetProtocolExtra().AlterId.Should().BeEqualTo(source.GetProtocolExtra().AlterId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetShareUriAndResolveConfig_Vless_ShouldRoundTripBasicFields()
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Vless_ShouldRoundTripBasicFields()
|
||||
{
|
||||
var source = CreateVlessProfile();
|
||||
|
||||
var resolved = ExportThenImport(source);
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
resolved.ConfigType.Should().Be(EConfigType.VLESS);
|
||||
resolved.Remarks.Should().Be(source.Remarks);
|
||||
resolved.Address.Should().Be(source.Address);
|
||||
resolved.Port.Should().Be(source.Port);
|
||||
resolved.Password.Should().Be(source.Password);
|
||||
resolved.GetProtocolExtra().VlessEncryption.Should().Be(Global.None);
|
||||
await resolved.ConfigType.Should().BeEqualTo(EConfigType.VLESS);
|
||||
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
|
||||
await resolved.Address.Should().BeEqualTo(source.Address);
|
||||
await resolved.Port.Should().BeEqualTo(source.Port);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.GetProtocolExtra().VlessEncryption.Should().BeEqualTo(Global.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetShareUriAndResolveConfig_Shadowsocks_ShouldRoundTripBasicFields()
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Shadowsocks_ShouldRoundTripBasicFields()
|
||||
{
|
||||
var source = CreateShadowsocksProfile();
|
||||
|
||||
var resolved = ExportThenImport(source);
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
resolved.ConfigType.Should().Be(EConfigType.Shadowsocks);
|
||||
resolved.Remarks.Should().Be(source.Remarks);
|
||||
resolved.Address.Should().Be(source.Address);
|
||||
resolved.Port.Should().Be(source.Port);
|
||||
resolved.Password.Should().Be(source.Password);
|
||||
resolved.GetProtocolExtra().SsMethod.Should().Be(source.GetProtocolExtra().SsMethod);
|
||||
await resolved.ConfigType.Should().BeEqualTo(EConfigType.Shadowsocks);
|
||||
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
|
||||
await resolved.Address.Should().BeEqualTo(source.Address);
|
||||
await resolved.Port.Should().BeEqualTo(source.Port);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.GetProtocolExtra().SsMethod.Should().BeEqualTo(source.GetProtocolExtra().SsMethod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetShareUriAndResolveConfig_Socks_ShouldRoundTripBasicFields()
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Socks_ShouldRoundTripBasicFields()
|
||||
{
|
||||
var source = CreateSocksProfile();
|
||||
|
||||
var resolved = ExportThenImport(source);
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
resolved.ConfigType.Should().Be(EConfigType.SOCKS);
|
||||
resolved.Remarks.Should().Be(source.Remarks);
|
||||
resolved.Address.Should().Be(source.Address);
|
||||
resolved.Port.Should().Be(source.Port);
|
||||
resolved.Username.Should().Be(source.Username);
|
||||
resolved.Password.Should().Be(source.Password);
|
||||
await resolved.ConfigType.Should().BeEqualTo(EConfigType.SOCKS);
|
||||
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
|
||||
await resolved.Address.Should().BeEqualTo(source.Address);
|
||||
await resolved.Port.Should().BeEqualTo(source.Port);
|
||||
await resolved.Username.Should().BeEqualTo(source.Username);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveConfig_UnsupportedProtocol_ShouldReturnNull()
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Trojan_ShouldRoundTripBasicFields()
|
||||
{
|
||||
var source = CreateTrojanProfile();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await AssertRawTransportFields(source, resolved);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.Sni.Should().BeEqualTo(source.Sni);
|
||||
await resolved.GetProtocolExtra().Flow.Should().BeEqualTo(source.GetProtocolExtra().Flow);
|
||||
await resolved.GetAllowInsecure().Should().BeTrue();
|
||||
|
||||
// Trojan is the one exporter that writes both spellings of the flag.
|
||||
await AssertExportContains(source, "allowInsecure=1", "insecure=1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Tuic_ShouldRoundTripUserInfoAndCongestionControl()
|
||||
{
|
||||
var source = CreateTuicProfile();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await resolved.Username.Should().BeEqualTo(source.Username);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.Sni.Should().BeEqualTo(source.Sni);
|
||||
await resolved.Alpn.Should().BeEqualTo(source.Alpn);
|
||||
await resolved.GetProtocolExtra().CongestionControl.Should()
|
||||
.BeEqualTo(source.GetProtocolExtra().CongestionControl);
|
||||
await resolved.GetAllowInsecure().Should().BeTrue();
|
||||
|
||||
await AssertExportContains(source, "allow_insecure=1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Anytls_ShouldRoundTripBasicFields()
|
||||
{
|
||||
var source = CreateAnytlsProfile();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await AssertRawTransportFields(source, resolved);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.Sni.Should().BeEqualTo(source.Sni);
|
||||
await resolved.Alpn.Should().BeEqualTo(source.Alpn);
|
||||
await resolved.GetAllowInsecure().Should().BeTrue();
|
||||
|
||||
await AssertExportContains(source, "insecure=1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Hysteria2_ShouldRoundTripObfsAndNormalizePortRange()
|
||||
{
|
||||
var source = CreateHysteria2Profile();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
var sourceExtra = source.GetProtocolExtra();
|
||||
var resolvedExtra = resolved.GetProtocolExtra();
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.Sni.Should().BeEqualTo(source.Sni);
|
||||
await resolved.Alpn.Should().BeEqualTo(source.Alpn);
|
||||
await resolved.EchConfigList.Should().BeEqualTo(source.EchConfigList);
|
||||
await resolved.GetAllowInsecure().Should().BeTrue();
|
||||
await resolvedExtra.SalamanderPass.Should().BeEqualTo(sourceExtra.SalamanderPass);
|
||||
|
||||
// Hysteria2Fmt stores a port range internally as "5000:6000" and emits the URI form.
|
||||
await resolvedExtra.Ports.Should().BeEqualTo("5000-6000");
|
||||
|
||||
await AssertExportContains(source, "insecure=1", "obfs=salamander", "mport=5000-6000");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Wireguard_ShouldRoundTripKeysAndInterface()
|
||||
{
|
||||
var source = CreateWireguardProfile();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
var extra = resolved.GetProtocolExtra();
|
||||
var sourceExtra = source.GetProtocolExtra();
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await extra.WgPublicKey.Should().BeEqualTo(sourceExtra.WgPublicKey);
|
||||
await extra.WgPresharedKey.Should().BeEqualTo(sourceExtra.WgPresharedKey);
|
||||
await extra.WgReserved.Should().BeEqualTo(sourceExtra.WgReserved);
|
||||
await extra.WgInterfaceAddress.Should().BeEqualTo(sourceExtra.WgInterfaceAddress);
|
||||
await extra.WgMtu.Should().BeEqualTo(sourceExtra.WgMtu);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUri_Wireguard_ShouldEncodeKeysAndBracketIpv6()
|
||||
{
|
||||
var source = CreateWireguardProfile();
|
||||
|
||||
// Base64 keys carry '/', '+' and '=', and the address is an IPv6 literal: both have to
|
||||
// survive the wire form, which a round trip through the same encoder would not prove.
|
||||
await AssertExportContains(
|
||||
source,
|
||||
Uri.EscapeDataString(source.Password),
|
||||
Uri.EscapeDataString(source.GetProtocolExtra().WgPublicKey ?? string.Empty),
|
||||
"@[2001:db8::40]:51820");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Naive_ShouldRoundTripCredentialsOverHttps()
|
||||
{
|
||||
var source = CreateNaiveProfile(false);
|
||||
|
||||
var resolved = await ExportThenImport(source, Global.NaiveHttpsProtocolShare);
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await AssertRawTransportFields(source, resolved);
|
||||
await resolved.Username.Should().BeEqualTo(source.Username);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.GetProtocolExtra().InsecureConcurrency.Should()
|
||||
.BeEqualTo(source.GetProtocolExtra().InsecureConcurrency);
|
||||
|
||||
// NaiveFmt only ever sets this flag on the quic branch, so the https branch leaves it
|
||||
// unset rather than false - assert "is not quic" instead of an explicit false.
|
||||
await (resolved.GetProtocolExtra().NaiveQuic == true).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_NaiveQuic_ShouldRoundTripQuicScheme()
|
||||
{
|
||||
var source = CreateNaiveProfile(true);
|
||||
|
||||
var resolved = await ExportThenImport(source, Global.NaiveQuicProtocolShare);
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await AssertRawTransportFields(source, resolved);
|
||||
await resolved.Username.Should().BeEqualTo(source.Username);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.GetProtocolExtra().InsecureConcurrency.Should()
|
||||
.BeEqualTo(source.GetProtocolExtra().InsecureConcurrency);
|
||||
await resolved.GetProtocolExtra().NaiveQuic.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("p:a@ss#% +/=")]
|
||||
[Arguments("пароль 東京")]
|
||||
public async Task GetShareUriAndResolveConfig_Trojan_ShouldRoundTripEncodedCredentials(string password)
|
||||
{
|
||||
var source = CreateTrojanProfile();
|
||||
source.Password = password;
|
||||
source.Remarks = "Trojan — тест 東京 #1";
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
await resolved.Password.Should().BeEqualTo(password);
|
||||
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResolveConfig_UnsupportedProtocol_ShouldReturnNull()
|
||||
{
|
||||
var resolved = FmtHandler.ResolveConfig("not-a-share-uri", out var msg);
|
||||
|
||||
resolved.Should().BeNull();
|
||||
msg.Should().NotBeNullOrWhiteSpace();
|
||||
await resolved.Should().BeNull();
|
||||
await msg.Should().NotBeNull();
|
||||
await msg.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetShareUri_UnsupportedConfigType_ShouldReturnNull()
|
||||
[Test]
|
||||
public async Task GetShareUri_UnsupportedConfigType_ShouldReturnNull()
|
||||
{
|
||||
var item = new ProfileItem { ConfigType = EConfigType.PolicyGroup, Remarks = "group", };
|
||||
|
||||
var uri = FmtHandler.GetShareUri(item);
|
||||
|
||||
uri.Should().BeNull();
|
||||
await uri.Should().BeNull();
|
||||
}
|
||||
|
||||
private static ProfileItem ExportThenImport(ProfileItem source)
|
||||
private static async Task AssertCommonShareFields(ProfileItem source, ProfileItem resolved)
|
||||
{
|
||||
await resolved.ConfigType.Should().BeEqualTo(source.ConfigType);
|
||||
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
|
||||
await resolved.Address.Should().BeEqualTo(source.Address);
|
||||
await resolved.Port.Should().BeEqualTo(source.Port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only for protocols whose exporter goes through the shared transport query
|
||||
/// (<c>security</c>, <c>type</c>, <c>headerType</c>). TUIC, Hysteria2 and WireGuard do not.
|
||||
/// </summary>
|
||||
private static async Task AssertRawTransportFields(ProfileItem source, ProfileItem resolved)
|
||||
{
|
||||
await resolved.Network.Should().BeEqualTo(source.Network);
|
||||
await resolved.StreamSecurity.Should().BeEqualTo(source.StreamSecurity);
|
||||
await resolved.GetTransportExtra().RawHeaderType.Should()
|
||||
.BeEqualTo(source.GetTransportExtra().RawHeaderType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts on the wire form itself. A round trip cannot catch an exporter and an importer that
|
||||
/// agree on the wrong spelling of a parameter, and the insecure flag is spelled differently by
|
||||
/// every protocol.
|
||||
/// </summary>
|
||||
private static async Task AssertExportContains(ProfileItem source, params string[] expectedFragments)
|
||||
{
|
||||
var uri = FmtHandler.GetShareUri(source);
|
||||
|
||||
uri.Should().NotBeNullOrWhiteSpace();
|
||||
uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase).Should()
|
||||
.BeTrue();
|
||||
await uri.Should().NotBeNull();
|
||||
|
||||
foreach (var fragment in expectedFragments)
|
||||
{
|
||||
await uri!.Contains(fragment, StringComparison.Ordinal).Should()
|
||||
.BeTrue().Because($"uri: {uri}, expected fragment: {fragment}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExpectedShareScheme(ProfileItem item)
|
||||
{
|
||||
if (item.ConfigType != EConfigType.Naive)
|
||||
{
|
||||
return Global.ProtocolShares[item.ConfigType];
|
||||
}
|
||||
|
||||
// NaiveFmt never emits the "naive://" prefix that Global.ProtocolShares records for the
|
||||
// type; that entry is only read when importing.
|
||||
return item.GetProtocolExtra().NaiveQuic == true
|
||||
? Global.NaiveQuicProtocolShare
|
||||
: Global.NaiveHttpsProtocolShare;
|
||||
}
|
||||
|
||||
private static async Task<ProfileItem> ExportThenImport(ProfileItem source)
|
||||
{
|
||||
return await ExportThenImport(source, ExpectedShareScheme(source));
|
||||
}
|
||||
|
||||
private static async Task<ProfileItem> ExportThenImport(ProfileItem source, string expectedPrefix)
|
||||
{
|
||||
var uri = FmtHandler.GetShareUri(source);
|
||||
|
||||
await uri.Should().NotBeNull();
|
||||
await uri.Should().NotBeEmpty();
|
||||
await uri!.StartsWith(expectedPrefix, StringComparison.OrdinalIgnoreCase).Should().BeTrue();
|
||||
|
||||
var resolved = FmtHandler.ResolveConfig(uri, out var msg);
|
||||
|
||||
resolved.Should().NotBeNull($"uri: {uri}, msg: {msg}");
|
||||
await resolved.Should().NotBeNull().Because($"uri: {uri}, msg: {msg}");
|
||||
return resolved!;
|
||||
}
|
||||
|
||||
@@ -170,4 +421,142 @@ public class FmtHandlerTests
|
||||
Password = "pass",
|
||||
};
|
||||
}
|
||||
|
||||
private static ProfileItem CreateTrojanProfile()
|
||||
{
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.Trojan,
|
||||
Remarks = "trojan demo",
|
||||
Address = "trojan.example",
|
||||
Port = 443,
|
||||
Password = "trojan-pass",
|
||||
Network = nameof(ETransport.raw),
|
||||
StreamSecurity = Global.StreamSecurity,
|
||||
Sni = "sni.trojan.example",
|
||||
AllowInsecure = Global.StringTrue,
|
||||
};
|
||||
|
||||
item.SetProtocolExtra(new ProtocolExtraItem { Flow = Global.Flows[1], });
|
||||
item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, });
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ProfileItem CreateTuicProfile()
|
||||
{
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.TUIC,
|
||||
Remarks = "tuic demo",
|
||||
Address = "tuic.example",
|
||||
Port = 8443,
|
||||
// A colon separates the two halves of the TUIC user info, so it cannot appear in the
|
||||
// uuid; a fixed value also keeps a failure reproducible.
|
||||
Username = "01234567-89ab-cdef-0123-456789abcdef",
|
||||
Password = "tuic-pass",
|
||||
Sni = "sni.tuic.example",
|
||||
Alpn = "h3",
|
||||
AllowInsecure = Global.StringTrue,
|
||||
};
|
||||
|
||||
item.SetProtocolExtra(new ProtocolExtraItem { CongestionControl = "bbr", });
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ProfileItem CreateAnytlsProfile()
|
||||
{
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.Anytls,
|
||||
Remarks = "anytls demo",
|
||||
Address = "anytls.example",
|
||||
Port = 8443,
|
||||
Password = "anytls-pass",
|
||||
Network = nameof(ETransport.raw),
|
||||
StreamSecurity = Global.StreamSecurity,
|
||||
Sni = "sni.anytls.example",
|
||||
Alpn = "h2,http/1.1",
|
||||
AllowInsecure = Global.StringTrue,
|
||||
};
|
||||
|
||||
item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, });
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ProfileItem CreateHysteria2Profile()
|
||||
{
|
||||
// CertSha is deliberately left unset: the importer turns AllowInsecure on by itself when a
|
||||
// pinSHA256 is present, which would mask an exporter that stopped emitting insecure=1.
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.Hysteria2,
|
||||
Remarks = "hysteria2 demo",
|
||||
Address = "hy2.example",
|
||||
Port = 8443,
|
||||
Password = "demo-user:demo-pass",
|
||||
Sni = "sni.hy2.example",
|
||||
Alpn = "h3",
|
||||
EchConfigList = "AAj+DQAEAAAAAA==",
|
||||
AllowInsecure = Global.StringTrue,
|
||||
};
|
||||
|
||||
item.SetProtocolExtra(new ProtocolExtraItem
|
||||
{
|
||||
SalamanderPass = "salamander-pass",
|
||||
Ports = "5000:6000",
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static string CreateWireguardKey(byte value)
|
||||
{
|
||||
return Convert.ToBase64String(Enumerable.Repeat(value, 32).ToArray());
|
||||
}
|
||||
|
||||
private static ProfileItem CreateWireguardProfile()
|
||||
{
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.WireGuard,
|
||||
Remarks = "WireGuard — тест 東京 #1",
|
||||
Address = "2001:db8::40",
|
||||
Port = 51820,
|
||||
Password = CreateWireguardKey(0xFE),
|
||||
};
|
||||
|
||||
item.SetProtocolExtra(new ProtocolExtraItem
|
||||
{
|
||||
WgPublicKey = CreateWireguardKey(0xFD),
|
||||
WgPresharedKey = CreateWireguardKey(0xFC),
|
||||
WgReserved = "1,2,255",
|
||||
WgInterfaceAddress = "10.0.0.2/32,fd00::2/128",
|
||||
WgMtu = 1420,
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ProfileItem CreateNaiveProfile(bool quic)
|
||||
{
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.Naive,
|
||||
Remarks = quic ? "naive quic demo" : "naive https demo",
|
||||
Address = "naive.example",
|
||||
Port = 443,
|
||||
Username = "naive-user",
|
||||
Password = "päss:word@/?#&=+ 東京",
|
||||
Network = nameof(ETransport.raw),
|
||||
StreamSecurity = Global.None,
|
||||
};
|
||||
|
||||
item.SetProtocolExtra(new ProtocolExtraItem { NaiveQuic = quic, InsecureConcurrency = 4, });
|
||||
item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, });
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
namespace ServiceLib.Tests.Fmt;
|
||||
|
||||
public class HyRealmTests
|
||||
{
|
||||
[Test]
|
||||
public async Task 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);
|
||||
await result.Should().BeTrue();
|
||||
await realm.Should().NotBeNull();
|
||||
|
||||
await realm.IsHttp.Should().BeFalse();
|
||||
await realm.Token.Should().BeEqualTo("public");
|
||||
await realm.RendezvousHost.Should().BeEqualTo("realm.hy2.io");
|
||||
await realm.RendezvousPort.Should().BeEqualTo(443);
|
||||
await realm.RealmName.Should().BeEqualTo("57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
|
||||
await realm.StunList.Should().HaveCount(2);
|
||||
await realm.StunList.Should().Contain("example.stun:3478");
|
||||
await realm.StunList.Should().Contain("example2.stun:3478");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task 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();
|
||||
await uri.Should().Contain("realm://public@realm.hy2.io");
|
||||
await uri.Should().Contain("/57f9be7c-2810-4f5b-8cb9-260bc84d6c90");
|
||||
await uri.Should().Contain("stun=example.stun:3478");
|
||||
await uri.Should().Contain("stun=example2.stun:3478");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task 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);
|
||||
await resolved.Should().NotBeNull();
|
||||
await resolved.Password.Should().BeEqualTo("your_password");
|
||||
var result = HyRealm.TryParse(resolved.GetProtocolExtra().Hy2RealmUrl, out var realm);
|
||||
await result.Should().BeTrue();
|
||||
await realm.Should().NotBeNull();
|
||||
await realm.Token.Should().BeEqualTo("mytoken");
|
||||
|
||||
// To uri
|
||||
var uri = Hysteria2Fmt.ToUri(resolved);
|
||||
await uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com");
|
||||
await uri.Should().EndWith("#remark");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task 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"]
|
||||
);
|
||||
|
||||
await realm.ToServerUrl().Should().BeEqualTo("https://realm.hy2.io:443");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task 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 _);
|
||||
await resolved.Should().NotBeNull();
|
||||
|
||||
await HyRealm.TryParse(resolved!.GetProtocolExtra().Hy2RealmUrl, out var realm).Should().BeTrue();
|
||||
await realm!.ToServerUrl().Should().StartWith("https://");
|
||||
await realm.ToServerUrl().Should().Contain("realm.hy2.io");
|
||||
await realm.StunList.Should().Contain("turn.cloudflare.com:3478");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace ServiceLib.Tests.Fmt;
|
||||
|
||||
public class Hysteria2FmtTests
|
||||
{
|
||||
// "The hostname and optional port of the server. If the port is omitted, it defaults to 443."
|
||||
// -- https://v2.hysteria.network/docs/developers/URI-Scheme/
|
||||
// A ':' with no digits after it is an omitted port too, per RFC 3986 'port = *DIGIT'.
|
||||
[Test]
|
||||
[Arguments("hysteria2://password@hy2.example/")]
|
||||
[Arguments("hysteria2://password@hy2.example")]
|
||||
[Arguments("hysteria2://password@hy2.example:/")]
|
||||
[Arguments("hy2://password@hy2.example/?sni=real.example")]
|
||||
public async Task ResolveConfig_WithoutPort_ShouldDefaultTo443(string shareUri)
|
||||
{
|
||||
var resolved = FmtHandler.ResolveConfig(shareUri, out var msg);
|
||||
|
||||
await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}");
|
||||
await resolved!.ConfigType.Should().BeEqualTo(EConfigType.Hysteria2);
|
||||
await resolved.Address.Should().BeEqualTo("hy2.example");
|
||||
await resolved.Port.Should().BeEqualTo(443);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResolveConfig_WithoutPort_ShouldProduceAValidProfile()
|
||||
{
|
||||
// Uri.Port is -1 for an unregistered scheme with no port, and ProfileItem.IsValid rejects
|
||||
// any port outside 1..65535 - so the default is what keeps such a link usable at all.
|
||||
var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example/", out _);
|
||||
|
||||
await resolved.Should().NotBeNull();
|
||||
await resolved!.IsValid().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResolveConfig_WithExplicitPort_ShouldKeepIt()
|
||||
{
|
||||
var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example:8443/", out _);
|
||||
|
||||
await resolved.Should().NotBeNull();
|
||||
await resolved!.Port.Should().BeEqualTo(8443);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResolveConfig_WithExplicitZeroPort_ShouldNotApplyTheDefault()
|
||||
{
|
||||
// Uri.Port is 0 here, not -1: the port is present, it is just not a usable one. Treating
|
||||
// it as "omitted" would silently move the endpoint to :443, so it stays invalid instead.
|
||||
var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example:0/", out _);
|
||||
|
||||
await resolved.Should().NotBeNull();
|
||||
await resolved!.Port.Should().BeEqualTo(0);
|
||||
await resolved.IsValid().Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
using AwesomeAssertions;
|
||||
using ServiceLib.Enums;
|
||||
using ServiceLib.Handler.Fmt;
|
||||
using ServiceLib.Tests.CoreConfig;
|
||||
using Xunit;
|
||||
|
||||
namespace ServiceLib.Tests.Fmt;
|
||||
|
||||
public class InnerFmtTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToUriAndResolve_ShouldRoundTripPolicyGroupReferences()
|
||||
[Test]
|
||||
public async Task ToUriAndResolve_ShouldRoundTripPolicyGroupReferences()
|
||||
{
|
||||
var childA = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "child-a", "child-a");
|
||||
var childB = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "child-b", "child-b");
|
||||
@@ -19,19 +15,20 @@ public class InnerFmtTests
|
||||
|
||||
var uri = InnerFmt.ToUri([group, childA, childB]);
|
||||
|
||||
uri.Should().NotBeNullOrWhiteSpace();
|
||||
await uri.Should().NotBeNull();
|
||||
await uri.Should().NotBeEmpty();
|
||||
|
||||
var resolved = InnerFmt.Resolve(uri!, "sub-123");
|
||||
|
||||
resolved.Should().NotBeNull();
|
||||
resolved.Should().HaveCount(3);
|
||||
await resolved.Should().NotBeNull();
|
||||
await resolved.Should().HaveCount(3);
|
||||
|
||||
var resolvedGroup = resolved!.Single(x => x.Remarks == group.Remarks);
|
||||
var resolvedChildA = resolved.Single(x => x.Remarks == childA.Remarks);
|
||||
var resolvedChildB = resolved.Single(x => x.Remarks == childB.Remarks);
|
||||
|
||||
resolvedGroup.ConfigType.Should().Be(EConfigType.PolicyGroup);
|
||||
resolvedGroup.GetProtocolExtra().SubChildItems.Should().Be("sub-123");
|
||||
resolvedGroup.GetProtocolExtra().ChildItems.Should().Be($"{resolvedChildA.IndexId},{resolvedChildB.IndexId}");
|
||||
await resolvedGroup.ConfigType.Should().BeEqualTo(EConfigType.PolicyGroup);
|
||||
await resolvedGroup.GetProtocolExtra().SubChildItems.Should().BeEqualTo("sub-123");
|
||||
await resolvedGroup.GetProtocolExtra().ChildItems.Should().BeEqualTo($"{resolvedChildA.IndexId},{resolvedChildB.IndexId}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
namespace ServiceLib.Tests.Fmt;
|
||||
|
||||
public class ShareUriQueryTests
|
||||
{
|
||||
[Test]
|
||||
[Arguments("ob%41fs")]
|
||||
[Arguments("66%ff")]
|
||||
[Arguments("100%")]
|
||||
public async Task GetShareUriAndResolveConfig_QueryValueWithPercent_ShouldSurviveTheRoundTrip(string obfsPassword)
|
||||
{
|
||||
var source = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.Hysteria2,
|
||||
Remarks = "percent demo",
|
||||
Address = "hy2.example",
|
||||
Port = 8443,
|
||||
Password = "pw",
|
||||
};
|
||||
source.SetProtocolExtra(new ProtocolExtraItem { SalamanderPass = obfsPassword, });
|
||||
|
||||
var uri = FmtHandler.GetShareUri(source);
|
||||
|
||||
await uri.Should().NotBeNull();
|
||||
|
||||
var resolved = FmtHandler.ResolveConfig(uri!, out var msg);
|
||||
|
||||
await resolved.Should().NotBeNull().Because($"uri: {uri}, msg: {msg}");
|
||||
await resolved!.GetProtocolExtra().SalamanderPass.Should().BeEqualTo(obfsPassword);
|
||||
}
|
||||
|
||||
// RFC 3986 lists '=' among the sub-delimiters a query value may carry, so only the first one
|
||||
// separates the key from the value. Splitting on every '=' discarded the pair outright.
|
||||
[Test]
|
||||
public async Task ResolveConfig_QueryValueWithUnescapedEquals_ShouldNotBeDropped()
|
||||
{
|
||||
const string shareUri = "hysteria2://pw@hy2.example:8443/?ech=AAj+DQAEAAAAAA==&sni=real.example";
|
||||
|
||||
var resolved = FmtHandler.ResolveConfig(shareUri, out var msg);
|
||||
|
||||
await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}");
|
||||
await resolved!.EchConfigList.Should().BeEqualTo("AAj+DQAEAAAAAA==");
|
||||
await resolved.Sni.Should().BeEqualTo("real.example");
|
||||
}
|
||||
|
||||
// Canonical SIP002 percent-encodes the plugin argument, and that is what this client's own
|
||||
// exporter emits. This covers the non-canonical spelling instead: the options are a ';'
|
||||
// separated list of 'key=value' pairs, so left unescaped the value always carries '='.
|
||||
[Test]
|
||||
public async Task ResolveConfig_NonCanonicalSip002PluginWithLiteralEquals_ShouldConfigureObfs()
|
||||
{
|
||||
const string shareUri =
|
||||
"ss://YWVzLTEyOC1nY206cGFzczEyMw==@1.2.3.4:8388/?plugin=obfs-local;obfs=http;obfs-host=example.com#ss";
|
||||
|
||||
var resolved = FmtHandler.ResolveConfig(shareUri, out var msg);
|
||||
|
||||
await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}");
|
||||
await resolved!.ConfigType.Should().BeEqualTo(EConfigType.Shadowsocks);
|
||||
await resolved.GetTransportExtra().Host.Should().BeEqualTo("example.com");
|
||||
await resolved.GetTransportExtra().RawHeaderType.Should().BeEqualTo(Global.RawHeaderHttp);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResolveConfig_EscapedQueryValue_ShouldStillDecodeExactlyOnce()
|
||||
{
|
||||
const string shareUri = "hysteria2://pw@hy2.example:8443/?ech=AAj%2BDQAEAAAAAA%3D%3D&obfs=salamander&obfs-password=a%20b";
|
||||
|
||||
var resolved = FmtHandler.ResolveConfig(shareUri, out var msg);
|
||||
|
||||
await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}");
|
||||
await resolved!.EchConfigList.Should().BeEqualTo("AAj+DQAEAAAAAA==");
|
||||
await resolved.GetProtocolExtra().SalamanderPass.Should().BeEqualTo("a b");
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
using AwesomeAssertions;
|
||||
using ServiceLib.Handler.Fmt;
|
||||
using Xunit;
|
||||
|
||||
namespace ServiceLib.Tests.Fmt;
|
||||
|
||||
public class WireguardFmtTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveConfig_ShouldParsePeersAndIgnoreInlineComments()
|
||||
[Test]
|
||||
public async Task ResolveConfig_ShouldParsePeersAndIgnoreInlineComments()
|
||||
{
|
||||
const string config =
|
||||
"""
|
||||
@@ -15,6 +11,7 @@ public class WireguardFmtTests
|
||||
PrivateKey = interface-private-key
|
||||
Address = 10.0.0.2/32, fd00::2/128 ; inline comment
|
||||
MTU = 1420
|
||||
DNS = 2001::db8::53, 1.2.3.4, 2001:db8::54
|
||||
|
||||
[Peer]
|
||||
PublicKey = peer-public-key
|
||||
@@ -29,19 +26,20 @@ public class WireguardFmtTests
|
||||
|
||||
var resolved = WireguardFmt.ResolveConfig(config);
|
||||
|
||||
resolved.Should().NotBeNull();
|
||||
resolved.Should().HaveCount(2);
|
||||
await resolved.Should().NotBeNull();
|
||||
await resolved.Should().HaveCount(2);
|
||||
|
||||
var first = resolved![0];
|
||||
first.Address.Should().Be("2001:db8::1");
|
||||
first.Port.Should().Be(51820);
|
||||
first.Password.Should().Be("interface-private-key");
|
||||
first.GetProtocolExtra().WgReserved.Should().Be("1, 2, 3");
|
||||
first.GetProtocolExtra().WgInterfaceAddress.Should().Be("10.0.0.2/32, fd00::2/128");
|
||||
first.GetProtocolExtra().WgMtu.Should().Be(1420);
|
||||
await first.Address.Should().BeEqualTo("2001:db8::1");
|
||||
await first.Port.Should().BeEqualTo(51820);
|
||||
await first.Password.Should().BeEqualTo("interface-private-key");
|
||||
await first.GetProtocolExtra().WgReserved.Should().BeEqualTo("1, 2, 3");
|
||||
await first.GetProtocolExtra().WgInterfaceAddress.Should().BeEqualTo("10.0.0.2/32, fd00::2/128");
|
||||
await first.GetProtocolExtra().WgMtu.Should().BeEqualTo(1420);
|
||||
await first.GetProtocolExtra().WgDns.Should().BeEqualTo("2001::db8::53, 1.2.3.4, 2001:db8::54");
|
||||
|
||||
var second = resolved[1];
|
||||
second.Address.Should().Be("example.com");
|
||||
second.Port.Should().Be(12345);
|
||||
await second.Address.Should().BeEqualTo("example.com");
|
||||
await second.Port.Should().BeEqualTo(12345);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ global using System.Diagnostics;
|
||||
global using System.Net;
|
||||
global using System.Net.NetworkInformation;
|
||||
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.Runtime.InteropServices;
|
||||
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.Serialization;
|
||||
global using System.Text.RegularExpressions;
|
||||
global using DynamicData;
|
||||
global using DynamicData.Binding;
|
||||
global using ReactiveUI;
|
||||
global using ReactiveUI.Fody.Helpers;
|
||||
global using ServiceLib.Base;
|
||||
global using ServiceLib.Common;
|
||||
global using ServiceLib.Enums;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace ServiceLib.Tests.Manager;
|
||||
|
||||
public class CoreManagerTests
|
||||
{
|
||||
[Test]
|
||||
[Arguments(ECoreType.sing_box)]
|
||||
[Arguments(ECoreType.mihomo)]
|
||||
[Arguments(ECoreType.Xray)]
|
||||
public async Task ShouldRunAsSudo_TunLaunchOnNonWindows_RequiresElevation(ECoreType coreType)
|
||||
{
|
||||
await CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task 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.
|
||||
await CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.sing_box, isNonWindows: true).Should().BeFalse();
|
||||
await CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.Xray, isNonWindows: true).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ShouldRunAsSudo_OnWindows_ShouldNotElevate()
|
||||
{
|
||||
await CoreManager.ShouldRunAsSudo(isTunLaunch: true, ECoreType.sing_box, isNonWindows: false).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(ECoreType.v2fly)]
|
||||
[Arguments(ECoreType.hysteria)]
|
||||
[Arguments(null)]
|
||||
public async Task ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType)
|
||||
{
|
||||
await CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AwesomeAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit.v3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TUnit" />
|
||||
<PackageReference Include="TUnit.Assertions.Should" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ServiceLib\ServiceLib.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ServiceLib\ServiceLib.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="TUnit.Assertions.Should" />
|
||||
<Using Include="TUnit.Assertions.Should.Extensions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ServiceLib.Base;
|
||||
|
||||
public interface ICloseable
|
||||
{
|
||||
public event EventHandler? RequestClose;
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
protected static Config? _config;
|
||||
protected Func<EViewAction, object?, Task<bool>>? _updateView;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ServiceLib.Common;
|
||||
|
||||
@@ -92,7 +93,7 @@ public static class Extension
|
||||
|
||||
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>
|
||||
@@ -134,4 +135,30 @@ public static class Extension
|
||||
.Replace("\r", replacement)
|
||||
.Replace("\n", replacement);
|
||||
}
|
||||
|
||||
public static async Task<TOutput> HandleSafe<TInput, TOutput>(
|
||||
this Interaction<TInput, TOutput> interaction,
|
||||
TInput input,
|
||||
TOutput defaultValue = default!,
|
||||
[CallerMemberName] string memberName = "",
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerLineNumber] int lineNumber = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await interaction.Handle(input);
|
||||
}
|
||||
catch (UnhandledInteractionException<TInput, TOutput> ex)
|
||||
{
|
||||
var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}";
|
||||
Logging.SaveLog(title, ex);
|
||||
return defaultValue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var title = $"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}";
|
||||
Logging.SaveLog(title, ex);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Security.Principal;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
@@ -201,7 +200,9 @@ public class Utils
|
||||
var parts = query[1..].Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var keyValue = part.Split('=');
|
||||
// Split on the FIRST '=' only: RFC 3986 lists '=' among the sub-delimiters a query
|
||||
// value may carry, so everything after the first one belongs to the value.
|
||||
var keyValue = part.Split('=', 2);
|
||||
if (keyValue.Length != 2)
|
||||
{
|
||||
continue;
|
||||
@@ -324,6 +325,29 @@ public class Utils
|
||||
.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
|
||||
{
|
||||
return Enum.GetValues(typeof(TEnum))
|
||||
@@ -462,12 +486,17 @@ public class Utils
|
||||
|
||||
public static string? DomainStrategy4Sbox(string? strategy)
|
||||
{
|
||||
if (strategy is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return strategy switch
|
||||
{
|
||||
not null when strategy.StartsWith("UseIPv4") => "prefer_ipv4",
|
||||
not null when strategy.StartsWith("UseIPv6") => "prefer_ipv6",
|
||||
not null when strategy.StartsWith("ForceIPv4") => "ipv4_only",
|
||||
not null when strategy.StartsWith("ForceIPv6") => "ipv6_only",
|
||||
_ when strategy.StartsWith("UseIPv6") => "prefer_ipv6",
|
||||
_ when strategy.StartsWith("UseIP") => "prefer_ipv4",
|
||||
_ when strategy.StartsWith("ForceIPv6") => "ipv6_only",
|
||||
_ when strategy.StartsWith("ForceIP") => "ipv4_only",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
@@ -576,6 +605,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)
|
||||
{
|
||||
if (IPAddress.TryParse(ip, out var address))
|
||||
@@ -673,11 +754,11 @@ public class Utils
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int GetFreePort(int defaultPort = 0)
|
||||
public static int GetFreePort(int defaultPort)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!(defaultPort == 0 || Utils.PortInUse(defaultPort)))
|
||||
if (!PortInUse(defaultPort))
|
||||
{
|
||||
return defaultPort;
|
||||
}
|
||||
@@ -728,6 +809,12 @@ public class Utils
|
||||
.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
|
||||
|
||||
#region Miscellaneous
|
||||
|
||||
@@ -14,6 +14,7 @@ public enum EConfigType
|
||||
HTTP = 10,
|
||||
Anytls = 11,
|
||||
Naive = 12,
|
||||
Outbound = 13,
|
||||
PolicyGroup = 101,
|
||||
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 readonly EventChannel<Unit> ReloadRequested = 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<RxVoid> AddServerViaClipboardRequested = 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<string> SendSnackMsgRequested = 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<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();
|
||||
}
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
using System.Reactive.Subjects;
|
||||
|
||||
namespace ServiceLib.Events;
|
||||
|
||||
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()
|
||||
{
|
||||
return _subject.AsObservable();
|
||||
return _observable;
|
||||
}
|
||||
|
||||
public void Publish(T value)
|
||||
{
|
||||
_subject.OnNext(value);
|
||||
lock (_gate)
|
||||
{
|
||||
_signal.OnNext(value);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
#region const
|
||||
|
||||
public const string AppName = "v2rayN";
|
||||
public const string GithubUrl = "https://github.com";
|
||||
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 KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh";
|
||||
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 DefaultNetwork = "raw";
|
||||
@@ -92,6 +95,7 @@ public class Global
|
||||
public const string LinuxBash = "/bin/bash";
|
||||
public const string StringTrue = "true";
|
||||
public const string StringFalse = "false";
|
||||
public const int SqliteMaxBatchSize = 10000;
|
||||
|
||||
public const string SingboxDirectDNSTag = "direct_dns";
|
||||
public const string SingboxRemoteDNSTag = "remote_dns";
|
||||
@@ -100,9 +104,7 @@ public class Global
|
||||
public const string SingboxFakeDNSTag = "fake_dns";
|
||||
|
||||
public const int Hysteria2DefaultHopInt = 30;
|
||||
|
||||
public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset";
|
||||
|
||||
public const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$";
|
||||
|
||||
public static readonly List<string> PolicyGroupDefaultFilterList =
|
||||
@@ -210,6 +212,10 @@ public class Global
|
||||
|
||||
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 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 =
|
||||
[
|
||||
"chrome",
|
||||
@@ -537,7 +553,6 @@ public class Global
|
||||
"http",
|
||||
"tls",
|
||||
"quic",
|
||||
"fakedns",
|
||||
];
|
||||
|
||||
public static readonly List<int> TunMtus =
|
||||
@@ -667,6 +682,14 @@ public class Global
|
||||
"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 =
|
||||
[
|
||||
ProxyTag,
|
||||
@@ -709,5 +732,42 @@ public class Global
|
||||
"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.ObjectModel;
|
||||
global using System.Collections.Specialized;
|
||||
global using System.ComponentModel;
|
||||
global using System.Diagnostics;
|
||||
global using System.Net;
|
||||
global using System.Net.NetworkInformation;
|
||||
global using System.Net.Sockets;
|
||||
global using System.Reactive;
|
||||
global using System.Reactive.Disposables;
|
||||
global using System.Reactive.Linq;
|
||||
global using ReactiveUI.Primitives;
|
||||
global using ReactiveUI.Primitives.Concurrency;
|
||||
global using ReactiveUI.Primitives.Disposables;
|
||||
global using ReactiveUI.Primitives.Signals;
|
||||
global using System.Reflection;
|
||||
global using System.Runtime.InteropServices;
|
||||
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.Serialization;
|
||||
global using System.Text.RegularExpressions;
|
||||
global using DynamicData;
|
||||
global using DynamicData.Binding;
|
||||
global using ReactiveUI;
|
||||
global using ReactiveUI.Fody.Helpers;
|
||||
global using ReactiveUI.SourceGenerators;
|
||||
global using ServiceLib.Base;
|
||||
global using ServiceLib.Common;
|
||||
global using ServiceLib.Enums;
|
||||
@@ -30,8 +32,8 @@ global using ServiceLib.Handler.Fmt;
|
||||
global using ServiceLib.Handler.SysProxy;
|
||||
global using ServiceLib.Helper;
|
||||
global using ServiceLib.Manager;
|
||||
global using ServiceLib.Models.CoreConfigs;
|
||||
global using ServiceLib.Models.Configs;
|
||||
global using ServiceLib.Models.CoreConfigs;
|
||||
global using ServiceLib.Models.Dto;
|
||||
global using ServiceLib.Models.Entities;
|
||||
global using ServiceLib.Resx;
|
||||
@@ -39,3 +41,5 @@ global using ServiceLib.Services;
|
||||
global using ServiceLib.Services.CoreConfig;
|
||||
global using ServiceLib.Services.Statistics;
|
||||
global using SQLite;
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ public class CoreConfigContextBuilder
|
||||
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
|
||||
IsWindows = Utils.IsWindows(),
|
||||
IsMacOS = Utils.IsMacOS(),
|
||||
ProtectCoreTypeList = config.TunModeItem.EnableTun ? [ECoreType.Xray, ECoreType.sing_box] : []
|
||||
};
|
||||
var validatorResult = NodeValidatorResult.Empty();
|
||||
var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node);
|
||||
@@ -94,19 +95,42 @@ public class CoreConfigContextBuilder
|
||||
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
|
||||
{
|
||||
var appConfig = JsonUtils.DeepCopy(config);
|
||||
var routeExcludeAddressList = new List<string>();
|
||||
foreach (var addr in context.AppConfig.TunModeItem.RouteExcludeAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
IPNetwork2.Parse(addr);
|
||||
routeExcludeAddressList.Add(addr);
|
||||
}
|
||||
catch
|
||||
{
|
||||
validatorResult.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);
|
||||
@@ -171,12 +195,16 @@ public class CoreConfigContextBuilder
|
||||
if (preSocksItem != null)
|
||||
{
|
||||
var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem);
|
||||
|
||||
var protectCoreTypeList = new HashSet<ECoreType>(nodeContext.ProtectCoreTypeList) { nodeContext.RunCoreType };
|
||||
|
||||
return preSocksResult with
|
||||
{
|
||||
Context = preSocksResult.Context with
|
||||
{
|
||||
ProtectDomainList =
|
||||
[.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
|
||||
[.. nodeContext.ProtectDomainList, .. preSocksResult.Context.ProtectDomainList],
|
||||
ProtectCoreTypeList = protectCoreTypeList,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -295,14 +323,14 @@ public class CoreConfigContextBuilder
|
||||
{
|
||||
return await RegisterGroupNodeAsync(context, node);
|
||||
}
|
||||
return RegisterSingleNodeAsync(context, node);
|
||||
return await RegisterSingleNodeAsync(context, node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node)
|
||||
private static async Task<NodeValidatorResult> RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node)
|
||||
{
|
||||
if (node.ConfigType.IsGroupType())
|
||||
{
|
||||
@@ -310,6 +338,29 @@ public class CoreConfigContextBuilder
|
||||
}
|
||||
|
||||
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]);
|
||||
if (msgs.Count > 0)
|
||||
{
|
||||
@@ -333,7 +384,7 @@ public class CoreConfigContextBuilder
|
||||
{
|
||||
var echQuerySni = node.Sni;
|
||||
if (node.StreamSecurity == Global.StreamSecurity
|
||||
&& node.EchConfigList?.Contains("://") == true)
|
||||
&& node.EchConfigList.Contains("://"))
|
||||
{
|
||||
var idx = node.EchConfigList.IndexOf('+');
|
||||
echQuerySni = idx > 0 ? node.EchConfigList[..idx] : node.Sni;
|
||||
@@ -411,7 +462,7 @@ public class CoreConfigContextBuilder
|
||||
|
||||
if (!childNode.ConfigType.IsGroupType())
|
||||
{
|
||||
var childNodeResult = RegisterSingleNodeAsync(context, childNode);
|
||||
var childNodeResult = await RegisterSingleNodeAsync(context, childNode);
|
||||
childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w =>
|
||||
string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w)));
|
||||
childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e =>
|
||||
|
||||
@@ -36,6 +36,15 @@ public class NodeValidator
|
||||
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())
|
||||
{
|
||||
// Group logic is handled in ValidateGroupNode
|
||||
@@ -43,8 +52,8 @@ public class NodeValidator
|
||||
}
|
||||
|
||||
// Basic Property Validation
|
||||
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Address"));
|
||||
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, "Port"));
|
||||
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, ResUI.TbAddress));
|
||||
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, ResUI.TbPort));
|
||||
|
||||
// Network & Core Logic
|
||||
var net = item.GetNetwork();
|
||||
@@ -75,25 +84,25 @@ public class NodeValidator
|
||||
{
|
||||
case EConfigType.VMess:
|
||||
v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password),
|
||||
string.Format(ResUI.MsgInvalidProperty, "Password"));
|
||||
string.Format(ResUI.MsgInvalidProperty, ResUI.TbId));
|
||||
break;
|
||||
|
||||
case EConfigType.VLESS:
|
||||
v.Assert(
|
||||
!item.Password.IsNullOrEmpty()
|
||||
&& (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),
|
||||
string.Format(ResUI.MsgInvalidProperty, "Flow"));
|
||||
string.Format(ResUI.MsgInvalidProperty, ResUI.TbFlow5));
|
||||
break;
|
||||
|
||||
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(
|
||||
!string.IsNullOrEmpty(protocolExtra.SsMethod) &&
|
||||
Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod),
|
||||
string.Format(ResUI.MsgInvalidProperty, "SsMethod"));
|
||||
string.Format(ResUI.MsgInvalidProperty, ResUI.TbSecurity3));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -106,44 +115,41 @@ public class NodeValidator
|
||||
|
||||
if (item.GetNetwork() is nameof(ETransport.ws)
|
||||
&& item.EchConfigList.IsNullOrEmpty()
|
||||
&& item.GetAlpn()?.FirstOrDefault() == "h3")
|
||||
&& item.GetAlpn()?.FirstOrDefault() is "h3" or "h2")
|
||||
{
|
||||
v.Warning(
|
||||
"WebSocket but ALPN is set to h3, the core may ignore the ALPN setting or cause unexpected issues.");
|
||||
$"WebSocket but ALPN is set to {item.Alpn}, the core may ignore the ALPN setting or cause unexpected issues.");
|
||||
}
|
||||
|
||||
// TLS & Security
|
||||
if (item.StreamSecurity == Global.StreamSecurity)
|
||||
{
|
||||
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 &&
|
||||
!item.CertSha.IsNullOrEmpty())
|
||||
var isCertProvided = !item.Cert.IsNullOrEmpty();
|
||||
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0)
|
||||
{
|
||||
v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate"));
|
||||
}
|
||||
|
||||
// Check for deprecated allowInsecure property when TLS is enabled
|
||||
if (item.GetAllowInsecure()
|
||||
&& item.Cert.IsNullOrEmpty()
|
||||
&& item.CertSha.IsNullOrEmpty())
|
||||
{
|
||||
v.Warning(ResUI.MsgAllowInsecureDeprecated);
|
||||
v.Error(string.Format(ResUI.MsgInvalidProperty, ResUI.TbFullCertTips));
|
||||
isCertProvided = false;
|
||||
}
|
||||
|
||||
if ((coreType == ECoreType.Xray
|
||||
&& item.GetAllowInsecure()
|
||||
&& item.Cert.IsNullOrEmpty()
|
||||
&& !isCertProvided
|
||||
&& item.CertSha.IsNullOrEmpty())
|
||||
|| (coreType == ECoreType.sing_box
|
||||
&& item.GetAllowInsecure()
|
||||
&& item.Cert.IsNullOrEmpty()))
|
||||
&& !isCertProvided))
|
||||
{
|
||||
v.Warning("Insecure configuration detected: AllowInsecure is enabled but no certificate is provided. This may cause MITM attacks.");
|
||||
if (coreType == ECoreType.Xray)
|
||||
{
|
||||
v.Warning(ResUI.MsgAllowInsecureDeprecated);
|
||||
}
|
||||
v.Warning(ResUI.MsgInsecureConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -151,7 +157,7 @@ public class NodeValidator
|
||||
{
|
||||
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 +165,7 @@ public class NodeValidator
|
||||
{
|
||||
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,
|
||||
Mtu = 9000,
|
||||
IcmpRouting = Global.TunIcmpRoutingPolicies.First(),
|
||||
EnableLegacyProtect = false,
|
||||
EnableLegacyProtect = true,
|
||||
};
|
||||
config.GuiItem ??= new();
|
||||
if (!Global.RootCertProviders.Contains(config.GuiItem.RootCertProvider))
|
||||
{
|
||||
config.GuiItem.RootCertProvider = Global.RootCertProviders.First();
|
||||
}
|
||||
config.MsgUIItem ??= new();
|
||||
|
||||
config.UiItem ??= new();
|
||||
@@ -111,10 +115,13 @@ public static class ConfigHandler
|
||||
config.ConstItem ??= new ConstItem();
|
||||
|
||||
config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
|
||||
config.SimpleDNSItem.BlockAAAAQuery ??= false;
|
||||
config.SimpleDNSItem.FakeIPRange ??= Global.FakeIPRanges.FirstOrDefault();
|
||||
config.SimpleDNSItem.GlobalFakeIp ??= true;
|
||||
config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault();
|
||||
config.SimpleDNSItem.ServeStale ??= false;
|
||||
config.SimpleDNSItem.ParallelQuery ??= false;
|
||||
config.SimpleDNSItem.EnableHappyEyeballs ??= false;
|
||||
|
||||
config.SpeedTestItem ??= new();
|
||||
if (config.SpeedTestItem.SpeedTestTimeout < 10)
|
||||
@@ -161,16 +168,27 @@ public static class ConfigHandler
|
||||
config.SystemProxyItem ??= new();
|
||||
config.WebDavItem ??= new();
|
||||
config.CheckUpdateItem ??= new();
|
||||
config.NetBridgeItem ??= new()
|
||||
{
|
||||
RuleProcess = string.Empty
|
||||
};
|
||||
config.Fragment4RayItem ??= new()
|
||||
{
|
||||
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 ??= [];
|
||||
|
||||
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
|
||||
@@ -365,6 +383,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
|
||||
{
|
||||
await AddServerCommon(config, profileItem, true);
|
||||
@@ -562,6 +587,43 @@ public static class ConfigHandler
|
||||
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>
|
||||
/// Edit an existing custom server configuration
|
||||
/// Updates the server's properties without changing the file
|
||||
@@ -583,6 +645,8 @@ public static class ConfigHandler
|
||||
item.CoreType = profileItem.CoreType;
|
||||
item.DisplayLog = profileItem.DisplayLog;
|
||||
item.PreSocksPort = profileItem.PreSocksPort;
|
||||
|
||||
item.ProtoExtra = profileItem.ProtoExtra;
|
||||
}
|
||||
|
||||
if (await SQLiteHelper.Instance.UpdateAsync(item) > 0)
|
||||
@@ -724,12 +788,51 @@ public static class ConfigHandler
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
|
||||
var protocolExtra = profileItem.GetProtocolExtra();
|
||||
profileItem.SetProtocolExtra(protocolExtra with
|
||||
{
|
||||
SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(),
|
||||
HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(),
|
||||
SalamanderPass = protocolExtra.SalamanderPass?.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);
|
||||
|
||||
return 0;
|
||||
@@ -819,6 +922,7 @@ public static class ConfigHandler
|
||||
WgInterfaceAddress = profileItem.GetProtocolExtra().WgInterfaceAddress?.TrimEx(),
|
||||
WgReserved = wgReserved,
|
||||
WgMtu = profileItem.GetProtocolExtra().WgMtu is null or <= 0 ? Global.TunMtus.First() : profileItem.GetProtocolExtra().WgMtu,
|
||||
WgDns = profileItem.GetProtocolExtra().WgDns?.TrimEx(),
|
||||
});
|
||||
|
||||
if (profileItem.Password.IsNullOrEmpty())
|
||||
@@ -1423,7 +1527,7 @@ public static class ConfigHandler
|
||||
var matchedChildProfiles = childProfiles?.Where(p =>
|
||||
p != null &&
|
||||
p.IsValid() &&
|
||||
!p.ConfigType.IsComplexType() &&
|
||||
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
||||
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
|
||||
)
|
||||
.ToList() ?? [];
|
||||
@@ -1454,8 +1558,7 @@ public static class ConfigHandler
|
||||
public static ProfileItem? GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType)
|
||||
{
|
||||
ProfileItem? itemSocks = null;
|
||||
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect
|
||||
|| Utils.IsNonWindows();
|
||||
var enableLegacyProtect = config.TunModeItem.EnableLegacyProtect;
|
||||
if (node.ConfigType != EConfigType.Custom
|
||||
&& coreType != ECoreType.sing_box
|
||||
&& config.TunModeItem.EnableTun
|
||||
@@ -1472,7 +1575,8 @@ public static class ConfigHandler
|
||||
else if (node.ConfigType == EConfigType.Custom
|
||||
&& 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()
|
||||
{
|
||||
CoreType = preCoreType,
|
||||
@@ -1619,68 +1723,191 @@ public static class ConfigHandler
|
||||
}
|
||||
|
||||
var subItem = await AppManager.Instance.GetSubItem(subid);
|
||||
var subRemarks = subItem?.Remarks;
|
||||
var preSocksPort = subItem?.PreSocksPort;
|
||||
|
||||
List<ProfileItem>? lstProfiles = null;
|
||||
//Is sing-box array configuration
|
||||
if (lstProfiles is null || lstProfiles.Count <= 0)
|
||||
if (subItem?.CustomCoreType is null)
|
||||
{
|
||||
lstProfiles = SingboxFmt.ResolveFullArray(strData, subRemarks);
|
||||
return await AddBatchServersDefaultCustom(config, strData, subid, isSub, subItem);
|
||||
}
|
||||
//Is v2ray array configuration
|
||||
if (lstProfiles is null || lstProfiles.Count <= 0)
|
||||
|
||||
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;
|
||||
// Prioritize using complete custom parsing, followed by custom outbound parsing.
|
||||
var lstProfiles = V2rayFmt.ResolveToCustom(strData, subRemarks);
|
||||
if (lstProfiles.Count == 0)
|
||||
{
|
||||
lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks);
|
||||
lstProfiles = SingboxFmt.ResolveToCustom(strData, subRemarks);
|
||||
}
|
||||
if (lstProfiles is { Count: > 0 })
|
||||
if (lstProfiles.Count == 0)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var it in lstProfiles)
|
||||
{
|
||||
it.Subid = subid;
|
||||
it.IsSub = isSub;
|
||||
it.PreSocksPort = preSocksPort;
|
||||
if (await AddCustomServer(config, it, true) == 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
lstProfiles = V2rayFmt.ResolveToCustomOutbound(strData, subRemarks);
|
||||
}
|
||||
if (lstProfiles.Count == 0)
|
||||
{
|
||||
lstProfiles = SingboxFmt.ResolveToCustomOutbound(strData, subRemarks);
|
||||
}
|
||||
if (lstProfiles.Count > 0)
|
||||
{
|
||||
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
||||
if (count > 0)
|
||||
{
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
if (HtmlPageFmt.IsHtmlPage(strData))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
//Is Clash configuration
|
||||
profileItem ??= ClashFmt.ResolveFull(strData, subRemarks);
|
||||
//Is hysteria configuration
|
||||
profileItem ??= Hysteria2Fmt.ResolveFull2(strData, subRemarks);
|
||||
if (profileItem is null || profileItem.Address.IsNullOrEmpty())
|
||||
|
||||
var profileItem = ClashFmt.ResolveFull(strData, subRemarks)
|
||||
?? Hysteria2Fmt.ResolveFull2(strData, subRemarks);
|
||||
|
||||
if (profileItem == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
profileItem.Subid = subid;
|
||||
profileItem.IsSub = isSub;
|
||||
profileItem.PreSocksPort = preSocksPort;
|
||||
if (await AddCustomServer(config, profileItem, true) == 0)
|
||||
profileItem.PreSocksPort = subItem?.PreSocksPort;
|
||||
|
||||
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?.Count > 0)
|
||||
{
|
||||
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
||||
if (count > 0)
|
||||
{
|
||||
return count;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType);
|
||||
}
|
||||
|
||||
private static async Task<int> AddBatchCustomServers(
|
||||
Config config,
|
||||
List<ProfileItem> lstProfiles,
|
||||
string subid,
|
||||
bool isSub)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var it in lstProfiles)
|
||||
{
|
||||
return -1;
|
||||
it.Subid = subid;
|
||||
it.IsSub = isSub;
|
||||
|
||||
if (it.ConfigType == EConfigType.Custom)
|
||||
{
|
||||
if (await AddCustomServer(config, it, true) == 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (await AddCustomOutboundServer(config, it, true) == 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static async Task<int> SaveCustomRawFileServer(
|
||||
Config config,
|
||||
string strData,
|
||||
string subid,
|
||||
bool isSub,
|
||||
SubItem subItem,
|
||||
ECoreType customCoreType)
|
||||
{
|
||||
var ext = DetectFileExtension(strData);
|
||||
var fileName = Utils.GetTempPath($"{Utils.GetGuid(false)}{ext}");
|
||||
await File.WriteAllTextAsync(fileName, strData);
|
||||
|
||||
var profileItem = new ProfileItem
|
||||
{
|
||||
CoreType = customCoreType,
|
||||
ConfigType = EConfigType.Custom,
|
||||
Address = fileName,
|
||||
Remarks = subItem.Remarks ?? customCoreType.ToString(),
|
||||
Subid = subid,
|
||||
IsSub = isSub,
|
||||
PreSocksPort = subItem.PreSocksPort,
|
||||
};
|
||||
|
||||
return await AddCustomServer(config, profileItem, true) == 0 ? 1 : -1;
|
||||
|
||||
static string DetectFileExtension(string data)
|
||||
{
|
||||
var trimmed = data.AsSpan().TrimStart();
|
||||
if (trimmed.IsEmpty)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (trimmed[0] is '{' or '[')
|
||||
{
|
||||
return ".json";
|
||||
}
|
||||
|
||||
if (trimmed.StartsWith("---"))
|
||||
{
|
||||
return ".yaml";
|
||||
}
|
||||
|
||||
foreach (var line in trimmed.EnumerateLines())
|
||||
{
|
||||
var lineTrimmed = line.TrimStart();
|
||||
if (lineTrimmed.IsEmpty || lineTrimmed.StartsWith("#"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var colonIndex = lineTrimmed.IndexOf(':');
|
||||
if (colonIndex > 0)
|
||||
{
|
||||
var keySpan = lineTrimmed[..colonIndex];
|
||||
if (!keySpan.Contains(' ') && !keySpan.Contains('\t'))
|
||||
{
|
||||
if (colonIndex == lineTrimmed.Length - 1 || lineTrimmed[colonIndex + 1] is ' ' or '\t' or '\r' or '\n')
|
||||
{
|
||||
return ".yaml";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1781,6 +2008,7 @@ public static class ConfigHandler
|
||||
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
|
||||
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
|
||||
EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false),
|
||||
EConfigType.Outbound => await AddCustomOutboundServer(config, profileItem, true, false),
|
||||
_ => -1,
|
||||
};
|
||||
if (addStatus == 0)
|
||||
@@ -1982,6 +2210,7 @@ public static class ConfigHandler
|
||||
item.NextProfile = subItem.NextProfile;
|
||||
item.PreSocksPort = subItem.PreSocksPort;
|
||||
item.Memo = subItem.Memo;
|
||||
item.CustomCoreType = subItem.CustomCoreType;
|
||||
}
|
||||
|
||||
if (item.Id.IsNullOrEmpty())
|
||||
@@ -2022,7 +2251,7 @@ public static class ConfigHandler
|
||||
{
|
||||
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)
|
||||
{
|
||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileItem where isSub = 1 and subid = '{subid}'");
|
||||
@@ -2382,7 +2611,7 @@ public static class ConfigHandler
|
||||
items = await AppManager.Instance.RoutingItems();
|
||||
}
|
||||
|
||||
if (!blImportAdvancedRules && items.Count(u => u.Remarks.StartsWith(ver)) > 0)
|
||||
if (!blImportAdvancedRules && items.Count() > 0) // items.Count(u => u.Remarks.StartsWith(ver)) > 0)
|
||||
{
|
||||
//migrate
|
||||
//TODO Temporary code to be removed later
|
||||
|
||||
@@ -38,7 +38,7 @@ public static class ConnectionHandler
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
responseTime = await GetRealPingTime(webProxy, 10);
|
||||
responseTime = await GetRealPingTime(webProxy);
|
||||
if (responseTime > 0)
|
||||
{
|
||||
break;
|
||||
@@ -66,7 +66,7 @@ public static class ConnectionHandler
|
||||
/// <summary>
|
||||
/// Measures response time by sending HTTP requests through proxy.
|
||||
/// </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 responseTime = -1;
|
||||
@@ -77,7 +77,8 @@ public static class ConnectionHandler
|
||||
using var client = new HttpClient(new SocketsHttpHandler()
|
||||
{
|
||||
Proxy = webProxy,
|
||||
UseProxy = webProxy != null
|
||||
UseProxy = webProxy != null,
|
||||
ConnectTimeout = TimeSpan.FromSeconds(3)
|
||||
});
|
||||
|
||||
List<int> oneTime = [];
|
||||
|
||||
@@ -17,7 +17,7 @@ public static class CoreConfigHandler
|
||||
{
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ public class AnytlsFmt : BaseFmt
|
||||
var query = Utils.ParseQueryString(parsedUrl.Query);
|
||||
ResolveUriQuery(query, ref item);
|
||||
|
||||
if (GetQueryValue(query, "insecure") == "1")
|
||||
{
|
||||
item.AllowInsecure = Global.StringTrue;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -41,6 +46,10 @@ public class AnytlsFmt : BaseFmt
|
||||
}
|
||||
var pw = item.Password;
|
||||
var dicQuery = new Dictionary<string, string>();
|
||||
if (item.GetAllowInsecure())
|
||||
{
|
||||
dicQuery.Add("insecure", "1");
|
||||
}
|
||||
ToUriQuery(item, Global.None, ref dicQuery);
|
||||
|
||||
return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark);
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace ServiceLib.Handler.Fmt;
|
||||
|
||||
public class BaseFmt
|
||||
{
|
||||
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure" };
|
||||
|
||||
private static string UrlEncodeSafe(string? value) => Utils.UrlEncode(value ?? string.Empty);
|
||||
|
||||
protected static string GetIpv6(string address)
|
||||
@@ -67,7 +63,6 @@ public class BaseFmt
|
||||
{
|
||||
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
|
||||
}
|
||||
ToUriQueryAllowInsecure(item, ref dicQuery);
|
||||
}
|
||||
if (item.EchConfigList.IsNotEmpty())
|
||||
{
|
||||
@@ -196,25 +191,6 @@ public class BaseFmt
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -252,19 +228,6 @@ public class BaseFmt
|
||||
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));
|
||||
if (net == Global.RawNetworkAlias)
|
||||
{
|
||||
@@ -379,8 +342,13 @@ public class BaseFmt
|
||||
return query[key] ?? defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values are already unescaped by <see cref="Utils.ParseQueryString" />, so this must not
|
||||
/// unescape them a second time: a value that still holds a valid percent sequence after the
|
||||
/// first pass - an obfuscation password of "ob%41fs", say - would decay into "obAfs".
|
||||
/// </summary>
|
||||
protected static string GetQueryDecoded(NameValueCollection query, string key, string defaultValue = "")
|
||||
{
|
||||
return Utils.UrlDecode(GetQueryValue(query, key, defaultValue));
|
||||
return GetQueryValue(query, key, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,11 @@ public class FmtHandler
|
||||
{
|
||||
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]))
|
||||
{
|
||||
return TuicFmt.Resolve(str, out msg);
|
||||
|
||||
@@ -17,21 +17,17 @@ public class Hysteria2Fmt : BaseFmt
|
||||
}
|
||||
|
||||
item.Address = url.IdnHost;
|
||||
item.Port = url.Port;
|
||||
// The URI scheme makes the port optional and defaults it to 443. Uri.Port answers -1 for
|
||||
// an unregistered scheme carrying no port, which ProfileItem.IsValid then rejects.
|
||||
// Only -1 means "omitted": an explicit ":0" has to stay 0 and be rejected the way it
|
||||
// always was, instead of being quietly redirected to a server the link never named.
|
||||
item.Port = url.Port == -1 ? 443 : url.Port;
|
||||
item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped);
|
||||
item.Password = Utils.UrlDecode(url.UserInfo);
|
||||
|
||||
var query = Utils.ParseQueryString(url.Query);
|
||||
ResolveUriQuery(query, ref item);
|
||||
if (item.CertSha.IsNullOrEmpty())
|
||||
{
|
||||
item.CertSha = GetQueryDecoded(query, "pinSHA256");
|
||||
}
|
||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||
{
|
||||
Ports = GetQueryDecoded(query, "mport"),
|
||||
SalamanderPass = GetQueryDecoded(query, "obfs-password"),
|
||||
});
|
||||
ResolveHy2UriQuery(query, ref item);
|
||||
|
||||
return item;
|
||||
}
|
||||
@@ -42,8 +38,11 @@ public class Hysteria2Fmt : BaseFmt
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var url = string.Empty;
|
||||
var protocolExtraItem = item.GetProtocolExtra();
|
||||
if (!protocolExtraItem.Hy2RealmUrl.TrimEx().IsNullOrEmpty())
|
||||
{
|
||||
return ToUriForRealm(item);
|
||||
}
|
||||
|
||||
var remark = string.Empty;
|
||||
if (item.Remarks.IsNotEmpty())
|
||||
@@ -52,27 +51,7 @@ public class Hysteria2Fmt : BaseFmt
|
||||
}
|
||||
var dicQuery = new Dictionary<string, string>();
|
||||
ToUriQueryLite(item, ref dicQuery);
|
||||
var protocolExtraItem = item.GetProtocolExtra();
|
||||
|
||||
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));
|
||||
}
|
||||
ToHy2UriQuery(item, ref dicQuery);
|
||||
|
||||
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark);
|
||||
}
|
||||
@@ -94,4 +73,185 @@ public class Hysteria2Fmt : BaseFmt
|
||||
|
||||
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())
|
||||
{
|
||||
var pinSHA256 = GetQueryDecoded(query, "pinSHA256");
|
||||
item.CertSha = pinSHA256;
|
||||
if (!pinSHA256.IsNullOrEmpty())
|
||||
{
|
||||
// NOTE:
|
||||
// To accommodate Xray changes,
|
||||
// some providers issue self-signed cert links with `insecure = false` and a certificate fingerprint,
|
||||
// breaking interoperability between Xray, official Hysteria 2 client, and sing-box.
|
||||
// Since this won't compromise the overall security model,
|
||||
// `insecure = true` is automatically set when a fingerprint is detected,
|
||||
// and the value is restored when generating configurations.
|
||||
item.AllowInsecure = Global.StringTrue;
|
||||
}
|
||||
}
|
||||
item.EchConfigList = GetQueryDecoded(query, "ech");
|
||||
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));
|
||||
}
|
||||
else if (!item.Cert.IsNullOrEmpty()
|
||||
&& CertPemManager.GetLeafCertSha256Thumbprint(item.Cert) is { Length: > 0 } thumbprint)
|
||||
{
|
||||
dicQuery.Add("pinSHA256", Utils.UrlEncode(thumbprint));
|
||||
}
|
||||
if (!item.EchConfigList.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
|
||||
}
|
||||
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;
|
||||
|
||||
public class InnerFmt
|
||||
public class InnerFmt : BaseFmt
|
||||
{
|
||||
private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false));
|
||||
|
||||
@@ -50,19 +50,19 @@ public class InnerFmt
|
||||
var protocolExtra = item.GetProtocolExtra();
|
||||
// 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
|
||||
//if (!protocolExtra.SubChildItems.IsNullOrEmpty())
|
||||
// if (!protocolExtra.SubChildItems.IsNullOrEmpty())
|
||||
if (protocolExtra.SubChildItems == "self")
|
||||
{
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
SubChildItems = subid
|
||||
SubChildItems = subid,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
SubChildItems = null
|
||||
SubChildItems = null,
|
||||
};
|
||||
}
|
||||
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
|
||||
@@ -73,14 +73,14 @@ public class InnerFmt
|
||||
.ToList();
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
ChildItems = Utils.List2String(newChildIndexIds)
|
||||
ChildItems = Utils.List2String(newChildIndexIds),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
ChildItems = null
|
||||
ChildItems = null,
|
||||
};
|
||||
}
|
||||
item.SetProtocolExtra(protocolExtra);
|
||||
@@ -120,7 +120,7 @@ public class InnerFmt
|
||||
{
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
SubChildItems = "self"
|
||||
SubChildItems = "self",
|
||||
};
|
||||
}
|
||||
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
|
||||
@@ -131,7 +131,7 @@ public class InnerFmt
|
||||
.ToList();
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
ChildItems = Utils.List2String(newChildIndexIds)
|
||||
ChildItems = Utils.List2String(newChildIndexIds),
|
||||
};
|
||||
}
|
||||
itemClone.SetProtocolExtra(protocolExtra);
|
||||
@@ -175,6 +175,19 @@ public class InnerFmt
|
||||
jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false);
|
||||
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));
|
||||
if (profileItem is null)
|
||||
{
|
||||
@@ -193,6 +206,14 @@ public class InnerFmt
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (profileItem.ConfigType is EConfigType.Outbound)
|
||||
{
|
||||
if (customOutboundFilePath.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
profileItem.Address = customOutboundFilePath;
|
||||
}
|
||||
var protocolExtra = profileItem.GetProtocolExtra();
|
||||
var multipleLoad = protocolExtra.MultipleLoad;
|
||||
if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad))
|
||||
@@ -209,6 +230,26 @@ public class InnerFmt
|
||||
{
|
||||
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
|
||||
// move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj
|
||||
// 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),
|
||||
JsonObject obj => obj.Count == 0,
|
||||
JsonArray arr => arr.Count == 0,
|
||||
_ => false
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,103 @@ namespace ServiceLib.Handler.Fmt;
|
||||
|
||||
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);
|
||||
if (configObjects is not { Length: > 0 })
|
||||
var jsonNode = JsonUtils.ParseJson(strData);
|
||||
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);
|
||||
if (fullProfile is not null)
|
||||
{
|
||||
profileList.Add(fullProfile);
|
||||
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;
|
||||
}
|
||||
|
||||
List<ProfileItem> lstResult = [];
|
||||
foreach (var configObject in configObjects)
|
||||
if (jsonObject["outbounds"] is JsonArray outboundsArray)
|
||||
{
|
||||
var objectString = JsonUtils.Serialize(configObject);
|
||||
var profileIt = ResolveFull(objectString, subRemarks);
|
||||
if (!outboundsArray.Any(IsValidSingboxOutbound))
|
||||
{
|
||||
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)
|
||||
{
|
||||
lstResult.Add(profileIt);
|
||||
@@ -23,25 +107,58 @@ public class SingboxFmt : BaseFmt
|
||||
return lstResult;
|
||||
}
|
||||
|
||||
public static ProfileItem? ResolveFull(string strData, string? subRemarks)
|
||||
private static ProfileItem? ResolveOutbound(JsonObject jsonObject, string? subRemarks)
|
||||
{
|
||||
var config = JsonUtils.ParseJson(strData);
|
||||
if (config?["inbounds"] == null
|
||||
|| config["outbounds"] == null
|
||||
|| config["route"] == null
|
||||
|| config["dns"] == null)
|
||||
if (!IsValidSingboxOutbound(jsonObject))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var fileName = WriteAllText(strData);
|
||||
var type = jsonObject["type"]?.ToString();
|
||||
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
|
||||
{
|
||||
ConfigType = EConfigType.Outbound,
|
||||
CoreType = ECoreType.sing_box,
|
||||
Address = fileName,
|
||||
Remarks = subRemarks ?? "singbox_custom"
|
||||
Remarks = remarks,
|
||||
};
|
||||
|
||||
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
|
||||
{
|
||||
private static readonly List<string> _insecureQueryKeys = new() { "allowInsecure", "insecure" };
|
||||
|
||||
public static ProfileItem? Resolve(string str, out string msg)
|
||||
{
|
||||
msg = ResUI.ConfigurationFormatIncorrect;
|
||||
@@ -23,6 +25,10 @@ public class TrojanFmt : BaseFmt
|
||||
item.Password = Utils.UrlDecode(url.UserInfo);
|
||||
|
||||
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") });
|
||||
ResolveUriQuery(query, ref item);
|
||||
|
||||
@@ -41,6 +47,10 @@ public class TrojanFmt : BaseFmt
|
||||
remark = "#" + Utils.UrlEncode(item.Remarks);
|
||||
}
|
||||
var dicQuery = new Dictionary<string, string>();
|
||||
if (item.GetAllowInsecure())
|
||||
{
|
||||
_insecureQueryKeys.ForEach(q => dicQuery.Add(q, "1"));
|
||||
}
|
||||
if (!item.GetProtocolExtra().Flow.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("flow", item.GetProtocolExtra().Flow);
|
||||
|
||||
@@ -30,6 +30,10 @@ public class TuicFmt : BaseFmt
|
||||
|
||||
var query = Utils.ParseQueryString(url.Query);
|
||||
ResolveUriQuery(query, ref item);
|
||||
if (GetQueryValue(query, "allow_insecure") == "1")
|
||||
{
|
||||
item.AllowInsecure = Global.StringTrue;
|
||||
}
|
||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||
{
|
||||
CongestionControl = GetQueryValue(query, "congestion_control")
|
||||
@@ -53,7 +57,10 @@ public class TuicFmt : BaseFmt
|
||||
|
||||
var dicQuery = new Dictionary<string, string>();
|
||||
ToUriQueryLite(item, ref dicQuery);
|
||||
|
||||
if (item.GetAllowInsecure())
|
||||
{
|
||||
dicQuery.Add("allow_insecure", "1");
|
||||
}
|
||||
if (!item.GetProtocolExtra().CongestionControl.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("congestion_control", item.GetProtocolExtra().CongestionControl);
|
||||
|
||||
@@ -2,47 +2,166 @@ namespace ServiceLib.Handler.Fmt;
|
||||
|
||||
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);
|
||||
if (configObjects is not { Length: > 0 })
|
||||
var jsonNode = JsonUtils.ParseJson(strData);
|
||||
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);
|
||||
if (fullProfile is not null)
|
||||
{
|
||||
profileList.Add(fullProfile);
|
||||
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;
|
||||
}
|
||||
|
||||
List<ProfileItem> lstResult = [];
|
||||
foreach (var configObject in configObjects)
|
||||
if (jsonObject["outbounds"] is JsonArray outboundsArray)
|
||||
{
|
||||
var objectString = JsonUtils.Serialize(configObject);
|
||||
var profileIt = ResolveFull(objectString, subRemarks);
|
||||
if (!outboundsArray.Any(IsValidV2rayOutbound))
|
||||
{
|
||||
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)
|
||||
{
|
||||
lstResult.Add(profileIt);
|
||||
}
|
||||
}
|
||||
|
||||
return lstResult;
|
||||
}
|
||||
|
||||
public static ProfileItem? ResolveFull(string strData, string? subRemarks)
|
||||
public static ProfileItem? ResolveOutbound(JsonObject jsonObject, string? subRemarks)
|
||||
{
|
||||
var config = JsonUtils.ParseJson(strData);
|
||||
if (config?["inbounds"] == null
|
||||
|| config["outbounds"] == null
|
||||
|| config["routing"] == null)
|
||||
if (!IsValidV2rayOutbound(jsonObject))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var fileName = WriteAllText(strData);
|
||||
|
||||
var protocol = jsonObject["protocol"]?.ToString();
|
||||
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
|
||||
{
|
||||
ConfigType = EConfigType.Outbound,
|
||||
CoreType = ECoreType.Xray,
|
||||
Address = fileName,
|
||||
Remarks = config?["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom"
|
||||
Remarks = remarks,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public class WireguardFmt : BaseFmt
|
||||
WgReserved = GetQueryDecoded(query, "reserved"),
|
||||
WgInterfaceAddress = GetQueryDecoded(query, "address"),
|
||||
WgMtu = int.TryParse(GetQueryDecoded(query, "mtu"), out var mtuVal) ? mtuVal : null,
|
||||
WgDns = GetQueryDecoded(query, "dns"),
|
||||
});
|
||||
|
||||
return item;
|
||||
@@ -71,6 +72,10 @@ public class WireguardFmt : BaseFmt
|
||||
{
|
||||
dicQuery.Add("mtu", protoExtra.WgMtu.ToString());
|
||||
}
|
||||
if (!protoExtra.WgDns.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("dns", Utils.UrlEncode(protoExtra.WgDns));
|
||||
}
|
||||
return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark);
|
||||
}
|
||||
|
||||
@@ -133,6 +138,7 @@ public class WireguardFmt : BaseFmt
|
||||
|
||||
var wgMtu = interfaceDic.TryGetValue("MTU", out var mtuStr) && int.TryParse(mtuStr, out var mtuVal) ? mtuVal : 0;
|
||||
var wgInterfaceAddress = interfaceDic.TryGetValue("Address", out var interfaceAddress) ? interfaceAddress : string.Empty;
|
||||
var wgDns = interfaceDic.TryGetValue("DNS", out var dns) ? dns : string.Empty;
|
||||
|
||||
var index = 0;
|
||||
var resultList = new List<ProfileItem>();
|
||||
@@ -156,6 +162,7 @@ public class WireguardFmt : BaseFmt
|
||||
WgInterfaceAddress = wgInterfaceAddress,
|
||||
WgReserved = (peerDic.TryGetValue("Reserved", out var reserved) ? reserved : string.Empty).NullIfEmpty(),
|
||||
WgMtu = wgMtu > 0 ? wgMtu : null,
|
||||
WgDns = wgDns,
|
||||
};
|
||||
|
||||
var item = new ProfileItem
|
||||
|
||||
@@ -16,7 +16,6 @@ public static class SysProxyHandler
|
||||
try
|
||||
{
|
||||
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
var exceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
|
||||
if (port <= 0)
|
||||
{
|
||||
return false;
|
||||
@@ -24,17 +23,18 @@ public static class SysProxyHandler
|
||||
switch (type)
|
||||
{
|
||||
case ESysProxyType.ForcedChange when Utils.IsWindows():
|
||||
{
|
||||
GetWindowsProxyString(config, port, out var strProxy, out var strExceptions);
|
||||
ProxySettingWindows.SetProxy(strProxy, strExceptions, 2);
|
||||
break;
|
||||
}
|
||||
var (strProxy, strExceptions) = GetWindowsProxyString(config, port);
|
||||
ProxySettingWindows.SetProxy(strProxy, strExceptions, 2);
|
||||
break;
|
||||
|
||||
case ESysProxyType.ForcedChange when Utils.IsLinux():
|
||||
var exceptions = SanitizeExceptions(config);
|
||||
await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions);
|
||||
break;
|
||||
|
||||
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;
|
||||
|
||||
case ESysProxyType.ForcedClear when Utils.IsWindows():
|
||||
@@ -66,15 +66,31 @@ public static class SysProxyHandler
|
||||
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)
|
||||
{
|
||||
strExceptions = $"<local>;{strExceptions}";
|
||||
}
|
||||
|
||||
strProxy = string.Empty;
|
||||
var strProxy = string.Empty;
|
||||
if (config.SystemProxyItem.SystemProxyAdvancedProtocol.IsNullOrEmpty())
|
||||
{
|
||||
strProxy = $"{Global.Loopback}:{port}";
|
||||
@@ -86,6 +102,8 @@ public static class SysProxyHandler
|
||||
.Replace("{http_port}", port.ToString())
|
||||
.Replace("{socks_port}", port.ToString());
|
||||
}
|
||||
|
||||
return (strProxy, strExceptions);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Security.Authentication;
|
||||
using Downloader;
|
||||
|
||||
namespace ServiceLib.Helper;
|
||||
@@ -14,6 +15,8 @@ public class DownloaderHelper
|
||||
return null;
|
||||
}
|
||||
|
||||
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
|
||||
|
||||
Uri uri = new(url);
|
||||
//Authorization Header
|
||||
var headers = new WebHeaderCollection();
|
||||
@@ -22,17 +25,19 @@ public class DownloaderHelper
|
||||
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()
|
||||
{
|
||||
BlockTimeout = timeout * 1000,
|
||||
MaxTryAgainOnFailure = 2,
|
||||
RequestConfiguration =
|
||||
{
|
||||
Headers = headers,
|
||||
UserAgent = userAgent,
|
||||
ConnectTimeout = timeout * 1000,
|
||||
Proxy = webProxy
|
||||
}
|
||||
RequestConfiguration = requestConfiguration,
|
||||
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
||||
};
|
||||
|
||||
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
||||
@@ -45,10 +50,10 @@ public class DownloaderHelper
|
||||
};
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token);
|
||||
using StreamReader reader = new(stream);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
|
||||
|
||||
downloadOpt = null;
|
||||
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
|
||||
using StreamReader reader = new(stream);
|
||||
|
||||
return await reader.ReadToEndAsync(cts.Token);
|
||||
}
|
||||
@@ -60,15 +65,18 @@ public class DownloaderHelper
|
||||
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()
|
||||
{
|
||||
BlockTimeout = timeout * 1000,
|
||||
MaxTryAgainOnFailure = 2,
|
||||
RequestConfiguration =
|
||||
{
|
||||
ConnectTimeout= timeout * 1000,
|
||||
Proxy = webProxy
|
||||
}
|
||||
RequestConfiguration = requestConfiguration,
|
||||
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
||||
};
|
||||
|
||||
var lastUpdateTime = DateTime.Now;
|
||||
@@ -116,70 +124,222 @@ public class DownloaderHelper
|
||||
};
|
||||
//progress.Report("......");
|
||||
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);
|
||||
|
||||
downloadOpt = null;
|
||||
}
|
||||
|
||||
public async Task DownloadFileAsync(IWebProxy? webProxy, string url, string fileName, IProgress<double> progress, int timeout)
|
||||
public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (url.IsNullOrEmpty())
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
if (request.FilePath.IsNullOrEmpty())
|
||||
{
|
||||
throw new ArgumentNullException(nameof(url));
|
||||
throw new ArgumentNullException(nameof(request.FilePath));
|
||||
}
|
||||
if (fileName.IsNullOrEmpty())
|
||||
if (File.Exists(request.FilePath))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileName));
|
||||
}
|
||||
if (File.Exists(fileName))
|
||||
{
|
||||
File.Delete(fileName);
|
||||
File.Delete(request.FilePath);
|
||||
}
|
||||
|
||||
var downloadOpt = new DownloadConfiguration()
|
||||
var state = new FileDownloadState
|
||||
{
|
||||
BlockTimeout = timeout * 1000,
|
||||
MaxTryAgainOnFailure = 2,
|
||||
RequestConfiguration =
|
||||
{
|
||||
ConnectTimeout= timeout * 1000,
|
||||
Proxy = webProxy
|
||||
}
|
||||
Request = request,
|
||||
};
|
||||
|
||||
var requestConfiguration = new RequestConfiguration()
|
||||
{
|
||||
ConnectTimeout = (int)connectTimeout.TotalMilliseconds,
|
||||
Proxy = webProxy,
|
||||
};
|
||||
var downloadOpt = new DownloadConfiguration()
|
||||
{
|
||||
ChunkCount = 100,
|
||||
MinimumChunkSize = 8 * 1024 * 1024, // 8 MB
|
||||
MinimumSizeOfChunking = 8 * 1024 * 1024, // 8 MB
|
||||
ParallelDownload = true,
|
||||
ParallelCount = 4,
|
||||
|
||||
RequestConfiguration = requestConfiguration,
|
||||
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
||||
};
|
||||
|
||||
var progressPercentage = 0;
|
||||
var hasValue = false;
|
||||
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
||||
downloader.DownloadStarted += (sender, value) => progress?.Report(0);
|
||||
downloader.DownloadStarted += (sender, value) =>
|
||||
{
|
||||
state = state with
|
||||
{
|
||||
TotalBytes = value.TotalBytesToReceive,
|
||||
};
|
||||
onProgress.Invoke(state);
|
||||
};
|
||||
downloader.DownloadProgressChanged += (sender, value) =>
|
||||
{
|
||||
hasValue = true;
|
||||
var percent = (int)value.ProgressPercentage;// Convert.ToInt32((totalRead * 1d) / (total * 1d) * 100);
|
||||
if (progressPercentage != percent && percent % 10 == 0)
|
||||
state = state with
|
||||
{
|
||||
progressPercentage = percent;
|
||||
progress.Report(percent);
|
||||
}
|
||||
DownloadedBytes = value.ReceivedBytesSize,
|
||||
TotalBytes = value.TotalBytesToReceive,
|
||||
SpeedBytesPerSecond = value.BytesPerSecondSpeed,
|
||||
};
|
||||
onProgress.Invoke(state);
|
||||
};
|
||||
downloader.DownloadFileCompleted += (sender, value) =>
|
||||
{
|
||||
if (progress != null)
|
||||
state = state with
|
||||
{
|
||||
if (hasValue && value.Error == null)
|
||||
{
|
||||
progress.Report(101);
|
||||
}
|
||||
else if (value.Error != null)
|
||||
{
|
||||
throw value.Error;
|
||||
}
|
||||
}
|
||||
Completed = true,
|
||||
Error = value.Error,
|
||||
};
|
||||
onProgress.Invoke(state);
|
||||
};
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
await downloader.DownloadFileTaskAsync(url, fileName, cts.Token);
|
||||
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, cancellationToken);
|
||||
}
|
||||
|
||||
downloadOpt = null;
|
||||
public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (requests is not { Count: > 0 })
|
||||
{
|
||||
throw new ArgumentNullException(nameof(requests));
|
||||
}
|
||||
|
||||
var states = new FileDownloadState[requests.Count];
|
||||
for (var i = 0; i < requests.Count; i++)
|
||||
{
|
||||
states[i] = new FileDownloadState
|
||||
{
|
||||
Request = requests[i],
|
||||
};
|
||||
}
|
||||
var readOnlyStates = new ReadOnlyMemory<FileDownloadState>(states);
|
||||
|
||||
var requestConfiguration = new RequestConfiguration()
|
||||
{
|
||||
ConnectTimeout = (int)connectTimeout.TotalMilliseconds,
|
||||
Proxy = webProxy,
|
||||
|
||||
KeepAlive = true,
|
||||
};
|
||||
using var socketsHttpHandler = GetSocketsHttpHandler(requestConfiguration);
|
||||
|
||||
var parallelOptions = new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = 4,
|
||||
//CancellationToken = cancellationToken,
|
||||
};
|
||||
|
||||
await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions, async (index, parallelCancellationToken) =>
|
||||
{
|
||||
var request = requests[index];
|
||||
var downloadOpt = new DownloadConfiguration()
|
||||
{
|
||||
RequestConfiguration = requestConfiguration,
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
CustomHttpMessageHandlerFactory = () => socketsHttpHandler,
|
||||
};
|
||||
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
||||
downloader.DownloadStarted += (sender, value) =>
|
||||
{
|
||||
states[index] = states[index] with
|
||||
{
|
||||
DownloadedBytes = 0,
|
||||
TotalBytes = value.TotalBytesToReceive,
|
||||
SpeedBytesPerSecond = 0,
|
||||
Completed = false,
|
||||
};
|
||||
onProgress.Invoke(readOnlyStates);
|
||||
};
|
||||
downloader.DownloadProgressChanged += (sender, value) =>
|
||||
{
|
||||
states[index] = states[index] with
|
||||
{
|
||||
DownloadedBytes = value.ReceivedBytesSize,
|
||||
TotalBytes = value.TotalBytesToReceive,
|
||||
SpeedBytesPerSecond = value.BytesPerSecondSpeed,
|
||||
Completed = false,
|
||||
};
|
||||
onProgress.Invoke(readOnlyStates);
|
||||
};
|
||||
downloader.DownloadFileCompleted += (sender, value) =>
|
||||
{
|
||||
var newState = states[index] with { Completed = true };
|
||||
if (value.Error != null)
|
||||
{
|
||||
newState = newState with { Error = value.Error };
|
||||
}
|
||||
states[index] = newState;
|
||||
onProgress.Invoke(readOnlyStates);
|
||||
};
|
||||
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, parallelCancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
public static AppManager Instance => _instance.Value;
|
||||
public Config Config => _config;
|
||||
public IWindowDialog WindowDialog { get; set; } = null!;
|
||||
|
||||
public int StatePort
|
||||
{
|
||||
@@ -244,9 +245,26 @@ public sealed class AppManager
|
||||
{
|
||||
return [];
|
||||
}
|
||||
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
|
||||
.Where(it => ids.Contains(it.IndexId))
|
||||
.ToListAsync();
|
||||
|
||||
if (ids.Count <= Global.SqliteMaxBatchSize)
|
||||
{
|
||||
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)
|
||||
@@ -257,18 +275,11 @@ public sealed class AppManager
|
||||
|
||||
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
|
||||
{
|
||||
var idList = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
|
||||
if (idList.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var items = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
|
||||
.Where(it => idList.Contains(it.IndexId))
|
||||
.ToListAsync();
|
||||
var ids = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
|
||||
var items = await GetProfileItemsByIndexIds(ids);
|
||||
var itemMap = items.ToDictionary(it => it.IndexId);
|
||||
|
||||
return idList.Select(itemMap.GetValueOrDefault)
|
||||
return ids.Select(itemMap.GetValueOrDefault)
|
||||
.Where(item => item != null)
|
||||
.ToList();
|
||||
}
|
||||
@@ -652,7 +663,7 @@ public sealed class AppManager
|
||||
return Global.SsSecuritiesInSingbox;
|
||||
}
|
||||
|
||||
public ECoreType GetCoreType(ProfileItem profileItem, EConfigType eConfigType)
|
||||
public ECoreType GetCoreType(ProfileItem? profileItem, EConfigType eConfigType)
|
||||
{
|
||||
if (profileItem?.CoreType != null)
|
||||
{
|
||||
|
||||
@@ -10,180 +10,17 @@ public class CertPemManager
|
||||
{
|
||||
private static readonly string _tag = "CertPemManager";
|
||||
private static readonly Lazy<CertPemManager> _instance = new(() => new());
|
||||
private Config _config;
|
||||
|
||||
/// <summary>
|
||||
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase)
|
||||
public async Task Init(Config config)
|
||||
{
|
||||
"EBD41040E4BB3EC742C9E381D31EF2A41A48B6685C96E7CEF3C1DF6CD4331C99", // GlobalSign Root CA
|
||||
"6DC47172E01CBCB0BF62580D895FE2B8AC9AD4F873801E0C10B9C837D21EB177", // Entrust.net Premium 2048 Secure Server CA
|
||||
"73C176434F1BC6D5ADF45B0E76E727287C8DE57616C1E6E6141A2B2CBC7D8E4C", // Entrust Root Certification Authority
|
||||
"D8E0FEBC1DB2E38D00940F37D27D41344D993E734B99D5656D9778D4D8143624", // Certum Root CA
|
||||
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root
|
||||
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2
|
||||
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3
|
||||
"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
|
||||
};
|
||||
if (_config != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_config = config;
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static CertPemManager Instance => _instance.Value;
|
||||
|
||||
@@ -292,7 +129,7 @@ public class CertPemManager
|
||||
/// <summary>
|
||||
/// Validate server certificate with CA pinning
|
||||
/// </summary>
|
||||
private static bool ValidateServerCertificate(
|
||||
private bool ValidateServerCertificate(
|
||||
object _,
|
||||
X509Certificate? certificate,
|
||||
X509Chain? chain,
|
||||
@@ -327,9 +164,10 @@ public class CertPemManager
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -442,4 +280,81 @@ public class CertPemManager
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetLeafCertSha256Thumbprint(string pemCertChain, bool includeColon = false)
|
||||
{
|
||||
var certs = ParsePemChain(pemCertChain);
|
||||
if (certs.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
foreach (var certStr in certs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cert = X509Certificate2.CreateFromPem(certStr);
|
||||
var extension = cert.Extensions
|
||||
.OfType<X509BasicConstraintsExtension>()
|
||||
.FirstOrDefault();
|
||||
var isCa = extension?.CertificateAuthority == true;
|
||||
if (isCa)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
|
||||
return includeColon ? string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))) : thumbprint;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
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();
|
||||
sb.AppendLine("#!/bin/bash");
|
||||
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 procService = new ProcessService(
|
||||
@@ -80,6 +91,8 @@ public class CoreAdminManager
|
||||
.ExecuteBufferedAsync();
|
||||
|
||||
await UpdateFunc(false, result.StandardOutput.ToString());
|
||||
|
||||
await Task.Delay(1000); // Wait for a second to ensure the process is killed
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -8,8 +8,10 @@ public class CoreManager
|
||||
private static readonly Lazy<CoreManager> _instance = new(() => new());
|
||||
public static CoreManager Instance => _instance.Value;
|
||||
private Config _config;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private WindowsJobService? _processJob;
|
||||
|
||||
private ProcessService? _processService;
|
||||
private ProcessService? _processPreService;
|
||||
private bool _linuxSudo = false;
|
||||
@@ -83,13 +85,14 @@ public class CoreManager
|
||||
await CoreStop();
|
||||
await Task.Delay(100);
|
||||
|
||||
if (Utils.IsWindows() && _config.TunModeItem.EnableTun)
|
||||
if (Utils.IsWindows() && (mainContext?.IsTunEnabled == true || preContext?.IsTunEnabled == true))
|
||||
{
|
||||
await Task.Delay(100);
|
||||
await WindowsUtils.RemoveTunDevice();
|
||||
}
|
||||
|
||||
await CoreStart(mainContext);
|
||||
await WaitForProxyPort(preContext);
|
||||
await CoreStartPreService(preContext);
|
||||
|
||||
AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType;
|
||||
@@ -180,7 +183,7 @@ public class CoreManager
|
||||
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
|
||||
|
||||
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)
|
||||
{
|
||||
return;
|
||||
@@ -198,7 +201,7 @@ public class CoreManager
|
||||
if (result.Success)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return;
|
||||
@@ -213,11 +216,93 @@ public class CoreManager
|
||||
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
|
||||
|
||||
#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);
|
||||
if (fileName.IsNullOrEmpty())
|
||||
@@ -229,9 +314,7 @@ public class CoreManager
|
||||
try
|
||||
{
|
||||
if (mayNeedSudo
|
||||
&& _config.TunModeItem.EnableTun
|
||||
&& (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo)
|
||||
&& Utils.IsNonWindows())
|
||||
&& ShouldRunAsSudo(isTunLaunch, coreInfo.CoreType, Utils.IsNonWindows()))
|
||||
{
|
||||
_linuxSudo = true;
|
||||
await CoreAdminManager.Instance.Init(_config, _updateFunc);
|
||||
|
||||
@@ -118,7 +118,7 @@ public class GroupProfileManager
|
||||
return childProfiles?.Where(p =>
|
||||
p != null &&
|
||||
p.IsValid() &&
|
||||
!p.ConfigType.IsComplexType() &&
|
||||
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
||||
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
|
||||
)
|
||||
.ToList() ?? [];
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
using NetBridgeLib.Services;
|
||||
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public sealed class NetBridgeManager
|
||||
{
|
||||
private static readonly Lazy<NetBridgeManager> _instance = new(() => new());
|
||||
public static NetBridgeManager Instance => _instance.Value;
|
||||
private readonly Config _config = AppManager.Instance.Config;
|
||||
private NetBridgeService? _netBridgeService;
|
||||
private bool _isProxyRunning;
|
||||
private bool _isInitialized;
|
||||
private List<NetBridgeRuleConfig> _ruleConfigs = [];
|
||||
private Func<bool, string, Task>? _updateFunc;
|
||||
private uint _proxyConfigId;
|
||||
|
||||
public async Task Init(Func<bool, string, Task>? updateFunc = null)
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_updateFunc = updateFunc;
|
||||
|
||||
try
|
||||
{
|
||||
_netBridgeService = new NetBridgeService();
|
||||
_netBridgeService.LogReceived += msg =>
|
||||
{
|
||||
var message = $"NetBridge Log: {msg}";
|
||||
_ = _updateFunc?.Invoke(false, message);
|
||||
};
|
||||
|
||||
_netBridgeService.ConnectionReceived += (processName, pid, destIp, destPort, proxyInfo) =>
|
||||
{
|
||||
var message = $"NetBridge Connection: {processName} (PID: {pid}) -> {destIp}:{destPort} -> {proxyInfo}";
|
||||
_ = _updateFunc?.Invoke(false, message);
|
||||
};
|
||||
|
||||
_ruleConfigs = BuildRuleConfigs(_config.NetBridgeItem?.RuleProcess);
|
||||
_isInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var error = $"Failed to initialize NetBridgeService: {ex.Message}";
|
||||
await _updateFunc?.Invoke(true, error);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> Start()
|
||||
{
|
||||
if (_isProxyRunning)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_netBridgeService == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var started = _netBridgeService.Start();
|
||||
if (!started)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_isProxyRunning = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var error = $"Failed to start NetBridgeService: {ex.Message}";
|
||||
await _updateFunc?.Invoke(true, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> Stop()
|
||||
{
|
||||
if (!_isProxyRunning)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_netBridgeService == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stopped = _netBridgeService.Stop();
|
||||
if (!stopped)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_isProxyRunning = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var error = $"Failed to stop NetBridgeService: {ex.Message}";
|
||||
await _updateFunc?.Invoke(true, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateRoutes(string? ruleProcess)
|
||||
{
|
||||
var newRuleConfigs = BuildRuleConfigs(ruleProcess);
|
||||
|
||||
_ruleConfigs = newRuleConfigs;
|
||||
|
||||
if (!_isProxyRunning)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return await ApplyRoutesInternal();
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateProxyConfig(string proxyHost, int proxyPort)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_netBridgeService == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var proxyType = "SOCKS5";
|
||||
var username = "";
|
||||
var password = "";
|
||||
|
||||
if (_proxyConfigId > 0)
|
||||
{
|
||||
var edited = _netBridgeService.EditProxyConfig(_proxyConfigId, proxyType, proxyHost, (ushort)proxyPort, username, password);
|
||||
if (!edited)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_proxyConfigId = _netBridgeService.AddProxyConfig(proxyType, proxyHost, (ushort)proxyPort, username, password);
|
||||
if (_proxyConfigId == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return await Task.FromResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var error = $"Failed to update proxy config: {ex.Message}";
|
||||
await _updateFunc?.Invoke(true, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SetDnsViaProxy(bool enable)
|
||||
{
|
||||
if (_netBridgeService == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_netBridgeService.SetDnsViaProxy(enable);
|
||||
|
||||
return await Task.FromResult(true);
|
||||
}
|
||||
|
||||
private async Task<bool> ApplyRoutesInternal()
|
||||
{
|
||||
if (_netBridgeService == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
List<NetBridgeRuleConfig> rules;
|
||||
|
||||
rules = _ruleConfigs.Select(JsonUtils.DeepCopy).ToList();
|
||||
|
||||
foreach (var rule in rules.Where(x => x.RuleId > 0))
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = _netBridgeService.DeleteRule(rule.RuleId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < rules.Count; i++)
|
||||
{
|
||||
var rule = rules[i];
|
||||
var newRuleId = _netBridgeService.AddRule(rule.ProcessName, rule.TargetHosts, rule.TargetPorts, rule.Protocol, rule.Action, rule.ProxyConfigId);
|
||||
if (newRuleId == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
rules[i].RuleId = newRuleId;
|
||||
}
|
||||
|
||||
_ruleConfigs = rules;
|
||||
|
||||
return await Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static List<NetBridgeRuleConfig> BuildRuleConfigs(string? ruleProcess)
|
||||
{
|
||||
if (ruleProcess.IsNullOrEmpty())
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
var processNames = Utils.String2List(Utils.Convert2Comma(ruleProcess));
|
||||
return processNames.Select(processName => new NetBridgeRuleConfig
|
||||
{
|
||||
ProcessName = processName,
|
||||
TargetHosts = "*",
|
||||
TargetPorts = "*",
|
||||
Protocol = "BOTH",
|
||||
Action = "PROXY",
|
||||
ProxyConfigId = 0
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
//using System.Reactive.Linq;
|
||||
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public class ProfileExManager
|
||||
|
||||
@@ -29,12 +29,12 @@ public class Config
|
||||
public SystemProxyItem SystemProxyItem { get; set; }
|
||||
public WebDavItem WebDavItem { get; set; }
|
||||
public CheckUpdateItem CheckUpdateItem { get; set; }
|
||||
public NetBridgeItem NetBridgeItem { get; set; }
|
||||
public Fragment4RayItem? Fragment4RayItem { get; set; }
|
||||
public List<InItem> Inbound { get; set; }
|
||||
public List<KeyEventItem> GlobalHotkeys { get; set; }
|
||||
public List<CoreTypeItem> CoreTypeItem { get; set; }
|
||||
public SimpleDNSItem SimpleDNSItem { get; set; }
|
||||
public HappyEyeballs4RayItem HappyEyeballs4RayItem { get; set; }
|
||||
|
||||
#endregion other entities
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ public class GUIItem
|
||||
public int TrayMenuServersLimit { get; set; } = 20;
|
||||
public bool EnableHWA { get; set; } = false;
|
||||
public bool EnableLog { get; set; } = true;
|
||||
public string? RootCertProvider { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -102,6 +103,7 @@ public class UIItem
|
||||
public bool MacOSShowInDock { get; set; }
|
||||
public List<ColumnItem> MainColumnItem { get; set; }
|
||||
public List<WindowSizeItem> WindowSizeItem { get; set; }
|
||||
public bool HideColumnIpInfo { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -145,8 +147,10 @@ public class TunModeItem
|
||||
public int Mtu { get; set; }
|
||||
public bool EnableIPv6Address { get; set; }
|
||||
public string IcmpRouting { get; set; }
|
||||
public bool EnableLegacyProtect { get; set; }
|
||||
public bool EnableLegacyProtect { get; set; } = true;
|
||||
public List<string>? RouteExcludeAddress { get; set; }
|
||||
public string IPv4Address { get; set; }
|
||||
public string IPv6Address { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -247,8 +251,15 @@ public class CheckUpdateItem
|
||||
public class Fragment4RayItem
|
||||
{
|
||||
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? Interval { get; set; }
|
||||
// migration end
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -266,21 +277,27 @@ public class SimpleDNSItem
|
||||
public bool? AddCommonHosts { get; set; }
|
||||
public bool? FakeIP { get; set; }
|
||||
public bool? GlobalFakeIp { get; set; }
|
||||
public string? FakeIPRange { get; set; }
|
||||
public bool? BlockBindingQuery { get; set; }
|
||||
public bool? BlockAAAAQuery { get; set; }
|
||||
public string? DirectDNS { get; set; }
|
||||
public string? RemoteDNS { get; set; }
|
||||
public string? BootstrapDNS { get; set; }
|
||||
public string? Strategy4Freedom { get; set; }
|
||||
public string? Strategy4Proxy { get; set; }
|
||||
public string? Strategy4ProxyDial { get; set; }
|
||||
public bool? ServeStale { get; set; }
|
||||
public bool? ParallelQuery { get; set; }
|
||||
public string? Hosts { get; set; }
|
||||
public string? DirectExpectedIPs { get; set; }
|
||||
public bool? EnableHappyEyeballs { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class NetBridgeItem
|
||||
public class HappyEyeballs4RayItem
|
||||
{
|
||||
public string? RuleProcess { get; set; }
|
||||
public bool EnableDnsViaProxy { get; set; }
|
||||
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 FullConfigTemplateItem? FullConfigTemplate { get; init; } = new();
|
||||
|
||||
public Dictionary<string, string> CustomOutboundContent { get; init; } = new();
|
||||
|
||||
// Test ServerTestItem Map
|
||||
public Dictionary<string, string> ServerTestItemMap { get; init; } = new();
|
||||
|
||||
// TUN Compatibility
|
||||
public bool IsTunEnabled { get; init; } = false;
|
||||
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 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? 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
|
||||
@@ -334,6 +344,7 @@ public abstract class DialFields4Sbox
|
||||
public Multiplex4Sbox? multiplex { get; set; }
|
||||
public Transport4Sbox? transport { get; set; }
|
||||
public HyObfs4Sbox? obfs { get; set; }
|
||||
public HyRealm4Sbox? realm { get; set; }
|
||||
}
|
||||
|
||||
public abstract class BaseServer4Sbox : DialFields4Sbox
|
||||
|
||||
@@ -4,6 +4,7 @@ public class V2rayConfig
|
||||
{
|
||||
public Log4Ray log { get; set; }
|
||||
public object dns { get; set; }
|
||||
public FakeDns4Ray? fakedns { get; set; }
|
||||
public List<Inbounds4Ray> inbounds { get; set; }
|
||||
public List<Outbounds4Ray> outbounds { get; set; }
|
||||
public Routing4Ray routing { get; set; }
|
||||
@@ -43,6 +44,12 @@ public class Log4Ray
|
||||
public string? loglevel { get; set; }
|
||||
}
|
||||
|
||||
public class FakeDns4Ray
|
||||
{
|
||||
public string? ipPool { get; set; }
|
||||
public long? poolSize { get; set; }
|
||||
}
|
||||
|
||||
public class Inbounds4Ray
|
||||
{
|
||||
public string tag { get; set; }
|
||||
@@ -136,8 +143,6 @@ public class Outboundsettings4Ray
|
||||
|
||||
public Response4Ray? response { get; set; }
|
||||
|
||||
public string? domainStrategy { get; set; }
|
||||
|
||||
public int? userLevel { get; set; }
|
||||
|
||||
public string? secretKey { get; set; }
|
||||
@@ -146,6 +151,16 @@ public class Outboundsettings4Ray
|
||||
|
||||
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 bool? noKernelTun { get; set; }
|
||||
@@ -157,6 +172,8 @@ public class Outboundsettings4Ray
|
||||
public int? workers { get; set; }
|
||||
|
||||
public int? version { get; set; }
|
||||
|
||||
public List<string>? remoteDNS { get; set; }
|
||||
}
|
||||
|
||||
public class WireguardPeer4Ray
|
||||
@@ -226,6 +243,7 @@ public class Dns4Ray
|
||||
public List<object> servers { get; set; }
|
||||
public bool? serveStale { get; set; }
|
||||
public bool? enableParallelQuery { get; set; }
|
||||
public string? queryStrategy { get; set; }
|
||||
public string? tag { get; set; }
|
||||
}
|
||||
|
||||
@@ -276,7 +294,6 @@ public class BalancersItem4Ray
|
||||
public List<string>? selector { get; set; }
|
||||
public BalancersStrategy4Ray? strategy { get; set; }
|
||||
public string? tag { get; set; }
|
||||
public string? fallbackTag { get; set; }
|
||||
}
|
||||
|
||||
public class BalancersStrategy4Ray
|
||||
@@ -289,16 +306,7 @@ public class BalancersStrategySettings4Ray
|
||||
{
|
||||
public int? expected { get; set; }
|
||||
public string? maxRTT { get; set; }
|
||||
public float? 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 double? tolerance { get; set; }
|
||||
}
|
||||
|
||||
public class Observatory4Ray
|
||||
@@ -359,8 +367,6 @@ public class StreamSettings4Ray
|
||||
|
||||
public class TlsSettings4Ray
|
||||
{
|
||||
public bool? allowInsecure { get; set; }
|
||||
|
||||
public string? serverName { get; set; }
|
||||
|
||||
public List<string>? alpn { get; set; }
|
||||
@@ -507,11 +513,18 @@ public class MaskSettings4Ray
|
||||
|
||||
public string? password { get; set; }
|
||||
|
||||
public string? url { get; set; }
|
||||
public List<string>? stunServers { get; set; }
|
||||
public string? packetSize { get; set; }
|
||||
|
||||
// fragment
|
||||
public string? packets { get; set; }
|
||||
|
||||
public string? length { 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
|
||||
public int? reset { get; set; }
|
||||
@@ -542,15 +555,20 @@ public class AccountsItem4Ray
|
||||
|
||||
public class Sockopt4Ray
|
||||
{
|
||||
public string? domainStrategy { get; set; }
|
||||
|
||||
public string? dialerProxy { get; set; }
|
||||
|
||||
[JsonPropertyName("interface")]
|
||||
public string? Interface { get; set; }
|
||||
|
||||
public HappyEyeballs4Ray? happyEyeballs { get; set; }
|
||||
}
|
||||
|
||||
public class FragmentItem4Ray
|
||||
public class HappyEyeballs4Ray
|
||||
{
|
||||
public string? packets { get; set; }
|
||||
public string? length { get; set; }
|
||||
public string? interval { get; set; }
|
||||
public int? tryDelayMs { get; set; }
|
||||
public bool? prioritizeIPv6 { get; set; }
|
||||
public int? interleave { get; set; }
|
||||
public int? maxConcurrentTry { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
public class CheckUpdateModel : ReactiveObject
|
||||
public partial class CheckUpdateModel : ReactiveObject
|
||||
{
|
||||
public bool? IsSelected { 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 bool? IsFinished { get; set; }
|
||||
public bool IsGeoFile { get; set; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
[Serializable]
|
||||
public class ClashProxyModel : ReactiveObject
|
||||
public partial class ClashProxyModel : ReactiveObject
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
@@ -9,9 +9,9 @@ public class ClashProxyModel : ReactiveObject
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
public record FileDownloadState
|
||||
{
|
||||
public required FileDownloadRequest Request { get; init; }
|
||||
public long DownloadedBytes { get; init; } = 0;
|
||||
public long TotalBytes { get; init; } = 0;
|
||||
public double SpeedBytesPerSecond { get; init; } = 0;
|
||||
public bool Completed { get; init; } = false;
|
||||
|
||||
public Exception? Error { get; init; }
|
||||
public bool IsFailed => Error != null;
|
||||
}
|
||||
|
||||
public record FileDownloadRequest
|
||||
{
|
||||
public required string FileUrl { get; init; }
|
||||
public required string FilePath { get; init; }
|
||||
public string? DisplayFileName { get; init; }
|
||||
|
||||
public string FileName => DisplayFileName ?? Path.GetFileName(FilePath);
|
||||
}
|
||||
@@ -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,12 +0,0 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
public sealed class NetBridgeRuleConfig
|
||||
{
|
||||
public uint RuleId { get; set; }
|
||||
public string ProcessName { get; set; }
|
||||
public string TargetHosts { get; set; }
|
||||
public string TargetPorts { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public string Action { get; set; }
|
||||
public uint ProxyConfigId { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
[Serializable]
|
||||
public class ProfileItemModel : ReactiveObject
|
||||
public partial class ProfileItemModel : ReactiveObject
|
||||
{
|
||||
public bool IsActive { get; set; }
|
||||
public string IndexId { get; set; }
|
||||
@@ -16,30 +16,30 @@ public class ProfileItemModel : ReactiveObject
|
||||
public int Sort { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public int Delay { get; set; }
|
||||
public partial int Delay { get; set; }
|
||||
|
||||
public decimal Speed { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string DelayVal { get; set; }
|
||||
public partial string DelayVal { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string SpeedVal { get; set; }
|
||||
public partial string SpeedVal { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string IpInfo { get; set; }
|
||||
public partial string IpInfo { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string TodayUp { get; set; }
|
||||
public partial string TodayUp { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string TodayDown { get; set; }
|
||||
public partial string TodayDown { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string TotalUp { get; set; }
|
||||
public partial string TotalUp { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string TotalDown { get; set; }
|
||||
public partial string TotalDown { get; set; }
|
||||
|
||||
public string GetSummary()
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ public class ProfileItem
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
if (IsComplex())
|
||||
if (IsComplex() || ConfigType == EConfigType.Outbound)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ public record ProtocolExtraItem
|
||||
public bool? Uot { get; init; }
|
||||
public string? CongestionControl { get; init; }
|
||||
|
||||
// http outbound
|
||||
public string? HttpHeaders { get; init; }
|
||||
|
||||
// vmess
|
||||
public string? AlterId { get; init; }
|
||||
public string? VmessSecurity { get; init; }
|
||||
@@ -24,6 +27,7 @@ public record ProtocolExtraItem
|
||||
public string? WgInterfaceAddress { get; init; }
|
||||
public string? WgReserved { get; init; }
|
||||
public int? WgMtu { get; init; }
|
||||
public string? WgDns { get; init; }
|
||||
|
||||
// hysteria2
|
||||
public string? SalamanderPass { get; init; }
|
||||
@@ -31,6 +35,12 @@ public record ProtocolExtraItem
|
||||
public int? DownMbps { get; init; }
|
||||
public string? Ports { 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
|
||||
public int? InsecureConcurrency { get; init; }
|
||||
@@ -42,4 +52,7 @@ public record ProtocolExtraItem
|
||||
public string? SubChildItems { get; init; }
|
||||
public string? Filter { 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 string? Memo { get; set; }
|
||||
|
||||
public ECoreType? CustomCoreType { get; set; }
|
||||
}
|
||||
|
||||
Generated
+325
-46
@@ -178,7 +178,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Failed to import custom configuration Configuration 的本地化字符串。
|
||||
/// 查找类似 Failed to import configuration 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string FailedImportedCustomServer {
|
||||
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>
|
||||
/// 查找类似 Please enter the correct port format. 的本地化字符串。
|
||||
/// </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>
|
||||
/// 查找类似 Please enter the local listening port. 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -268,7 +268,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Please browse to import Configuration configuration 的本地化字符串。
|
||||
/// 查找类似 Please browse to import configuration 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string FillServerAddressCustom {
|
||||
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>
|
||||
/// 查找类似 Invalid address (URL) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -420,6 +438,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Custom config core 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string LvCustomCoreType {
|
||||
get {
|
||||
return ResourceManager.GetString("LvCustomCoreType", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Custom icon 的本地化字符串。
|
||||
/// </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>
|
||||
/// 查找类似 Add a custom configuration 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1320,15 +1356,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Process traffic hijacking (experimental) 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuNetBridge {
|
||||
get {
|
||||
return ResourceManager.GetString("menuNetBridge", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 New Update 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1924,7 +1951,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public static string MsgAllowInsecureDeprecated {
|
||||
get {
|
||||
@@ -1968,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>
|
||||
/// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2076,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>
|
||||
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2310,15 +2355,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 The process names that need to be proxied; separate multiple processes with commas. Processes not in the list will also be hijacked, but will connect directly. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string NetBridgeRuleTips {
|
||||
get {
|
||||
return ResourceManager.GetString("NetBridgeRuleTips", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Non-VMess or SS protocol 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2617,7 +2653,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Custom configuration Configuration imported successfully 的本地化字符串。
|
||||
/// 查找类似 Custom configuration imported successfully 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string SuccessfullyImportedCustomServer {
|
||||
get {
|
||||
@@ -2733,6 +2769,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Block AAAA Queries 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbBlockAAAAQueries {
|
||||
get {
|
||||
return ResourceManager.GetString("TbBlockAAAAQueries", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Block IPv6 queries when enabled 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbBlockAAAAQueriesTips {
|
||||
get {
|
||||
return ResourceManager.GetString("TbBlockAAAAQueriesTips", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Block SVCB and HTTPS Queries 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2743,7 +2797,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Block ECH and HTTP/3 availability checks when enabled 的本地化字符串。
|
||||
/// 查找类似 Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbBlockSVCBHTTPSQueriesTips {
|
||||
get {
|
||||
@@ -2916,6 +2970,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>
|
||||
/// 查找类似 Direct Target Resolution Strategy 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2952,6 +3015,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 DNS 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbDNS {
|
||||
get {
|
||||
return ResourceManager.GetString("TbDNS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 DNS Hosts: ("domain1 ip1 ip2" per line) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3052,11 +3124,20 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Enables process traffic hijacking (experimental), similar to Proxifer; can coexist with system proxy; conflicts with TUN mode, please do not enable them simultaneously;please allow v2rayN to use a private network in your firewall. 的本地化字符串。
|
||||
/// 查找类似 Enable Happy Eyeballs 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbEnableNetBridge {
|
||||
public static string TbEnableHappyEyeballs {
|
||||
get {
|
||||
return ResourceManager.GetString("TbEnableNetBridge", resourceCulture);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3088,7 +3169,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public static string TbFakeIPTips {
|
||||
get {
|
||||
@@ -3177,6 +3258,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Gecko Packet Size (min/max) 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbGeckoPacketSize {
|
||||
get {
|
||||
return ResourceManager.GetString("TbGeckoPacketSize", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Global Hotkey Settings 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3240,6 +3330,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>
|
||||
/// 查找类似 ICMP routing policy 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3294,6 +3411,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>
|
||||
/// 查找类似 Most Stable 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3321,6 +3456,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>
|
||||
/// 查找类似 Address (IPv4, IPv6) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3483,6 +3627,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>
|
||||
/// 查找类似 Public Key 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3582,6 +3744,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>
|
||||
/// 查找类似 Round Robin 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3708,15 +3888,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Save rules 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSaveNetBridgeRule {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSaveNetBridgeRule", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 sing-box Full Config Template 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3853,7 +4024,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public static string TbSettingsBindInterfaceTip {
|
||||
get {
|
||||
@@ -4140,6 +4311,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>
|
||||
/// 查找类似 Geo files source (optional) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -4797,6 +5058,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Xray Only 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbXrayOnly {
|
||||
get {
|
||||
return ResourceManager.GetString("TbXrayOnly", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 The delay: {0} ms, {1} 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -4860,6 +5130,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>
|
||||
/// 查找类似 *Default value raw 的本地化字符串。
|
||||
/// </summary>
|
||||
|
||||
@@ -1104,6 +1104,39 @@
|
||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||
<value>فعال کردن فرگمنت</value>
|
||||
</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">
|
||||
<value>فعال کردن کش فایل مجموعه قوانین برای sing-box</value>
|
||||
</data>
|
||||
@@ -1423,7 +1456,7 @@
|
||||
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled</value>
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Please fill in the correct config template</value>
|
||||
@@ -1462,7 +1495,7 @@
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
<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 name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Please Add At Least One Configuration</value>
|
||||
@@ -1710,9 +1743,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
</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">
|
||||
<value>Bind Interface</value>
|
||||
</data>
|
||||
@@ -1740,4 +1770,4 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="menuNewUpdate" xml:space="preserve">
|
||||
<value>New Update</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
@@ -1101,6 +1101,39 @@
|
||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||
<value>Activer le fragmentation (Fragment)</value>
|
||||
</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">
|
||||
<value>Activer le fichier de cache de sing-box (fichiers règles)</value>
|
||||
</data>
|
||||
@@ -1420,7 +1453,7 @@
|
||||
<value>DNS personnalisé activé ; la configuration de cette page sera ignorée</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Une fois activé, bloque les requêtes ECH et de disponibilité HTTP/3</value>
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Veuillez saisir un modèle de configuration valide</value>
|
||||
@@ -1459,7 +1492,7 @@
|
||||
<value>Choisir une config.</value>
|
||||
</data>
|
||||
<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 name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Veuillez ajouter au moins une configuration</value>
|
||||
@@ -1701,9 +1734,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>Veuillez saisir l’adresse IPv4 correcte de SendThrough.</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||
<value>Lier l'interface</value>
|
||||
</data>
|
||||
|
||||
@@ -1104,6 +1104,39 @@
|
||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||
<value>Fragment engedélyezése</value>
|
||||
</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">
|
||||
<value>Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok)</value>
|
||||
</data>
|
||||
@@ -1423,7 +1456,7 @@
|
||||
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled</value>
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Please fill in the correct config template</value>
|
||||
@@ -1462,7 +1495,7 @@
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
<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 name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Please Add At Least One Configuration</value>
|
||||
@@ -1710,9 +1743,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
</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">
|
||||
<value>Bind Interface</value>
|
||||
</data>
|
||||
|
||||
@@ -1104,6 +1104,39 @@
|
||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||
<value>Aktifkan fragment</value>
|
||||
</data>
|
||||
<data name="TbSettingsFragmentPackets" xml:space="preserve">
|
||||
<value>Paket Fragment</value>
|
||||
</data>
|
||||
<data name="TbSettingsFragmentPacketsTip" xml:space="preserve">
|
||||
<value>Paket untuk difragmentasikan: tlshello (TLS ClientHello) atau 1-1 s.d. 1-5 (N paket TCP pertama)</value>
|
||||
</data>
|
||||
<data name="TbSettingsFragmentLength" xml:space="preserve">
|
||||
<value>Panjang Fragment</value>
|
||||
</data>
|
||||
<data name="TbSettingsFragmentLengthTip" xml:space="preserve">
|
||||
<value>Rentang ukuran fragment (byte), cth: 50-100. Nilai pertama harus ≤ kedua. Kosong = default.</value>
|
||||
</data>
|
||||
<data name="TbSettingsFragmentInterval" xml:space="preserve">
|
||||
<value>Interval Fragment</value>
|
||||
</data>
|
||||
<data name="TbSettingsFragmentIntervalTip" xml:space="preserve">
|
||||
<value>Jeda antar fragment (ms), cth: 10-20. Rentang 1-100. Nilai pertama harus ≤ kedua. Kosong = default.</value>
|
||||
</data>
|
||||
<data name="TbSettingsFragmentMaxSplit" xml:space="preserve">
|
||||
<value>Maks Split</value>
|
||||
</data>
|
||||
<data name="TbSettingsFragmentMaxSplitTip" xml:space="preserve">
|
||||
<value>Maks jumlah split (0 = unlimited, 0-10000). Hanya Xray-core. Kosong = default.</value>
|
||||
</data>
|
||||
<data name="TbSettingsFragmentFallbackDelay" xml:space="preserve">
|
||||
<value>Fallback Delay</value>
|
||||
</data>
|
||||
<data name="TbSettingsFragmentFallbackDelayTip" xml:space="preserve">
|
||||
<value>Delay fallback untuk TLS fragment. Hanya sing-box. Kosong = default.</value>
|
||||
</data>
|
||||
<data name="FillFragmentParameterError" xml:space="preserve">
|
||||
<value>Format range tidak valid. Gunakan 'from-to' (cth: 50-100).</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableCacheFile4Sbox" xml:space="preserve">
|
||||
<value>Aktifkan file cache untuk sing-box (file ruleset)</value>
|
||||
</data>
|
||||
@@ -1423,7 +1456,7 @@
|
||||
<value>DNS kustom diaktifkan, pengaturan halaman ini tidak berlaku</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Blokir pemeriksaan ketersediaan ECH dan HTTP/3 saat diaktifkan</value>
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Isi template konfigurasi yang benar</value>
|
||||
@@ -1462,7 +1495,7 @@
|
||||
<value>Pilih profil</value>
|
||||
</data>
|
||||
<data name="TbFakeIPTips" xml:space="preserve">
|
||||
<value>Berlaku secara global secara default, dengan filter FakeIP bawaan (hanya sing-box).</value>
|
||||
<value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
|
||||
</data>
|
||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Silakan tambahkan setidaknya satu konfigurasi.</value>
|
||||
@@ -1701,12 +1734,6 @@ Tindakan "Ambil sertifikat" mungkin gagal jika sertifikat self-signed digunakan
|
||||
<data name="TransportExtra" xml:space="preserve">
|
||||
<value>XHTTP Extra</value>
|
||||
</data>
|
||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
||||
<value>Izinkan pengambilan sertifikat tidak aman (self-signed)</value>
|
||||
</data>
|
||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
||||
<value>Hanya untuk mengambil sertifikat self-signed. Fitur ini dapat mengekspos Anda pada risiko MITM.</value>
|
||||
</data>
|
||||
<data name="menuUdpTestServer" xml:space="preserve">
|
||||
<value>Uji latensi UDP konfigurasi</value>
|
||||
</data>
|
||||
@@ -1719,9 +1746,6 @@ Tindakan "Ambil sertifikat" mungkin gagal jika sertifikat self-signed digunakan
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>Untuk lingkungan multi-antarmuka, masukkan alamat IPv4 mesin lokal.</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>Isi alamat IPv4 yang benar untuk SendThrough.</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||
<value>Bind Interface</value>
|
||||
</data>
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
<value>Failed to get the default configuration</value>
|
||||
</data>
|
||||
<data name="FailedImportedCustomServer" xml:space="preserve">
|
||||
<value>Failed to import custom configuration Configuration</value>
|
||||
<value>Failed to import configuration</value>
|
||||
</data>
|
||||
<data name="FailedReadConfiguration" xml:space="preserve">
|
||||
<value>Failed to read configuration file</value>
|
||||
@@ -283,7 +283,7 @@
|
||||
<value>Configuration successful. {0}</value>
|
||||
</data>
|
||||
<data name="SuccessfullyImportedCustomServer" xml:space="preserve">
|
||||
<value>Custom configuration Configuration imported successfully</value>
|
||||
<value>Custom configuration imported successfully</value>
|
||||
</data>
|
||||
<data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve">
|
||||
<value>{0} Configurations have been imported from clipboard</value>
|
||||
@@ -385,7 +385,7 @@
|
||||
<value>All</value>
|
||||
</data>
|
||||
<data name="FillServerAddressCustom" xml:space="preserve">
|
||||
<value>Please browse to import Configuration configuration</value>
|
||||
<value>Please browse to import configuration</value>
|
||||
</data>
|
||||
<data name="Speedtesting" xml:space="preserve">
|
||||
<value>Testing...</value>
|
||||
@@ -1044,6 +1044,12 @@
|
||||
<data name="TbHeaderType8" xml:space="preserve">
|
||||
<value>Congestion control</value>
|
||||
</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">
|
||||
<value>Previous proxy remarks</value>
|
||||
</data>
|
||||
@@ -1104,6 +1110,39 @@
|
||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||
<value>Enable fragment</value>
|
||||
</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">
|
||||
<value>Enable cache file for sing-box (ruleset files)</value>
|
||||
</data>
|
||||
@@ -1423,7 +1462,7 @@
|
||||
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled</value>
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Please fill in the correct config template</value>
|
||||
@@ -1462,7 +1501,7 @@
|
||||
<value>Select Profile</value>
|
||||
</data>
|
||||
<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 name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Please Add At Least One Configuration</value>
|
||||
@@ -1713,14 +1752,11 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
</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">
|
||||
<value>Bind Interface</value>
|
||||
</data>
|
||||
<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 name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
@@ -1744,7 +1780,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<value>New Update</value>
|
||||
</data>
|
||||
<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 name="TbRouteExcludeAddress" xml:space="preserve">
|
||||
<value>Route Exclude Address</value>
|
||||
@@ -1764,19 +1800,76 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||
<value>Split the tail of packets into smaller fragments. This may affect throughput and latency.</value>
|
||||
</data>
|
||||
<data name="TbEnableNetBridge" xml:space="preserve">
|
||||
<value>Enables process traffic hijacking (experimental), similar to Proxifer; can coexist with system proxy; conflicts with TUN mode, please do not enable them simultaneously;please allow v2rayN to use a private network in your firewall.</value>
|
||||
</data>
|
||||
<data name="NetBridgeRuleTips" xml:space="preserve">
|
||||
<value>The process names that need to be proxied; separate multiple processes with commas. Processes not in the list will also be hijacked, but will connect directly.</value>
|
||||
</data>
|
||||
<data name="menuNetBridge" xml:space="preserve">
|
||||
<value>Process traffic hijacking (experimental)</value>
|
||||
</data>
|
||||
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
|
||||
<value>DNS via Bridge</value>
|
||||
</data>
|
||||
<data name="TbSaveNetBridgeRule" xml:space="preserve">
|
||||
<value>Save rules</value>
|
||||
<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>
|
||||
<data name="TbXrayOnly" xml:space="preserve">
|
||||
<value>Xray Only</value>
|
||||
</data>
|
||||
<data name="TbBlockAAAAQueries" xml:space="preserve">
|
||||
<value>Block AAAA Queries</value>
|
||||
</data>
|
||||
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
|
||||
<value>Block IPv6 queries when enabled</value>
|
||||
</data>
|
||||
<data name="TbDNS" xml:space="preserve">
|
||||
<value>DNS</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -268,7 +268,7 @@
|
||||
<value>Сначала выберите сервер</value>
|
||||
</data>
|
||||
<data name="RemoveDuplicateServerResult" xml:space="preserve">
|
||||
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}.</value>
|
||||
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}</value>
|
||||
</data>
|
||||
<data name="RemoveServer" xml:space="preserve">
|
||||
<value>Вы уверены, что хотите удалить сервер?</value>
|
||||
@@ -1044,6 +1044,12 @@
|
||||
<data name="TbHeaderType8" xml:space="preserve">
|
||||
<value>Управление перегрузками</value>
|
||||
</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">
|
||||
<value>Псевдоним предыдущего прокси</value>
|
||||
</data>
|
||||
@@ -1104,6 +1110,39 @@
|
||||
<data name="TbSettingsEnableFragment" xml:space="preserve">
|
||||
<value>Включить фрагментацию (Fragment)</value>
|
||||
</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">
|
||||
<value>Включить файл кэша для sing-box (файлы наборов правил)</value>
|
||||
</data>
|
||||
@@ -1312,7 +1351,7 @@
|
||||
<value>Пароль sudo системы</value>
|
||||
</data>
|
||||
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
|
||||
<value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска.</value>
|
||||
<value>Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска</value>
|
||||
</data>
|
||||
<data name="TransportHeaderType5" xml:space="preserve">
|
||||
<value>XHTTP-режим</value>
|
||||
@@ -1372,7 +1411,7 @@
|
||||
<value>Можно указать псевдоним из конфигурации, убедитесь, что он существует и уникален</value>
|
||||
</data>
|
||||
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
|
||||
<value>Неверный пароль, попробуйте ещё раз.</value>
|
||||
<value>Неверный пароль, попробуйте ещё раз</value>
|
||||
</data>
|
||||
<data name="TbMldsa65Verify" xml:space="preserve">
|
||||
<value>Mldsa65Verify</value>
|
||||
@@ -1423,7 +1462,7 @@
|
||||
<value>Включён пользовательский DNS — настройки на этой странице не применяются</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>При включении блокирует проверки доступности ECH и HTTP/3</value>
|
||||
<value>При включении блокирует запросы доступности ECH и HTTP/3. В Xray включено всегда</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Пожалуйста, заполните корректный шаблон конфигурации</value>
|
||||
@@ -1453,7 +1492,7 @@
|
||||
<value>Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ</value>
|
||||
</data>
|
||||
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
|
||||
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value>
|
||||
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную</value>
|
||||
</data>
|
||||
<data name="MsgStartParsingSubscription" xml:space="preserve">
|
||||
<value>Начинается разбор и обработка содержимого подписки</value>
|
||||
@@ -1462,7 +1501,7 @@
|
||||
<value>Выбрать профиль</value>
|
||||
</data>
|
||||
<data name="TbFakeIPTips" xml:space="preserve">
|
||||
<value>По умолчанию применяется глобально, со встроенной фильтрацией FakeIP (только sing-box).</value>
|
||||
<value>Применяется глобально по умолчанию; встроенная фильтрация FakeIP есть только в sing-box</value>
|
||||
</data>
|
||||
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
|
||||
<value>Добавьте хотя бы одну конфигурацию</value>
|
||||
@@ -1549,7 +1588,7 @@
|
||||
<value>Привязанный сертификат (заполните любое из полей)
|
||||
При указании сертификат будет привязан, а «Пропустить проверку сертификата» отключится.
|
||||
|
||||
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
|
||||
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе</value>
|
||||
</data>
|
||||
<data name="TbFetchCert" xml:space="preserve">
|
||||
<value>Получить сертификат</value>
|
||||
@@ -1581,6 +1620,9 @@
|
||||
<data name="TbEchConfigList" xml:space="preserve">
|
||||
<value>EchConfigList</value>
|
||||
</data>
|
||||
<data name="TbVerifyPeerCertByName" xml:space="preserve">
|
||||
<value>Проверять сертификат узла по имени</value>
|
||||
</data>
|
||||
<data name="TbFullCertTips" xml:space="preserve">
|
||||
<value>Полный сертификат (цепочка) в формате PEM</value>
|
||||
</data>
|
||||
@@ -1600,10 +1642,10 @@
|
||||
<value>По умолчанию используется только при разрешении имён в процессе маршрутизации; убедитесь, что удалённый сервер может достичь этого DNS</value>
|
||||
</data>
|
||||
<data name="TbDirectResolveStrategyTips" xml:space="preserve">
|
||||
<value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль.</value>
|
||||
<value>Если не задано или «AsIs», используется системный DNS; иначе — встроенный DNS-модуль</value>
|
||||
</data>
|
||||
<data name="TbRemoteResolveStrategyTips" xml:space="preserve">
|
||||
<value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль.</value>
|
||||
<value>Если не задано или «AsIs», разрешение DNS выполняется DNS удалённого сервера; иначе — встроенный DNS-модуль</value>
|
||||
</data>
|
||||
<data name="TbHopInt7" xml:space="preserve">
|
||||
<value>Интервал смены портов (Port Hopping)</value>
|
||||
@@ -1618,37 +1660,37 @@
|
||||
<value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value>
|
||||
</data>
|
||||
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
|
||||
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел.</value>
|
||||
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел</value>
|
||||
</data>
|
||||
<data name="MsgGroupCycleDependency" xml:space="preserve">
|
||||
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен.</value>
|
||||
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен</value>
|
||||
</data>
|
||||
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
|
||||
<value>Группа {0}: предупреждение дочернего узла {1}: {2}</value>
|
||||
</data>
|
||||
<data name="MsgGroupChildNodeError" xml:space="preserve">
|
||||
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен.</value>
|
||||
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен</value>
|
||||
</data>
|
||||
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
|
||||
<value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value>
|
||||
</data>
|
||||
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
|
||||
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен.</value>
|
||||
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен</value>
|
||||
</data>
|
||||
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
|
||||
<value>У группы {0} нет допустимых дочерних узлов.</value>
|
||||
<value>У группы {0} нет допустимых дочерних узлов</value>
|
||||
</data>
|
||||
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
|
||||
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел.</value>
|
||||
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел</value>
|
||||
</data>
|
||||
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
|
||||
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел.</value>
|
||||
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел</value>
|
||||
</data>
|
||||
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
|
||||
<value>Предыдущий прокси подписки {0} не найден. Пропущено.</value>
|
||||
<value>Предыдущий прокси подписки {0} не найден. Пропущено</value>
|
||||
</data>
|
||||
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
|
||||
<value>Следующий прокси подписки {0} не найден. Пропущено.</value>
|
||||
<value>Следующий прокси подписки {0} не найден. Пропущено</value>
|
||||
</data>
|
||||
<data name="menuGenGroupServer" xml:space="preserve">
|
||||
<value>Сгенерировать группу политик</value>
|
||||
@@ -1710,14 +1752,11 @@
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>Укажите корректный IPv4-адрес для SendThrough.</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||
<value>Привязать интерфейс</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN</value>
|
||||
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для исходящих подключений. На Linux/macOS работает только при включённом TUN</value>
|
||||
</data>
|
||||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>Общий ключ (PSK)</value>
|
||||
@@ -1740,4 +1779,94 @@
|
||||
<data name="menuNewUpdate" xml:space="preserve">
|
||||
<value>Доступно обновление</value>
|
||||
</data>
|
||||
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
|
||||
<value>Предупреждение: 1 августа 2026 г. Xray отключит пропуск проверки сертификата (allowInsecure). Как можно скорее перейдите на привязанный отпечаток сертификата (pinnedPeerCertSha256). После этой даты allowInsecure использовать будет нельзя</value>
|
||||
</data>
|
||||
<data name="TbRouteExcludeAddress" xml:space="preserve">
|
||||
<value>Адреса, исключаемые из маршрутизации</value>
|
||||
</data>
|
||||
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
|
||||
<value>Разделяйте запятыми (,)</value>
|
||||
</data>
|
||||
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
|
||||
<value>Недопустимый адрес в списке исключений маршрутизации TUN: {0}</value>
|
||||
</data>
|
||||
<data name="MsgOptionsConflict" xml:space="preserve">
|
||||
<value>Конфликт между {0} и {1}</value>
|
||||
</data>
|
||||
<data name="TbEnableFinalFragment" xml:space="preserve">
|
||||
<value>Включить финальную фрагментацию (Final Fragment)</value>
|
||||
</data>
|
||||
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
|
||||
<value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку</value>
|
||||
</data>
|
||||
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
|
||||
<value>DNS через Bridge</value>
|
||||
</data>
|
||||
<data name="MsgInsecureConfiguration" xml:space="preserve">
|
||||
<value>Обнаружена небезопасная конфигурация: AllowInsecure включён, но сертификат не предоставлен. Это может привести к атаке «человек посередине» (MITM)</value>
|
||||
</data>
|
||||
<data name="TbHy2RealmUrl" xml:space="preserve">
|
||||
<value>Realm URL</value>
|
||||
</data>
|
||||
<data name="InvalidHy2RealmUrl" xml:space="preserve">
|
||||
<value>Некорректный Realm URL</value>
|
||||
</data>
|
||||
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
|
||||
<value>Введите корректный JSON заголовков HTTP-запроса</value>
|
||||
</data>
|
||||
<data name="TbHy2RealmUrlTip" xml:space="preserve">
|
||||
<value>Формат: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port]</value>
|
||||
</data>
|
||||
<data name="TbGeckoPacketSize" xml:space="preserve">
|
||||
<value>Размер пакета Gecko (мин/макс)</value>
|
||||
</data>
|
||||
<data name="TbLegacyProtectTip" xml:space="preserve">
|
||||
<value>Если включено, используется sing-box TUN; иначе — xray TUN</value>
|
||||
</data>
|
||||
<data name="TbRootCertificateProvider" xml:space="preserve">
|
||||
<value>Поставщик корневых сертификатов</value>
|
||||
</data>
|
||||
<data name="TbRootCertificateProviderTip" xml:space="preserve">
|
||||
<value>Применяется только к загрузкам и сетевым запросам графического интерфейса v2rayN. Не влияет на проверку сертификатов ядром</value>
|
||||
</data>
|
||||
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
|
||||
<value>Стратегия разрешения DNS при подключении к прокси</value>
|
||||
</data>
|
||||
<data name="TbProxyDialResolveStrategyTip" xml:space="preserve">
|
||||
<value>Не рекомендуется; может вызвать петли маршрутизации</value>
|
||||
</data>
|
||||
<data name="TbEnableHappyEyeballs" xml:space="preserve">
|
||||
<value>Включить Happy Eyeballs</value>
|
||||
</data>
|
||||
<data name="TbEnableHappyEyeballsTip" xml:space="preserve">
|
||||
<value>Требуется стратегия UseIP. При включении одновременно устанавливаются соединения по IPv4 и IPv6 и автоматически выбирается более быстрый доступный путь</value>
|
||||
</data>
|
||||
<data name="TbIpv6Address" xml:space="preserve">
|
||||
<value>Адрес IPv6</value>
|
||||
</data>
|
||||
<data name="TbIpv4Address" xml:space="preserve">
|
||||
<value>Адрес IPv4</value>
|
||||
</data>
|
||||
<data name="menuAddCustomOutboundServer" xml:space="preserve">
|
||||
<value>Добавить пользовательский outbound</value>
|
||||
</data>
|
||||
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
|
||||
<value>Файл пользовательского outbound {0} не найден: {1}</value>
|
||||
</data>
|
||||
<data name="TbCustomOutboundTip" xml:space="preserve">
|
||||
<value>Для xray/sing-box поддерживается только один outbound/endpoint</value>
|
||||
</data>
|
||||
<data name="LvCustomCoreType" xml:space="preserve">
|
||||
<value>Ядро пользовательской конфигурации</value>
|
||||
</data>
|
||||
<data name="TbXrayOnly" xml:space="preserve">
|
||||
<value>Только Xray</value>
|
||||
</data>
|
||||
<data name="TbBlockAAAAQueries" xml:space="preserve">
|
||||
<value>Блокировать DNS-запросы AAAA</value>
|
||||
</data>
|
||||
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
|
||||
<value>При включении блокирует DNS-запросы IPv6</value>
|
||||
</data>
|
||||
</root>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user