Compare commits

...

53 Commits

Author SHA1 Message Date
2dust
d5460d758b up 7.16.1 2025-11-09 15:17:08 +08:00
2dust
6e38357b7d Add macOS Dock visibility option to settings 2025-11-09 14:47:53 +08:00
DHR60
1990850d9a Optimize Cert Pinning (#8282) 2025-11-09 11:20:30 +08:00
2dust
e6cb146671 Refactor UI platform visibility to use ViewModel properties 2025-11-09 11:11:23 +08:00
2dust
4da59cd767 Rename IsOSX to IsMacOS in Utils and usages 2025-11-09 10:52:46 +08:00
2dust
e20c11c1a7 Refactor reload logic with semaphore for concurrency 2025-11-08 20:48:55 +08:00
2dust
a6af95e083 Bug fix
https://github.com/2dust/v2rayN/issues/8276
2025-11-08 20:10:20 +08:00
2dust
6f06b16c76 up 7.16.0 2025-11-08 11:29:18 +08:00
2dust
70ddf4ecfc Add allowInsecure and insecure to the shared URI
https://github.com/2dust/v2rayN/issues/8267
2025-11-08 11:14:01 +08:00
JieXu
187356cb9e Update ResUI.fr.resx (#8270) 2025-11-08 11:10:04 +08:00
2dust
32583ea8b3 Bug fix
Replaced direct assignments to BlReloadEnabled with a new SetReloadEnabled method that schedules updates on the main thread.
2025-11-07 21:06:43 +08:00
2dust
69797c10f2 Update ConfigHandler.cs 2025-11-07 19:52:03 +08:00
2dust
ddc8c9b1cd Add support for custom PAC and proxy script paths
Introduces options to specify custom PAC file and system proxy script paths for system proxy settings. Updates configuration models, view models, UI bindings, and logic for Linux/OSX proxy handling and PAC management to use these custom paths if provided. Also adds UI elements and localization for the new settings.
2025-11-07 19:28:16 +08:00
2dust
753e7b81b6 Add timeout and error handling to certificate fetching 2025-11-04 20:43:51 +08:00
2dust
725b094fb1 Update Directory.Packages.props 2025-11-04 20:43:28 +08:00
2dust
6de5a5215d Refactor code
Renamed FileManager to FileUtils and updated all references accordingly. Moved SemanticVersion to the Models namespace. Replaced WindowsJob with WindowsJobService, relocating and updating the implementation. Updated usages in CoreManager and related classes to use the new service and utility names. These changes improve code organization and naming consistency.
2025-11-03 20:01:36 +08:00
2dust
8d86aa2b72 Refactor ping and HTTP helpers, update usages
Moved GetRealPingTime from HttpClientHelper to ConnectionHandler and refactored related methods for clarity. Removed unused and redundant HTTP helper methods. Updated DownloadService and SpeedtestService to use the new method signatures. Simplified UpdateService constructor using primary constructor syntax.
2025-11-03 19:41:02 +08:00
DHR60
1aee3950f4 Fix (#8244) 2025-11-03 19:18:18 +08:00
2dust
091b79f7cf Refactor UpdateService to use instance config and callbacks 2025-11-02 21:04:23 +08:00
DHR60
ed2c77062e Disallow insecure when pinned cert (#8239) 2025-11-02 19:01:58 +08:00
JieXu
8b1105c7e2 Update ResUI.fr.resx (#8238) 2025-11-02 18:58:50 +08:00
2dust
11c203ad19 Update UI and localization for policy group 2025-11-02 16:24:57 +08:00
2dust
ab6a6b879e Code clean 2025-11-02 15:25:41 +08:00
DHR60
b218f0b501 Cert Pinning (#8234)
* Cert Pinning

* Cert Chain Pinning

* Add Trusted Ca Pinning

* Tip

* Perf UI
2025-11-02 15:17:47 +08:00
2dust
7b5686cd8f In the policy group, automatically add filtered configurations from the subscription group.
https://github.com/2dust/v2rayN/issues/8214
2025-11-01 21:13:39 +08:00
2dust
d727ff40bb Code clean 2025-10-31 20:25:54 +08:00
2dust
1b5069a933 Code clean 2025-10-31 20:25:50 +08:00
2dust
18ea6fdc00 Code clean 2025-10-31 20:25:45 +08:00
2dust
67494108ad up 7.15.7 2025-10-31 19:05:57 +08:00
2dust
38b2a7d2ca Rename QRCodeWindowsUtils 2025-10-31 17:50:28 +08:00
dependabot[bot]
bf3703bca1 Bump actions/download-artifact from 4 to 6 (#8225)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-30 20:36:43 +08:00
2dust
554632cc07 Bug fix
https://github.com/2dust/v2rayN/issues/8207
2025-10-30 20:34:47 +08:00
2dust
12fc3e9566 Bug fix 2025-10-30 20:00:15 +08:00
2dust
c2ef3a4a8c Add Design.IsDesignMode to the desktop version 2025-10-29 20:21:41 +08:00
2dust
86eb8297dd Update JsonUtils.cs 2025-10-29 20:20:44 +08:00
2dust
c63d4e83f9 Use OperatingSystem replace RuntimeInformation 2025-10-29 20:20:40 +08:00
JieXu
bf1fb0f92e RPM file remove x86-64-v1 Support. Update French translation. (#8216)
* Update ResUI.fr.resx

* Update build-linux.yml

* Update package-rhel.sh

* 更新 build-linux.yml

* Update ResUI.fr.resx

* Update ResUI.fr.resx

* Update ResUI.fr.resx

* Update ResUI.fr.resx

* Update ResUI.fr.resx

* Update ResUI.fr.resx

* Update ResUI.fr.resx

* Update ResUI.fr.resx
2025-10-29 09:21:37 +08:00
dependabot[bot]
3c4865982b Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#8211)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 5.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.2...v5.0.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 13:55:57 +08:00
Aron Yang
22c233f0cd Fix TUN mode cleanup on Linux/macOS (#8202) 2025-10-26 11:09:23 +08:00
JieXu
b2d6282755 Remove AppImage. Update package.sh (#8201)
* Refactor AppRun script generation in packaging

* Update minimum kernel version requirement to 6.13

* Update minimum kernel version requirement to 5.14

* Revise runtime dependencies with version constraints

Updated runtime dependencies for package-rhel.sh to include version constraints and additional requirements.

* Modify package dependencies in package-debian.sh

Updated package dependencies to include libc6, fontconfig, coreutils, and bash.

* Remove AppImage packaging and upload steps

Removed AppImage packaging and upload steps from the workflow.

* Delete package-appimage.sh

* Simplify environment checks in Utils.cs

Removed checks for APPIMAGE environment variable and mount path.

* Update v2rayN.slnx

* Remove package scripts from v2rayN solution

Removed references to package-appimage.sh and pkg2appimage.yml from the solution file.
2025-10-26 10:13:49 +08:00
2dust
c8d89e3dce Adjusted the items in the configuration right-click menu 2025-10-25 20:10:42 +08:00
DHR60
d3b1810eab Update Directory.Packages.props (#8191) 2025-10-25 10:27:31 +08:00
JieXu
51409a3e28 Add French support | Ajouter le support du français | 添加法语支持 (#8186)
* Add files via upload

* Add files via upload

* Update Global.cs

* Add French resource file to project

* Update ResUI.fr.resx

* Update ResUI.fr.resx

* Update ResUI.fr.resx

* Delete v2rayN/AmazTool/Resx/Resource.fr.resx
2025-10-24 19:38:40 +08:00
2dust
1a0f50a41e up 7.15.6
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
2025-10-24 14:22:35 +08:00
2dust
83d4a9c18e Update Directory.Packages.props 2025-10-24 14:22:05 +08:00
2dust
b4c20e7b81 Bug fix
https://github.com/2dust/v2rayN/discussions/8168
2025-10-23 19:03:16 +08:00
DHR60
7c76308c93 Fix (#8180) 2025-10-23 17:58:02 +08:00
DHR60
f28fa31c14 Fix tcp dns (#8179) 2025-10-23 17:57:47 +08:00
DHR60
bbedc4dbb1 Fix (#8175) 2025-10-23 09:10:21 +08:00
DHR60
ecf42cb85d Fix dns (#8174) 2025-10-23 09:09:26 +08:00
2dust
e4701d6703 Add one-click test of real connection delay 2025-10-22 19:54:24 +08:00
mlds23
54a47d00a3 更新繁體中文翻譯 (#8166) 2025-10-22 17:00:19 +08:00
DHR60
964572817b Fix (#8161) 2025-10-22 09:07:53 +08:00
149 changed files with 4472 additions and 1274 deletions

View File

@@ -9,6 +9,12 @@ on:
push:
branches:
- master
tags:
- 'v*'
- 'V*'
permissions:
contents: write
env:
OutputArch: "linux-64"
@@ -21,7 +27,6 @@ jobs:
strategy:
matrix:
configuration: [Release]
runs-on: ubuntu-24.04
steps:
@@ -31,21 +36,21 @@ jobs:
submodules: 'recursive'
fetch-depth: '0'
- name: Setup
- name: Setup .NET
uses: actions/setup-dotnet@v5.0.0
with:
dotnet-version: '8.0.x'
- name: Build
run: |
cd v2rayN
dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r linux-x64 --self-contained=true -o $OutputPath64
dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r linux-arm64 --self-contained=true -o $OutputPathArm64
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-x64 --self-contained=true -p:PublishTrimmed=true -o $OutputPath64
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-arm64 --self-contained=true -p:PublishTrimmed=true -o $OutputPathArm64
cd v2rayN
dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r linux-x64 --self-contained=true -o "$OutputPath64"
dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r linux-arm64 --self-contained=true -o "$OutputPathArm64"
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-x64 --self-contained=true -p:PublishTrimmed=true -o "$OutputPath64"
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-arm64 --self-contained=true -p:PublishTrimmed=true -o "$OutputPathArm64"
- name: Upload build artifacts
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v5.0.0
with:
name: v2rayN-linux
path: |
@@ -56,8 +61,8 @@ jobs:
if: github.event.inputs.release_tag != ''
run: |
chmod 755 package-debian.sh
./package-debian.sh $OutputArch $OutputPath64 ${{ github.event.inputs.release_tag }}
./package-debian.sh $OutputArchArm $OutputPathArm64 ${{ github.event.inputs.release_tag }}
./package-debian.sh "$OutputArch" "$OutputPath64" "${{ github.event.inputs.release_tag }}"
./package-debian.sh "$OutputArchArm" "$OutputPathArm64" "${{ github.event.inputs.release_tag }}"
- name: Upload deb to release
uses: svenstaro/upload-release-action@v2
@@ -68,28 +73,13 @@ jobs:
file_glob: true
prerelease: true
- name: Package AppImage
if: github.event.inputs.release_tag != ''
run: |
chmod a+x package-appimage.sh
./package-appimage.sh
- name: Upload AppImage to release
uses: svenstaro/upload-release-action@v2
if: github.event.inputs.release_tag != ''
with:
file: ${{ github.workspace }}/v2rayN*.AppImage
tag: ${{ github.event.inputs.release_tag }}
file_glob: true
prerelease: true
# release zip archive
- name: Package release zip archive
if: github.event.inputs.release_tag != ''
run: |
chmod 755 package-release-zip.sh
./package-release-zip.sh $OutputArch $OutputPath64
./package-release-zip.sh $OutputArchArm $OutputPathArm64
./package-release-zip.sh "$OutputArch" "$OutputPath64"
./package-release-zip.sh "$OutputArchArm" "$OutputPathArm64"
- name: Upload zip archive to release
uses: svenstaro/upload-release-action@v2
@@ -100,36 +90,62 @@ jobs:
file_glob: true
prerelease: true
# release RHEL package
- name: Package RPM (RHEL-family)
if: github.event.inputs.release_tag != ''
rpm:
needs: build
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
container:
image: quay.io/almalinuxorg/10-base:latest
options: --platform=linux/amd64/v2
env:
RELEASE_TAG: ${{ github.event.inputs.release_tag != '' && github.event.inputs.release_tag || github.ref_name }}
steps:
- name: Prepare tools (Red Hat)
run: |
chmod 755 package-rhel.sh
# Build for both x86_64 and aarch64 in one go (explicit version passed; no --buildfrom)
./package-rhel.sh "${{ github.event.inputs.release_tag }}" --arch all
dnf -y makecache
dnf -y install epel-release
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which
- name: Checkout repo (for scripts)
uses: actions/checkout@v5.0.0
with:
submodules: 'recursive'
fetch-depth: '0'
- name: Restore build artifacts
uses: actions/download-artifact@v6
with:
name: v2rayN-linux
path: ${{ github.workspace }}/v2rayN/Release
- name: Ensure script permissions
run: chmod 755 package-rhel.sh
- name: Package RPM (RHEL-family)
run: ./package-rhel.sh "${RELEASE_TAG}" --arch all
- name: Collect RPMs into workspace
if: github.event.inputs.release_tag != ''
run: |
mkdir -p "${{ github.workspace }}/dist/rpm"
rsync -av "$HOME/rpmbuild/RPMS/" "${{ github.workspace }}/dist/rpm/"
# Rename to requested filenames
find "${{ github.workspace }}/dist/rpm" -name "v2rayN-*-1.x86_64.rpm" -exec mv {} "${{ github.workspace }}/dist/rpm/v2rayN-linux-rhel-x64.rpm" \; || true
find "${{ github.workspace }}/dist/rpm" -name "v2rayN-*-1.aarch64.rpm" -exec mv {} "${{ github.workspace }}/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true
mkdir -p "$GITHUB_WORKSPACE/dist/rpm"
rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm/" || true
find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.x86_64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-64.rpm" \; || true
find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.aarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
- name: Upload RPM artifacts
if: github.event.inputs.release_tag != ''
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v5.0.0
with:
name: v2rayN-rpm
path: |
${{ github.workspace }}/dist/rpm/**/*.rpm
path: dist/rpm/**/*.rpm
- name: Upload RPMs to release
uses: svenstaro/upload-release-action@v2
if: github.event.inputs.release_tag != ''
with:
file: ${{ github.workspace }}/dist/rpm/**/*.rpm
tag: ${{ github.event.inputs.release_tag }}
file: dist/rpm/**/*.rpm
tag: ${{ env.RELEASE_TAG }}
file_glob: true
prerelease: true

View File

@@ -45,7 +45,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r osx-arm64 --self-contained=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v5.0.0
with:
name: v2rayN-macos
path: |

View File

@@ -45,7 +45,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 --self-contained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v5.0.0
with:
name: v2rayN-windows-desktop
path: |

View File

@@ -46,7 +46,7 @@ jobs:
- name: Upload build artifacts
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v5.0.0
with:
name: v2rayN-windows
path: |

View File

@@ -1,67 +0,0 @@
#!/bin/bash
set -euo pipefail
# Install deps
sudo apt update -y
sudo apt install -y libfuse2 wget file
# Get tools
wget -qO appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool
# x86_64 AppDir
APPDIR_X64="AppDir-x86_64"
rm -rf "$APPDIR_X64"
mkdir -p "$APPDIR_X64/usr/lib/v2rayN" "$APPDIR_X64/usr/bin" "$APPDIR_X64/usr/share/applications" "$APPDIR_X64/usr/share/pixmaps"
cp -rf "$OutputPath64"/* "$APPDIR_X64/usr/lib/v2rayN" || true
[ -f "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_X64/usr/share/pixmaps/v2rayN.png" || true
[ -f "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_X64/v2rayN.png" || true
printf '%s\n' '#!/bin/sh' 'HERE="$(dirname "$(readlink -f "$0")")"' 'cd "$HERE/usr/lib/v2rayN"' 'exec "$HERE/usr/lib/v2rayN/v2rayN" "$@"' > "$APPDIR_X64/AppRun"
chmod +x "$APPDIR_X64/AppRun"
ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_X64/usr/bin/v2rayN"
cat > "$APPDIR_X64/v2rayN.desktop" <<EOF
[Desktop Entry]
Name=v2rayN
Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
Exec=v2rayN
Icon=v2rayN
Terminal=false
Type=Application
Categories=Network;
EOF
install -Dm644 "$APPDIR_X64/v2rayN.desktop" "$APPDIR_X64/usr/share/applications/v2rayN.desktop"
ARCH=x86_64 ./appimagetool "$APPDIR_X64" "v2rayN-${OutputArch}.AppImage"
file "v2rayN-${OutputArch}.AppImage" | grep -q 'x86-64'
# aarch64 AppDir
APPDIR_ARM64="AppDir-aarch64"
rm -rf "$APPDIR_ARM64"
mkdir -p "$APPDIR_ARM64/usr/lib/v2rayN" "$APPDIR_ARM64/usr/bin" "$APPDIR_ARM64/usr/share/applications" "$APPDIR_ARM64/usr/share/pixmaps"
cp -rf "$OutputPathArm64"/* "$APPDIR_ARM64/usr/lib/v2rayN" || true
[ -f "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_ARM64/usr/share/pixmaps/v2rayN.png" || true
[ -f "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_ARM64/v2rayN.png" || true
printf '%s\n' '#!/bin/sh' 'HERE="$(dirname "$(readlink -f "$0")")"' 'cd "$HERE/usr/lib/v2rayN"' 'exec "$HERE/usr/lib/v2rayN/v2rayN" "$@"' > "$APPDIR_ARM64/AppRun"
chmod +x "$APPDIR_ARM64/AppRun"
ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_ARM64/usr/bin/v2rayN"
cat > "$APPDIR_ARM64/v2rayN.desktop" <<EOF
[Desktop Entry]
Name=v2rayN
Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
Exec=v2rayN
Icon=v2rayN
Terminal=false
Type=Application
Categories=Network;
EOF
install -Dm644 "$APPDIR_ARM64/v2rayN.desktop" "$APPDIR_ARM64/usr/share/applications/v2rayN.desktop"
# aarch64 runtime
wget -qO runtime-aarch64 https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-aarch64
chmod +x runtime-aarch64
# build aarch64 AppImage
ARCH=aarch64 ./appimagetool --runtime-file ./runtime-aarch64 "$APPDIR_ARM64" "v2rayN-${OutputArchArm}.AppImage"
file "v2rayN-${OutputArchArm}.AppImage" | grep -q 'ARM aarch64'

View File

@@ -28,7 +28,7 @@ Package: v2rayN
Version: $Version
Architecture: $Arch2
Maintainer: https://github.com/2dust/v2rayN
Depends: desktop-file-utils, xdg-utils
Depends: libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1)
Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others
EOF

View File

@@ -19,6 +19,23 @@ else
exit 1
fi
# ======================== Kernel version check (require >= 6.11) =======================
MIN_KERNEL_MAJOR=6
MIN_KERNEL_MINOR=11
KERNEL_FULL=$(uname -r)
KERNEL_MAJOR=$(echo "$KERNEL_FULL" | cut -d. -f1)
KERNEL_MINOR=$(echo "$KERNEL_FULL" | cut -d. -f2)
echo "[INFO] Detected kernel version: $KERNEL_FULL"
if (( KERNEL_MAJOR < MIN_KERNEL_MAJOR )) || { (( KERNEL_MAJOR == MIN_KERNEL_MAJOR )) && (( KERNEL_MINOR < MIN_KERNEL_MINOR )); }; then
echo "[ERROR] Kernel $KERNEL_FULL is too old. Requires Linux >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}."
echo "Please upgrade your system or use a newer container (e.g. Fedora 42+, RHEL 10+, Debian 13+)."
exit 1
fi
echo "[OK] Kernel version >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}."
# ===== Config & Parse arguments =========================================================
VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty
WITH_CORE="both" # Default: bundle both xray+sing-box
@@ -614,8 +631,13 @@ ExclusiveArch: aarch64 x86_64
Source0: __PKGROOT__.tar.gz
# Runtime dependencies (Avalonia / X11 / Fonts / GL)
Requires: libX11, libXrandr, libXcursor, libXi, libXext, libxcb, libXrender, libXfixes, libXinerama, libxkbcommon
Requires: fontconfig, freetype, cairo, pango, mesa-libEGL, mesa-libGL, xdg-utils
Requires: freetype, cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.34
Requires: fontconfig >= 2.13.1
Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 8.32
Requires: bash >= 5.1
%description
v2rayN Linux for Red Hat Enterprise Linux

View File

@@ -1,14 +1,14 @@
<Project>
<PropertyGroup>
<Version>7.15.5</Version>
<Version>7.16.1</Version>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<NoWarn>CA1031;CS1591;NU1507;CA1416;IDE0058</NoWarn>
<NoWarn>CA1031;CS1591;NU1507;CA1416;IDE0058;IDE0053;IDE0200</NoWarn>
<Nullable>annotations</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Authors>2dust</Authors>

View File

@@ -6,19 +6,19 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.7" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.7" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.7" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.7" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.8" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.8" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.8" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageVersion Include="CliWrap" Version="3.9.0" />
<PackageVersion Include="Downloader" Version="4.0.3" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.2" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.0" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.2.0" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.3.0" />
<PackageVersion Include="QRCoder" Version="1.7.0" />
<PackageVersion Include="ReactiveUI" Version="20.4.1" />
<PackageVersion Include="ReactiveUI" Version="22.2.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
<PackageVersion Include="ReactiveUI.WPF" Version="22.2.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.7" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7" />

View File

@@ -3,7 +3,7 @@ using System.IO.Compression;
namespace ServiceLib.Common;
public static class FileManager
public static class FileUtils
{
private static readonly string _tag = "FileManager";

View File

@@ -35,9 +35,13 @@ public class JsonUtils
/// <typeparam name="T"></typeparam>
/// <param name="obj"></param>
/// <returns></returns>
public static T DeepCopy<T>(T obj)
public static T? DeepCopy<T>(T? obj)
{
return Deserialize<T>(Serialize(obj, false))!;
if (obj is null)
{
return default;
}
return Deserialize<T>(Serialize(obj, false));
}
/// <summary>
@@ -67,7 +71,7 @@ public class JsonUtils
/// </summary>
/// <param name="strJson"></param>
/// <returns></returns>
public static JsonNode? ParseJson(string strJson)
public static JsonNode? ParseJson(string? strJson)
{
try
{
@@ -116,7 +120,7 @@ public class JsonUtils
/// <param name="obj"></param>
/// <param name="options"></param>
/// <returns></returns>
public static string Serialize(object? obj, JsonSerializerOptions options)
public static string Serialize(object? obj, JsonSerializerOptions? options)
{
var result = string.Empty;
try
@@ -125,7 +129,7 @@ public class JsonUtils
{
return result;
}
result = JsonSerializer.Serialize(obj, options);
result = JsonSerializer.Serialize(obj, options ?? _defaultSerializeOptions);
}
catch (Exception ex)
{

View File

@@ -9,7 +9,7 @@ public class Utils
{
private static readonly string _tag = "Utils";
#region
#region Conversion Functions
/// <summary>
/// Convert to comma-separated string
@@ -306,7 +306,10 @@ public class Utils
public static bool IsBase64String(string? plainText)
{
if (plainText.IsNullOrEmpty())
{
return false;
}
var buffer = new Span<byte>(new byte[plainText.Length]);
return Convert.TryFromBase64String(plainText, buffer, out var _);
}
@@ -424,7 +427,7 @@ public class Utils
// Handle IPv6 addresses, e.g., "[2001:db8::1]:443"
if (authority.StartsWith("[") && authority.Contains("]"))
{
int closingBracketIndex = authority.LastIndexOf(']');
var closingBracketIndex = authority.LastIndexOf(']');
if (closingBracketIndex < authority.Length - 1 && authority[closingBracketIndex + 1] == ':')
{
// Port exists
@@ -459,9 +462,9 @@ public class Utils
return (domain, port);
}
#endregion
#endregion Conversion Functions
#region
#region Data Checks
/// <summary>
/// Determine if the input is a number
@@ -520,40 +523,62 @@ public class Utils
{
// Loopback address check (127.0.0.1 for IPv4, ::1 for IPv6)
if (IPAddress.IsLoopback(address))
{
return true;
}
var ipBytes = address.GetAddressBytes();
if (address.AddressFamily == AddressFamily.InterNetwork)
{
// IPv4 private address check
if (ipBytes[0] == 10)
{
return true;
}
if (ipBytes[0] == 172 && ipBytes[1] >= 16 && ipBytes[1] <= 31)
{
return true;
}
if (ipBytes[0] == 192 && ipBytes[1] == 168)
{
return true;
}
}
else if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
// IPv6 private address check
// Link-local address fe80::/10
if (ipBytes[0] == 0xfe && (ipBytes[1] & 0xc0) == 0x80)
{
return true;
}
// Unique local address fc00::/7 (typically fd00::/8)
if ((ipBytes[0] & 0xfe) == 0xfc)
{
return true;
}
// Private portion in IPv4-mapped addresses ::ffff:0:0/96
if (address.IsIPv4MappedToIPv6)
{
var ipv4Bytes = ipBytes.Skip(12).ToArray();
if (ipv4Bytes[0] == 10)
{
return true;
}
if (ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31)
{
return true;
}
if (ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168)
{
return true;
}
}
}
}
@@ -561,9 +586,9 @@ public class Utils
return false;
}
#endregion
#endregion Data Checks
#region
#region Speed Test
private static bool PortInUse(int port)
{
@@ -616,9 +641,9 @@ public class Utils
return 59090;
}
#endregion
#endregion Speed Test
#region
#region Miscellaneous
public static bool UpgradeAppExists(out string upgradeFileName)
{
@@ -708,10 +733,16 @@ public class Utils
foreach (var host in hostsList)
{
if (host.StartsWith("#"))
{
continue;
}
var hostItem = host.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (hostItem.Length < 2)
{
continue;
}
systemHosts.Add(hostItem[1], hostItem[0]);
}
}
@@ -762,7 +793,7 @@ public class Utils
return null;
}
#endregion
#endregion Miscellaneous
#region TempPath
@@ -963,13 +994,13 @@ public class Utils
#region Platform
public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public static bool IsWindows() => OperatingSystem.IsWindows();
public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
public static bool IsLinux() => OperatingSystem.IsLinux();
public static bool IsOSX() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public static bool IsMacOS() => OperatingSystem.IsMacOS();
public static bool IsNonWindows() => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public static bool IsNonWindows() => !OperatingSystem.IsWindows();
public static string GetExeName(string name)
{
@@ -989,16 +1020,11 @@ public class Utils
{
try
{
if (IsWindows() || IsOSX())
if (IsWindows() || IsMacOS())
{
return false;
}
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPIMAGE")))
{
return true;
}
var exePath = GetExePath();
var baseDir = string.IsNullOrEmpty(exePath) ? StartupPath() : Path.GetDirectoryName(exePath) ?? "";
var p = baseDir.Replace('\\', '/');
@@ -1008,11 +1034,6 @@ public class Utils
return false;
}
if (p.Contains("/.mount_", StringComparison.Ordinal))
{
return true;
}
if (p.StartsWith("/opt/v2rayN", StringComparison.OrdinalIgnoreCase))
{
return true;

View File

@@ -1,180 +0,0 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ServiceLib.Common;
/*
* See:
* http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net
*/
public sealed class WindowsJob : IDisposable
{
private IntPtr handle = IntPtr.Zero;
public WindowsJob()
{
handle = CreateJobObject(IntPtr.Zero, null);
var extendedInfoPtr = IntPtr.Zero;
var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = 0x2000
};
var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = info
};
try
{
var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
extendedInfoPtr = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr,
(uint)length))
{
throw new Exception(string.Format("Unable to set information. Error: {0}",
Marshal.GetLastWin32Error()));
}
}
finally
{
if (extendedInfoPtr != IntPtr.Zero)
{
Marshal.FreeHGlobal(extendedInfoPtr);
}
}
}
public bool AddProcess(IntPtr processHandle)
{
var succ = AssignProcessToJobObject(handle, processHandle);
if (!succ)
{
Logging.SaveLog("Failed to call AssignProcessToJobObject! GetLastError=" + Marshal.GetLastWin32Error());
}
return succ;
}
public bool AddProcess(int processId)
{
return AddProcess(Process.GetProcessById(processId).Handle);
}
#region IDisposable
private bool disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposed)
{
return;
}
disposed = true;
if (disposing)
{
// no managed objects to free
}
if (handle != IntPtr.Zero)
{
CloseHandle(handle);
handle = IntPtr.Zero;
}
}
~WindowsJob()
{
Dispose(false);
}
#endregion IDisposable
#region Interop
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr CreateJobObject(IntPtr a, string? lpName);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
#endregion Interop
}
#region Helper classes
[StructLayout(LayoutKind.Sequential)]
internal struct IO_COUNTERS
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public uint LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public UIntPtr Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public uint nLength;
public IntPtr lpSecurityDescriptor;
public int bInheritHandle;
}
[StructLayout(LayoutKind.Sequential)]
internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public UIntPtr ProcessMemoryLimit;
public UIntPtr JobMemoryLimit;
public UIntPtr PeakProcessMemoryUsed;
public UIntPtr PeakJobMemoryUsed;
}
public enum JobObjectInfoType
{
AssociateCompletionPortInformation = 7,
BasicLimitInformation = 2,
BasicUIRestrictions = 4,
EndOfJobTimeInformation = 6,
ExtendedLimitInformation = 9,
SecurityLimitInformation = 5,
GroupInformation = 11
}
#endregion Helper classes

View File

@@ -5,5 +5,6 @@ public enum ESpeedActionType
Tcping,
Realping,
Speedtest,
Mixedtest
Mixedtest,
FastRealping
}

View File

@@ -427,6 +427,7 @@ public class Global
"zh-Hant",
"en",
"fa-Ir",
"fr",
"ru",
"hu"
];

View File

@@ -26,7 +26,7 @@ public static class AutoStartupHandler
await SetTaskLinux();
}
}
else if (Utils.IsOSX())
else if (Utils.IsMacOS())
{
await ClearTaskOSX();

View File

@@ -97,7 +97,7 @@ public static class ConfigHandler
config.UiItem ??= new UIItem()
{
EnableAutoAdjustMainLvColWidth = true
EnableUpdateSubOnlyRemarksExist = true
};
config.UiItem.MainColumnItem ??= new();
config.UiItem.WindowSizeItem ??= new();
@@ -252,6 +252,7 @@ public static class ConfigHandler
item.Mldsa65Verify = profileItem.Mldsa65Verify;
item.Extra = profileItem.Extra;
item.MuxEnabled = profileItem.MuxEnabled;
item.Cert = profileItem.Cert;
}
var ret = item.ConfigType switch
@@ -447,13 +448,13 @@ public static class ConfigHandler
/// <returns>0 if successful, -1 if failed</returns>
public static async Task<int> MoveServer(Config config, List<ProfileItem> lstProfile, int index, EMove eMove, int pos = -1)
{
int count = lstProfile.Count;
var count = lstProfile.Count;
if (index < 0 || index > lstProfile.Count - 1)
{
return -1;
}
for (int i = 0; i < lstProfile.Count; i++)
for (var i = 0; i < lstProfile.Count; i++)
{
ProfileExManager.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10);
}
@@ -527,7 +528,7 @@ public static class ConfigHandler
return -1;
}
var ext = Path.GetExtension(fileName);
string newFileName = $"{Utils.GetGuid()}{ext}";
var newFileName = $"{Utils.GetGuid()}{ext}";
//newFileName = Path.Combine(Utile.GetTempPath(), newFileName);
try
@@ -1221,6 +1222,7 @@ public static class ConfigHandler
CoreType = coreType,
ConfigType = EConfigType.PolicyGroup,
Remarks = remark,
IsSub = false
};
if (!subId.IsNullOrEmpty())
{
@@ -1355,7 +1357,7 @@ public static class ConfigHandler
}
continue;
}
var profileItem = FmtHandler.ResolveConfig(str, out string msg);
var profileItem = FmtHandler.ResolveConfig(str, out var msg);
if (profileItem is null)
{
continue;
@@ -1439,7 +1441,7 @@ public static class ConfigHandler
{
await RemoveServersViaSubid(config, subid, isSub);
}
int count = 0;
var count = 0;
foreach (var it in lstProfiles)
{
it.Subid = subid;
@@ -1529,7 +1531,7 @@ public static class ConfigHandler
var lstSsServer = ShadowsocksFmt.ResolveSip008(strData);
if (lstSsServer?.Count > 0)
{
int counter = 0;
var counter = 0;
foreach (var ssItem in lstSsServer)
{
ssItem.Subid = subid;
@@ -1649,7 +1651,9 @@ public static class ConfigHandler
var uri = Utils.TryUri(url);
if (uri == null)
{
return -1;
}
//Do not allow http protocol
if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost))
{
@@ -1704,7 +1708,7 @@ public static class ConfigHandler
var maxSort = 0;
if (await SQLiteHelper.Instance.TableAsync<SubItem>().CountAsync() > 0)
{
var lstSubs = (await AppManager.Instance.SubItems());
var lstSubs = await AppManager.Instance.SubItems();
maxSort = lstSubs.LastOrDefault()?.Sort ?? 0;
}
item.Sort = maxSort + 1;
@@ -1866,7 +1870,7 @@ public static class ConfigHandler
/// <returns>0 if successful, -1 if failed</returns>
public static async Task<int> MoveRoutingRule(List<RulesItem> rules, int index, EMove eMove, int pos = -1)
{
int count = rules.Count;
var count = rules.Count;
if (index < 0 || index > rules.Count - 1)
{
return -1;
@@ -2016,11 +2020,15 @@ public static class ConfigHandler
var downloadHandle = new DownloadService();
var templateContent = await downloadHandle.TryDownloadString(config.ConstItem.RouteRulesTemplateSourceUrl, true, "");
if (templateContent.IsNullOrEmpty())
{
return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback
}
var template = JsonUtils.Deserialize<RoutingTemplate>(templateContent);
if (template == null)
{
return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback
}
var items = await AppManager.Instance.RoutingItems();
var maxSort = items.Count;
@@ -2033,14 +2041,18 @@ public static class ConfigHandler
var item = template.RoutingItems[i];
if (item.Url.IsNullOrEmpty() && item.RuleSet.IsNullOrEmpty())
{
continue;
}
var ruleSetsString = !item.RuleSet.IsNullOrEmpty()
? item.RuleSet
: await downloadHandle.TryDownloadString(item.Url, true, "");
if (ruleSetsString.IsNullOrEmpty())
{
continue;
}
item.Remarks = $"{template.Version}-{item.Remarks}";
item.Enabled = true;
@@ -2236,17 +2248,25 @@ public static class ConfigHandler
var downloadHandle = new DownloadService();
var templateContent = await downloadHandle.TryDownloadString(url, true, "");
if (templateContent.IsNullOrEmpty())
{
return currentItem;
}
var template = JsonUtils.Deserialize<DNSItem>(templateContent);
if (template == null)
{
return currentItem;
}
if (!template.NormalDNS.IsNullOrEmpty())
{
template.NormalDNS = await downloadHandle.TryDownloadString(template.NormalDNS, true, "");
}
if (!template.TunDNS.IsNullOrEmpty())
{
template.TunDNS = await downloadHandle.TryDownloadString(template.TunDNS, true, "");
}
template.Id = currentItem.Id;
template.Enabled = currentItem.Enabled;
@@ -2280,10 +2300,16 @@ public static class ConfigHandler
var downloadHandle = new DownloadService();
var templateContent = await downloadHandle.TryDownloadString(url, true, "");
if (templateContent.IsNullOrEmpty())
{
return null;
}
var template = JsonUtils.Deserialize<SimpleDNSItem>(templateContent);
if (template == null)
{
return null;
}
return template;
}

View File

@@ -6,7 +6,7 @@ public static class ConnectionHandler
public static async Task<string> RunAvailabilityCheck()
{
var time = await GetRealPingTime();
var time = await GetRealPingTimeInfo();
var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None;
return string.Format(ResUI.TestMeOutput, time, ip);
@@ -39,7 +39,7 @@ public static class ConnectionHandler
return $"({country ?? "unknown"}) {ip}";
}
private static async Task<int> GetRealPingTime()
private static async Task<int> GetRealPingTimeInfo()
{
var responseTime = -1;
try
@@ -50,7 +50,7 @@ public static class ConnectionHandler
for (var i = 0; i < 2; i++)
{
responseTime = await HttpClientHelper.Instance.GetRealPingTime(url, webProxy, 10);
responseTime = await GetRealPingTime(url, webProxy, 10);
if (responseTime > 0)
{
break;
@@ -65,4 +65,34 @@ public static class ConnectionHandler
}
return responseTime;
}
public static async Task<int> GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout)
{
var responseTime = -1;
try
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout));
using var client = new HttpClient(new SocketsHttpHandler()
{
Proxy = webProxy,
UseProxy = webProxy != null
});
List<int> oneTime = new();
for (var i = 0; i < 2; i++)
{
var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
await Task.Delay(100);
}
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
}
catch
{
}
return responseTime;
}
}

View File

@@ -58,7 +58,7 @@ public static class CoreConfigHandler
File.Delete(fileName);
}
string addressFileName = node.Address;
var addressFileName = node.Address;
if (!File.Exists(addressFileName))
{
addressFileName = Utils.GetConfigPath(addressFileName);

View File

@@ -23,7 +23,7 @@ public class AnytlsFmt : BaseFmt
item.Id = rawUserInfo;
var query = Utils.ParseQueryString(parsedUrl.Query);
_ = ResolveStdTransport(query, ref item);
ResolveUriQuery(query, ref item);
return item;
}
@@ -41,7 +41,7 @@ public class AnytlsFmt : BaseFmt
}
var pw = item.Id;
var dicQuery = new Dictionary<string, string>();
_ = GetStdTransport(item, Global.None, ref dicQuery);
ToUriQuery(item, Global.None, ref dicQuery);
return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark);
}

View File

@@ -4,6 +4,8 @@ namespace ServiceLib.Handler.Fmt;
public class BaseFmt
{
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure", "verify" };
protected static string GetIpv6(string address)
{
if (Utils.IsIpv6(address))
@@ -17,7 +19,7 @@ public class BaseFmt
}
}
protected static int GetStdTransport(ProfileItem item, string? securityDef, ref Dictionary<string, string> dicQuery)
protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dictionary<string, string> dicQuery)
{
if (item.Flow.IsNotEmpty())
{
@@ -37,11 +39,7 @@ public class BaseFmt
}
if (item.Sni.IsNotEmpty())
{
dicQuery.Add("sni", item.Sni);
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
dicQuery.Add("sni", Utils.UrlEncode(item.Sni));
}
if (item.Fingerprint.IsNotEmpty())
{
@@ -63,9 +61,14 @@ public class BaseFmt
{
dicQuery.Add("pqv", Utils.UrlEncode(item.Mldsa65Verify));
}
if (item.AllowInsecure.Equals("true"))
if (item.StreamSecurity.Equals(Global.StreamSecurity))
{
dicQuery.Add("allowInsecure", "1");
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
}
ToUriQueryAllowInsecure(item, ref dicQuery);
}
dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp));
@@ -153,7 +156,40 @@ public class BaseFmt
return 0;
}
protected static int ResolveStdTransport(NameValueCollection query, ref ProfileItem item)
protected static int ToUriQueryLite(ProfileItem item, ref Dictionary<string, string> dicQuery)
{
if (item.Sni.IsNotEmpty())
{
dicQuery.Add("sni", Utils.UrlEncode(item.Sni));
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
}
ToUriQueryAllowInsecure(item, ref dicQuery);
return 0;
}
private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary<string, string> dicQuery)
{
if (item.AllowInsecure.Equals(Global.AllowInsecure.First()))
{
// Add two for compatibility
dicQuery.Add("insecure", "1");
dicQuery.Add("allowInsecure", "1");
}
else
{
dicQuery.Add("insecure", "0");
dicQuery.Add("allowInsecure", "0");
}
return 0;
}
protected static int ResolveUriQuery(NameValueCollection query, ref ProfileItem item)
{
item.Flow = GetQueryValue(query, "flow");
item.StreamSecurity = GetQueryValue(query, "security");
@@ -164,7 +200,19 @@ public class BaseFmt
item.ShortId = GetQueryDecoded(query, "sid");
item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
item.AllowInsecure = new[] { "allowInsecure", "allow_insecure", "insecure" }.Any(k => (query[k] ?? "") == "1") ? "true" : "";
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
{
item.AllowInsecure = Global.AllowInsecure.First();
}
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
{
item.AllowInsecure = Global.AllowInsecure.Skip(1).First();
}
else
{
item.AllowInsecure = string.Empty;
}
item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp));
switch (item.Network)

View File

@@ -37,7 +37,7 @@ public class FmtHandler
try
{
string str = config.TrimEx();
var str = config.TrimEx();
if (str.IsNullOrEmpty())
{
msg = ResUI.FailedReadConfiguration;

View File

@@ -12,7 +12,9 @@ public class Hysteria2Fmt : BaseFmt
var url = Utils.TryUri(str);
if (url == null)
{
return null;
}
item.Address = url.IdnHost;
item.Port = url.Port;
@@ -20,10 +22,8 @@ public class Hysteria2Fmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item);
ResolveUriQuery(query, ref item);
item.Path = GetQueryDecoded(query, "obfs-password");
item.AllowInsecure = GetQueryValue(query, "insecure") == "1" ? "true" : "false";
item.Ports = GetQueryDecoded(query, "mport");
return item;
@@ -32,29 +32,25 @@ public class Hysteria2Fmt : BaseFmt
public static string? ToUri(ProfileItem? item)
{
if (item == null)
{
return null;
string url = string.Empty;
}
string remark = string.Empty;
var url = string.Empty;
var remark = string.Empty;
if (item.Remarks.IsNotEmpty())
{
remark = "#" + Utils.UrlEncode(item.Remarks);
}
var dicQuery = new Dictionary<string, string>();
if (item.Sni.IsNotEmpty())
{
dicQuery.Add("sni", item.Sni);
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
}
ToUriQueryLite(item, ref dicQuery);
if (item.Path.IsNotEmpty())
{
dicQuery.Add("obfs", "salamander");
dicQuery.Add("obfs-password", Utils.UrlEncode(item.Path));
}
dicQuery.Add("insecure", item.AllowInsecure.ToLower() == "true" ? "1" : "0");
if (item.Ports.IsNotEmpty())
{
dicQuery.Add("mport", Utils.UrlEncode(item.Ports.Replace(':', '-')));

View File

@@ -23,7 +23,7 @@ public class TrojanFmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query);
_ = ResolveStdTransport(query, ref item);
ResolveUriQuery(query, ref item);
return item;
}
@@ -40,7 +40,7 @@ public class TrojanFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks);
}
var dicQuery = new Dictionary<string, string>();
_ = GetStdTransport(item, null, ref dicQuery);
ToUriQuery(item, null, ref dicQuery);
return ToUri(EConfigType.Trojan, item.Address, item.Port, item.Id, dicQuery, remark);
}

View File

@@ -29,7 +29,7 @@ public class TuicFmt : BaseFmt
}
var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item);
ResolveUriQuery(query, ref item);
item.HeaderType = GetQueryValue(query, "congestion_control");
return item;
@@ -47,15 +47,10 @@ public class TuicFmt : BaseFmt
{
remark = "#" + Utils.UrlEncode(item.Remarks);
}
var dicQuery = new Dictionary<string, string>();
if (item.Sni.IsNotEmpty())
{
dicQuery.Add("sni", item.Sni);
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
}
ToUriQueryLite(item, ref dicQuery);
dicQuery.Add("congestion_control", item.HeaderType);
return ToUri(EConfigType.TUIC, item.Address, item.Port, $"{item.Id}:{item.Security}", dicQuery, remark);

View File

@@ -26,7 +26,7 @@ public class VLESSFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
item.Security = GetQueryValue(query, "encryption", Global.None);
item.StreamSecurity = GetQueryValue(query, "security");
_ = ResolveStdTransport(query, ref item);
ResolveUriQuery(query, ref item);
return item;
}
@@ -52,7 +52,7 @@ public class VLESSFmt : BaseFmt
{
dicQuery.Add("encryption", Global.None);
}
_ = GetStdTransport(item, Global.None, ref dicQuery);
ToUriQuery(item, Global.None, ref dicQuery);
return ToUri(EConfigType.VLESS, item.Address, item.Port, item.Id, dicQuery, remark);
}

View File

@@ -39,7 +39,8 @@ public class VmessFmt : BaseFmt
tls = item.StreamSecurity,
sni = item.Sni,
alpn = item.Alpn,
fp = item.Fingerprint
fp = item.Fingerprint,
insecure = item.AllowInsecure.Equals(Global.AllowInsecure.First()) ? "1" : "0"
};
var url = JsonUtils.Serialize(vmessQRCode);
@@ -94,6 +95,7 @@ public class VmessFmt : BaseFmt
item.Sni = Utils.ToString(vmessQRCode.sni);
item.Alpn = Utils.ToString(vmessQRCode.alpn);
item.Fingerprint = Utils.ToString(vmessQRCode.fp);
item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.AllowInsecure.First() : string.Empty;
return item;
}
@@ -118,7 +120,7 @@ public class VmessFmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item);
ResolveUriQuery(query, ref item);
return item;
}

View File

@@ -18,7 +18,13 @@ public static class ProxySettingLinux
private static async Task ExecCmd(List<string> args)
{
var fileName = await FileManager.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetLinuxShellFileName), false);
var customSystemProxyScriptPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyScriptPath;
var fileName = (customSystemProxyScriptPath.IsNotEmpty() && File.Exists(customSystemProxyScriptPath))
? customSystemProxyScriptPath
: await FileUtils.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetLinuxShellFileName), false);
// TODO: temporarily notify which script is being used
NoticeManager.Instance.SendMessage(fileName);
await Utils.GetCliWrapOutput(fileName, args);
}

View File

@@ -23,7 +23,13 @@ public static class ProxySettingOSX
private static async Task ExecCmd(List<string> args)
{
var fileName = await FileManager.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetOSXShellFileName), false);
var customSystemProxyScriptPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyScriptPath;
var fileName = (customSystemProxyScriptPath.IsNotEmpty() && File.Exists(customSystemProxyScriptPath))
? customSystemProxyScriptPath
: await FileUtils.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetOSXShellFileName), false);
// TODO: temporarily notify which script is being used
NoticeManager.Instance.SendMessage(fileName);
await Utils.GetCliWrapOutput(fileName, args);
}

View File

@@ -33,7 +33,7 @@ public static class SysProxyHandler
await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions);
break;
case ESysProxyType.ForcedChange when Utils.IsOSX():
case ESysProxyType.ForcedChange when Utils.IsMacOS():
await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions);
break;
@@ -45,7 +45,7 @@ public static class SysProxyHandler
await ProxySettingLinux.UnsetProxy();
break;
case ESysProxyType.ForcedClear when Utils.IsOSX():
case ESysProxyType.ForcedClear when Utils.IsMacOS():
await ProxySettingOSX.UnsetProxy();
break;
@@ -91,7 +91,7 @@ public static class SysProxyHandler
private static async Task SetWindowsProxyPac(int port)
{
var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac);
await PacManager.Instance.StartAsync(Utils.GetConfigPath(), port, portPac);
await PacManager.Instance.StartAsync(port, portPac);
var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}";
ProxySettingWindows.SetProxy(strProxy, "", 4);
}

View File

@@ -49,15 +49,6 @@ public class HttpClientHelper
return await httpClient.GetStringAsync(url);
}
public async Task<string?> GetAsync(HttpClient client, string url, CancellationToken token = default)
{
if (url.IsNullOrEmpty())
{
return null;
}
return await client.GetStringAsync(url, token);
}
public async Task PutAsync(string url, Dictionary<string, string> headers)
{
var jsonContent = JsonUtils.Serialize(headers);
@@ -80,156 +71,4 @@ public class HttpClientHelper
{
await httpClient.DeleteAsync(url);
}
public static async Task DownloadFileAsync(HttpClient client, string url, string fileName, IProgress<double>? progress, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(url);
ArgumentNullException.ThrowIfNull(fileName);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
if (!response.IsSuccessStatusCode)
{
throw new Exception(response.StatusCode.ToString());
}
var total = response.Content.Headers.ContentLength ?? -1L;
var canReportProgress = total != -1 && progress != null;
await using var stream = await response.Content.ReadAsStreamAsync(token);
await using var file = File.Create(fileName);
var totalRead = 0L;
var buffer = new byte[1024 * 1024];
var progressPercentage = 0;
while (true)
{
token.ThrowIfCancellationRequested();
var read = await stream.ReadAsync(buffer, token);
totalRead += read;
if (read == 0)
{
break;
}
await file.WriteAsync(buffer.AsMemory(0, read), token);
if (canReportProgress)
{
var percent = (int)(100.0 * totalRead / total);
//if (progressPercentage != percent && percent % 10 == 0)
{
progressPercentage = percent;
progress?.Report(percent);
}
}
}
if (canReportProgress)
{
progress?.Report(101);
}
}
public async Task DownloadDataAsync4Speed(HttpClient client, string url, IProgress<string> progress, CancellationToken token = default)
{
if (url.IsNullOrEmpty())
{
throw new ArgumentNullException(nameof(url));
}
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
if (!response.IsSuccessStatusCode)
{
throw new Exception(response.StatusCode.ToString());
}
//var total = response.Content.Headers.ContentLength.HasValue ? response.Content.Headers.ContentLength.Value : -1L;
//var canReportProgress = total != -1 && progress != null;
await using var stream = await response.Content.ReadAsStreamAsync(token);
var totalRead = 0L;
var buffer = new byte[1024 * 64];
var isMoreToRead = true;
var progressSpeed = string.Empty;
var totalDatetime = DateTime.Now;
var totalSecond = 0;
do
{
if (token.IsCancellationRequested)
{
if (totalRead > 0)
{
return;
}
else
{
token.ThrowIfCancellationRequested();
}
}
var read = await stream.ReadAsync(buffer, token);
if (read == 0)
{
isMoreToRead = false;
}
else
{
var data = new byte[read];
buffer.ToList().CopyTo(0, data, 0, read);
totalRead += read;
var ts = DateTime.Now - totalDatetime;
if (progress != null && ts.Seconds > totalSecond)
{
totalSecond = ts.Seconds;
var speed = (totalRead * 1d / ts.TotalMilliseconds / 1000).ToString("#0.0");
if (progressSpeed != speed)
{
progressSpeed = speed;
progress.Report(speed);
}
}
}
} while (isMoreToRead);
}
public async Task<int> GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout)
{
var responseTime = -1;
try
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout));
using var client = new HttpClient(new SocketsHttpHandler()
{
Proxy = webProxy,
UseProxy = webProxy != null
});
List<int> oneTime = new();
for (var i = 0; i < 2; i++)
{
var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
await Task.Delay(100);
}
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
}
catch //(Exception ex)
{
//Utile.SaveLog(ex.Message, ex);
}
return responseTime;
}
}

View File

@@ -81,21 +81,36 @@ public class ActionPrecheckManager(Config config)
{
case EConfigType.VMess:
if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
{
errors.Add(string.Format(ResUI.InvalidProperty, "Id"));
}
break;
case EConfigType.VLESS:
if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id) && item.Id.Length > 30)
if (item.Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Id) && item.Id.Length > 30))
{
errors.Add(string.Format(ResUI.InvalidProperty, "Id"));
}
if (!Global.Flows.Contains(item.Flow))
{
errors.Add(string.Format(ResUI.InvalidProperty, "Flow"));
}
break;
case EConfigType.Shadowsocks:
if (item.Id.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "Id"));
}
if (string.IsNullOrEmpty(item.Security) || !Global.SsSecuritiesInSingbox.Contains(item.Security))
{
errors.Add(string.Format(ResUI.InvalidProperty, "Security"));
}
break;
}
@@ -115,7 +130,7 @@ public class ActionPrecheckManager(Config config)
if (item.ConfigType.IsGroupType())
{
ProfileGroupItemManager.Instance.TryGet(item.IndexId, out var group);
if (group is null || group.ChildItems.IsNullOrEmpty())
if (group is null || group.NotHasChild())
{
errors.Add(string.Format(ResUI.GroupEmpty, item.Remarks));
return errors;
@@ -128,7 +143,11 @@ public class ActionPrecheckManager(Config config)
return errors;
}
foreach (var child in Utils.String2List(group.ChildItems))
var childIds = Utils.String2List(group.ChildItems) ?? [];
var subItems = await ProfileGroupItemManager.GetSubChildProfileItems(group);
childIds.AddRange(subItems.Select(p => p.IndexId));
foreach (var child in childIds)
{
var childErrors = new List<string>();
if (child.IsNullOrEmpty())

View File

@@ -0,0 +1,415 @@
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace ServiceLib.Manager;
/// <summary>
/// Manager for certificate operations with CA pinning to prevent MITM attacks
/// </summary>
public class CertPemManager
{
private static readonly string _tag = "CertPemManager";
private static readonly Lazy<CertPemManager> _instance = new(() => new());
public static CertPemManager Instance => _instance.Value;
/// <summary>
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
/// </summary>
private static readonly HashSet<string> TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase)
{
"EBD41040E4BB3EC742C9E381D31EF2A41A48B6685C96E7CEF3C1DF6CD4331C99", // GlobalSign Root CA
"6DC47172E01CBCB0BF62580D895FE2B8AC9AD4F873801E0C10B9C837D21EB177", // Entrust.net Premium 2048 Secure Server CA
"73C176434F1BC6D5ADF45B0E76E727287C8DE57616C1E6E6141A2B2CBC7D8E4C", // Entrust Root Certification Authority
"D8E0FEBC1DB2E38D00940F37D27D41344D993E734B99D5656D9778D4D8143624", // Certum Root CA
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3
"CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root
"C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA
"1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA
"3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA
"4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA
"7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA
"62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2
"F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA
"4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA
"0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority
"1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority
"41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA
"E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna
"C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority
"EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA
"6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány
"3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009
"CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3
"2530CC8E98321502BAD96F9B1FBA1B099E2D299E0F4548BB914F363BC0D4531F", // Izenpe.com
"45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2
"2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2
"568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services Root Certificate Authority - G2
"0376AB1D54C5F9803CE4B2E201A0EE7EEF7B57B636E8A93C9B8D4860C96F5FA7", // AffirmTrust Commercial
"0A81EC5A929777F145904AF38D5D509F66B5E2C58FCDB531058B0E17F3F0B41B", // AffirmTrust Networking
"70A73F7F376B60074248904534B11482D5BF0E698ECC498DF52577EBF2E93B9A", // AffirmTrust Premium
"BD71FDF6DA97E4CF62D1647ADD2581B07D79ADF8397EB4ECBA9C5E8488821423", // AffirmTrust Premium ECC
"5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA
"BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority
"513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2
"55926084EC963A64B96E2ABE01CE0BA86A64FBFEBCC7AAB5AFC155B37FD76066", // Actalis Authentication Root CA
"9A114025197C5BB95D94E63D55CD43790847B646B23CDF11ADA4A00EFF15FB48", // Buypass Class 2 Root CA
"EDF7EBBCA27A2A384D387B7D4010C666E2EDB4843E4C29B4AE1D5B9332E6B24D", // Buypass Class 3 Root CA
"FD73DAD31C644FF1B43BEF0CCDDA96710B9CD9875ECA7E31707AF3E96D522BBD", // T-TeleSec GlobalRoot Class 3
"49E7A442ACF0EA6287050054B52564B650E4F49E42E348D6AA38E039E957B1C1", // D-TRUST Root Class 3 CA 2 2009
"EEC5496B988CE98625B934092EEC2908BED0B0F316C2D4730C84EAF1F3D34881", // D-TRUST Root Class 3 CA 2 EV 2009
"E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2
"9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1
"59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA
"DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1
"91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2
"F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011
"8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3
"8FE4FB0AF93A4D0D67DB0BEBB23E37C71BF325DCBCDD240EA04DAF58B47E1840", // QuoVadis Root CA 2 G3
"88EF81DE202EB018452E43F864725CEA5FBD1FC2D9D205730709C5D8B8690F46", // QuoVadis Root CA 3 G3
"7D05EBB682339F8C9451EE094EEBFEFA7953A114EDB2F44949452FAB7D2FC185", // DigiCert Assured ID Root G2
"7E37CB8B4C47090CAB36551BA6F45DB840680FBA166A952DB100717F43053FC2", // DigiCert Assured ID Root G3
"CB3CCBB76031E5E0138F8DD39A23F9DE47FFC35E43C1144CEA27D46A5AB1CB5F", // DigiCert Global Root G2
"31AD6648F8104138C738F39EA4320133393E3A18CC02296EF97C2AC9EF6731D0", // DigiCert Global Root G3
"552F7BDCF1A7AF9E6CE672017F4F12ABF77240C78E761AC203D1D9D20AC89988", // DigiCert Trusted Root G4
"52F0E1C4E58EC629291B60317F074671B85D7EA80D5B07273463534B32B40234", // COMODO RSA Certification Authority
"E793C9B02FD8AA13E21C31228ACCB08119643B749C898964B1746D46C3D4CBD2", // USERTrust RSA Certification Authority
"4FF460D54B9C86DABFBCFC5712E0400D2BED3FBC4D4FBDAA86E06ADCD2A9AD7A", // USERTrust ECC Certification Authority
"179FBC148A3DD00FD24EA13458CC43BFA7F59C8182D783A513F6EBEC100C8924", // GlobalSign ECC Root CA - R5
"3C4FB0B95AB8B30032F432B86F535FE172C185D0FD39865837CF36187FA6F428", // Staat der Nederlanden Root CA - G3
"5D56499BE4D2E08BCFCAD08A3E38723D50503BDE706948E42F55603019E528AE", // IdenTrust Commercial Root CA 1
"30D0895A9A448A262091635522D1F52010B5867ACAE12C78EF958FD4F4389F2F", // IdenTrust Public Sector Root CA 1
"43DF5774B03E7FEF5FE40D931A7BEDF1BB2E6B42738C4E6D3841103D3AA7F339", // Entrust Root Certification Authority - G2
"02ED0EB28C14DA45165C566791700D6451D7FB56F0B2AB1D3B8EB070E56EDFF5", // Entrust Root Certification Authority - EC1
"5CC3D78E4E1D5E45547A04E6873E64F90CF9536D1CCC2EF800F355C4C5FD70FD", // CFCA EV ROOT
"6B9C08E86EB0F767CFAD65CD98B62149E5494A67F5845E7BD1ED019F27B86BD6", // OISTE WISeKey Global Root GB CA
"A1339D33281A0B56E557D3D32B1CE7F9367EB094BD5FA72A7E5004C8DED7CAFE", // SZAFIR ROOT CA2
"B676F2EDDAE8775CD36CB0F63CD1D4603961F49E6265BA013A2F0307B6D0B804", // Certum Trusted Network CA 2
"A040929A02CE53B4ACF4F2FFC6981CE4496F755E6D45FE0B2A692BCD52523F36", // Hellenic Academic and Research Institutions RootCA 2015
"44B545AA8A25E65A73CA15DC27FC36D24C1CB9953A066539B11582DC487B4833", // Hellenic Academic and Research Institutions ECC RootCA 2015
"96BCEC06264976F37460779ACF28C5A7CFE8A3C0AAE11A8FFCEE05C0BDDF08C6", // ISRG Root X1
"EBC5570C29018C4D67B1AA127BAF12F703B4611EBC17B7DAB5573894179B93FA", // AC RAIZ FNMT-RCM
"8ECDE6884F3D87B1125BA31AC3FCB13D7016DE7F57CC904FE1CB97C6AE98196E", // Amazon Root CA 1
"1BA5B2AA8C65401A82960118F80BEC4F62304D83CEC4713A19C39C011EA46DB4", // Amazon Root CA 2
"18CE6CFE7BF14E60B2E347B8DFE868CB31D02EBB3ADA271569F50343B46DB3A4", // Amazon Root CA 3
"E35D28419ED02025CFA69038CD623962458DA5C695FBDEA3C22B0BFB25897092", // Amazon Root CA 4
"A1A86D04121EB87F027C66F53303C28E5739F943FC84B38AD6AF009035DD9457", // D-TRUST Root CA 3 2013
"46EDC3689046D53A453FB3104AB80DCAEC658B2660EA1629DD7E867990648716", // TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1
"BFFF8FD04433487D6A8AA60C1A29767A9FC2BBB05E420F713A13B992891D3893", // GDCA TrustAUTH R5 ROOT
"85666A562EE0BE5CE925C1D8890A6F76A87EC16D4D7D5F29EA7419CF20123B69", // SSL.com Root Certification Authority RSA
"3417BB06CC6007DA1B961C920B8AB4CE3FAD820E4AA30B9ACBC4A74EBDCEBC65", // SSL.com Root Certification Authority ECC
"2E7BF16CC22485A7BBE2AA8696750761B0AE39BE3B2FE9D0CC6D4EF73491425C", // SSL.com EV Root Certification Authority RSA R2
"22A2C1F7BDED704CC1E701B5F408C310880FE956B5DE2A4A44F99C873A25A7C8", // SSL.com EV Root Certification Authority ECC
"2CABEAFE37D06CA22ABA7391C0033D25982952C453647349763A3AB5AD6CCF69", // GlobalSign Root CA - R6
"8560F91C3624DABA9570B5FEA0DBE36FF11A8323BE9486854FB3F34A5571198D", // OISTE WISeKey Global Root GC CA
"9BEA11C976FE014764C1BE56A6F914B5A560317ABD9988393382E5161AA0493C", // UCA Global G2 Root
"D43AF9B35473755C9684FC06D7D8CB70EE5C28E773FB294EB41EE71722924D24", // UCA Extended Validation Root
"D48D3D23EEDB50A459E55197601C27774B9D7B18C94D5A059511A10250B93168", // Certigna Root CA
"40F6AF0346A99AA1CD1D555A4E9CCE62C7F9634603EE406615833DC8C8D00367", // emSign Root CA - G1
"86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B", // emSign ECC Root CA - G3
"125609AA301DA0A249B97A8239CB6A34216F44DCAC9F3954B14292F2E8C8608F", // emSign Root CA - C1
"BC4D809B15189D78DB3E1D8CF4F9726A795DA1643CA5F1358E1DDB0EDC0D7EB3", // emSign ECC Root CA - C3
"5A2FC03F0C83B090BBFA40604B0988446C7636183DF9846E17101A447FB8EFD6", // Hongkong Post Root CA 3
"DB3517D1F6732A2D5AB97C533EC70779EE3270A62FB4AC4238372460E6F01E88", // Entrust Root Certification Authority - G4
"358DF39D764AF9E1B766E9C972DF352EE15CFAC227AF6AD1D70E8E4A6EDCBA02", // Microsoft ECC Root Certificate Authority 2017
"C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017
"BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017
"657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2
"97552015F5DDFC3C8788C006944555408894450084F100867086BC1A2BB58DC8", // Trustwave Global Certification Authority
"945BBC825EA554F489D1FD51A73DDF2EA624AC7019A05205225C22A78CCFA8B4", // Trustwave Global ECC P256 Certification Authority
"55903859C8C0C3EBB8759ECE4E2557225FF5758BBD38EBD48276601E1BD58097", // Trustwave Global ECC P384 Certification Authority
"88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority
"554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS
"319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45
"5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45
"4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46
"CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46
"9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020
"FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA
"6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA
"FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA
"2E44102AB58CB85419451C8E19D9ACF3662CAFBC614B6A53960A30F7D0E2EB41", // TunTrust Root CA
"D95D0E8EDA79525BF9BEB11B14D2100D3294985F0C62D9FABD9CD999ECCB7B1D", // HARICA TLS RSA Root CA 2021
"3F99CC474ACFCE4DFED58794665E478D1547739F2E780F1BB4CA9B133097D401", // HARICA TLS ECC Root CA 2021
"1BE7ABE30686B16348AFD1C61B6866A0EA7F4821E67D5E8AF937CF8011BC750D", // HARICA Client RSA Root CA 2021
"8DD4B5373CB0DE36769C12339280D82746B3AA6CD426E797A31BABE4279CF00B", // HARICA Client ECC Root CA 2021
"57DE0583EFD2B26E0361DA99DA9DF4648DEF7EE8441C3B728AFA9BCDE0F9B26A", // Autoridad de Certificacion Firmaprofesional CIF A62634068
"30FBBA2C32238E2A98547AF97931E550428B9B3F1C8EEB6633DCFA86C5B27DD3", // vTrus ECC Root CA
"8A71DE6559336F426C26E53880D00D88A18DA4C6A91F0DCB6194E206C5C96387", // vTrus Root CA
"69729B8E15A86EFC177A57AFB7171DFC64ADD28C2FCA8CF1507E34453CCB1470", // ISRG Root X2
"F015CE3CC239BFEF064BE9F1D2C417E1A0264A0A94BE1F0C8D121864EB6949CC", // HiPKI Root CA - G1
"B085D70B964F191A73E4AF0D54AE7A0E07AAFDAF9B71DD0862138AB7325A24A2", // GlobalSign ECC Root CA - R4
"D947432ABDE7B7FA90FC2E6B59101B1280E0E1C7E4E40FA3C6887FFF57A7F4CF", // GTS Root R1
"8D25CD97229DBF70356BDA4EB3CC734031E24CF00FAFCFD32DC76EB5841C7EA8", // GTS Root R2
"34D8A73EE208D9BCDB0D956520934B4E40E69482596E8B6F73C8426B010A6F48", // GTS Root R3
"349DFA4058C5E263123B398AE795573C4E1313C83FE68F93556CD5E8031B3C7D", // GTS Root R4
"242B69742FCB1E5B2ABF98898B94572187544E5B4D9911786573621F6A74B82C", // Telia Root CA v2
"E59AAA816009C22BFF5B25BAD37DF306F049797C1F81D85AB089E657BD8F0044", // D-TRUST BR Root CA 1 2020
"08170D1AA36453901A2F959245E347DB0C8D37ABAABC56B81AA100DC958970DB", // D-TRUST EV Root CA 1 2020
"018E13F0772532CF809BD1B17281867283FC48C6E13BE9C69812854A490C1B05", // DigiCert TLS ECC P384 Root G5
"371A00DC0533B3721A7EEB40E8419E70799D2B0A0F2C1D80693165F7CEC4AD75", // DigiCert TLS RSA4096 Root G5
"E8E8176536A60CC2C4E10187C3BEFCA20EF263497018F566D5BEA0F94D0C111B", // DigiCert SMIME ECC P384 Root G5
"90370D3EFA88BF58C30105BA25104A358460A7FA52DFC2011DF233A0F417912A", // DigiCert SMIME RSA4096 Root G5
"77B82CD8644C4305F7ACC5CB156B45675004033D51C60C6202A8E0C33467D3A0", // Certainly Root R1
"B4585F22E4AC756A4E8612A1361C5D9D031A93FD84FEBB778FA3068B0FC42DC2", // Certainly Root E1
"82BD5D851ACF7F6E1BA7BFCBC53030D0E7BC3C21DF772D858CAB41D199BDF595", // DIGITALSIGN GLOBAL ROOT RSA CA
"261D7114AE5F8FF2D8C7209A9DE4289E6AFC9D717023D85450909199F1857CFE", // DIGITALSIGN GLOBAL ROOT ECDSA CA
"E74FBDA55BD564C473A36B441AA799C8A68E077440E8288B9FA1E50E4BBACA11", // Security Communication ECC RootCA1
"F3896F88FE7C0A882766A7FA6AD2749FB57A7F3E98FB769C1FA7B09C2C44D5AE", // BJCA Global Root CA1
"574DF6931E278039667B720AFDC1600FC27EB66DD3092979FB73856487212882", // BJCA Global Root CA2
"48E1CF9E43B688A51044160F46D773B8277FE45BEAAD0E4DF90D1974382FEA99", // LAWtrust Root CA2 (4096)
"22D9599234D60F1D4BC7C7E96F43FA555B07301FD475175089DAFB8C25E477B3", // Sectigo Public Email Protection Root E46
"D5917A7791EB7CF20A2E57EB98284A67B28A57E89182DA53D546678C9FDE2B4F", // Sectigo Public Email Protection Root R46
"C90F26F0FB1B4018B22227519B5CA2B53E2CA5B3BE5CF18EFE1BEF47380C5383", // Sectigo Public Server Authentication Root E46
"7BB647A62AEEAC88BF257AA522D01FFEA395E0AB45C73F93F65654EC38F25A06", // Sectigo Public Server Authentication Root R46
"8FAF7D2E2CB4709BB8E0B33666BF75A5DD45B5DE480F8EA8D4BFE6BEBC17F2ED", // SSL.com TLS RSA Root CA 2022
"C32FFD9F46F936D16C3673990959434B9AD60AAFBB9E7CF33654F144CC1BA143", // SSL.com TLS ECC Root CA 2022
"AD7DD58D03AEDB22A30B5084394920CE12230C2D8017AD9B81AB04079BDD026B", // SSL.com Client ECC Root CA 2022
"1D4CA4A2AB21D0093659804FC0EB2175A617279B56A2475245C9517AFEB59153", // SSL.com Client RSA Root CA 2022
"E38655F4B0190C84D3B3893D840A687E190A256D98052F159E6D4A39F589A6EB", // Atos TrustedRoot Root CA ECC G2 2020
"78833A783BB2986C254B9370D3C20E5EBA8FA7840CBF63FE17297A0B0119685E", // Atos TrustedRoot Root CA RSA G2 2020
"B2FAE53E14CCD7AB9212064701AE279C1D8988FACB775FA8A008914E663988A8", // Atos TrustedRoot Root CA ECC TLS 2021
"81A9088EA59FB364C548A6F85559099B6F0405EFBF18E5324EC9F457BA00112F", // Atos TrustedRoot Root CA RSA TLS 2021
"E0D3226AEB1163C2E48FF9BE3B50B4C6431BE7BB1EACC5C36B5D5EC509039A08", // TrustAsia Global Root CA G3
"BE4B56CB5056C0136A526DF444508DAA36A0B54F42E4AC38F72AF470E479654C", // TrustAsia Global Root CA G4
"D92C171F5CF890BA428019292927FE22F3207FD2B54449CB6F675AF4922146E2", // D-Trust SBR Root CA 1 2022
"DBA84DD7EF622D485463A90137EA4D574DF8550928F6AFA03B4D8B1141E636CC", // D-Trust SBR Root CA 2 2022
"3AE6DF7E0D637A65A8C81612EC6F9A142F85A16834C10280D88E707028518755", // Telekom Security SMIME ECC Root 2021
"578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020
"78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023
"EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023
"BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB
"3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA
"3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2
"3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12
"4B009C1034494F9AB56BBA3BA1D62731FC4D20D8955ADCEC10A925607261E338", // SecureSign Root CA14
"E778F0F095FE843729CD1A0082179E5314A9C291442805E1FB1D8FB6B8886C3A", // SecureSign Root CA15
"0552E6F83FDF65E8FA9670E666DF28A4E21340B510CBE52566F97C4FB94B2BD1", // D-TRUST BR Root CA 2 2023
"436472C1009A325C54F1A5BBB5468A7BAEECCBE05DE5F099CB70D3FE41E13C16", // TrustAsia SMIME ECC Root CA
"C7796BEB62C101BB143D262A7C96A0C6168183223EF50D699632D86E03B8CC9B", // TrustAsia SMIME RSA Root CA
"C0076B9EF0531FB1A656D67C4EBE97CD5DBAA41EF44598ACC2489878C92D8711", // TrustAsia TLS ECC Root CA
"06C08D7DAFD876971EB1124FE67F847EC0C7A158D3EA53CBE940E2EA9791F4C3", // TrustAsia TLS RSA Root CA
"8E8221B2E7D4007836A1672F0DCC299C33BC07D316F132FA1A206D587150F1CE", // D-TRUST EV Root CA 2 2023
"9A12C392BFE57891A0C545309D4D9FD567E480CB613D6342278B195C79A7931F", // SwissSign RSA SMIME Root CA 2022 - 1
"193144F431E0FDDB740717D4DE926A571133884B4360D30E272913CBE660CE41", // SwissSign RSA TLS Root CA 2022 - 1
"D9A32485A8CCA85539CEF12FFFFF711378A17851D73DA2732AB4302D763BD62B", // OISTE Client Root ECC G1
"D02A0F994A868C66395F2E7A880DF509BD0C29C96DE16015A0FD501EDA4F96A9", // OISTE Client Root RSA G1
"EEC997C0C30F216F7E3B8B307D2BAE42412D753FC8219DAFD1520B2572850F49", // OISTE Server Root ECC G1
"9AE36232A5189FFDDB353DFD26520C015395D22777DAC59DB57B98C089A651E6", // OISTE Server Root RSA G1
};
/// <summary>
/// Get certificate in PEM format from a server with CA pinning validation
/// </summary>
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4)
{
try
{
var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate);
var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = ValidateServerCertificate
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
var remote = ssl.RemoteCertificate;
if (remote == null)
{
return (null, null);
}
var leaf = new X509Certificate2(remote);
return (ExportCertToPem(leaf), null);
}
catch (OperationCanceledException)
{
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds"));
return (null, $"Connection timeout after {timeout} seconds");
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return (null, ex.Message);
}
}
/// <summary>
/// Get certificate chain in PEM format from a server with CA pinning validation
/// </summary>
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4)
{
var pemList = new List<string>();
try
{
var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate);
var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = ValidateServerCertificate
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
if (ssl.RemoteCertificate is not X509Certificate2 certChain)
{
return (pemList, null);
}
var chain = new X509Chain();
chain.Build(certChain);
foreach (var element in chain.ChainElements)
{
var pem = ExportCertToPem(element.Certificate);
pemList.Add(pem);
}
return (pemList, null);
}
catch (OperationCanceledException)
{
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds"));
return (pemList, $"Connection timeout after {timeout} seconds");
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return (pemList, ex.Message);
}
}
/// <summary>
/// Validate server certificate with CA pinning
/// </summary>
private bool ValidateServerCertificate(
object sender,
X509Certificate? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
if (certificate == null)
{
return false;
}
// Check certificate name mismatch
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
{
return false;
}
// Build certificate chain
var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
var certChain = chain ?? new X509Chain();
certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
certChain.ChainPolicy.VerificationTime = DateTime.Now;
certChain.Build(cert2);
// Find root CA
if (certChain.ChainElements.Count == 0)
{
return false;
}
var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate;
var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256);
return TrustedCaThumbprints.Contains(rootThumbprint);
}
public static string ExportCertToPem(X509Certificate2 cert)
{
var der = cert.Export(X509ContentType.Cert);
var b64 = Convert.ToBase64String(der);
return $"-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----\n";
}
/// <summary>
/// Parse concatenated PEM certificates string into a list of individual certificates
/// Normalizes format: removes line breaks from base64 content for better compatibility
/// </summary>
/// <param name="pemChain">Concatenated PEM certificates string (supports both \r\n and \n line endings)</param>
/// <returns>List of individual PEM certificate strings with normalized format</returns>
public static List<string> ParsePemChain(string pemChain)
{
var certs = new List<string>();
if (string.IsNullOrWhiteSpace(pemChain))
{
return certs;
}
// Normalize line endings (CRLF -> LF) at the beginning
pemChain = pemChain.Replace("\r\n", "\n").Replace("\r", "\n");
const string beginMarker = "-----BEGIN CERTIFICATE-----";
const string endMarker = "-----END CERTIFICATE-----";
var index = 0;
while (index < pemChain.Length)
{
var beginIndex = pemChain.IndexOf(beginMarker, index, StringComparison.Ordinal);
if (beginIndex == -1)
break;
var endIndex = pemChain.IndexOf(endMarker, beginIndex, StringComparison.Ordinal);
if (endIndex == -1)
break;
// Extract certificate content
var base64Start = beginIndex + beginMarker.Length;
var base64Content = pemChain.Substring(base64Start, endIndex - base64Start);
// Remove all whitespace from base64 content
base64Content = new string(base64Content.Where(c => !char.IsWhiteSpace(c)).ToArray());
// Reconstruct with clean format: BEGIN marker + base64 (no line breaks) + END marker
var normalizedCert = $"{beginMarker}\n{base64Content}\n{endMarker}\n";
certs.Add(normalizedCert);
// Move to next certificate
index = endIndex + endMarker.Length;
}
return certs;
}
/// <summary>
/// Concatenate a list of PEM certificates into a single string
/// </summary>
/// <param name="pemList">List of individual PEM certificate strings</param>
/// <returns>Concatenated PEM certificates string</returns>
public static string ConcatenatePemChain(IEnumerable<string> pemList)
{
if (pemList == null)
{
return string.Empty;
}
return string.Concat(pemList);
}
}

View File

@@ -34,8 +34,8 @@ public class CoreAdminManager
StringBuilder sb = new();
sb.AppendLine("#!/bin/bash");
var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}";
sb.AppendLine($"sudo -S {cmdLine}");
var shFilePath = await FileManager.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
sb.AppendLine($"exec sudo -S -- {cmdLine}");
var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
var procService = new ProcessService(
fileName: shFilePath,
@@ -67,8 +67,8 @@ public class CoreAdminManager
try
{
var shellFileName = Utils.IsOSX() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName;
var shFilePath = await FileManager.CreateLinuxShellFile("kill_as_sudo.sh", EmbedUtils.GetEmbedText(shellFileName), true);
var shellFileName = Utils.IsMacOS() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName;
var shFilePath = await FileUtils.CreateLinuxShellFile("kill_as_sudo.sh", EmbedUtils.GetEmbedText(shellFileName), true);
if (shFilePath.Contains(' '))
{
shFilePath = shFilePath.AppendQuotes();

View File

@@ -8,7 +8,7 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value;
private Config _config;
private WindowsJob? _processJob;
private WindowsJobService? _processJob;
private ProcessService? _processService;
private ProcessService? _processPreService;
private bool _linuxSudo = false;
@@ -27,7 +27,7 @@ public class CoreManager
var toPath = Utils.GetBinPath("");
if (fromPath != toPath)
{
FileManager.CopyDirectory(fromPath, toPath, true, false);
FileUtils.CopyDirectory(fromPath, toPath, true, false);
}
}

View File

@@ -5,7 +5,6 @@ public class PacManager
private static readonly Lazy<PacManager> _instance = new(() => new PacManager());
public static PacManager Instance => _instance.Value;
private string _configPath;
private int _httpPort;
private int _pacPort;
private TcpListener? _tcpListener;
@@ -13,11 +12,10 @@ public class PacManager
private bool _isRunning;
private bool _needRestart = true;
public async Task StartAsync(string configPath, int httpPort, int pacPort)
public async Task StartAsync(int httpPort, int pacPort)
{
_needRestart = configPath != _configPath || httpPort != _httpPort || pacPort != _pacPort || !_isRunning;
_needRestart = httpPort != _httpPort || pacPort != _pacPort || !_isRunning;
_configPath = configPath;
_httpPort = httpPort;
_pacPort = pacPort;
@@ -32,22 +30,22 @@ public class PacManager
private async Task InitText()
{
var path = Path.Combine(_configPath, "pac.txt");
var customSystemProxyPacPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyPacPath;
var fileName = (customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath))
? customSystemProxyPacPath
: Path.Combine(Utils.GetConfigPath(), "pac.txt");
// Delete the old pac file
if (File.Exists(path) && Utils.GetFileHash(path).Equals("b590c07280f058ef05d5394aa2f927fe"))
{
File.Delete(path);
}
// TODO: temporarily notify which script is being used
NoticeManager.Instance.SendMessage(fileName);
if (!File.Exists(path))
if (!File.Exists(fileName))
{
var pac = EmbedUtils.GetEmbedText(Global.PacFileName);
await File.AppendAllTextAsync(path, pac);
await File.AppendAllTextAsync(fileName, pac);
}
var pacText =
(await File.ReadAllTextAsync(path)).Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;");
var pacText = await File.ReadAllTextAsync(fileName);
pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;");
var sb = new StringBuilder();
sb.AppendLine("HTTP/1.0 200 OK");

View File

@@ -173,13 +173,19 @@ public class ProfileGroupItemManager
public static bool HasCycle(string? indexId, HashSet<string> visited, HashSet<string> stack)
{
if (indexId.IsNullOrEmpty())
{
return false;
}
if (stack.Contains(indexId))
{
return true;
}
if (visited.Contains(indexId))
{
return false;
}
visited.Add(indexId);
stack.Add(indexId);
@@ -220,11 +226,14 @@ public class ProfileGroupItemManager
public static async Task<(List<ProfileItem> Items, ProfileGroupItem? Group)> GetChildProfileItems(string? indexId)
{
Instance.TryGet(indexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
if (profileGroupItem == null || profileGroupItem.NotHasChild())
{
return (new List<ProfileItem>(), profileGroupItem);
}
var items = await GetChildProfileItems(profileGroupItem);
var subItems = await GetSubChildProfileItems(profileGroupItem);
items.AddRange(subItems);
return (items, profileGroupItem);
}
@@ -248,20 +257,47 @@ public class ProfileGroupItemManager
return childProfiles;
}
public static async Task<List<ProfileItem>> GetSubChildProfileItems(ProfileGroupItem? group)
{
if (group == null || group.SubChildItems.IsNullOrEmpty())
{
return new();
}
var childProfiles = await AppManager.Instance.ProfileItems(group.SubChildItems);
return childProfiles.Where(p =>
p != null &&
p.IsValid() &&
!p.ConfigType.IsComplexType() &&
(group.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, group.Filter))
)
.ToList();
}
public static async Task<HashSet<string>> GetAllChildDomainAddresses(string indexId)
{
// include grand children
var childAddresses = new HashSet<string>();
if (!Instance.TryGet(indexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty())
if (!Instance.TryGet(indexId, out var groupItem) || groupItem == null)
{
return childAddresses;
}
var childIds = Utils.String2List(groupItem.ChildItems);
if (groupItem.SubChildItems.IsNotEmpty())
{
var subItems = await GetSubChildProfileItems(groupItem);
subItems.ForEach(p => childAddresses.Add(p.Address));
}
var childIds = Utils.String2List(groupItem.ChildItems) ?? [];
foreach (var childId in childIds)
{
var childNode = await AppManager.Instance.GetProfileItem(childId);
if (childNode == null)
{
continue;
}
if (!childNode.IsComplex())
{

View File

@@ -56,9 +56,9 @@ public class TaskManager
{
//Logging.SaveLog("Execute delete expired files");
FileManager.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1));
FileManager.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1));
FileManager.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1));
FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1));
FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
try
{
@@ -111,11 +111,10 @@ public class TaskManager
{
Logging.SaveLog("Execute update geo files");
var updateHandle = new UpdateService();
await updateHandle.UpdateGeoFileAll(_config, async (success, msg) =>
await new UpdateService(_config, async (success, msg) =>
{
await _updateFunc?.Invoke(false, msg);
});
}).UpdateGeoFileAll();
}
}
}

View File

@@ -219,6 +219,8 @@ public class SystemProxyItem
public string SystemProxyExceptions { get; set; }
public bool NotProxyLocalAddress { get; set; } = true;
public string SystemProxyAdvancedProtocol { get; set; }
public string? CustomSystemProxyPacPath { get; set; }
public string? CustomSystemProxyScriptPath { get; set; }
}
[Serializable]

View File

@@ -8,5 +8,14 @@ public class ProfileGroupItem
public string ChildItems { get; set; }
public string? SubChildItems { get; set; }
public string? Filter { get; set; }
public EMultipleLoad MultipleLoad { get; set; } = EMultipleLoad.LeastPing;
public bool NotHasChild()
{
return string.IsNullOrWhiteSpace(ChildItems) && string.IsNullOrWhiteSpace(SubChildItems);
}
}

View File

@@ -28,7 +28,7 @@ public class ProfileItem : ReactiveObject
public string GetSummary()
{
var summary = $"[{(ConfigType).ToString()}] ";
var summary = $"[{ConfigType.ToString()}] ";
if (IsComplex())
{
summary += $"[{CoreType.ToString()}]{Remarks}";
@@ -69,30 +69,49 @@ public class ProfileItem : ReactiveObject
public bool IsValid()
{
if (IsComplex())
{
return true;
}
if (Address.IsNullOrEmpty() || Port is <= 0 or >= 65536)
{
return false;
}
switch (ConfigType)
{
case EConfigType.VMess:
if (Id.IsNullOrEmpty() || !Utils.IsGuidByParse(Id))
{
return false;
}
break;
case EConfigType.VLESS:
if (Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(Id) && Id.Length > 30))
{
return false;
}
if (!Global.Flows.Contains(Flow))
{
return false;
}
break;
case EConfigType.Shadowsocks:
if (Id.IsNullOrEmpty())
{
return false;
}
if (string.IsNullOrEmpty(Security) || !Global.SsSecuritiesInSingbox.Contains(Security))
{
return false;
}
break;
}
@@ -141,4 +160,5 @@ public class ProfileItem : ReactiveObject
public string Mldsa65Verify { get; set; }
public string Extra { get; set; }
public bool? MuxEnabled { get; set; }
public string Cert { get; set; }
}

View File

@@ -1,4 +1,4 @@
namespace ServiceLib.Common;
namespace ServiceLib.Models;
public class SemanticVersion
{

View File

@@ -181,6 +181,7 @@ public class Tls4Sbox
public bool? fragment { get; set; }
public string? fragment_fallback_delay { get; set; }
public bool? record_fragment { get; set; }
public List<string>? certificate { get; set; }
}
public class Multiplex4Sbox

View File

@@ -215,6 +215,7 @@ public class Dns4Ray
public class DnsServer4Ray
{
public string? address { get; set; }
public int? port { get; set; }
public List<string>? domains { get; set; }
public bool? skipFallback { get; set; }
public List<string>? expectedIPs { get; set; }
@@ -353,6 +354,14 @@ public class TlsSettings4Ray
public string? shortId { get; set; }
public string? spiderX { get; set; }
public string? mldsa65Verify { get; set; }
public List<CertificateSettings4Ray>? certificates { get; set; }
public bool? disableSystemRoot { get; set; }
}
public class CertificateSettings4Ray
{
public List<string>? certificate { get; set; }
public string? usage { get; set; }
}
public class TcpSettings4Ray

View File

@@ -38,4 +38,6 @@ public class VmessQRCode
public string alpn { get; set; } = string.Empty;
public string fp { get; set; } = string.Empty;
public string insecure { get; set; } = string.Empty;
}

View File

@@ -87,6 +87,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Certificate not set 的本地化字符串。
/// </summary>
public static string CertNotSet {
get {
return ResourceManager.GetString("CertNotSet", resourceCulture);
}
}
/// <summary>
/// 查找类似 Certificate set 的本地化字符串。
/// </summary>
public static string CertSet {
get {
return ResourceManager.GetString("CertSet", resourceCulture);
}
}
/// <summary>
/// 查找类似 Please check the Configuration settings first. 的本地化字符串。
/// </summary>
@@ -1023,6 +1041,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Test real delay 的本地化字符串。
/// </summary>
public static string menuFastRealPing {
get {
return ResourceManager.GetString("menuFastRealPing", resourceCulture);
}
}
/// <summary>
/// 查找类似 Full Config Template Setting 的本地化字符串。
/// </summary>
@@ -1636,7 +1663,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Server List 的本地化字符串。
/// 查找类似 Configuration List 的本地化字符串。
/// </summary>
public static string menuServerList {
get {
@@ -2292,6 +2319,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Please set a valid domain 的本地化字符串。
/// </summary>
public static string ServerNameMustBeValidDomain {
get {
return ResourceManager.GetString("ServerNameMustBeValidDomain", resourceCulture);
}
}
/// <summary>
/// 查找类似 {0} : {1}/s↑ | {2}/s↓ 的本地化字符串。
/// </summary>
@@ -2553,6 +2589,27 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Certificate Pinning 的本地化字符串。
/// </summary>
public static string TbCertPinning {
get {
return ResourceManager.GetString("TbCertPinning", resourceCulture);
}
}
/// <summary>
/// 查找类似 Server Certificate (PEM format, optional)
///When specified, the certificate will be pinned, and &quot;Allow Insecure&quot; will be disabled.
///
///The &quot;Get Certificate&quot; action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. 的本地化字符串。
/// </summary>
public static string TbCertPinningTips {
get {
return ResourceManager.GetString("TbCertPinningTips", resourceCulture);
}
}
/// <summary>
/// 查找类似 Clear system proxy 的本地化字符串。
/// </summary>
@@ -2760,6 +2817,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Fetch Certificate 的本地化字符串。
/// </summary>
public static string TbFetchCert {
get {
return ResourceManager.GetString("TbFetchCert", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fetch Certificate Chain 的本地化字符串。
/// </summary>
public static string TbFetchCertChain {
get {
return ResourceManager.GetString("TbFetchCertChain", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fingerprint 的本地化字符串。
/// </summary>
@@ -2949,6 +3024,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Auto add filtered configuration from subscription groups 的本地化字符串。
/// </summary>
public static string TbPolicyGroupSubChildTip {
get {
return ResourceManager.GetString("TbPolicyGroupSubChildTip", resourceCulture);
}
}
/// <summary>
/// 查找类似 Policy Group Type 的本地化字符串。
/// </summary>
@@ -3426,6 +3510,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Custom PAC file path 的本地化字符串。
/// </summary>
public static string TbSettingsCustomSystemProxyPacPath {
get {
return ResourceManager.GetString("TbSettingsCustomSystemProxyPacPath", resourceCulture);
}
}
/// <summary>
/// 查找类似 Custom system proxy script file path 的本地化字符串。
/// </summary>
public static string TbSettingsCustomSystemProxyScriptPath {
get {
return ResourceManager.GetString("TbSettingsCustomSystemProxyScriptPath", resourceCulture);
}
}
/// <summary>
/// 查找类似 Allow Insecure 的本地化字符串。
/// </summary>
@@ -3750,6 +3852,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 macOS displays this in the Dock (requires restart) 的本地化字符串。
/// </summary>
public static string TbSettingsMacOSShowInDock {
get {
return ResourceManager.GetString("TbSettingsMacOSShowInDock", resourceCulture);
}
}
/// <summary>
/// 查找类似 Main layout orientation (requires restart) 的本地化字符串。
/// </summary>

View File

@@ -1537,7 +1537,7 @@
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
<value>Configuration List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
@@ -1596,4 +1596,43 @@
<data name="TbBootstrapDNSTips" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
</data>
<data name="menuFastRealPing" xml:space="preserve">
<value>Test real delay</value>
</data>
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
<value>Auto add filtered configuration from subscription groups</value>
</data>
<data name="TbCertPinning" xml:space="preserve">
<value>Certificate Pinning</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data>
<data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value>
</data>
<data name="TbFetchCertChain" xml:space="preserve">
<value>Fetch Certificate Chain</value>
</data>
<data name="ServerNameMustBeValidDomain" xml:space="preserve">
<value>Please set a valid domain</value>
</data>
<data name="CertNotSet" xml:space="preserve">
<value>Certificate not set</value>
</data>
<data name="CertSet" xml:space="preserve">
<value>Certificate set</value>
</data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root>

File diff suppressed because it is too large Load Diff

View File

@@ -1537,7 +1537,7 @@
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
<value>Configuration List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
@@ -1596,4 +1596,43 @@
<data name="TbBootstrapDNSTips" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
</data>
<data name="menuFastRealPing" xml:space="preserve">
<value>Test real delay</value>
</data>
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
<value>Auto add filtered configuration from subscription groups</value>
</data>
<data name="TbCertPinning" xml:space="preserve">
<value>Certificate Pinning</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data>
<data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value>
</data>
<data name="TbFetchCertChain" xml:space="preserve">
<value>Fetch Certificate Chain</value>
</data>
<data name="ServerNameMustBeValidDomain" xml:space="preserve">
<value>Please set a valid domain</value>
</data>
<data name="CertNotSet" xml:space="preserve">
<value>Certificate not set</value>
</data>
<data name="CertSet" xml:space="preserve">
<value>Certificate set</value>
</data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root>

View File

@@ -1537,7 +1537,7 @@
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
<value>Configuration List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
@@ -1596,4 +1596,43 @@
<data name="TbBootstrapDNSTips" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
</data>
<data name="menuFastRealPing" xml:space="preserve">
<value>Test real delay</value>
</data>
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
<value>Auto add filtered configuration from subscription groups</value>
</data>
<data name="TbCertPinning" xml:space="preserve">
<value>Certificate Pinning</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data>
<data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value>
</data>
<data name="TbFetchCertChain" xml:space="preserve">
<value>Fetch Certificate Chain</value>
</data>
<data name="ServerNameMustBeValidDomain" xml:space="preserve">
<value>Please set a valid domain</value>
</data>
<data name="CertNotSet" xml:space="preserve">
<value>Certificate not set</value>
</data>
<data name="CertSet" xml:space="preserve">
<value>Certificate set</value>
</data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root>

View File

@@ -1537,7 +1537,7 @@
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
<value>Configuration List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
@@ -1596,4 +1596,43 @@
<data name="TbBootstrapDNSTips" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
</data>
<data name="menuFastRealPing" xml:space="preserve">
<value>Test real delay</value>
</data>
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
<value>Auto add filtered configuration from subscription groups</value>
</data>
<data name="TbCertPinning" xml:space="preserve">
<value>Certificate Pinning</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data>
<data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value>
</data>
<data name="TbFetchCertChain" xml:space="preserve">
<value>Fetch Certificate Chain</value>
</data>
<data name="ServerNameMustBeValidDomain" xml:space="preserve">
<value>Please set a valid domain</value>
</data>
<data name="CertNotSet" xml:space="preserve">
<value>Certificate not set</value>
</data>
<data name="CertSet" xml:space="preserve">
<value>Certificate set</value>
</data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root>

View File

@@ -427,7 +427,7 @@
<value>路由设置</value>
</data>
<data name="menuServers" xml:space="preserve">
<value>配置文件</value>
<value>配置</value>
</data>
<data name="menuSetting" xml:space="preserve">
<value>设置</value>
@@ -1528,13 +1528,13 @@
<value>添加链式代理</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>添加子</value>
<value>添加子配置</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>删除子</value>
<value>删除子配置</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>子项列表</value>
<value>子配置项</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>故障转移</value>
@@ -1593,4 +1593,43 @@
<data name="TbBootstrapDNSTips" xml:space="preserve">
<value>解析 DNS 服务器域名,需指定为 IP</value>
</data>
<data name="menuFastRealPing" xml:space="preserve">
<value>一键测试真连接延迟</value>
</data>
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
<value>自动从订阅分组添加过滤后的配置</value>
</data>
<data name="TbCertPinning" xml:space="preserve">
<value>固定证书</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>服务器证书PEM 格式,可选)
当指定此证书后,将固定该证书,并禁用“跳过证书验证”选项。
“获取证书”操作可能失败,原因可能是使用了自签证书,或系统中存在不受信任或恶意的 CA。</value>
</data>
<data name="TbFetchCert" xml:space="preserve">
<value>获取证书</value>
</data>
<data name="TbFetchCertChain" xml:space="preserve">
<value>获取证书链</value>
</data>
<data name="ServerNameMustBeValidDomain" xml:space="preserve">
<value>请设置有效的域名</value>
</data>
<data name="CertNotSet" xml:space="preserve">
<value>证书未设置</value>
</data>
<data name="CertSet" xml:space="preserve">
<value>证书已设置</value>
</data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>自定义 PAC 文件路径</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>自定义系统代理脚本文件路径</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS 在 Dock 栏中显示 (需重启)</value>
</data>
</root>

View File

@@ -121,7 +121,7 @@
<value>匯出分享連結至剪貼簿成功</value>
</data>
<data name="CheckServerSettings" xml:space="preserve">
<value>請先檢查設定檔設定</value>
<value>請先檢查設定</value>
</data>
<data name="ConfigurationFormatIncorrect" xml:space="preserve">
<value>設定格式不正確</value>
@@ -133,7 +133,7 @@
<value>下載開始...</value>
</data>
<data name="FailedConversionConfiguration" xml:space="preserve">
<value>轉換設定失敗</value>
<value>轉換設定失敗</value>
</data>
<data name="FailedGenDefaultConfiguration" xml:space="preserve">
<value>生成預設設定檔失敗</value>
@@ -142,10 +142,10 @@
<value>獲取預設設定失敗</value>
</data>
<data name="FailedImportedCustomServer" xml:space="preserve">
<value>匯入自訂設定設定檔失敗</value>
<value>匯入自訂設定失敗</value>
</data>
<data name="FailedReadConfiguration" xml:space="preserve">
<value>讀取設定失敗</value>
<value>讀取設定失敗</value>
</data>
<data name="FillCorrectServerPort" xml:space="preserve">
<value>請填寫正確格式的埠</value>
@@ -265,13 +265,13 @@
<value>請選擇協定</value>
</data>
<data name="PleaseSelectServer" xml:space="preserve">
<value>請先選擇設定</value>
<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>
<value>是否確定移除?</value>
</data>
<data name="SaveClientConfigurationIn" xml:space="preserve">
<value>用戶端設定檔儲存在:{0}</value>
@@ -283,10 +283,10 @@
<value>設定成功。{0}</value>
</data>
<data name="SuccessfullyImportedCustomServer" xml:space="preserve">
<value>成功匯入自訂設定設定檔</value>
<value>成功匯入自訂節點</value>
</data>
<data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve">
<value>成功從剪貼簿匯入 {0} 個設定檔</value>
<value>成功從剪貼簿匯入 {0} 個節點</value>
</data>
<data name="SuccessfullyImportedServerViaScan" xml:space="preserve">
<value>掃描匯入分享連結成功</value>
@@ -385,7 +385,7 @@
<value>所有</value>
</data>
<data name="FillServerAddressCustom" xml:space="preserve">
<value>請瀏覽匯入設定檔設定</value>
<value>請瀏覽匯入設定</value>
</data>
<data name="Speedtesting" xml:space="preserve">
<value>測試中...</value>
@@ -397,7 +397,7 @@
<value>本機</value>
</data>
<data name="MsgServerTitle" xml:space="preserve">
<value>設定檔過濾,按 Enter 執行</value>
<value>過濾,按 Enter 執行</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>檢查更新</value>
@@ -478,55 +478,55 @@
<value>掃描螢幕上的二維碼 (Ctrl+S)</value>
</data>
<data name="menuCopyServer" xml:space="preserve">
<value>複製所選設定檔</value>
<value>複製所選</value>
</data>
<data name="menuRemoveDuplicateServer" xml:space="preserve">
<value>移除重複的設定檔</value>
<value>移除重複</value>
</data>
<data name="menuRemoveServer" xml:space="preserve">
<value>移除所選設定檔 (多選) (Delete)</value>
<value>移除所選 (多選) (Delete)</value>
</data>
<data name="menuSetDefaultServer" xml:space="preserve">
<value>設為活動設定檔 (Enter)</value>
<value>設為活動 (Enter)</value>
</data>
<data name="menuClearServerStatistics" xml:space="preserve">
<value>清除所有服務統計資料</value>
</data>
<data name="menuRealPingServer" xml:space="preserve">
<value>測試設定檔真連線延遲 (多選) (Ctrl+R)</value>
<value>測試真連線延遲 (多選) (Ctrl+R)</value>
</data>
<data name="menuSortServerResult" xml:space="preserve">
<value>按測試結果排序</value>
</data>
<data name="menuSpeedServer" xml:space="preserve">
<value>測試設定檔速度 (多選) (Ctrl+T)</value>
<value>測試速度 (多選) (Ctrl+T)</value>
</data>
<data name="menuTcpingServer" xml:space="preserve">
<value>測試設定檔延遲 Tcping (多選) (Ctrl+O)</value>
<value>測試延遲 Tcping (多選) (Ctrl+O)</value>
</data>
<data name="menuExport2ClientConfig" xml:space="preserve">
<value>匯出所選設定檔完整設定</value>
<value>匯出所選完整設定</value>
</data>
<data name="menuExport2ShareUrl" xml:space="preserve">
<value>匯出分享連結至剪貼簿 (多選) (Ctrl+C)</value>
</data>
<data name="menuAddCustomServer" xml:space="preserve">
<value>新增自訂設定設定檔</value>
<value>新增自訂節點</value>
</data>
<data name="menuAddShadowsocksServer" xml:space="preserve">
<value>新增 [Shadowsocks] 設定檔</value>
<value>新增 [Shadowsocks] 節點</value>
</data>
<data name="menuAddSocksServer" xml:space="preserve">
<value>新增 [SOCKS] 設定檔</value>
<value>新增 [SOCKS] 節點</value>
</data>
<data name="menuAddTrojanServer" xml:space="preserve">
<value>新增 [Trojan] 設定檔</value>
<value>新增 [Trojan] 節點</value>
</data>
<data name="menuAddVlessServer" xml:space="preserve">
<value>新增 [VLESS] 設定檔</value>
<value>新增 [VLESS] 節點</value>
</data>
<data name="menuAddVmessServer" xml:space="preserve">
<value>新增 [VMess] 設定檔</value>
<value>新增 [VMess] 節點</value>
</data>
<data name="menuSelectAll" xml:space="preserve">
<value>全選 (Ctrl+A)</value>
@@ -676,7 +676,7 @@
<value>Core: 基礎設定</value>
</data>
<data name="TbCustomDnsRay" xml:space="preserve">
<value>V2ray Custom DNS</value>
<value>v2ray 自訂 DNS</value>
</data>
<data name="TbSettingsCoreKcp" xml:space="preserve">
<value>Core: KCP 設定</value>
@@ -691,7 +691,7 @@
<value>Outbound Freedom domainStrategy</value>
</data>
<data name="TbSettingsEnableAutoAdjustMainLvColWidth" xml:space="preserve">
<value>在更新訂閱後自動調整設定檔列寬</value>
<value>在更新訂閱後自動調整列寬</value>
</data>
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
<value>檢查 Pre-Release 更新 (請謹慎啟用)</value>
@@ -700,7 +700,7 @@
<value>例外</value>
</data>
<data name="TbSettingsExceptionTip" xml:space="preserve">
<value>例外:對於下列字元開頭的位址,不使用代理設定檔。使用分號 (;) 分隔。</value>
<value>例外:對於下列字元開頭的位址,不使用代理。使用分號 (;) 分隔。</value>
</data>
<data name="TbSettingsDisplayRealTimeSpeed" xml:space="preserve">
<value>顯示即時速度(需重啟)</value>
@@ -748,7 +748,7 @@
<value>系統代理設定</value>
</data>
<data name="TbSettingsTrayMenuServersLimit" xml:space="preserve">
<value>工具列右鍵選單設定展示數量限制</value>
<value>工具列右鍵選單設定展示數量限制</value>
</data>
<data name="TbSettingsUdpEnabled" xml:space="preserve">
<value>開啟 UDP</value>
@@ -781,7 +781,7 @@
<value>PAC 模式</value>
</data>
<data name="menuShareServer" xml:space="preserve">
<value>分享設定檔 (Ctrl+F)</value>
<value>分享 (Ctrl+F)</value>
</data>
<data name="menuRouting" xml:space="preserve">
<value>路由</value>
@@ -883,7 +883,7 @@
<value>請勿將代理伺服器用於本機Intranet位址</value>
</data>
<data name="menuMixedTestServer" xml:space="preserve">
<value>一鍵多執行緒測試延遲速度 (Ctrl+E)</value>
<value>一鍵延遲速度測試 (Ctrl+E)</value>
</data>
<data name="LvTestDelay" xml:space="preserve">
<value>延遲 (ms)</value>
@@ -913,7 +913,7 @@
<value>移至訂閱分組</value>
</data>
<data name="TbSettingsEnableDragDropSort" xml:space="preserve">
<value>啟動設定檔拖放排序 (需重啟)</value>
<value>啟拖放排序 (需重啟)</value>
</data>
<data name="TbAutoRefresh" xml:space="preserve">
<value>自動重新整理</value>
@@ -922,10 +922,10 @@
<value>跳過測試</value>
</data>
<data name="menuEditServer" xml:space="preserve">
<value>編輯設定檔 (Ctrl+D)</value>
<value>編輯 (Ctrl+D)</value>
</data>
<data name="TbSettingsDoubleClick2Activate" xml:space="preserve">
<value>主介面輕按兩下設為活動設定檔</value>
<value>主介面輕按兩下設為活動</value>
</data>
<data name="SpeedtestingCompleted" xml:space="preserve">
<value>測試完成</value>
@@ -943,7 +943,7 @@
<value>目前字型 (需重啟)</value>
</data>
<data name="TbSettingsCurrentFontFamilyTip" xml:space="preserve">
<value>複製字型 TTF/TTC 檔案到目錄 guiFonts啟設定</value>
<value>複製字型 TTF/TTC 檔案到目錄 guiFonts新啟動後生效</value>
</data>
<data name="TbSettingsSocksPortTip" xml:space="preserve">
<value>Pac 連接埠 = +3Xray API 連接埠 = +4mihomo API 連接埠 = +5</value>
@@ -1003,10 +1003,10 @@
<value>不需要轉換時請留空</value>
</data>
<data name="menuDNSSetting" xml:space="preserve">
<value>DNS 設定</value>
<value>DNS設定</value>
</data>
<data name="TbCustomDnsSingbox" xml:space="preserve">
<value>sing-box Custom DNS</value>
<value>sing-box 自訂 DNS</value>
</data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>請填寫 DNS JSON 結構,點擊查看檔案</value>
@@ -1030,7 +1030,7 @@
<value>Domain</value>
</data>
<data name="menuAddHysteria2Server" xml:space="preserve">
<value>添加 [Hysteria2] 設定檔</value>
<value>新增 [Hysteria2] 節點</value>
</data>
<data name="TbSettingsHysteriaBandwidth" xml:space="preserve">
<value>Hysteria 最大頻寬 (Up/Dw)</value>
@@ -1039,19 +1039,19 @@
<value>使用系統 hosts</value>
</data>
<data name="menuAddTuicServer" xml:space="preserve">
<value>新增 [TUIC] 設定檔</value>
<value>新增 [TUIC] 節點</value>
</data>
<data name="TbHeaderType8" xml:space="preserve">
<value>擁塞控制算法</value>
</data>
<data name="LvPrevProfile" xml:space="preserve">
<value>前置代理設定檔別名</value>
<value>前置代理節點別名</value>
</data>
<data name="LvNextProfile" xml:space="preserve">
<value>落地代理設定檔別名</value>
<value>落地代理節點別名</value>
</data>
<data name="LvPrevProfileTip" xml:space="preserve">
<value>請確保設定檔別名存在並且唯一</value>
<value>請確保節點別名存在並且唯一</value>
</data>
<data name="TbSettingsTunAutoRoute" xml:space="preserve">
<value>自動路由</value>
@@ -1072,7 +1072,7 @@
<value>啟用 IPv6</value>
</data>
<data name="menuAddWireguardServer" xml:space="preserve">
<value>添加 [WireGuard] 設定檔</value>
<value>新增 [WireGuard] 節點</value>
</data>
<data name="TbPrivateKey" xml:space="preserve">
<value>PrivateKey</value>
@@ -1105,7 +1105,7 @@
<value>*grpc Authority</value>
</data>
<data name="menuAddHttpServer" xml:space="preserve">
<value>新增 [HTTP] 設定檔</value>
<value>新增 [HTTP] 節點</value>
</data>
<data name="TbSettingsEnableFragmentTips" xml:space="preserve">
<value>和分組前置代理衝突</value>
@@ -1219,13 +1219,13 @@
<value>匯出分享連結至剪貼簿 (多選) Base64 編碼</value>
</data>
<data name="menuExport2ClientConfigClipboard" xml:space="preserve">
<value>匯出所選設定檔完整設定至剪貼簿</value>
<value>匯出所選完整設定至剪貼簿</value>
</data>
<data name="menuShowOrHideMainWindow" xml:space="preserve">
<value>顯示或隱藏主介面</value>
</data>
<data name="TbPreSocksPort4Sub" xml:space="preserve">
<value>自訂設定的 Socks 連接埠</value>
<value>自訂 Socks 連接埠</value>
</data>
<data name="menuBackupAndRestore" xml:space="preserve">
<value>備份和還原</value>
@@ -1309,7 +1309,7 @@
<value>請不要使用不安全的 HTTP 協定訂閱位址</value>
</data>
<data name="TbSettingsCurrentFontFamilyLinuxTip" xml:space="preserve">
<value>安裝字體到系統中,選擇或填入字體名稱,重新啟動設定</value>
<value>安裝字體到系統中,選擇或填入字體名稱,重新啟動後生效</value>
</data>
<data name="menuExitTips" xml:space="preserve">
<value>是否確定退出?</value>
@@ -1336,7 +1336,7 @@
<value>多執行緒測試時的並發數量</value>
</data>
<data name="TbSettingsExceptionTip2" xml:space="preserve">
<value>例外:對於下列位址不使用代理設定檔,使用逗號 (,) 分隔。</value>
<value>例外:對於下列位址不使用代理,使用逗號 (,) 分隔。</value>
</data>
<data name="TbSettingsDestOverride" xml:space="preserve">
<value>流量探測類型</value>
@@ -1372,31 +1372,31 @@
<value>會覆蓋埠,多組時用逗號 (,) 隔開</value>
</data>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Generate Policy Group from Multiple Profiles</value>
<value>多選生成策略組</value>
</data>
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>多設定檔隨機 Xray</value>
<value>多隨機 Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>多設定檔負載平衡 Xray</value>
<value>多負載平衡 Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>多設定檔最低延遲 Xray</value>
<value>多最低延遲 Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>多設定檔最穩定 Xray</value>
<value>多最穩定 Xray</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>多設定檔最低延遲 sing-box</value>
<value>多最低延遲 sing-box</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
<value>匯出設定檔</value>
<value>匯出</value>
</data>
<data name="TbSettingsIPAPIUrl" xml:space="preserve">
<value>目前連接資訊測試地址</value>
</data>
<data name="TbRuleOutboundTagTip" xml:space="preserve">
<value>可以填寫設定檔別名,請確保存在並唯一</value>
<value>可以填寫節點別名,請確保存在並唯一</value>
</data>
<data name="SudoIncorrectPasswordTip" xml:space="preserve">
<value>密碼錯誤,請重試。</value>
@@ -1405,192 +1405,231 @@
<value>Mldsa65Verify</value>
</data>
<data name="menuAddAnytlsServer" xml:space="preserve">
<value>新增 [Anytls] 設定檔</value>
<value>新增 [Anytls] 節點</value>
</data>
<data name="TbRemoteDNS" xml:space="preserve">
<value>Remote DNS</value>
<value>遠程 DNS</value>
</data>
<data name="TbDomesticDNS" xml:space="preserve">
<value>Domestic DNS</value>
<value>直連 DNS</value>
</data>
<data name="TbRemoteDNSTips" xml:space="preserve">
<value>Via proxy — please ensure remote availability</value>
<value>通过代理,请确保远程可用</value>
</data>
<data name="TbXrayFreedomStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
<value>xray freedom 解析策略</value>
</data>
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
<value>sing-box Direct Resolution Strategy</value>
<value>sing-box 直連解析策略</value>
</data>
<data name="TbSBRemoteResolveStrategy" xml:space="preserve">
<value>sing-box Remote Resolution Strategy</value>
<value>sing-box 遠程解析策略</value>
</data>
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
<value>新增常用 DNS Hosts</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>
<data name="TbBlockSVCBHTTPSQueries" xml:space="preserve">
<value>Block SVCB and HTTPS Queries</value>
<value>阻止 SVCB HTTPS 查詢</value>
</data>
<data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
<value>DNS Hosts“網域名稱1 ip1 ip2” 一行一個)</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value>
<value>DNS 基礎設定</value>
</data>
<data name="ThAdvancedDNSSettings" xml:space="preserve">
<value>Advanced DNS Settings</value>
<value>DNS 進階設定</value>
</data>
<data name="TbValidateDirectExpectedIPs" xml:space="preserve">
<value>Validate Regional Domain IPs</value>
<value>校驗相應地區域名 IP</value>
</data>
<data name="TbValidateDirectExpectedIPsDesc" xml:space="preserve">
<value>When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs</value>
<value>配置後,會對相應地區域名(如 geosite:cn的返回 IP 進行校驗,僅返回期望 IP</value>
</data>
<data name="TbCustomDNSEnable" xml:space="preserve">
<value>Enable Custom DNS</value>
<value>啟用自訂 DNS</value>
</data>
<data name="TbCustomDNSEnabledPageInvalid" xml:space="preserve">
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
<value>自訂 DNS 已啟用,此頁面配置將無效</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Block ECH and HTTP/3 availability checks when enabled</value>
<value>開啟後將阻止 ECH HTTP/3 可用性查詢</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value>
<value>請填寫正確的配置範本</value>
</data>
<data name="menuFullConfigTemplate" xml:space="preserve">
<value>Full Config Template Setting</value>
<value>完整配置範本設定</value>
</data>
<data name="TbFullConfigTemplateEnable" xml:space="preserve">
<value>Enable Full Config Template</value>
<value>啟用完整配置範本</value>
</data>
<data name="TbRayFullConfigTemplate" xml:space="preserve">
<value>v2ray Full Config Template</value>
<value>v2ray 完整配置範本</value>
</data>
<data name="TbRayFullConfigTemplateDesc" xml:space="preserve">
<value>Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document</value>
<value>僅添加出站配置,routing.balancers routing.rules.outboundTag,點擊查看文檔</value>
</data>
<data name="TbAddProxyProtocolOutboundOnly" xml:space="preserve">
<value>Do Not Add Non-Proxy Protocol Outbound</value>
<value>不添加非代理協定出站</value>
</data>
<data name="TbSetUpstreamProxyDetour" xml:space="preserve">
<value>Set Upstream Proxy Tag</value>
<value>設定上游代理 tag</value>
</data>
<data name="TbSBFullConfigTemplate" xml:space="preserve">
<value>sing-box Full Config Template</value>
<value>sing-box 完整配置範本</value>
</data>
<data name="TbSBFullConfigTemplateDesc" xml:space="preserve">
<value>Add Outbound and Endpoint Config Only, Click to view the document</value>
<value>僅添加出站和端點配置,點擊查看文檔</value>
</data>
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
<value>此功能供高級用戶和有特殊需求的用戶使用。 啟用此功能後,將忽略 Core 的基礎設定DNS 設定 ,路由設定。你需要保證系統代理的埠和流量統計等功能的配置正確,一切都由你來設定。</value>
</data>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>開始解析和處理訂閱內容</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
<value>選擇節點</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
<value>默認全域生效,內置 FakeIP 過濾,僅在 sing-box 中生效</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
<value>請至少添加一個節點</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
<value>策略組</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
<value>鏈式代理</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
<value>最低延遲</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
<value>隨機</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
<value>負載均衡</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
<value>最穩定</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
<value>策略組類型</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
<value>添加策略組</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
<value>添加鏈式代理</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
<value>添加子配置</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
<value>刪除子配置</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
<value>子配置項</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
<value>容錯移轉</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
<value>多選容錯移轉 sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
<value>多選容錯移轉 Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value>
<value>核心 '{0}' 不支援網路類型 '{1}'.</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
<value>核心 '{0}' 在使用傳輸方式 '{2}' 時不支援協定 '{1}'.</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value>
<value>核心 '{0}' 不支援協定 '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Proxy chained: </value>
<value>代理鏈: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Routing rule outbound: </value>
<value>路由規則出站: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
<value>策略組: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
<value>別名 '{0}' 不存在。</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
<value>組“{0}”為空.請至少添加一個配置。</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
<value>{0}屬性無效,請檢查</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} 分組不能引用自身或循環引用</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
<value>不支援協定 '{0}'.</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>如果系統沒有盤功能,請不要開啟</value>
<value>如果系統沒有盤功能,請不要開啟</value>
</data>
<data name="TbRuleType" xml:space="preserve">
<value>规则类型</value>
<value>規則類型</value>
</data>
<data name="TbRuleTypeTips" xml:space="preserve">
<value>可 Routing 和 DNS 单独设定规则ALL 都生效</value>
<value>可 Routing 和 DNS 單獨設定規則ALL 都生效</value>
</data>
<data name="TbBootstrapDNS" xml:space="preserve">
<value>Bootstrap DNS</value>
</data>
<data name="TbBootstrapDNSTips" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
<value>解析 DNS 伺服器網域名稱,需指定為 IP</value>
</data>
<data name="menuFastRealPing" xml:space="preserve">
<value>一鍵測試真連線延遲</value>
</data>
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
<value>自動從訂閱分組新增過濾後的配置</value>
</data>
<data name="TbCertPinning" xml:space="preserve">
<value>Certificate Pinning</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Server Certificate (PEM format, optional)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data>
<data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value>
</data>
<data name="TbFetchCertChain" xml:space="preserve">
<value>Fetch Certificate Chain</value>
</data>
<data name="ServerNameMustBeValidDomain" xml:space="preserve">
<value>Please set a valid domain</value>
</data>
<data name="CertNotSet" xml:space="preserve">
<value>Certificate not set</value>
</data>
<data name="CertSet" xml:space="preserve">
<value>Certificate set</value>
</data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>自訂 PAC 檔案路徑</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>自訂系統代理程式腳本檔案路徑</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS 在 Dock 欄顯示 (需重啟)</value>
</data>
</root>

View File

@@ -28,15 +28,15 @@ fi
kill_children() {
local parent=$1
local children=$(ps -o pid --no-headers --ppid "$parent")
# Output information about processes being terminated
echo "Processing children of PID: $parent..."
# Process each child
for child in $children; do
# Recursively find and kill child's children first
kill_children "$child"
# Force kill the child process
echo "Terminating child process: $child"
kill -9 "$child" 2>/dev/null || true
@@ -47,6 +47,18 @@ echo "============================================"
echo "Starting termination of process $PID and all its children"
echo "============================================"
# Try graceful termination first
echo "Attempting graceful termination (SIGTERM) of PID: $PID"
kill -15 "$PID" 2>/dev/null || true
sleep 1
# If still running, fall back to kill_children
if ps -p $PID > /dev/null; then
echo "Process $PID did not exit after SIGTERM; proceeding with forced termination of its children and itself"
else
echo "Process $PID exited cleanly after SIGTERM"
exit 0
fi
# Find and kill all child processes
kill_children "$PID"

View File

@@ -42,6 +42,20 @@ echo "============================================"
echo "Starting termination of process $PID and all its descendants"
echo "============================================"
# Try graceful termination first
echo "Attempting graceful termination (SIGTERM) of PID: $PID"
kill -15 "$PID" 2>/dev/null || true
sleep 1
# If still running, fall back to kill_descendants
# Use the macOS-native 'kill -0' check
if kill -0 $PID 2>/dev/null; then
echo "Process $PID did not exit after SIGTERM; proceeding with forced termination of its descendants and itself"
else
echo "Process $PID exited cleanly after SIGTERM"
exit 0
fi
# Find and kill all descendant processes
kill_descendants "$PID"

View File

@@ -57,6 +57,9 @@
<SubType>Designer</SubType>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.fr.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.hu.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
@@ -79,4 +82,4 @@
</EmbeddedResource>
</ItemGroup>
</Project>
</Project>

View File

@@ -202,7 +202,9 @@ public partial class CoreConfigSingboxService
var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing == null)
{
return 0;
}
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
var expectedIPCidr = new List<string>();
@@ -470,14 +472,16 @@ public partial class CoreConfigSingboxService
{
// udp dns
server.type = "udp";
server.server = addressFirst;
return server;
}
else
{
// server.type = scheme.ToLower();
// remove "+local" suffix
// TODO: "+local" suffix decide server.detour = "direct" ?
server.type = scheme.Replace("+local", "", StringComparison.OrdinalIgnoreCase).ToLower();
}
//server.type = scheme.ToLower();
// remove "+local" suffix
// TODO: "+local" suffix decide server.detour = "direct" ?
server.type = scheme.Replace("+local", "", StringComparison.OrdinalIgnoreCase).ToLower();
server.server = domain;
if (port != 0)
{

View File

@@ -61,7 +61,7 @@ public partial class CoreConfigSingboxService
}
var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { };
tunInbound.interface_name = Utils.IsOSX() ? $"utun{new Random().Next(99)}" : "singbox_tun";
tunInbound.interface_name = Utils.IsMacOS() ? $"utun{new Random().Next(99)}" : "singbox_tun";
tunInbound.mtu = _config.TunModeItem.Mtu;
tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute;

View File

@@ -204,54 +204,6 @@ public partial class CoreConfigSingboxService
return await Task.FromResult<BaseServer4Sbox?>(null);
}
private async Task<int> GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false)
{
try
{
if (!node.ConfigType.IsGroupType())
{
return -1;
}
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
if (hasCycle)
{
return -1;
}
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
return -1;
}
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
if (ignoreOriginChain)
{
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
}
else
{
await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
}
break;
case EConfigType.ProxyChain:
await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName);
break;
default:
break;
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private async Task<int> GenOutboundMux(ProfileItem node, Outbound4Sbox outbound)
{
try
@@ -280,7 +232,7 @@ public partial class CoreConfigSingboxService
{
try
{
if (node.StreamSecurity == Global.StreamSecurityReality || node.StreamSecurity == Global.StreamSecurity)
if (node.StreamSecurity is Global.StreamSecurityReality or Global.StreamSecurity)
{
var server_name = string.Empty;
if (node.Sni.IsNotEmpty())
@@ -294,7 +246,7 @@ public partial class CoreConfigSingboxService
var tls = new Tls4Sbox()
{
enabled = true,
record_fragment = _config.CoreBasicItem.EnableFragment,
record_fragment = _config.CoreBasicItem.EnableFragment ? true : null,
server_name = server_name,
insecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure),
alpn = node.GetAlpn(),
@@ -307,7 +259,16 @@ public partial class CoreConfigSingboxService
fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint
};
}
if (node.StreamSecurity == Global.StreamSecurityReality)
if (node.StreamSecurity == Global.StreamSecurity)
{
var certs = CertPemManager.ParsePemChain(node.Cert);
if (certs.Count > 0)
{
tls.certificate = certs;
tls.insecure = false;
}
}
else if (node.StreamSecurity == Global.StreamSecurityReality)
{
tls.reality = new Reality4Sbox()
{
@@ -404,6 +365,54 @@ public partial class CoreConfigSingboxService
return await Task.FromResult(0);
}
private async Task<int> GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false)
{
try
{
if (!node.ConfigType.IsGroupType())
{
return -1;
}
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
if (hasCycle)
{
return -1;
}
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
return -1;
}
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
if (ignoreOriginChain)
{
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
}
else
{
await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
}
break;
case EConfigType.ProxyChain:
await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName);
break;
default:
break;
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private async Task<int> GenMoreOutbounds(ProfileItem node, SingboxConfig singboxConfig)
{
if (node.Subid.IsNullOrEmpty())
@@ -668,7 +677,10 @@ public partial class CoreConfigSingboxService
{
var node = nodes[i];
if (node == null)
{
continue;
}
if (node.ConfigType.IsGroupType())
{
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);

View File

@@ -250,7 +250,9 @@ public partial class CoreConfigSingboxService
foreach (var it in item.Domain)
{
if (ParseV2Domain(it, rule1))
{
countDomain++;
}
}
if (countDomain > 0)
{
@@ -265,7 +267,9 @@ public partial class CoreConfigSingboxService
foreach (var it in item.Ip)
{
if (ParseV2Address(it, rule2))
{
countIp++;
}
}
if (countIp > 0)
{

View File

@@ -7,7 +7,9 @@ public partial class CoreConfigSingboxService
static void AddRuleSets(List<string> ruleSets, List<string>? rule_set)
{
if (rule_set != null)
{
ruleSets.AddRange(rule_set);
}
}
var geosite = "geosite";
var geoip = "geoip";

View File

@@ -94,8 +94,8 @@ public partial class CoreConfigV2rayService(Config config)
ret.Msg = ResUI.InitialConfiguration;
string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
@@ -137,7 +137,9 @@ public partial class CoreConfigV2rayService(Config config)
foreach (var rule in rules)
{
if (rule.outboundTag == null)
{
continue;
}
if (balancerTagSet.Contains(rule.outboundTag))
{
@@ -200,8 +202,8 @@ public partial class CoreConfigV2rayService(Config config)
ret.Msg = ResUI.InitialConfiguration;
string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;

View File

@@ -11,7 +11,9 @@ public partial class CoreConfigV2rayService
// Case 1: exact match already exists -> nothing to do
if (subjectSelectors.Any(baseTagName.StartsWith))
{
return await Task.FromResult(0);
}
// Case 2: prefix match exists -> reuse it and move to the first position
var matched = subjectSelectors.FirstOrDefault(s => s.StartsWith(baseTagName));

View File

@@ -86,8 +86,8 @@ public partial class CoreConfigV2rayService
}
}
// Handle outbounds - append instead of override
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
var customOutboundsNode = new JsonArray();
foreach (var outbound in v2rayConfig.outbounds)
{
if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom")
@@ -97,14 +97,30 @@ public partial class CoreConfigV2rayService
continue;
}
}
else if ((outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() == true) && (!fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) && !(Utils.IsPrivateNetwork(outbound.settings?.servers?.FirstOrDefault()?.address ?? string.Empty) || Utils.IsPrivateNetwork(outbound.settings?.vnext?.FirstOrDefault()?.address ?? string.Empty)))
else if ((!fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
&& ((outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() ?? true) == true))
{
outbound.streamSettings ??= new StreamSettings4Ray();
outbound.streamSettings.sockopt ??= new Sockopt4Ray();
outbound.streamSettings.sockopt.dialerProxy = fullConfigTemplate.ProxyDetour;
var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address
?? outbound.settings?.vnext?.FirstOrDefault()?.address
?? string.Empty;
if (!Utils.IsPrivateNetwork(outboundAddress))
{
outbound.streamSettings ??= new StreamSettings4Ray();
outbound.streamSettings.sockopt ??= new Sockopt4Ray();
outbound.streamSettings.sockopt.dialerProxy = fullConfigTemplate.ProxyDetour;
}
}
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
}
if (fullConfigTemplateNode["outbounds"] is JsonArray templateOutbounds)
{
foreach (var outbound in templateOutbounds)
{
customOutboundsNode.Add(outbound?.DeepClone());
}
}
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
return await Task.FromResult(JsonUtils.Serialize(fullConfigTemplateNode));

View File

@@ -79,9 +79,23 @@ public partial class CoreConfigV2rayService
static object CreateDnsServer(string dnsAddress, List<string> domains, List<string>? expectedIPs = null)
{
var (domain, scheme, port, path) = Utils.ParseUrl(dnsAddress);
var domainFinal = dnsAddress;
int? portFinal = null;
if (scheme.IsNullOrEmpty() || scheme.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
{
domainFinal = domain;
portFinal = port > 0 ? port : null;
}
else if (scheme.StartsWith("tcp", StringComparison.OrdinalIgnoreCase))
{
domainFinal = scheme + "://" + domain;
portFinal = port > 0 ? port : null;
}
var dnsServer = new DnsServer4Ray
{
address = dnsAddress,
address = domainFinal,
port = portFinal,
skipFallback = true,
domains = domains.Count > 0 ? domains : null,
expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null
@@ -175,7 +189,10 @@ public partial class CoreConfigV2rayService
foreach (var domain in item.Domain)
{
if (domain.StartsWith('#'))
{
continue;
}
var normalizedDomain = domain.Replace(Global.RoutingRuleComma, ",");
if (item.OutboundTag == Global.DirectTag)
@@ -333,8 +350,8 @@ public partial class CoreConfigV2rayService
if (obj is null)
{
List<string> servers = [];
string[] arrDNS = normalDNS.Split(',');
foreach (string str in arrDNS)
var arrDNS = normalDNS.Split(',');
foreach (var str in arrDNS)
{
servers.Add(str);
}
@@ -354,7 +371,10 @@ public partial class CoreConfigV2rayService
foreach (var host in systemHosts)
{
if (normalHost1[host.Key] != null)
{
continue;
}
normalHost1[host.Key] = host.Value;
}
}

View File

@@ -48,7 +48,7 @@ public partial class CoreConfigV2rayService
private Inbounds4Ray GetInbound(InItem inItem, EInboundProtocol protocol, bool bSocks)
{
string result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound);
var result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound);
if (result.IsNullOrEmpty())
{
return new();

View File

@@ -277,6 +277,23 @@ public partial class CoreConfigV2rayService
{
tlsSettings.serverName = Utils.String2List(host)?.First();
}
var certs = CertPemManager.ParsePemChain(node.Cert);
if (certs.Count > 0)
{
var certsettings = new List<CertificateSettings4Ray>();
foreach (var cert in certs)
{
var certPerLine = cert.Split("\n").ToList();
certsettings.Add(new CertificateSettings4Ray
{
certificate = certPerLine,
usage = "verify",
});
}
tlsSettings.certificates = certsettings;
tlsSettings.disableSystemRoot = true;
tlsSettings.allowInsecure = false;
}
streamSettings.tlsSettings = tlsSettings;
}
@@ -453,16 +470,16 @@ public partial class CoreConfigV2rayService
};
//request Host
string request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName);
string[] arrHost = host.Split(',');
string host2 = string.Join(",".AppendQuotes(), arrHost);
var request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName);
var arrHost = host.Split(',');
var host2 = string.Join(",".AppendQuotes(), arrHost);
request = request.Replace("$requestHost$", $"{host2.AppendQuotes()}");
request = request.Replace("$requestUserAgent$", $"{useragent.AppendQuotes()}");
//Path
string pathHttp = @"/";
var pathHttp = @"/";
if (path.IsNotEmpty())
{
string[] arrPath = path.Split(',');
var arrPath = path.Split(',');
pathHttp = string.Join(",".AppendQuotes(), arrPath);
}
request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}");
@@ -623,10 +640,10 @@ public partial class CoreConfigV2rayService
// Cache for chain proxies to avoid duplicate generation
var nextProxyCache = new Dictionary<string, Outbounds4Ray?>();
var prevProxyTags = new Dictionary<string, string?>(); // Map from profile name to tag
int prevIndex = 0; // Index for prev outbounds
var prevIndex = 0; // Index for prev outbounds
// Process nodes
int index = 0;
var index = 0;
foreach (var node in nodes)
{
index++;
@@ -781,7 +798,10 @@ public partial class CoreConfigV2rayService
{
var node = nodes[i];
if (node == null)
{
continue;
}
if (node.ConfigType.IsGroupType())
{
var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);

View File

@@ -6,7 +6,7 @@ public partial class CoreConfigV2rayService
{
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
{
string tag = EInboundProtocol.api.ToString();
var tag = EInboundProtocol.api.ToString();
Metrics4Ray apiObj = new();
Policy4Ray policyObj = new();
SystemPolicy4Ray policySystemSetting = new();

View File

@@ -71,7 +71,7 @@ public class DownloadService
AllowAutoRedirect = false,
Proxy = await GetWebProxy(blProxy)
};
HttpClient client = new(webRequestHandler);
var client = new HttpClient(webRequestHandler);
var response = await client.GetAsync(url);
if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null)
@@ -156,7 +156,7 @@ public class DownloadService
}
using var cts = new CancellationTokenSource();
var result = await HttpClientHelper.Instance.GetAsync(client, url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token);
var result = await client.GetStringAsync(url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token);
return result;
}
catch (Exception ex)

View File

@@ -21,7 +21,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
if (_lstExitLoop.Count > 0)
{
UpdateFunc("", ResUI.SpeedtestingStop);
_ = UpdateFunc("", ResUI.SpeedtestingStop);
_lstExitLoop.Clear();
}
@@ -272,7 +272,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<int> DoRealPing(ServerTestItem it)
{
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await HttpClientHelper.Instance.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10);
var responseTime = await ConnectionHandler.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());

View File

@@ -1,14 +1,14 @@
namespace ServiceLib.Services;
public class UpdateService
public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
{
private Func<bool, string, Task>? _updateFunc;
private readonly Config? _config = config;
private readonly Func<bool, string, Task>? _updateFunc = updateFunc;
private readonly int _timeout = 30;
private static readonly string _tag = "UpdateService";
public async Task CheckUpdateGuiN(Config config, Func<bool, string, Task> updateFunc, bool preRelease)
public async Task CheckUpdateGuiN(bool preRelease)
{
_updateFunc = updateFunc;
var url = string.Empty;
var fileName = string.Empty;
@@ -47,9 +47,8 @@ public class UpdateService
}
}
public async Task CheckUpdateCore(ECoreType type, Config config, Func<bool, string, Task> updateFunc, bool preRelease)
public async Task CheckUpdateCore(ECoreType type, bool preRelease)
{
_updateFunc = updateFunc;
var url = string.Empty;
var fileName = string.Empty;
@@ -101,11 +100,11 @@ public class UpdateService
}
}
public async Task UpdateGeoFileAll(Config config, Func<bool, string, Task> updateFunc)
public async Task UpdateGeoFileAll()
{
await UpdateGeoFiles(config, updateFunc);
await UpdateOtherFiles(config, updateFunc);
await UpdateSrsFileAll(config, updateFunc);
await UpdateGeoFiles();
await UpdateOtherFiles();
await UpdateSrsFileAll();
await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo"));
}
@@ -167,7 +166,7 @@ public class UpdateService
try
{
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type);
string filePath = string.Empty;
var filePath = string.Empty;
foreach (var name in coreInfo.CoreExes)
{
var vName = Utils.GetBinPath(Utils.GetExeName(name), coreInfo.CoreType.ToString());
@@ -180,14 +179,14 @@ public class UpdateService
if (!File.Exists(filePath))
{
string msg = string.Format(ResUI.NotFoundCore, @"", "", "");
var msg = string.Format(ResUI.NotFoundCore, @"", "", "");
//ShowMsg(true, msg);
return new SemanticVersion("");
}
var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg);
var echo = result ?? "";
string version = string.Empty;
var version = string.Empty;
switch (type)
{
case ECoreType.v2fly:
@@ -314,7 +313,7 @@ public class UpdateService
_ => null,
};
}
else if (Utils.IsOSX())
else if (Utils.IsMacOS())
{
return RuntimeInformation.ProcessArchitecture switch
{
@@ -330,13 +329,11 @@ public class UpdateService
#region Geo private
private async Task UpdateGeoFiles(Config config, Func<bool, string, Task> updateFunc)
private async Task UpdateGeoFiles()
{
_updateFunc = updateFunc;
var geoUrl = string.IsNullOrEmpty(config?.ConstItem.GeoSourceUrl)
var geoUrl = string.IsNullOrEmpty(_config?.ConstItem.GeoSourceUrl)
? Global.GeoUrl
: config.ConstItem.GeoSourceUrl;
: _config.ConstItem.GeoSourceUrl;
List<string> files = ["geosite", "geoip"];
foreach (var geoName in files)
@@ -345,33 +342,29 @@ public class UpdateService
var targetPath = Utils.GetBinPath($"{fileName}");
var url = string.Format(geoUrl, geoName);
await DownloadGeoFile(url, fileName, targetPath, updateFunc);
await DownloadGeoFile(url, fileName, targetPath);
}
}
private async Task UpdateOtherFiles(Config config, Func<bool, string, Task> updateFunc)
private async Task UpdateOtherFiles()
{
//If it is not in China area, no update is required
if (config.ConstItem.GeoSourceUrl.IsNotEmpty())
if (_config.ConstItem.GeoSourceUrl.IsNotEmpty())
{
return;
}
_updateFunc = updateFunc;
foreach (var url in Global.OtherGeoUrls)
{
var fileName = Path.GetFileName(url);
var targetPath = Utils.GetBinPath($"{fileName}");
await DownloadGeoFile(url, fileName, targetPath, updateFunc);
await DownloadGeoFile(url, fileName, targetPath);
}
}
private async Task UpdateSrsFileAll(Config config, Func<bool, string, Task> updateFunc)
private async Task UpdateSrsFileAll()
{
_updateFunc = updateFunc;
var geoipFiles = new List<string>();
var geoSiteFiles = new List<string>();
@@ -414,29 +407,29 @@ public class UpdateService
}
foreach (var item in geoipFiles.Distinct())
{
await UpdateSrsFile("geoip", item, config, updateFunc);
await UpdateSrsFile("geoip", item);
}
foreach (var item in geoSiteFiles.Distinct())
{
await UpdateSrsFile("geosite", item, config, updateFunc);
await UpdateSrsFile("geosite", item);
}
}
private async Task UpdateSrsFile(string type, string srsName, Config config, Func<bool, string, Task> updateFunc)
private async Task UpdateSrsFile(string type, string srsName)
{
var srsUrl = string.IsNullOrEmpty(config.ConstItem.SrsSourceUrl)
var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl)
? Global.SingboxRulesetUrl
: config.ConstItem.SrsSourceUrl;
: _config.ConstItem.SrsSourceUrl;
var fileName = $"{type}-{srsName}.srs";
var targetPath = Path.Combine(Utils.GetBinPath("srss"), fileName);
var url = string.Format(srsUrl, type, $"{type}-{srsName}", srsName);
await DownloadGeoFile(url, fileName, targetPath, updateFunc);
await DownloadGeoFile(url, fileName, targetPath);
}
private async Task DownloadGeoFile(string url, string fileName, string targetPath, Func<bool, string, Task> updateFunc)
private async Task DownloadGeoFile(string url, string fileName, string targetPath)
{
var tmpFileName = Utils.GetTempPath(Utils.GetGuid());

View File

@@ -0,0 +1,171 @@
namespace ServiceLib.Services;
/// <summary>
/// http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net
/// </summary>
public sealed class WindowsJobService : IDisposable
{
private nint handle = nint.Zero;
public WindowsJobService()
{
handle = CreateJobObject(nint.Zero, null);
var extendedInfoPtr = nint.Zero;
var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = 0x2000
};
var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = info
};
try
{
var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
extendedInfoPtr = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr,
(uint)length))
{
throw new Exception(string.Format("Unable to set information. Error: {0}",
Marshal.GetLastWin32Error()));
}
}
finally
{
if (extendedInfoPtr != nint.Zero)
{
Marshal.FreeHGlobal(extendedInfoPtr);
}
}
}
public bool AddProcess(nint processHandle)
{
var succ = AssignProcessToJobObject(handle, processHandle);
if (!succ)
{
Logging.SaveLog("Failed to call AssignProcessToJobObject! GetLastError=" + Marshal.GetLastWin32Error());
}
return succ;
}
public bool AddProcess(int processId)
{
return AddProcess(Process.GetProcessById(processId).Handle);
}
#region IDisposable
private bool disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposed)
{
return;
}
disposed = true;
if (disposing)
{
// no managed objects to free
}
if (handle != nint.Zero)
{
CloseHandle(handle);
handle = nint.Zero;
}
}
~WindowsJobService()
{
Dispose(false);
}
#endregion IDisposable
#region Interop
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern nint CreateJobObject(nint a, string? lpName);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetInformationJobObject(nint hJob, JobObjectInfoType infoType, nint lpJobObjectInfo, uint cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AssignProcessToJobObject(nint job, nint process);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(nint hObject);
#endregion Interop
}
[StructLayout(LayoutKind.Sequential)]
internal struct IO_COUNTERS
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public uint LimitFlags;
public nuint MinimumWorkingSetSize;
public nuint MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public nuint Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public uint nLength;
public nint lpSecurityDescriptor;
public int bInheritHandle;
}
[StructLayout(LayoutKind.Sequential)]
internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public nuint ProcessMemoryLimit;
public nuint JobMemoryLimit;
public nuint PeakProcessMemoryUsed;
public nuint PeakJobMemoryUsed;
}
public enum JobObjectInfoType
{
AssociateCompletionPortInformation = 7,
BasicLimitInformation = 2,
BasicUIRestrictions = 4,
EndOfJobTimeInformation = 6,
ExtendedLimitInformation = 9,
SecurityLimitInformation = 5,
GroupInformation = 11
}

View File

@@ -17,6 +17,14 @@ public class AddGroupServerViewModel : MyReactiveObject
[Reactive]
public string? PolicyGroupType { get; set; }
[Reactive]
public SubItem? SelectedSubItem { get; set; }
[Reactive]
public string? Filter { get; set; }
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
public IObservableCollection<ProfileItem> ChildItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>();
//public ReactiveCommand<Unit, Unit> AddCmd { get; }
@@ -64,10 +72,14 @@ public class AddGroupServerViewModel : MyReactiveObject
});
SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem);
CoreType = (SelectedSource?.CoreType ?? ECoreType.Xray).ToString();
ProfileGroupItemManager.Instance.TryGet(profileItem.IndexId, out var profileGroup);
_ = Init();
}
public async Task Init()
{
ProfileGroupItemManager.Instance.TryGet(SelectedSource.IndexId, out var profileGroup);
PolicyGroupType = (profileGroup?.MultipleLoad ?? EMultipleLoad.LeastPing) switch
{
EMultipleLoad.LeastPing => ResUI.TbLeastPing,
@@ -78,15 +90,16 @@ public class AddGroupServerViewModel : MyReactiveObject
_ => ResUI.TbLeastPing,
};
_ = Init();
}
var subs = await AppManager.Instance.SubItems();
subs.Add(new SubItem());
SubItems.AddRange(subs);
SelectedSubItem = SubItems.Where(s => s.Id == profileGroup?.SubChildItems).FirstOrDefault();
Filter = profileGroup?.Filter;
public async Task Init()
{
var childItemMulti = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource?.IndexId);
if (childItemMulti != null)
{
var childIndexIds = childItemMulti.ChildItems.IsNullOrEmpty() ? new List<string>() : Utils.String2List(childItemMulti.ChildItems);
var childIndexIds = Utils.String2List(childItemMulti.ChildItems) ?? [];
foreach (var item in childIndexIds)
{
var child = await AppManager.Instance.GetProfileItem(item);
@@ -181,7 +194,7 @@ public class AddGroupServerViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks);
return;
}
if (ChildItemsObs.Count == 0)
if (ChildItemsObs.Count == 0 && SelectedSubItem?.Id.IsNullOrEmpty() == true)
{
NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer);
return;
@@ -213,6 +226,9 @@ public class AddGroupServerViewModel : MyReactiveObject
_ => EMultipleLoad.LeastPing,
};
profileGroup.SubChildItems = SelectedSubItem?.Id;
profileGroup.Filter = Filter;
var hasCycle = ProfileGroupItemManager.HasCycle(profileGroup.IndexId);
if (hasCycle)
{

View File

@@ -8,6 +8,14 @@ public class AddServerViewModel : MyReactiveObject
[Reactive]
public string? CoreType { get; set; }
[Reactive]
public string Cert { get; set; }
[Reactive]
public string CertTip { get; set; }
public ReactiveCommand<Unit, Unit> FetchCertCmd { get; }
public ReactiveCommand<Unit, Unit> FetchCertChainCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public AddServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView)
@@ -15,11 +23,22 @@ public class AddServerViewModel : MyReactiveObject
_config = AppManager.Instance.Config;
_updateView = updateView;
FetchCertCmd = ReactiveCommand.CreateFromTask(async () =>
{
await FetchCert();
});
FetchCertChainCmd = ReactiveCommand.CreateFromTask(async () =>
{
await FetchCertChain();
});
SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SaveServerAsync();
});
this.WhenAnyValue(x => x.Cert)
.Subscribe(_ => UpdateCertTip());
if (profileItem.IndexId.IsNullOrEmpty())
{
profileItem.Network = Global.DefaultNetwork;
@@ -33,6 +52,7 @@ public class AddServerViewModel : MyReactiveObject
SelectedSource = JsonUtils.DeepCopy(profileItem);
}
CoreType = SelectedSource?.CoreType?.ToString();
Cert = SelectedSource?.Cert?.ToString() ?? string.Empty;
}
private async Task SaveServerAsync()
@@ -77,6 +97,7 @@ public class AddServerViewModel : MyReactiveObject
}
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
SelectedSource.Cert = Cert.IsNullOrEmpty() ? null : Cert;
if (await ConfigHandler.AddServer(_config, SelectedSource) == 0)
{
@@ -88,4 +109,72 @@ public class AddServerViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
}
}
private void UpdateCertTip(string? errorMessage = null)
{
CertTip = errorMessage.IsNullOrEmpty()
? (Cert.IsNullOrEmpty() ? ResUI.CertNotSet : ResUI.CertSet)
: errorMessage;
}
private async Task FetchCert()
{
if (SelectedSource.StreamSecurity != Global.StreamSecurity)
{
return;
}
var domain = SelectedSource.Address;
var serverName = SelectedSource.Sni;
if (serverName.IsNullOrEmpty())
{
serverName = SelectedSource.RequestHost;
}
if (serverName.IsNullOrEmpty())
{
serverName = SelectedSource.Address;
}
if (!Utils.IsDomain(serverName))
{
UpdateCertTip(ResUI.ServerNameMustBeValidDomain);
return;
}
if (SelectedSource.Port > 0)
{
domain += $":{SelectedSource.Port}";
}
string certError;
(Cert, certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName);
UpdateCertTip(certError);
}
private async Task FetchCertChain()
{
if (SelectedSource.StreamSecurity != Global.StreamSecurity)
{
return;
}
var domain = SelectedSource.Address;
var serverName = SelectedSource.Sni;
if (serverName.IsNullOrEmpty())
{
serverName = SelectedSource.RequestHost;
}
if (serverName.IsNullOrEmpty())
{
serverName = SelectedSource.Address;
}
if (!Utils.IsDomain(serverName))
{
UpdateCertTip(ResUI.ServerNameMustBeValidDomain);
return;
}
if (SelectedSource.Port > 0)
{
domain += $":{SelectedSource.Port}";
}
string certError;
(var certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName);
Cert = CertPemManager.ConcatenatePemChain(certs);
UpdateCertTip(certError);
}
}

View File

@@ -119,7 +119,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject
return;
}
//check
var lstFiles = FileManager.GetFilesFromZip(fileName);
var lstFiles = FileUtils.GetFilesFromZip(fileName);
if (lstFiles is null || !lstFiles.Any(t => t.Contains(_guiConfigs)))
{
DisplayOperationMsg(ResUI.LocalRestoreInvalidZipTips);
@@ -135,7 +135,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject
await SQLiteHelper.Instance.DisposeDbConnectionAsync();
var toPath = Utils.GetConfigPath();
FileManager.ZipExtractToFile(fileName, toPath, "");
FileUtils.ZipExtractToFile(fileName, toPath, "");
if (Utils.IsWindows())
{
@@ -167,8 +167,8 @@ public class BackupAndRestoreViewModel : MyReactiveObject
var configDirZipTemp = Utils.GetTempPath($"v2rayN_{DateTime.Now:yyyyMMddHHmmss}");
var configDirTemp = Path.Combine(configDirZipTemp, _guiConfigs);
FileManager.CopyDirectory(configDir, configDirTemp, false, true, "");
var ret = FileManager.CreateFromDirectory(configDirZipTemp, fileName);
FileUtils.CopyDirectory(configDir, configDirTemp, false, true, "");
var ret = FileUtils.CreateFromDirectory(configDirZipTemp, fileName);
Directory.Delete(configDirZipTemp, true);
return await Task.FromResult(ret);
}

View File

@@ -148,7 +148,7 @@ public class CheckUpdateViewModel : MyReactiveObject
UpdatedPlusPlus(_geo, "");
}
}
await new UpdateService().UpdateGeoFileAll(_config, _updateUI)
await new UpdateService(_config, _updateUI).UpdateGeoFileAll()
.ContinueWith(t => UpdatedPlusPlus(_geo, ""));
}
@@ -163,7 +163,7 @@ public class CheckUpdateViewModel : MyReactiveObject
UpdatedPlusPlus(_v2rayN, msg);
}
}
await new UpdateService().CheckUpdateGuiN(_config, _updateUI, preRelease)
await new UpdateService(_config, _updateUI).CheckUpdateGuiN(preRelease)
.ContinueWith(t => UpdatedPlusPlus(_v2rayN, ""));
}
@@ -180,7 +180,7 @@ public class CheckUpdateViewModel : MyReactiveObject
}
}
var type = (ECoreType)Enum.Parse(typeof(ECoreType), model.CoreType);
await new UpdateService().CheckUpdateCore(type, _config, _updateUI, preRelease)
await new UpdateService(_config, _updateUI).CheckUpdateCore(type, preRelease)
.ContinueWith(t => UpdatedPlusPlus(model.CoreType, ""));
}
@@ -209,6 +209,7 @@ public class CheckUpdateViewModel : MyReactiveObject
_ = UpdateFinishedResult(blReload);
return Disposable.Empty;
});
await Task.CompletedTask;
}
public async Task UpdateFinishedResult(bool blReload)
@@ -270,24 +271,24 @@ public class CheckUpdateViewModel : MyReactiveObject
if (fileName.Contains(".tar.gz"))
{
FileManager.DecompressTarFile(fileName, toPath);
FileUtils.DecompressTarFile(fileName, toPath);
var dir = new DirectoryInfo(toPath);
if (dir.Exists)
{
foreach (var subDir in dir.GetDirectories())
{
FileManager.CopyDirectory(subDir.FullName, toPath, false, true);
FileUtils.CopyDirectory(subDir.FullName, toPath, false, true);
subDir.Delete(true);
}
}
}
else if (fileName.Contains(".gz"))
{
FileManager.DecompressFile(fileName, toPath, item.CoreType);
FileUtils.DecompressFile(fileName, toPath, item.CoreType);
}
else
{
FileManager.ZipExtractToFile(fileName, toPath, "geo");
FileUtils.ZipExtractToFile(fileName, toPath, "geo");
}
if (Utils.IsNonWindows())
@@ -321,6 +322,7 @@ public class CheckUpdateViewModel : MyReactiveObject
_ = UpdateViewResult(model);
return Disposable.Empty;
});
await Task.CompletedTask;
}
public async Task UpdateViewResult(CheckUpdateModel model)
@@ -331,5 +333,6 @@ public class CheckUpdateViewModel : MyReactiveObject
return;
}
found.Remarks = model.Remarks;
await Task.CompletedTask;
}
}

View File

@@ -96,6 +96,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
}
ConnectionItems.AddRange(lstModel);
await Task.CompletedTask;
}
public async Task ClashConnectionClose(bool all)

View File

@@ -211,7 +211,7 @@ public class ClashProxiesViewModel : MyReactiveObject
}
//from api
foreach (KeyValuePair<string, ProxiesItem> kv in _proxies)
foreach (var kv in _proxies)
{
if (!Global.allowSelectType.Contains(kv.Value.type.ToLower()))
{
@@ -245,6 +245,7 @@ public class ClashProxiesViewModel : MyReactiveObject
{
SelectedGroup = new();
}
await Task.CompletedTask;
}
private void RefreshProxyDetails(bool c)
@@ -319,7 +320,7 @@ public class ClashProxiesViewModel : MyReactiveObject
//from providers
if (_providers != null)
{
foreach (KeyValuePair<string, ProvidersItem> kv in _providers)
foreach (var kv in _providers)
{
if (Global.proxyVehicleType.Contains(kv.Value.vehicleType.ToLower()))
{
@@ -391,6 +392,7 @@ public class ClashProxiesViewModel : MyReactiveObject
_ = ProxiesDelayTestResult(model);
return Disposable.Empty;
});
await Task.CompletedTask;
});
await Task.CompletedTask;
}
@@ -419,6 +421,7 @@ public class ClashProxiesViewModel : MyReactiveObject
detail.Delay = _delayTimeout;
detail.DelayName = string.Empty;
}
await Task.CompletedTask;
}
#endregion proxy function

View File

@@ -66,10 +66,14 @@ public class FullConfigTemplateViewModel : MyReactiveObject
private async Task SaveSettingAsync()
{
if (!await SaveXrayConfigAsync())
{
return;
}
if (!await SaveSingboxConfigAsync())
{
return;
}
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_ = _updateView?.Invoke(EViewAction.CloseWindow, null);

View File

@@ -62,9 +62,9 @@ public class MainWindowViewModel : MyReactiveObject
[Reactive]
public int TabMainSelectedIndex { get; set; }
#endregion Menu
[Reactive] public bool BlIsWindows { get; set; }
private bool _hasNextReloadJob = false;
#endregion Menu
#region Init
@@ -72,61 +72,62 @@ public class MainWindowViewModel : MyReactiveObject
{
_config = AppManager.Instance.Config;
_updateView = updateView;
BlIsWindows = Utils.IsWindows();
#region WhenAnyValue && ReactiveCommand
//servers
AddVmessServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.VMess);
await AddServerAsync(EConfigType.VMess);
});
AddVlessServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.VLESS);
await AddServerAsync(EConfigType.VLESS);
});
AddShadowsocksServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.Shadowsocks);
await AddServerAsync(EConfigType.Shadowsocks);
});
AddSocksServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.SOCKS);
await AddServerAsync(EConfigType.SOCKS);
});
AddHttpServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.HTTP);
await AddServerAsync(EConfigType.HTTP);
});
AddTrojanServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.Trojan);
await AddServerAsync(EConfigType.Trojan);
});
AddHysteria2ServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.Hysteria2);
await AddServerAsync(EConfigType.Hysteria2);
});
AddTuicServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.TUIC);
await AddServerAsync(EConfigType.TUIC);
});
AddWireguardServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.WireGuard);
await AddServerAsync(EConfigType.WireGuard);
});
AddAnytlsServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.Anytls);
await AddServerAsync(EConfigType.Anytls);
});
AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.Custom);
await AddServerAsync(EConfigType.Custom);
});
AddPolicyGroupServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.PolicyGroup);
await AddServerAsync(EConfigType.PolicyGroup);
});
AddProxyChainServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.ProxyChain);
await AddServerAsync(EConfigType.ProxyChain);
});
AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
{
@@ -268,7 +269,6 @@ public class MainWindowViewModel : MyReactiveObject
}
await RefreshServers();
BlReloadEnabled = true;
await Reload();
}
@@ -283,6 +283,7 @@ public class MainWindowViewModel : MyReactiveObject
{
NoticeManager.Instance.Enqueue(msg);
}
await Task.CompletedTask;
}
private async Task UpdateTaskHandler(bool success, string msg)
@@ -310,6 +311,7 @@ public class MainWindowViewModel : MyReactiveObject
return;
}
AppEvents.DispatcherStatisticsRequested.Publish(update);
await Task.CompletedTask;
}
#endregion Actions
@@ -332,7 +334,7 @@ public class MainWindowViewModel : MyReactiveObject
#region Add Servers
public async Task AddServerAsync(bool blNew, EConfigType eConfigType)
public async Task AddServerAsync(EConfigType eConfigType)
{
ProfileItem item = new()
{
@@ -512,7 +514,7 @@ public class MainWindowViewModel : MyReactiveObject
{
ProcUtils.ProcessStart("xdg-open", path);
}
else if (Utils.IsOSX())
else if (Utils.IsMacOS())
{
ProcUtils.ProcessStart("open", path);
}
@@ -523,58 +525,74 @@ public class MainWindowViewModel : MyReactiveObject
#region core job
private bool _hasNextReloadJob = false;
private readonly SemaphoreSlim _reloadSemaphore = new(1, 1);
public async Task Reload()
{
//If there are unfinished reload job, marked with next job.
if (!BlReloadEnabled)
if (!await _reloadSemaphore.WaitAsync(0))
{
_hasNextReloadJob = true;
return;
}
BlReloadEnabled = false;
var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId);
if (msgs.Count > 0)
try
{
foreach (var msg in msgs)
SetReloadEnabled(false);
var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId);
if (msgs.Count > 0)
{
NoticeManager.Instance.SendMessage(msg);
foreach (var msg in msgs)
{
NoticeManager.Instance.SendMessage(msg);
}
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
return;
}
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
BlReloadEnabled = true;
return;
await Task.Run(async () =>
{
await LoadCore();
await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000);
});
AppEvents.TestServerRequested.Publish();
var showClashUI = _config.IsRunningCore(ECoreType.sing_box);
if (showClashUI)
{
AppEvents.ProxiesReloadRequested.Publish();
}
ReloadResult(showClashUI);
}
await Task.Run(async () =>
finally
{
await LoadCore();
await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000);
});
AppEvents.TestServerRequested.Publish();
var showClashUI = _config.IsRunningCore(ECoreType.sing_box);
if (showClashUI)
{
AppEvents.ProxiesReloadRequested.Publish();
}
RxApp.MainThreadScheduler.Schedule(() => ReloadResult(showClashUI));
BlReloadEnabled = true;
if (_hasNextReloadJob)
{
_hasNextReloadJob = false;
await Reload();
SetReloadEnabled(true);
_reloadSemaphore.Release();
//If there is a next reload job, execute it.
if (_hasNextReloadJob)
{
_hasNextReloadJob = false;
await Reload();
}
}
}
private void ReloadResult(bool showClashUI)
{
// BlReloadEnabled = true;
ShowClashUI = showClashUI;
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0;
RxApp.MainThreadScheduler.Schedule(() =>
{
ShowClashUI = showClashUI;
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0;
});
}
private void SetReloadEnabled(bool enabled)
{
RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled);
}
private async Task LoadCore()
@@ -594,7 +612,7 @@ public class MainWindowViewModel : MyReactiveObject
AppEvents.RoutingsMenuRefreshRequested.Publish();
await ConfigHandler.SaveConfig(_config);
await new UpdateService().UpdateGeoFileAll(_config, UpdateTaskHandler);
await new UpdateService(_config, UpdateTaskHandler).UpdateGeoFileAll();
await Reload();
}

View File

@@ -50,6 +50,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public bool EnableUpdateSubOnlyRemarksExist { get; set; }
[Reactive] public bool AutoHideStartup { get; set; }
[Reactive] public bool Hide2TrayWhenClose { get; set; }
[Reactive] public bool MacOSShowInDock { get; set; }
[Reactive] public bool EnableDragDropSort { get; set; }
[Reactive] public bool DoubleClick2Activate { get; set; }
[Reactive] public int AutoUpdateInterval { get; set; }
@@ -69,11 +70,22 @@ public class OptionSettingViewModel : MyReactiveObject
#endregion UI
#region UI visibility
[Reactive] public bool BlIsWindows { get; set; }
[Reactive] public bool BlIsLinux { get; set; }
[Reactive] public bool BlIsIsMacOS { get; set; }
[Reactive] public bool BlIsNonWindows { get; set; }
#endregion UI visibility
#region System proxy
[Reactive] public bool notProxyLocalAddress { get; set; }
[Reactive] public string systemProxyAdvancedProtocol { get; set; }
[Reactive] public string systemProxyExceptions { get; set; }
[Reactive] public string CustomSystemProxyPacPath { get; set; }
[Reactive] public string CustomSystemProxyScriptPath { get; set; }
#endregion System proxy
@@ -106,6 +118,10 @@ public class OptionSettingViewModel : MyReactiveObject
{
_config = AppManager.Instance.Config;
_updateView = updateView;
BlIsWindows = Utils.IsWindows();
BlIsLinux = Utils.IsLinux();
BlIsIsMacOS = Utils.IsMacOS();
BlIsNonWindows = Utils.IsNonWindows();
SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{
@@ -167,6 +183,7 @@ public class OptionSettingViewModel : MyReactiveObject
EnableUpdateSubOnlyRemarksExist = _config.UiItem.EnableUpdateSubOnlyRemarksExist;
AutoHideStartup = _config.UiItem.AutoHideStartup;
Hide2TrayWhenClose = _config.UiItem.Hide2TrayWhenClose;
MacOSShowInDock = _config.UiItem.MacOSShowInDock;
EnableDragDropSort = _config.UiItem.EnableDragDropSort;
DoubleClick2Activate = _config.UiItem.DoubleClick2Activate;
AutoUpdateInterval = _config.GuiItem.AutoUpdateInterval;
@@ -191,6 +208,8 @@ public class OptionSettingViewModel : MyReactiveObject
notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress;
systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol;
systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions;
CustomSystemProxyPacPath = _config.SystemProxyItem.CustomSystemProxyPacPath;
CustomSystemProxyScriptPath = _config.SystemProxyItem.CustomSystemProxyScriptPath;
#endregion System proxy
@@ -273,12 +292,12 @@ public class OptionSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort);
return;
}
var needReboot = (EnableStatistics != _config.GuiItem.EnableStatistics
var needReboot = EnableStatistics != _config.GuiItem.EnableStatistics
|| DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed
|| EnableDragDropSort != _config.UiItem.EnableDragDropSort
|| EnableHWA != _config.GuiItem.EnableHWA
|| CurrentFontFamily != _config.UiItem.CurrentFontFamily
|| MainGirdOrientation != (int)_config.UiItem.MainGirdOrientation);
|| MainGirdOrientation != (int)_config.UiItem.MainGirdOrientation;
//if (Utile.IsNullOrEmpty(Kcpmtu.ToString()) || !Utile.IsNumeric(Kcpmtu.ToString())
// || Utile.IsNullOrEmpty(Kcptti.ToString()) || !Utile.IsNumeric(Kcptti.ToString())
@@ -326,6 +345,7 @@ public class OptionSettingViewModel : MyReactiveObject
_config.UiItem.EnableUpdateSubOnlyRemarksExist = EnableUpdateSubOnlyRemarksExist;
_config.UiItem.AutoHideStartup = AutoHideStartup;
_config.UiItem.Hide2TrayWhenClose = Hide2TrayWhenClose;
_config.UiItem.MacOSShowInDock = MacOSShowInDock;
_config.GuiItem.AutoUpdateInterval = AutoUpdateInterval;
_config.UiItem.EnableDragDropSort = EnableDragDropSort;
_config.UiItem.DoubleClick2Activate = DoubleClick2Activate;
@@ -347,6 +367,8 @@ public class OptionSettingViewModel : MyReactiveObject
_config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions;
_config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress;
_config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol;
_config.SystemProxyItem.CustomSystemProxyPacPath = CustomSystemProxyPacPath;
_config.SystemProxyItem.CustomSystemProxyScriptPath = CustomSystemProxyScriptPath;
//tun mode
_config.TunModeItem.AutoRoute = TunAutoRoute;
@@ -375,7 +397,7 @@ public class OptionSettingViewModel : MyReactiveObject
private async Task SaveCoreType()
{
for (int k = 1; k <= _config.CoreTypeItem.Count; k++)
for (var k = 1; k <= _config.CoreTypeItem.Count; k++)
{
var item = _config.CoreTypeItem[k - 1];
var type = string.Empty;

View File

@@ -66,6 +66,7 @@ public class ProfilesViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SpeedServerCmd { get; }
public ReactiveCommand<Unit, Unit> SortServerResultCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveInvalidServerResultCmd { get; }
public ReactiveCommand<Unit, Unit> FastRealPingCmd { get; }
//servers export
public ReactiveCommand<Unit, Unit> Export2ClientConfigCmd { get; }
@@ -109,7 +110,7 @@ public class ProfilesViewModel : MyReactiveObject
//servers delete
EditServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await EditServerAsync(EConfigType.Custom);
await EditServerAsync();
}, canEditRemove);
RemoveServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
@@ -179,6 +180,10 @@ public class ProfilesViewModel : MyReactiveObject
}, canEditRemove);
//servers ping
FastRealPingCmd = ReactiveCommand.CreateFromTask(async () =>
{
await ServerSpeedtest(ESpeedActionType.FastRealping);
});
MixedTestServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await ServerSpeedtest(ESpeedActionType.Mixedtest);
@@ -295,14 +300,14 @@ public class ProfilesViewModel : MyReactiveObject
if (result.Delay.IsNotEmpty())
{
int.TryParse(result.Delay, out var temp);
item.Delay = temp;
item.Delay = result.Delay.ToInt();
item.DelayVal = result.Delay ?? string.Empty;
}
if (result.Speed.IsNotEmpty())
{
item.SpeedVal = result.Speed ?? string.Empty;
}
await Task.CompletedTask;
}
public async Task UpdateStatistics(ServerSpeedItem update)
@@ -328,6 +333,7 @@ public class ProfilesViewModel : MyReactiveObject
catch
{
}
await Task.CompletedTask;
}
#endregion Actions
@@ -482,7 +488,7 @@ public class ProfilesViewModel : MyReactiveObject
return lstSelected;
}
public async Task EditServerAsync(EConfigType eConfigType)
public async Task EditServerAsync()
{
if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
{
@@ -494,7 +500,7 @@ public class ProfilesViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return;
}
eConfigType = item.ConfigType;
var eConfigType = item.ConfigType;
bool? ret = false;
if (eConfigType == EConfigType.Custom)
@@ -653,7 +659,7 @@ public class ProfilesViewModel : MyReactiveObject
}
_dicHeaderSort.TryAdd(colName, true);
_dicHeaderSort.TryGetValue(colName, out bool asc);
_dicHeaderSort.TryGetValue(colName, out var asc);
if (await ConfigHandler.SortServers(_config, _config.SubIndexId, colName, asc) != 0)
{
return;
@@ -729,6 +735,12 @@ public class ProfilesViewModel : MyReactiveObject
{
SelectedProfiles = ProfileItems;
}
else if (actionType == ESpeedActionType.FastRealping)
{
SelectedProfiles = ProfileItems;
actionType = ESpeedActionType.Realping;
}
var lstSelected = await GetProfileItems(false);
if (lstSelected == null)
{
@@ -742,6 +754,7 @@ public class ProfilesViewModel : MyReactiveObject
_ = SetSpeedTestResult(result);
return Disposable.Empty;
});
await Task.CompletedTask;
});
_speedtestService?.RunLoop(actionType, lstSelected);
}

View File

@@ -215,7 +215,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
private async Task SaveRoutingAsync()
{
string remarks = SelectedRouting.Remarks;
var remarks = SelectedRouting.Remarks;
if (remarks.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks);
@@ -286,7 +286,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
return;
}
DownloadService downloadHandle = new DownloadService();
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, true, "");
var ret = await AddBatchRoutingRulesAsync(SelectedRouting, result);
if (ret == 0)
@@ -298,7 +298,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
private async Task<int> AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData)
{
bool blReplace = false;
var blReplace = false;
if (await _updateView?.Invoke(EViewAction.AddBatchRoutingRulesYesNo, null) == false)
{
blReplace = true;

View File

@@ -120,7 +120,7 @@ public class StatusBarViewModel : MyReactiveObject
this.WhenAnyValue(
x => x.SelectedServer,
y => y != null && !y.Text.IsNullOrEmpty())
.Subscribe(c => ServerSelectedChanged(c));
.Subscribe(ServerSelectedChanged);
SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType;
this.WhenAnyValue(
@@ -313,10 +313,10 @@ public class StatusBarViewModel : MyReactiveObject
}
BlServers = true;
for (int k = 0; k < lstModel.Count; k++)
for (var k = 0; k < lstModel.Count; k++)
{
ProfileItem it = lstModel[k];
string name = it.GetSummary();
var name = it.GetSummary();
var item = new ComboItem() { ID = it.IndexId, Text = name };
Servers.Add(item);
@@ -367,11 +367,13 @@ public class StatusBarViewModel : MyReactiveObject
_ = TestServerAvailabilityResult(msg);
return Disposable.Empty;
});
await Task.CompletedTask;
}
public async Task TestServerAvailabilityResult(string msg)
{
RunningInfoDisplay = msg;
await Task.CompletedTask;
}
#region System proxy and Routings
@@ -384,7 +386,7 @@ public class StatusBarViewModel : MyReactiveObject
}
_config.SystemProxyItem.SysProxyType = type;
await ChangeSystemProxyAsync(type, true);
NoticeManager.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType.ToString()}");
NoticeManager.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType}");
SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType;
await ConfigHandler.SaveConfig(_config);
@@ -394,10 +396,10 @@ public class StatusBarViewModel : MyReactiveObject
{
await SysProxyHandler.UpdateSysProxy(_config, false);
BlSystemProxyClear = (type == ESysProxyType.ForcedClear);
BlSystemProxySet = (type == ESysProxyType.ForcedChange);
BlSystemProxyNothing = (type == ESysProxyType.Unchanged);
BlSystemProxyPac = (type == ESysProxyType.Pac);
BlSystemProxyClear = type == ESysProxyType.ForcedClear;
BlSystemProxySet = type == ESysProxyType.ForcedChange;
BlSystemProxyNothing = type == ESysProxyType.Unchanged;
BlSystemProxyPac = type == ESysProxyType.Pac;
if (blChange)
{
@@ -502,7 +504,7 @@ public class StatusBarViewModel : MyReactiveObject
{
return AppManager.Instance.LinuxSudoPwd.IsNotEmpty();
}
else if (Utils.IsOSX())
else if (Utils.IsMacOS())
{
return AppManager.Instance.LinuxSudoPwd.IsNotEmpty();
}
@@ -561,6 +563,7 @@ public class StatusBarViewModel : MyReactiveObject
catch
{
}
await Task.CompletedTask;
}
#endregion UI

View File

@@ -10,15 +10,17 @@ public partial class App : Application
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
DataContext = StatusBarViewModel.Instance;
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
AppManager.Instance.InitComponents();
if (!Design.IsDesignMode)
{
AppManager.Instance.InitComponents();
DataContext = StatusBarViewModel.Instance;
}
desktop.Exit += OnExit;
desktop.MainWindow = new MainWindow();

View File

@@ -26,7 +26,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
Height = sizeItem.Height;
var workingArea = (Screens.ScreenFromWindow(this) ?? Screens.Primary).WorkingArea;
var scaling = (Utils.IsOSX() ? null : VisualRoot?.RenderScaling) ?? 1.0;
var scaling = (Utils.IsMacOS() ? null : VisualRoot?.RenderScaling) ?? 1.0;
var x = workingArea.X + ((workingArea.Width - (Width * scaling)) / 2);
var y = workingArea.Y + ((workingArea.Height - (Height * scaling)) / 2);

View File

@@ -28,7 +28,10 @@ internal class AvaUtils
{
var clipboard = TopLevel.GetTopLevel(visual)?.Clipboard;
if (clipboard == null)
{
return;
}
await clipboard.SetTextAsync(strData);
}
catch

View File

@@ -6,7 +6,7 @@ public class DelayColorConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
_ = int.TryParse(value?.ToString(), out var delay);
var delay = value.ToString().ToInt();
return delay switch
{

View File

@@ -3,7 +3,7 @@ global using System.Collections.Generic;
global using System.Globalization;
global using System.IO;
global using System.Linq;
global using System.Reactive.Disposables;
global using System.Reactive.Disposables.Fluent;
global using System.Reactive.Linq;
global using System.Text;
global using System.Threading;
@@ -17,13 +17,13 @@ global using Avalonia.Markup.Xaml;
global using Avalonia.Media;
global using Avalonia.Media.Imaging;
global using Avalonia.Platform;
global using Avalonia.ReactiveUI;
global using Avalonia.Styling;
global using Avalonia.Threading;
global using ReactiveUI;
global using ReactiveUI.Fody.Helpers;
global using DynamicData;
global using MsBox.Avalonia.Enums;
global using ReactiveUI;
global using ReactiveUI.Avalonia;
global using ReactiveUI.Fody.Helpers;
global using ServiceLib;
global using ServiceLib.Base;
global using ServiceLib.Common;

View File

@@ -54,12 +54,19 @@ internal class Program
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
return AppBuilder.Configure<App>()
.UsePlatformDetect()
//.WithInterFont()
.WithFontByDefault()
.LogToTrace()
.UseReactiveUI()
.With(new MacOSPlatformOptions { ShowInDock = AppManager.Instance.Config.UiItem.MacOSShowInDock });
var builder = AppBuilder.Configure<App>()
.UsePlatformDetect()
//.WithInterFont()
.WithFontByDefault()
.LogToTrace()
.UseReactiveUI();
if (OperatingSystem.IsMacOS())
{
var showInDock = Design.IsDesignMode || AppManager.Instance.Config.UiItem.MacOSShowInDock;
builder = builder.With(new MacOSPlatformOptions { ShowInDock = showInDock });
}
return builder;
}
}

View File

@@ -95,7 +95,9 @@ public class ThemeSettingViewModel : MyReactiveObject
{
double size = CurrentFontSize;
if (size < Global.MinFontSize)
{
return;
}
Style style = new(x => Selectors.Or(
x.OfType<Button>(),

View File

@@ -33,63 +33,106 @@
IsCancel="True" />
</StackPanel>
<Grid DockPanel.Dock="Top" RowDefinitions="Auto,*,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid
Grid.Row="0"
ColumnDefinitions="180,Auto,Auto"
DockPanel.Dock="Top"
RowDefinitions="Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
Text="{x:Static resx:ResUI.menuServers}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRemarks}" />
<TextBox
x:Name="txtRemarks"
Grid.Row="1"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCoreType}" />
<ComboBox
x:Name="cmbCoreType"
Grid.Row="2"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
<Grid
Grid.Row="0"
ColumnDefinitions="180,Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
x:Name="gridPolicyGroup"
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="3"
ColumnDefinitions="180,Auto,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
Text="{x:Static resx:ResUI.menuServers}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRemarks}" />
<TextBox
x:Name="txtRemarks"
Grid.Row="1"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCoreType}" />
Text="{x:Static resx:ResUI.TbPolicyGroupType}" />
<ComboBox
x:Name="cmbCoreType"
Grid.Row="2"
x:Name="cmbPolicyGroupType"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
</Grid>
</Grid>
<TabControl DockPanel.Dock="Top">
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList}">
<Grid
x:Name="gridPolicyGroup"
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="3"
ColumnDefinitions="180,Auto,Auto">
Margin="{StaticResource Margin8}"
ColumnDefinitions="180,Auto,Auto"
RowDefinitions="Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPolicyGroupType}" />
Text="{x:Static resx:ResUI.menuSubscription}" />
<ComboBox
x:Name="cmbPolicyGroupType"
x:Name="cmbSubChildItems"
Grid.Row="0"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
Margin="{StaticResource Margin4}"
DisplayMemberBinding="{Binding Remarks}"
ItemsSource="{Binding SubItems}" />
<TextBlock
Grid.Row="0"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPolicyGroupSubChildTip}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvFilter}" />
<TextBox
x:Name="txtFilter"
Grid.Row="1"
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center" />
</Grid>
</Grid>
</Grid>
</TabItem>
</TabControl>
<TabControl>
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList}">
<DataGrid

View File

@@ -13,8 +13,8 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
{
InitializeComponent();
this.Loaded += Window_Loaded;
btnCancel.Click += (s, e) => this.Close();
Loaded += Window_Loaded;
btnCancel.Click += (s, e) => Close();
lstChild.SelectionChanged += LstChild_SelectionChanged;
ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler);
@@ -32,11 +32,11 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
switch (profileItem.ConfigType)
{
case EConfigType.PolicyGroup:
this.Title = ResUI.TbConfigTypePolicyGroup;
Title = ResUI.TbConfigTypePolicyGroup;
break;
case EConfigType.ProxyChain:
this.Title = ResUI.TbConfigTypeProxyChain;
Title = ResUI.TbConfigTypeProxyChain;
gridPolicyGroup.IsVisible = false;
break;
}
@@ -46,6 +46,9 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.SelectedValue).DisposeWith(disposables);
//this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbSubChildItems.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ChildItemsObs, v => v.lstChild.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
@@ -64,7 +67,7 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
menuSelectAllChild.Click += (s, e) => lstChild.SelectAll();
// Keyboard shortcuts when focus is within grid
this.AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel);
AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel);
lstChild.LoadingRow += LstChild_LoadingRow;
}
@@ -78,7 +81,7 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
switch (action)
{
case EViewAction.CloseWindow:
this.Close(true);
Close(true);
break;
}
return await Task.FromResult(true);
@@ -92,7 +95,9 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
private void AddGroupServerWindow_KeyDown(object? sender, KeyEventArgs e)
{
if (!lstChild.IsKeyboardFocusWithin)
{
return;
}
if ((e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Meta)) != 0)
{

View File

@@ -14,8 +14,8 @@ public partial class AddServer2Window : WindowBase<AddServer2ViewModel>
{
InitializeComponent();
this.Loaded += Window_Loaded;
btnCancel.Click += (s, e) => this.Close();
Loaded += Window_Loaded;
btnCancel.Click += (s, e) => Close();
ViewModel = new AddServer2ViewModel(profileItem, UpdateViewHandler);
cmbCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != ECoreType.v2rayN.ToString()).ToList().AppendEmpty();
@@ -39,7 +39,7 @@ public partial class AddServer2Window : WindowBase<AddServer2ViewModel>
switch (action)
{
case EViewAction.CloseWindow:
this.Close(true);
Close(true);
break;
case EViewAction.BrowseServer:

View File

@@ -607,10 +607,10 @@
<Button
x:Name="btnExtra"
Classes="IconButton"
Margin="{StaticResource MarginLr8}">
Margin="{StaticResource MarginLr8}"
Classes="IconButton">
<Button.Content>
<PathIcon Data="{StaticResource SemiIconMore}" >
<PathIcon Data="{StaticResource SemiIconMore}">
<PathIcon.RenderTransform>
<RotateTransform Angle="90" />
</PathIcon.RenderTransform>
@@ -713,7 +713,7 @@
Grid.Row="7"
ColumnDefinitions="180,Auto"
IsVisible="False"
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="1"
@@ -767,6 +767,66 @@
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="5"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCertPinning}" />
<StackPanel
Grid.Row="5"
Grid.Column="1"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock
x:Name="labCertPinning"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center" />
<Button
Margin="{StaticResource MarginLr4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Classes="IconButton">
<Button.Content>
<PathIcon Data="{StaticResource SemiIconMore}">
<PathIcon.RenderTransform>
<RotateTransform Angle="90" />
</PathIcon.RenderTransform>
</PathIcon>
</Button.Content>
<Button.Flyout>
<Flyout>
<StackPanel>
<TextBlock
Width="400"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCertPinningTips}"
TextWrapping="Wrap" />
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Button
x:Name="btnFetchCert"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbFetchCert}" />
<Button
x:Name="btnFetchCertChain"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbFetchCertChain}" />
</StackPanel>
<TextBox
x:Name="txtCert"
Width="400"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
Classes="TextArea"
TextWrapping="NoWrap" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>
</Grid>
<Grid
x:Name="gridRealityMore"

View File

@@ -13,8 +13,8 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
{
InitializeComponent();
this.Loaded += Window_Loaded;
btnCancel.Click += (s, e) => this.Close();
Loaded += Window_Loaded;
btnCancel.Click += (s, e) => Close();
cmbNetwork.SelectionChanged += CmbNetwork_SelectionChanged;
cmbStreamSecurity.SelectionChanged += CmbStreamSecurity_SelectionChanged;
btnGUID.Click += btnGUID_Click;
@@ -185,6 +185,8 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);
//reality
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables);
@@ -193,10 +195,12 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
this.Title = $"{profileItem.ConfigType}";
Title = $"{profileItem.ConfigType}";
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
@@ -204,7 +208,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
switch (action)
{
case EViewAction.CloseWindow:
this.Close(true);
Close(true);
break;
}
return await Task.FromResult(true);

View File

@@ -7,7 +7,7 @@ public partial class ClashProxiesView : ReactiveUserControl<ClashProxiesViewMode
InitializeComponent();
ViewModel = new ClashProxiesViewModel(UpdateViewHandler);
lstProxyDetails.DoubleTapped += LstProxyDetails_DoubleTapped;
this.KeyDown += ClashProxiesView_KeyDown;
KeyDown += ClashProxiesView_KeyDown;
this.WhenActivated(disposables =>
{

View File

@@ -12,7 +12,7 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
_config = AppManager.Instance.Config;
Loaded += Window_Loaded;
btnCancel.Click += (s, e) => this.Close();
btnCancel.Click += (s, e) => Close();
ViewModel = new DNSSettingViewModel(UpdateViewHandler);
cmbRayFreedomDNSStrategy.ItemsSource = Global.DomainStrategy4Freedoms;
@@ -77,7 +77,7 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
switch (action)
{
case EViewAction.CloseWindow:
this.Close(true);
Close(true);
break;
}
return await Task.FromResult(true);

View File

@@ -12,7 +12,7 @@ public partial class FullConfigTemplateWindow : WindowBase<FullConfigTemplateVie
_config = AppManager.Instance.Config;
Loaded += Window_Loaded;
btnCancel.Click += (s, e) => this.Close();
btnCancel.Click += (s, e) => Close();
ViewModel = new FullConfigTemplateViewModel(UpdateViewHandler);
this.WhenActivated(disposables =>
@@ -36,7 +36,7 @@ public partial class FullConfigTemplateWindow : WindowBase<FullConfigTemplateVie
switch (action)
{
case EViewAction.CloseWindow:
this.Close(true);
Close(true);
break;
}
return await Task.FromResult(true);

View File

@@ -17,8 +17,8 @@ public partial class GlobalHotkeySettingWindow : WindowBase<GlobalHotkeySettingV
HotkeyManager.Instance.IsPause = true;
Loaded += Window_Loaded;
this.Closing += (s, e) => HotkeyManager.Instance.IsPause = false;
btnCancel.Click += (s, e) => this.Close();
Closing += (s, e) => HotkeyManager.Instance.IsPause = false;
btnCancel.Click += (s, e) => Close();
this.WhenActivated(disposables =>
{
@@ -34,7 +34,7 @@ public partial class GlobalHotkeySettingWindow : WindowBase<GlobalHotkeySettingV
switch (action)
{
case EViewAction.CloseWindow:
this.Close(true);
Close(true);
break;
}
return await Task.FromResult(true);

Some files were not shown because too many files have changed in this diff Show More